diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index 123bded10afc..4bfddd3ab202 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -1466,7 +1466,7 @@ Returns an extruded version of this geometry. %End - SIP_PYOBJECT randomPointsInPolygon( int count, unsigned long seed = 0 ) /TypeHint="QgsPolylineXY"/; + SIP_PYOBJECT randomPointsInPolygon( int count, unsigned long seed = 0 ) const /TypeHint="QgsPolylineXY"/; %Docstring Returns a list of ``count`` random points generated inside a (multi)polygon geometry. diff --git a/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in b/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in index 25e06e295b53..d4aea697ebc3 100644 --- a/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in @@ -28,6 +28,11 @@ class QgsSimpleFillSymbolLayer : QgsFillSymbolLayer static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; +%Docstring +Creates a new QgsSimpleFillSymbolLayer using the specified ``properties`` map containing symbol properties (see properties()). + +Caller takes ownership of the returned symbol layer. +%End static QgsSymbolLayer *createFromSld( QDomElement &element ) /Factory/; @@ -193,6 +198,11 @@ class QgsGradientFillSymbolLayer : QgsFillSymbolLayer static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; +%Docstring +Creates a new QgsGradientFillSymbolLayer using the specified ``properties`` map containing symbol properties (see properties()). + +Caller takes ownership of the returned symbol layer. +%End virtual QString layerType() const; @@ -348,6 +358,11 @@ class QgsShapeburstFillSymbolLayer : QgsFillSymbolLayer static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; +%Docstring +Creates a new QgsShapeburstFillSymbolLayer using the specified ``properties`` map containing symbol properties (see properties()). + +Caller takes ownership of the returned symbol layer. +%End virtual QString layerType() const; @@ -1588,6 +1603,11 @@ class QgsPointPatternFillSymbolLayer: QgsImageFillSymbolLayer ~QgsPointPatternFillSymbolLayer(); static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; +%Docstring +Creates a new QgsPointPatternFillSymbolLayer using the specified ``properties`` map containing symbol properties (see properties()). + +Caller takes ownership of the returned symbol layer. +%End static QgsSymbolLayer *createFromSld( QDomElement &element ) /Factory/; virtual QString layerType() const; @@ -1891,6 +1911,128 @@ Returns the unit scale for the vertical offset between rows in the pattern. QgsPointPatternFillSymbolLayer( const QgsPointPatternFillSymbolLayer &other ); }; +class QgsRandomMarkerFillSymbolLayer : QgsFillSymbolLayer +{ +%Docstring + +A fill symbol layer which places markers at random locations within polygons. + +.. versionadded:: 3.12 +%End + +%TypeHeaderCode +#include "qgsfillsymbollayer.h" +%End + public: + + QgsRandomMarkerFillSymbolLayer( int pointCount = 10, unsigned long seed = 0 ); +%Docstring +Constructor for QgsRandomMarkerFillSymbolLayer, with the specified ``pointCount``. + +Optionally a specific random number ``seed`` can be used when generating points. A ``seed`` of 0 indicates that +a truly random sequence will be used on every rendering, causing points to appear in different locations with every map refresh. +%End + + static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; +%Docstring +Creates a new QgsRandomMarkerFillSymbolLayer using the specified ``properties`` map containing symbol properties (see properties()). + +Caller takes ownership of the returned symbol layer. +%End + + virtual QString layerType() const; + + virtual void startRender( QgsSymbolRenderContext &context ); + + virtual void stopRender( QgsSymbolRenderContext &context ); + + virtual void renderPolygon( const QPolygonF &points, QList *rings, QgsSymbolRenderContext &context ); + + virtual QgsStringMap properties() const; + + virtual QgsRandomMarkerFillSymbolLayer *clone() const /Factory/; + + + virtual void setColor( const QColor &color ); + + virtual QColor color() const; + + + virtual QgsSymbol *subSymbol(); + + virtual bool setSubSymbol( QgsSymbol *symbol /Transfer/ ); + + + virtual void setOutputUnit( QgsUnitTypes::RenderUnit unit ); + + virtual QgsUnitTypes::RenderUnit outputUnit() const; + + + virtual void setMapUnitScale( const QgsMapUnitScale &scale ); + + virtual QgsMapUnitScale mapUnitScale() const; + + + virtual QSet usedAttributes( const QgsRenderContext &context ) const; + + virtual bool hasDataDefinedProperties() const; + + + int pointCount() const; +%Docstring +Returns the count of random points to render in the fill. + +.. seealso:: :py:func:`setPointCount` +%End + + void setPointCount( int count ); +%Docstring +Sets the ``count`` of random points to render in the fill. + +.. seealso:: :py:func:`pointCount` +%End + + unsigned long seed() const; +%Docstring +Returns the random number seed to use when generating points, or 0 if +a truly random sequence will be used (causing points to appear in different locations with every map refresh). + +.. seealso:: :py:func:`setSeed` +%End + + void setSeed( unsigned long seed ); +%Docstring +Sets the random number ``seed`` to use when generating points, or 0 if +a truly random sequence will be used on every rendering (causing points to appear +in different locations with every map refresh). + +.. seealso:: :py:func:`seed` +%End + + bool clipPoints() const; +%Docstring +Returns ``True`` if point markers should be clipped to the polygon boundary. + +.. seealso:: :py:func:`setClipPoints` +%End + + void setClipPoints( bool clipped ); +%Docstring +Sets whether point markers should be ``clipped`` to the polygon boundary. + +.. seealso:: :py:func:`clipPoints` +%End + + virtual void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context ); + + virtual void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context ); + + + private: + QgsRandomMarkerFillSymbolLayer( const QgsRandomMarkerFillSymbolLayer &other ); +}; + + class QgsCentroidFillSymbolLayer : QgsFillSymbolLayer { @@ -1902,6 +2044,11 @@ class QgsCentroidFillSymbolLayer : QgsFillSymbolLayer static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; +%Docstring +Creates a new QgsCentroidFillSymbolLayer using the specified ``properties`` map containing symbol properties (see properties()). + +Caller takes ownership of the returned symbol layer. +%End static QgsSymbolLayer *createFromSld( QDomElement &element ) /Factory/; diff --git a/python/core/auto_generated/symbology/qgssymbollayer.sip.in b/python/core/auto_generated/symbology/qgssymbollayer.sip.in index b67d39471275..40fb45ad1c72 100644 --- a/python/core/auto_generated/symbology/qgssymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayer.sip.in @@ -69,6 +69,8 @@ class QgsSymbolLayer sipType = sipType_QgsGradientFillSymbolLayer; else if ( sipCpp->layerType() == "ShapeburstFill" ) sipType = sipType_QgsShapeburstFillSymbolLayer; + else if ( sipCpp->layerType() == "RandomMarkerFill" ) + sipType = sipType_QgsRandomMarkerFillSymbolLayer; else sipType = sipType_QgsFillSymbolLayer; break; @@ -135,6 +137,9 @@ class QgsSymbolLayer PropertyArrowType, PropertyOffsetX, PropertyOffsetY, + PropertyPointCount, + PropertyRandomSeed, + PropertyClipPoints, }; static const QgsPropertiesDefinition &propertyDefinitions(); @@ -213,7 +218,81 @@ Should match with the string used to register this symbol layer in the registry. %End virtual void startRender( QgsSymbolRenderContext &context ) = 0; +%Docstring +Called before a set of rendering operations commences on the supplied render ``context``. + +This is always followed by a call to stopRender() after all rendering operations +have been completed. + +Subclasses can use this method to prepare for a set of rendering operations, e.g. by +pre-evaluating paths or images to render, and performing other one-time optimisations. + +.. seealso:: :py:func:`startFeatureRender` + +.. seealso:: :py:func:`stopRender` +%End + virtual void stopRender( QgsSymbolRenderContext &context ) = 0; +%Docstring +Called after a set of rendering operations has finished on the supplied render ``context``. + +This is always preceded by a call to startRender() before all rendering operations +are commenced. + +Subclasses can use this method to cleanup after a set of rendering operations. + +.. seealso:: :py:func:`startRender` + +.. seealso:: :py:func:`stopFeatureRender` +%End + + virtual void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context ); +%Docstring +Called before the layer will be rendered for a particular ``feature``. + +This is always followed by a call to stopFeatureRender() after the feature +has been completely rendered (i.e. all parts have been rendered). + +The default implementation does nothing. + +.. note:: + + In some circumstances, startFeatureRender() and stopFeatureRender() may not be called + before a symbol layer is rendered. E.g., when a symbol layer is being rendered in isolation + and not as a result of rendering a feature (for instance, when rendering a legend patch or other + non-feature based shape). + +.. seealso:: :py:func:`stopFeatureRender` + +.. seealso:: :py:func:`startRender` + + +.. versionadded:: 3.12 +%End + + virtual void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context ); +%Docstring +Called after the layer has been rendered for a particular ``feature``. + +This is always preceded by a call to startFeatureRender() just before the feature +will be rendered. + +The default implementation does nothing. + +.. note:: + + In some circumstances, startFeatureRender() and stopFeatureRender() may not be called + before a symbol layer is rendered. E.g., when a symbol layer is being rendered in isolation + and not as a result of rendering a feature (for instance, when rendering a legend patch or other + non-feature based shape). + +.. seealso:: :py:func:`startFeatureRender` + +.. seealso:: :py:func:`stopRender` + + +.. versionadded:: 3.12 +%End virtual QgsSymbolLayer *clone() const = 0 /Factory/; %Docstring diff --git a/python/gui/auto_generated/symbology/qgssymbollayerwidget.sip.in b/python/gui/auto_generated/symbology/qgssymbollayerwidget.sip.in index 9ecc54042822..fd2e83bfef25 100644 --- a/python/gui/auto_generated/symbology/qgssymbollayerwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgssymbollayerwidget.sip.in @@ -655,6 +655,46 @@ Creates a new QgsPointPatternFillSymbolLayerWidget. + +class QgsRandomMarkerFillSymbolLayerWidget: QgsSymbolLayerWidget +{ +%Docstring + +Widget for controlling the properties of a :py:class:`QgsRandomMarkerFillSymbolLayer`. + +.. versionadded:: 3.12 +%End + +%TypeHeaderCode +#include "qgssymbollayerwidget.h" +%End + public: + + QgsRandomMarkerFillSymbolLayerWidget( QgsVectorLayer *vl, QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsRandomMarkerFillSymbolLayerWidget. + +:param vl: associated vector layer +:param parent: parent widget +%End + + static QgsSymbolLayerWidget *create( QgsVectorLayer *vl ) /Factory/; +%Docstring +Creates a new QgsRandomMarkerFillSymbolLayerWidget. + +:param vl: associated vector layer +%End + + virtual void setSymbolLayer( QgsSymbolLayer *layer ); + + virtual QgsSymbolLayer *symbolLayer(); + + +}; + + + + class QgsFontMarkerSymbolLayerWidget : QgsSymbolLayerWidget { diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 8b59e72f895f..09dcf7051cb3 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -2330,7 +2330,7 @@ QgsGeometry QgsGeometry::extrude( double x, double y ) } ///@cond PRIVATE // avoid dox warning -QVector QgsGeometry::randomPointsInPolygon( int count, const std::function< bool( const QgsPointXY & ) > &acceptPoint, unsigned long seed, QgsFeedback *feedback ) +QVector QgsGeometry::randomPointsInPolygon( int count, const std::function< bool( const QgsPointXY & ) > &acceptPoint, unsigned long seed, QgsFeedback *feedback ) const { if ( type() != QgsWkbTypes::PolygonGeometry ) return QVector< QgsPointXY >(); @@ -2338,7 +2338,7 @@ QVector QgsGeometry::randomPointsInPolygon( int count, const std::fu return QgsInternalGeometryEngine::randomPointsInPolygon( *this, count, acceptPoint, seed, feedback ); } -QVector QgsGeometry::randomPointsInPolygon( int count, unsigned long seed, QgsFeedback *feedback ) +QVector QgsGeometry::randomPointsInPolygon( int count, unsigned long seed, QgsFeedback *feedback ) const { if ( type() != QgsWkbTypes::PolygonGeometry ) return QVector< QgsPointXY >(); diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index 378a2e6db33e..49232c4ff0e5 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -1466,7 +1466,7 @@ class CORE_EXPORT QgsGeometry * * \since QGIS 3.10 */ - QVector< QgsPointXY > randomPointsInPolygon( int count, const std::function< bool( const QgsPointXY & ) > &acceptPoint, unsigned long seed = 0, QgsFeedback *feedback = nullptr ); + QVector< QgsPointXY > randomPointsInPolygon( int count, const std::function< bool( const QgsPointXY & ) > &acceptPoint, unsigned long seed = 0, QgsFeedback *feedback = nullptr ) const; /** * Returns a list of \a count random points generated inside a (multi)polygon geometry. @@ -1481,7 +1481,7 @@ class CORE_EXPORT QgsGeometry * * \since QGIS 3.10 */ - QVector< QgsPointXY > randomPointsInPolygon( int count, unsigned long seed = 0, QgsFeedback *feedback = nullptr ); + QVector< QgsPointXY > randomPointsInPolygon( int count, unsigned long seed = 0, QgsFeedback *feedback = nullptr ) const; ///@cond PRIVATE #else @@ -1497,7 +1497,7 @@ class CORE_EXPORT QgsGeometry * * \since QGIS 3.10 */ - SIP_PYOBJECT randomPointsInPolygon( int count, unsigned long seed = 0 ) SIP_TYPEHINT( QgsPolylineXY ); + SIP_PYOBJECT randomPointsInPolygon( int count, unsigned long seed = 0 ) const SIP_TYPEHINT( QgsPolylineXY ); % MethodCode const QgsWkbTypes::GeometryType type = sipCpp->type(); if ( sipCpp->isNull() ) diff --git a/src/core/symbology/qgsfillsymbollayer.cpp b/src/core/symbology/qgsfillsymbollayer.cpp index cb71aab3de7a..46969c0adc21 100644 --- a/src/core/symbology/qgsfillsymbollayer.cpp +++ b/src/core/symbology/qgsfillsymbollayer.cpp @@ -31,12 +31,15 @@ #include "qgsmessagelog.h" #include "qgsapplication.h" #include "qgsimageoperation.h" +#include "qgspolygon.h" +#include "qgslinestring.h" #include #include #include #include #include +#include QgsSimpleFillSymbolLayer::QgsSimpleFillSymbolLayer( const QColor &color, Qt::BrushStyle style, const QColor &strokeColor, Qt::PenStyle strokeStyle, double strokeWidth, Qt::PenJoinStyle penJoinStyle ) @@ -3909,3 +3912,306 @@ void QgsRasterFillSymbolLayer::applyPattern( QBrush &brush, const QString &image brush.setTextureImage( img ); } + + +// +// QgsRandomMarkerFillSymbolLayer +// + +QgsRandomMarkerFillSymbolLayer::QgsRandomMarkerFillSymbolLayer( int pointCount, unsigned long seed ) + : mPointCount( pointCount ) + , mSeed( seed ) +{ + setSubSymbol( new QgsMarkerSymbol() ); +} + +QgsSymbolLayer *QgsRandomMarkerFillSymbolLayer::create( const QgsStringMap &properties ) +{ + const int pointCount = properties.value( QStringLiteral( "point_count" ), QStringLiteral( "10" ) ).toInt(); + + unsigned long seed = 0; + if ( properties.contains( QStringLiteral( "seed" ) ) ) + seed = properties.value( QStringLiteral( "seed" ) ).toULong(); + else + { + // if we a creating a new random marker fill from scratch, we default to a random seed + // because seed based fills are just nicer for users vs seeing points jump around with every map refresh + std::random_device rd; + std::mt19937 mt( seed == 0 ? rd() : seed ); + std::uniform_int_distribution<> uniformDist( 1, 999999999 ); + seed = uniformDist( mt ); + } + + std::unique_ptr< QgsRandomMarkerFillSymbolLayer > sl = qgis::make_unique< QgsRandomMarkerFillSymbolLayer >( pointCount, seed ); + + if ( properties.contains( QStringLiteral( "clip_points" ) ) ) + { + sl->setClipPoints( properties[QStringLiteral( "clip_points" )].toInt() ); + } + + return sl.release(); +} + +QString QgsRandomMarkerFillSymbolLayer::layerType() const +{ + return QStringLiteral( "RandomMarkerFill" ); +} + +void QgsRandomMarkerFillSymbolLayer::setColor( const QColor &color ) +{ + mMarker->setColor( color ); + mColor = color; +} + +QColor QgsRandomMarkerFillSymbolLayer::color() const +{ + return mMarker ? mMarker->color() : mColor; +} + +void QgsRandomMarkerFillSymbolLayer::startRender( QgsSymbolRenderContext &context ) +{ + mMarker->setOpacity( context.opacity() ); + mMarker->startRender( context.renderContext(), context.fields() ); +} + +void QgsRandomMarkerFillSymbolLayer::stopRender( QgsSymbolRenderContext &context ) +{ + mMarker->stopRender( context.renderContext() ); +} + +void QgsRandomMarkerFillSymbolLayer::renderPolygon( const QPolygonF &points, QList *rings, QgsSymbolRenderContext &context ) +{ + Part part; + part.exterior = points; + if ( rings ) + part.rings = *rings; + + if ( mRenderingFeature ) + { + // in the middle of rendering a possibly multi-part feature, so we collect all the parts and defer the actual rendering + // until after we've received the final part + mCurrentParts << part; + } + else + { + // not rendering a feature, so we can just render the polygon immediately + render( context.renderContext(), QVector< Part>() << part, context.feature() ? *context.feature() : QgsFeature(), context.selected() ); + } +} + +void QgsRandomMarkerFillSymbolLayer::render( QgsRenderContext &context, const QVector &parts, const QgsFeature &feature, bool selected ) +{ + bool clipPoints = mClipPoints; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyClipPoints ) ) + { + context.expressionContext().setOriginalValueVariable( clipPoints ); + clipPoints = mDataDefinedProperties.valueAsBool( QgsSymbolLayer::PropertyClipPoints, context.expressionContext(), clipPoints ); + } + + QVector< QgsGeometry > geometryParts; + geometryParts.reserve( parts.size() ); + QPainterPath path; + + for ( const Part &part : parts ) + { + QgsGeometry geom = QgsGeometry::fromQPolygonF( part.exterior ); + if ( !geom.isNull() && !part.rings.empty() ) + { + QgsPolygon *poly = qgsgeometry_cast< QgsPolygon * >( geom.get() ); + for ( const QPolygonF &ring : part.rings ) + { + poly->addInteriorRing( QgsLineString::fromQPolygonF( ring ) ); + } + } + if ( !geom.isGeosValid() ) + { + geom = geom.buffer( 0, 0 ); + } + geometryParts << geom; + + if ( clipPoints ) + { + path.addPolygon( part.exterior ); + for ( const QPolygonF &ring : part.rings ) + { + path.addPolygon( ring ); + } + } + } + + const QgsGeometry geom = geometryParts.count() != 1 ? QgsGeometry::unaryUnion( geometryParts ) : geometryParts.at( 0 ); + + if ( clipPoints ) + { + context.painter()->save(); + context.painter()->setClipPath( path ); + } + + int count = mPointCount; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyPointCount ) ) + { + context.expressionContext().setOriginalValueVariable( count ); + count = mDataDefinedProperties.valueAsInt( QgsSymbolLayer::PropertyPointCount, context.expressionContext(), count ); + } + unsigned long seed = mSeed; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyRandomSeed ) ) + { + context.expressionContext().setOriginalValueVariable( static_cast< unsigned long long >( seed ) ); + seed = mDataDefinedProperties.valueAsInt( QgsSymbolLayer::PropertyRandomSeed, context.expressionContext(), seed ); + } + + QVector< QgsPointXY > randomPoints = geom.randomPointsInPolygon( count, seed ); +#if 0 + // in some cases rendering from top to bottom is nice (e.g. randomised tree markers), but in other cases it's not wanted.. + // TODO consider exposing this as an option + std::sort( randomPoints.begin(), randomPoints.end(), []( const QgsPointXY & a, const QgsPointXY & b )->bool + { + return a.y() < b.y(); + } ); +#endif + for ( const QgsPointXY &p : qgis::as_const( randomPoints ) ) + { + mMarker->renderPoint( QPointF( p.x(), p.y() ), feature.isValid() ? &feature : nullptr, context, -1, selected ); + } + + if ( clipPoints ) + { + context.painter()->restore(); + } +} + +QgsStringMap QgsRandomMarkerFillSymbolLayer::properties() const +{ + QgsStringMap map; + map.insert( QStringLiteral( "point_count" ), QString::number( mPointCount ) ); + map.insert( QStringLiteral( "seed" ), QString::number( mSeed ) ); + map.insert( QStringLiteral( "clip_points" ), QString::number( mClipPoints ) ); + return map; +} + +QgsRandomMarkerFillSymbolLayer *QgsRandomMarkerFillSymbolLayer::clone() const +{ + std::unique_ptr< QgsRandomMarkerFillSymbolLayer > res = qgis::make_unique< QgsRandomMarkerFillSymbolLayer >( mPointCount, mSeed ); + res->mAngle = mAngle; + res->mColor = mColor; + res->mClipPoints = mClipPoints; + res->setSubSymbol( mMarker->clone() ); + copyDataDefinedProperties( res.get() ); + copyPaintEffect( res.get() ); + return res.release(); +} + +QgsSymbol *QgsRandomMarkerFillSymbolLayer::subSymbol() +{ + return mMarker.get(); +} + +bool QgsRandomMarkerFillSymbolLayer::setSubSymbol( QgsSymbol *symbol ) +{ + if ( !symbol || symbol->type() != QgsSymbol::Marker ) + { + delete symbol; + return false; + } + + mMarker.reset( static_cast( symbol ) ); + mColor = mMarker->color(); + return true; +} + +QSet QgsRandomMarkerFillSymbolLayer::usedAttributes( const QgsRenderContext &context ) const +{ + QSet attributes = QgsFillSymbolLayer::usedAttributes( context ); + + if ( mMarker ) + attributes.unite( mMarker->usedAttributes( context ) ); + + return attributes; +} + +bool QgsRandomMarkerFillSymbolLayer::hasDataDefinedProperties() const +{ + if ( QgsSymbolLayer::hasDataDefinedProperties() ) + return true; + if ( mMarker && mMarker->hasDataDefinedProperties() ) + return true; + return false; +} + +int QgsRandomMarkerFillSymbolLayer::pointCount() const +{ + return mPointCount; +} + +void QgsRandomMarkerFillSymbolLayer::setPointCount( int pointCount ) +{ + mPointCount = pointCount; +} + +unsigned long QgsRandomMarkerFillSymbolLayer::seed() const +{ + return mSeed; +} + +void QgsRandomMarkerFillSymbolLayer::setSeed( unsigned long seed ) +{ + mSeed = seed; +} + +bool QgsRandomMarkerFillSymbolLayer::clipPoints() const +{ + return mClipPoints; +} + +void QgsRandomMarkerFillSymbolLayer::setClipPoints( bool clipPoints ) +{ + mClipPoints = clipPoints; +} + +void QgsRandomMarkerFillSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & ) +{ + mRenderingFeature = true; + mCurrentParts.clear(); +} + +void QgsRandomMarkerFillSymbolLayer::stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context ) +{ + mRenderingFeature = false; + render( context, mCurrentParts, feature, false ); +} + + +void QgsRandomMarkerFillSymbolLayer::setOutputUnit( QgsUnitTypes::RenderUnit unit ) +{ + if ( mMarker ) + { + mMarker->setOutputUnit( unit ); + } +} + +QgsUnitTypes::RenderUnit QgsRandomMarkerFillSymbolLayer::outputUnit() const +{ + if ( mMarker ) + { + return mMarker->outputUnit(); + } + return QgsUnitTypes::RenderUnknownUnit; //mOutputUnit; +} + +void QgsRandomMarkerFillSymbolLayer::setMapUnitScale( const QgsMapUnitScale &scale ) +{ + if ( mMarker ) + { + mMarker->setMapUnitScale( scale ); + } +} + +QgsMapUnitScale QgsRandomMarkerFillSymbolLayer::mapUnitScale() const +{ + if ( mMarker ) + { + return mMarker->mapUnitScale(); + } + return QgsMapUnitScale(); +} + diff --git a/src/core/symbology/qgsfillsymbollayer.h b/src/core/symbology/qgsfillsymbollayer.h index 1de6d99b4963..ccd2404a14fd 100644 --- a/src/core/symbology/qgsfillsymbollayer.h +++ b/src/core/symbology/qgsfillsymbollayer.h @@ -49,6 +49,11 @@ class CORE_EXPORT QgsSimpleFillSymbolLayer : public QgsFillSymbolLayer // static stuff + /** + * Creates a new QgsSimpleFillSymbolLayer using the specified \a properties map containing symbol properties (see properties()). + * + * Caller takes ownership of the returned symbol layer. + */ static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) SIP_FACTORY; static QgsSymbolLayer *createFromSld( QDomElement &element ) SIP_FACTORY; @@ -209,6 +214,11 @@ class CORE_EXPORT QgsGradientFillSymbolLayer : public QgsFillSymbolLayer // static stuff + /** + * Creates a new QgsGradientFillSymbolLayer using the specified \a properties map containing symbol properties (see properties()). + * + * Caller takes ownership of the returned symbol layer. + */ static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) SIP_FACTORY; // implemented from base classes @@ -366,6 +376,11 @@ class CORE_EXPORT QgsShapeburstFillSymbolLayer : public QgsFillSymbolLayer // static stuff + /** + * Creates a new QgsShapeburstFillSymbolLayer using the specified \a properties map containing symbol properties (see properties()). + * + * Caller takes ownership of the returned symbol layer. + */ static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) SIP_FACTORY; // implemented from base classes @@ -1452,6 +1467,11 @@ class CORE_EXPORT QgsPointPatternFillSymbolLayer: public QgsImageFillSymbolLayer QgsPointPatternFillSymbolLayer(); ~QgsPointPatternFillSymbolLayer() override; + /** + * Creates a new QgsPointPatternFillSymbolLayer using the specified \a properties map containing symbol properties (see properties()). + * + * Caller takes ownership of the returned symbol layer. + */ static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) SIP_FACTORY; static QgsSymbolLayer *createFromSld( QDomElement &element ) SIP_FACTORY; @@ -1702,6 +1722,126 @@ class CORE_EXPORT QgsPointPatternFillSymbolLayer: public QgsImageFillSymbolLayer double displacementX, double displacementY, double offsetX, double offsetY ); }; +/** + * \ingroup core + * \class QgsRandomMarkerFillSymbolLayer + * + * A fill symbol layer which places markers at random locations within polygons. + * + * \since QGIS 3.12 + */ +class CORE_EXPORT QgsRandomMarkerFillSymbolLayer : public QgsFillSymbolLayer +{ + public: + + /** + * Constructor for QgsRandomMarkerFillSymbolLayer, with the specified \a pointCount. + * + * Optionally a specific random number \a seed can be used when generating points. A \a seed of 0 indicates that + * a truly random sequence will be used on every rendering, causing points to appear in different locations with every map refresh. + */ + QgsRandomMarkerFillSymbolLayer( int pointCount = 10, unsigned long seed = 0 ); + + /** + * Creates a new QgsRandomMarkerFillSymbolLayer using the specified \a properties map containing symbol properties (see properties()). + * + * Caller takes ownership of the returned symbol layer. + */ + static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) SIP_FACTORY; + + QString layerType() const override; + void startRender( QgsSymbolRenderContext &context ) override; + void stopRender( QgsSymbolRenderContext &context ) override; + void renderPolygon( const QPolygonF &points, QList *rings, QgsSymbolRenderContext &context ) override; + QgsStringMap properties() const override; + QgsRandomMarkerFillSymbolLayer *clone() const override SIP_FACTORY; + + void setColor( const QColor &color ) override; + QColor color() const override; + + QgsSymbol *subSymbol() override; + bool setSubSymbol( QgsSymbol *symbol SIP_TRANSFER ) override; + + void setOutputUnit( QgsUnitTypes::RenderUnit unit ) override; + QgsUnitTypes::RenderUnit outputUnit() const override; + + void setMapUnitScale( const QgsMapUnitScale &scale ) override; + QgsMapUnitScale mapUnitScale() const override; + + QSet usedAttributes( const QgsRenderContext &context ) const override; + bool hasDataDefinedProperties() const override; + + /** + * Returns the count of random points to render in the fill. + * + * \see setPointCount() + */ + int pointCount() const; + + /** + * Sets the \a count of random points to render in the fill. + * + * \see pointCount() + */ + void setPointCount( int count ); + + /** + * Returns the random number seed to use when generating points, or 0 if + * a truly random sequence will be used (causing points to appear in different locations with every map refresh). + * \see setSeed() + */ + unsigned long seed() const; + + /** + * Sets the random number \a seed to use when generating points, or 0 if + * a truly random sequence will be used on every rendering (causing points to appear + * in different locations with every map refresh). + * + * \see seed() + */ + void setSeed( unsigned long seed ); + + /** + * Returns TRUE if point markers should be clipped to the polygon boundary. + * + * \see setClipPoints() + */ + bool clipPoints() const; + + /** + * Sets whether point markers should be \a clipped to the polygon boundary. + * + * \see clipPoints() + */ + void setClipPoints( bool clipped ); + + void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context ) override; + void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context ) override; + + private: +#ifdef SIP_RUN + QgsRandomMarkerFillSymbolLayer( const QgsRandomMarkerFillSymbolLayer &other ); +#endif + + struct Part + { + QPolygonF exterior; + QList rings; + }; + + QVector< Part > mCurrentParts; + + void render( QgsRenderContext &context, const QVector< Part > &parts, const QgsFeature &feature, bool selected ); + + std::unique_ptr< QgsMarkerSymbol > mMarker; + int mPointCount = 10; + unsigned long mSeed = 0; + bool mClipPoints = false; + + bool mRenderingFeature = false; +}; + + /** * \ingroup core * \class QgsCentroidFillSymbolLayer @@ -1713,6 +1853,11 @@ class CORE_EXPORT QgsCentroidFillSymbolLayer : public QgsFillSymbolLayer // static stuff + /** + * Creates a new QgsCentroidFillSymbolLayer using the specified \a properties map containing symbol properties (see properties()). + * + * Caller takes ownership of the returned symbol layer. + */ static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) SIP_FACTORY; static QgsSymbolLayer *createFromSld( QDomElement &element ) SIP_FACTORY; diff --git a/src/core/symbology/qgssymbol.cpp b/src/core/symbology/qgssymbol.cpp index 9cc6706cacf3..0c404d4cdbbe 100644 --- a/src/core/symbology/qgssymbol.cpp +++ b/src/core/symbology/qgssymbol.cpp @@ -856,6 +856,8 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont QgsGeometry renderedBoundsGeom; + startFeatureRender( feature, context, layer ); + switch ( QgsWkbTypes::flatType( segmentizedGeometry.constGet()->wkbType() ) ) { case QgsWkbTypes::Point: @@ -1115,6 +1117,8 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont .arg( geom.wkbType(), 0, 16 ) ); } + stopFeatureRender( feature, context, layer ); + if ( context.hasRenderedFeatureHandlers() ) { QgsRenderedFeatureHandlerInterface::RenderedFeatureContext featureContext( context ); @@ -1172,6 +1176,54 @@ void QgsSymbol::renderVertexMarker( QPointF pt, QgsRenderContext &context, int c QgsSymbolLayerUtils::drawVertexMarker( pt.x(), pt.y(), *context.painter(), static_cast< QgsSymbolLayerUtils::VertexMarkerType >( currentVertexMarkerType ), markerSize ); } +void QgsSymbol::startFeatureRender( const QgsFeature &feature, QgsRenderContext &context, const int layer ) +{ + if ( layer != -1 ) + { + QgsSymbolLayer *symbolLayer = mLayers.value( layer ); + if ( symbolLayer && symbolLayer->enabled() ) + { + symbolLayer->startFeatureRender( feature, context ); + } + return; + } + else + { + const QList< QgsSymbolLayer * > layers = mLayers; + for ( QgsSymbolLayer *symbolLayer : layers ) + { + if ( !symbolLayer->enabled() ) + continue; + + symbolLayer->startFeatureRender( feature, context ); + } + } +} + +void QgsSymbol::stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context, int layer ) +{ + if ( layer != -1 ) + { + QgsSymbolLayer *symbolLayer = mLayers.value( layer ); + if ( symbolLayer && symbolLayer->enabled() ) + { + symbolLayer->stopFeatureRender( feature, context ); + } + return; + } + else + { + const QList< QgsSymbolLayer * > layers = mLayers; + for ( QgsSymbolLayer *symbolLayer : layers ) + { + if ( !symbolLayer->enabled() ) + continue; + + symbolLayer->stopFeatureRender( feature, context ); + } + } +} + //////////////////// @@ -1909,7 +1961,7 @@ void QgsLineSymbol::renderPolyline( const QPolygonF &points, const QgsFeature *f for ( QgsSymbolLayer *symbolLayer : constMLayers ) { if ( context.renderingStopped() ) - break;; + break; if ( !symbolLayer->enabled() ) continue; diff --git a/src/core/symbology/qgssymbol.h b/src/core/symbology/qgssymbol.h index 9d1183d6717d..a5820db6546f 100644 --- a/src/core/symbology/qgssymbol.h +++ b/src/core/symbology/qgssymbol.h @@ -628,6 +628,32 @@ class CORE_EXPORT QgsSymbol //! Initialized in startRender, destroyed in stopRender std::unique_ptr< QgsSymbolRenderContext > mSymbolRenderContext; + /** + * Called before symbol layers will be rendered for a particular \a feature. + * + * This is always followed by a call to stopFeatureRender() after the feature + * has been completely rendered (i.e. all parts have been rendered). + * + * Internally, this notifies all symbol layers which will be used via a call to + * QgsSymbolLayer::startFeatureRender(). + * + * \since QGIS 3.12 + */ + void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context, int layer = -1 ); + + /** + * Called after symbol layers have been rendered for a particular \a feature. + * + * This is always preceded by a call to startFeatureRender() just before the feature + * will be rendered. + * + * Internally, this notifies all symbol layers which were used via a call to + * QgsSymbolLayer::stopFeatureRender(). + * + * \since QGIS 3.12 + */ + void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context, int layer = -1 ); + Q_DISABLE_COPY( QgsSymbol ) }; diff --git a/src/core/symbology/qgssymbollayer.cpp b/src/core/symbology/qgssymbollayer.cpp index 1fe112034614..22d905667adf 100644 --- a/src/core/symbology/qgssymbollayer.cpp +++ b/src/core/symbology/qgssymbollayer.cpp @@ -93,9 +93,11 @@ void QgsSymbolLayer::initPropertyDefinitions() { QgsSymbolLayer::PropertyArrowStartWidth, QgsPropertyDefinition( "arrowStartWidth", QObject::tr( "Arrow line start width" ), QgsPropertyDefinition::StrokeWidth, origin )}, { QgsSymbolLayer::PropertyArrowHeadLength, QgsPropertyDefinition( "arrowHeadLength", QObject::tr( "Arrow head length" ), QgsPropertyDefinition::DoublePositive, origin )}, { QgsSymbolLayer::PropertyArrowHeadThickness, QgsPropertyDefinition( "arrowHeadThickness", QObject::tr( "Arrow head thickness" ), QgsPropertyDefinition::DoublePositive, origin )}, - { QgsSymbolLayer::PropertyArrowHeadType, QgsPropertyDefinition( "arrowHeadType", QgsPropertyDefinition::DataTypeString, QObject::tr( "Arrow head type" ), QObject::tr( "string " ) + QLatin1String( "[single|reversed|double]" ) )}, - { QgsSymbolLayer::PropertyArrowType, QgsPropertyDefinition( "arrowType", QgsPropertyDefinition::DataTypeString, QObject::tr( "Arrow type" ), QObject::tr( "string " ) + QLatin1String( "[plain|lefthalf|righthalf]" ) )}, - + { QgsSymbolLayer::PropertyArrowHeadType, QgsPropertyDefinition( "arrowHeadType", QgsPropertyDefinition::DataTypeString, QObject::tr( "Arrow head type" ), QObject::tr( "string " ) + QLatin1String( "[single|reversed|double]" ), origin )}, + { QgsSymbolLayer::PropertyArrowType, QgsPropertyDefinition( "arrowType", QgsPropertyDefinition::DataTypeString, QObject::tr( "Arrow type" ), QObject::tr( "string " ) + QLatin1String( "[plain|lefthalf|righthalf]" ), origin )}, + { QgsSymbolLayer::PropertyPointCount, QgsPropertyDefinition( "pointCount", QObject::tr( "Point count" ), QgsPropertyDefinition::IntegerPositive, origin )}, + { QgsSymbolLayer::PropertyRandomSeed, QgsPropertyDefinition( "randomSeed", QgsPropertyDefinition::DataTypeNumeric, QObject::tr( "Random number seed" ), QObject::tr( "integer > 0, or 0 for completely random sequence" ), origin )}, + { QgsSymbolLayer::PropertyClipPoints, QgsPropertyDefinition( "clipPoints", QObject::tr( "Clip markers" ), QgsPropertyDefinition::Boolean, origin )}, }; } @@ -104,6 +106,16 @@ void QgsSymbolLayer::setDataDefinedProperty( QgsSymbolLayer::Property key, const dataDefinedProperties().setProperty( key, property ); } +void QgsSymbolLayer::startFeatureRender( const QgsFeature &, QgsRenderContext & ) +{ + +} + +void QgsSymbolLayer::stopFeatureRender( const QgsFeature &, QgsRenderContext & ) +{ + +} + bool QgsSymbolLayer::writeDxf( QgsDxfExport &e, double mmMapUnitScaleFactor, const QString &layerName, QgsSymbolRenderContext &context, QPointF shift ) const { Q_UNUSED( e ) diff --git a/src/core/symbology/qgssymbollayer.h b/src/core/symbology/qgssymbollayer.h index 7a5f412427da..b7f1369e9bf8 100644 --- a/src/core/symbology/qgssymbollayer.h +++ b/src/core/symbology/qgssymbollayer.h @@ -106,6 +106,8 @@ class CORE_EXPORT QgsSymbolLayer sipType = sipType_QgsGradientFillSymbolLayer; else if ( sipCpp->layerType() == "ShapeburstFill" ) sipType = sipType_QgsShapeburstFillSymbolLayer; + else if ( sipCpp->layerType() == "RandomMarkerFill" ) + sipType = sipType_QgsRandomMarkerFillSymbolLayer; else sipType = sipType_QgsFillSymbolLayer; break; @@ -177,6 +179,9 @@ class CORE_EXPORT QgsSymbolLayer PropertyArrowType, //!< Arrow type PropertyOffsetX, //!< Horizontal offset PropertyOffsetY, //!< Vertical offset + PropertyPointCount, //!< Point count + PropertyRandomSeed, //!< Random number seed + PropertyClipPoints, //!< Whether markers should be clipped to polygon boundaries }; /** @@ -245,9 +250,73 @@ class CORE_EXPORT QgsSymbolLayer */ virtual QString layerType() const = 0; + /** + * Called before a set of rendering operations commences on the supplied render \a context. + * + * This is always followed by a call to stopRender() after all rendering operations + * have been completed. + * + * Subclasses can use this method to prepare for a set of rendering operations, e.g. by + * pre-evaluating paths or images to render, and performing other one-time optimisations. + * + * \see startFeatureRender() + * \see stopRender() + */ virtual void startRender( QgsSymbolRenderContext &context ) = 0; + + /** + * Called after a set of rendering operations has finished on the supplied render \a context. + * + * This is always preceded by a call to startRender() before all rendering operations + * are commenced. + * + * Subclasses can use this method to cleanup after a set of rendering operations. + * + * \see startRender() + * \see stopFeatureRender() + */ virtual void stopRender( QgsSymbolRenderContext &context ) = 0; + /** + * Called before the layer will be rendered for a particular \a feature. + * + * This is always followed by a call to stopFeatureRender() after the feature + * has been completely rendered (i.e. all parts have been rendered). + * + * The default implementation does nothing. + * + * \note In some circumstances, startFeatureRender() and stopFeatureRender() may not be called + * before a symbol layer is rendered. E.g., when a symbol layer is being rendered in isolation + * and not as a result of rendering a feature (for instance, when rendering a legend patch or other + * non-feature based shape). + * + * \see stopFeatureRender() + * \see startRender() + * + * \since QGIS 3.12 + */ + virtual void startFeatureRender( const QgsFeature &feature, QgsRenderContext &context ); + + /** + * Called after the layer has been rendered for a particular \a feature. + * + * This is always preceded by a call to startFeatureRender() just before the feature + * will be rendered. + * + * The default implementation does nothing. + * + * \note In some circumstances, startFeatureRender() and stopFeatureRender() may not be called + * before a symbol layer is rendered. E.g., when a symbol layer is being rendered in isolation + * and not as a result of rendering a feature (for instance, when rendering a legend patch or other + * non-feature based shape). + * + * \see startFeatureRender() + * \see stopRender() + * + * \since QGIS 3.12 + */ + virtual void stopFeatureRender( const QgsFeature &feature, QgsRenderContext &context ); + /** * Shall be reimplemented by subclasses to create a deep copy of the instance. */ diff --git a/src/core/symbology/qgssymbollayerregistry.cpp b/src/core/symbology/qgssymbollayerregistry.cpp index 918e17fc4a2e..9909c431f1f8 100644 --- a/src/core/symbology/qgssymbollayerregistry.cpp +++ b/src/core/symbology/qgssymbollayerregistry.cpp @@ -61,6 +61,8 @@ QgsSymbolLayerRegistry::QgsSymbolLayerRegistry() QgsSVGFillSymbolLayer::create, QgsSVGFillSymbolLayer::createFromSld, QgsSVGFillSymbolLayer::resolvePaths ) ); addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "CentroidFill" ), QObject::tr( "Centroid fill" ), QgsSymbol::Fill, QgsCentroidFillSymbolLayer::create, QgsCentroidFillSymbolLayer::createFromSld ) ); + addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "RandomMarkerFill" ), QObject::tr( "Random marker fill" ), QgsSymbol::Fill, + QgsRandomMarkerFillSymbolLayer::create ) ); addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "LinePatternFill" ), QObject::tr( "Line pattern fill" ), QgsSymbol::Fill, QgsLinePatternFillSymbolLayer::create, QgsLinePatternFillSymbolLayer::createFromSld ) ); addSymbolLayerType( new QgsSymbolLayerMetadata( QStringLiteral( "PointPatternFill" ), QObject::tr( "Point pattern fill" ), QgsSymbol::Fill, diff --git a/src/gui/symbology/qgslayerpropertieswidget.cpp b/src/gui/symbology/qgslayerpropertieswidget.cpp index 340d0f9df5ba..c2f30a0ae101 100644 --- a/src/gui/symbology/qgslayerpropertieswidget.cpp +++ b/src/gui/symbology/qgslayerpropertieswidget.cpp @@ -87,6 +87,7 @@ static void _initWidgetFunctions() _initWidgetFunction( QStringLiteral( "CentroidFill" ), QgsCentroidFillSymbolLayerWidget::create ); _initWidgetFunction( QStringLiteral( "LinePatternFill" ), QgsLinePatternFillSymbolLayerWidget::create ); _initWidgetFunction( QStringLiteral( "PointPatternFill" ), QgsPointPatternFillSymbolLayerWidget::create ); + _initWidgetFunction( QStringLiteral( "RandomMarkerFill" ), QgsRandomMarkerFillSymbolLayerWidget::create ); _initWidgetFunction( QStringLiteral( "GeometryGenerator" ), QgsGeometryGeneratorSymbolLayerWidget::create ); diff --git a/src/gui/symbology/qgssymbollayerwidget.cpp b/src/gui/symbology/qgssymbollayerwidget.cpp index ddf319364dc0..231c7c86aadc 100644 --- a/src/gui/symbology/qgssymbollayerwidget.cpp +++ b/src/gui/symbology/qgssymbollayerwidget.cpp @@ -4109,3 +4109,68 @@ void QgsGeometryGeneratorSymbolLayerWidget::updateSymbolType() emit symbolChanged(); } + +// +// QgsRandomMarkerFillSymbolLayerWidget +// + + +QgsRandomMarkerFillSymbolLayerWidget::QgsRandomMarkerFillSymbolLayerWidget( QgsVectorLayer *vl, QWidget *parent ): + QgsSymbolLayerWidget( parent, vl ) +{ + setupUi( this ); + mPointCountSpinBox->setShowClearButton( true ); + mPointCountSpinBox->setClearValue( 100 ); + mSeedSpinBox->setShowClearButton( true ); + mSeedSpinBox->setClearValue( 0 ); + connect( mPointCountSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsRandomMarkerFillSymbolLayerWidget::countChanged ); + connect( mSeedSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsRandomMarkerFillSymbolLayerWidget::seedChanged ); + connect( mClipPointsCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) + { + if ( mLayer ) + { + mLayer->setClipPoints( checked ); + emit changed(); + } + } ); +} + +void QgsRandomMarkerFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) +{ + if ( !layer || layer->layerType() != QLatin1String( "RandomMarkerFill" ) ) + { + return; + } + + mLayer = static_cast( layer ); + whileBlocking( mPointCountSpinBox )->setValue( mLayer->pointCount() ); + whileBlocking( mSeedSpinBox )->setValue( mLayer->seed() ); + whileBlocking( mClipPointsCheckBox )->setChecked( mLayer->clipPoints() ); + + registerDataDefinedButton( mPointCountDdbtn, QgsSymbolLayer::PropertyPointCount ); + registerDataDefinedButton( mSeedDdbtn, QgsSymbolLayer::PropertyRandomSeed ); + registerDataDefinedButton( mClipPointsDdbtn, QgsSymbolLayer::PropertyClipPoints ); +} + +QgsSymbolLayer *QgsRandomMarkerFillSymbolLayerWidget::symbolLayer() +{ + return mLayer; +} + +void QgsRandomMarkerFillSymbolLayerWidget::countChanged( int d ) +{ + if ( mLayer ) + { + mLayer->setPointCount( d ); + emit changed(); + } +} + +void QgsRandomMarkerFillSymbolLayerWidget::seedChanged( int d ) +{ + if ( mLayer ) + { + mLayer->setSeed( d ); + emit changed(); + } +} diff --git a/src/gui/symbology/qgssymbollayerwidget.h b/src/gui/symbology/qgssymbollayerwidget.h index 2336bb3eb263..57946557d3c2 100644 --- a/src/gui/symbology/qgssymbollayerwidget.h +++ b/src/gui/symbology/qgssymbollayerwidget.h @@ -888,6 +888,52 @@ class GUI_EXPORT QgsPointPatternFillSymbolLayerWidget: public QgsSymbolLayerWidg void mVerticalOffsetUnitWidget_changed(); }; + +////////// + +#include "ui_widget_randommarkerfill.h" + +class QgsRandomMarkerFillSymbolLayer; + +/** + * \ingroup gui + * \class QgsRandomMarkerFillSymbolLayerWidget + * + * Widget for controlling the properties of a QgsRandomMarkerFillSymbolLayer. + * + * \since QGIS 3.12 + */ +class GUI_EXPORT QgsRandomMarkerFillSymbolLayerWidget: public QgsSymbolLayerWidget, private Ui::WidgetRandomMarkerFill +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsRandomMarkerFillSymbolLayerWidget. + * \param vl associated vector layer + * \param parent parent widget + */ + QgsRandomMarkerFillSymbolLayerWidget( QgsVectorLayer *vl, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Creates a new QgsRandomMarkerFillSymbolLayerWidget. + * \param vl associated vector layer + */ + static QgsSymbolLayerWidget *create( QgsVectorLayer *vl ) SIP_FACTORY { return new QgsRandomMarkerFillSymbolLayerWidget( vl ); } + + void setSymbolLayer( QgsSymbolLayer *layer ) override; + QgsSymbolLayer *symbolLayer() override; + + private: + QgsRandomMarkerFillSymbolLayer *mLayer = nullptr; + + private slots: + + void countChanged( int d ); + void seedChanged( int d ); +}; + ///////// #include "ui_widget_fontmarker.h" diff --git a/src/ui/symbollayer/widget_randommarkerfill.ui b/src/ui/symbollayer/widget_randommarkerfill.ui new file mode 100644 index 000000000000..c6f9aa4df80f --- /dev/null +++ b/src/ui/symbollayer/widget_randommarkerfill.ui @@ -0,0 +1,120 @@ + + + WidgetRandomMarkerFill + + + + 0 + 0 + 401 + 225 + + + + Form + + + + + + … + + + + + + + Seed + + + + + + + Point count + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 1 + 0 + + + + 999999999 + + + + + + + + 1 + 0 + + + + randomised + + + 999999999 + + + + + + + … + + + + + + + Clip markers to polygon boundary + + + + + + + … + + + + + + + + QgsPropertyOverrideButton + QToolButton +
qgspropertyoverridebutton.h
+
+ + QgsSpinBox + QSpinBox +
qgsspinbox.h
+
+
+ + mPointCountDdbtn + + + +
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 7670ec1b6686..ecada0be0885 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -176,6 +176,7 @@ ADD_PYTHON_TEST(PyQgsProjectMetadata test_qgsprojectmetadata.py) ADD_PYTHON_TEST(PyQgsPropertyOverrideButton test_qgspropertyoverridebutton.py) ADD_PYTHON_TEST(PyQgsProviderConnectionPostgres test_qgsproviderconnection_postgres.py) ADD_PYTHON_TEST(PyQgsProviderConnectionGpkg test_qgsproviderconnection_ogr_gpkg.py) +ADD_PYTHON_TEST(TestQgsRandomMarkerSymbolLayer test_qgsrandommarkersymbollayer.py) ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py) ADD_PYTHON_TEST(PyQgsRangeWidgets test_qgsrangewidgets.py) ADD_PYTHON_TEST(PyQgsRasterBandComboBox test_qgsrasterbandcombobox.py) diff --git a/tests/src/python/test_qgsrandommarkersymbollayer.py b/tests/src/python/test_qgsrandommarkersymbollayer.py new file mode 100644 index 000000000000..d23a7695a6a6 --- /dev/null +++ b/tests/src/python/test_qgsrandommarkersymbollayer.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + test_qgsrandommarkersymbollayer.py + --------------------- + Date : October 2019 + Copyright : (C) 2019 by Nyall Dawson + Email : nyall dot dawson at gmail dot com +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +__author__ = 'Nyall Dawson' +__date__ = 'October 2019' +__copyright__ = '(C) 2019, Nyall Dawson' + +import qgis # NOQA + +from utilities import unitTestDataPath + +from qgis.PyQt.QtCore import QDir, Qt, QSize +from qgis.PyQt.QtGui import QImage, QColor, QPainter +from qgis.PyQt.QtXml import QDomDocument + +from qgis.core import (QgsGeometry, + QgsFillSymbol, + QgsRenderContext, + QgsFeature, + QgsMapSettings, + QgsRenderChecker, + QgsReadWriteContext, + QgsSymbolLayerUtils, + QgsSimpleMarkerSymbolLayer, + QgsSimpleFillSymbolLayer, + QgsMarkerSymbol, + QgsRandomMarkerFillSymbolLayer + ) + +from qgis.testing import unittest, start_app + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsRandomMarkerSymbolLayer(unittest.TestCase): + + def setUp(self): + self.report = "

