Skip to content

Commit

Permalink
[FEATURE] Expose GEOS single sided buffer through QgsGeometry
Browse files Browse the repository at this point in the history
Makes it easy for PyQGIS code to perform a single sided buffer
operation
  • Loading branch information
nyalldawson committed Aug 16, 2016
1 parent 616a80f commit e3f0d3d
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 22 deletions.
58 changes: 49 additions & 9 deletions python/core/geometry/qgsgeometry.sip
Expand Up @@ -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;
Expand Down
44 changes: 42 additions & 2 deletions src/core/geometry/qgsgeometry.cpp
Expand Up @@ -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 )
{
Expand All @@ -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 )
{
Expand Down Expand Up @@ -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<QgsGeometry> parts = asGeometryCollection();
QList<QgsGeometry> 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 )
Expand Down
58 changes: 49 additions & 9 deletions src/core/geometry/qgsgeometry.h
Expand Up @@ -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;
Expand Down
27 changes: 27 additions & 0 deletions src/core/geometry/qgsgeos.cpp
Expand Up @@ -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 )
Expand Down
18 changes: 18 additions & 0 deletions src/core/geometry/qgsgeos.h
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/core/symbology-ng/qgssymbollayerutils.cpp
Expand Up @@ -718,9 +718,10 @@ QList<QPolygonF> 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() )
{
Expand Down
30 changes: 30 additions & 0 deletions tests/src/python/test_qgsgeometry.py
Expand Up @@ -3314,6 +3314,36 @@ def testDeleteVertexCurvePolygon(self):
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
Expand Down

0 comments on commit e3f0d3d

Please sign in to comment.