Skip to content

Commit b7f888b

Browse files
committed
Port Distance Matrix algorithm to new API
Enhancements: - support source/target layers in different CRS - output layers with geometry (i.e. keep input point geometry - avoids need to rejoin result back to original table to get geometry) - keep original data types for id fields - don't fire off many single feature requests - instead request multiple features at once to improve speed
1 parent 7f58af1 commit b7f888b

File tree

5 files changed

+844
-93
lines changed

5 files changed

+844
-93
lines changed

python/plugins/processing/algs/qgis/PointDistance.py

Lines changed: 127 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,38 @@
3232
import math
3333

3434
from qgis.PyQt.QtGui import QIcon
35-
36-
from qgis.core import QgsFeatureRequest, QgsProject, QgsDistanceArea, QgsFeatureSink, QgsProcessingUtils
35+
from qgis.PyQt.QtCore import QVariant
36+
37+
from qgis.core import (QgsFeatureRequest,
38+
QgsField,
39+
QgsFields,
40+
QgsProject,
41+
QgsFeature,
42+
QgsGeometry,
43+
QgsDistanceArea,
44+
QgsFeatureSink,
45+
QgsProcessingParameterFeatureSource,
46+
QgsProcessing,
47+
QgsProcessingParameterEnum,
48+
QgsProcessingParameterField,
49+
QgsProcessingParameterNumber,
50+
QgsProcessingParameterFeatureSink,
51+
QgsSpatialIndex,
52+
QgsWkbTypes)
3753

3854
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
39-
from processing.core.parameters import ParameterNumber
40-
from processing.core.parameters import ParameterVector
41-
from processing.core.parameters import ParameterSelection
42-
from processing.core.parameters import ParameterTableField
43-
from processing.core.outputs import OutputTable
44-
from processing.tools import dataobjects
4555

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

4858

4959
class PointDistance(QgisAlgorithm):
50-
51-
INPUT_LAYER = 'INPUT_LAYER'
60+
INPUT = 'INPUT'
5261
INPUT_FIELD = 'INPUT_FIELD'
53-
TARGET_LAYER = 'TARGET_LAYER'
62+
TARGET = 'TARGET'
5463
TARGET_FIELD = 'TARGET_FIELD'
5564
MATRIX_TYPE = 'MATRIX_TYPE'
5665
NEAREST_POINTS = 'NEAREST_POINTS'
57-
DISTANCE_MATRIX = 'DISTANCE_MATRIX'
66+
OUTPUT = 'OUTPUT'
5867

5968
def icon(self):
6069
return QIcon(os.path.join(pluginPath, 'images', 'ftools', 'matrix.png'))
@@ -70,22 +79,26 @@ def initAlgorithm(self, config=None):
7079
self.tr('Standard (N x T) distance matrix'),
7180
self.tr('Summary distance matrix (mean, std. dev., min, max)')]
7281

73-
self.addParameter(ParameterVector(self.INPUT_LAYER,
74-
self.tr('Input point layer'), [dataobjects.TYPE_VECTOR_POINT]))
75-
self.addParameter(ParameterTableField(self.INPUT_FIELD,
76-
self.tr('Input unique ID field'), self.INPUT_LAYER,
77-
ParameterTableField.DATA_TYPE_ANY))
78-
self.addParameter(ParameterVector(self.TARGET_LAYER,
79-
self.tr('Target point layer'), dataobjects.TYPE_VECTOR_POINT))
80-
self.addParameter(ParameterTableField(self.TARGET_FIELD,
81-
self.tr('Target unique ID field'), self.TARGET_LAYER,
82-
ParameterTableField.DATA_TYPE_ANY))
83-
self.addParameter(ParameterSelection(self.MATRIX_TYPE,
84-
self.tr('Output matrix type'), self.mat_types, 0))
85-
self.addParameter(ParameterNumber(self.NEAREST_POINTS,
86-
self.tr('Use only the nearest (k) target points'), 0, 9999, 0))
87-
88-
self.addOutput(OutputTable(self.DISTANCE_MATRIX, self.tr('Distance matrix')))
82+
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
83+
self.tr('Input point layer'),
84+
[QgsProcessing.TypeVectorPoint]))
85+
self.addParameter(QgsProcessingParameterField(self.INPUT_FIELD,
86+
self.tr('Input unique ID field'),
87+
parentLayerParameterName=self.INPUT,
88+
type=QgsProcessingParameterField.Any))
89+
self.addParameter(QgsProcessingParameterFeatureSource(self.TARGET,
90+
self.tr('Target point layer'),
91+
[QgsProcessing.TypeVectorPoint]))
92+
self.addParameter(QgsProcessingParameterField(self.TARGET_FIELD,
93+
self.tr('Target unique ID field'),
94+
parentLayerParameterName=self.TARGET,
95+
type=QgsProcessingParameterField.Any))
96+
self.addParameter(QgsProcessingParameterEnum(self.MATRIX_TYPE,
97+
self.tr('Output matrix type'), options=self.mat_types, defaultValue=0))
98+
self.addParameter(QgsProcessingParameterNumber(self.NEAREST_POINTS,
99+
self.tr('Use only the nearest (k) target points'), type=QgsProcessingParameterNumber.Integer, minValue=0, maxValue=9999, defaultValue=0))
100+
101+
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Distance matrix'), QgsProcessing.TypeVectorPoint))
89102

