Skip to content
Permalink
Browse files

Merge pull request #3391 from nyalldawson/single_sided

Single sided buffers
  • Loading branch information
nyalldawson committed Aug 16, 2016
2 parents 40b70a3 + d193574 commit f5b171597a56eb9d1bbb31769d5178de25d3a500
Showing with 666 additions and 184 deletions.
  1. +3 −3 cmake/FindGEOS.cmake
  2. +49 −9 python/core/geometry/qgsgeometry.sip
  3. +9 −0 python/plugins/processing/algs/help/qgis.yaml
  4. +2 −1 python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
  5. +118 −0 python/plugins/processing/algs/qgis/SingleSidedBuffer.py
  6. +15 −0 python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gfs
  7. +48 −0 python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gml
  8. +15 −0 python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gfs
  9. +48 −0 python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gml
  10. +14 −0 python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gfs
  11. +33 −0 python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gml
  12. +49 −0 python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
  13. +15 −0 resources/function_help/json/offset_curve
  14. +15 −0 resources/function_help/json/single_sided_buffer
  15. +0 −6 src/analysis/vector/qgsgeometryanalyzer.cpp
  16. +0 −11 src/app/qgisapp.cpp
  17. +0 −6 src/app/qgsmaptooloffsetcurve.cpp
  18. +0 −3 src/app/qgsoptions.cpp
  19. +43 −3 src/core/geometry/qgsgeometry.cpp
  20. +49 −9 src/core/geometry/qgsgeometry.h
  21. +27 −6 src/core/geometry/qgsgeos.cpp
  22. +19 −0 src/core/geometry/qgsgeos.h
  23. +47 −3 src/core/qgsexpression.cpp
  24. +3 −3 src/core/qgsexpression.h
  25. +0 −3 src/core/qgsgeometryvalidator.cpp
  26. +3 −118 src/core/symbology-ng/qgssymbollayerutils.cpp
  27. +12 −0 tests/src/core/testqgsexpression.cpp
  28. +30 −0 tests/src/python/test_qgsgeometry.py
@@ -87,9 +87,9 @@ ELSE(WIN32)
STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\1" GEOS_VERSION_MAJOR "${GEOS_VERSION}")
STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\2" GEOS_VERSION_MINOR "${GEOS_VERSION}")

IF (GEOS_VERSION_MAJOR LESS 3 OR (GEOS_VERSION_MAJOR EQUAL 3 AND GEOS_VERSION_MINOR LESS 1) )
MESSAGE (FATAL_ERROR "GEOS version is too old (${GEOS_VERSION}). Use 3.1.0 or higher.")
ENDIF (GEOS_VERSION_MAJOR LESS 3 OR (GEOS_VERSION_MAJOR EQUAL 3 AND GEOS_VERSION_MINOR LESS 1) )
IF (GEOS_VERSION_MAJOR LESS 3 OR (GEOS_VERSION_MAJOR EQUAL 3 AND GEOS_VERSION_MINOR LESS 3) )
MESSAGE (FATAL_ERROR "GEOS version is too old (${GEOS_VERSION}). Use 3.3.0 or higher.")
ENDIF (GEOS_VERSION_MAJOR LESS 3 OR (GEOS_VERSION_MAJOR EQUAL 3 AND GEOS_VERSION_MINOR LESS 3) )

# set INCLUDE_DIR to prefix+include
EXEC_PROGRAM(${GEOS_CONFIG}
@@ -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;
@@ -425,6 +425,15 @@ qgis:simplifygeometries: >
qgis:singlepartstomultipart:


qgis:singlesidedbuffer: >
This algorithm buffers lines by a specified distance on one side of the line only.

The segments parameter controls the number of line segments to use to approximate a quarter circle when creating rounded buffers.

The join style parameter specifies whether round, mitre or beveled joins should be used when buffering corners in a line.

The mitre limit parameter is only applicable for mitre join styles, and controls the maximum distance from the buffer to use when creating a mitred join.

qgis:snappointstogrid: >
This algorithm modifies the position of points in a vector layer, so they fall in the coordinates of a grid.

@@ -153,6 +153,7 @@
from .OffsetLine import OffsetLine
from .PolygonCentroids import PolygonCentroids
from .Translate import Translate
from .SingleSidedBuffer import SingleSidedBuffer

pluginPath = os.path.normpath(os.path.join(
os.path.split(os.path.dirname(__file__))[0], os.pardir))
@@ -207,7 +208,7 @@ def __init__(self):
RectanglesOvalsDiamondsFixed(), MergeLines(),
BoundingBox(), Boundary(), PointOnSurface(),
OffsetLine(), PolygonCentroids(),
Translate()
Translate(), SingleSidedBuffer()
]

