From fa2302d73edb76bb7bcba6e8fb9d576d8daf031e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 18 Sep 2021 07:32:21 +1000 Subject: [PATCH] Expose choice of units for geometry generator symbol layers Usually a geometry generator expression must return a geometry in the associated layer's CRS. But this doesn't work in situations where a geometry generator symbol is NOT associated with a layer, e.g. when used in a layout item. So add a new option to allow users to specify which unit the expression will return geometries in, with choices of map units (the default, previous behavior), millimeters, pixels, inches and points. When millimeters, points, inches or pixels is selected then the @map_geometry variable will be available for the expression, and contain the feature's geometry in the specified units (relative to the map frame). (The $geometry variable remains available and still in layer CRS, in case an expression needs to calculate the original area, perimeter, etc in real-world units while returning results in a different unit!) A step toward fixing #39159 --- .../qgsgeometrygeneratorsymbollayer.sip.in | 25 +++++++++ .../qgsunitselectionwidget.sip.in | 18 ++++++ .../qgsgeometrygeneratorsymbollayer.cpp | 55 ++++++++++++++++++- .../qgsgeometrygeneratorsymbollayer.h | 25 +++++++++ src/gui/qgsunitselectionwidget.cpp | 16 +++++- src/gui/qgsunitselectionwidget.h | 17 ++++++ src/gui/symbology/qgssymbollayerwidget.cpp | 20 +++++++ src/gui/symbology/qgssymbollayerwidget.h | 1 + .../qgsgeometrygeneratorwidgetbase.ui | 34 ++++++++++-- 9 files changed, 202 insertions(+), 9 deletions(-) diff --git a/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in b/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in index 8fcbfa1efc78..9de6e8960e58 100644 --- a/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in @@ -70,6 +70,30 @@ Set the expression to generate this geometry. QString geometryExpression() const; %Docstring Gets the expression to generate this geometry. +%End + + QgsUnitTypes::RenderUnit units() const; +%Docstring +Returns the unit for the geometry expression. + +By default this is :py:class:`QgsUnitTypes`.MapUnits, which means that the :py:func:`~QgsGeometryGeneratorSymbolLayer.geometryExpression` +will return geometries in the associated layer's CRS. + +.. seealso:: :py:func:`setUnits` + +.. versionadded:: 3.22 +%End + + void setUnits( QgsUnitTypes::RenderUnit units ); +%Docstring +Sets the ``units`` for the geometry expression. + +By default this is :py:class:`QgsUnitTypes`.MapUnits, which means that the :py:func:`~QgsGeometryGeneratorSymbolLayer.geometryExpression` +will return geometries in the associated layer's CRS. + +.. seealso:: :py:func:`units` + +.. versionadded:: 3.22 %End virtual QgsSymbol *subSymbol(); @@ -105,6 +129,7 @@ context which is available to the evaluated expression. virtual void setColor( const QColor &color ); + private: QgsGeometryGeneratorSymbolLayer( const QgsGeometryGeneratorSymbolLayer © ); }; diff --git a/python/gui/auto_generated/qgsunitselectionwidget.sip.in b/python/gui/auto_generated/qgsunitselectionwidget.sip.in index 2ef85c3591c6..991b7beaa05f 100644 --- a/python/gui/auto_generated/qgsunitselectionwidget.sip.in +++ b/python/gui/auto_generated/qgsunitselectionwidget.sip.in @@ -222,6 +222,24 @@ map scale from the canvas. :param canvas: map canvas .. versionadded:: 2.12 +%End + + bool showMapScaleButton() const; +%Docstring +Returns ``True`` if the widget can show the map scale button when the Map Units option is selected. + +.. seealso:: :py:func:`setShowMapScaleButton` + +.. versionadded:: 3.22 +%End + + void setShowMapScaleButton( bool show ); +%Docstring +Sets whether the widget can show the map scale button when the Map Units option is selected. + +.. seealso:: :py:func:`showMapScaleButton` + +.. versionadded:: 3.22 %End signals: diff --git a/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp b/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp index bd42d7d62095..f2d6eefef066 100644 --- a/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp +++ b/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp @@ -42,6 +42,8 @@ QgsSymbolLayer *QgsGeometryGeneratorSymbolLayer::create( const QVariantMap &prop { symbolLayer->setSubSymbol( QgsFillSymbol::createSimple( properties ) ); } + symbolLayer->setUnits( QgsUnitTypes::decodeRenderUnit( properties.value( QStringLiteral( "units" ), QStringLiteral( "mapunits" ) ).toString() ) ); + symbolLayer->restoreOldDataDefinedProperties( properties ); return symbolLayer; @@ -133,6 +135,7 @@ QgsSymbolLayer *QgsGeometryGeneratorSymbolLayer::clone() const clone->mMarkerSymbol.reset( mMarkerSymbol->clone() ); clone->setSymbolType( mSymbolType ); + clone->setUnits( mUnits ); copyDataDefinedProperties( clone ); copyPaintEffect( clone ); @@ -156,6 +159,8 @@ QVariantMap QgsGeometryGeneratorSymbolLayer::properties() const props.insert( QStringLiteral( "SymbolType" ), QStringLiteral( "Fill" ) ); break; } + props.insert( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); + return props; } @@ -222,11 +227,55 @@ void QgsGeometryGeneratorSymbolLayer::render( QgsSymbolRenderContext &context ) if ( context.feature() ) { - const QgsExpressionContext &expressionContext = context.renderContext().expressionContext(); + QgsExpressionContext &expressionContext = context.renderContext().expressionContext(); QgsFeature f = expressionContext.feature(); - const QgsGeometry geom = mExpression->evaluate( &expressionContext ).value(); - f.setGeometry( geom ); + + switch ( mUnits ) + { + case QgsUnitTypes::RenderMapUnits: + case QgsUnitTypes::RenderUnknownUnit: // unsupported, not exposed as an option + case QgsUnitTypes::RenderMetersInMapUnits: // unsupported, not exposed as an option + case QgsUnitTypes::RenderPercentage: // unsupported, not exposed as an option + { + QgsGeometry geom = mExpression->evaluate( &expressionContext ).value(); + f.setGeometry( geom ); + break; + } + + case QgsUnitTypes::RenderMillimeters: + case QgsUnitTypes::RenderPixels: + case QgsUnitTypes::RenderPoints: + case QgsUnitTypes::RenderInches: + { + QgsExpressionContextScope *generatorScope = new QgsExpressionContextScope(); + expressionContext.appendScope( generatorScope ); + + QgsGeometry transformed = f.geometry(); + transformed.transform( context.renderContext().coordinateTransform() ); + QTransform mapToPixel = context.renderContext().mapToPixel().transform(); + + // scale transform to target units + const double scale = 1 / context.renderContext().convertToPainterUnits( 1, mUnits ); + mapToPixel.scale( scale, scale ); + + if ( mExpression->referencedVariables().contains( QStringLiteral( "map_geometry" ) ) ) + { + transformed.transform( mapToPixel ); + generatorScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_geometry" ), transformed ) ); + } + + QgsGeometry geom = mExpression->evaluate( &expressionContext ).value(); + + geom.transform( mapToPixel.inverted() ); + geom.transform( context.renderContext().coordinateTransform(), QgsCoordinateTransform::ReverseTransform ); + + f.setGeometry( geom ); + + delete expressionContext.popScope(); + break; + } + } QgsExpressionContextScope *subSymbolExpressionContextScope = mSymbol->symbolRenderContext()->expressionContextScope(); diff --git a/src/core/symbology/qgsgeometrygeneratorsymbollayer.h b/src/core/symbology/qgsgeometrygeneratorsymbollayer.h index 80f4e67932a4..5cc3174999ac 100644 --- a/src/core/symbology/qgsgeometrygeneratorsymbollayer.h +++ b/src/core/symbology/qgsgeometrygeneratorsymbollayer.h @@ -76,6 +76,28 @@ class CORE_EXPORT QgsGeometryGeneratorSymbolLayer : public QgsSymbolLayer */ QString geometryExpression() const { return mExpression->expression(); } + /** + * Returns the unit for the geometry expression. + * + * By default this is QgsUnitTypes::MapUnits, which means that the geometryExpression() + * will return geometries in the associated layer's CRS. + * + * \see setUnits() + * \since QGIS 3.22 + */ + QgsUnitTypes::RenderUnit units() const { return mUnits; } + + /** + * Sets the \a units for the geometry expression. + * + * By default this is QgsUnitTypes::MapUnits, which means that the geometryExpression() + * will return geometries in the associated layer's CRS. + * + * \see units() + * \since QGIS 3.22 + */ + void setUnits( QgsUnitTypes::RenderUnit units ) { mUnits = units;} + QgsSymbol *subSymbol() override { return mSymbol; } bool setSubSymbol( QgsSymbol *symbol SIP_TRANSFER ) override; @@ -104,6 +126,7 @@ class CORE_EXPORT QgsGeometryGeneratorSymbolLayer : public QgsSymbolLayer void setColor( const QColor &color ) override; + private: QgsGeometryGeneratorSymbolLayer( const QString &expression ); @@ -122,6 +145,8 @@ class CORE_EXPORT QgsGeometryGeneratorSymbolLayer : public QgsSymbolLayer */ Qgis::SymbolType mSymbolType; + QgsUnitTypes::RenderUnit mUnits = QgsUnitTypes::RenderMapUnits; + bool mRenderingFeature = false; bool mHasRenderedFeature = false; }; diff --git a/src/gui/qgsunitselectionwidget.cpp b/src/gui/qgsunitselectionwidget.cpp index f439d46b8ce1..4c077dafb72a 100644 --- a/src/gui/qgsunitselectionwidget.cpp +++ b/src/gui/qgsunitselectionwidget.cpp @@ -252,11 +252,11 @@ void QgsUnitSelectionWidget::toggleUnitRangeButton() { if ( unit() != QgsUnitTypes::RenderUnknownUnit ) { - mMapScaleButton->setVisible( unit() == QgsUnitTypes::RenderMapUnits ); + mMapScaleButton->setVisible( mShowMapScaleButton && unit() == QgsUnitTypes::RenderMapUnits ); } else { - mMapScaleButton->setVisible( mMapUnitIdx != -1 && mUnitCombo->currentIndex() == mMapUnitIdx ); + mMapScaleButton->setVisible( mShowMapScaleButton && mMapUnitIdx != -1 && mUnitCombo->currentIndex() == mMapUnitIdx ); } } @@ -266,6 +266,18 @@ void QgsUnitSelectionWidget::widgetChanged( const QgsMapUnitScale &scale ) emit changed(); } +bool QgsUnitSelectionWidget::showMapScaleButton() const +{ + return mShowMapScaleButton; +} + +void QgsUnitSelectionWidget::setShowMapScaleButton( bool show ) +{ + mShowMapScaleButton = show; + if ( !show ) + mMapScaleButton->hide(); +} + QgsMapUnitScaleDialog::QgsMapUnitScaleDialog( QWidget *parent ) : QDialog( parent ) diff --git a/src/gui/qgsunitselectionwidget.h b/src/gui/qgsunitselectionwidget.h index a00f1f92960c..1d8dfabe9402 100644 --- a/src/gui/qgsunitselectionwidget.h +++ b/src/gui/qgsunitselectionwidget.h @@ -225,6 +225,22 @@ class GUI_EXPORT QgsUnitSelectionWidget : public QWidget, private Ui::QgsUnitSel */ void setMapCanvas( QgsMapCanvas *canvas ); + /** + * Returns TRUE if the widget can show the map scale button when the Map Units option is selected. + * + * \see setShowMapScaleButton() + * \since QGIS 3.22 + */ + bool showMapScaleButton() const; + + /** + * Sets whether the widget can show the map scale button when the Map Units option is selected. + * + * \see showMapScaleButton() + * \since QGIS 3.22 + */ + void setShowMapScaleButton( bool show ); + signals: void changed(); @@ -237,6 +253,7 @@ class GUI_EXPORT QgsUnitSelectionWidget : public QWidget, private Ui::QgsUnitSel QgsMapUnitScale mMapUnitScale; int mMapUnitIdx; QgsMapCanvas *mCanvas = nullptr; + bool mShowMapScaleButton = true; }; diff --git a/src/gui/symbology/qgssymbollayerwidget.cpp b/src/gui/symbology/qgssymbollayerwidget.cpp index bbe4a5d7b115..e36798016ead 100644 --- a/src/gui/symbology/qgssymbollayerwidget.cpp +++ b/src/gui/symbology/qgssymbollayerwidget.cpp @@ -4288,15 +4288,35 @@ QgsGeometryGeneratorSymbolLayerWidget::QgsGeometryGeneratorSymbolLayerWidget( Qg cbxGeometryType->addItem( QgsIconUtils::iconPolygon(), tr( "Polygon / MultiPolygon" ), static_cast< int >( Qgis::SymbolType::Fill ) ); cbxGeometryType->addItem( QgsIconUtils::iconLine(), tr( "LineString / MultiLineString" ), static_cast< int >( Qgis::SymbolType::Line ) ); cbxGeometryType->addItem( QgsIconUtils::iconPoint(), tr( "Point / MultiPoint" ), static_cast< int >( Qgis::SymbolType::Marker ) ); + + mUnitWidget->setUnits( {QgsUnitTypes::RenderMillimeters, + QgsUnitTypes::RenderPoints, + QgsUnitTypes::RenderPixels, + QgsUnitTypes::RenderInches, + QgsUnitTypes::RenderMapUnits + } ); + mUnitWidget->setShowMapScaleButton( false ); + connect( modificationExpressionSelector, &QgsExpressionLineEdit::expressionChanged, this, &QgsGeometryGeneratorSymbolLayerWidget::updateExpression ); connect( cbxGeometryType, static_cast( &QComboBox::currentIndexChanged ), this, &QgsGeometryGeneratorSymbolLayerWidget::updateSymbolType ); + connect( mUnitWidget, &QgsUnitSelectionWidget::changed, this, [ = ] + { + if ( !mBlockSignals ) + { + mLayer->setUnits( mUnitWidget->unit() ); + emit symbolChanged(); + } + } ); } void QgsGeometryGeneratorSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *l ) { + mBlockSignals++; mLayer = static_cast( l ); modificationExpressionSelector->setExpression( mLayer->geometryExpression() ); cbxGeometryType->setCurrentIndex( cbxGeometryType->findData( static_cast< int >( mLayer->symbolType() ) ) ); + mUnitWidget->setUnit( mLayer->units() ); + mBlockSignals--; } QgsSymbolLayer *QgsGeometryGeneratorSymbolLayerWidget::symbolLayer() diff --git a/src/gui/symbology/qgssymbollayerwidget.h b/src/gui/symbology/qgssymbollayerwidget.h index 270d046f9d61..cfb97d70a909 100644 --- a/src/gui/symbology/qgssymbollayerwidget.h +++ b/src/gui/symbology/qgssymbollayerwidget.h @@ -1138,6 +1138,7 @@ class GUI_EXPORT QgsGeometryGeneratorSymbolLayerWidget : public QgsSymbolLayerWi private: QgsGeometryGeneratorSymbolLayer *mLayer = nullptr; + int mBlockSignals = 0; private slots: void updateExpression( const QString &string ); diff --git a/src/ui/symbollayer/qgsgeometrygeneratorwidgetbase.ui b/src/ui/symbollayer/qgsgeometrygeneratorwidgetbase.ui index c64b81dc8054..ef82faae108d 100644 --- a/src/ui/symbollayer/qgsgeometrygeneratorwidgetbase.ui +++ b/src/ui/symbollayer/qgsgeometrygeneratorwidgetbase.ui @@ -13,12 +13,16 @@ Form - - + + - - + + + + Units + + @@ -33,6 +37,22 @@ + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + @@ -42,6 +62,12 @@
qgsexpressionlineedit.h
1 + + QgsUnitSelectionWidget + QWidget +
qgsunitselectionwidget.h
+ 1 +