]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py
Add minimum span length paremeter to CSV generator script.
[simantics/district.git] / org.simantics.district.feature / rootFiles / QGIS scripts / generateCSV.py
1 # -*- coding: utf-8 -*-
2
3 """
4 ***************************************************************************
5 *                                                                         *
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.                                   *
10 *                                                                         *
11 ***************************************************************************
12 """
13
14 from PyQt5.QtCore import (QCoreApplication, QVariant)
15 from qgis.core import (QgsProcessing,
16                        QgsFeatureSink,
17                        QgsFeature,
18                        QgsProcessingException,
19                        QgsProcessingAlgorithm,
20                        QgsProcessingParameterFeatureSource,
21                        QgsProcessingParameterFeatureSink,
22                        QgsProcessingParameterVectorLayer,
23                        QgsProcessingParameterDistance,
24                        QgsVectorDataProvider,
25                        QgsFields,
26                        QgsField,
27                        QgsUnitTypes)
28 import processing
29 import re
30
31
32 class SpanCoordinatesAlgorithm(QgsProcessingAlgorithm):
33     """
34     This is an example algorithm that takes a vector layer and
35     creates a new identical one.
36
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.
41
42     All Processing algorithms should extend the QgsProcessingAlgorithm
43     class.
44     """
45
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.
49
50     PIPE = 'PIPE'
51     SPAN = 'SPAN'
52     TOLERANCE = 'TOLERANCE'
53     MINIMUM_LENGTH = 'MINIMUM_LENGTH'
54     OUTPUT = 'OUTPUT'
55     
56     # Constants for feature field names
57     FATHER_ID = 'FatherId'
58     LINESTRING = 'LineString'
59     
60     # RegExp for DN values in 
61     DN_PATTERN = re.compile(r"DN(\d+)(-.*)?")
62
63     def tr(self, string):
64         """
65         Returns a translatable string with the self.tr() function.
66         """
67         return QCoreApplication.translate('Processing', string)
68
69     def createInstance(self):
70         return SpanCoordinatesAlgorithm()
71
72     def name(self):
73         """
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.
79         """
80         return 'calculateDistrictCSV'
81
82     def displayName(self):
83         """
84         Returns the translated algorithm name, which should be used for any
85         user-visible display of the algorithm name.
86         """
87         return self.tr('Calculate CSV for Apros District')
88
89     def group(self):
90         """
91         Returns the name of the group this algorithm belongs to. This string
92         should be localised.
93         """
94         return self.tr('District network scripts')
95
96     def groupId(self):
97         """
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.
103         """
104         return 'districtscripts'
105
106     def shortHelpString(self):
107         """
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..
111         """
112         return self.tr("Calculate a string of coordinate points for a point span")
113
114     def initAlgorithm(self, config=None):
115         """
116         Here we define the inputs and output of the algorithm, along
117         with some other properties.
118         """
119
120         # We add the input vector features source. It can have any kind of
121         # geometry.
122         self.addParameter(
123             QgsProcessingParameterVectorLayer(
124                 self.PIPE,
125                 self.tr('Pipeline layer'),
126                 [QgsProcessing.TypeVectorLine]
127             )
128         )
129
130         self.addParameter(
131             QgsProcessingParameterVectorLayer(
132                 self.SPAN,
133                 self.tr('Point span layer'),
134                 [QgsProcessing.TypeVectorLine]
135             )
136         )
137         
138         tol = QgsProcessingParameterDistance(
139             self.TOLERANCE,
140             self.tr('Location tolerance'),
141             0.001,
142             minValue = 0.0
143         )
144         tol.setDefaultUnit(QgsUnitTypes.DistanceMeters)
145         self.addParameter( tol )
146         
147         dist = QgsProcessingParameterDistance(
148             self.MINIMUM_LENGTH,
149             self.tr('Minimum span length'),
150             0.25,
151             minValue = 0.0
152         )
153         dist.setDefaultUnit(QgsUnitTypes.DistanceMeters)
154         self.addParameter( dist )
155         
156         self.addParameter(
157             QgsProcessingParameterFeatureSink(
158                 self.OUTPUT,
159                 self.tr('Output layer'),
160                 QgsProcessing.TypeVectorLine
161             )
162         )
163
164     def processAlgorithm(self, parameters, context, feedback):
165         """
166         Here is where the processing itself takes place.
167         """
168
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(
173             parameters,
174             self.PIPE,
175             context
176         )
177
178         span = self.parameterAsLayer(
179             parameters,
180             self.SPAN,
181             context
182         )
183         
184         eps = self.parameterAsDouble(
185             parameters,
186             self.TOLERANCE,
187             context
188         )
189         
190         minLength = self.parameterAsDouble(
191             parameters,
192             self.MINIMUM_LENGTH,
193             context
194         )
195         
196         feedback.pushInfo('Tolerance: {} m\nMinimum span length: {} m'.format(eps, minLength))
197         
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         
211         (output, outputId) = self.parameterAsSink(
212             parameters,
213             self.OUTPUT,
214             context,
215             outputFields
216         )
217
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
222         if pipe is None:
223             raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE))
224         if span is None:
225             raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN))
226
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
230         
231         # Dictionary from span feature ids to lengths
232         lengths = dict()
233         
234         # Dictionary from span feature ids to lists of lists of QgsPoint objects
235         strings = dict()
236         
237         spanFeatures = span.getFeatures()
238         pipeFeatures = pipe.getFeatures()
239         
240         for counter, feature in enumerate(pipeFeatures):
241             if feedback.isCanceled(): break
242                 
243             geometry = feature.geometry()
244             if geometry == None: continue
245             
246             fatherID = feature[self.FATHER_ID]
247             
248             # Length
249             myLength = geometry.length()
250             
251             oldLength = lengths.get(fatherID, 0.0)
252             lengths[fatherID] = oldLength + myLength
253             
254             # Segment points
255             pointList = strings.get(fatherID, [])
256             # feedback.pushInfo('Point list: {}'.format(pointList))
257             mylist = []
258             
259             vertices = geometry.vertices()
260             while vertices.hasNext():
261                 mylist.append(vertices.next())
262                 
263             # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
264             
265             pointList.append(mylist)
266             strings[fatherID] = pointList
267             
268             # Update the progress bar
269             feedback.setProgress(int(counter * total))
270
271         if feedback.isCanceled():
272             return
273
274         feedback.pushInfo('Done')
275         
276         #span.startEditing()
277         
278         feedback.pushInfo('Started editing')
279         feedback.pushInfo(str(spanFeatures))
280         
281         for feature in spanFeatures:
282             if feedback.isCanceled(): break
283
284             #feedback.pushInfo(str(feature))
285             id = feature['Id']
286             #feedback.pushInfo(str(id))
287
288             # Length
289             myLength = feature['Length']
290             
291             # Ignore short stumps
292             if myLength <= minLength:
293                 continue
294             
295             # Vertices
296             mypoints = list(feature.geometry().vertices())
297             mylist = strings.get(id, None)
298             if mylist == None:
299                 feedback.pushInfo('No points for feature {}'.format(id))
300                 mylist = [mypoints]
301             
302             #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
303             
304             head = feature.geometry().vertices().next()
305             resultList = [head]
306             
307             #feedback.pushInfo(str(resultList))
308             
309             i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
310             if i == 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)
313                 if i == None:
314                     feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
315                     mylist = [mypoints]
316                     i = 0
317             
318             vertices = mylist.pop(i)
319             
320             while i != None:
321                 tail = vertices[-1]
322                 resultList.extend(vertices[1:])
323                 if tail.distance(mypoints[-1]) <= eps:
324                     break
325                 
326                 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
327                 if i != None:
328                     vertices = mylist.pop(i)
329                 else:
330                     i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
331                     if i != None:
332                         vertices = list(reversed(mylist.pop(i)))
333
334             # feedback.pushInfo(str(resultList))
335
336             # Convert to string
337             result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
338
339             # feedback.pushInfo('Feature {}: {}'.format(id, result))
340             
341             outputFeature = QgsFeature()
342             outputFeature.setFields(outputFields)
343             for i, x in enumerate(feature.attributes()):
344                 fieldName = sourceFields[i].name()
345                 outputFeature[fieldName] = x
346             
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
355             
356             label = feature['Label']
357             m = self.DN_PATTERN.fullmatch(label)
358             if m:
359                 outputFeature['DimensionDN'] = int(m.group(1))
360             
361             output.addFeature(outputFeature)
362             
363         feedback.pushInfo('Loop done')
364         
365         #if feedback.isCanceled():
366         #    span.rollBack()
367         #else:
368         #    span.commitChanges()
369
370         feedback.pushInfo('Changes committed')
371         
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
377         # or output names.
378         return {self.OUTPUT: outputId}