90103
def name(self):
91104
return 'distancematrix'
@@ -94,65 +107,86 @@ def displayName(self):
94107
return self.tr('Distance matrix')
95108

96109
def processAlgorithm(self, parameters, context, feedback):
97-
inLayer = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.INPUT_LAYER), context)
98-
inField = self.getParameterValue(self.INPUT_FIELD)
99-
targetLayer = QgsProcessingUtils.mapLayerFromString(self.getParameterValue(self.TARGET_LAYER), context)
100-
targetField = self.getParameterValue(self.TARGET_FIELD)
101-
matType = self.getParameterValue(self.MATRIX_TYPE)
102-
nPoints = self.getParameterValue(self.NEAREST_POINTS)
103-
104-
outputFile = self.getOutputFromName(self.DISTANCE_MATRIX)
110+
source = self.parameterAsSource(parameters, self.INPUT, context)
111+
source_field = self.parameterAsString(parameters, self.INPUT_FIELD, context)
112+
target_source = self.parameterAsSource(parameters, self.TARGET, context)
113+
target_field = self.parameterAsString(parameters, self.TARGET_FIELD, context)
114+
matType = self.parameterAsEnum(parameters, self.MATRIX_TYPE, context)
115+
nPoints = self.parameterAsInt(parameters, self.NEAREST_POINTS, context)
105116

106117
if nPoints < 1:
107-
nPoints = QgsProcessingUtils.featureCount(targetLayer, context)
108-
109-
self.writer = outputFile.getTableWriter([])
118+
nPoints = target_source.featureCount()
110119

111120
if matType == 0:
112121
# Linear distance matrix
113-
self.linearMatrix(context, inLayer, inField, targetLayer, targetField,
114-
matType, nPoints, feedback)
122+
return self.linearMatrix(parameters, context, source, source_field, target_source, target_field,
123+
matType, nPoints, feedback)
115124
elif matType == 1:
116125
# Standard distance matrix
117-
self.regularMatrix(context, inLayer, inField, targetLayer, targetField,
118-
nPoints, feedback)
126+
return self.regularMatrix(parameters, context, source, source_field, target_source, target_field,
127+
nPoints, feedback)
119128
elif matType == 2:
120129
# Summary distance matrix
121-
self.linearMatrix(context, inLayer, inField, targetLayer, targetField,
122-
matType, nPoints, feedback)
130+
return self.linearMatrix(parameters, context, source, source_field, target_source, target_field,
131+
matType, nPoints, feedback)
123132

124-
def linearMatrix(self, context, inLayer, inField, targetLayer, targetField,
133+
def linearMatrix(self, parameters, context, source, inField, target_source, targetField,
125134
matType, nPoints, feedback):
135+
inIdx = source.fields().lookupField(inField)
136+
outIdx = target_source.fields().lookupField(targetField)
137+
138+
fields = QgsFields()
139+
input_id_field = source.fields()[inIdx]
140+
input_id_field.setName('InputID')
141+
fields.append(input_id_field)
126142
if matType == 0:
127-
self.writer.addRecord(['InputID', 'TargetID', 'Distance'])
143+
target_id_field = target_source.fields()[outIdx]
144+
target_id_field.setName('TargetID')
145+
fields.append(target_id_field)
146+
fields.append(QgsField('Distance', QVariant.Double))
128147
else:
129-
self.writer.addRecord(['InputID', 'MEAN', 'STDDEV', 'MIN', 'MAX'])
148+
fields.append(QgsField('MEAN', QVariant.Double))
149+
fields.append(QgsField('STDDEV', QVariant.Double))
150+
fields.append(QgsField('MIN', QVariant.Double))
151+
fields.append(QgsField('MAX', QVariant.Double))
130152

131-
index = QgsProcessingUtils.createSpatialIndex(targetLayer, context)
153+
out_wkb = QgsWkbTypes.multiType(source.wkbType()) if matType == 0 else source.wkbType()
154+
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
155+
fields, out_wkb, source.sourceCrs())
132156

