Skip to content
Permalink
Browse files
[feature][symbology] Add option to control marker clipping for
point pattern fill

A new option exposes the ability to clip markers in the fill by:

- "Clip to shape": default behaviour, same as previous versions. Markers
are clipped so that only the portions inside the polygon are visible
- "Marker centroid within shape": only markers where the center of
the marker falls inside the polygon are drawn, but these markers
won't be clipped to the outside of the polygon
- "Marker Completely Within Shape": only markers which fall
completely within the polygon are shown
- "No clipping": any marker which intersects at all with the polygon
will be completely rendered

The clipping mode can be overridden via a data driven expression if
desired.

Sponsored by North Road, thanks to SLYR

Fixes #37825
  • Loading branch information
nyalldawson committed Oct 23, 2021
1 parent 4533577 commit b56e86181cc27232bb9978ffdda03a9f10aec2b3
@@ -1087,3 +1087,11 @@
Qgis.PointCountMethod.__doc__ = 'Methods which define the number of points randomly filling a polygon.\n\n.. note::\n\n Prior to QGIS 3.24 this was available as :py:class:`QgsRandomMarkerFillSymbolLayer`.CountMethod\n\n.. versionadded:: 3.24\n\n' + '* ``AbsoluteCount``: ' + Qgis.PointCountMethod.Absolute.__doc__ + '\n' + '* ``DensityBasedCount``: ' + Qgis.PointCountMethod.DensityBased.__doc__
# --
Qgis.PointCountMethod.baseClass = Qgis
# monkey patching scoped based enum
Qgis.MarkerClipMode.NoClipping.__doc__ = "No clipping, render complete markers"
Qgis.MarkerClipMode.Shape.__doc__ = "Clip to polygon shape"
Qgis.MarkerClipMode.CentroidWithin.__doc__ = "Render complete markers wherever their centroid falls within the polygon shape"
Qgis.MarkerClipMode.CompletelyWithin.__doc__ = "Render complete markers wherever the completely fall within the polygon shape"
Qgis.MarkerClipMode.__doc__ = 'Marker clipping modes.\n\n.. versionadded:: 3.24\n\n' + '* ``NoClipping``: ' + Qgis.MarkerClipMode.NoClipping.__doc__ + '\n' + '* ``Shape``: ' + Qgis.MarkerClipMode.Shape.__doc__ + '\n' + '* ``CentroidWithin``: ' + Qgis.MarkerClipMode.CentroidWithin.__doc__ + '\n' + '* ``CompletelyWithin``: ' + Qgis.MarkerClipMode.CompletelyWithin.__doc__
# --
Qgis.MarkerClipMode.baseClass = Qgis
@@ -691,6 +691,14 @@ The development version
DensityBased,
};

enum class MarkerClipMode
{
NoClipping,
Shape,
CentroidWithin,
CompletelyWithin,
};

static const double DEFAULT_SEARCH_RADIUS_MM;

static const float DEFAULT_MAPTOPIXEL_THRESHOLD;
@@ -2207,6 +2207,24 @@ Returns the unit scale for the vertical offset between rows in the pattern.
.. seealso:: :py:func:`offsetXMapUnitScale`

.. versionadded:: 3.8
%End

Qgis::MarkerClipMode clipMode() const;
%Docstring
Returns the marker clipping mode, which defines how markers are clipped at the edges of shapes.

.. seealso:: :py:func:`setClipMode`

.. versionadded:: 3.24
%End

void setClipMode( Qgis::MarkerClipMode mode );
%Docstring
Sets the marker clipping ``mode``, which defines how markers are clipped at the edges of shapes.

.. seealso:: :py:func:`clipMode`

.. versionadded:: 3.24
%End

protected:
@@ -156,6 +156,7 @@ class QgsSymbolLayer
PropertyLineEndWidthValue,
PropertyLineStartColorValue,
PropertyLineEndColorValue,
PropertyMarkerClipping,
};

static const QgsPropertiesDefinition &propertyDefinitions();
@@ -68,6 +68,29 @@ Decodes a ``value`` representing an arrow head type.
Decodes a ``value`` representing an arrow type.

.. versionadded:: 3.2
%End

static Qgis::MarkerClipMode decodeMarkerClipMode( const QString &string, bool *ok /Out/ = 0 );
%Docstring
Decodes a ``string`` representing a marker clip mode.

:param string: string to decode

