Skip to content
Permalink
Browse files

[FEATURE] API + expression function for merging linestrings

Adds a new method to QgsGeometry for merging linestrings.
By passing a multilinestring, any connected lines will
be joined into single linestrings. Behind the scenes this
uses GEOS' line merge.

A corresponding expression function "line_merge" has also
been added.

(cherry-picked from 10c9239)
  • Loading branch information
nyalldawson committed Aug 9, 2016
1 parent 59895a8 commit 63f946ec79dd15388d4e5c0377486c93d0ef021d
@@ -485,6 +485,15 @@ class QgsGeometry
*/
QgsGeometry* combine( const QgsGeometry* geometry ) const /Factory/;

/** Merges any connected lines in a LineString/MultiLineString geometry and
* converts them to single line strings.
* @returns a LineString or MultiLineString geometry, with any connected lines
* joined. An empty geometry will be returned if the input geometry was not a
* MultiLineString geometry.
* @note added in QGIS 3.0
*/
QgsGeometry mergeLines() const;

/** Returns a geometry representing the points making up this geometry that do not make up other. */
QgsGeometry* difference( const QgsGeometry* geometry ) const /Factory/;

@@ -0,0 +1,9 @@
{
"name": "line_merge",
"type": "function",
"description":"Returns a LineString or MultiLineString geometry, where any connected LineStrings from the input geometry have been merged into a single linestring. This function will return null if passed a geometry which is not a LineString/MultiLineString.",
"arguments": [ {"arg":"geometry","description":"a LineString/MultiLineString geometry"} ],
"examples": [ { "expression":"geom_to_wkt(line_merge(geom_from_wkt('MULTILINESTRING((0 0, 1 1),(1 1, 2 2))')))", "returns":"'LineString(0 0,1 1,2 2)'"},
{ "expression":"geom_to_wkt(line_merge(geom_from_wkt('MULTILINESTRING((0 0, 1 1),(11 1, 21 2))')))", "returns":"'MultiLineString((0 0, 1 1),(11 1, 21 2)'"}]
}

@@ -1421,6 +1421,23 @@ QgsGeometry* QgsGeometry::combine( const QgsGeometry* geometry ) const
return new QgsGeometry( resultGeom );
}

QgsGeometry QgsGeometry::mergeLines() const
{
if ( !d->geometry )
{
return QgsGeometry();
}

if ( QgsWKBTypes::flatType( d->geometry->wkbType() ) == QgsWKBTypes::LineString )
{
// special case - a single linestring was passed
return QgsGeometry( *this );
}

QgsGeos geos( d->geometry );
return geos.mergeLines();
}

QgsGeometry* QgsGeometry::difference( const QgsGeometry* geometry ) const
{
if ( !d->geometry || !geometry->d->geometry )
@@ -527,6 +527,15 @@ class CORE_EXPORT QgsGeometry
*/
QgsGeometry* combine( const QgsGeometry* geometry ) const;

/** Merges any connected lines in a LineString/MultiLineString geometry and
* converts them to single line strings.
* @returns a LineString or MultiLineString geometry, with any connected lines
* joined. An empty geometry will be returned if the input geometry was not a
* MultiLineString geometry.
* @note added in QGIS 3.0
*/
QgsGeometry mergeLines() const;

/** Returns a geometry representing the points making up this geometry that do not make up other. */
QgsGeometry* difference( const QgsGeometry* geometry ) const;

@@ -1794,6 +1794,25 @@ QgsAbstractGeometryV2* QgsGeos::reshapeGeometry( const QgsLineStringV2& reshapeW
}
}

QgsGeometry QgsGeos::mergeLines( QString* errorMsg ) const
{
if ( !mGeos )
{
return QgsGeometry();
}

if ( GEOSGeomTypeId_r( geosinit.ctxt, mGeos ) != GEOS_MULTILINESTRING )
return QgsGeometry();

GEOSGeomScopedPtr geos;
try
{
geos.reset( GEOSLineMerge_r( geosinit.ctxt, mGeos ) );
}
CATCH_GEOS_WITH_ERRMSG( QgsGeometry() );
return QgsGeometry( fromGeos( geos.get() ) );
}