133-
inIdx = inLayer.fields().lookupField(inField)
134-
outIdx = targetLayer.fields().lookupField(targetField)
157+
index = QgsSpatialIndex(target_source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]).setDestinationCrs(source.sourceCrs())), feedback)
135158

136159
distArea = QgsDistanceArea()
137-
distArea.setSourceCrs(inLayer.crs())
160+
distArea.setSourceCrs(source.sourceCrs())
138161
distArea.setEllipsoid(QgsProject.instance().ellipsoid())
139162

140-
features = QgsProcessingUtils.getFeatures(inLayer, context)
141-
total = 100.0 / inLayer.featureCount() if inLayer.featureCount() else 0
163+
features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([inIdx]))
164+
total = 100.0 / source.featureCount() if source.featureCount() else 0
142165
for current, inFeat in enumerate(features):
166+
if feedback.isCanceled():
167+
break
168+
143169
inGeom = inFeat.geometry()
144170
inID = str(inFeat.attributes()[inIdx])
145171
featList = index.nearestNeighbor(inGeom.asPoint(), nPoints)
146172
distList = []
147173
vari = 0.0
148-
request = QgsFeatureRequest().setFilterFids(featList).setSubsetOfAttributes([outIdx])
149-
for outFeat in targetLayer.getFeatures(request):
174+
request = QgsFeatureRequest().setFilterFids(featList).setSubsetOfAttributes([outIdx]).setDestinationCrs(source.sourceCrs())
175+
for outFeat in target_source.getFeatures(request):
176+
if feedback.isCanceled():
177+
break
178+
150179
outID = outFeat.attributes()[outIdx]
151180
outGeom = outFeat.geometry()
152181
dist = distArea.measureLine(inGeom.asPoint(),
153182
outGeom.asPoint())
183+
154184
if matType == 0:
155-
self.writer.addRecord([inID, str(outID), str(dist)])
185+
out_feature = QgsFeature()
186+
out_geom = QgsGeometry.unaryUnion([inFeat.geometry(), outFeat.geometry()])
187+
out_feature.setGeometry(out_geom)
188+
out_feature.setAttributes([inID, outID, dist])
189+
sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
156190
else:
157191
distList.append(float(dist))
158192

@@ -161,44 +195,61 @@ def linearMatrix(self, context, inLayer, inField, targetLayer, targetField,
161195
for i in distList:
162196
vari += (i - mean) * (i - mean)
163197
vari = math.sqrt(vari / len(distList))
164-
self.writer.addRecord([inID, str(mean),
165-
str(vari), str(min(distList)),
166-
str(max(distList))])
198+
199+
out_feature = QgsFeature()
200+
out_feature.setGeometry(inFeat.geometry())
201+
out_feature.setAttributes([inID, mean, vari, min(distList), max(distList)])
202+
sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
167203

168204
feedback.setProgress(int(current * total))
169205

170-
def regularMatrix(self, context, inLayer, inField, targetLayer, targetField,
206+
return {self.OUTPUT: dest_id}
207+
208+
def regularMatrix(self, parameters, context, source, inField, target_source, targetField,
171209
nPoints, feedback):
172-
index = QgsProcessingUtils.createSpatialIndex(targetLayer, context)
173210

174-
inIdx = inLayer.fields().lookupField(inField)
211+
index = QgsSpatialIndex(target_source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]).setDestinationCrs(source.sourceCrs())), feedback)
212+
inIdx = source.fields().lookupField(inField)
175213

176214
distArea = QgsDistanceArea()
177-
distArea.setSourceCrs(inLayer.sourceCrs())
215+
distArea.setSourceCrs(source.sourceCrs())
178216
distArea.setEllipsoid(QgsProject.instance().ellipsoid())
179217

