Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix artifacts when rendering filled line symbol #60554

Merged
merged 4 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/PyQt6/core/auto_additions/qgssymbollayerutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
QgsSymbolLayerUtils.decodeBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeBrushStyle)
QgsSymbolLayerUtils.encodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.encodeSldBrushStyle)
QgsSymbolLayerUtils.decodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeSldBrushStyle)
QgsSymbolLayerUtils.penCapStyleToEndCapStyle = staticmethod(QgsSymbolLayerUtils.penCapStyleToEndCapStyle)
QgsSymbolLayerUtils.penJoinStyleToJoinStyle = staticmethod(QgsSymbolLayerUtils.penJoinStyleToJoinStyle)
QgsSymbolLayerUtils.hasSldSymbolizer = staticmethod(QgsSymbolLayerUtils.hasSldSymbolizer)
QgsSymbolLayerUtils.decodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.decodeCoordinateReference)
QgsSymbolLayerUtils.encodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.encodeCoordinateReference)
Expand Down
3 changes: 3 additions & 0 deletions python/PyQt6/core/auto_generated/geometry/qgsgeos.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ Curved geometries are not supported.

virtual QgsAbstractGeometry *buffer( double distance, int segments, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = 0 ) const;



virtual QgsAbstractGeometry *simplify( double tolerance, QString *errorMsg = 0 ) const;

virtual QgsAbstractGeometry *interpolate( double distance, QString *errorMsg = 0 ) const;
Expand Down Expand Up @@ -287,6 +289,7 @@ This method requires a QGIS build based on GEOS 3.7 or later.
QgsPointSequence &topologyTestPoints,
QString *errorMsg = 0, bool skipIntersectionCheck = false ) const;


virtual QgsAbstractGeometry *offsetCurve( double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = 0 ) const;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ Contains utility functions for working with symbols and symbol layers.
static QString encodeSldBrushStyle( Qt::BrushStyle style );
static Qt::BrushStyle decodeSldBrushStyle( const QString &str );

static Qgis::EndCapStyle penCapStyleToEndCapStyle( Qt::PenCapStyle style );
%Docstring
Converts a Qt pen cap style to a QGIS end cap style.

.. versionadded:: 3.42
%End

static Qgis::JoinStyle penJoinStyleToJoinStyle( Qt::PenJoinStyle style );
%Docstring
Converts a Qt pen joinstyle to a QGIS join style.

.. versionadded:: 3.42
%End

static bool hasSldSymbolizer( const QDomElement &element );
%Docstring
Returns ``True`` if a DOM ``element`` contains an SLD Symbolizer element.
Expand Down Expand Up @@ -814,6 +828,15 @@ how the geometry should be drawn for a symbol of the given ``type``,
as a list of geometry parts and rings.

.. versionadded:: 3.40
%End

static QList< QList< QPolygonF > > toQPolygonF( const QgsAbstractGeometry *geometry, Qgis::SymbolType type );
%Docstring
Converts a ``geometry`` to a set of QPolygonF objects representing
how the geometry should be drawn for a symbol of the given ``type``,
as a list of geometry parts and rings.

.. versionadded:: 3.42
%End

static QPointF polygonCentroid( const QPolygonF &points );
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_additions/qgssymbollayerutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
QgsSymbolLayerUtils.decodeBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeBrushStyle)
QgsSymbolLayerUtils.encodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.encodeSldBrushStyle)
QgsSymbolLayerUtils.decodeSldBrushStyle = staticmethod(QgsSymbolLayerUtils.decodeSldBrushStyle)
QgsSymbolLayerUtils.penCapStyleToEndCapStyle = staticmethod(QgsSymbolLayerUtils.penCapStyleToEndCapStyle)
QgsSymbolLayerUtils.penJoinStyleToJoinStyle = staticmethod(QgsSymbolLayerUtils.penJoinStyleToJoinStyle)
QgsSymbolLayerUtils.hasSldSymbolizer = staticmethod(QgsSymbolLayerUtils.hasSldSymbolizer)
QgsSymbolLayerUtils.decodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.decodeCoordinateReference)
QgsSymbolLayerUtils.encodeCoordinateReference = staticmethod(QgsSymbolLayerUtils.encodeCoordinateReference)
Expand Down
3 changes: 3 additions & 0 deletions python/core/auto_generated/geometry/qgsgeos.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ Curved geometries are not supported.