:return: - decoded marker clip mode
- ok: will be set to ``True`` if ``string`` was successfully decoded

.. seealso:: :py:func:`encodeMarkerClipMode`

.. versionadded:: 3.24
%End

static QString encodeMarkerClipMode( Qgis::MarkerClipMode mode );
%Docstring
Encodes a marker clip ``mode`` to a string.

.. seealso:: :py:func:`decodeMarkerClipMode`

.. versionadded:: 3.24
%End

static QString encodePoint( QPointF point );
@@ -1125,6 +1125,20 @@ class CORE_EXPORT Qgis
};
Q_ENUM( PointCountMethod )

/**
* Marker clipping modes.
*
* \since QGIS 3.24
*/
enum class MarkerClipMode : int
{
NoClipping, //!< No clipping, render complete markers
Shape, //!< Clip to polygon shape
CentroidWithin, //!< Render complete markers wherever their centroid falls within the polygon shape
CompletelyWithin, //!< Render complete markers wherever the completely fall within the polygon shape
};
Q_ENUM( MarkerClipMode )

/**
* Identify search radius in mm
* \since QGIS 2.3
@@ -38,6 +38,7 @@
#include "qgsmarkersymbol.h"
#include "qgslinesymbol.h"
#include "qgsfeedback.h"
#include "qgsgeometryengine.h"

#include <QPainter>
#include <QFile>
@@ -3186,20 +3187,11 @@ QgsSymbolLayer *QgsLinePatternFillSymbolLayer::createFromSld( QDomElement &eleme
QgsPointPatternFillSymbolLayer::QgsPointPatternFillSymbolLayer()
: QgsImageFillSymbolLayer()
{
mDistanceX = 15;
mDistanceY = 15;
mDisplacementX = 0;
mDisplacementY = 0;
mOffsetX = 0;
mOffsetY = 0;
setSubSymbol( new QgsMarkerSymbol() );
QgsImageFillSymbolLayer::setSubSymbol( nullptr ); //no stroke
}

QgsPointPatternFillSymbolLayer::~QgsPointPatternFillSymbolLayer()
{
delete mMarkerSymbol;
}
QgsPointPatternFillSymbolLayer::~QgsPointPatternFillSymbolLayer() = default;

void QgsPointPatternFillSymbolLayer::setOutputUnit( QgsUnitTypes::RenderUnit unit )
{
@@ -3214,7 +3206,6 @@ void QgsPointPatternFillSymbolLayer::setOutputUnit( QgsUnitTypes::RenderUnit uni
{
mMarkerSymbol->setOutputUnit( unit );
}

}

QgsUnitTypes::RenderUnit QgsPointPatternFillSymbolLayer::outputUnit() const
@@ -3347,6 +3338,10 @@ QgsSymbolLayer *QgsPointPatternFillSymbolLayer::create( const QVariantMap &prope
{
layer->setStrokeWidthMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( properties[QStringLiteral( "outline_width_map_unit_scale" )].toString() ) );
}
if ( properties.contains( QStringLiteral( "clip_mode" ) ) )
{
layer->setClipMode( QgsSymbolLayerUtils::decodeMarkerClipMode( properties.value( QStringLiteral( "clip_mode" ) ).toString() ) );
}

layer->restoreOldDataDefinedProperties( properties );

@@ -3454,7 +3449,10 @@ void QgsPointPatternFillSymbolLayer::startRender( QgsSymbolRenderContext &contex
{
// if we are using a vector based output, we need to render points as vectors
// (OR if the marker has data defined symbology, in which case we need to evaluate this point-by-point)
mRenderUsingMarkers = context.renderContext().forceVectorOutput() || mMarkerSymbol->hasDataDefinedProperties();
mRenderUsingMarkers = context.renderContext().forceVectorOutput()
|| mMarkerSymbol->hasDataDefinedProperties()
|| mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyMarkerClipping )
|| mClipMode != Qgis::MarkerClipMode::Shape;

if ( mRenderUsingMarkers )
{
@@ -3564,16 +3562,57 @@ void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, con

p->save();

QPainterPath path;
path.addPolygon( points );
if ( rings )
Qgis::MarkerClipMode clipMode = mClipMode;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyMarkerClipping ) )
{
for ( const QPolygonF &ring : *rings )
context.setOriginalValueVariable( QgsSymbolLayerUtils::encodeMarkerClipMode( clipMode ) );
bool ok = false;
const QString valueString = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyMarkerClipping, context.renderContext().expressionContext(), QString(), &ok );
if ( ok )
{
path.addPolygon( ring );
Qgis::MarkerClipMode decodedMode = QgsSymbolLayerUtils::decodeMarkerClipMode( valueString, &ok );
if ( ok )
clipMode = decodedMode;
}
}

std::unique_ptr< QgsPolygon > shapePolygon;
std::unique_ptr< QgsGeometryEngine > shapeEngine;
switch ( clipMode )
{
case Qgis::MarkerClipMode::NoClipping:
case Qgis::MarkerClipMode::CentroidWithin:
case Qgis::MarkerClipMode::CompletelyWithin:
{
shapePolygon = std::make_unique< QgsPolygon >();
shapePolygon->setExteriorRing( QgsLineString::fromQPolygonF( points ) );
if ( rings )
{
for ( const QPolygonF &ring : *rings )
{
shapePolygon->addInteriorRing( QgsLineString::fromQPolygonF( ring ) );
}
}
shapeEngine.reset( QgsGeometry::createGeometryEngine( shapePolygon.get() ) );
shapeEngine->prepareGeometry();
break;
}

case Qgis::MarkerClipMode::Shape:
{
QPainterPath path;
path.addPolygon( points );
if ( rings )
{
for ( const QPolygonF &ring : *rings )
{
path.addPolygon( ring );
}
}
p->setClipPath( path, Qt::IntersectClip );
break;
}
}
p->setClipPath( path, Qt::IntersectClip );

const double left = points.boundingRect().left() - 2 * width;
const double top = points.boundingRect().top() - 2 * height;
@@ -3611,6 +3650,40 @@ void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, con
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_marker_row" ), ++currentRow, true ) );
}

if ( shapeEngine )
{
bool renderPoint = true;
switch ( clipMode )
{
case Qgis::MarkerClipMode::CentroidWithin:
{
// we test using the marker bounds here and NOT just the x,y point, as the marker symbol may have offsets or other data defined properties which affect its visual placement
const QgsRectangle markerRect = QgsRectangle( mMarkerSymbol->bounds( QPointF( x, y ), context.renderContext(), context.feature() ? *context.feature() : QgsFeature() ) );
QgsPoint p( markerRect.center() );
renderPoint = shapeEngine->intersects( &p );
break;
}

case Qgis::MarkerClipMode::NoClipping:
case Qgis::MarkerClipMode::CompletelyWithin:
{
const QgsGeometry markerBounds = QgsGeometry::fromRect( QgsRectangle( mMarkerSymbol->bounds( QPointF( x, y ), context.renderContext(), context.feature() ? *context.feature() : QgsFeature() ) ) );

if ( clipMode == Qgis::MarkerClipMode::CompletelyWithin )
renderPoint = shapeEngine->contains( markerBounds.constGet() );
else
renderPoint = shapeEngine->intersects( markerBounds.constGet() );
break;
}

case Qgis::MarkerClipMode::Shape:
break;
}

if ( !renderPoint )
continue;
}

mMarkerSymbol->renderPoint( QPointF( x, y ), context.feature(), context.renderContext() );
}
}
@@ -3653,6 +3726,7 @@ QVariantMap QgsPointPatternFillSymbolLayer::properties() const
map.insert( QStringLiteral( "offset_y_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetYMapUnitScale ) );
map.insert( QStringLiteral( "outline_width_unit" ), QgsUnitTypes::encodeUnit( mStrokeWidthUnit ) );
map.insert( QStringLiteral( "outline_width_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mStrokeWidthMapUnitScale ) );
map.insert( QStringLiteral( "clip_mode" ), QgsSymbolLayerUtils::encodeMarkerClipMode( mClipMode ) );
return map;
}

@@ -3663,6 +3737,7 @@ QgsPointPatternFillSymbolLayer *QgsPointPatternFillSymbolLayer::clone() const
{
clonedLayer->setSubSymbol( mMarkerSymbol->clone() );
}
clonedLayer->setClipMode( mClipMode );
copyDataDefinedProperties( clonedLayer );
copyPaintEffect( clonedLayer );
return clonedLayer;
@@ -3727,15 +3802,14 @@ bool QgsPointPatternFillSymbolLayer::setSubSymbol( QgsSymbol *symbol )
if ( symbol->type() == Qgis::SymbolType::Marker )
{
QgsMarkerSymbol *markerSymbol = static_cast<QgsMarkerSymbol *>( symbol );
delete mMarkerSymbol;
mMarkerSymbol = markerSymbol;
mMarkerSymbol.reset( markerSymbol );
}
return true;
}

QgsSymbol *QgsPointPatternFillSymbolLayer::subSymbol()
{
return mMarkerSymbol;
return mMarkerSymbol.get();
}

void QgsPointPatternFillSymbolLayer::applyDataDefinedSettings( QgsSymbolRenderContext &context )
@@ -3804,7 +3878,7 @@ QSet<QString> QgsPointPatternFillSymbolLayer::usedAttributes( const QgsRenderCon

bool QgsPointPatternFillSymbolLayer::hasDataDefinedProperties() const
{
if ( QgsSymbolLayer::hasDataDefinedProperties() )
if ( QgsImageFillSymbolLayer::hasDataDefinedProperties() )
return true;
if ( mMarkerSymbol && mMarkerSymbol->hasDataDefinedProperties() )
return true;
@@ -1965,8 +1965,24 @@ class CORE_EXPORT QgsPointPatternFillSymbolLayer: public QgsImageFillSymbolLayer
*/
const QgsMapUnitScale &offsetYMapUnitScale() const { return mOffsetYMapUnitScale; }