180218
first = True
181-
features = QgsProcessingUtils.getFeatures(inLayer, context)
182-
total = 100.0 / inLayer.featureCount() if inLayer.featureCount() else 0
219+
sink = None
220+
dest_id = None
221+
features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([inIdx]))
222+
total = 100.0 / source.featureCount() if source.featureCount() else 0
183223
for current, inFeat in enumerate(features):
224+
if feedback.isCanceled():
225+
break
226+
184227
inGeom = inFeat.geometry()
185228
inID = str(inFeat.attributes()[inIdx])
186229
featList = index.nearestNeighbor(inGeom.asPoint(), nPoints)
187230
if first:
188231
first = False
189-
data = ['ID']
232+
fields = QgsFields()
233+
input_id_field = source.fields()[inIdx]
234+
input_id_field.setName('ID')
235+
fields.append(input_id_field)
190236
for i in range(len(featList)):
191-
data.append('DIST_{0}'.format(i + 1))
192-
self.writer.addRecord(data)
237+
fields.append(QgsField('DIST_{0}'.format(i + 1), QVariant.Double))
238+
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
239+
fields, source.wkbType(), source.sourceCrs())
193240

194241
data = [inID]
195-
for i in featList:
196-
request = QgsFeatureRequest().setFilterFid(i)
197-
outFeat = next(targetLayer.getFeatures(request))
198-
outGeom = outFeat.geometry()
242+
for target in target_source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([]).setFilterFids(featList).setDestinationCrs(source.sourceCrs())):
243+
if feedback.isCanceled():
244+
break
245+
outGeom = target.geometry()
199246
dist = distArea.measureLine(inGeom.asPoint(),
200247
outGeom.asPoint())
201-
data.append(str(float(dist)))
202-
self.writer.addRecord(data)
203-
248+
data.append(float(dist))
249+
out_feature = QgsFeature()
250+
out_feature.setGeometry(inGeom)
251+
out_feature.setAttributes(data)
252+
sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
204253
feedback.setProgress(int(current * total))
254+
255+
return {self.OUTPUT: dest_id}

python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from .MeanCoords import MeanCoords
7070
from .Merge import Merge
7171
from .NearestNeighbourAnalysis import NearestNeighbourAnalysis
72+
from .PointDistance import PointDistance
7273
from .PointsInPolygon import PointsInPolygon
7374
from .PointsLayerFromTable import PointsLayerFromTable
7475
from .PolygonsToLines import PolygonsToLines
@@ -93,7 +94,6 @@
9394
from .ZonalStatistics import ZonalStatistics
9495

9596
# from .ExtractByLocation import ExtractByLocation
96-
# from .PointDistance import PointDistance
9797
# from .UniqueValues import UniqueValues
9898
# from .ExportGeometryInfo import ExportGeometryInfo
9999
# from .SinglePartsToMultiparts import SinglePartsToMultiparts
@@ -184,7 +184,7 @@ def __init__(self):
184184

185185
def getAlgs(self):
186186
# algs = [
187-
# UniqueValues(), PointDistance(),
187+
# UniqueValues(),
188188
# ExportGeometryInfo(),
189189
# SinglePartsToMultiparts(),
190190
# ExtractNodes(),
@@ -263,6 +263,7 @@ def getAlgs(self):
263263
MeanCoords(),
264264
Merge(),
265265
NearestNeighbourAnalysis(),
266+
PointDistance(),
266267
PointsInPolygon(),
267268
PointsLayerFromTable(),
268269
PolygonsToLines(),
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<GMLFeatureClassList>
2+
<GMLFeatureClass>
3+
<Name>distance_matrix</Name>
4+
<ElementPath>distance_matrix</ElementPath>
5+
<!--MULTIPOINT-->
6+
<GeometryType>4</GeometryType>
7+
<SRSName>EPSG:4326</SRSName>
8+
<DatasetSpecificInfo>
9+
<FeatureCount>81</FeatureCount>
10+
<ExtentXMin>0.00000</ExtentXMin>
11+
<ExtentXMax>8.00000</ExtentXMax>
12+
<ExtentYMin>-5.00000</ExtentYMin>
13+
<ExtentYMax>3.00000</ExtentYMax>
14+
</DatasetSpecificInfo>
15+
<PropertyDefn>
16+
<Name>InputID</Name>
17+
<ElementPath>InputID</ElementPath>
18+
<Type>String</Type>
19+
<Width>8</Width>
20+
</PropertyDefn>
21+
<PropertyDefn>
22+
<Name>TargetID</Name>
23+
<ElementPath>TargetID</ElementPath>
24+
<Type>String</Type>
25+
<Width>8</Width>
26+
</PropertyDefn>
27+
<PropertyDefn>
28+
<Name>Distance</Name>
29+
<ElementPath>Distance</ElementPath>
30+
<Type>Real</Type>
31+
</PropertyDefn>
32+
</GMLFeatureClass>
33+
</GMLFeatureClassList>

0 commit comments

Comments
 (0)