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,
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))
211 (output, outputId) = self.parameterAsSink(
218 # If source was not found, throw an exception to indicate that the algorithm
219 # encountered a fatal error. The exception text can be any string, but in this
220 # case we use the pre-built invalidSourceError method to return a standard
221 # helper text for when a source cannot be evaluated
223 raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE))
225 raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN))
227 # Compute the number of steps to display within the progress bar and
228 # get features from source
229 total = 100.0 / pipe.featureCount() if pipe.featureCount() else 0
231 # Dictionary from span feature ids to lengths
234 # Dictionary from span feature ids to lists of lists of QgsPoint objects
237 spanFeatures = span.getFeatures()
238 pipeFeatures = pipe.getFeatures()
240 for counter, feature in enumerate(pipeFeatures):
241 if feedback.isCanceled(): break
243 geometry = feature.geometry()
244 if geometry == None: continue
246 fatherID = feature[self.FATHER_ID]
249 myLength = geometry.length()
251 oldLength = lengths.get(fatherID, 0.0)
252 lengths[fatherID] = oldLength + myLength
255 pointList = strings.get(fatherID, [])
256 # feedback.pushInfo('Point list: {}'.format(pointList))
259 vertices = geometry.vertices()
260 while vertices.hasNext():
261 mylist.append(vertices.next())
263 # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
265 pointList.append(mylist)
266 strings[fatherID] = pointList
268 # Update the progress bar
269 feedback.setProgress(int(counter * total))
271 if feedback.isCanceled():
274 feedback.pushInfo('Done')
278 feedback.pushInfo('Started editing')
279 feedback.pushInfo(str(spanFeatures))
281 for feature in spanFeatures:
282 if feedback.isCanceled(): break
284 #feedback.pushInfo(str(feature))
286 #feedback.pushInfo(str(id))
289 myLength = feature['Length']
291 # Ignore short stumps
292 if myLength <= minLength:
296 mypoints = list(feature.geometry().vertices())
297 mylist = strings.get(id, None)
299 feedback.pushInfo('No points for feature {}'.format(id))
302 #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
304 head = feature.geometry().vertices().next()
307 #feedback.pushInfo(str(resultList))
309 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
311 mylist = list(map(lambda x: list(reversed(x)), mylist))
312 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
314 feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
318 vertices = mylist.pop(i)
322 resultList.extend(vertices[1:])
323 if tail.distance(mypoints[-1]) <= eps:
326 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
328 vertices = mylist.pop(i)
330 i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
332 vertices = list(reversed(mylist.pop(i)))
334 # feedback.pushInfo(str(resultList))
337 result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
339 # feedback.pushInfo('Feature {}: {}'.format(id, result))
341 outputFeature = QgsFeature()
342 outputFeature.setFields(outputFields)
343 for i, x in enumerate(feature.attributes()):
344 fieldName = sourceFields[i].name()
345 outputFeature[fieldName] = x
347 outputFeature['x1'] = feature['x1']
348 outputFeature['y1'] = feature['y1']
349 outputFeature['z1'] = feature['z1']
350 outputFeature['x2'] = feature['x2']
351 outputFeature['y2'] = feature['y2']
352 outputFeature['z2'] = feature['z2']
353 outputFeature['Length'] = feature['Length'] # myLength
354 outputFeature['LineString'] = result
356 label = feature['Label']
357 m = self.DN_PATTERN.fullmatch(label)
359 outputFeature['DimensionDN'] = int(m.group(1))
361 output.addFeature(outputFeature)
363 feedback.pushInfo('Loop done')
365 #if feedback.isCanceled():
368 # span.commitChanges()
370 feedback.pushInfo('Changes committed')
372 # Return the results of the algorithm. In this case our only result is
373 # the feature sink which contains the processed features, but some
374 # algorithms may return multiple feature sinks, calculated numeric
375 # statistics, etc. These should all be included in the returned
376 # dictionary, with keys matching the feature corresponding parameter
378 return {self.OUTPUT: outputId}