QgsGeometry QgsGeos::closestPoint( const QgsGeometry& other, QString* errorMsg ) const
{
if ( !mGeos || other.isEmpty() )
@@ -88,6 +88,16 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
QgsAbstractGeometryV2* offsetCurve( double distance, int segments, int joinStyle, double mitreLimit, QString* errorMsg = nullptr ) const override;
QgsAbstractGeometryV2* reshapeGeometry( const QgsLineStringV2& reshapeWithLine, int* errorCode, QString* errorMsg = nullptr ) const;

/** Merges any connected lines in a LineString/MultiLineString geometry and
* converts them to single line strings.
* @param errorMsg if specified, will be set to any reported GEOS errors
* @returns a LineString or MultiLineString geometry, with any connected lines
* joined. An empty geometry will be returned if the input geometry was not a
* LineString/MultiLineString geometry.
* @note added in QGIS 3.0
*/
QgsGeometry mergeLines( QString* errorMsg = nullptr ) const;

/** Returns the closest point on the geometry to the other geometry.
* @note added in QGIS 2.14
* @see shortestLine()
@@ -1824,6 +1824,20 @@ static QVariant fcnGeometryN( const QVariantList& values, const QgsExpressionCon
return result;
}

static QVariant fcnLineMerge( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent )
{
QgsGeometry geom = getGeometry( values.at( 0 ), parent );

if ( geom.isEmpty() )
return QVariant();

QgsGeometry merged = geom.mergeLines();
if ( merged.isEmpty() )
return QVariant();

return QVariant::fromValue( merged );
}

static QVariant fcnMakePoint( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent )
{
if ( values.count() < 2 || values.count() > 4 )
@@ -3172,6 +3186,7 @@ const QStringList& QgsExpression::BuiltinFunctions()
<< "disjoint" << "intersects" << "touches" << "crosses" << "contains"
<< "relate"
<< "overlaps" << "within" << "buffer" << "centroid" << "bounds" << "reverse" << "exterior_ring"
<< "line_merge"
<< "bounds_width" << "bounds_height" << "is_closed" << "convex_hull" << "difference"
<< "distance" << "intersection" << "sym_difference" << "combine"
<< "extrude" << "azimuth" << "project" << "closest_point" << "shortest_line"
@@ -3363,6 +3378,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
<< new StaticFunction( "exterior_ring", 1, fcnExteriorRing, "GeometryGroup" )
<< new StaticFunction( "interior_ring_n", 2, fcnInteriorRingN, "GeometryGroup" )
<< new StaticFunction( "geometry_n", 2, fcnGeometryN, "GeometryGroup" )
<< new StaticFunction( "line_merge", ParameterList() << Parameter( "geometry" ), fcnLineMerge, "GeometryGroup" )
<< new StaticFunction( "bounds", 1, fcnBounds, "GeometryGroup" )
<< new StaticFunction( "num_points", 1, fcnGeomNumPoints, "GeometryGroup" )
<< new StaticFunction( "num_interior_rings", 1, fcnGeomNumInteriorRings, "GeometryGroup" )
@@ -665,6 +665,11 @@ class TestQgsExpression: public QObject
QTest::newRow( "geometry_n collection" ) << "geom_to_wkt(geometry_n(geom_from_wkt('GEOMETRYCOLLECTION(POINT(0 1), POINT(0 0), POINT(1 0), POINT(1 1))'),3))" << false << QVariant( QString( "Point (1 0)" ) );
QTest::newRow( "geometry_n collection bad index 1" ) << "geometry_n(geom_from_wkt('GEOMETRYCOLLECTION(POINT(0 1), POINT(0 0), POINT(1 0), POINT(1 1))'),0)" << false << QVariant();
QTest::newRow( "geometry_n collection bad index 2" ) << "geometry_n(geom_from_wkt('GEOMETRYCOLLECTION(POINT(0 1), POINT(0 0), POINT(1 0), POINT(1 1))'),5)" << false << QVariant();
QTest::newRow( "line_merge not geom" ) << "line_merge('g')" << true << QVariant();
QTest::newRow( "line_merge null" ) << "line_merge(NULL)" << false << QVariant();
QTest::newRow( "line_merge point" ) << "line_merge(geom_from_wkt('POINT(1 2)'))" << false << QVariant();
QTest::newRow( "line_merge line" ) << "geom_to_wkt(line_merge(geometry:=geom_from_wkt('LineString(0 0, 10 10)')))" << false << QVariant( "LineString (0 0, 10 10)" );
QTest::newRow( "line_merge multiline" ) << "geom_to_wkt(line_merge(geom_from_wkt('MultiLineString((0 0, 10 10),(10 10, 20 20))')))" << false << QVariant( "LineString (0 0, 10 10, 20 20)" );
QTest::newRow( "start_point point" ) << "geom_to_wkt(start_point(geom_from_wkt('POINT(2 0)')))" << false << QVariant( "Point (2 0)" );
QTest::newRow( "start_point multipoint" ) << "geom_to_wkt(start_point(geom_from_wkt('MULTIPOINT((3 3), (1 1), (2 2))')))" << false << QVariant( "Point (3 3)" );
QTest::newRow( "start_point line" ) << "geom_to_wkt(start_point(geom_from_wkt('LINESTRING(4 1, 1 1, 2 2)')))" << false << QVariant( "Point (4 1)" );
@@ -3360,5 +3360,35 @@ def testMisc(self):
wkb2 = geom.asWkb()
self.assertEqual(wkb1, wkb2)

def testMergeLines(self):
""" test merging linestrings """

# not a (multi)linestring
geom = QgsGeometry.fromWkt('Point(1 2)')
result = geom.mergeLines()
self.assertTrue(result.isEmpty())

# linestring should be returned intact
geom = QgsGeometry.fromWkt('LineString(0 0, 10 10)')
result = geom.mergeLines().exportToWkt()
exp = 'LineString(0 0, 10 10)'
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))

# multilinestring
geom = QgsGeometry.fromWkt('MultiLineString((0 0, 10 10),(10 10, 20 20))')
result = geom.mergeLines().exportToWkt()
exp = 'LineString(0 0, 10 10, 20 20)'
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))

geom = QgsGeometry.fromWkt('MultiLineString((0 0, 10 10),(12 2, 14 4),(10 10, 20 20))')
result = geom.mergeLines().exportToWkt()
exp = 'MultiLineString((0 0, 10 10, 20 20),(12 2, 14 4))'
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))

geom = QgsGeometry.fromWkt('MultiLineString((0 0, 10 10),(12 2, 14 4))')
result = geom.mergeLines().exportToWkt()
exp = 'MultiLineString((0 0, 10 10),(12 2, 14 4))'
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))

if __name__ == '__main__':
unittest.main()

0 comments on commit 63f946e

Please sign in to comment.
You can’t perform that action at this time.