From: Reino Ruusu Date: Thu, 18 Jul 2019 13:28:13 +0000 (+0300) Subject: Alternative QGIS scripts for different network data models. X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F30%2F3030%2F2;p=simantics%2Fdistrict.git Alternative QGIS scripts for different network data models. gitlab #40 Change-Id: I109ee8f132975e384fed9009edb78e2350887723 --- diff --git a/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV2.py b/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV2.py new file mode 100644 index 00000000..bb4bc8be --- /dev/null +++ b/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV2.py @@ -0,0 +1,415 @@ +# -*- 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, + QgsUnitTypes) +import processing +import re +from operator import itemgetter + +class SpanCoordinatesAlgorithm2(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' + MINIMUM_LENGTH = 'MINIMUM_LENGTH' + OUTPUT = 'OUTPUT' + + # Constants for feature field names + FATHER_ID = 'FatherId' + LINESTRING = 'LineString' + + # RegExp for DN values in + DN_PATTERN = re.compile(r"DN(\d+)(-.*)?") + + def tr(self, string): + """ + Returns a translatable string with the self.tr() function. + """ + return QCoreApplication.translate('Processing', string) + + def createInstance(self): + return SpanCoordinatesAlgorithm2() + + 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 'calculateDistrictCSV2' + + 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 (No coordinates)') + + 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] + ) + ) + + tol = QgsProcessingParameterDistance( + self.TOLERANCE, + self.tr('Location tolerance'), + 0.001, + minValue = 0.0 + ) + tol.setDefaultUnit(QgsUnitTypes.DistanceMeters) + self.addParameter( tol ) + + dist = QgsProcessingParameterDistance( + self.MINIMUM_LENGTH, + self.tr('Minimum span length'), + 0.25, + minValue = 0.0 + ) + dist.setDefaultUnit(QgsUnitTypes.DistanceMeters) + self.addParameter( dist ) + + 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 + ) + + minLength = self.parameterAsDouble( + parameters, + self.MINIMUM_LENGTH, + context + ) + + feedback.pushInfo('Tolerance: {} m\nMinimum span length: {} m'.format(eps, minLength)) + + 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)) + if not ('DimensionDN' in sourceNames): outputFields.append(QgsField('DimensionDN', QVariant.Int)) + if not ('PipeStruct' in sourceNames): outputFields.append(QgsField('PipeStruct', 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() + + # Dictionary for the PipeStruct field values + types = 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 + myLength = None + if feature.fields().lookupField('Length') != -1: + myLength = feature['Length'] + if myLength == None: + myLength = geometry.length() + + oldLength = lengths.get(fatherID, 0.0) + lengths[fatherID] = oldLength + myLength + + # 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 + + # Store the value of PipeStruct + t = feature['PipeStruct'] + tt = types.get(fatherID, {}) + types[fatherID] = tt + c = tt.get(t, 0) + c += myLength + tt[t] = c + + # 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 + + f = feature.fields() + + #feedback.pushInfo(str(feature)) + id = feature['Id'] + #feedback.pushInfo(str(id)) + + # Length + myLength = None + if f.lookupField('Length') != -1: + myLength = feature['Length'] + if myLength == None: + myLength = feature.geometry().length() + + # Ignore short stumps + if myLength <= minLength: + continue + + # Vertices + mypoints = list(feature.geometry().vertices()) + mylist = strings.get(id, None) + if mylist == None: + feedback.pushInfo('No points for feature {}'.format(id)) + mylist = [mypoints] + + #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist)))) + + 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('Warning: No matching start vertex for feature {}'.format(id)) + mylist = [mypoints] + i = 0 + + vertices = mylist.pop(i) + + while i != None: + tail = vertices[-1] + resultList.extend(vertices[1:]) + if tail.distance(mypoints[-1]) <= eps: + break + + i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None) + if i != None: + vertices = mylist.pop(i) + else: + i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None) + if i != None: + vertices = list(reversed(mylist.pop(i))) + + # 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 + + vts = list(feature.geometry().vertices()) + p1 = vts[0] + p2 = vts[-1] + + outputFeature['x1'] = feature['x1'] if f.lookupField('x1') != -1 else p1.x() + outputFeature['y1'] = feature['y1'] if f.lookupField('y1') != -1 else p1.y() + outputFeature['z1'] = feature['z1'] if f.lookupField('z1') != -1 else p1.z() + outputFeature['x2'] = feature['x2'] if f.lookupField('x2') != -1 else p2.x() + outputFeature['y2'] = feature['y2'] if f.lookupField('y2') != -1 else p2.y() + outputFeature['z2'] = feature['z2'] if f.lookupField('z2') != -1 else p2.z() + outputFeature['Length'] = feature['Length'] if f.lookupField('Length') != -1 else feature.geometry().length() + outputFeature['LineString'] = result + + # Handle pipe type codes + mytypes = list(types.get(id, {}).items()) + if len(mytypes) == 0: + feedback.pushInfo('No type codes for feature {}'.format(id)) + else: + if len(mytypes) > 1: + mytypes.sort(key = itemgetter(1)) + feedback.pushInfo('No unique type code for feature {}: {}'.format(id, mytypes)) + outputFeature['PipeStruct'] = mytypes[-1][0] + + if f.lookupField('Label') != -1: + label = feature['Label'] + m = self.DN_PATTERN.fullmatch(label) + if m: + outputFeature['DimensionDN'] = int(m.group(1)) + + 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} diff --git a/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV3.py b/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV3.py new file mode 100644 index 00000000..5b372557 --- /dev/null +++ b/org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV3.py @@ -0,0 +1,412 @@ +# -*- 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, + QgsUnitTypes) +import processing +import re +from operator import itemgetter + +class SpanCoordinatesAlgorithm3(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' + MINIMUM_LENGTH = 'MINIMUM_LENGTH' + OUTPUT = 'OUTPUT' + + # Constants for feature field names + FATHER_ID = 'FatherId' + LINESTRING = 'LineString' + + # RegExp for DN values in + DN_PATTERN = re.compile(r"DN(\d+)(-.*)?") + + def tr(self, string): + """ + Returns a translatable string with the self.tr() function. + """ + return QCoreApplication.translate('Processing', string) + + def createInstance(self): + return SpanCoordinatesAlgorithm3() + + 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 'calculateDistrictCSV3' + + 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 (TechTypeData)') + + 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] + ) + ) + + tol = QgsProcessingParameterDistance( + self.TOLERANCE, + self.tr('Location tolerance'), + 0.001, + minValue = 0.0 + ) + tol.setDefaultUnit(QgsUnitTypes.DistanceMeters) + self.addParameter( tol ) + + dist = QgsProcessingParameterDistance( + self.MINIMUM_LENGTH, + self.tr('Minimum span length'), + 0.25, + minValue = 0.0 + ) + dist.setDefaultUnit(QgsUnitTypes.DistanceMeters) + self.addParameter( dist ) + + 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 + ) + + minLength = self.parameterAsDouble( + parameters, + self.MINIMUM_LENGTH, + context + ) + + feedback.pushInfo('Tolerance: {} m\nMinimum span length: {} m'.format(eps, minLength)) + + 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)) + if not ('TechTypeId' in sourceNames): outputFields.append(QgsField('TechTypeId', QVariant.String)) + if not ('PipeStruct' in sourceNames): outputFields.append(QgsField('PipeStruct', 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() + + # Dictionary for the PipeStruct field values + types = 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 + myLength = None + if feature.fields().lookupField('Length') != -1: + myLength = feature['Length'] + if myLength == None: + myLength = geometry.length() + + oldLength = lengths.get(fatherID, 0.0) + lengths[fatherID] = oldLength + myLength + + # 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 + + # Store the value of PipeStruct + t = feature['PipeStruct'] + tt = types.get(fatherID, {}) + types[fatherID] = tt + c = tt.get(t, 0) + c += myLength + tt[t] = c + + # 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 + + f = feature.fields() + + #feedback.pushInfo(str(feature)) + id = feature['Id'] + #feedback.pushInfo(str(id)) + + # Length + myLength = None + if f.lookupField('Length') != -1: + myLength = feature['Length'] + if myLength == None: + myLength = feature.geometry().length() + + # Ignore short stumps + if myLength <= minLength: + continue + + # Vertices + mypoints = list(feature.geometry().vertices()) + mylist = strings.get(id, None) + if mylist == None: + feedback.pushInfo('No points for feature {}'.format(id)) + mylist = [mypoints] + + #feedback.pushInfo('Points: {}'.format("|".join(map(lambda x: ";".join(('{} {}'.format(p.x(), p.y()) for p in x)), mylist)))) + + 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('Warning: No matching start vertex for feature {}'.format(id)) + mylist = [mypoints] + i = 0 + + vertices = mylist.pop(i) + + while i != None: + tail = vertices[-1] + resultList.extend(vertices[1:]) + if tail.distance(mypoints[-1]) <= eps: + break + + i = next((i for i, x in enumerate(mylist) if tail.distance(x[0]) <= eps), None) + if i != None: + vertices = mylist.pop(i) + else: + i = next((i for i, x in enumerate(mylist) if tail.distance(x[-1]) <= eps), None) + if i != None: + vertices = list(reversed(mylist.pop(i))) + + # 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 + + vts = list(feature.geometry().vertices()) + p1 = vts[0] + p2 = vts[-1] + + outputFeature['x1'] = feature['x1'] if f.lookupField('x1') != -1 else p1.x() + outputFeature['y1'] = feature['y1'] if f.lookupField('y1') != -1 else p1.y() + outputFeature['z1'] = feature['z1'] if f.lookupField('z1') != -1 else p1.z() + outputFeature['x2'] = feature['x2'] if f.lookupField('x2') != -1 else p2.x() + outputFeature['y2'] = feature['y2'] if f.lookupField('y2') != -1 else p2.y() + outputFeature['z2'] = feature['z2'] if f.lookupField('z2') != -1 else p2.z() + outputFeature['Length'] = feature['Length'] if f.lookupField('Length') != -1 else feature.geometry().length() + outputFeature['LineString'] = result + + # Handle pipe type codes + mytypes = list(types.get(id, {}).items()) + if len(mytypes) == 0: + feedback.pushInfo('No type codes for feature {}'.format(id)) + else: + if len(mytypes) > 1: + mytypes.sort(key = itemgetter(1)) + feedback.pushInfo('No unique type code for feature {}: {}'.format(id, mytypes)) + outputFeature['PipeStruct'] = mytypes[-1][0] + + if f.lookupField('TechTypeId') != -1: + outputFeature['TechTypeId'] = feature['TechTypeId'] + + 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}