diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index f325ebe7279..8919deffa1e 100644 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -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. diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index a4e88ad1f1e..bd77e361815 100644 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -153,6 +153,7 @@ from .PointOnSurface import PointOnSurface 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 @@ class QGISAlgorithmProvider(AlgorithmProvider): RectanglesOvalsDiamondsFixed(), MergeLines(), BoundingBox(), Boundary(), PointOnSurface(), OffsetLine(), PolygonCentroids(), - Translate() + Translate(), SingleSidedBuffer() ] if hasMatplotlib: diff --git a/python/plugins/processing/algs/qgis/SingleSidedBuffer.py b/python/plugins/processing/algs/qgis/SingleSidedBuffer.py new file mode 100644 index 00000000000..da539a3f20c --- /dev/null +++ b/python/plugins/processing/algs/qgis/SingleSidedBuffer.py @@ -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 diff --git a/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gfs b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gfs new file mode 100644 index 00000000000..a8c2eef2aaf --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gfs @@ -0,0 +1,15 @@ + + + single_sided_buffer_line + single_sided_buffer_line + 3 + EPSG:4326 + + 7 + -1.00000 + 11.00000 + -3.00000 + 5.70711 + + + diff --git a/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gml b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gml new file mode 100644 index 00000000000..d1158aeeba2 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line.gml @@ -0,0 +1,48 @@ + + + + + -1-3 + 115.707106781186548 + + + + + + 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 + + + + + 1,-1 -1,-1 -1,0 1,0 1,-1 + + + + + 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 + + + + + 5,1 3,1 3,2 5,2 5,1 + + + + + 10,-3 7,-3 7,-2 10,-2 10,-3 + + + + + 10,1 6,-3 5.292893218813452,-2.292893218813453 9.292893218813452,1.707106781186547 10,1 + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gfs b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gfs new file mode 100644 index 00000000000..a54045e71b1 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gfs @@ -0,0 +1,15 @@ + + + single_sided_buffer_line_mitre + single_sided_buffer_line_mitre + 3 + EPSG:4326 + + 7 + -1.00000 + 11.70711 + -4.00000 + 5.00000 + + + diff --git a/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gml b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gml new file mode 100644 index 00000000000..17462a4f976 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_line_mitre.gml @@ -0,0 +1,48 @@ + + + + + -1-4 + 11.707106781186555 + + + + + + 6,2 9,2 9,3 11,5 11.707106781186548,4.292893218813452 10.0,2.585786437626905 10,1 6,1 6,2 + + + + + -1,-1 1,-1 1,-2 -1,-2 -1,-1 + + + + + 2,0 2,2 3,2 3,3 4,3 4,1 3,1 3,0 2,0 + + + + + 3,1 5,1 5,0 3,0 3,1 + + + + + 7,-3 10,-3 10,-4 7,-4 7,-3 + + + + + 6,-3 10,1 10.707106781186548,0.292893218813453 6.707106781186548,-3.707106781186547 6,-3 + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gfs b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gfs new file mode 100644 index 00000000000..ad6196b036e --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gfs @@ -0,0 +1,14 @@ + + + single_sided_buffer_multiline_bevel + single_sided_buffer_multiline_bevel + EPSG:4326 + + 4 + -1.00000 + 6.02404 + -1.00000 + 5.11935 + + + diff --git a/python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gml b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gml new file mode 100644 index 00000000000..e3fd94fb93f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/single_sided_buffer_multiline_bevel.gml @@ -0,0 +1,33 @@ + + + + + -1-1 + 6.0240381903374715.119353882875417 + + + + + + 1,-1 -1,-1 -1,0 1,0 1,-1 + + + + + 5,1 3,1 3,2 5,2 5,15,1 5.024184261036468,2.414779270633399 6.024038190337471,2.397687750474408 5.999853929301003,0.982908479841009 5,1 + + + + + + + + + 3,3 3,2 2,2 2,0 1,0 1,2 2,3 3,35.459500959692898,4.119769673704415 2.944337811900192,4.04721689059501 2.915503652020259,5.046801099766013 5.430666799812965,5.119353882875417 5.459500959692898,4.1197696737044155.58042226487524,2.946833013435702 3,3 3.020599614854323,3.999787805420657 5.601021879729563,3.946620818856359 5.58042226487524,2.946833013435702 + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 95c67b00a21..12896a84040 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -821,3 +821,52 @@ tests: OUTPUT_LAYER: name: expected/lines_translated.gml type: vector + + + - algorithm: qgis:singlesidedbuffer + name: Single sided buffer lines (left, round) + params: + DISTANCE: 1.0 + INPUT_LAYER: + name: lines.gml + type: vector + JOIN_STYLE: '0' + MITRE_LIMIT: 2 + SEGMENTS: 8 + SIDE: '0' + results: + OUTPUT_LAYER: + name: expected/single_sided_buffer_line.gml + type: vector + + - algorithm: qgis:singlesidedbuffer + name: Single sided buffer lines (Right, mitre) + params: + DISTANCE: 1.0 + INPUT_LAYER: + name: lines.gml + type: vector + JOIN_STYLE: '1' + MITRE_LIMIT: 2 + SEGMENTS: 8 + SIDE: '1' + results: + OUTPUT_LAYER: + name: expected/single_sided_buffer_line_mitre.gml + type: vector + + - algorithm: qgis:singlesidedbuffer + name: Single sided buffer multiline (bevel) + params: + DISTANCE: 1.0 + INPUT_LAYER: + name: multilines.gml + type: vector + JOIN_STYLE: '2' + MITRE_LIMIT: 2 + SEGMENTS: 8 + SIDE: '0' + results: + OUTPUT_LAYER: + name: expected/single_sided_buffer_multiline_bevel.gml + type: vector \ No newline at end of file