]> gerrit.simantics Code Review - simantics/district.git/commitdiff
Alternative QGIS scripts for different network data models.
authorReino Ruusu <reino.ruusu@semantum.fi>
Thu, 18 Jul 2019 13:28:13 +0000 (16:28 +0300)
committerTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Sat, 31 Aug 2019 20:08:43 +0000 (23:08 +0300)
gitlab #40

Change-Id: I109ee8f132975e384fed9009edb78e2350887723

org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV2.py [new file with mode: 0644]
org.simantics.district.feature/rootFiles/QGIS scripts/generateCSV3.py [new file with mode: 0644]

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 (file)
index 0000000..bb4bc8b
--- /dev/null
@@ -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 (file)
index 0000000..5b37255
--- /dev/null
@@ -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}