Python QgsRandomFillSymbolLayer Tests

\n" + + def tearDown(self): + report_file_path = "%s/qgistest.html" % QDir.tempPath() + with open(report_file_path, 'a') as report_file: + report_file.write(self.report) + + def testSimple(self): + s = QgsFillSymbol() + s.deleteSymbolLayer(0) + + random_fill = QgsRandomMarkerFillSymbolLayer(10, seed=481523) + marker = QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayer.Triangle, 4) + marker.setColor(QColor(255, 0, 0)) + marker.setStrokeStyle(Qt.NoPen) + marker_symbol = QgsMarkerSymbol() + marker_symbol.changeSymbolLayer(0, marker) + random_fill.setSubSymbol(marker_symbol) + + s.appendSymbolLayer(random_fill.clone()) + self.assertEqual(s.symbolLayer(0).pointCount(), 10) + self.assertEqual(s.symbolLayer(0).seed(), 481523) + s.symbolLayer(0).setPointCount(5) + s.symbolLayer(0).setSeed(42) + self.assertEqual(s.symbolLayer(0).pointCount(), 5) + self.assertEqual(s.symbolLayer(0).seed(), 42) + + s2 = s.clone() + self.assertEqual(s2.symbolLayer(0).pointCount(), 5) + self.assertEqual(s2.symbolLayer(0).seed(), 42) + + doc = QDomDocument() + context = QgsReadWriteContext() + element = QgsSymbolLayerUtils.saveSymbol('test', s, doc, context) + + s2 = QgsSymbolLayerUtils.loadSymbol(element, context) + self.assertEqual(s2.symbolLayer(0).pointCount(), 5) + self.assertEqual(s2.symbolLayer(0).seed(), 42) + + # rendering test + s3 = QgsFillSymbol() + s3.deleteSymbolLayer(0) + s3.appendSymbolLayer(random_fill.clone()) + + g = QgsGeometry.fromWkt( + 'Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))') + rendered_image = self.renderGeometry(s3, g) + self.assertTrue(self.imageCheck('randommarkerfill', 'randommarkerfill', rendered_image)) + + s3.symbolLayer(0).setPointCount(3) + g = QgsGeometry.fromWkt( + 'Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))') + rendered_image = self.renderGeometry(s3, g) + self.assertTrue(self.imageCheck('randommarkerfill_3', 'randommarkerfill_3', rendered_image)) + + s3.symbolLayer(0).setSeed(12783) + g = QgsGeometry.fromWkt( + 'Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))') + rendered_image = self.renderGeometry(s3, g) + self.assertTrue(self.imageCheck('randommarkerfill_seed', 'randommarkerfill_seed', rendered_image)) + + # random seed + s3.symbolLayer(0).setSeed(0) + g = QgsGeometry.fromWkt( + 'Polygon((0 0, 10 0, 10 10, 0 10, 0 0),(1 1, 1 2, 2 2, 2 1, 1 1),(8 8, 9 8, 9 9, 8 9, 8 8))') + rendered_image = self.renderGeometry(s3, g) + self.assertFalse(self.imageCheck('randommarkerfill_seed', 'randommarkerfill_seed', rendered_image)) + + def testCreate(self): + random_fill = QgsRandomMarkerFillSymbolLayer(10, seed=5) + self.assertEqual(random_fill.seed(), 5) + fill2 = QgsRandomMarkerFillSymbolLayer.create(random_fill.properties()) + self.assertEqual(fill2.seed(), 5) + + random_fill = QgsRandomMarkerFillSymbolLayer(10) + self.assertEqual(random_fill.seed(), 0) + fill2 = QgsRandomMarkerFillSymbolLayer.create(random_fill.properties()) + self.assertEqual(fill2.seed(), 0) + + # a newly created random fill should default to a random seed, not 0 + random_fill = QgsRandomMarkerFillSymbolLayer.create({}) + self.assertNotEqual(random_fill.seed(), 0) + + def testMultipart(self): + """ + Test rendering a multi-part random marker fill symbol + """ + s = QgsFillSymbol() + s.deleteSymbolLayer(0) + + simple_fill = QgsSimpleFillSymbolLayer(color=QColor(255, 255, 255)) + s.appendSymbolLayer(simple_fill.clone()) + + random_fill = QgsRandomMarkerFillSymbolLayer(12, seed=481523) + marker = QgsSimpleMarkerSymbolLayer(QgsSimpleMarkerSymbolLayer.Triangle, 4) + marker.setColor(QColor(255, 0, 0)) + marker.setStrokeStyle(Qt.NoPen) + marker_symbol = QgsMarkerSymbol() + marker_symbol.changeSymbolLayer(0, marker) + random_fill.setSubSymbol(marker_symbol) + + s.appendSymbolLayer(random_fill.clone()) + + g = QgsGeometry.fromWkt( + 'MultiPolygon(((0 0, 5 0, 5 10, 0 10, 0 0),(1 1, 1 9, 4 9, 4 1, 1 1)), ((6 0, 10 0, 10 5, 6 5, 6 0)), ((8 6, 10 6, 10 10, 8 10, 8 6)))') + rendered_image = self.renderGeometry(s, g) + self.assertTrue(self.imageCheck('randommarkerfill_multipoly', 'randommarkerfill_multipoly', rendered_image)) + + def renderGeometry(self, symbol, geom, buffer=20): + f = QgsFeature() + f.setGeometry(geom) + + image = QImage(200, 200, QImage.Format_RGB32) + + painter = QPainter() + ms = QgsMapSettings() + extent = geom.get().boundingBox() + # buffer extent by 10% + if extent.width() > 0: + extent = extent.buffered((extent.height() + extent.width()) / buffer) + else: + extent = extent.buffered(buffer / 2) + + ms.setExtent(extent) + ms.setOutputSize(image.size()) + context = QgsRenderContext.fromMapSettings(ms) + context.setPainter(painter) + context.setScaleFactor(96 / 25.4) # 96 DPI + context.expressionContext().setFeature(f) + + painter.begin(image) + try: + image.fill(QColor(0, 0, 0)) + symbol.startRender(context) + symbol.renderFeature(f, context) + symbol.stopRender(context) + finally: + painter.end() + + return image + + def imageCheck(self, name, reference_image, image): + self.report += "

Render {}

\n".format(name) + temp_dir = QDir.tempPath() + '/' + file_name = temp_dir + 'symbol_' + name + ".png" + image.save(file_name, "PNG") + checker = QgsRenderChecker() + checker.setControlPathPrefix("symbol_randommarkerfill") + checker.setControlName("expected_" + reference_image) + checker.setRenderedImage(file_name) + checker.setColorTolerance(2) + result = checker.compareImages(name, 20) + self.report += checker.report() + print((self.report)) + return result + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill/expected_randommarkerfill.png b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill/expected_randommarkerfill.png new file mode 100644 index 000000000000..9e76a8bc3c06 Binary files /dev/null and b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill/expected_randommarkerfill.png differ diff --git a/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_3/expected_randommarkerfill_3.png b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_3/expected_randommarkerfill_3.png new file mode 100644 index 000000000000..cce16f747ebd Binary files /dev/null and b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_3/expected_randommarkerfill_3.png differ diff --git a/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_multipoly/expected_randommarkerfill_multipoly.png b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_multipoly/expected_randommarkerfill_multipoly.png new file mode 100644 index 000000000000..18135be7f321 Binary files /dev/null and b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_multipoly/expected_randommarkerfill_multipoly.png differ diff --git a/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_seed/expected_randommarkerfill_seed.png b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_seed/expected_randommarkerfill_seed.png new file mode 100644 index 000000000000..0bbccd1a77f7 Binary files /dev/null and b/tests/testdata/control_images/symbol_randommarkerfill/expected_randommarkerfill_seed/expected_randommarkerfill_seed.png differ diff --git a/tests/testdata/control_images/symbol_simpleline (copy)/expected_simpleline_exterioronly/expected_simpleline_exterioronly.png b/tests/testdata/control_images/symbol_simpleline (copy)/expected_simpleline_exterioronly/expected_simpleline_exterioronly.png deleted file mode 100644 index 50857b0832eb..000000000000 Binary files a/tests/testdata/control_images/symbol_simpleline (copy)/expected_simpleline_exterioronly/expected_simpleline_exterioronly.png and /dev/null differ diff --git a/tests/testdata/control_images/symbol_simpleline (copy)/expected_simpleline_interioronly/expected_simpleline_interioronly.png b/tests/testdata/control_images/symbol_simpleline (copy)/expected_simpleline_interioronly/expected_simpleline_interioronly.png deleted file mode 100644 index afd66a9cc509..000000000000 Binary files a/tests/testdata/control_images/symbol_simpleline (copy)/expected_simpleline_interioronly/expected_simpleline_interioronly.png and /dev/null differ