[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
This commit is contained in:
Nyall Dawson 2021-10-31 13:58:49 +10:00
parent 80ed176035
commit c36a042828
3 changed files with 36 additions and 25 deletions

View File

@ -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"}]

View File

@ -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<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" ) )

View File

@ -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)" );