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 SpanCoordinatesAlgorithm3(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 SpanCoordinatesAlgorithm3()
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 'calculateDistrictCSV3'
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 (TechTypeData)')
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 ('TechTypeId' in sourceNames): outputFields.append(QgsField('TechTypeId', QVariant.String))
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]
254 if feature.fields().lookupField('Length') != -1:
255 myLength = feature['Length']
257 myLength = geometry.length()
259 oldLength = lengths.get(fatherID, 0.0)
260 lengths[fatherID] = oldLength + myLength
263 pointList = strings.get(fatherID, [])
264 # feedback.pushInfo('Point list: {}'.format(pointList))
267 vertices = geometry.vertices()
268 while vertices.hasNext():
269 mylist.append(vertices.next())
271 # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
273 pointList.append(mylist)
274 strings[fatherID] = pointList
276 # Store the value of PipeStruct
277 t = feature['PipeStruct']
278 tt = types.get(fatherID, {})
284 # Update the progress bar
285 feedback.setProgress(int(counter * total))
287 if feedback.isCanceled():
290 feedback.pushInfo('Done')
294 feedback.pushInfo('Started editing')
295 feedback.pushInfo(str(spanFeatures))
297 for feature in spanFeatures:
298 if feedback.isCanceled(): break
302 #feedback.pushInfo(str(feature))
304 #feedback.pushInfo(str(id))
308 if f.lookupField('Length') != -1:
309 myLength = feature['Length']
311 myLength = feature.geometry().length()
313 # Ignore short stumps
314 if myLength <= minLength:
318 mypoints = list(feature.geometry().vertices())
319 mylist = strings.get(id, None)
321 feedback.pushInfo('No points for feature {}'.format(id))
324 #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
326 head = feature.geometry().vertices().next()
329 #feedback.pushInfo(str(resultList))
331 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
333 mylist = list(map(lambda x: list(reversed(x)), mylist))
334 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
336 feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
340 vertices = mylist.pop(i)
344 resultList.extend(vertices[1:])
345 if tail.distance(mypoints[-1]) <= eps:
348 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
350 vertices = mylist.pop(i)
352 i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
354 vertices = list(reversed(mylist.pop(i)))
356 # feedback.pushInfo(str(resultList))
359 result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
361 # feedback.pushInfo('Feature {}: {}'.format(id, result))
363 outputFeature = QgsFeature()
364 outputFeature.setFields(outputFields)
365 for i, x in enumerate(feature.attributes()):
366 fieldName = sourceFields[i].name()
367 outputFeature[fieldName] = x
369 vts = list(feature.geometry().vertices())
373 outputFeature['x1'] = feature['x1'] if f.lookupField('x1') != -1 else p1.x()
374 outputFeature['y1'] = feature['y1'] if f.lookupField('y1') != -1 else p1.y()
375 outputFeature['z1'] = feature['z1'] if f.lookupField('z1') != -1 else p1.z()
376 outputFeature['x2'] = feature['x2'] if f.lookupField('x2') != -1 else p2.x()
377 outputFeature['y2'] = feature['y2'] if f.lookupField('y2') != -1 else p2.y()
378 outputFeature['z2'] = feature['z2'] if f.lookupField('z2') != -1 else p2.z()
379 outputFeature['Length'] = feature['Length'] if f.lookupField('Length') != -1 else feature.geometry().length()
380 outputFeature['LineString'] = result
382 # Handle pipe type codes
383 mytypes = list(types.get(id, {}).items())
384 if len(mytypes) == 0:
385 feedback.pushInfo('No type codes for feature {}'.format(id))
388 mytypes.sort(key = itemgetter(1))
389 feedback.pushInfo('No unique type code for feature {}: {}'.format(id, mytypes))
390 outputFeature['PipeStruct'] = mytypes[-1][0]
392 if f.lookupField('TechTypeId') != -1:
393 outputFeature['TechTypeId'] = feature['TechTypeId']
395 output.addFeature(outputFeature)
397 feedback.pushInfo('Loop done')
399 #if feedback.isCanceled():
402 # span.commitChanges()
404 feedback.pushInfo('Changes committed')
406 # Return the results of the algorithm. In this case our only result is
407 # the feature sink which contains the processed features, but some
408 # algorithms may return multiple feature sinks, calculated numeric
409 # statistics, etc. These should all be included in the returned
410 # dictionary, with keys matching the feature corresponding parameter
412 return {self.OUTPUT: outputId}