From 679797e22bcc3d29840248c9ec73fc2911c6cb40 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 3 Sep 2016 21:52:06 +1000 Subject: [PATCH] [FEATURE] New simplify + smoothing expression functions Exposes simplification and smoothing algorithms to expression engine, via: - simplify(): applies Douglas-Peucker geometry simplification - simplify_vw(): applies Visvalingam-Whyatt geometry simplification - smooth(): smoothes a geometry Carto tip: use smooth along with geometry generators to minimise the typical "GIS" noded look of rendered geometries! --- resources/function_help/json/simplify | 9 ++++ resources/function_help/json/simplify_vw | 9 ++++ resources/function_help/json/smooth | 12 +++++ src/core/qgsexpression.cpp | 61 ++++++++++++++++++++++++ tests/src/core/testqgsexpression.cpp | 12 +++++ 5 files changed, 103 insertions(+) create mode 100644 resources/function_help/json/simplify create mode 100644 resources/function_help/json/simplify_vw create mode 100644 resources/function_help/json/smooth diff --git a/resources/function_help/json/simplify b/resources/function_help/json/simplify new file mode 100644 index 000000000000..9f1eacdb228e --- /dev/null +++ b/resources/function_help/json/simplify @@ -0,0 +1,9 @@ +{ + "name": "simplify", + "type": "function", + "description":"Simplifies a geometry by removing nodes using a distance based threshold (ie, the Douglas Peucker algorithm). The algorithm preserves large deviations in geometries and reduces the number of vertices in nearly straight segments.", + "arguments": [ {"arg":"geometry","description":"a geometry"}, + {"arg":"tolerance","description":"maximum deviation from straight segments for points to be removed"} ], + "examples": [ { "expression":"geom_to_wkt(simplify(geometry:=geom_from_wkt('LineString(0 0, 5 0.1, 10 0)'),tolerance:=5))", "returns":"'LineString(0 0, 10 0)'"}] +} + diff --git a/resources/function_help/json/simplify_vw b/resources/function_help/json/simplify_vw new file mode 100644 index 000000000000..ad4f84b9009a --- /dev/null +++ b/resources/function_help/json/simplify_vw @@ -0,0 +1,9 @@ +{ + "name": "simplify_vw", + "type": "function", + "description":"Simplifies a geometry by removing nodes using an area based threshold (ie, the Visvalingam-Whyatt algorithm). The algorithm removes vertices which create small areas in geometries, eg narrow spikes or nearly straight segments.", + "arguments": [ {"arg":"geometry","description":"a geometry"}, + {"arg":"tolerance","description":"a measure of the maximum area created by a node for the node to be removed"} ], + "examples": [ { "expression":"geom_to_wkt(simplify_vw(geometry:=geom_from_wkt('LineString(0 0, 5 0, 5.01 10, 5.02 0, 10 0)'),tolerance:=5))", "returns":"'LineString(0 0, 10 0)'"}] +} + diff --git a/resources/function_help/json/smooth b/resources/function_help/json/smooth new file mode 100644 index 000000000000..fef1ec7093a8 --- /dev/null +++ b/resources/function_help/json/smooth @@ -0,0 +1,12 @@ +{ + "name": "smooth", + "type": "function", + "description":"Smooths a geometry by adding extra nodes which round off corners in the geometry.", + "arguments": [ {"arg":"geometry","description":"a geometry"}, + {"arg":"iterations", "optional":true,"description":"number of smoothing iterations to apply. Larger numbers result in smoother but more complex geometries."}, + {"arg":"offset", "optional":true,"description":"value between 0 and 0.5 which controls how tightly the smoothed geometry follow the original geometry. Smaller values result in a tighter smoothing, larger values result in looser smoothing."}, + {"arg":"min_length", "optional":true,"description":"minimum length of segments to apply smoothing to. This parameter can be used to avoid placing excessive additional nodes in shorter segments of the geometry."}, + {"arg":"max_angle", "optional":true,"description":"maximum angle at node for smoothing to be applied (0-180). By lowering the maximum angle intentionally sharp corners in the geometry can be preserved. For instance, a value of 80 degrees will retain right angles in the geometry."} ], + "examples": [ { "expression":"geom_to_wkt(smooth(geometry:=geom_from_wkt('LineString(0 0, 5 0, 5 5)'),iterations:=1,offset:=0.2,min_length:=-1,max_angle:=180))", "returns":"'LineString (0 0, 4 0, 5 1, 5 5)'"}] +} + diff --git a/src/core/qgsexpression.cpp b/src/core/qgsexpression.cpp index ace1be02656e..a18f9bc4ccf6 100644 --- a/src/core/qgsexpression.cpp +++ b/src/core/qgsexpression.cpp @@ -51,6 +51,7 @@ #include "qgscurvepolygon.h" #include "qgsexpressionprivate.h" #include "qgsexpressionsorter.h" +#include "qgsmaptopixelgeometrysimplifier.h" #if QT_VERSION < 0x050000 #include @@ -1767,6 +1768,59 @@ static QVariant fcnLineMerge( const QVariantList& values, const QgsExpressionCon return QVariant::fromValue( merged ); } +static QVariant fcnSimplify( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent ) +{ + QgsGeometry geom = getGeometry( values.at( 0 ), parent ); + + if ( geom.isEmpty() ) + return QVariant(); + + double tolerance = getDoubleValue( values.at( 1 ), parent ); + + QgsGeometry simplified = geom.simplify( tolerance ); + if ( simplified.isEmpty() ) + return QVariant(); + + return simplified; +} + +static QVariant fcnSimplifyVW( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent ) +{ + QgsGeometry geom = getGeometry( values.at( 0 ), parent ); + + if ( geom.isEmpty() ) + return QVariant(); + + double tolerance = getDoubleValue( values.at( 1 ), parent ); + + QgsMapToPixelSimplifier simplifier( QgsMapToPixelSimplifier::SimplifyGeometry, tolerance, QgsMapToPixelSimplifier::Visvalingam ); + + QgsGeometry simplified = simplifier.simplify( geom ); + if ( simplified.isEmpty() ) + return QVariant(); + + return simplified; +} + +static QVariant fcnSmooth( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent ) +{ + QgsGeometry geom = getGeometry( values.at( 0 ), parent ); + + if ( geom.isEmpty() ) + return QVariant(); + + int iterations = qMin( getIntValue( values.at( 1 ), parent ), 10 ); + double offset = qBound( 0.0, getDoubleValue( values.at( 2 ), parent ), 0.5 ); + double minLength = getDoubleValue( values.at( 3 ), parent ); + double maxAngle = qBound( 0.0, getDoubleValue( values.at( 4 ), parent ), 180.0 ); + + QgsGeometry smoothed = geom.smooth( iterations, offset, minLength, maxAngle ); + if ( smoothed.isEmpty() ) + return QVariant(); + + return smoothed; +} + static QVariant fcnMakePoint( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent ) { if ( values.count() < 2 || values.count() > 4 ) @@ -3166,6 +3220,7 @@ const QStringList& QgsExpression::BuiltinFunctions() << "disjoint" << "intersects" << "touches" << "crosses" << "contains" << "relate" << "overlaps" << "within" << "buffer" << "offset_curve" << "single_sided_buffer" + << "simplify" << "simplify_vw" << "smooth" << "centroid" << "bounds" << "reverse" << "exterior_ring" << "boundary" << "line_merge" << "bounds_width" << "bounds_height" << "is_closed" << "convex_hull" << "difference" @@ -3377,6 +3432,12 @@ const QList& QgsExpression::Functions() << new StaticFunction( "boundary", ParameterList() << Parameter( "geometry" ), fcnBoundary, "GeometryGroup" ) << new StaticFunction( "line_merge", ParameterList() << Parameter( "geometry" ), fcnLineMerge, "GeometryGroup" ) << new StaticFunction( "bounds", 1, fcnBounds, "GeometryGroup" ) + << new StaticFunction( "simplify", ParameterList() << Parameter( "geometry" ) << Parameter( "tolerance" ), fcnSimplify, "GeometryGroup" ) + << new StaticFunction( "simplify_vw", ParameterList() << Parameter( "geometry" ) << Parameter( "tolerance" ), fcnSimplifyVW, "GeometryGroup" ) + << new StaticFunction( "smooth", ParameterList() << Parameter( "geometry" ) << Parameter( "iterations", true, 1 ) + << Parameter( "offset", true, 0.25 ) + << Parameter( "min_length", true, -1 ) + << Parameter( "max_angle", true, 180 ), fcnSmooth, "GeometryGroup" ) << new StaticFunction( "num_points", 1, fcnGeomNumPoints, "GeometryGroup" ) << new StaticFunction( "num_interior_rings", 1, fcnGeomNumInteriorRings, "GeometryGroup" ) << new StaticFunction( "num_rings", 1, fcnGeomNumRings, "GeometryGroup" ) diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index dc773adc1413..a9da5c697505 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -798,6 +798,18 @@ class TestQgsExpression: public QObject QTest::newRow( "distance_to_vertex null" ) << "distance_to_vertex(NULL, 0)" << false << QVariant(); QTest::newRow( "distance_to_vertex point" ) << "distance_to_vertex(geom_from_wkt('POINT(1 2)'),0)" << false << QVariant( 0.0 ); QTest::newRow( "distance_to_vertex line" ) << "distance_to_vertex(geometry:=geom_from_wkt('LineString(0 0, 10 0, 10 10)'),vertex:=1)" << false << QVariant( 10.0 ); + QTest::newRow( "simplify not geom" ) << "simplify('g',5)" << true << QVariant(); + QTest::newRow( "simplify null" ) << "simplify(NULL,5)" << false << QVariant(); + QTest::newRow( "simplify point" ) << "geom_to_wkt(simplify(geom_from_wkt('POINT(1 2)'),10))" << false << QVariant( "Point (1 2)" ); + QTest::newRow( "simplify line" ) << "geom_to_wkt(simplify(geometry:=geom_from_wkt('LineString(0 0, 5 0, 10 0)'),tolerance:=5))" << false << QVariant( "LineString (0 0, 10 0)" ); + QTest::newRow( "simplify_vw not geom" ) << "simplify_vw('g',5)" << true << QVariant(); + QTest::newRow( "simplify_vw null" ) << "simplify_vw(NULL,5)" << false << QVariant(); + QTest::newRow( "simplify_vw point" ) << "geom_to_wkt(simplify_vw(geom_from_wkt('POINT(1 2)'),10))" << false << QVariant( "Point (1 2)" ); + QTest::newRow( "simplify_vw line" ) << "geom_to_wkt(simplify_vw(geometry:=geom_from_wkt('LineString(0 0, 5 0, 5.01 10, 5.02 0, 10 0)'),tolerance:=5))" << false << QVariant( "LineString (0 0, 10 0)" ); + QTest::newRow( "smooth not geom" ) << "smooth('g',5)" << true << QVariant(); + QTest::newRow( "smooth null" ) << "smooth(NULL,5)" << false << QVariant(); + QTest::newRow( "smooth point" ) << "geom_to_wkt(smooth(geom_from_wkt('POINT(1 2)'),10))" << false << QVariant( "Point (1 2)" ); + QTest::newRow( "smooth line" ) << "geom_to_wkt(smooth(geometry:=geom_from_wkt('LineString(0 0, 5 0, 5 5)'),iterations:=1,offset:=0.2,min_length:=-1,max_angle:=180))" << false << QVariant( "LineString (0 0, 4 0, 5 1, 5 5)" ); // string functions QTest::newRow( "lower" ) << "lower('HeLLo')" << false << QVariant( "hello" );