From c36a04282852388f9c25bfa75160c7c290478b91 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 31 Oct 2021 13:58:49 +1000 Subject: [PATCH] [feature][expression] Add a "per_part" option to "rotate" expression function If set to true and no explicit rotation center point is specified (ie rotation happens around the geometry's center), then the rotation is applied around the center of each part individually instead of the geometry as a whole. This is designed mostly to aid cases when rotate is used as a cartographic tool as part of a geometry generator symbol layer. Sponsored by North Road, thanks to SLYR --- resources/function_help/json/rotate | 3 +- src/core/expression/qgsexpressionfunction.cpp | 56 +++++++++++-------- tests/src/core/testqgsexpression.cpp | 2 + 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/resources/function_help/json/rotate b/resources/function_help/json/rotate index e90b4d2377a..04fdaeb7947 100644 --- a/resources/function_help/json/rotate +++ b/resources/function_help/json/rotate @@ -5,7 +5,8 @@ "description": "Returns a rotated version of a geometry. Calculations are in the Spatial Reference System of this geometry.", "arguments": [ {"arg":"geometry","description":"a geometry"}, {"arg":"rotation","description":"clockwise rotation in degrees"}, - {"arg":"center", "optional":true,"description":"rotation center point. If not specified, the center of the geometry's bounding box is used."} + {"arg":"center", "optional":true,"default":"NULL", "description":"rotation center point. If not specified, the center of the geometry's bounding box is used."}, + {"arg":"per_part", "optional":true, "default":"false","description": "apply rotation per part. If true, then rotation will apply around the center of each part's bounding box when the input geometry is multipart and an explicit rotation center point is not specified."} ], "examples": [ { "expression":"rotate($geometry, 45, make_point(4, 5))", "returns":"geometry rotated 45 degrees clockwise around the (4, 5) point"}, { "expression":"rotate($geometry, 45)", "returns":"geometry rotated 45 degrees clockwise around the center of its bounding box"}] diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index b0b5280ac15..1a6270c4eb4 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -4194,38 +4194,45 @@ static QVariant fcnRotate( const QVariantList &values, const QgsExpressionContex const double rotation = QgsExpressionUtils::getDoubleValue( values.at( 1 ), parent ); const QgsGeometry center = values.at( 2 ).isValid() ? QgsExpressionUtils::getGeometry( values.at( 2 ), parent ) : QgsGeometry(); + const bool perPart = values.value( 3 ).toBool(); - QgsPointXY pt; - if ( center.isNull() ) + if ( center.isNull() && perPart && fGeom.isMultipart() ) { - // if center wasn't specified, use bounding box centroid - pt = fGeom.boundingBox().center(); - } - else if ( center.type() != QgsWkbTypes::PointGeometry ) - { - parent->setEvalErrorString( QObject::tr( "Function 'rotate' requires a point value for the center" ) ); - return QVariant(); - } - else if ( center.isMultipart() ) - { - QgsMultiPointXY multiPoint = center.asMultiPoint(); - if ( multiPoint.count() == 1 ) + // no explicit center, rotating per part + // (note that we only do this branch for multipart geometries -- for singlepart geometries + // the result is equivalent to setting perPart as false anyway) + std::unique_ptr< QgsGeometryCollection > collection( qgsgeometry_cast< QgsGeometryCollection * >( fGeom.constGet()->clone() ) ); + for ( auto it = collection->parts_begin(); it != collection->parts_end(); ++it ) { - pt = multiPoint[0]; + const QgsPointXY partCenter = ( *it )->boundingBox().center(); + QTransform t = QTransform::fromTranslate( partCenter.x(), partCenter.y() ); + t.rotate( -rotation ); + t.translate( -partCenter.x(), -partCenter.y() ); + ( *it )->transform( t ); } - else + return QVariant::fromValue( QgsGeometry( std::move( collection ) ) ); + } + else + { + QgsPointXY pt; + if ( center.isEmpty() ) + { + // if center wasn't specified, use bounding box centroid + pt = fGeom.boundingBox().center(); + } + else if ( QgsWkbTypes::flatType( center.constGet()->simplifiedTypeRef()->wkbType() ) != QgsWkbTypes::Point ) { parent->setEvalErrorString( QObject::tr( "Function 'rotate' requires a point value for the center" ) ); return QVariant(); } - } - else - { - pt = center.asPoint(); - } + else + { + pt = QgsPointXY( *qgsgeometry_cast< const QgsPoint * >( center.constGet()->simplifiedTypeRef() ) ); + } - fGeom.rotate( rotation, pt ); - return QVariant::fromValue( fGeom ); + fGeom.rotate( rotation, pt ); + return QVariant::fromValue( fGeom ); + } } static QVariant fcnScale( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * ) @@ -7263,7 +7270,8 @@ const QList &QgsExpression::Functions() fcnTranslate, QStringLiteral( "GeometryGroup" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "rotate" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "rotation" ) ) - << QgsExpressionFunction::Parameter( QStringLiteral( "center" ), true ), + << QgsExpressionFunction::Parameter( QStringLiteral( "center" ), true ) + << QgsExpressionFunction::Parameter( QStringLiteral( "per_part" ), true, false ), fcnRotate, QStringLiteral( "GeometryGroup" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "scale" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "x_scale" ) ) diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 244b16d1b54..5e27c806277 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1323,6 +1323,8 @@ class TestQgsExpression: public QObject QTest::newRow( "rotate line fixed multi point" ) << "geom_to_wkt(rotate(geom_from_wkt('LineString(0 0, 10 0, 10 10)'),90, geom_from_wkt('MULTIPOINT((-5 -3))')))" << false << QVariant( "LineString (-2 -8, -2 -18, 8 -18)" ); QTest::newRow( "rotate line fixed multi point multiple" ) << "geom_to_wkt(rotate(geom_from_wkt('LineString(0 0, 10 0, 10 10)'),90, geom_from_wkt('MULTIPOINT(-5 -3,1 2)')))" << true << QVariant(); QTest::newRow( "rotate polygon centroid" ) << "geom_to_wkt(rotate(geom_from_wkt('Polygon((0 0, 10 0, 10 10, 0 0))'),-90))" << false << QVariant( "Polygon ((10 0, 10 10, 0 10, 10 0))" ); + QTest::newRow( "rotate multiline centroid, not per part" ) << "geom_to_wkt(rotate(geom_from_wkt('MultiLineString((0 0, 10 0, 10 10), (12 0, 12 12))'),90))" << false << QVariant( "MultiLineString ((0 12, 0 2, 10 2),(0 0, 12 0))" ); + QTest::newRow( "rotate multiline centroid, per part" ) << "geom_to_wkt(rotate(geom_from_wkt('MultiLineString((0 0, 10 0, 10 10), (12 0, 12 12))'),90, per_part:=true))" << false << QVariant( "MultiLineString ((0 10, 0 0, 10 0),(6 6, 18 6))" ); QTest::newRow( "scale not geom" ) << "scale('g', 1.2, 0.8)" << true << QVariant(); QTest::newRow( "scale null" ) << "scale(NULL, 1.2, 0.8)" << false << QVariant(); QTest::newRow( "scale point" ) << "geom_to_wkt(scale(geom_from_wkt('POINT( 20 10)'), 1.2, 0.8, geom_from_wkt('POINT( 30 15)')))" << false << QVariant( "Point (18 11)" );