/**
* Returns the marker clipping mode, which defines how markers are clipped at the edges of shapes.
*
* \see setClipMode()
* \since QGIS 3.24
*/
Qgis::MarkerClipMode clipMode() const { return mClipMode; }

/**
* Sets the marker clipping \a mode, which defines how markers are clipped at the edges of shapes.
*
* \see clipMode()
* \since QGIS 3.24
*/
void setClipMode( Qgis::MarkerClipMode mode ) { mClipMode = mode; }

protected:
QgsMarkerSymbol *mMarkerSymbol = nullptr;
std::unique_ptr< QgsMarkerSymbol > mMarkerSymbol;
double mDistanceX = 15;
QgsUnitTypes::RenderUnit mDistanceXUnit = QgsUnitTypes::RenderMillimeters;
QgsMapUnitScale mDistanceXMapUnitScale;
@@ -1996,6 +2012,8 @@ class CORE_EXPORT QgsPointPatternFillSymbolLayer: public QgsImageFillSymbolLayer
void applyPattern( const QgsSymbolRenderContext &context, QBrush &brush, double distanceX, double distanceY,
double displacementX, double displacementY, double offsetX, double offsetY );

Qgis::MarkerClipMode mClipMode = Qgis::MarkerClipMode::Shape;

bool mRenderUsingMarkers = false;
};

