diff --git a/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in b/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in index 1edb450b6378..5328203facf8 100644 --- a/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in @@ -2250,11 +2250,173 @@ Sets the marker clipping ``mode``, which defines how markers are clipped at the .. seealso:: :py:func:`clipMode` +.. versionadded:: 3.24 +%End + + void setMaximumRandomDeviationX( double deviation ); +%Docstring +Sets the maximum horizontal random ``deviation`` of points in the pattern. + +Units are set via :py:func:`~QgsPointPatternFillSymbolLayer.setMaximumRandomDeviationXUnit`. + +.. seealso:: :py:func:`maximumRandomDeviationX` + +.. seealso:: :py:func:`setMaximumRandomDeviationY` + +.. versionadded:: 3.24 +%End + + double maximumRandomDeviationX() const; +%Docstring +Returns the maximum horizontal random deviation of points in the pattern. + +Units are retrieved via :py:func:`~QgsPointPatternFillSymbolLayer.maximumRandomDeviationXUnit`. + +.. seealso:: :py:func:`setMaximumRandomDeviationX` + +.. seealso:: :py:func:`maximumRandomDeviationY` + +.. versionadded:: 3.24 +%End + + void setMaximumRandomDeviationY( double deviation ); +%Docstring +Sets the maximum vertical random ``deviation`` of points in the pattern. + +Units are set via :py:func:`~QgsPointPatternFillSymbolLayer.setMaximumRandomDeviationYUnit`. + +.. seealso:: :py:func:`maximumRandomDeviationY` + +.. seealso:: :py:func:`setMaximumRandomDeviationX` + +.. versionadded:: 3.24 +%End + + double maximumRandomDeviationY() const; +%Docstring +Returns the maximum vertical random deviation of points in the pattern. + +Units are retrieved via :py:func:`~QgsPointPatternFillSymbolLayer.maximumRandomDeviationYUnit`. + +.. seealso:: :py:func:`setMaximumRandomDeviationY` + +.. seealso:: :py:func:`maximumRandomDeviationX` + +.. versionadded:: 3.24 +%End + + void setRandomDeviationXUnit( QgsUnitTypes::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the horizontal random deviation of points in the pattern. + +.. seealso:: :py:func:`randomDeviationXUnit` + +.. seealso:: :py:func:`setRandomDeviationYUnit` + +.. versionadded:: 3.24 +%End + + QgsUnitTypes::RenderUnit randomDeviationXUnit() const; +%Docstring +Returns the units for the horizontal random deviation of points in the pattern. + +.. seealso:: :py:func:`setRandomDeviationXUnit` + +.. seealso:: :py:func:`randomDeviationYUnit` + +.. versionadded:: 3.24 +%End + + void setRandomDeviationYUnit( QgsUnitTypes::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the vertical random deviation of points in the pattern. + +.. seealso:: :py:func:`randomDeviationYUnit` + +.. seealso:: :py:func:`setRandomDeviationXUnit` + +.. versionadded:: 3.24 +%End + + QgsUnitTypes::RenderUnit randomDeviationYUnit() const; +%Docstring +Returns the units for the vertical random deviation of points in the pattern. + +.. seealso:: :py:func:`setRandomDeviationYUnit` + +.. seealso:: :py:func:`randomDeviationXUnit` + +.. versionadded:: 3.24 +%End + + const QgsMapUnitScale &randomDeviationXMapUnitScale() const; +%Docstring +Returns the unit scale for the horizontal random deviation of points in the pattern. + +.. seealso:: :py:func:`setRandomDeviationXMapUnitScale` + +.. seealso:: :py:func:`randomDeviationYMapUnitScale` + +.. versionadded:: 3.24 +%End + + const QgsMapUnitScale &randomDeviationYMapUnitScale() const; +%Docstring +Returns the unit scale for the vertical random deviation of points in the pattern. + +.. seealso:: :py:func:`setRandomDeviationXMapUnitScale` + +.. seealso:: :py:func:`randomDeviationXMapUnitScale` + +.. versionadded:: 3.24 +%End + + void setRandomDeviationXMapUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the unit ``scale`` for the horizontal random deviation of points in the pattern. + +.. seealso:: :py:func:`randomDeviationXMapUnitScale` + +.. seealso:: :py:func:`setRandomDeviationYMapUnitScale` + +.. versionadded:: 3.24 +%End + + void setRandomDeviationYMapUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the unit ``scale`` for the vertical random deviation of points in the pattern. + +.. seealso:: :py:func:`randomDeviationYMapUnitScale` + +.. seealso:: :py:func:`setRandomDeviationXMapUnitScale` + +.. versionadded:: 3.24 +%End + + unsigned long seed() const; +%Docstring +Returns the random number seed to use when randomly shifting 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` + +.. versionadded:: 3.24 +%End + + void setSeed( unsigned long seed ); +%Docstring +Sets the random number ``seed`` to use when randomly shifting 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` + .. versionadded:: 3.24 %End protected: + virtual void applyDataDefinedSettings( QgsSymbolRenderContext &context ); diff --git a/python/core/auto_generated/symbology/qgssymbollayer.sip.in b/python/core/auto_generated/symbology/qgssymbollayer.sip.in index 5e1b5f22db34..fb518d338040 100644 --- a/python/core/auto_generated/symbology/qgssymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayer.sip.in @@ -157,6 +157,8 @@ class QgsSymbolLayer PropertyLineStartColorValue, PropertyLineEndColorValue, PropertyMarkerClipping, + PropertyRandomOffsetX, + PropertyRandomOffsetY, }; static const QgsPropertiesDefinition &propertyDefinitions(); diff --git a/src/core/symbology/qgsfillsymbollayer.cpp b/src/core/symbology/qgsfillsymbollayer.cpp index 13d6d6947468..acca7d79c5c2 100644 --- a/src/core/symbology/qgsfillsymbollayer.cpp +++ b/src/core/symbology/qgsfillsymbollayer.cpp @@ -3233,6 +3233,12 @@ void QgsPointPatternFillSymbolLayer::setOutputUnit( QgsUnitTypes::RenderUnit uni mDisplacementYUnit = unit; mOffsetXUnit = unit; mOffsetYUnit = unit; + // don't change "percentage" units -- since they adapt directly to whatever other unit is set + if ( mRandomDeviationXUnit != QgsUnitTypes::RenderPercentage ) + mRandomDeviationXUnit = unit; + if ( mRandomDeviationYUnit != QgsUnitTypes::RenderPercentage ) + mRandomDeviationYUnit = unit; + if ( mMarkerSymbol ) { mMarkerSymbol->setOutputUnit( unit ); @@ -3242,7 +3248,14 @@ void QgsPointPatternFillSymbolLayer::setOutputUnit( QgsUnitTypes::RenderUnit uni QgsUnitTypes::RenderUnit QgsPointPatternFillSymbolLayer::outputUnit() const { QgsUnitTypes::RenderUnit unit = QgsImageFillSymbolLayer::outputUnit(); - if ( mDistanceXUnit != unit || mDistanceYUnit != unit || mDisplacementXUnit != unit || mDisplacementYUnit != unit || mOffsetXUnit != unit || mOffsetYUnit != unit ) + if ( mDistanceXUnit != unit || + mDistanceYUnit != unit || + mDisplacementXUnit != unit || + mDisplacementYUnit != unit || + mOffsetXUnit != unit || + mOffsetYUnit != unit || + ( mRandomDeviationXUnit != QgsUnitTypes::RenderPercentage && mRandomDeviationXUnit != unit ) || + ( mRandomDeviationYUnit != QgsUnitTypes::RenderPercentage && mRandomDeviationYUnit != unit ) ) { return QgsUnitTypes::RenderUnknownUnit; } @@ -3256,7 +3269,9 @@ bool QgsPointPatternFillSymbolLayer::usesMapUnits() const || mDisplacementXUnit == QgsUnitTypes::RenderMapUnits || mDisplacementXUnit == QgsUnitTypes::RenderMetersInMapUnits || mDisplacementYUnit == QgsUnitTypes::RenderMapUnits || mDisplacementYUnit == QgsUnitTypes::RenderMetersInMapUnits || mOffsetXUnit == QgsUnitTypes::RenderMapUnits || mOffsetXUnit == QgsUnitTypes::RenderMetersInMapUnits - || mOffsetYUnit == QgsUnitTypes::RenderMapUnits || mOffsetYUnit == QgsUnitTypes::RenderMetersInMapUnits; + || mOffsetYUnit == QgsUnitTypes::RenderMapUnits || mOffsetYUnit == QgsUnitTypes::RenderMetersInMapUnits + || mRandomDeviationXUnit == QgsUnitTypes::RenderMapUnits || mRandomDeviationXUnit == QgsUnitTypes::RenderMetersInMapUnits + || mRandomDeviationYUnit == QgsUnitTypes::RenderMapUnits || mRandomDeviationYUnit == QgsUnitTypes::RenderMetersInMapUnits; } void QgsPointPatternFillSymbolLayer::setMapUnitScale( const QgsMapUnitScale &scale ) @@ -3268,6 +3283,8 @@ void QgsPointPatternFillSymbolLayer::setMapUnitScale( const QgsMapUnitScale &sca mDisplacementYMapUnitScale = scale; mOffsetXMapUnitScale = scale; mOffsetYMapUnitScale = scale; + mRandomDeviationXMapUnitScale = scale; + mRandomDeviationYMapUnitScale = scale; } QgsMapUnitScale QgsPointPatternFillSymbolLayer::mapUnitScale() const @@ -3277,7 +3294,9 @@ QgsMapUnitScale QgsPointPatternFillSymbolLayer::mapUnitScale() const mDistanceYMapUnitScale == mDisplacementXMapUnitScale && mDisplacementXMapUnitScale == mDisplacementYMapUnitScale && mDisplacementYMapUnitScale == mOffsetXMapUnitScale && - mOffsetXMapUnitScale == mOffsetYMapUnitScale ) + mOffsetXMapUnitScale == mOffsetYMapUnitScale && + mRandomDeviationXMapUnitScale == mOffsetYMapUnitScale && + mRandomDeviationYMapUnitScale == mRandomDeviationXMapUnitScale ) { return mDistanceXMapUnitScale; } @@ -3361,6 +3380,44 @@ QgsSymbolLayer *QgsPointPatternFillSymbolLayer::create( const QVariantMap &prope layer->setOffsetYMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( properties[QStringLiteral( "offset_y_map_unit_scale" )].toString() ) ); } + if ( properties.contains( QStringLiteral( "random_deviation_x" ) ) ) + { + layer->setMaximumRandomDeviationX( properties[QStringLiteral( "random_deviation_x" )].toDouble() ); + } + if ( properties.contains( QStringLiteral( "random_deviation_y" ) ) ) + { + layer->setMaximumRandomDeviationY( properties[QStringLiteral( "random_deviation_y" )].toDouble() ); + } + if ( properties.contains( QStringLiteral( "random_deviation_x_unit" ) ) ) + { + layer->setRandomDeviationXUnit( QgsUnitTypes::decodeRenderUnit( properties[QStringLiteral( "random_deviation_x_unit" )].toString() ) ); + } + if ( properties.contains( QStringLiteral( "random_deviation_x_map_unit_scale" ) ) ) + { + layer->setRandomDeviationXMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( properties[QStringLiteral( "random_deviation_x_map_unit_scale" )].toString() ) ); + } + if ( properties.contains( QStringLiteral( "random_deviation_y_unit" ) ) ) + { + layer->setRandomDeviationYUnit( QgsUnitTypes::decodeRenderUnit( properties[QStringLiteral( "random_deviation_y_unit" )].toString() ) ); + } + if ( properties.contains( QStringLiteral( "random_deviation_y_map_unit_scale" ) ) ) + { + layer->setRandomDeviationYMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( properties[QStringLiteral( "random_deviation_y_map_unit_scale" )].toString() ) ); + } + unsigned long seed = 0; + if ( properties.contains( QStringLiteral( "seed" ) ) ) + seed = properties.value( QStringLiteral( "seed" ) ).toUInt(); + else + { + // if we a creating a new point pattern 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 ); + } + layer->setSeed( seed ); + if ( properties.contains( QStringLiteral( "outline_width_unit" ) ) ) { layer->setStrokeWidthUnit( QgsUnitTypes::decodeRenderUnit( properties[QStringLiteral( "outline_width_unit" )].toString() ) ); @@ -3487,7 +3544,11 @@ void QgsPointPatternFillSymbolLayer::startRender( QgsSymbolRenderContext &contex mRenderUsingMarkers = context.renderContext().forceVectorOutput() || mMarkerSymbol->hasDataDefinedProperties() || mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyMarkerClipping ) - || mClipMode != Qgis::MarkerClipMode::Shape; + || mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyRandomOffsetX ) + || mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyRandomOffsetY ) + || mClipMode != Qgis::MarkerClipMode::Shape + || !qgsDoubleNear( mRandomDeviationX, 0 ) + || !qgsDoubleNear( mRandomDeviationY, 0 ); if ( mRenderUsingMarkers ) { @@ -3660,6 +3721,36 @@ void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, con top -= boundingRect.top() - ( height * std::floor( boundingRect.top() / height ) ); } + unsigned long seed = mSeed; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyRandomSeed ) ) + { + context.renderContext().expressionContext().setOriginalValueVariable( static_cast< unsigned long long >( seed ) ); + seed = mDataDefinedProperties.valueAsInt( QgsSymbolLayer::PropertyRandomSeed, context.renderContext().expressionContext(), seed ); + } + + double maxRandomDeviationX = mRandomDeviationX; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyRandomOffsetX ) ) + { + context.setOriginalValueVariable( maxRandomDeviationX ); + maxRandomDeviationX = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyRandomOffsetX, context.renderContext().expressionContext(), maxRandomDeviationX ); + } + const double maxRandomDeviationPixelX = mRandomDeviationXUnit == QgsUnitTypes::RenderPercentage ? ( maxRandomDeviationX * width / 100 ) + : context.renderContext().convertToPainterUnits( maxRandomDeviationX, mRandomDeviationXUnit, mRandomDeviationXMapUnitScale ); + + double maxRandomDeviationY = mRandomDeviationY; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyRandomOffsetY ) ) + { + context.setOriginalValueVariable( maxRandomDeviationY ); + maxRandomDeviationY = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyRandomOffsetY, context.renderContext().expressionContext(), maxRandomDeviationY ); + } + const double maxRandomDeviationPixelY = mRandomDeviationYUnit == QgsUnitTypes::RenderPercentage ? ( maxRandomDeviationY * height / 100 ) + : context.renderContext().convertToPainterUnits( maxRandomDeviationY, mRandomDeviationYUnit, mRandomDeviationYMapUnitScale ); + + std::random_device rd; + std::mt19937 mt( seed == 0 ? rd() : seed ); + std::uniform_real_distribution<> uniformDist( 0, 1 ); + const bool useRandomShift = !qgsDoubleNear( maxRandomDeviationPixelX, 0 ) || !qgsDoubleNear( maxRandomDeviationPixelY, 0 ); + QgsExpressionContextScope *scope = new QgsExpressionContextScope(); QgsExpressionContextScopePopper scopePopper( context.renderContext().expressionContext(), scope ); int pointNum = 0; @@ -3685,6 +3776,12 @@ void QgsPointPatternFillSymbolLayer::renderPolygon( const QPolygonF &points, con if ( !alternateColumn ) y -= displacementPixelY; + if ( useRandomShift ) + { + x += ( 2 * uniformDist( mt ) - 1 ) * maxRandomDeviationPixelX; + y += ( 2 * uniformDist( mt ) - 1 ) * maxRandomDeviationPixelY; + } + if ( needsExpressionContext ) { scope->addVariable( QgsExpressionContextScope::StaticVariable( QgsExpressionContext::EXPR_GEOMETRY_POINT_NUM, ++pointNum, true ) ); @@ -3768,6 +3865,13 @@ QVariantMap QgsPointPatternFillSymbolLayer::properties() const 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 ) ); + map.insert( QStringLiteral( "random_deviation_x" ), QString::number( mRandomDeviationX ) ); + map.insert( QStringLiteral( "random_deviation_y" ), QString::number( mRandomDeviationY ) ); + map.insert( QStringLiteral( "random_deviation_x_unit" ), QgsUnitTypes::encodeUnit( mRandomDeviationXUnit ) ); + map.insert( QStringLiteral( "random_deviation_y_unit" ), QgsUnitTypes::encodeUnit( mRandomDeviationYUnit ) ); + map.insert( QStringLiteral( "random_deviation_x_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mRandomDeviationXMapUnitScale ) ); + map.insert( QStringLiteral( "random_deviation_y_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mRandomDeviationYMapUnitScale ) ); + map.insert( QStringLiteral( "seed" ), QString::number( mSeed ) ); return map; } diff --git a/src/core/symbology/qgsfillsymbollayer.h b/src/core/symbology/qgsfillsymbollayer.h index daab59f79658..ff4864e19846 100644 --- a/src/core/symbology/qgsfillsymbollayer.h +++ b/src/core/symbology/qgsfillsymbollayer.h @@ -2001,6 +2001,141 @@ class CORE_EXPORT QgsPointPatternFillSymbolLayer: public QgsImageFillSymbolLayer */ void setClipMode( Qgis::MarkerClipMode mode ) { mClipMode = mode; } + /** + * Sets the maximum horizontal random \a deviation of points in the pattern. + * + * Units are set via setMaximumRandomDeviationXUnit(). + * + * \see maximumRandomDeviationX() + * \see setMaximumRandomDeviationY() + * \since QGIS 3.24 + */ + void setMaximumRandomDeviationX( double deviation ) { mRandomDeviationX = deviation; } + + /** + * Returns the maximum horizontal random deviation of points in the pattern. + * + * Units are retrieved via maximumRandomDeviationXUnit(). + * + * \see setMaximumRandomDeviationX() + * \see maximumRandomDeviationY() + * \since QGIS 3.24 + */ + double maximumRandomDeviationX() const { return mRandomDeviationX; } + + /** + * Sets the maximum vertical random \a deviation of points in the pattern. + * + * Units are set via setMaximumRandomDeviationYUnit(). + * + * \see maximumRandomDeviationY() + * \see setMaximumRandomDeviationX() + * \since QGIS 3.24 + */ + void setMaximumRandomDeviationY( double deviation ) { mRandomDeviationY = deviation; } + + /** + * Returns the maximum vertical random deviation of points in the pattern. + * + * Units are retrieved via maximumRandomDeviationYUnit(). + * + * \see setMaximumRandomDeviationY() + * \see maximumRandomDeviationX() + * \since QGIS 3.24 + */ + double maximumRandomDeviationY() const { return mRandomDeviationY; } + + /** + * Sets the \a unit for the horizontal random deviation of points in the pattern. + * + * \see randomDeviationXUnit() + * \see setRandomDeviationYUnit() + * \since QGIS 3.24 + */ + void setRandomDeviationXUnit( QgsUnitTypes::RenderUnit unit ) { mRandomDeviationXUnit = unit; } + + /** + * Returns the units for the horizontal random deviation of points in the pattern. + * \see setRandomDeviationXUnit() + * \see randomDeviationYUnit() + * + * \since QGIS 3.24 + */ + QgsUnitTypes::RenderUnit randomDeviationXUnit() const { return mRandomDeviationXUnit; } + + /** + * Sets the \a unit for the vertical random deviation of points in the pattern. + * + * \see randomDeviationYUnit() + * \see setRandomDeviationXUnit() + * \since QGIS 3.24 + */ + void setRandomDeviationYUnit( QgsUnitTypes::RenderUnit unit ) { mRandomDeviationYUnit = unit; } + + /** + * Returns the units for the vertical random deviation of points in the pattern. + * + * \see setRandomDeviationYUnit() + * \see randomDeviationXUnit() + * + * \since QGIS 3.24 + */ + QgsUnitTypes::RenderUnit randomDeviationYUnit() const { return mRandomDeviationYUnit; } + + /** + * Returns the unit scale for the horizontal random deviation of points in the pattern. + * + * \see setRandomDeviationXMapUnitScale() + * \see randomDeviationYMapUnitScale() + * \since QGIS 3.24 + */ + const QgsMapUnitScale &randomDeviationXMapUnitScale() const { return mRandomDeviationXMapUnitScale; } + + /** + * Returns the unit scale for the vertical random deviation of points in the pattern. + * + * \see setRandomDeviationXMapUnitScale() + * \see randomDeviationXMapUnitScale() + * \since QGIS 3.24 + */ + const QgsMapUnitScale &randomDeviationYMapUnitScale() const { return mRandomDeviationYMapUnitScale; } + + /** + * Sets the unit \a scale for the horizontal random deviation of points in the pattern. + * + * \see randomDeviationXMapUnitScale() + * \see setRandomDeviationYMapUnitScale() + * \since QGIS 3.24 + */ + void setRandomDeviationXMapUnitScale( const QgsMapUnitScale &scale ) { mRandomDeviationXMapUnitScale = scale; } + + /** + * Sets the unit \a scale for the vertical random deviation of points in the pattern. + * + * \see randomDeviationYMapUnitScale() + * \see setRandomDeviationXMapUnitScale() + * \since QGIS 3.24 + */ + void setRandomDeviationYMapUnitScale( const QgsMapUnitScale &scale ) { mRandomDeviationYMapUnitScale = scale; } + + /** + * Returns the random number seed to use when randomly shifting points, or 0 if + * a truly random sequence will be used (causing points to appear in different locations with every map refresh). + * \see setSeed() + * \since QGIS 3.24 + */ + unsigned long seed() const { return mSeed; } + + /** + * Sets the random number \a seed to use when randomly shifting 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() + * \since QGIS 3.24 + */ + void setSeed( unsigned long seed ) { mSeed = seed; } + protected: std::unique_ptr< QgsMarkerSymbol > mMarkerSymbol; double mDistanceX = 15; @@ -2022,6 +2157,14 @@ class CORE_EXPORT QgsPointPatternFillSymbolLayer: public QgsImageFillSymbolLayer QgsUnitTypes::RenderUnit mOffsetYUnit = QgsUnitTypes::RenderMillimeters; QgsMapUnitScale mOffsetYMapUnitScale; + double mRandomDeviationX = 0; + QgsUnitTypes::RenderUnit mRandomDeviationXUnit = QgsUnitTypes::RenderMillimeters; + QgsMapUnitScale mRandomDeviationXMapUnitScale; + double mRandomDeviationY = 0; + QgsUnitTypes::RenderUnit mRandomDeviationYUnit = QgsUnitTypes::RenderMillimeters; + QgsMapUnitScale mRandomDeviationYMapUnitScale; + unsigned long mSeed = 0; + void applyDataDefinedSettings( QgsSymbolRenderContext &context ) override; private: diff --git a/src/core/symbology/qgssymbollayer.cpp b/src/core/symbology/qgssymbollayer.cpp index 4fc2e86045f4..082913f3e558 100644 --- a/src/core/symbology/qgssymbollayer.cpp +++ b/src/core/symbology/qgssymbollayer.cpp @@ -117,6 +117,8 @@ void QgsSymbolLayer::initPropertyDefinitions() { 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( "[no|shape|centroid_within|completely_within]" ), origin )}, + { QgsSymbolLayer::PropertyRandomOffsetX, QgsPropertyDefinition( "randomOffsetX", QObject::tr( "Horizontal random offset" ), QgsPropertyDefinition::Double, origin )}, + { QgsSymbolLayer::PropertyRandomOffsetY, QgsPropertyDefinition( "randomOffsetY", QObject::tr( "Vertical random offset" ), QgsPropertyDefinition::Double, origin )}, }; } diff --git a/src/core/symbology/qgssymbollayer.h b/src/core/symbology/qgssymbollayer.h index 5d5adcc9df24..bb410349c43b 100644 --- a/src/core/symbology/qgssymbollayer.h +++ b/src/core/symbology/qgssymbollayer.h @@ -201,6 +201,8 @@ class CORE_EXPORT QgsSymbolLayer 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) + PropertyRandomOffsetX, //!< Random offset X (since QGIS 3.24) + PropertyRandomOffsetY, //!< Random offset Y (since QGIS 3.24) }; /** diff --git a/src/gui/symbology/qgssymbollayerwidget.cpp b/src/gui/symbology/qgssymbollayerwidget.cpp index f86115d2cc2b..db27bbf698c0 100644 --- a/src/gui/symbology/qgssymbollayerwidget.cpp +++ b/src/gui/symbology/qgssymbollayerwidget.cpp @@ -3236,8 +3236,59 @@ QgsPointPatternFillSymbolLayerWidget::QgsPointPatternFillSymbolLayerWidget( QgsV emit changed(); } } ); -} + mSeedSpinBox->setShowClearButton( true ); + mSeedSpinBox->setClearValue( 0 ); + mRandomXSpinBox->setClearValue( 0 ); + mRandomYSpinBox->setClearValue( 0 ); + + mRandomXOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels + << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches << QgsUnitTypes::RenderPercentage ); + mRandomYOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels + << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches << QgsUnitTypes::RenderPercentage ); + connect( mRandomXSpinBox, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double d ) + { + if ( mLayer ) + { + mLayer->setMaximumRandomDeviationX( d ); + emit changed(); + } + } ); + connect( mRandomYSpinBox, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [ = ]( double d ) + { + if ( mLayer ) + { + mLayer->setMaximumRandomDeviationY( d ); + emit changed(); + } + } ); + connect( mRandomXOffsetUnitWidget, &QgsUnitSelectionWidget::changed, this, [ = ] + { + if ( mLayer ) + { + mLayer->setRandomDeviationXUnit( mRandomXOffsetUnitWidget->unit() ); + mLayer->setRandomDeviationXMapUnitScale( mRandomXOffsetUnitWidget->getMapUnitScale() ); + emit changed(); + } + } ); + connect( mRandomYOffsetUnitWidget, &QgsUnitSelectionWidget::changed, this, [ = ] + { + if ( mLayer ) + { + mLayer->setRandomDeviationYUnit( mRandomYOffsetUnitWidget->unit() ); + mLayer->setRandomDeviationYMapUnitScale( mRandomYOffsetUnitWidget->getMapUnitScale() ); + emit changed(); + } + } ); + connect( mSeedSpinBox, qOverload< int > ( &QSpinBox::valueChanged ), this, [ = ]( int v ) + { + if ( mLayer ) + { + mLayer->setSeed( v ); + emit changed(); + } + } ); +} void QgsPointPatternFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) { @@ -3282,6 +3333,14 @@ void QgsPointPatternFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer whileBlocking( mClipModeComboBox )->setCurrentIndex( mClipModeComboBox->findData( static_cast< int >( mLayer->clipMode() ) ) ); whileBlocking( mCoordinateReferenceComboBox )->setCurrentIndex( mCoordinateReferenceComboBox->findData( static_cast< int >( mLayer->coordinateReference() ) ) ); + whileBlocking( mRandomXSpinBox )->setValue( mLayer->maximumRandomDeviationX() ); + whileBlocking( mRandomYSpinBox )->setValue( mLayer->maximumRandomDeviationY() ); + whileBlocking( mRandomXOffsetUnitWidget )->setUnit( mLayer->randomDeviationXUnit() ); + whileBlocking( mRandomXOffsetUnitWidget )->setMapUnitScale( mLayer->randomDeviationXMapUnitScale() ); + whileBlocking( mRandomYOffsetUnitWidget )->setUnit( mLayer->randomDeviationYUnit() ); + whileBlocking( mRandomYOffsetUnitWidget )->setMapUnitScale( mLayer->randomDeviationYMapUnitScale() ); + whileBlocking( mSeedSpinBox )->setValue( mLayer->seed() ); + registerDataDefinedButton( mHorizontalDistanceDDBtn, QgsSymbolLayer::PropertyDistanceX ); registerDataDefinedButton( mVerticalDistanceDDBtn, QgsSymbolLayer::PropertyDistanceY ); registerDataDefinedButton( mHorizontalDisplacementDDBtn, QgsSymbolLayer::PropertyDisplacementX ); @@ -3290,6 +3349,9 @@ void QgsPointPatternFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer registerDataDefinedButton( mVerticalOffsetDDBtn, QgsSymbolLayer::PropertyOffsetY ); registerDataDefinedButton( mClippingDDBtn, QgsSymbolLayer::PropertyMarkerClipping ); registerDataDefinedButton( mCoordinateReferenceDDBtn, QgsSymbolLayer::PropertyCoordinateMode ); + registerDataDefinedButton( mRandomXDDBtn, QgsSymbolLayer::PropertyRandomOffsetX ); + registerDataDefinedButton( mRandomYDDBtn, QgsSymbolLayer::PropertyRandomOffsetY ); + registerDataDefinedButton( mSeedDdbtn, QgsSymbolLayer::PropertyRandomSeed ); } QgsSymbolLayer *QgsPointPatternFillSymbolLayerWidget::symbolLayer() diff --git a/src/ui/symbollayer/widget_pointpatternfill.ui b/src/ui/symbollayer/widget_pointpatternfill.ui index 3aa7ba2fb9ec..fc91855e1b6e 100644 --- a/src/ui/symbollayer/widget_pointpatternfill.ui +++ b/src/ui/symbollayer/widget_pointpatternfill.ui @@ -7,7 +7,7 @@ 0 0 401 - 342 + 521 @@ -406,6 +406,163 @@ + + + + Randomize Pattern + + + + + + Vertically + + + + + + + … + + + + + + + + + + 1 + 0 + + + + 6 + + + 0.000000000000000 + + + 99999999.989999994635582 + + + 0.200000000000000 + + + 0.000000000000000 + + + true + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + + + … + + + + + + + + + + 1 + 0 + + + + 6 + + + 0.000000000000000 + + + 99999999.989999994635582 + + + 0.200000000000000 + + + 0.000000000000000 + + + true + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + + + Horizontally + + + + + + + Seed + + + + + + + … + + + + + + + + 1 + 0 + + + + randomised + + + 999999999 + + + + + + @@ -425,6 +582,17 @@
qgsunitselectionwidget.h
1 + + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsSpinBox + QSpinBox +
qgsspinbox.h
+
mCoordinateReferenceComboBox @@ -447,6 +615,16 @@ mVerticalOffsetSpinBox mVerticalOffsetUnitWidget mVerticalOffsetDDBtn + mClipModeComboBox + mClippingDDBtn + mRandomXSpinBox + mRandomXOffsetUnitWidget + mRandomXDDBtn + mRandomYSpinBox + mRandomYOffsetUnitWidget + mRandomYDDBtn + mSeedSpinBox + mSeedDdbtn diff --git a/tests/src/core/testqgspointpatternfillsymbol.cpp b/tests/src/core/testqgspointpatternfillsymbol.cpp index 13da11e18f69..6eb0b8c66fd4 100644 --- a/tests/src/core/testqgspointpatternfillsymbol.cpp +++ b/tests/src/core/testqgspointpatternfillsymbol.cpp @@ -72,6 +72,9 @@ class TestQgsPointPatternFillSymbol : public QObject void pointPatternFillCompletelyWithin(); void pointPatternFillCentroidWithin(); void pointPatternFillDataDefinedClip(); + void pointPatternRandomOffset(); + void pointPatternRandomOffsetPercent(); + void pointPatternRandomOffsetDataDefined(); private: bool mTestHasError = false ; @@ -484,6 +487,95 @@ void TestQgsPointPatternFillSymbol::pointPatternFillDataDefinedClip() QVERIFY( res ); } +void TestQgsPointPatternFillSymbol::pointPatternRandomOffset() +{ + mReport += QLatin1String( "

Point pattern random offset

\n" ); + + std::unique_ptr< QgsVectorLayer> layer = std::make_unique< QgsVectorLayer>( mTestDataDir + "polys.shp" ); + QVERIFY( layer->isValid() ); + + QgsPointPatternFillSymbolLayer *pointPatternFill = new QgsPointPatternFillSymbolLayer(); + QgsFillSymbol *fillSymbol = new QgsFillSymbol(); + fillSymbol->changeSymbolLayer( 0, pointPatternFill ); + layer->setRenderer( new QgsSingleSymbolRenderer( fillSymbol ) ); + QVariantMap properties; + properties.insert( QStringLiteral( "color" ), QStringLiteral( "0,0,0,255" ) ); + properties.insert( QStringLiteral( "outline_color" ), QStringLiteral( "#000000" ) ); + properties.insert( QStringLiteral( "name" ), QStringLiteral( "circle" ) ); + properties.insert( QStringLiteral( "size" ), QStringLiteral( "5.0" ) ); + QgsMarkerSymbol *pointSymbol = QgsMarkerSymbol::createSimple( properties ); + + pointPatternFill->setSubSymbol( pointSymbol ); + pointPatternFill->setDistanceX( 10 ); + pointPatternFill->setDistanceY( 10 ); + pointPatternFill->setMaximumRandomDeviationX( 5 ); + pointPatternFill->setMaximumRandomDeviationY( 3 ); + pointPatternFill->setSeed( 1 ); + + const bool res = imageCheck( "symbol_pointfill_random_offset", layer.get() ); + QVERIFY( res ); +} + +void TestQgsPointPatternFillSymbol::pointPatternRandomOffsetPercent() +{ + mReport += QLatin1String( "

Point pattern random offset in percent

\n" ); + + std::unique_ptr< QgsVectorLayer> layer = std::make_unique< QgsVectorLayer>( mTestDataDir + "polys.shp" ); + QVERIFY( layer->isValid() ); + + QgsPointPatternFillSymbolLayer *pointPatternFill = new QgsPointPatternFillSymbolLayer(); + QgsFillSymbol *fillSymbol = new QgsFillSymbol(); + fillSymbol->changeSymbolLayer( 0, pointPatternFill ); + layer->setRenderer( new QgsSingleSymbolRenderer( fillSymbol ) ); + QVariantMap properties; + properties.insert( QStringLiteral( "color" ), QStringLiteral( "0,0,0,255" ) ); + properties.insert( QStringLiteral( "outline_color" ), QStringLiteral( "#000000" ) ); + properties.insert( QStringLiteral( "name" ), QStringLiteral( "circle" ) ); + properties.insert( QStringLiteral( "size" ), QStringLiteral( "5.0" ) ); + QgsMarkerSymbol *pointSymbol = QgsMarkerSymbol::createSimple( properties ); + + pointPatternFill->setSubSymbol( pointSymbol ); + pointPatternFill->setDistanceX( 10 ); + pointPatternFill->setDistanceY( 10 ); + pointPatternFill->setMaximumRandomDeviationX( 50 ); + pointPatternFill->setMaximumRandomDeviationY( 30 ); + pointPatternFill->setRandomDeviationXUnit( QgsUnitTypes::RenderPercentage ); + pointPatternFill->setRandomDeviationYUnit( QgsUnitTypes::RenderPercentage ); + pointPatternFill->setSeed( 1 ); + + const bool res = imageCheck( "symbol_pointfill_percent_random_offset", layer.get() ); + QVERIFY( res ); +} + +void TestQgsPointPatternFillSymbol::pointPatternRandomOffsetDataDefined() +{ + mReport += QLatin1String( "

Point pattern data defined random offset

\n" ); + + std::unique_ptr< QgsVectorLayer> layer = std::make_unique< QgsVectorLayer>( mTestDataDir + "polys.shp" ); + QVERIFY( layer->isValid() ); + + QgsPointPatternFillSymbolLayer *pointPatternFill = new QgsPointPatternFillSymbolLayer(); + QgsFillSymbol *fillSymbol = new QgsFillSymbol(); + fillSymbol->changeSymbolLayer( 0, pointPatternFill ); + layer->setRenderer( new QgsSingleSymbolRenderer( fillSymbol ) ); + QVariantMap properties; + properties.insert( QStringLiteral( "color" ), QStringLiteral( "0,0,0,255" ) ); + properties.insert( QStringLiteral( "outline_color" ), QStringLiteral( "#000000" ) ); + properties.insert( QStringLiteral( "name" ), QStringLiteral( "circle" ) ); + properties.insert( QStringLiteral( "size" ), QStringLiteral( "5.0" ) ); + QgsMarkerSymbol *pointSymbol = QgsMarkerSymbol::createSimple( properties ); + + pointPatternFill->setSubSymbol( pointSymbol ); + pointPatternFill->setDistanceX( 10 ); + pointPatternFill->setDistanceY( 10 ); + pointPatternFill->dataDefinedProperties().setProperty( QgsSymbolLayer::PropertyRandomOffsetX, QgsProperty::fromExpression( QStringLiteral( "case when $id % 2 = 0 then 5 else 10 end" ) ) ); + pointPatternFill->dataDefinedProperties().setProperty( QgsSymbolLayer::PropertyRandomOffsetY, QgsProperty::fromExpression( QStringLiteral( "case when $id % 2 = 0 then 3 else 6 end" ) ) ); + pointPatternFill->dataDefinedProperties().setProperty( QgsSymbolLayer::PropertyRandomSeed, QgsProperty::fromExpression( QStringLiteral( "case when $id % 2 = 0 then 1 else 2 end" ) ) ); + + const bool res = imageCheck( "symbol_pointfill_data_defined_random_offset", layer.get() ); + QVERIFY( res ); +} + // // Private helper functions not called directly by CTest // diff --git a/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_data_defined_random_offset/expected_symbol_pointfill_data_defined_random_offset.png b/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_data_defined_random_offset/expected_symbol_pointfill_data_defined_random_offset.png new file mode 100644 index 000000000000..336e14dcc541 Binary files /dev/null and b/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_data_defined_random_offset/expected_symbol_pointfill_data_defined_random_offset.png differ diff --git a/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_percent_random_offset/expected_symbol_pointfill_percent_random_offset.png b/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_percent_random_offset/expected_symbol_pointfill_percent_random_offset.png new file mode 100644 index 000000000000..8eb9c021de21 Binary files /dev/null and b/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_percent_random_offset/expected_symbol_pointfill_percent_random_offset.png differ diff --git a/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_random_offset/expected_symbol_pointfill_random_offset.png b/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_random_offset/expected_symbol_pointfill_random_offset.png new file mode 100644 index 000000000000..8eb9c021de21 Binary files /dev/null and b/tests/testdata/control_images/symbol_pointpatternfill/expected_symbol_pointfill_random_offset/expected_symbol_pointfill_random_offset.png differ