From fc1746e7706e35ff54a75c664071a2c86732386d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 3 Aug 2017 01:34:28 +1000 Subject: [PATCH] Port Hub Distance (points) to new API Improvements: - handle different CRS between points and hubs - add unit test --- .../processing/algs/qgis/HubDistancePoints.py | 113 +++++++++--------- .../algs/qgis/QGISAlgorithmProvider.py | 5 +- .../tests/testdata/custom/hub_points.gfs | 22 ++++ .../tests/testdata/custom/hub_points.gml | 32 +++++ .../testdata/expected/hub_distance_points.gfs | 37 ++++++ .../testdata/expected/hub_distance_points.gml | 95 +++++++++++++++ .../tests/testdata/qgis_algorithm_tests.yaml | 16 +++ 7 files changed, 261 insertions(+), 59 deletions(-) create mode 100644 python/plugins/processing/tests/testdata/custom/hub_points.gfs create mode 100644 python/plugins/processing/tests/testdata/custom/hub_points.gml create mode 100644 python/plugins/processing/tests/testdata/expected/hub_distance_points.gfs create mode 100644 python/plugins/processing/tests/testdata/expected/hub_distance_points.gml diff --git a/python/plugins/processing/algs/qgis/HubDistancePoints.py b/python/plugins/processing/algs/qgis/HubDistancePoints.py index ea670b39b4d..32effadfc14 100644 --- a/python/plugins/processing/algs/qgis/HubDistancePoints.py +++ b/python/plugins/processing/algs/qgis/HubDistancePoints.py @@ -33,34 +33,32 @@ from qgis.core import (QgsField, QgsDistanceArea, QgsFeature, QgsFeatureRequest, + QgsSpatialIndex, QgsWkbTypes, - QgsApplication, - QgsProject, - QgsProcessingUtils) + QgsUnitTypes, + QgsProcessing, + QgsProcessingUtils, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterEnum, + QgsProcessingParameterFeatureSink, + QgsProcessingException) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException -from processing.core.parameters import ParameterVector -from processing.core.parameters import ParameterTableField -from processing.core.parameters import ParameterSelection -from processing.core.outputs import OutputVector - -from processing.tools import dataobjects - -from math import sqrt class HubDistancePoints(QgisAlgorithm): - POINTS = 'POINTS' + INPUT = 'INPUT' HUBS = 'HUBS' FIELD = 'FIELD' UNIT = 'UNIT' OUTPUT = 'OUTPUT' + LAYER_UNITS = 'LAYER_UNITS' - UNITS = ['Meters', - 'Feet', - 'Miles', - 'Kilometers', - 'Layer units' + UNITS = [QgsUnitTypes.DistanceMeters, + QgsUnitTypes.DistanceFeet, + QgsUnitTypes.DistanceMiles, + QgsUnitTypes.DistanceKilometers, + LAYER_UNITS ] def group(self): @@ -76,16 +74,16 @@ class HubDistancePoints(QgisAlgorithm): self.tr('Kilometers'), self.tr('Layer units')] - self.addParameter(ParameterVector(self.POINTS, - self.tr('Source points layer'))) - self.addParameter(ParameterVector(self.HUBS, - self.tr('Destination hubs layer'))) - self.addParameter(ParameterTableField(self.FIELD, - self.tr('Hub layer name attribute'), self.HUBS)) - self.addParameter(ParameterSelection(self.UNIT, - self.tr('Measurement unit'), self.units)) + self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, + self.tr('Source points layer'))) + self.addParameter(QgsProcessingParameterFeatureSource(self.HUBS, + self.tr('Destination hubs layer'))) + self.addParameter(QgsProcessingParameterField(self.FIELD, + self.tr('Hub layer name attribute'), parentLayerParameterName=self.HUBS)) + self.addParameter(QgsProcessingParameterEnum(self.UNIT, + self.tr('Measurement unit'), self.units)) - self.addOutput(OutputVector(self.OUTPUT, self.tr('Hub distance'), datatype=[dataobjects.TYPE_VECTOR_POINT])) + self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Hub distance'), QgsProcessing.TypeVectorPoint)) def name(self): return 'distancetonearesthubpoints' @@ -94,61 +92,62 @@ class HubDistancePoints(QgisAlgorithm): return self.tr('Distance to nearest hub (points)') def processAlgorithm(self, parameters, context, feedback): - layerPoints = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.POINTS), context) - layerHubs = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.HUBS), context) - fieldName = self.getParameterValue(self.FIELD) - - units = self.UNITS[self.getParameterValue(self.UNIT)] - - if layerPoints.source() == layerHubs.source(): - raise GeoAlgorithmExecutionException( + if parameters[self.INPUT] == parameters[self.HUBS]: + raise QgsProcessingException( self.tr('Same layer given for both hubs and spokes')) - fields = layerPoints.fields() + point_source = self.parameterAsSource(parameters, self.INPUT, context) + hub_source = self.parameterAsSource(parameters, self.HUBS, context) + fieldName = self.parameterAsString(parameters, self.FIELD, context) + + units = self.UNITS[self.parameterAsEnum(parameters, self.UNIT, context)] + + fields = point_source.fields() fields.append(QgsField('HubName', QVariant.String)) fields.append(QgsField('HubDist', QVariant.Double)) - writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(fields, QgsWkbTypes.Point, layerPoints.crs(), - context) + (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, + fields, QgsWkbTypes.Point, point_source.sourceCrs()) - index = QgsProcessingUtils.createSpatialIndex(layerHubs, context) + index = QgsSpatialIndex(hub_source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]).setDestinationCrs(point_source.sourceCrs()))) distance = QgsDistanceArea() - distance.setSourceCrs(layerPoints.crs()) + distance.setSourceCrs(point_source.sourceCrs()) distance.setEllipsoid(context.project().ellipsoid()) # Scan source points, find nearest hub, and write to output file - features = QgsProcessingUtils.getFeatures(layerPoints, context) - total = 100.0 / layerPoints.featureCount() if layerPoints.featureCount() else 0 + features = point_source.getFeatures() + total = 100.0 / point_source.featureCount() if point_source.featureCount() else 0 for current, f in enumerate(features): + if feedback.isCanceled(): + break + + if not f.hasGeometry(): + sink.addFeature(f, QgsFeatureSink.FastInsert) + continue + src = f.geometry().boundingBox().center() neighbors = index.nearestNeighbor(src, 1) - ft = next(layerHubs.getFeatures(QgsFeatureRequest().setFilterFid(neighbors[0]).setSubsetOfAttributes([fieldName], layerHubs.fields()))) + ft = next(hub_source.getFeatures(QgsFeatureRequest().setFilterFid(neighbors[0]).setSubsetOfAttributes([fieldName], hub_source.fields()).setDestinationCrs(point_source.sourceCrs()))) closest = ft.geometry().boundingBox().center() hubDist = distance.measureLine(src, closest) + if units != self.LAYER_UNITS: + hub_dist_in_desired_units = distance.convertLengthMeasurement(hubDist, units) + else: + hub_dist_in_desired_units = hubDist + attributes = f.attributes() attributes.append(ft[fieldName]) - if units == 'Feet': - attributes.append(hubDist * 3.2808399) - elif units == 'Miles': - attributes.append(hubDist * 0.000621371192) - elif units == 'Kilometers': - attributes.append(hubDist / 1000.0) - elif units != 'Meters': - attributes.append(sqrt( - pow(src.x() - closest.x(), 2.0) + - pow(src.y() - closest.y(), 2.0))) - else: - attributes.append(hubDist) + attributes.append(hub_dist_in_desired_units) feat = QgsFeature() feat.setAttributes(attributes) feat.setGeometry(QgsGeometry.fromPoint(src)) - writer.addFeature(feat, QgsFeatureSink.FastInsert) + sink.addFeature(feat, QgsFeatureSink.FastInsert) 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 97d113597dc..d58960a5770 100644 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -75,6 +75,7 @@ from .GridLine import GridLine from .GridPolygon import GridPolygon from .Heatmap import Heatmap from .Hillshade import Hillshade +from .HubDistancePoints import HubDistancePoints from .ImportIntoPostGIS import ImportIntoPostGIS from .ImportIntoSpatialite import ImportIntoSpatialite from .Intersection import Intersection @@ -141,7 +142,7 @@ from .ZonalStatistics import ZonalStatistics # from .ExtractByLocation import ExtractByLocation # from .SelectByLocation import SelectByLocation # from .SpatialJoin import SpatialJoin -# from .HubDistancePoints import HubDistancePoints + # from .HubDistanceLines import HubDistanceLines # from .HubLines import HubLines # from .GeometryConvert import GeometryConvert @@ -188,7 +189,6 @@ class QGISAlgorithmProvider(QgsProcessingProvider): # SelectByLocation(), # ExtractByLocation(), # SpatialJoin(), - # HubDistancePoints(), # HubDistanceLines(), HubLines(), # GeometryConvert(), FieldsCalculator(), # JoinAttributes(), @@ -245,6 +245,7 @@ class QGISAlgorithmProvider(QgsProcessingProvider): GridPolygon(), Heatmap(), Hillshade(), + HubDistancePoints(), ImportIntoPostGIS(), ImportIntoSpatialite(), Intersection(), diff --git a/python/plugins/processing/tests/testdata/custom/hub_points.gfs b/python/plugins/processing/tests/testdata/custom/hub_points.gfs new file mode 100644 index 00000000000..35cbfd91664 --- /dev/null +++ b/python/plugins/processing/tests/testdata/custom/hub_points.gfs @@ -0,0 +1,22 @@ + + + hub_points + hub_points + + 1 + EPSG:4326 + + 3 + 1.34481 + 6.29897 + -1.25947 + 2.27221 + + + name + name + String + 6 + + + diff --git a/python/plugins/processing/tests/testdata/custom/hub_points.gml b/python/plugins/processing/tests/testdata/custom/hub_points.gml new file mode 100644 index 00000000000..99d14c1d02a --- /dev/null +++ b/python/plugins/processing/tests/testdata/custom/hub_points.gml @@ -0,0 +1,32 @@ + + + + + 1.344807662693645-1.259467184083282 + 6.2989682466352512.272211648033507 + + + + + + 1.34480766269365,-1.25946718408328 + point1 + + + + + 6.29896824663525,0.138489020296281 + point2 + + + + + 3.12290985247467,2.27221164803351 + point3 + + + diff --git a/python/plugins/processing/tests/testdata/expected/hub_distance_points.gfs b/python/plugins/processing/tests/testdata/expected/hub_distance_points.gfs new file mode 100644 index 00000000000..43674410adb --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/hub_distance_points.gfs @@ -0,0 +1,37 @@ + + + hub_distance_points + hub_distance_points + + 1 + EPSG:4326 + + 9 + 0.00000 + 8.00000 + -5.00000 + 3.00000 + + + id + id + Integer + + + id2 + id2 + Integer + + + HubName + HubName + String + 6 + + + HubDist + HubDist + Real + + + diff --git a/python/plugins/processing/tests/testdata/expected/hub_distance_points.gml b/python/plugins/processing/tests/testdata/expected/hub_distance_points.gml new file mode 100644 index 00000000000..4e767e8b117 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/hub_distance_points.gml @@ -0,0 +1,95 @@ + + + + + 0-5 + 83 + + + + + + 1,1 + 1 + 2 + point1 + 254434.675423572 + + + + + 3,3 + 2 + 1 + point3 + 82164.2455422206 + + + + + 2,2 + 3 + 0 + point3 + 128622.227687308 + + + + + 5,2 + 4 + 2 + point3 + 211142.486929284 + + + + + 4,1 + 5 + 1 + point3 + 172016.876891364 + + + + + 0,-5 + 6 + 0 + point1 + 442487.532089586 + + + + + 8,-1 + 7 + 0 + point2 + 227856.24000978 + + + + + 7,-1 + 8 + 0 + point2 + 148835.564980152 + + + + + 0,-1 + 9 + 0 + point1 + 152464.26003518 + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index ac3d7d2929a..e79b77c2cd3 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -2184,6 +2184,22 @@ tests: name: expected/gridify_lines.gml type: vector + - algorithm: qgis:distancetonearesthubpoints + name: Hub distance points + params: + INPUT: + name: points.gml + type: vector + HUBS: + name: custom/hub_points.gml + type: vector + FIELD: name + UNIT: 0 + results: + OUTPUT: + name: expected/hub_distance_points.gml + type: vector + # - algorithm: qgis:joinattributestable # name: join the attribute table by common field # params: