Skip to content

Commit 10c9239

Browse files
committedJul 28, 2016
[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.
1 parent 27697e6 commit 10c9239

File tree

9 files changed

+124
-1
lines changed

9 files changed

+124
-1
lines changed
 

‎python/core/geometry/qgsgeometry.sip

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,15 @@ class QgsGeometry
485485
*/
486486
QgsGeometry* combine( const QgsGeometry* geometry ) const /Factory/;
487487

488+
/** Merges any connected lines in a LineString/MultiLineString geometry and
489+
* converts them to single line strings.
490+
* @returns a LineString or MultiLineString geometry, with any connected lines
491+
* joined. An empty geometry will be returned if the input geometry was not a
492+
* MultiLineString geometry.
493+
* @note added in QGIS 3.0
494+
*/
495+
QgsGeometry mergeLines() const;
496+
488497
/** Returns a geometry representing the points making up this geometry that do not make up other. */
489498
QgsGeometry* difference( const QgsGeometry* geometry ) const /Factory/;
490499

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "line_merge",
3+
"type": "function",
4+
"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.",
5+
"arguments": [ {"arg":"geometry","description":"a LineString/MultiLineString geometry"} ],
6+
"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)'"},
7+
{ "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)'"}]
8+
}
9+

‎src/core/geometry/qgsgeometry.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,23 @@ QgsGeometry* QgsGeometry::combine( const QgsGeometry* geometry ) const
14221422
return new QgsGeometry( resultGeom );
14231423
}
14241424

1425+
QgsGeometry QgsGeometry::mergeLines() const
1426+
{
1427+
if ( !d->geometry )
1428+
{
1429+
return QgsGeometry();
1430+
}
1431+
1432+
if ( QgsWKBTypes::flatType( d->geometry->wkbType() ) == QgsWKBTypes::LineString )
1433+
{
1434+
// special case - a single linestring was passed
1435+
return QgsGeometry( *this );
1436+
}
1437+
1438+
QgsGeos geos( d->geometry );
1439+
return geos.mergeLines();
1440+
}
1441+
14251442
QgsGeometry* QgsGeometry::difference( const QgsGeometry* geometry ) const
14261443
{
14271444
if ( !d->geometry || !geometry->d->geometry )

‎src/core/geometry/qgsgeometry.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,15 @@ class CORE_EXPORT QgsGeometry
528528
*/
529529
QgsGeometry* combine( const QgsGeometry* geometry ) const;
530530

531+
/** Merges any connected lines in a LineString/MultiLineString geometry and
532+
* converts them to single line strings.
533+
* @returns a LineString or MultiLineString geometry, with any connected lines
534+
* joined. An empty geometry will be returned if the input geometry was not a
535+
* MultiLineString geometry.
536+
* @note added in QGIS 3.0
537+
*/
538+
QgsGeometry mergeLines() const;
539+
531540
/** Returns a geometry representing the points making up this geometry that do not make up other. */
532541
QgsGeometry* difference( const QgsGeometry* geometry ) const;
533542

‎src/core/geometry/qgsgeos.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,25 @@ QgsAbstractGeometryV2* QgsGeos::reshapeGeometry( const QgsLineStringV2& reshapeW
17931793
}
17941794
}
17951795