if hasMatplotlib:
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
SingleSidedBuffer.py
--------------------
Date : August 2016
Copyright : (C) 2016 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************
"""

__author__ = 'Nyall Dawson'
__date__ = 'August 2016'
__copyright__ = '(C) 2016, Nyall Dawson'

# This will get replaced with a git SHA1 when you do a git archive323

__revision__ = '$Format:%H$'

import os

from qgis.core import QgsGeometry, QgsWkbTypes

from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.parameters import ParameterVector, ParameterSelection, ParameterNumber
from processing.core.outputs import OutputVector
from processing.tools import dataobjects, vector

pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]


class SingleSidedBuffer(GeoAlgorithm):

INPUT_LAYER = 'INPUT_LAYER'
OUTPUT_LAYER = 'OUTPUT_LAYER'
DISTANCE = 'DISTANCE'
SIDE = 'SIDE'
SEGMENTS = 'SEGMENTS'
JOIN_STYLE = 'JOIN_STYLE'
MITRE_LIMIT = 'MITRE_LIMIT'

def defineCharacteristics(self):
self.name, self.i18n_name = self.trAlgorithm('Single sided buffer')
self.group, self.i18n_group = self.trAlgorithm('Vector geometry tools')

self.addParameter(ParameterVector(self.INPUT_LAYER,
self.tr('Input layer'), [ParameterVector.VECTOR_TYPE_LINE]))
self.addParameter(ParameterNumber(self.DISTANCE,
self.tr('Distance'), default=10.0))
self.sides = [self.tr('Left'),
'Right']
self.addParameter(ParameterSelection(
self.SIDE,
self.tr('Side'),
self.sides))

self.addParameter(ParameterNumber(self.SEGMENTS,
self.tr('Segments'), 1, default=8))

self.join_styles = [self.tr('Round'),
'Mitre',
'Bevel']
self.addParameter(ParameterSelection(
self.JOIN_STYLE,
self.tr('Join style'),
self.join_styles))
self.addParameter(ParameterNumber(self.MITRE_LIMIT,
self.tr('Mitre limit'), 1, default=2))

self.addOutput(OutputVector(self.OUTPUT_LAYER, self.tr('Single sided buffers')))

def processAlgorithm(self, progress):
layer = dataobjects.getObjectFromUri(
self.getParameterValue(self.INPUT_LAYER))

writer = self.getOutputFromName(
self.OUTPUT_LAYER).getVectorWriter(
layer.fields().toList(),
QgsWkbTypes.Polygon,
layer.crs())

distance = self.getParameterValue(self.DISTANCE)
segments = int(self.getParameterValue(self.SEGMENTS))
join_style = self.getParameterValue(self.JOIN_STYLE) + 1
if self.getParameterValue(self.SIDE) == 0:
side = QgsGeometry.SideLeft
else:
side = QgsGeometry.SideRight
miter_limit = self.getParameterValue(self.MITRE_LIMIT)

features = vector.features(layer)
total = 100.0 / len(features)

for current, input_feature in enumerate(features):
output_feature = input_feature
input_geometry = input_feature.geometry()
if input_geometry:
output_geometry = input_geometry.singleSidedBuffer(distance, segments,
side, join_style, miter_limit)
if not output_geometry:
raise GeoAlgorithmExecutionException(
self.tr('Error calculating single sided buffer'))

output_feature.setGeometry(output_geometry)

writer.addFeature(output_feature)
progress.setPercentage(int(current * total))

del writer
@@ -0,0 +1,15 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>single_sided_buffer_line</Name>
<ElementPath>single_sided_buffer_line</ElementPath>
<GeometryType>3</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>7</FeatureCount>
<ExtentXMin>-1.00000</ExtentXMin>
<ExtentXMax>11.00000</ExtentXMax>
<ExtentYMin>-3.00000</ExtentYMin>
<ExtentYMax>5.70711</ExtentYMax>
</DatasetSpecificInfo>
</GMLFeatureClass>
</GMLFeatureClassList>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=""
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>-1</gml:X><gml:Y>-3</gml:Y></gml:coord>
<gml:coord><gml:X>11</gml:X><gml:Y>5.707106781186548</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.0">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>11,5 9,3 9,2 6,2 6,3 8,3 8.01921471959677,3.195090322016128 8.076120467488714,3.38268343236509 8.168530387697455,3.555570233019602 8.292893218813452,3.707106781186547 10.292893218813452,5.707106781186548 11,5</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.1">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>1,-1 -1,-1 -1,0 1,0 1,-1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.2">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>3,3 3,2 2,2 2,0 1,0 1,2 1.01921471959677,2.195090322016128 1.076120467488713,2.38268343236509 1.168530387697455,2.555570233019602 1.292893218813453,2.707106781186547 1.444429766980398,2.831469612302545 1.61731656763491,2.923879532511287 1.804909677983872,2.98078528040323 2,3 3,3</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.3">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>5,1 3,1 3,2 5,2 5,1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.4">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>10,-3 7,-3 7,-2 10,-2 10,-3</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.5">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>10,1 6,-3 5.292893218813452,-2.292893218813453 9.292893218813452,1.707106781186547 10,1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line fid="lines.6">
</ogr:single_sided_buffer_line>
</gml:featureMember>
</ogr:FeatureCollection>
@@ -0,0 +1,15 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>single_sided_buffer_line_mitre</Name>
<ElementPath>single_sided_buffer_line_mitre</ElementPath>
<GeometryType>3</GeometryType>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>7</FeatureCount>
<ExtentXMin>-1.00000</ExtentXMin>
<ExtentXMax>11.70711</ExtentXMax>
<ExtentYMin>-4.00000</ExtentYMin>
<ExtentYMax>5.00000</ExtentYMax>
</DatasetSpecificInfo>
</GMLFeatureClass>
</GMLFeatureClassList>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=""
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:boundedBy>
<gml:Box>
<gml:coord><gml:X>-1</gml:X><gml:Y>-4</gml:Y></gml:coord>
<gml:coord><gml:X>11.70710678118655</gml:X><gml:Y>5</gml:Y></gml:coord>
</gml:Box>
</gml:boundedBy>

<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.0">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,2 9,2 9,3 11,5 11.707106781186548,4.292893218813452 10.0,2.585786437626905 10,1 6,1 6,2</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.1">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-1,-1 1,-1 1,-2 -1,-2 -1,-1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.2">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>2,0 2,2 3,2 3,3 4,3 4,1 3,1 3,0 2,0</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.3">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>3,1 5,1 5,0 3,0 3,1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.4">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>7,-3 10,-3 10,-4 7,-4 7,-3</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.5">
<ogr:geometryProperty><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>6,-3 10,1 10.707106781186548,0.292893218813453 6.707106781186548,-3.707106781186547 6,-3</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></ogr:geometryProperty>
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
<gml:featureMember>
<ogr:single_sided_buffer_line_mitre fid="lines.6">
</ogr:single_sided_buffer_line_mitre>
</gml:featureMember>
</ogr:FeatureCollection>
@@ -0,0 +1,14 @@
<GMLFeatureClassList>
<GMLFeatureClass>
<Name>single_sided_buffer_multiline_bevel</Name>
<ElementPath>single_sided_buffer_multiline_bevel</ElementPath>
<SRSName>EPSG:4326</SRSName>
<DatasetSpecificInfo>
<FeatureCount>4</FeatureCount>
<ExtentXMin>-1.00000</ExtentXMin>
<ExtentXMax>6.02404</ExtentXMax>
<ExtentYMin>-1.00000</ExtentYMin>
<ExtentYMax>5.11935</ExtentYMax>
</DatasetSpecificInfo>
</GMLFeatureClass>
</GMLFeatureClassList>

0 comments on commit f5b1715

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