@@ -116,6 +116,7 @@ void QgsSymbolLayer::initPropertyDefinitions()
{ QgsSymbolLayer::PropertyLineEndWidthValue, QgsPropertyDefinition( "lineEndWidthValue", QObject::tr( "Line end width value" ), QgsPropertyDefinition::Double, origin )},
{ QgsSymbolLayer::PropertyLineStartColorValue, QgsPropertyDefinition( "lineStartColorValue", QObject::tr( "Line start color value" ), QgsPropertyDefinition::Double, origin )},
{ QgsSymbolLayer::PropertyLineEndColorValue, QgsPropertyDefinition( "lineEndColorValue", QObject::tr( "Line end color value" ), QgsPropertyDefinition::Double, origin )},
{ QgsSymbolLayer::PropertyMarkerClipping, QgsPropertyDefinition( "markerClipping", QgsPropertyDefinition::DataTypeString, QObject::tr( "Marker clipping mode" ), QObject::tr( "string " ) + QLatin1String( "[<b>no</b>|<b>shape</b>|<b>centroid_within</b>|<b>completely_within</b>]" ), origin )},
};
}

@@ -200,6 +200,7 @@ class CORE_EXPORT QgsSymbolLayer
PropertyLineEndWidthValue, //!< End line width for interpolated line renderer (since QGIS 3.22)
PropertyLineStartColorValue, //!< Start line color for interpolated line renderer (since QGIS 3.22)
PropertyLineEndColorValue, //!< End line color for interpolated line renderer (since QGIS 3.22)
PropertyMarkerClipping, //!< Marker clipping mode (since QGIS 3.24)
};

/**

0 comments on commit b56e861

Please sign in to comment.