From ec4df6c019efddc05e9555c43d58a155b811d078 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 3 Aug 2017 23:26:39 +1000 Subject: [PATCH] Port points to path to new API Improvements: - Maintain Z/M values - Keep original data type for group/order fields - Group field is optional - Added unit tests - Don't export text files for features by default --- python/plugins/processing/algs/help/qgis.yaml | 2 + .../processing/algs/qgis/PointsToPaths.py | 179 ++++++++++-------- .../algs/qgis/QGISAlgorithmProvider.py | 5 +- .../testdata/expected/points_to_path.gfs | 26 +++ .../testdata/expected/points_to_path.gml | 21 ++ .../expected/points_to_path_grouped.gfs | 31 +++ .../expected/points_to_path_grouped.gml | 38 ++++ .../tests/testdata/qgis_algorithm_tests.yaml | 25 +++ 8 files changed, 250 insertions(+), 77 deletions(-) create mode 100644 python/plugins/processing/tests/testdata/expected/points_to_path.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/points_to_path.gml create mode 100644 python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gml diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index ba5ed67e8e1..a60b0af4a27 100755 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -370,7 +370,9 @@ qgis:pointslayerfromtable: > The attributes table of the resulting layer will be the input table. qgis:pointstopath: + Converts a point layer to a line layer, by joining points in a defined order. + Points can be grouped by a field to output individual line features per group. qgis:polarplot: > This algorithm generates a polar plot based on the value of an input vector layer. diff --git a/python/plugins/processing/algs/qgis/PointsToPaths.py b/python/plugins/processing/algs/qgis/PointsToPaths.py index e73c57a9fd5..df2105b33c0 100644 --- a/python/plugins/processing/algs/qgis/PointsToPaths.py +++ b/python/plugins/processing/algs/qgis/PointsToPaths.py @@ -29,36 +29,34 @@ __revision__ = '$Format:%H$' import os from datetime import datetime -from qgis.PyQt.QtCore import QVariant -from qgis.core import (QgsApplication, - QgsFeature, +from qgis.core import (QgsFeature, QgsFeatureSink, QgsFields, QgsField, QgsGeometry, QgsDistanceArea, - QgsProject, + QgsPointXY, + QgsLineString, QgsWkbTypes, - QgsProcessingUtils) + QgsFeatureRequest, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterString, + QgsProcessing, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFolderDestination) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.core.parameters import ParameterVector -from processing.core.parameters import ParameterTableField -from processing.core.parameters import ParameterString -from processing.core.outputs import OutputVector -from processing.core.outputs import OutputDirectory -from processing.tools import dataobjects class PointsToPaths(QgisAlgorithm): - VECTOR = 'VECTOR' + INPUT = 'INPUT' GROUP_FIELD = 'GROUP_FIELD' ORDER_FIELD = 'ORDER_FIELD' DATE_FORMAT = 'DATE_FORMAT' - #GAP_PERIOD = 'GAP_PERIOD' - OUTPUT_LINES = 'OUTPUT_LINES' - OUTPUT_TEXT = 'OUTPUT_TEXT' + OUTPUT = 'OUTPUT' + OUTPUT_TEXT_DIR = 'OUTPUT_TEXT_DIR' def group(self): return self.tr('Vector creation tools') @@ -66,20 +64,23 @@ class PointsToPaths(QgisAlgorithm): def __init__(self): super().__init__() + def tags(self): + return self.tr('join,points,lines,connect').split(',') + def initAlgorithm(self, config=None): - self.addParameter(ParameterVector(self.VECTOR, - self.tr('Input point layer'), [dataobjects.TYPE_VECTOR_POINT])) - self.addParameter(ParameterTableField(self.GROUP_FIELD, - self.tr('Group field'), self.VECTOR)) - self.addParameter(ParameterTableField(self.ORDER_FIELD, - self.tr('Order field'), self.VECTOR)) - self.addParameter(ParameterString(self.DATE_FORMAT, - self.tr('Date format (if order field is DateTime)'), '', optional=True)) - #self.addParameter(ParameterNumber( - # self.GAP_PERIOD, - # 'Gap period (if order field is DateTime)', 0, 60, 0)) - self.addOutput(OutputVector(self.OUTPUT_LINES, self.tr('Paths'), datatype=[dataobjects.TYPE_VECTOR_LINE])) - self.addOutput(OutputDirectory(self.OUTPUT_TEXT, self.tr('Directory'))) + self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, + self.tr('Input point layer'), [QgsProcessing.TypeVectorPoint])) + self.addParameter(QgsProcessingParameterField(self.ORDER_FIELD, + self.tr('Order field'), parentLayerParameterName=self.INPUT)) + self.addParameter(QgsProcessingParameterField(self.GROUP_FIELD, + self.tr('Group field'), parentLayerParameterName=self.INPUT, optional=True)) + self.addParameter(QgsProcessingParameterString(self.DATE_FORMAT, + self.tr('Date format (if order field is DateTime)'), optional=True)) + + self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Paths'), QgsProcessing.TypeVectorLine)) + output_dir_param = QgsProcessingParameterFolderDestination(self.OUTPUT_TEXT_DIR, self.tr('Directory for text output'), optional=True) + output_dir_param.setCreateByDefault(False) + self.addParameter(output_dir_param) def name(self): return 'pointstopath' @@ -88,29 +89,58 @@ class PointsToPaths(QgisAlgorithm): return self.tr('Points to path') def processAlgorithm(self, parameters, context, feedback): - layer = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.VECTOR), context) - groupField = self.getParameterValue(self.GROUP_FIELD) - orderField = self.getParameterValue(self.ORDER_FIELD) - dateFormat = str(self.getParameterValue(self.DATE_FORMAT)) - #gap = int(self.getParameterValue(self.GAP_PERIOD)) - dirName = self.getOutputValue(self.OUTPUT_TEXT) + source = self.parameterAsSource(parameters, self.INPUT, context) + group_field_name = self.parameterAsString(parameters, self.GROUP_FIELD, context) + order_field_name = self.parameterAsString(parameters, self.ORDER_FIELD, context) + date_format = self.parameterAsString(parameters, self.DATE_FORMAT, context) + text_dir = self.parameterAsString(parameters, self.OUTPUT_TEXT_DIR, context) + + group_field_index = source.fields().lookupField(group_field_name) + order_field_index = source.fields().lookupField(order_field_name) + + if group_field_index >= 0: + group_field_def = source.fields().at(group_field_index) + else: + group_field_def = None + order_field_def = source.fields().at(order_field_index) fields = QgsFields() - fields.append(QgsField('group', QVariant.String, '', 254, 0)) - fields.append(QgsField('begin', QVariant.String, '', 254, 0)) - fields.append(QgsField('end', QVariant.String, '', 254, 0)) - writer = self.getOutputFromName(self.OUTPUT_LINES).getVectorWriter(fields, QgsWkbTypes.LineString, layer.crs(), - context) + if group_field_def is not None: + fields.append(group_field_def) + begin_field = QgsField(order_field_def) + begin_field.setName('begin') + fields.append(begin_field) + end_field = QgsField(order_field_def) + end_field.setName('end') + fields.append(end_field) + + output_wkb = QgsWkbTypes.LineString + if QgsWkbTypes.hasM(source.wkbType()): + output_wkb = QgsWkbTypes.addM(output_wkb) + if QgsWkbTypes.hasZ(source.wkbType()): + output_wkb = QgsWkbTypes.addZ(output_wkb) + + (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, + fields, output_wkb, source.sourceCrs()) points = dict() - features = QgsProcessingUtils.getFeatures(layer, context) - total = 100.0 / layer.featureCount() if layer.featureCount() else 0 + features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([group_field_index, order_field_index])) + total = 100.0 / source.featureCount() if source.featureCount() else 0 for current, f in enumerate(features): - point = f.geometry().asPoint() - group = f[groupField] - order = f[orderField] - if dateFormat != '': - order = datetime.strptime(str(order), dateFormat) + if feedback.isCanceled(): + break + + if not f.hasGeometry(): + continue + + point = f.geometry().geometry().clone() + if group_field_index >= 0: + group = f.attributes()[group_field_index] + else: + group = 1 + order = f.attributes()[order_field_index] + if date_format != '': + order = datetime.strptime(str(order), date_format) if group in points: points[group].append((order, point)) else: @@ -121,46 +151,45 @@ class PointsToPaths(QgisAlgorithm): feedback.setProgress(0) da = QgsDistanceArea() - da.setSourceCrs(layer.sourceCrs()) + da.setSourceCrs(source.sourceCrs()) da.setEllipsoid(context.project().ellipsoid()) current = 0 total = 100.0 / len(points) if points else 1 for group, vertices in list(points.items()): + if feedback.isCanceled(): + break + vertices.sort() f = QgsFeature() - f.initAttributes(len(fields)) - f.setFields(fields) - f['group'] = group - f['begin'] = vertices[0][0] - f['end'] = vertices[-1][0] + attributes = [] + if group_field_index >= 0: + attributes.append(group) + attributes.extend([vertices[0][0], vertices[-1][0]]) + f.setAttributes(attributes) + line = [node[1] for node in vertices] - fileName = os.path.join(dirName, '%s.txt' % group) + if text_dir: + fileName = os.path.join(text_dir, '%s.txt' % group) - with open(fileName, 'w') as fl: - fl.write('angle=Azimuth\n') - fl.write('heading=Coordinate_System\n') - fl.write('dist_units=Default\n') + with open(fileName, 'w') as fl: + fl.write('angle=Azimuth\n') + fl.write('heading=Coordinate_System\n') + fl.write('dist_units=Default\n') - line = [] - i = 0 - for node in vertices: - line.append(node[1]) + for i in range(len(line)): + if i == 0: + fl.write('startAt=%f;%f;90\n' % (line[i].x(), line[i].y())) + fl.write('survey=Polygonal\n') + fl.write('[data]\n') + else: + angle = line[i - 1].azimuth(line[i]) + distance = da.measureLine(QgsPointXY(line[i - 1]), QgsPointXY(line[i])) + fl.write('%f;%f;90\n' % (angle, distance)) - if i == 0: - fl.write('startAt=%f;%f;90\n' % (node[1].x(), node[1].y())) - fl.write('survey=Polygonal\n') - fl.write('[data]\n') - else: - angle = line[i - 1].azimuth(line[i]) - distance = da.measureLine(line[i - 1], line[i]) - fl.write('%f;%f;90\n' % (angle, distance)) - - i += 1 - - f.setGeometry(QgsGeometry.fromPolyline(line)) - writer.addFeature(f, QgsFeatureSink.FastInsert) + f.setGeometry(QgsGeometry(QgsLineString(line))) + sink.addFeature(f, QgsFeatureSink.FastInsert) current += 1 feedback.setProgress(int(current * total)) - del writer + return {self.OUTPUT: dest_id} diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index e6da61cf343..c9f11fabbd2 100644 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -94,6 +94,7 @@ from .PointOnSurface import PointOnSurface from .PointsAlongGeometry import PointsAlongGeometry from .PointsInPolygon import PointsInPolygon from .PointsLayerFromTable import PointsLayerFromTable +from .PointsToPaths import PointsToPaths from .PoleOfInaccessibility import PoleOfInaccessibility from .Polygonize import Polygonize from .PolygonsToLines import PolygonsToLines @@ -152,7 +153,6 @@ from .ZonalStatistics import ZonalStatistics # from .PointsDisplacement import PointsDisplacement # from .PointsFromPolygons import PointsFromPolygons # from .PointsFromLines import PointsFromLines -# from .PointsToPaths import PointsToPaths # from .SetVectorStyle import SetVectorStyle # from .SetRasterStyle import SetRasterStyle # from .SelectByAttributeSum import SelectByAttributeSum @@ -194,7 +194,7 @@ class QGISAlgorithmProvider(QgsProcessingProvider): # StatisticsByCategories(), # RasterLayerStatistics(), PointsDisplacement(), # PointsFromPolygons(), - # PointsFromLines(), PointsToPaths(), + # PointsFromLines(), # SetVectorStyle(), SetRasterStyle(), # HypsometricCurves(), # FieldsMapper(), SelectByAttributeSum(), Datasources2Vrt(), @@ -262,6 +262,7 @@ class QGISAlgorithmProvider(QgsProcessingProvider): PointsAlongGeometry(), PointsInPolygon(), PointsLayerFromTable(), + PointsToPaths(), PoleOfInaccessibility(), Polygonize(), PolygonsToLines(), diff --git a/python/plugins/processing/tests/testdata/expected/points_to_path.gfs b/python/plugins/processing/tests/testdata/expected/points_to_path.gfs new file mode 100644 index 00000000000..89a8bf0693f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/points_to_path.gfs @@ -0,0 +1,26 @@ + + + points_to_path + points_to_path + + 2 + EPSG:4326 + + 1 + 0.00000 + 8.00000 + -5.00000 + 3.00000 + + + begin + begin + Integer + + + end + end + Integer + + + diff --git a/python/plugins/processing/tests/testdata/expected/points_to_path.gml b/python/plugins/processing/tests/testdata/expected/points_to_path.gml new file mode 100644 index 00000000000..5057497c90d --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/points_to_path.gml @@ -0,0 +1,21 @@ + + + + + 0-5 + 83 + + + + + + 1,1 3,3 2,2 5,2 4,1 0,-5 8,-1 7,-1 0,-1 + 1 + 9 + + + diff --git a/python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gfs b/python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gfs new file mode 100644 index 00000000000..076d497b096 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gfs @@ -0,0 +1,31 @@ + + + points_to_path_grouped + points_to_path_grouped + + 2 + EPSG:4326 + + 3 + 0.00000 + 8.00000 + -5.00000 + 3.00000 + + + id2 + id2 + Integer + + + begin + begin + Integer + + + end + end + Integer + + + diff --git a/python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gml b/python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gml new file mode 100644 index 00000000000..e82344ba8ef --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/points_to_path_grouped.gml @@ -0,0 +1,38 @@ + + + + + 0-5 + 83 + + + + + + 1,1 5,2 + 2 + 1 + 4 + + + + + 3,3 4,1 + 1 + 2 + 5 + + + + + 2,2 0,-5 8,-1 7,-1 0,-1 + 0 + 3 + 9 + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 18fd8566b39..a0adf00e432 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -2232,6 +2232,31 @@ tests: name: expected/hub_lines.gml type: vector + - algorithm: qgis:pointstopath + name: Points to path (non grouped) + params: + INPUT: + name: points.gml + type: vector + ORDER_FIELD: id + results: + OUTPUT: + name: expected/points_to_path.gml + type: vector + + - algorithm: qgis:pointstopath + name: Points to path (grouped) + params: + INPUT: + name: points.gml + type: vector + ORDER_FIELD: id + GROUP_FIELD: id2 + results: + OUTPUT: + name: expected/points_to_path_grouped.gml + type: vector + # - algorithm: qgis:joinattributestable # name: join the attribute table by common field # params: