1 # -*- coding: utf-8 -*-
4 ***************************************************************************
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
11 ***************************************************************************
14 from PyQt5.QtCore import (QCoreApplication, QVariant)
15 from qgis.core import (QgsProcessing,
18 QgsProcessingException,
19 QgsProcessingAlgorithm,
20 QgsProcessingParameterFeatureSource,
21 QgsProcessingParameterFeatureSink,
22 QgsProcessingParameterVectorLayer,
23 QgsProcessingParameterDistance,
24 QgsVectorDataProvider,
30 from operator import itemgetter
32 class SpanCoordinatesAlgorithm(QgsProcessingAlgorithm):
34 This is an example algorithm that takes a vector layer and
35 creates a new identical one.
37 It is meant to be used as an example of how to create your own
38 algorithms and explain methods and variables used to do it. An
39 algorithm like this will be available in all elements, and there
40 is not need for additional work.
42 All Processing algorithms should extend the QgsProcessingAlgorithm
46 # Constants used to refer to parameters and outputs. They will be
47 # used when calling the algorithm from another algorithm, or when
48 # calling from the QGIS console.
52 TOLERANCE = 'TOLERANCE'
53 MINIMUM_LENGTH = 'MINIMUM_LENGTH'
56 # Constants for feature field names
57 FATHER_ID = 'FatherId'
58 LINESTRING = 'LineString'
60 # RegExp for DN values in
61 DN_PATTERN = re.compile(r"DN(\d+)(-.*)?")
65 Returns a translatable string with the self.tr() function.
67 return QCoreApplication.translate('Processing', string)
69 def createInstance(self):
70 return SpanCoordinatesAlgorithm()
74 Returns the algorithm name, used for identifying the algorithm. This
75 string should be fixed for the algorithm, and must not be localised.
76 The name should be unique within each provider. Names should contain
77 lowercase alphanumeric characters only and no spaces or other
78 formatting characters.
80 return 'calculateDistrictCSV'
82 def displayName(self):
84 Returns the translated algorithm name, which should be used for any
85 user-visible display of the algorithm name.
87 return self.tr('Calculate CSV for Apros District')
91 Returns the name of the group this algorithm belongs to. This string
94 return self.tr('District network scripts')
98 Returns the unique ID of the group this algorithm belongs to. This
99 string should be fixed for the algorithm, and must not be localised.
100 The group id should be unique within each provider. Group id should
101 contain lowercase alphanumeric characters only and no spaces or other
102 formatting characters.
104 return 'districtscripts'
106 def shortHelpString(self):
108 Returns a localised short helper string for the algorithm. This string
109 should provide a basic description about what the algorithm does and the
110 parameters and outputs associated with it..
112 return self.tr("Calculate a string of coordinate points for a point span")
114 def initAlgorithm(self, config=None):
116 Here we define the inputs and output of the algorithm, along
117 with some other properties.
120 # We add the input vector features source. It can have any kind of
123 QgsProcessingParameterVectorLayer(
125 self.tr('Pipeline layer'),
126 [QgsProcessing.TypeVectorLine]
131 QgsProcessingParameterVectorLayer(
133 self.tr('Point span layer'),
134 [QgsProcessing.TypeVectorLine]
138 tol = QgsProcessingParameterDistance(
140 self.tr('Location tolerance'),
144 tol.setDefaultUnit(QgsUnitTypes.DistanceMeters)
145 self.addParameter( tol )
147 dist = QgsProcessingParameterDistance(
149 self.tr('Minimum span length'),
153 dist.setDefaultUnit(QgsUnitTypes.DistanceMeters)
154 self.addParameter( dist )
157 QgsProcessingParameterFeatureSink(
159 self.tr('Output layer'),
160 QgsProcessing.TypeVectorLine
164 def processAlgorithm(self, parameters, context, feedback):
166 Here is where the processing itself takes place.
169 # Retrieve the feature source and sink. The 'dest_id' variable is used
170 # to uniquely identify the feature sink, and must be included in the
171 # dictionary returned by the processAlgorithm function.
172 pipe = self.parameterAsLayer(
178 span = self.parameterAsLayer(
184 eps = self.parameterAsDouble(
190 minLength = self.parameterAsDouble(
196 feedback.pushInfo('Tolerance: {} m\nMinimum span length: {} m'.format(eps, minLength))
198 sourceFields = span.fields()
199 sourceNames = sourceFields.names()
200 outputFields = QgsFields(sourceFields)
201 if not ('x1' in sourceNames): outputFields.append(QgsField('x1', QVariant.Double))
202 if not ('y1' in sourceNames): outputFields.append(QgsField('y1', QVariant.Double))
203 if not ('z1' in sourceNames): outputFields.append(QgsField('z1', QVariant.Double))
204 if not ('x2' in sourceNames): outputFields.append(QgsField('x2', QVariant.Double))
205 if not ('y2' in sourceNames): outputFields.append(QgsField('y2', QVariant.Double))
206 if not ('z2' in sourceNames): outputFields.append(QgsField('z2', QVariant.Double))
207 if not ('Length' in sourceNames): outputFields.append(QgsField('Length', QVariant.Double))
208 if not ('LineString' in sourceNames): outputFields.append(QgsField('LineString', QVariant.String))
209 if not ('DimensionDN' in sourceNames): outputFields.append(QgsField('DimensionDN', QVariant.Int))
210 if not ('PipeStruct' in sourceNames): outputFields.append(QgsField('PipeStruct', QVariant.String))
212 (output, outputId) = self.parameterAsSink(
219 # If source was not found, throw an exception to indicate that the algorithm
220 # encountered a fatal error. The exception text can be any string, but in this
221 # case we use the pre-built invalidSourceError method to return a standard
222 # helper text for when a source cannot be evaluated
224 raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE))
226 raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN))
228 # Compute the number of steps to display within the progress bar and
229 # get features from source
230 total = 100.0 / pipe.featureCount() if pipe.featureCount() else 0
232 # Dictionary from span feature ids to lengths
235 # Dictionary from span feature ids to lists of lists of QgsPoint objects
238 # Dictionary for the PipeStruct field values
241 spanFeatures = span.getFeatures()
242 pipeFeatures = pipe.getFeatures()
244 for counter, feature in enumerate(pipeFeatures):
245 if feedback.isCanceled(): break
247 geometry = feature.geometry()
248 if geometry == None: continue
250 fatherID = feature[self.FATHER_ID]
253 myLength = feature['Length']
255 myLength = geometry.length()
257 oldLength = lengths.get(fatherID, 0.0)
258 lengths[fatherID] = oldLength + myLength
261 pointList = strings.get(fatherID, [])
262 # feedback.pushInfo('Point list: {}'.format(pointList))
265 vertices = geometry.vertices()
266 while vertices.hasNext():
267 mylist.append(vertices.next())
269 # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
271 pointList.append(mylist)
272 strings[fatherID] = pointList
274 # Store the value of PipeStruct
275 t = feature['PipeStruct']
276 tt = types.get(fatherID, {})
282 # Update the progress bar
283 feedback.setProgress(int(counter * total))
285 if feedback.isCanceled():
288 feedback.pushInfo('Done')
292 feedback.pushInfo('Started editing')
293 feedback.pushInfo(str(spanFeatures))
295 for feature in spanFeatures:
296 if feedback.isCanceled(): break
298 #feedback.pushInfo(str(feature))
300 #feedback.pushInfo(str(id))
303 myLength = feature['Length']
305 # Ignore short stumps
306 if myLength <= minLength:
310 mypoints = list(feature.geometry().vertices())
311 mylist = strings.get(id, None)
313 feedback.pushInfo('No points for feature {}'.format(id))
316 #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
318 head = feature.geometry().vertices().next()
321 #feedback.pushInfo(str(resultList))
323 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
325 mylist = list(map(lambda x: list(reversed(x)), mylist))
326 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
328 feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
332 vertices = mylist.pop(i)
336 resultList.extend(vertices[1:])
337 if tail.distance(mypoints[-1]) <= eps:
340 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
342 vertices = mylist.pop(i)
344 i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
346 vertices = list(reversed(mylist.pop(i)))
348 # feedback.pushInfo(str(resultList))
351 result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
353 # feedback.pushInfo('Feature {}: {}'.format(id, result))
355 outputFeature = QgsFeature()
356 outputFeature.setFields(outputFields)
357 for i, x in enumerate(feature.attributes()):
358 fieldName = sourceFields[i].name()
359 outputFeature[fieldName] = x
361 outputFeature['x1'] = feature['x1']
362 outputFeature['y1'] = feature['y1']
363 outputFeature['z1'] = feature['z1']
364 outputFeature['x2'] = feature['x2']
365 outputFeature['y2'] = feature['y2']
366 outputFeature['z2'] = feature['z2']
367 outputFeature['Length'] = feature['Length'] # myLength
368 outputFeature['LineString'] = result
370 # Handle pipe type codes
371 mytypes = list(types.get(id, {}).items())
372 if len(mytypes) == 0:
373 feedback.pushInfo('No type codes for feature {}'.format(id))
376 mytypes.sort(key = itemgetter(1))
377 feedback.pushInfo('No unique type code for feature {}: {}'.format(id, mytypes))
378 outputFeature['PipeStruct'] = mytypes[-1][0]
380 label = feature['Label']
381 m = self.DN_PATTERN.fullmatch(label)
383 outputFeature['DimensionDN'] = int(m.group(1))
385 output.addFeature(outputFeature)
387 feedback.pushInfo('Loop done')
389 #if feedback.isCanceled():
392 # span.commitChanges()
394 feedback.pushInfo('Changes committed')
396 # Return the results of the algorithm. In this case our only result is
397 # the feature sink which contains the processed features, but some
398 # algorithms may return multiple feature sinks, calculated numeric
399 # statistics, etc. These should all be included in the returned
400 # dictionary, with keys matching the feature corresponding parameter
402 return {self.OUTPUT: outputId}