]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV2.py
Alternative QGIS scripts for different network data models.
[simantics/district.git] / org.simantics.district.feature / rootFiles / QGIS scripts / generateCSV2.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 from operator import itemgetter
31
32 class SpanCoordinatesAlgorithm2(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 SpanCoordinatesAlgorithm2()
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 'calculateDistrictCSV2'
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 (No coordinates)')
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         if not ('PipeStruct' in sourceNames): outputFields.append(QgsField('PipeStruct', QVariant.String))
211         
212         (output, outputId) = self.parameterAsSink(
213             parameters,
214             self.OUTPUT,
215             context,
216             outputFields
217         )
218
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
223         if pipe is None:
224             raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE))
225         if span is None:
226             raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN))
227
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
231         
232         # Dictionary from span feature ids to lengths
233         lengths = dict()
234         
235         # Dictionary from span feature ids to lists of lists of QgsPoint objects
236         strings = dict()
237         
238         # Dictionary for the PipeStruct field values
239         types = dict()
240         
241         spanFeatures = span.getFeatures()
242         pipeFeatures = pipe.getFeatures()
243         
244         for counter, feature in enumerate(pipeFeatures):
245             if feedback.isCanceled(): break
246                 
247             geometry = feature.geometry()
248             if geometry == None: continue
249             
250             fatherID = feature[self.FATHER_ID]
251             
252             # Length
253             myLength = None
254             if feature.fields().lookupField('Length') != -1:
255                 myLength = feature['Length']
256             if myLength == None:
257                 myLength = geometry.length()
258             
259             oldLength = lengths.get(fatherID, 0.0)
260             lengths[fatherID] = oldLength + myLength
261             
262             # Segment points
263             pointList = strings.get(fatherID, [])
264             # feedback.pushInfo('Point list: {}'.format(pointList))
265             mylist = []
266             
267             vertices = geometry.vertices()
268             while vertices.hasNext():
269                 mylist.append(vertices.next())
270                 
271             # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
272             
273             pointList.append(mylist)
274             strings[fatherID] = pointList
275             
276             # Store the value of PipeStruct
277             t = feature['PipeStruct']
278             tt = types.get(fatherID, {})
279             types[fatherID] = tt
280             c = tt.get(t, 0)
281             c += myLength
282             tt[t] = c
283             
284             # Update the progress bar
285             feedback.setProgress(int(counter * total))
286
287         if feedback.isCanceled():
288             return
289
290         feedback.pushInfo('Done')
291         
292         #span.startEditing()
293         
294         feedback.pushInfo('Started editing')
295         feedback.pushInfo(str(spanFeatures))
296         
297         for feature in spanFeatures:
298             if feedback.isCanceled(): break
299
300             f = feature.fields()
301             
302             #feedback.pushInfo(str(feature))
303             id = feature['Id']
304             #feedback.pushInfo(str(id))
305
306             # Length
307             myLength = None
308             if f.lookupField('Length') != -1:
309                 myLength = feature['Length']
310             if myLength == None:
311                 myLength = feature.geometry().length()
312             
313             # Ignore short stumps
314             if myLength <= minLength:
315                 continue
316             
317             # Vertices
318             mypoints = list(feature.geometry().vertices())
319             mylist = strings.get(id, None)
320             if mylist == None:
321                 feedback.pushInfo('No points for feature {}'.format(id))
322                 mylist = [mypoints]
323             
324             #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
325             
326             head = feature.geometry().vertices().next()
327             resultList = [head]
328             
329             #feedback.pushInfo(str(resultList))
330             
331             i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
332             if i == 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)
335                 if i == None:
336                     feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
337                     mylist = [mypoints]
338                     i = 0
339             
340             vertices = mylist.pop(i)
341             
342             while i != None:
343                 tail = vertices[-1]
344                 resultList.extend(vertices[1:])
345                 if tail.distance(mypoints[-1]) <= eps:
346                     break
347                 
348                 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
349                 if i != None:
350                     vertices = mylist.pop(i)
351                 else:
352                     i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
353                     if i != None:
354                         vertices = list(reversed(mylist.pop(i)))
355
356             # feedback.pushInfo(str(resultList))
357
358             # Convert to string
359             result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
360
361             # feedback.pushInfo('Feature {}: {}'.format(id, result))
362             
363             outputFeature = QgsFeature()
364             outputFeature.setFields(outputFields)
365             for i, x in enumerate(feature.attributes()):
366                 fieldName = sourceFields[i].name()
367                 outputFeature[fieldName] = x
368             
369             vts = list(feature.geometry().vertices())
370             p1 = vts[0]
371             p2 = vts[-1]
372             
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
381             
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))
386             else:
387                 if len(mytypes) > 1:
388                     mytypes.sort(key = itemgetter(1))
389                     feedback.pushInfo('No unique type code for feature {}: {}'.format(id, mytypes))
390                 outputFeature['PipeStruct'] = mytypes[-1][0]
391             
392             if f.lookupField('Label') != -1:
393                 label = feature['Label']
394                 m = self.DN_PATTERN.fullmatch(label)
395                 if m:
396                     outputFeature['DimensionDN'] = int(m.group(1))
397             
398             output.addFeature(outputFeature)
399             
400         feedback.pushInfo('Loop done')
401         
402         #if feedback.isCanceled():
403         #    span.rollBack()
404         #else:
405         #    span.commitChanges()
406
407         feedback.pushInfo('Changes committed')
408         
409         # Return the results of the algorithm. In this case our only result is
410         # the feature sink which contains the processed features, but some
411         # algorithms may return multiple feature sinks, calculated numeric
412         # statistics, etc. These should all be included in the returned
413         # dictionary, with keys matching the feature corresponding parameter
414         # or output names.
415         return {self.OUTPUT: outputId}