virtual QgsAbstractGeometry *buffer( double distance, int segments, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = 0 ) const;



virtual QgsAbstractGeometry *simplify( double tolerance, QString *errorMsg = 0 ) const;

virtual QgsAbstractGeometry *interpolate( double distance, QString *errorMsg = 0 ) const;
Expand Down Expand Up @@ -287,6 +289,7 @@ This method requires a QGIS build based on GEOS 3.7 or later.
QgsPointSequence &topologyTestPoints,
QString *errorMsg = 0, bool skipIntersectionCheck = false ) const;


virtual QgsAbstractGeometry *offsetCurve( double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = 0 ) const;


Expand Down
23 changes: 23 additions & 0 deletions python/core/auto_generated/symbology/qgssymbollayerutils.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ Contains utility functions for working with symbols and symbol layers.
static QString encodeSldBrushStyle( Qt::BrushStyle style );
static Qt::BrushStyle decodeSldBrushStyle( const QString &str );

static Qgis::EndCapStyle penCapStyleToEndCapStyle( Qt::PenCapStyle style );
%Docstring
Converts a Qt pen cap style to a QGIS end cap style.

.. versionadded:: 3.42
%End

static Qgis::JoinStyle penJoinStyleToJoinStyle( Qt::PenJoinStyle style );
%Docstring
Converts a Qt pen joinstyle to a QGIS join style.

.. versionadded:: 3.42
%End

static bool hasSldSymbolizer( const QDomElement &element );
%Docstring
Returns ``True`` if a DOM ``element`` contains an SLD Symbolizer element.
Expand Down Expand Up @@ -814,6 +828,15 @@ how the geometry should be drawn for a symbol of the given ``type``,
as a list of geometry parts and rings.

.. versionadded:: 3.40
%End

static QList< QList< QPolygonF > > toQPolygonF( const QgsAbstractGeometry *geometry, Qgis::SymbolType type );
%Docstring
Converts a ``geometry`` to a set of QPolygonF objects representing
how the geometry should be drawn for a symbol of the given ``type``,
as a list of geometry parts and rings.

.. versionadded:: 3.42
%End

static QPointF polygonCentroid( const QPolygonF &points );
Expand Down
30 changes: 22 additions & 8 deletions src/core/geometry/qgsgeos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2094,18 +2094,24 @@ QgsAbstractGeometry *QgsGeos::buffer( double distance, int segments, QString *er

QgsAbstractGeometry *QgsGeos::buffer( double distance, int segments, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg ) const
{
if ( !mGeos )
geos::unique_ptr geos = buffer( mGeos.get(), distance, segments, endCapStyle, joinStyle, miterLimit, errorMsg );
return fromGeos( geos.get() ).release();
}

geos::unique_ptr QgsGeos::buffer( const GEOSGeometry *geometry, double distance, int segments, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg )
{
if ( !geometry )
{
return nullptr;
}

geos::unique_ptr geos;
try
{
geos.reset( GEOSBufferWithStyle_r( QgsGeosContext::get(), mGeos.get(), distance, segments, static_cast< int >( endCapStyle ), static_cast< int >( joinStyle ), miterLimit ) );
geos.reset( GEOSBufferWithStyle_r( QgsGeosContext::get(), geometry, distance, segments, static_cast< int >( endCapStyle ), static_cast< int >( joinStyle ), miterLimit ) );
}
CATCH_GEOS_WITH_ERRMSG( nullptr )
return fromGeos( geos.get() ).release();
return geos;
}

QgsAbstractGeometry *QgsGeos::simplify( double tolerance, QString *errorMsg ) const
Expand Down Expand Up @@ -2743,9 +2749,9 @@ geos::unique_ptr QgsGeos::createGeosPolygon( const QgsAbstractGeometry *poly, do
return geosPolygon;
}

QgsAbstractGeometry *QgsGeos::offsetCurve( double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg ) const
geos::unique_ptr QgsGeos::offsetCurve( const GEOSGeometry *geometry, double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg )
{
if ( !mGeos )
if ( !geometry )
return nullptr;

geos::unique_ptr offset;
Expand All @@ -2755,11 +2761,19 @@ QgsAbstractGeometry *QgsGeos::offsetCurve( double distance, int segments, Qgis::
// https://github.com/qgis/QGIS/issues/53165#issuecomment-1563470832
if ( segments < 8 )
segments = 8;
offset.reset( GEOSOffsetCurve_r( QgsGeosContext::get(), mGeos.get(), distance, segments, static_cast< int >( joinStyle ), miterLimit ) );
offset.reset( GEOSOffsetCurve_r( QgsGeosContext::get(), geometry, distance, segments, static_cast< int >( joinStyle ), miterLimit ) );
}
CATCH_GEOS_WITH_ERRMSG( nullptr )
std::unique_ptr< QgsAbstractGeometry > offsetGeom = fromGeos( offset.get() );
return offsetGeom.release();
return offset;
}

QgsAbstractGeometry *QgsGeos::offsetCurve( double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg ) const
{
geos::unique_ptr res = offsetCurve( mGeos.get(), distance, segments, joinStyle, miterLimit, errorMsg );
if ( !res )
return nullptr;

return fromGeos( res.get() ).release();
}

std::unique_ptr<QgsAbstractGeometry> QgsGeos::singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg ) const
Expand Down
17 changes: 17 additions & 0 deletions src/core/geometry/qgsgeos.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
QgsAbstractGeometry *symDifference( const QgsAbstractGeometry *geom, QString *errorMsg = nullptr, const QgsGeometryParameters &parameters = QgsGeometryParameters() ) const override;
QgsAbstractGeometry *buffer( double distance, int segments, QString *errorMsg = nullptr ) const override;
QgsAbstractGeometry *buffer( double distance, int segments, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = nullptr ) const override;

