Skip to content
Permalink
Browse files
[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
  • Loading branch information
nyalldawson committed Oct 31, 2021
1 parent 80ed176 commit c36a04282852388f9c25bfa75160c7c290478b91
Showing with 34 additions and 23 deletions.
  1. +2 −1 resources/function_help/json/rotate
  2. +30 −22 src/core/expression/qgsexpressionfunction.cpp
  3. +2 −0 tests/src/core/testqgsexpression.cpp
@@ -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"}]
@@ -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 wasn't specified, use bounding box centroid
pt = fGeom.boundingBox().center();
}
else if ( center.type() != QgsWkbTypes::PointGeometry )
if ( center.isNull() && perPart && fGeom.isMultipart() )
{
parent->setEvalErrorString( QObject::tr( "Function 'rotate' requires a point value for the center" ) );
return QVariant();
// 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 )
{
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 );
}
return QVariant::fromValue( QgsGeometry( std::move( collection ) ) );
}
else if ( center.isMultipart() )
else
{
QgsMultiPointXY multiPoint = center.asMultiPoint();
if ( multiPoint.count() == 1 )
QgsPointXY pt;
if ( center.isEmpty() )
{
pt = multiPoint[0];
// if center wasn't specified, use bounding box centroid
pt = fGeom.boundingBox().center();
}
else
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<QgsExpressionFunction *> &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" ) )
@@ -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)" );

0 comments on commit c36a042

Please sign in to comment.