Skip to content

Commit e3f0d3d

Browse files
committedAug 16, 2016
[FEATURE] Expose GEOS single sided buffer through QgsGeometry
Makes it easy for PyQGIS code to perform a single sided buffer operation
1 parent 616a80f commit e3f0d3d

File tree

7 files changed

+218
-22
lines changed

7 files changed

+218
-22
lines changed
 

‎python/core/geometry/qgsgeometry.sip

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -447,27 +447,67 @@ class QgsGeometry
447447
* @note added in 1.5 */
448448
bool crosses( const QgsGeometry& geometry ) const;
449449

450+
//! Side of line to buffer
451+
enum BufferSide
452+
{
453+
SideLeft, //!< Buffer to left of line
454+
SideRight, //!< Buffer to right of line
455+
};
456+
457+
//! End cap styles for buffers
458+
enum EndCapStyle
459+
{
460+
CapRound, //!< Round cap
461+
CapFlat, //!< Flat cap (in line with start/end of line)
462+
CapSquare, //!< Square cap (extends past start/end of line by buffer distance)
463+
};
464+
465+
//! Join styles for buffers
466+
enum JoinStyle
467+
{
468+
JoinStyleRound, //!< Use rounded joins
469+
JoinStyleMitre, //!< Use mitred joins
470+
JoinStyleBevel, //!< Use beveled joins
471+
};
472+
450473
/** Returns a buffer region around this geometry having the given width and with a specified number
451474
of segments used to approximate curves */
452475
QgsGeometry buffer( double distance, int segments ) const;
453476

454477
/** Returns a buffer region around the geometry, with additional style options.
455478
* @param distance buffer distance
456-
* @param segments For round joins, number of segments to approximate quarter-circle
457-
* @param endCapStyle Round (1) / Flat (2) / Square (3) end cap style
458-
* @param joinStyle Round (1) / Mitre (2) / Bevel (3) join style
459-
* @param mitreLimit Limit on the mitre ratio used for very sharp corners
479+
* @param segments for round joins, number of segments to approximate quarter-circle
480+
* @param endCapStyle end cap style
481+
* @param joinStyle join style for corners in geometry
482+
* @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only)
460483
* @note added in 2.4
461-
* @note needs GEOS >= 3.3 - otherwise always returns 0
462484
*/
463-
QgsGeometry buffer( double distance, int segments, int endCapStyle, int joinStyle, double mitreLimit ) const;
485+
QgsGeometry buffer( double distance, int segments, EndCapStyle endCapStyle, JoinStyle joinStyle, double mitreLimit ) const;
464486

465487
/** Returns an offset line at a given distance and side from an input line.
466-
* See buffer() method for details on parameters.
488+
* @param distance buffer distance
489+
* @param segments for round joins, number of segments to approximate quarter-circle
490+
* @param joinStyle join style for corners in geometry
491+
* @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only)
467492
* @note added in 2.4
468-
* @note needs GEOS >= 3.3 - otherwise always returns 0
469493
*/
470-
QgsGeometry offsetCurve( double distance, int segments, int joinStyle, double mitreLimit ) const;
494+
QgsGeometry offsetCurve( double distance, int segments, JoinStyle joinStyle, double mitreLimit ) const;
495+
496+
/**
497+
* Returns a single sided buffer for a (multi)line geometry. The buffer is only
498+
* applied to one side of the line.
499+
* @param distance buffer distance
500+
* @param segments for round joins, number of segments to approximate quarter-circle
501+
* @param side side of geometry to buffer
502+
* @param joinStyle join style for corners
503+
* @param mitreLimit limit on the mitre ratio used for very sharp corners
504+
* @return buffered geometry, or an empty geometry if buffer could not be
505+
* calculated
506+
* @note added in QGIS 3.0
507+
*/
508+
QgsGeometry singleSidedBuffer( double distance, int segments, BufferSide side,
509+
JoinStyle joinStyle = JoinStyleRound,
510+
double mitreLimit = 2.0 ) const;
471511

472512
/** Returns a simplified version of this geometry using a specified tolerance value */
473513
QgsGeometry simplify( double tolerance ) const;

‎src/core/geometry/qgsgeometry.cpp

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,7 @@ QgsGeometry QgsGeometry::buffer( double distance, int segments ) const
12961296
return QgsGeometry( geom );
12971297
}
12981298