/**
* Directly calculates the buffer for a GEOS geometry object and returns a GEOS geometry result.
*
* \note Not available in Python bindings
* \since QGIS 3.42
*/
SIP_SKIP static geos::unique_ptr buffer( const GEOSGeometry *geometry, double distance, int segments, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = nullptr );

QgsAbstractGeometry *simplify( double tolerance, QString *errorMsg = nullptr ) const override;
QgsAbstractGeometry *interpolate( double distance, QString *errorMsg = nullptr ) const override;
QgsAbstractGeometry *envelope( QString *errorMsg = nullptr ) const override;
Expand Down Expand Up @@ -380,6 +389,14 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
QgsPointSequence &topologyTestPoints,
QString *errorMsg = nullptr, bool skipIntersectionCheck = false ) const override;

/**
* Directly calculates the offset curve for a GEOS geometry object and returns a GEOS geometry result.
*
* \note Not available in Python bindings
* \since QGIS 3.42
*/
SIP_SKIP static geos::unique_ptr offsetCurve( const GEOSGeometry *geometry, double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = nullptr );

QgsAbstractGeometry *offsetCurve( double distance, int segments, Qgis::JoinStyle joinStyle, double miterLimit, QString *errorMsg = nullptr ) const override;

/**
Expand Down
41 changes: 3 additions & 38 deletions src/core/painting/qgsgeometrypaintdevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "qgsmultipolygon.h"
#include "qgsmultilinestring.h"
#include "qgspainting.h"
#include "qgssymbollayerutils.h"

//
// QgsGeometryPaintEngine
Expand Down Expand Up @@ -395,42 +396,6 @@ void QgsGeometryPaintEngine::addStrokedLine( const QgsLineString *line, double p
}
}

Qgis::EndCapStyle QgsGeometryPaintEngine::penStyleToCapStyle( Qt::PenCapStyle style )
{
switch ( style )
{
case Qt::FlatCap:
return Qgis::EndCapStyle::Flat;
case Qt::SquareCap:
return Qgis::EndCapStyle::Square;
case Qt::RoundCap:
return Qgis::EndCapStyle::Round;
case Qt::MPenCapStyle:
// undocumented?
break;
}

return Qgis::EndCapStyle::Round;
}

Qgis::JoinStyle QgsGeometryPaintEngine::penStyleToJoinStyle( Qt::PenJoinStyle style )
{
switch ( style )
{
case Qt::MiterJoin:
case Qt::SvgMiterJoin:
return Qgis::JoinStyle::Miter;
case Qt::BevelJoin:
return Qgis::JoinStyle::Bevel;
case Qt::RoundJoin:
return Qgis::JoinStyle::Round;
case Qt::MPenJoinStyle:
// undocumented?
break;
}
return Qgis::JoinStyle::Round;
}

// based on QPainterPath::toSubpathPolygons()
void QgsGeometryPaintEngine::addSubpathGeometries( const QPainterPath &path, const QTransform &matrix )
{
Expand All @@ -439,8 +404,8 @@ void QgsGeometryPaintEngine::addSubpathGeometries( const QPainterPath &path, con

const bool transformIsIdentity = matrix.isIdentity();

const Qgis::EndCapStyle endCapStyle = penStyleToCapStyle( mPen.capStyle() );
const Qgis::JoinStyle joinStyle = penStyleToJoinStyle( mPen.joinStyle() );
const Qgis::EndCapStyle endCapStyle = QgsSymbolLayerUtils::penCapStyleToEndCapStyle( mPen.capStyle() );
const Qgis::JoinStyle joinStyle = QgsSymbolLayerUtils::penJoinStyleToJoinStyle( mPen.joinStyle() );
const double penWidth = mPen.widthF() <= 0 ? 1 : mPen.widthF();
const double miterLimit = mPen.miterLimit();

Expand Down
2 changes: 0 additions & 2 deletions src/core/painting/qgsgeometrypaintdevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ class QgsGeometryPaintEngine: public QPaintEngine

void addSubpathGeometries( const QPainterPath &path, const QTransform &matrix );
void addStrokedLine( const QgsLineString *line, double penWidth, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, const QTransform *matrix );
static Qgis::EndCapStyle penStyleToCapStyle( Qt::PenCapStyle style );
static Qgis::JoinStyle penStyleToJoinStyle( Qt::PenJoinStyle style );

bool mUsePathStroker = false;
QPen mPen;
Expand Down
86 changes: 55 additions & 31 deletions src/core/symbology/qgslinesymbollayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
#include "qgscolorrampimpl.h"
#include "qgsfillsymbol.h"
#include "qgscolorutils.h"

#include "qgsgeos.h"
#include "qgspolygon.h"
#include <algorithm>
#include <QPainter>
#include <QDomDocument>
Expand Down Expand Up @@ -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() )
std::unique_ptr< QgsAbstractGeometry > ls = QgsLineString::fromQPolygonF( points );
geos::unique_ptr lineGeom;

if ( !qgsDoubleNear( offset, 0 ) )
{
mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor );
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 );
}

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<QgsGeometry> 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
{
double scaledOffset = context.renderContext().convertToPainterUnits( offset, mOffsetUnit, mOffsetMapUnitScale );
if ( mOffsetUnit == Qgis::RenderUnit::MetersInMapUnits && context.renderContext().flags() & Qgis::RenderContextFlag::RenderSymbolPreview )
else
{
// 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 );
lineGeom = QgsGeos::asGeos( ls.get() );
}

const QList<QPolygonF> mline = ::offsetLine( points, scaledOffset, context.originalGeometryType() != Qgis::GeometryType::Unknown ? context.originalGeometryType() : Qgis::GeometryType::Line );
for ( const QPolygonF &part : mline )
if ( lineGeom )
{
QPainterPath path;
path.addPolygon( part );
const QPainterPath stroke = stroker.createStroke( path ).simplified();
const QPolygonF polygon = stroke.toFillPolygon();
if ( !polygon.isEmpty() )
geos::unique_ptr buffered = QgsGeos::buffer( lineGeom.get(), scaledWidth / 2, 8,
QgsSymbolLayerUtils::penCapStyleToEndCapStyle( cap ),
QgsSymbolLayerUtils::penJoinStyleToJoinStyle( join ), 8 );
if ( buffered )
{
mFill->renderPolygon( polygon, /* rings */ nullptr, context.feature(), context.renderContext(), -1, useSelectedColor );
// 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 );
}
}
}
}
Expand Down
Loading
Loading