]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py
Hide "enabled" column for non-component type tech type tables
[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 from operator import itemgetter
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         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 = feature['Length']
254             if myLength == None:
255                 myLength = geometry.length()
256             
257             oldLength = lengths.get(fatherID, 0.0)
258             lengths[fatherID] = oldLength + myLength
259             
260             # Segment points
261             pointList = strings.get(fatherID, [])
262             # feedback.pushInfo('Point list: {}'.format(pointList))
263             mylist = []
264             
265             vertices = geometry.vertices()
266             while vertices.hasNext():
267                 mylist.append(vertices.next())
268                 
269             # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
270             
271             pointList.append(mylist)
272             strings[fatherID] = pointList
273             
274             # Store the value of PipeStruct
275             t = feature['PipeStruct']
276             tt = types.get(fatherID, {})
277             types[fatherID] = tt
278             c = tt.get(t, 0)
279             c += myLength
280             tt[t] = c
281             
282             # Update the progress bar
283             feedback.setProgress(int(counter * total))
284
285         if feedback.isCanceled():
286             return
287
288         feedback.pushInfo('Done')
289         
290         #span.startEditing()
291         
292         feedback.pushInfo('Started editing')
293         feedback.pushInfo(str(spanFeatures))
294         
295         for feature in spanFeatures:
296             if feedback.isCanceled(): break
297
298             #feedback.pushInfo(str(feature))
299             id = feature['Id']
300             #feedback.pushInfo(str(id))
301
302             # Length
303             myLength = feature['Length']
304             
305             # Ignore short stumps
306             if myLength <= minLength:
307                 continue
308             
309             # Vertices
310             mypoints = list(feature.geometry().vertices())
311             mylist = strings.get(id, None)
312             if mylist == None:
313                 feedback.pushInfo('No points for feature {}'.format(id))
314                 mylist = [mypoints]
315             
316             #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
317             
318             head = feature.geometry().vertices().next()
319             resultList = [head]
320             
321             #feedback.pushInfo(str(resultList))
322             
323             i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
324             if i == None:
325                 mylist = list(map(lambda x: list(reversed(x)), mylist))
326                 i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
327                 if i == None:
328                     feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
329                     mylist = [mypoints]
330                     i = 0
331             
332             vertices = mylist.pop(i)
333             
334             while i != None:
335                 tail = vertices[-1]
336                 resultList.extend(vertices[1:])
337                 if tail.distance(mypoints[-1]) <= eps:
338                     break
339                 
340                 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
341                 if i != None:
342                     vertices = mylist.pop(i)
343                 else:
344                     i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
345                     if i != None:
346                         vertices = list(reversed(mylist.pop(i)))
347
348             # feedback.pushInfo(str(resultList))
349
350             # Convert to string
351             result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
352
353             # feedback.pushInfo('Feature {}: {}'.format(id, result))
354             
355             outputFeature = QgsFeature()
356             outputFeature.setFields(outputFields)
357             for i, x in enumerate(feature.attributes()):
358                 fieldName = sourceFields[i].name()
359                 outputFeature[fieldName] = x
360             
361             outputFeature['x1'] = feature['x1']
362             outputFeature['y1'] = feature['y1']
363             outputFeature['z1'] = feature['z1']
364             outputFeature['x2'] = feature['x2']
365             outputFeature['y2'] = feature['y2']
366             outputFeature['z2'] = feature['z2']
367             outputFeature['Length'] = feature['Length'] # myLength
368             outputFeature['LineString'] = result
369             
370             # Handle pipe type codes
371             mytypes = list(types.get(id, {}).items())
372             if len(mytypes) == 0:
373                 feedback.pushInfo('No type codes for feature {}'.format(id))
374             else:
375                 if len(mytypes) > 1:
376                     mytypes.sort(key = itemgetter(1))
377                     feedback.pushInfo('No unique type code for feature {}: {}'.format(id, mytypes))
378                 outputFeature['PipeStruct'] = mytypes[-1][0]
379             
380             label = feature['Label']
381             m = self.DN_PATTERN.fullmatch(label)
382             if m:
383                 outputFeature['DimensionDN'] = int(m.group(1))
384             
385             output.addFeature(outputFeature)
386             
387         feedback.pushInfo('Loop done')
388         
389         #if feedback.isCanceled():
390         #    span.rollBack()
391         #else:
392         #    span.commitChanges()
393
394         feedback.pushInfo('Changes committed')
395         
396         # Return the results of the algorithm. In this case our only result is
397         # the feature sink which contains the processed features, but some
398         # algorithms may return multiple feature sinks, calculated numeric
399         # statistics, etc. These should all be included in the returned
400         # dictionary, with keys matching the feature corresponding parameter
401         # or output names.
402         return {self.OUTPUT: outputId}