1796+
QgsGeometry QgsGeos::mergeLines( QString* errorMsg ) const
1797+
{
1798+
if ( !mGeos )
1799+
{
1800+
return QgsGeometry();
1801+
}
1802+
1803+
if ( GEOSGeomTypeId_r( geosinit.ctxt, mGeos ) != GEOS_MULTILINESTRING )
1804+
return QgsGeometry();
1805+
1806+
GEOSGeomScopedPtr geos;
1807+
try
1808+
{
1809+
geos.reset( GEOSLineMerge_r( geosinit.ctxt, mGeos ) );
1810+
}
1811+
CATCH_GEOS_WITH_ERRMSG( QgsGeometry() );
1812+
return QgsGeometry( fromGeos( geos.get() ) );
1813+
}
1814+
17961815
QgsGeometry QgsGeos::closestPoint( const QgsGeometry& other, QString* errorMsg ) const
17971816
{
17981817
if ( !mGeos || other.isEmpty() )

‎src/core/geometry/qgsgeos.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
8787
QgsAbstractGeometryV2* offsetCurve( double distance, int segments, int joinStyle, double mitreLimit, QString* errorMsg = nullptr ) const override;
8888
QgsAbstractGeometryV2* reshapeGeometry( const QgsLineStringV2& reshapeWithLine, int* errorCode, QString* errorMsg = nullptr ) const;
8989

90+
/** Merges any connected lines in a LineString/MultiLineString geometry and
91+
* converts them to single line strings.
92+
* @param errorMsg if specified, will be set to any reported GEOS errors
93+
* @returns a LineString or MultiLineString geometry, with any connected lines
94+
* joined. An empty geometry will be returned if the input geometry was not a
95+
* LineString/MultiLineString geometry.
96+
* @note added in QGIS 3.0
97+
*/
98+
QgsGeometry mergeLines( QString* errorMsg = nullptr ) const;
99+
90100
/** Returns the closest point on the geometry to the other geometry.
91101
* @note added in QGIS 2.14
92102
* @see shortestLine()

‎src/core/qgsexpression.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,20 @@ static QVariant fcnBoundary( const QVariantList& values, const QgsExpressionCont
18391839
return QVariant::fromValue( QgsGeometry( boundary ) );
18401840
}
18411841

1842+
static QVariant fcnLineMerge( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent )
1843+
{
1844+
QgsGeometry geom = getGeometry( values.at( 0 ), parent );
1845+
1846+
if ( geom.isEmpty() )
1847+
return QVariant();
1848+
1849+
QgsGeometry merged = geom.mergeLines();
1850+
if ( merged.isEmpty() )
1851+
return QVariant();
1852+
1853+
return QVariant::fromValue( merged );
1854+
}
1855+
18421856
static QVariant fcnMakePoint( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent )
18431857
{
18441858
if ( values.count() < 2 || values.count() > 4 )
@@ -3187,7 +3201,7 @@ const QStringList& QgsExpression::BuiltinFunctions()
31873201
<< "disjoint" << "intersects" << "touches" << "crosses" << "contains"
31883202
<< "relate"
31893203
<< "overlaps" << "within" << "buffer" << "centroid" << "bounds" << "reverse" << "exterior_ring"
3190-
<< "boundary"
3204+
<< "boundary" << "line_merge"
31913205
<< "bounds_width" << "bounds_height" << "is_closed" << "convex_hull" << "difference"
31923206
<< "distance" << "intersection" << "sym_difference" << "combine"
31933207
<< "extrude" << "azimuth" << "project" << "closest_point" << "shortest_line"
@@ -3380,6 +3394,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
33803394
<< new StaticFunction( "interior_ring_n", 2, fcnInteriorRingN, "GeometryGroup" )
33813395
<< new StaticFunction( "geometry_n", 2, fcnGeometryN, "GeometryGroup" )
33823396
<< new StaticFunction( "boundary", ParameterList() << Parameter( "geometry" ), fcnBoundary, "GeometryGroup" )
3397+
<< new StaticFunction( "line_merge", ParameterList() << Parameter( "geometry" ), fcnLineMerge, "GeometryGroup" )
33833398
<< new StaticFunction( "bounds", 1, fcnBounds, "GeometryGroup" )
33843399
<< new StaticFunction( "num_points", 1, fcnGeomNumPoints, "GeometryGroup" )
33853400
<< new StaticFunction( "num_interior_rings", 1, fcnGeomNumInteriorRings, "GeometryGroup" )

‎tests/src/core/testqgsexpression.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,11 @@ class TestQgsExpression: public QObject
672672
QTest::newRow( "boundary point" ) << "boundary(geom_from_wkt('POINT(1 2)'))" << false << QVariant();
673673
QTest::newRow( "boundary polygon" ) << "geom_to_wkt(boundary(geometry:=geom_from_wkt('POLYGON((-1 -1, 4 0, 4 2, 0 2, -1 -1))')))" << false << QVariant( "LineString (-1 -1, 4 0, 4 2, 0 2, -1 -1)" );
674674
QTest::newRow( "boundary line" ) << "geom_to_wkt(boundary(geom_from_wkt('LINESTRING(0 0, 1 1, 2 2)')))" << false << QVariant( "MultiPoint ((0 0),(2 2))" );
675+
QTest::newRow( "line_merge not geom" ) << "line_merge('g')" << true << QVariant();
676+
QTest::newRow( "line_merge null" ) << "line_merge(NULL)" << false << QVariant();
677+
QTest::newRow( "line_merge point" ) << "line_merge(geom_from_wkt('POINT(1 2)'))" << false << QVariant();
678+
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)" );
679+
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)" );
675680
QTest::newRow( "start_point point" ) << "geom_to_wkt(start_point(geom_from_wkt('POINT(2 0)')))" << false << QVariant( "Point (2 0)" );
676681
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)" );
677682
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)" );

‎tests/src/python/test_qgsgeometry.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3360,5 +3360,35 @@ def testMisc(self):
33603360
wkb2 = geom.asWkb()
33613361
self.assertEqual(wkb1, wkb2)
33623362

3363+
def testMergeLines(self):
3364+
""" test merging linestrings """
3365+
3366+
# not a (multi)linestring
3367+
geom = QgsGeometry.fromWkt('Point(1 2)')
3368+
result = geom.mergeLines()
3369+
self.assertTrue(result.isEmpty())
3370+
3371+
# linestring should be returned intact
3372+
geom = QgsGeometry.fromWkt('LineString(0 0, 10 10)')
3373+
result = geom.mergeLines().exportToWkt()
3374+
exp = 'LineString(0 0, 10 10)'
3375+
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))
3376+
3377+
# multilinestring
3378+
geom = QgsGeometry.fromWkt('MultiLineString((0 0, 10 10),(10 10, 20 20))')
3379+
result = geom.mergeLines().exportToWkt()
3380+
exp = 'LineString(0 0, 10 10, 20 20)'
3381+
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))
3382+
3383+
geom = QgsGeometry.fromWkt('MultiLineString((0 0, 10 10),(12 2, 14 4),(10 10, 20 20))')
3384+
result = geom.mergeLines().exportToWkt()
3385+
exp = 'MultiLineString((0 0, 10 10, 20 20),(12 2, 14 4))'
3386+
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))
3387+
3388+
geom = QgsGeometry.fromWkt('MultiLineString((0 0, 10 10),(12 2, 14 4))')
3389+
result = geom.mergeLines().exportToWkt()
3390+
exp = 'MultiLineString((0 0, 10 10),(12 2, 14 4))'
3391+
self.assertTrue(compareWkt(result, exp, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result))
3392+
33633393
if __name__ == '__main__':
33643394
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.