diff --git a/src/core/symbology/qgslinesymbollayer.cpp b/src/core/symbology/qgslinesymbollayer.cpp index 33d058b3f89..a1685fd56d8 100644 --- a/src/core/symbology/qgslinesymbollayer.cpp +++ b/src/core/symbology/qgslinesymbollayer.cpp @@ -32,7 +32,8 @@ #include "qgscolorrampimpl.h" #include "qgsfillsymbol.h" #include "qgscolorutils.h" - +#include "qgsgeos.h" +#include "qgspolygon.h" #include #include #include @@ -3952,44 +3953,67 @@ void QgsFilledLineSymbolLayer::renderPolyline( const QPolygonF &points, QgsSymbo const bool useSelectedColor = shouldRenderUsingSelectionColor( context ); - // stroke out the path using the correct line cap/join style. We'll then use this as the fill polygon - QPainterPathStroker stroker; - stroker.setWidth( scaledWidth ); - stroker.setCapStyle( cap ); - stroker.setJoinStyle( join ); - - QPolygonF polygon; - if ( qgsDoubleNear( offset, 0 ) ) + if ( points.count() >= 2 ) { - QPainterPath path; - path.addPolygon( points ); - const QPainterPath stroke = stroker.createStroke( path ).simplified(); - const QPolygonF polygon = stroke.toFillPolygon(); - if ( !polygon.isEmpty() ) - { - mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor ); - } - } - else - { - double scaledOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale ); - if ( mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits && context.renderContext().flags() & Qgis::RenderContextFlag::RenderSymbolPreview ) - { - // rendering for symbol previews -- a size in meters in map units can't be calculated, so treat the size as millimeters - // and clamp it to a reasonable range. It's the best we can do in this situation! - scaledOffset = std::min( std::max( context.renderContext().convertToPainterUnits( offset, Qgis::RenderUnit::Millimeters ), 3.0 ), 100.0 ); - } + std::unique_ptr< QgsAbstractGeometry > ls = QgsLineString::fromQPolygonF( points ); + geos::unique_ptr lineGeom; - const QList mline = ::offsetLine( points, scaledOffset, context.originalGeometryType() != Qgis::GeometryType::Unknown ? context.originalGeometryType() : Qgis::GeometryType::Line ); - for ( const QPolygonF &part : mline ) + if ( !qgsDoubleNear( offset, 0 ) ) { - QPainterPath path; - path.addPolygon( part ); - const QPainterPath stroke = stroker.createStroke( path ).simplified(); - const QPolygonF polygon = stroke.toFillPolygon(); - if ( !polygon.isEmpty() ) + double scaledOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale ); + if ( mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits && context.renderContext().flags() & Qgis::RenderContextFlag::RenderSymbolPreview ) { - mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor ); + // rendering for symbol previews -- a size in meters in map units can't be calculated, so treat the size as millimeters + // and clamp it to a reasonable range. It's the best we can do in this situation! + scaledOffset = std::min( std::max( context.renderContext().convertToPainterUnits( offset, Qgis::RenderUnit::Millimeters ), 3.0 ), 100.0 ); + } + + const Qgis::GeometryType geometryType = context.originalGeometryType() != Qgis::GeometryType::Unknown ? context.originalGeometryType() : Qgis::GeometryType::Line; + if ( geometryType == Qgis::GeometryType::Polygon ) + { + auto inputPoly = std::make_unique< QgsPolygon >( static_cast< QgsLineString * >( ls.release() ) ); + geos::unique_ptr g( QgsGeos::asGeos( inputPoly.get() ) ); + lineGeom = QgsGeos::buffer( g.get(), -scaledOffset, 0, Qgis::EndCapStyle::Flat, Qgis::JoinStyle::Miter, 2 ); + // the result is a polygon => extract line work + QgsGeometry polygon( QgsGeos::fromGeos( lineGeom.get() ) ); + QVector parts = polygon.coerceToType( Qgis::WkbType::MultiLineString ); + if ( !parts.empty() ) + { + lineGeom = QgsGeos::asGeos( parts.at( 0 ).constGet() ); + } + else + { + lineGeom.reset(); + } + } + else + { + geos::unique_ptr g( QgsGeos::asGeos( ls.get() ) ); + lineGeom = QgsGeos::offsetCurve( g.get(), scaledOffset, 0, Qgis::JoinStyle::Miter, 8.0 ); + } + } + else + { + lineGeom = QgsGeos::asGeos( ls.get() ); + } + + if ( lineGeom ) + { + geos::unique_ptr buffered = QgsGeos::buffer( lineGeom.get(), scaledWidth / 2, 8, + QgsSymbolLayerUtils::penCapStyleToEndCapStyle( cap ), + QgsSymbolLayerUtils::penJoinStyleToJoinStyle( join ), 8 ); + if ( buffered ) + { + // convert to rings + std::unique_ptr< QgsAbstractGeometry > bufferedGeom = QgsGeos::fromGeos( buffered.get() ); + const QList< QList< QPolygonF > > parts = QgsSymbolLayerUtils::toQPolygonF( bufferedGeom.get(), Qgis::SymbolType::Fill ); + for ( const QList< QPolygonF > &polygon : parts ) + { + QVector< QPolygonF > rings; + for ( int i = 1; i < polygon.size(); ++i ) + rings << polygon.at( i ); + mFill->renderPolygon( polygon.value( 0 ), &rings, context.feature(), context.renderContext(), -1, useSelectedColor ); + } } } } diff --git a/tests/src/python/test_qgsfilledlinesymbollayer.py b/tests/src/python/test_qgsfilledlinesymbollayer.py index fd7abf1c9db..0a3e8972828 100644 --- a/tests/src/python/test_qgsfilledlinesymbollayer.py +++ b/tests/src/python/test_qgsfilledlinesymbollayer.py @@ -19,8 +19,6 @@ __author__ = "Nyall Dawson" __date__ = "November 2023" __copyright__ = "(C) 2023, Nyall Dawson" -from typing import Optional - from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtGui import QColor, QImage, QPainter from qgis.core import ( @@ -30,6 +28,7 @@ from qgis.core import ( QgsMapSettings, QgsRenderContext, QgsFilledLineSymbolLayer, + QgsFillSymbol, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -129,6 +128,42 @@ class TestQgsFilledLineSymbolLayer(QgisTestCase): self.image_check("render_offset", "render_offset", rendered_image) ) + def testLineOffsetPolygon(self): + s = QgsFillSymbol() + s.deleteSymbolLayer(0) + + line = QgsFilledLineSymbolLayer() + line.setColor(QColor(255, 0, 0)) + line.setWidth(5) + line.setOffset(5) + + s.appendSymbolLayer(line.clone()) + + g = QgsGeometry.fromWkt("Polygon((2 2, 10 10, 10 0, 2 2))") + rendered_image = self.renderGeometry(s, g) + self.assertTrue( + self.image_check( + "render_offset_polygon", "render_offset_polygon", rendered_image + ) + ) + + def testRenderLooped(self): + s = QgsLineSymbol() + s.deleteSymbolLayer(0) + + line = QgsFilledLineSymbolLayer() + line.setColor(QColor(255, 0, 0)) + line.setWidth(8) + line.setPenCapStyle(Qt.PenCapStyle.FlatCap) + + s.appendSymbolLayer(line.clone()) + + g = QgsGeometry.fromWkt( + "LineString (13.07373737373737299 8.51161616161616053, 0.69292929292929273 5.93585858585858439, 11.32222222222222285 2.15808080808080582, 0 0, 10 10, 10 0)" + ) + rendered_image = self.renderGeometry(s, g) + self.assertTrue(self.image_check("loop", "loop", rendered_image)) + def renderGeometry(self, symbol, geom, buffer=20): f = QgsFeature() f.setGeometry(geom) diff --git a/tests/testdata/control_images/symbol_filledline/expected_loop/expected_loop.png b/tests/testdata/control_images/symbol_filledline/expected_loop/expected_loop.png new file mode 100644 index 00000000000..d52f77fd610 Binary files /dev/null and b/tests/testdata/control_images/symbol_filledline/expected_loop/expected_loop.png differ diff --git a/tests/testdata/control_images/symbol_filledline/expected_render_offset_polygon/expected_render_offset_polygon.png b/tests/testdata/control_images/symbol_filledline/expected_render_offset_polygon/expected_render_offset_polygon.png new file mode 100644 index 00000000000..abad1f2fbb0 Binary files /dev/null and b/tests/testdata/control_images/symbol_filledline/expected_render_offset_polygon/expected_render_offset_polygon.png differ