From 9d4ad8cc1d552e7066b51ee18f52af5e8dc47eb1 Mon Sep 17 00:00:00 2001 From: Reino Ruusu Date: Thu, 21 Mar 2019 14:36:48 +0200 Subject: [PATCH 1/1] QGIS Python script for generating pipelines CVS files. gitlab #40 Change-Id: I4c6527eddbb5d61b4cac81f5bed2cfd49863c83a --- .../build.properties | 3 +- .../rootFiles/QGIS scripts/generateCSV.py | 334 ++++++++++++++++++ 2 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py diff --git a/org.simantics.district.feature/build.properties b/org.simantics.district.feature/build.properties index 82ab19c6..191f8907 100644 --- a/org.simantics.district.feature/build.properties +++ b/org.simantics.district.feature/build.properties @@ -1 +1,2 @@ -bin.includes = feature.xml +bin.includes = feature.xml +root=rootFiles diff --git a/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py b/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py new file mode 100644 index 00000000..80c596b2 --- /dev/null +++ b/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +from PyQt5.QtCore import (QCoreApplication, QVariant) +from qgis.core import (QgsProcessing, + QgsFeatureSink, + QgsFeature, + QgsProcessingException, + QgsProcessingAlgorithm, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterVectorLayer, + QgsProcessingParameterDistance, + QgsVectorDataProvider, + QgsFields, + QgsField) +import processing + + +class SpanCoordinatesAlgorithm(QgsProcessingAlgorithm): + """ + This is an example algorithm that takes a vector layer and + creates a new identical one. + + It is meant to be used as an example of how to create your own + algorithms and explain methods and variables used to do it. An + algorithm like this will be available in all elements, and there + is not need for additional work. + + All Processing algorithms should extend the QgsProcessingAlgorithm + class. + """ + + # Constants used to refer to parameters and outputs. They will be + # used when calling the algorithm from another algorithm, or when + # calling from the QGIS console. + + PIPE = 'PIPE' + SPAN = 'SPAN' + TOLERANCE = 'TOLERANCE' + OUTPUT = 'OUTPUT' + + # Constants for feature field names + FATHER_ID = 'FatherId' + LINESTRING = 'LineString' + + def tr(self, string): + """ + Returns a translatable string with the self.tr() function. + """ + return QCoreApplication.translate('Processing', string) + + def createInstance(self): + return SpanCoordinatesAlgorithm() + + def name(self): + """ + Returns the algorithm name, used for identifying the algorithm. This + string should be fixed for the algorithm, and must not be localised. + The name should be unique within each provider. Names should contain + lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'calculateDistrictCSV' + + def displayName(self): + """ + Returns the translated algorithm name, which should be used for any + user-visible display of the algorithm name. + """ + return self.tr('Calculate CSV for Apros District') + + def group(self): + """ + Returns the name of the group this algorithm belongs to. This string + should be localised. + """ + return self.tr('District network scripts') + + def groupId(self): + """ + Returns the unique ID of the group this algorithm belongs to. This + string should be fixed for the algorithm, and must not be localised. + The group id should be unique within each provider. Group id should + contain lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'districtscripts' + + def shortHelpString(self): + """ + Returns a localised short helper string for the algorithm. This string + should provide a basic description about what the algorithm does and the + parameters and outputs associated with it.. + """ + return self.tr("Calculate a string of coordinate points for a point span") + + def initAlgorithm(self, config=None): + """ + Here we define the inputs and output of the algorithm, along + with some other properties. + """ + + # We add the input vector features source. It can have any kind of + # geometry. + self.addParameter( + QgsProcessingParameterVectorLayer( + self.PIPE, + self.tr('Pipeline layer'), + [QgsProcessing.TypeVectorLine] + ) + ) + + self.addParameter( + QgsProcessingParameterVectorLayer( + self.SPAN, + self.tr('Point span layer'), + [QgsProcessing.TypeVectorLine] + ) + ) + + self.addParameter( + QgsProcessingParameterDistance( + self.TOLERANCE, + self.tr('Location tolerance'), + 0.0, + minValue = 0.0 + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + self.tr('Output layer'), + QgsProcessing.TypeVectorLine + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + """ + Here is where the processing itself takes place. + """ + + # Retrieve the feature source and sink. The 'dest_id' variable is used + # to uniquely identify the feature sink, and must be included in the + # dictionary returned by the processAlgorithm function. + pipe = self.parameterAsLayer( + parameters, + self.PIPE, + context + ) + + span = self.parameterAsLayer( + parameters, + self.SPAN, + context + ) + + eps = self.parameterAsDouble( + parameters, + self.TOLERANCE, + context + ) + + sourceFields = span.fields() + sourceNames = sourceFields.names() + outputFields = QgsFields(sourceFields) + if not ('x1' in sourceNames): outputFields.append(QgsField('x1', QVariant.Double)) + if not ('y1' in sourceNames): outputFields.append(QgsField('y1', QVariant.Double)) + if not ('z1' in sourceNames): outputFields.append(QgsField('z1', QVariant.Double)) + if not ('x2' in sourceNames): outputFields.append(QgsField('x2', QVariant.Double)) + if not ('y2' in sourceNames): outputFields.append(QgsField('y2', QVariant.Double)) + if not ('z2' in sourceNames): outputFields.append(QgsField('z2', QVariant.Double)) + if not ('Length' in sourceNames): outputFields.append(QgsField('Length', QVariant.Double)) + if not ('LineString' in sourceNames): outputFields.append(QgsField('LineString', QVariant.String)) + + (output, outputId) = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + outputFields + ) + + # If source was not found, throw an exception to indicate that the algorithm + # encountered a fatal error. The exception text can be any string, but in this + # case we use the pre-built invalidSourceError method to return a standard + # helper text for when a source cannot be evaluated + if pipe is None: + raise QgsProcessingException(self.invalidSourceError(parameters, self.PIPE)) + if span is None: + raise QgsProcessingException(self.invalidSourceError(parameters, self.SPAN)) + + # Compute the number of steps to display within the progress bar and + # get features from source + total = 100.0 / pipe.featureCount() if pipe.featureCount() else 0 + + # Dictionary from span feature ids to lengths + lengths = dict() + + # Dictionary from span feature ids to lists of lists of QgsPoint objects + strings = dict() + + spanFeatures = span.getFeatures() + pipeFeatures = pipe.getFeatures() + + for counter, feature in enumerate(pipeFeatures): + if feedback.isCanceled(): break + + geometry = feature.geometry() + if geometry == None: continue + + fatherID = feature[self.FATHER_ID] + + # Length + length = geometry.length() + + oldLength = lengths.get(fatherID, 0.0) + lengths[fatherID] = oldLength + length + + # Segment points + pointList = strings.get(fatherID, []) + # feedback.pushInfo('Point list: {}'.format(pointList)) + mylist = [] + + vertices = geometry.vertices() + while vertices.hasNext(): + mylist.append(vertices.next()) + + # feedback.pushInfo('Feature {}, Father {}, Points: {}'.format(feature['Id'], fatherID, ";".join(map(lambda x: '{} {}'.format(x.x(), x.y()), mylist)))) + + pointList.append(mylist) + strings[fatherID] = pointList + + # Update the progress bar + feedback.setProgress(int(counter * total)) + + if feedback.isCanceled(): + return + + feedback.pushInfo('Done') + + #span.startEditing() + + feedback.pushInfo('Started editing') + feedback.pushInfo(str(spanFeatures)) + + for feature in spanFeatures: + if feedback.isCanceled(): break + + #feedback.pushInfo(str(feature)) + id = feature['Id'] + #feedback.pushInfo(str(id)) + + # Length + length = lengths.get(id, None) + + # Vertices + mylist = strings.get(id, None) + if mylist == None: + feedback.pushInfo('No points for feature {}'.format(id)) + continue + + #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist)))) + + mypoints = list(feature.geometry().vertices()) + head = feature.geometry().vertices().next() + resultList = [head] + + #feedback.pushInfo(str(resultList)) + + i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None) + if i == None: + mylist = list(map(lambda x: list(reversed(x)), mylist)) + i = next((i for i, x in enumerate(mylist) if head.distance(x[0]) <= eps), None) + if i == None: + feedback.pushInfo('No matching start vertex for feature {}'.format(id)) + continue + + while i != None: + vertices = mylist.pop(i) + tail = vertices[-1] + resultList.extend(vertices[1:]) + i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None) + + # feedback.pushInfo(str(resultList)) + + # Convert to string + result = ";".join(('{} {}'.format(p.x(), p.y()) for p in resultList)) + + feedback.pushInfo('Feature {}: {}'.format(id, result)) + + outputFeature = QgsFeature() + outputFeature.setFields(outputFields) + for i, x in enumerate(feature.attributes()): + fieldName = sourceFields[i].name() + outputFeature[fieldName] = x + + outputFeature['x1'] = feature['x1'] + outputFeature['y1'] = feature['y1'] + outputFeature['z1'] = feature['z1'] + outputFeature['x2'] = feature['x2'] + outputFeature['y2'] = feature['y2'] + outputFeature['z2'] = feature['z2'] + outputFeature['Length'] = feature['length'] # length + outputFeature['LineString'] = result + + output.addFeature(outputFeature) + + feedback.pushInfo('Loop done') + + #if feedback.isCanceled(): + # span.rollBack() + #else: + # span.commitChanges() + + feedback.pushInfo('Changes committed') + + # Return the results of the algorithm. In this case our only result is + # the feature sink which contains the processed features, but some + # algorithms may return multiple feature sinks, calculated numeric + # statistics, etc. These should all be included in the returned + # dictionary, with keys matching the feature corresponding parameter + # or output names. + return {self.OUTPUT: outputId} -- 2.45.2