1299-
QgsGeometry QgsGeometry::buffer( double distance, int segments, int endCapStyle, int joinStyle, double mitreLimit ) const
1299+
QgsGeometry QgsGeometry::buffer( double distance, int segments, EndCapStyle endCapStyle, JoinStyle joinStyle, double mitreLimit ) const
13001300
{
13011301
if ( !d->geometry )
13021302
{
@@ -1312,7 +1312,7 @@ QgsGeometry QgsGeometry::buffer( double distance, int segments, int endCapStyle,
13121312
return QgsGeometry( geom );
13131313
}
13141314

1315-
QgsGeometry QgsGeometry::offsetCurve( double distance, int segments, int joinStyle, double mitreLimit ) const
1315+
QgsGeometry QgsGeometry::offsetCurve( double distance, int segments, JoinStyle joinStyle, double mitreLimit ) const
13161316
{
13171317
if ( !d->geometry )
13181318
{
@@ -1351,6 +1351,46 @@ QgsGeometry QgsGeometry::offsetCurve( double distance, int segments, int joinSty
13511351
}
13521352
}
13531353

1354+
QgsGeometry QgsGeometry::singleSidedBuffer( double distance, int segments, BufferSide side , JoinStyle joinStyle, double mitreLimit ) const
1355+
{
1356+
if ( !d->geometry )
1357+
{
1358+
return QgsGeometry();
1359+
}
1360+
1361+
if ( QgsWkbTypes::isMultiType( d->geometry->wkbType() ) )
1362+
{
1363+
QList<QgsGeometry> parts = asGeometryCollection();
1364+
QList<QgsGeometry> results;
1365+
Q_FOREACH ( const QgsGeometry& part, parts )
1366+
{
1367+
QgsGeometry result = part.singleSidedBuffer( distance, segments, side, joinStyle, mitreLimit );
1368+
if ( result )
1369+
results << result;
1370+
}
1371+
if ( results.isEmpty() )
1372+
return QgsGeometry();
1373+
1374+
QgsGeometry first = results.takeAt( 0 );
1375+
Q_FOREACH ( const QgsGeometry& result, results )
1376+
{
1377+
first.addPart( & result );
1378+
}
1379+
return first;
1380+
}
1381+
else
1382+
{
1383+
QgsGeos geos( d->geometry );
1384+
QgsAbstractGeometry* bufferGeom = geos.singleSidedBuffer( distance, segments, side,
1385+
joinStyle, mitreLimit );
1386+
if ( !bufferGeom )
1387+
{
1388+
return QgsGeometry();
1389+
}
1390+
return QgsGeometry( bufferGeom );
1391+
}
1392+
}
1393+
13541394
QgsGeometry QgsGeometry::simplify( double tolerance ) const
13551395
{
13561396
if ( !d->geometry )

‎src/core/geometry/qgsgeometry.h

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -484,27 +484,67 @@ class CORE_EXPORT QgsGeometry
484484
* @note added in 1.5 */
485485
bool crosses( const QgsGeometry& geometry ) const;
486486

487+
//! Side of line to buffer
488+
enum BufferSide
489+
{
490+
SideLeft = 0, //!< Buffer to left of line
491+
SideRight, //!< Buffer to right of line
492+
};
493+
494+
//! End cap styles for buffers
495+
enum EndCapStyle
496+
{
497+
CapRound = 1, //!< Round cap
498+
CapFlat, //!< Flat cap (in line with start/end of line)
499+
CapSquare, //!< Square cap (extends past start/end of line by buffer distance)
500+
};
501+
502+
//! Join styles for buffers
503+
enum JoinStyle
504+
{
505+
JoinStyleRound = 1, //!< Use rounded joins
506+
JoinStyleMitre, //!< Use mitred joins
507+
JoinStyleBevel, //!< Use beveled joins
508+
};
509+
487510
/** Returns a buffer region around this geometry having the given width and with a specified number
488511
of segments used to approximate curves */
489512
QgsGeometry buffer( double distance, int segments ) const;
490513

491514
/** Returns a buffer region around the geometry, with additional style options.
492515
* @param distance buffer distance
493-
* @param segments For round joins, number of segments to approximate quarter-circle
494-
* @param endCapStyle Round (1) / Flat (2) / Square (3) end cap style
495-
* @param joinStyle Round (1) / Mitre (2) / Bevel (3) join style
496-
* @param mitreLimit Limit on the mitre ratio used for very sharp corners
516+
* @param segments for round joins, number of segments to approximate quarter-circle
517+
* @param endCapStyle end cap style
518+
* @param joinStyle join style for corners in geometry
519+
* @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only)
497520
* @note added in 2.4
498-
* @note needs GEOS >= 3.3 - otherwise always returns 0
499521
*/
500-
QgsGeometry buffer( double distance, int segments, int endCapStyle, int joinStyle, double mitreLimit ) const;
522+
QgsGeometry buffer( double distance, int segments, EndCapStyle endCapStyle, JoinStyle joinStyle, double mitreLimit ) const;
501523

502524
/** Returns an offset line at a given distance and side from an input line.
503-
* See buffer() method for details on parameters.
525+
* @param distance buffer distance
526+
* @param segments for round joins, number of segments to approximate quarter-circle
527+
* @param joinStyle join style for corners in geometry
528+
* @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only)
504529
* @note added in 2.4
505-
* @note needs GEOS >= 3.3 - otherwise always returns 0
506530
*/
507-
QgsGeometry offsetCurve( double distance, int segments, int joinStyle, double mitreLimit ) const;
531+
QgsGeometry offsetCurve( double distance, int segments, JoinStyle joinStyle, double mitreLimit ) const;
532+
533+
/**
534+
* Returns a single sided buffer for a (multi)line geometry. The buffer is only
535+
* applied to one side of the line.
536+
* @param distance buffer distance
537+
* @param segments for round joins, number of segments to approximate quarter-circle
538+
* @param side side of geometry to buffer
539+
* @param joinStyle join style for corners
540+
* @param mitreLimit limit on the mitre ratio used for very sharp corners
541+
* @return buffered geometry, or an empty geometry if buffer could not be
542+
* calculated
543+
* @note added in QGIS 3.0
544+
*/
545+
QgsGeometry singleSidedBuffer( double distance, int segments, BufferSide side,
546+
JoinStyle joinStyle = JoinStyleRound,
547+
double mitreLimit = 2.0 ) const;
508548

509549
/** Returns a simplified version of this geometry using a specified tolerance value */
510550
QgsGeometry simplify( double tolerance ) const;

‎src/core/geometry/qgsgeos.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,33 @@ QgsAbstractGeometry* QgsGeos::offsetCurve( double distance, int segments, int jo
16791679
return offsetGeom;
16801680
}
16811681

1682+
QgsAbstractGeometry* QgsGeos::singleSidedBuffer( double distance, int segments, int side, int joinStyle, double mitreLimit, QString* errorMsg ) const
1683+
{
1684+
if ( !mGeos )
1685+
{
1686+
return nullptr;
1687+
}
1688+
1689+
GEOSGeomScopedPtr geos;
1690+
try
1691+
{
1692+
GEOSBufferParams* bp = GEOSBufferParams_create_r( geosinit.ctxt );
1693+
GEOSBufferParams_setSingleSided_r( geosinit.ctxt, bp, 1 );
1694+
GEOSBufferParams_setQuadrantSegments_r( geosinit.ctxt, bp, segments );
1695+
GEOSBufferParams_setJoinStyle_r( geosinit.ctxt, bp, joinStyle );
1696+
GEOSBufferParams_setMitreLimit_r( geosinit.ctxt, bp, mitreLimit );
1697+
1698+
if ( side == 1 )
1699+
{
1700+
distance = -distance;
1701+
}
1702+
geos.reset( GEOSBufferWithParams_r( geosinit.ctxt, mGeos, bp, distance ) );
1703+
GEOSBufferParams_destroy_r( geosinit.ctxt, bp );
1704+
}
1705+
CATCH_GEOS_WITH_ERRMSG( nullptr );
1706+
return fromGeos( geos.get() );
1707+
}
1708+
16821709
QgsAbstractGeometry* QgsGeos::reshapeGeometry( const QgsLineString& reshapeWithLine, int* errorCode, QString* errorMsg ) const
16831710
{
16841711
if ( !mGeos || reshapeWithLine.numPoints() < 2 || mGeometry->dimension() == 0 )

‎src/core/geometry/qgsgeos.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
8585
QString* errorMsg = nullptr ) const override;
8686

8787
QgsAbstractGeometry* offsetCurve( double distance, int segments, int joinStyle, double mitreLimit, QString* errorMsg = nullptr ) const override;
88+
89+
/**
90+
* Returns a single sided buffer for a geometry. The buffer is only
91+
* applied to one side of the geometry.
92+
* @param distance buffer distance
93+
* @param segments for round joins, number of segments to approximate quarter-circle
94+
* @param side side of geometry to buffer (0 = left, 1 = right)
95+
* @param joinStyle join style for corners ( Round (1) / Mitre (2) / Bevel (3) )
96+
* @param mitreLimit limit on the mitre ratio used for very sharp corners
97+
* @return buffered geometry, or an nullptr if buffer could not be
98+
* calculated
99+
* @note added in QGIS 3.0
100+
*/
101+
QgsAbstractGeometry* singleSidedBuffer( double distance, int segments, int side,
102+
int joinStyle, double mitreLimit,
103+
QString* errorMsg = nullptr ) const;
104+
105+
88106
QgsAbstractGeometry* reshapeGeometry( const QgsLineString& reshapeWithLine, int* errorCode, QString* errorMsg = nullptr ) const;
89107

90108
/** Merges any connected lines in a LineString/MultiLineString geometry and

‎src/core/symbology-ng/qgssymbollayerutils.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,9 +718,10 @@ QList<QPolygonF> offsetLine( QPolygonF polyline, double dist, QgsWkbTypes::Geome
718718
double mitreLimit = 2.0; // the default value in GEOS (5.0) allows for fairly sharp endings
719719
QgsGeometry offsetGeom;
720720
if ( geometryType == QgsWkbTypes::PolygonGeometry )
721-
offsetGeom = tempGeometry.buffer( -dist, quadSegments, GEOSBUF_CAP_FLAT, GEOSBUF_JOIN_MITRE, mitreLimit );
721+
offsetGeom = tempGeometry.buffer( -dist, quadSegments, QgsGeometry::CapFlat,
722+
QgsGeometry::JoinStyleMitre, mitreLimit );
722723
else
723-
offsetGeom = tempGeometry.offsetCurve( dist, quadSegments, GEOSBUF_JOIN_MITRE, mitreLimit );
724+
offsetGeom = tempGeometry.offsetCurve( dist, quadSegments, QgsGeometry::JoinStyleMitre, mitreLimit );
724725

725726
if ( !offsetGeom.isEmpty() )
726727
{

‎tests/src/python/test_qgsgeometry.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3314,6 +3314,36 @@ def testDeleteVertexCurvePolygon(self):
33143314
expected_wkt = "CurvePolygon (CompoundCurve (CircularString (0 0, 1 1, 2 0),(2 0, 0 0)))"
33153315
self.assertEqual(geom.exportToWkt(), QgsGeometry.fromWkt(expected_wkt).exportToWkt())
33163316

3317+
def testSingleSidedBuffer(self):
3318+
3319+
wkt = "LineString( 0 0, 10 0)"
3320+
geom = QgsGeometry.fromWkt(wkt)
3321+
out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideLeft)
3322+
result = out.exportToWkt()
3323+
expected_wkt = "Polygon ((10 0, 0 0, 0 1, 10 1, 10 0))"
3324+
self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result))
3325+
3326+
wkt = "LineString( 0 0, 10 0)"
3327+
geom = QgsGeometry.fromWkt(wkt)
3328+
out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideRight)
3329+
result = out.exportToWkt()
3330+
expected_wkt = "Polygon ((0 0, 10 0, 10 -1, 0 -1, 0 0))"
3331+
self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result))
3332+
3333+
wkt = "LineString( 0 0, 10 0, 10 10)"
3334+
geom = QgsGeometry.fromWkt(wkt)
3335+
out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideRight, QgsGeometry.JoinStyleMitre)
3336+
result = out.exportToWkt()
3337+
expected_wkt = "Polygon ((0 0, 10 0, 10 10, 11 10, 11 -1, 0 -1, 0 0))"
3338+
self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result))
3339+
3340+
wkt = "LineString( 0 0, 10 0, 10 10)"
3341+
geom = QgsGeometry.fromWkt(wkt)
3342+
out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideRight, QgsGeometry.JoinStyleBevel)
3343+
result = out.exportToWkt()
3344+
expected_wkt = "Polygon ((0 0, 10 0, 10 10, 11 10, 11 0, 10 -1, 0 -1, 0 0))"
3345+
self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result))
3346+
33173347
def testMisc(self):
33183348

33193349
# Test that we cannot add a CurvePolygon in a MultiPolygon

0 commit comments

Comments
 (0)
Please sign in to comment.