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,
31 class SpanCoordinatesAlgorithm(QgsProcessingAlgorithm):
33 This is an example algorithm that takes a vector layer and
34 creates a new identical one.
36 It is meant to be used as an example of how to create your own
37 algorithms and explain methods and variables used to do it. An
38 algorithm like this will be available in all elements, and there
39 is not need for additional work.
41 All Processing algorithms should extend the QgsProcessingAlgorithm
45 # Constants used to refer to parameters and outputs. They will be
46 # used when calling the algorithm from another algorithm, or when
47 # calling from the QGIS console.
51 TOLERANCE = 'TOLERANCE'
54 # Constants for feature field names
55 FATHER_ID = 'FatherId'
56 LINESTRING = 'LineString'
58 # RegExp for DN values in
59 DN_PATTERN = re.compile(r"DN(\d+)(-.*)?")
63 Returns a translatable string with the self.tr() function.
65 return QCoreApplication.translate('Processing', string)
67 def createInstance(self):
68 return SpanCoordinatesAlgorithm()
72 Returns the algorithm name, used for identifying the algorithm. This
73 string should be fixed for the algorithm, and must not be localised.
74 The name should be unique within each provider. Names should contain
75 lowercase alphanumeric characters only and no spaces or other
76 formatting characters.
78 return 'calculateDistrictCSV'
80 def displayName(self):
82 Returns the translated algorithm name, which should be used for any
83 user-visible display of the algorithm name.
85 return self.tr('Calculate CSV for Apros District')
89 Returns the name of the group this algorithm belongs to. This string
92 return self.tr('District network scripts')
96 Returns the unique ID of the group this algorithm belongs to. This
97 string should be fixed for the algorithm, and must not be localised.
98 The group id should be unique within each provider. Group id should
99 contain lowercase alphanumeric characters only and no spaces or other
100 formatting characters.
102 return 'districtscripts'
104 def shortHelpString(self):
106 Returns a localised short helper string for the algorithm. This string
107 should provide a basic description about what the algorithm does and the
108 parameters and outputs associated with it..
110 return self.tr("Calculate a string of coordinate points for a point span")
112 def initAlgorithm(self, config=None):
114 Here we define the inputs and output of the algorithm, along
115 with some other properties.
118 # We add the input vector features source. It can have any kind of
121 QgsProcessingParameterVectorLayer(
123 self.tr('Pipeline layer'),
124 [QgsProcessing.TypeVectorLine]
129 QgsProcessingParameterVectorLayer(
131 self.tr('Point span layer'),
132 [QgsProcessing.TypeVectorLine]
137 QgsProcessingParameterDistance(
139 self.tr('Location tolerance'),
146 QgsProcessingParameterFeatureSink(
148 self.tr('Output layer'),
149 QgsProcessing.TypeVectorLine
153 def processAlgorithm(self, parameters, context, feedback):
155 Here is where the processing itself takes place.
158 # Retrieve the feature source and sink. The 'dest_id' variable is used
159 # to uniquely identify the feature sink, and must be included in the
160 # dictionary returned by the processAlgorithm function.
161 pipe = self.parameterAsLayer(
167 span = self.parameterAsLayer(
173 eps = self.parameterAsDouble(
179 sourceFields = span.fields()
180 sourceNames = sourceFields.names()
181 outputFields = QgsFields(sourceFields)
182 if not ('x1' in sourceNames): outputFields.append(QgsField('x1', QVariant.Double))
183 if not ('y1' in sourceNames): outputFields.append(QgsField('y1', QVariant.Double))
184 if not ('z1' in sourceNames): outputFields.append(QgsField('z1', QVariant.Double))
185 if not ('x2' in sourceNames): outputFields.append(QgsField('x2', QVariant.Double))
186 if not ('y2' in sourceNames): outputFields.append(QgsField('y2', QVariant.Double))
187 if not ('z2' in sourceNames): outputFields.append(QgsField('z2', QVariant.Double))
188 if not ('Length' in sourceNames): outputFields.append(QgsField('Length', QVariant.Double))
189 if not ('LineString' in sourceNames): outputFields.append(QgsField('LineString', QVariant.String))
190 if not ('DimensionDN' in sourceNames): outputFields.append(QgsField('DimensionDN', QVariant.Int))
192 (output, outputId) = self.parameterAsSink(
199 # If source was not found, throw an exception to indicate that the algorithm
200 # encountered a fatal error. The exception text can be any string, but in this
201 # case we use the pre-built invalidSourceError method to return a standard
202 # helper text for when a source cannot be evaluated
204 raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE))
206 raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN))
208 # Compute the number of steps to display within the progress bar and
209 # get features from source
210 total = 100.0 / pipe.featureCount() if pipe.featureCount() else 0
212 # Dictionary from span feature ids to lengths
215 # Dictionary from span feature ids to lists of lists of QgsPoint objects
218 spanFeatures = span.getFeatures()
219 pipeFeatures = pipe.getFeatures()
221 for counter, feature in enumerate(pipeFeatures):
222 if feedback.isCanceled(): break
224 geometry = feature.geometry()
225 if geometry == None: continue
227 fatherID = feature[self.FATHER_ID]
230 length = geometry.length()
232 oldLength = lengths.get(fatherID, 0.0)
233 lengths[fatherID] = oldLength + length
236 pointList = strings.get(fatherID, [])
237 # feedback.pushInfo('Point list: {}'.format(pointList))
240 vertices = geometry.vertices()
241 while vertices.hasNext():
242 mylist.append(vertices.next())
244 # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
246 pointList.append(mylist)
247 strings[fatherID] = pointList
249 # Update the progress bar
250 feedback.setProgress(int(counter * total))
252 if feedback.isCanceled():
255 feedback.pushInfo('Done')
259 feedback.pushInfo('Started editing')
260 feedback.pushInfo(str(spanFeatures))
262 for feature in spanFeatures:
263 if feedback.isCanceled(): break
265 #feedback.pushInfo(str(feature))
267 #feedback.pushInfo(str(id))
270 length = lengths.get(id, None)
273 mypoints = list(feature.geometry().vertices())
274 mylist = strings.get(id, None)
276 feedback.pushInfo('No points for feature {}'.format(id))
279 #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
281 head = feature.geometry().vertices().next()
284 #feedback.pushInfo(str(resultList))
286 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
288 mylist = list(map(lambda x: list(reversed(x)), mylist))
289 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
291 feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
295 vertices = mylist.pop(i)
299 resultList.extend(vertices[1:])
300 if tail.distance(mypoints[-1]) <= eps:
303 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
305 vertices = mylist.pop(i)
307 i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
309 vertices = list(reversed(mylist.pop(i)))
311 # feedback.pushInfo(str(resultList))
314 result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
316 # feedback.pushInfo('Feature {}: {}'.format(id, result))
318 outputFeature = QgsFeature()
319 outputFeature.setFields(outputFields)
320 for i, x in enumerate(feature.attributes()):
321 fieldName = sourceFields[i].name()
322 outputFeature[fieldName] = x
324 outputFeature['x1'] = feature['x1']
325 outputFeature['y1'] = feature['y1']
326 outputFeature['z1'] = feature['z1']
327 outputFeature['x2'] = feature['x2']
328 outputFeature['y2'] = feature['y2']
329 outputFeature['z2'] = feature['z2']
330 outputFeature['Length'] = feature['length'] # length
331 outputFeature['LineString'] = result
333 label = feature['Label']
334 m = self.DN_PATTERN.fullmatch(label)
336 outputFeature['DimensionDN'] = int(m.group(1))
338 output.addFeature(outputFeature)
340 feedback.pushInfo('Loop done')
342 #if feedback.isCanceled():
345 # span.commitChanges()
347 feedback.pushInfo('Changes committed')
349 # Return the results of the algorithm. In this case our only result is
350 # the feature sink which contains the processed features, but some
351 # algorithms may return multiple feature sinks, calculated numeric
352 # statistics, etc. These should all be included in the returned
353 # dictionary, with keys matching the feature corresponding parameter
355 return {self.OUTPUT: outputId}