diff --git a/python/core/geometry/qgsgeometry.sip b/python/core/geometry/qgsgeometry.sip index 1d02c19a416..ec48fb8e6ad 100644 --- a/python/core/geometry/qgsgeometry.sip +++ b/python/core/geometry/qgsgeometry.sip @@ -447,27 +447,67 @@ class QgsGeometry * @note added in 1.5 */ bool crosses( const QgsGeometry& geometry ) const; + //! Side of line to buffer + enum BufferSide + { + SideLeft, //!< Buffer to left of line + SideRight, //!< Buffer to right of line + }; + + //! End cap styles for buffers + enum EndCapStyle + { + CapRound, //!< Round cap + CapFlat, //!< Flat cap (in line with start/end of line) + CapSquare, //!< Square cap (extends past start/end of line by buffer distance) + }; + + //! Join styles for buffers + enum JoinStyle + { + JoinStyleRound, //!< Use rounded joins + JoinStyleMitre, //!< Use mitred joins + JoinStyleBevel, //!< Use beveled joins + }; + /** Returns a buffer region around this geometry having the given width and with a specified number of segments used to approximate curves */ QgsGeometry buffer( double distance, int segments ) const; /** Returns a buffer region around the geometry, with additional style options. * @param distance buffer distance - * @param segments For round joins, number of segments to approximate quarter-circle - * @param endCapStyle Round (1) / Flat (2) / Square (3) end cap style - * @param joinStyle Round (1) / Mitre (2) / Bevel (3) join style - * @param mitreLimit Limit on the mitre ratio used for very sharp corners + * @param segments for round joins, number of segments to approximate quarter-circle + * @param endCapStyle end cap style + * @param joinStyle join style for corners in geometry + * @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only) * @note added in 2.4 - * @note needs GEOS >= 3.3 - otherwise always returns 0 */ - QgsGeometry buffer( double distance, int segments, int endCapStyle, int joinStyle, double mitreLimit ) const; + QgsGeometry buffer( double distance, int segments, EndCapStyle endCapStyle, JoinStyle joinStyle, double mitreLimit ) const; /** Returns an offset line at a given distance and side from an input line. - * See buffer() method for details on parameters. + * @param distance buffer distance + * @param segments for round joins, number of segments to approximate quarter-circle + * @param joinStyle join style for corners in geometry + * @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only) * @note added in 2.4 - * @note needs GEOS >= 3.3 - otherwise always returns 0 */ - QgsGeometry offsetCurve( double distance, int segments, int joinStyle, double mitreLimit ) const; + QgsGeometry offsetCurve( double distance, int segments, JoinStyle joinStyle, double mitreLimit ) const; + + /** + * Returns a single sided buffer for a (multi)line geometry. The buffer is only + * applied to one side of the line. + * @param distance buffer distance + * @param segments for round joins, number of segments to approximate quarter-circle + * @param side side of geometry to buffer + * @param joinStyle join style for corners + * @param mitreLimit limit on the mitre ratio used for very sharp corners + * @return buffered geometry, or an empty geometry if buffer could not be + * calculated + * @note added in QGIS 3.0 + */ + QgsGeometry singleSidedBuffer( double distance, int segments, BufferSide side, + JoinStyle joinStyle = JoinStyleRound, + double mitreLimit = 2.0 ) const; /** Returns a simplified version of this geometry using a specified tolerance value */ QgsGeometry simplify( double tolerance ) const; diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index d880175819b..8162623847e 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1296,7 +1296,7 @@ QgsGeometry QgsGeometry::buffer( double distance, int segments ) const return QgsGeometry( geom ); } -QgsGeometry QgsGeometry::buffer( double distance, int segments, int endCapStyle, int joinStyle, double mitreLimit ) const +QgsGeometry QgsGeometry::buffer( double distance, int segments, EndCapStyle endCapStyle, JoinStyle joinStyle, double mitreLimit ) const { if ( !d->geometry ) { @@ -1312,7 +1312,7 @@ QgsGeometry QgsGeometry::buffer( double distance, int segments, int endCapStyle, return QgsGeometry( geom ); } -QgsGeometry QgsGeometry::offsetCurve( double distance, int segments, int joinStyle, double mitreLimit ) const +QgsGeometry QgsGeometry::offsetCurve( double distance, int segments, JoinStyle joinStyle, double mitreLimit ) const { if ( !d->geometry ) { @@ -1351,6 +1351,46 @@ QgsGeometry QgsGeometry::offsetCurve( double distance, int segments, int joinSty } } +QgsGeometry QgsGeometry::singleSidedBuffer( double distance, int segments, BufferSide side , JoinStyle joinStyle, double mitreLimit ) const +{ + if ( !d->geometry ) + { + return QgsGeometry(); + } + + if ( QgsWkbTypes::isMultiType( d->geometry->wkbType() ) ) + { + QList parts = asGeometryCollection(); + QList results; + Q_FOREACH ( const QgsGeometry& part, parts ) + { + QgsGeometry result = part.singleSidedBuffer( distance, segments, side, joinStyle, mitreLimit ); + if ( result ) + results << result; + } + if ( results.isEmpty() ) + return QgsGeometry(); + + QgsGeometry first = results.takeAt( 0 ); + Q_FOREACH ( const QgsGeometry& result, results ) + { + first.addPart( & result ); + } + return first; + } + else + { + QgsGeos geos( d->geometry ); + QgsAbstractGeometry* bufferGeom = geos.singleSidedBuffer( distance, segments, side, + joinStyle, mitreLimit ); + if ( !bufferGeom ) + { + return QgsGeometry(); + } + return QgsGeometry( bufferGeom ); + } +} + QgsGeometry QgsGeometry::simplify( double tolerance ) const { if ( !d->geometry ) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index c80557ec8ef..7a1f0cd5eb2 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -484,27 +484,67 @@ class CORE_EXPORT QgsGeometry * @note added in 1.5 */ bool crosses( const QgsGeometry& geometry ) const; + //! Side of line to buffer + enum BufferSide + { + SideLeft = 0, //!< Buffer to left of line + SideRight, //!< Buffer to right of line + }; + + //! End cap styles for buffers + enum EndCapStyle + { + CapRound = 1, //!< Round cap + CapFlat, //!< Flat cap (in line with start/end of line) + CapSquare, //!< Square cap (extends past start/end of line by buffer distance) + }; + + //! Join styles for buffers + enum JoinStyle + { + JoinStyleRound = 1, //!< Use rounded joins + JoinStyleMitre, //!< Use mitred joins + JoinStyleBevel, //!< Use beveled joins + }; + /** Returns a buffer region around this geometry having the given width and with a specified number of segments used to approximate curves */ QgsGeometry buffer( double distance, int segments ) const; /** Returns a buffer region around the geometry, with additional style options. * @param distance buffer distance - * @param segments For round joins, number of segments to approximate quarter-circle - * @param endCapStyle Round (1) / Flat (2) / Square (3) end cap style - * @param joinStyle Round (1) / Mitre (2) / Bevel (3) join style - * @param mitreLimit Limit on the mitre ratio used for very sharp corners + * @param segments for round joins, number of segments to approximate quarter-circle + * @param endCapStyle end cap style + * @param joinStyle join style for corners in geometry + * @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only) * @note added in 2.4 - * @note needs GEOS >= 3.3 - otherwise always returns 0 */ - QgsGeometry buffer( double distance, int segments, int endCapStyle, int joinStyle, double mitreLimit ) const; + QgsGeometry buffer( double distance, int segments, EndCapStyle endCapStyle, JoinStyle joinStyle, double mitreLimit ) const; /** Returns an offset line at a given distance and side from an input line. - * See buffer() method for details on parameters. + * @param distance buffer distance + * @param segments for round joins, number of segments to approximate quarter-circle + * @param joinStyle join style for corners in geometry + * @param mitreLimit limit on the mitre ratio used for very sharp corners (JoinStyleMitre only) * @note added in 2.4 - * @note needs GEOS >= 3.3 - otherwise always returns 0 */ - QgsGeometry offsetCurve( double distance, int segments, int joinStyle, double mitreLimit ) const; + QgsGeometry offsetCurve( double distance, int segments, JoinStyle joinStyle, double mitreLimit ) const; + + /** + * Returns a single sided buffer for a (multi)line geometry. The buffer is only + * applied to one side of the line. + * @param distance buffer distance + * @param segments for round joins, number of segments to approximate quarter-circle + * @param side side of geometry to buffer + * @param joinStyle join style for corners + * @param mitreLimit limit on the mitre ratio used for very sharp corners + * @return buffered geometry, or an empty geometry if buffer could not be + * calculated + * @note added in QGIS 3.0 + */ + QgsGeometry singleSidedBuffer( double distance, int segments, BufferSide side, + JoinStyle joinStyle = JoinStyleRound, + double mitreLimit = 2.0 ) const; /** Returns a simplified version of this geometry using a specified tolerance value */ QgsGeometry simplify( double tolerance ) const; diff --git a/src/core/geometry/qgsgeos.cpp b/src/core/geometry/qgsgeos.cpp index 4f7bc4f778b..20cdc3bfc75 100644 --- a/src/core/geometry/qgsgeos.cpp +++ b/src/core/geometry/qgsgeos.cpp @@ -1679,6 +1679,33 @@ QgsAbstractGeometry* QgsGeos::offsetCurve( double distance, int segments, int jo return offsetGeom; } +QgsAbstractGeometry* QgsGeos::singleSidedBuffer( double distance, int segments, int side, int joinStyle, double mitreLimit, QString* errorMsg ) const +{ + if ( !mGeos ) + { + return nullptr; + } + + GEOSGeomScopedPtr geos; + try + { + GEOSBufferParams* bp = GEOSBufferParams_create_r( geosinit.ctxt ); + GEOSBufferParams_setSingleSided_r( geosinit.ctxt, bp, 1 ); + GEOSBufferParams_setQuadrantSegments_r( geosinit.ctxt, bp, segments ); + GEOSBufferParams_setJoinStyle_r( geosinit.ctxt, bp, joinStyle ); + GEOSBufferParams_setMitreLimit_r( geosinit.ctxt, bp, mitreLimit ); + + if ( side == 1 ) + { + distance = -distance; + } + geos.reset( GEOSBufferWithParams_r( geosinit.ctxt, mGeos, bp, distance ) ); + GEOSBufferParams_destroy_r( geosinit.ctxt, bp ); + } + CATCH_GEOS_WITH_ERRMSG( nullptr ); + return fromGeos( geos.get() ); +} + QgsAbstractGeometry* QgsGeos::reshapeGeometry( const QgsLineString& reshapeWithLine, int* errorCode, QString* errorMsg ) const { if ( !mGeos || reshapeWithLine.numPoints() < 2 || mGeometry->dimension() == 0 ) diff --git a/src/core/geometry/qgsgeos.h b/src/core/geometry/qgsgeos.h index 9fe473727f3..f83a1ce9115 100644 --- a/src/core/geometry/qgsgeos.h +++ b/src/core/geometry/qgsgeos.h @@ -85,6 +85,24 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine QString* errorMsg = nullptr ) const override; QgsAbstractGeometry* offsetCurve( double distance, int segments, int joinStyle, double mitreLimit, QString* errorMsg = nullptr ) const override; + + /** + * Returns a single sided buffer for a geometry. The buffer is only + * applied to one side of the geometry. + * @param distance buffer distance + * @param segments for round joins, number of segments to approximate quarter-circle + * @param side side of geometry to buffer (0 = left, 1 = right) + * @param joinStyle join style for corners ( Round (1) / Mitre (2) / Bevel (3) ) + * @param mitreLimit limit on the mitre ratio used for very sharp corners + * @return buffered geometry, or an nullptr if buffer could not be + * calculated + * @note added in QGIS 3.0 + */ + QgsAbstractGeometry* singleSidedBuffer( double distance, int segments, int side, + int joinStyle, double mitreLimit, + QString* errorMsg = nullptr ) const; + + QgsAbstractGeometry* reshapeGeometry( const QgsLineString& reshapeWithLine, int* errorCode, QString* errorMsg = nullptr ) const; /** Merges any connected lines in a LineString/MultiLineString geometry and diff --git a/src/core/symbology-ng/qgssymbollayerutils.cpp b/src/core/symbology-ng/qgssymbollayerutils.cpp index b3c6d326c1f..f868107e3a5 100644 --- a/src/core/symbology-ng/qgssymbollayerutils.cpp +++ b/src/core/symbology-ng/qgssymbollayerutils.cpp @@ -718,9 +718,10 @@ QList offsetLine( QPolygonF polyline, double dist, QgsWkbTypes::Geome double mitreLimit = 2.0; // the default value in GEOS (5.0) allows for fairly sharp endings QgsGeometry offsetGeom; if ( geometryType == QgsWkbTypes::PolygonGeometry ) - offsetGeom = tempGeometry.buffer( -dist, quadSegments, GEOSBUF_CAP_FLAT, GEOSBUF_JOIN_MITRE, mitreLimit ); + offsetGeom = tempGeometry.buffer( -dist, quadSegments, QgsGeometry::CapFlat, + QgsGeometry::JoinStyleMitre, mitreLimit ); else - offsetGeom = tempGeometry.offsetCurve( dist, quadSegments, GEOSBUF_JOIN_MITRE, mitreLimit ); + offsetGeom = tempGeometry.offsetCurve( dist, quadSegments, QgsGeometry::JoinStyleMitre, mitreLimit ); if ( !offsetGeom.isEmpty() ) { diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 6c02c028834..30521f9033d 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -3314,6 +3314,36 @@ class TestQgsGeometry(unittest.TestCase): expected_wkt = "CurvePolygon (CompoundCurve (CircularString (0 0, 1 1, 2 0),(2 0, 0 0)))" self.assertEqual(geom.exportToWkt(), QgsGeometry.fromWkt(expected_wkt).exportToWkt()) + def testSingleSidedBuffer(self): + + wkt = "LineString( 0 0, 10 0)" + geom = QgsGeometry.fromWkt(wkt) + out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideLeft) + result = out.exportToWkt() + expected_wkt = "Polygon ((10 0, 0 0, 0 1, 10 1, 10 0))" + self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result)) + + wkt = "LineString( 0 0, 10 0)" + geom = QgsGeometry.fromWkt(wkt) + out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideRight) + result = out.exportToWkt() + expected_wkt = "Polygon ((0 0, 10 0, 10 -1, 0 -1, 0 0))" + self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result)) + + wkt = "LineString( 0 0, 10 0, 10 10)" + geom = QgsGeometry.fromWkt(wkt) + out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideRight, QgsGeometry.JoinStyleMitre) + result = out.exportToWkt() + expected_wkt = "Polygon ((0 0, 10 0, 10 10, 11 10, 11 -1, 0 -1, 0 0))" + self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result)) + + wkt = "LineString( 0 0, 10 0, 10 10)" + geom = QgsGeometry.fromWkt(wkt) + out = geom.singleSidedBuffer(1, 8, QgsGeometry.SideRight, QgsGeometry.JoinStyleBevel) + result = out.exportToWkt() + expected_wkt = "Polygon ((0 0, 10 0, 10 10, 11 10, 11 0, 10 -1, 0 -1, 0 0))" + self.assertTrue(compareWkt(result, expected_wkt, 0.00001), "Merge lines: mismatch Expected:\n{}\nGot:\n{}\n".format(expected_wkt, result)) + def testMisc(self): # Test that we cannot add a CurvePolygon in a MultiPolygon