]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py
Added DimensionDN column 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 import processing
28 import re
29
30
31 class SpanCoordinatesAlgorithm(QgsProcessingAlgorithm):
32     """
33     This is an example algorithm that takes a vector layer and
34     creates a new identical one.
35
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.
40
41     All Processing algorithms should extend the QgsProcessingAlgorithm
42     class.
43     """
44
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.
48
49     PIPE = 'PIPE'
50     SPAN = 'SPAN'
51     TOLERANCE = 'TOLERANCE'
52     OUTPUT = 'OUTPUT'
53     
54     # Constants for feature field names
55     FATHER_ID = 'FatherId'
56     LINESTRING = 'LineString'
57     
58     # RegExp for DN values in 
59     DN_PATTERN = re.compile(r"DN(\d+)(-.*)?")
60
61     def tr(self, string):
62         """
63         Returns a translatable string with the self.tr() function.
64         """
65         return QCoreApplication.translate('Processing', string)
66
67     def createInstance(self):
68         return SpanCoordinatesAlgorithm()
69
70     def name(self):
71         """
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.
77         """
78         return 'calculateDistrictCSV'
79
80     def displayName(self):
81         """
82         Returns the translated algorithm name, which should be used for any
83         user-visible display of the algorithm name.
84         """
85         return self.tr('Calculate CSV for Apros District')
86
87     def group(self):
88         """
89         Returns the name of the group this algorithm belongs to. This string
90         should be localised.
91         """
92         return self.tr('District network scripts')
93
94     def groupId(self):
95         """
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.
101         """
102         return 'districtscripts'
103
104     def shortHelpString(self):
105         """
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..
109         """
110         return self.tr("Calculate a string of coordinate points for a point span")
111
112     def initAlgorithm(self, config=None):
113         """
114         Here we define the inputs and output of the algorithm, along
115         with some other properties.
116         """
117
118         # We add the input vector features source. It can have any kind of
119         # geometry.
120         self.addParameter(
121             QgsProcessingParameterVectorLayer(
122                 self.PIPE,
123                 self.tr('Pipeline layer'),
124                 [QgsProcessing.TypeVectorLine]
125             )
126         )
127
128         self.addParameter(
129             QgsProcessingParameterVectorLayer(
130                 self.SPAN,
131                 self.tr('Point span layer'),
132                 [QgsProcessing.TypeVectorLine]
133             )
134         )
135         
136         self.addParameter(
137             QgsProcessingParameterDistance(
138                 self.TOLERANCE,
139                 self.tr('Location tolerance'),
140                 0.0,
141                 minValue = 0.0
142             )
143         )
144         
145         self.addParameter(
146             QgsProcessingParameterFeatureSink(
147                 self.OUTPUT,
148                 self.tr('Output layer'),
149                 QgsProcessing.TypeVectorLine
150             )
151         )
152
153     def processAlgorithm(self, parameters, context, feedback):
154         """
155         Here is where the processing itself takes place.
156         """
157
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(
162             parameters,
163             self.PIPE,
164             context
165         )
166
167         span = self.parameterAsLayer(
168             parameters,
169             self.SPAN,
170             context
171         )
172         
173         eps = self.parameterAsDouble(
174             parameters,
175             self.TOLERANCE,
176             context
177         )
178         
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))
191         
192         (output, outputId) = self.parameterAsSink(
193             parameters,
194             self.OUTPUT,
195             context,
196             outputFields
197         )
198
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
203         if pipe is None:
204             raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE))
205         if span is None:
206             raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN))
207
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
211         
212         # Dictionary from span feature ids to lengths
213         lengths = dict()
214         
215         # Dictionary from span feature ids to lists of lists of QgsPoint objects
216         strings = dict()
217         
218         spanFeatures = span.getFeatures()
219         pipeFeatures = pipe.getFeatures()
220         
221         for counter, feature in enumerate(pipeFeatures):
222             if feedback.isCanceled(): break
223                 
224             geometry = feature.geometry()
225             if geometry == None: continue
226             
227             fatherID = feature[self.FATHER_ID]
228             
229             # Length
230             length = geometry.length()
231             
232             oldLength = lengths.get(fatherID, 0.0)
233             lengths[fatherID] = oldLength + length
234             
235             # Segment points
236             pointList = strings.get(fatherID, [])
237             # feedback.pushInfo('Point list: {}'.format(pointList))
238             mylist = []
239             
240             vertices = geometry.vertices()
241             while vertices.hasNext():
242                 mylist.append(vertices.next())
243                 
244             # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist))))
245             
246             pointList.append(mylist)
247             strings[fatherID] = pointList
248             
249             # Update the progress bar
250             feedback.setProgress(int(counter * total))
251
252         if feedback.isCanceled():
253             return
254
255         feedback.pushInfo('Done')
256         
257         #span.startEditing()
258         
259         feedback.pushInfo('Started editing')
260         feedback.pushInfo(str(spanFeatures))
261         
262         for feature in spanFeatures:
263             if feedback.isCanceled(): break
264
265             #feedback.pushInfo(str(feature))
266             id = feature['Id']
267             #feedback.pushInfo(str(id))
268
269             # Length
270             length = lengths.get(id, None)
271             
272             # Vertices
273             mypoints = list(feature.geometry().vertices())
274             mylist = strings.get(id, None)
275             if mylist == None:
276                 feedback.pushInfo('No points for feature {}'.format(id))
277                 mylist = [mypoints]
278             
279             #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist))))
280             
281             head = feature.geometry().vertices().next()
282             resultList = [head]
283             
284             #feedback.pushInfo(str(resultList))
285             
286             i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None)
287             if i == 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)
290                 if i == None:
291                     feedback.pushInfo('Warning: No matching start vertex for feature {}'.format(id))
292                     mylist = [mypoints]
293                     i = 0
294             
295             vertices = mylist.pop(i)
296             
297             while i != None:
298                 tail = vertices[-1]
299                 resultList.extend(vertices[1:])
300                 if tail.distance(mypoints[-1]) <= eps:
301                     break
302                 
303                 i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None)
304                 if i != None:
305                     vertices = mylist.pop(i)
306                 else:
307                     i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None)
308                     if i != None:
309                         vertices = list(reversed(mylist.pop(i)))
310
311             # feedback.pushInfo(str(resultList))
312
313             # Convert to string
314             result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList))
315
316             # feedback.pushInfo('Feature {}: {}'.format(id, result))
317             
318             outputFeature = QgsFeature()
319             outputFeature.setFields(outputFields)
320             for i, x in enumerate(feature.attributes()):
321                 fieldName = sourceFields[i].name()
322                 outputFeature[fieldName] = x
323             
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
332             
333             label = feature['Label']
334             m = self.DN_PATTERN.fullmatch(label)
335             if m:
336                 outputFeature['DimensionDN'] = int(m.group(1))
337             
338             output.addFeature(outputFeature)
339             
340         feedback.pushInfo('Loop done')
341         
342         #if feedback.isCanceled():
343         #    span.rollBack()
344         #else:
345         #    span.commitChanges()
346
347         feedback.pushInfo('Changes committed')
348         
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
354         # or output names.
355         return {self.OUTPUT: outputId}