class LrsErrorLayerManager(LrsLayerManager):
    def __init__(self, layer):
        super(LrsErrorLayerManager, self).__init__(layer)

    # test if error geometry type matches this layer
    def errorTypeMatch(self, error):
        if self.layer.geometryType() == QgsWkbTypes.PointGeometry and error.geo.type() != QgsWkbTypes.PointGeometry:
            return False
        if self.layer.geometryType() == QgsWkbTypes.LineGeometry and error.geo.type() != QgsWkbTypes.LineGeometry:
            return False
        return True

    # get errors of layer type (point or line)
    def addErrors(self, errors, crs):
        if self.layer is None:
            return
        #debug("addErrors geometryType = %s errors count = %s" % (self.layer.geometryType(), len(errors)))

        features = []
        for error in errors:
            #debug("addErrors %s" % error)
            if not self.errorTypeMatch(error):
                continue
            feature = LrsErrorFeature(error)
            #debug("addErrors %s" % feature)
            features.append(feature)

        self.addFeatures(features, crs)

    def updateErrors(self, errorUpdates):
        # debug ( "%s" % errorUpdates )
        if self.layer is None:
            return

        # delete
        self.deleteChecksums(errorUpdates['removedErrorChecksums'])

        # update
        features = []
        for error in errorUpdates['updatedErrors']:
            if not self.errorTypeMatch(error):
                continue

            feature = LrsErrorFeature(error)
            features.append(feature)
        self.updateFeatures(features, errorUpdates['crs'])

        # add new
        self.addErrors(errorUpdates['addedErrors'], errorUpdates['crs']) class LrsErrorLineLayer(LrsErrorLayer):
    def __init__(self, crs):
        uri = "LineString?crs=%s" % crsString(crs)
        super(LrsErrorLineLayer, self).__init__(uri, 'LRS line errors') class LrsErrorModel(QAbstractTableModel):
    TYPE_COL = 0
    ROUTE_COL = 1
    MEASURE_COL = 2
    MESSAGE_COL = 3  # currently not used

    headerLabels = {
        TYPE_COL: 'Type',
        ROUTE_COL: 'Route',
        MEASURE_COL: 'Measure',
        MESSAGE_COL: 'Message',
    }

    def __init__(self):
        super(LrsErrorModel, self).__init__()
        self.errors = []

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if not Qt or role != Qt.DisplayRole: return None
        if orientation == Qt.Horizontal:
            if section in self.headerLabels:
                return self.headerLabels[section]
            else:
                return ""
        else:
            return "%s" % section

    def rowCount(self, index):
        return len(self.errors)

    def columnCount(self, index):
        return 3

    def data(self, index, role):
        if role != Qt.DisplayRole: return None

        error = self.getError(index)
        if not error: return

        col = index.column()
        value = ""
        if col == self.TYPE_COL:
            value = error.typeLabel()
        elif col == self.ROUTE_COL:
            value = "%s" % error.routeId
        elif col == self.MEASURE_COL:
            value = error.getMeasureString()
        elif col == self.MESSAGE_COL:
            value = error.message

        # return "row %s col %s" % ( index.row(), index.column() )
        return value

    def addErrors(self, errors):
        self.errors.extend(errors)

    def getError(self, index):
        if not index: return None
        row = index.row()
        if row < 0 or row >= len(self.errors): return None
        return self.errors[row]

    def getErrorIndexForChecksum(self, checksum):
        for i in range(len(self.errors)):
            if self.errors[i].getChecksum() == checksum:
                return i
        return None  # should not happen

    def rowsToBeRemoved(self, errorUpdates):
        rows = []
        for checksum in errorUpdates['removedErrorChecksums']:
            rows.append(self.getErrorIndexForChecksum(checksum))
        return rows

    def updateErrors(self, errorUpdates):
        # debug ( 'errorUpdates: %s' % errorUpdates )
        for checksum in errorUpdates['removedErrorChecksums']:
            idx = self.getErrorIndexForChecksum(checksum)
            # debug ( 'remove row %s' % idx )
            self.beginRemoveRows(QModelIndex(), idx, idx)
            del self.errors[idx]
            self.endRemoveRows()

        for error in errorUpdates['updatedErrors']:
            checksum = error.getChecksum()
            idx = self.getErrorIndexForChecksum(checksum)
            # debug ( 'update row %s' % idx )
            self.errors[idx] = error
            topLeft = self.createIndex(idx, 0)
            bottomRight = self.createIndex(idx, 3)
            self.dataChanged.emit(topLeft, bottomRight)

        for error in errorUpdates['addedErrors']:
            # debug ( 'add row' )
            idx = len(self.errors)
            self.beginInsertRows(QModelIndex(), idx, idx)
            self.errors.append(error)
            self.endInsertRows() class LrsErrorPointLayer(LrsErrorLayer):
    def __init__(self, crs):
        uri = "Point?crs=%s" % crsString(crs)
        super(LrsErrorPointLayer, self).__init__(uri, 'LRS point errors') class LrsErrorVisualizer(object):
    def __init__(self, mapCanvas):
        self.errorHighlight = None
        self.mapCanvas = mapCanvas

    def __del__(self):
        if self.errorHighlight:
            del self.errorHighlight

    def clearHighlight(self):
        if self.errorHighlight:
            del self.errorHighlight
            self.errorHighlight = None

    def highlight(self, error, crs):
        self.clearHighlight()
        if not error: return

        # QgsHighlight does reprojection from layer CRS
        layer = QgsVectorLayer('Point?crs=' + crsString(crs), 'LRS error highlight', 'memory')
        self.errorHighlight = QgsHighlight(self.mapCanvas, error.geo, layer)
        # highlight point size is hardcoded in QgsHighlight
        self.errorHighlight.setWidth(2)
        self.errorHighlight.setColor(Qt.yellow)
        self.errorHighlight.show()

    def zoom(self, error, crs):
        if not error: return
        geo = error.geo
        mapSettings = self.mapCanvas.mapSettings()
        if isProjectCrsEnabled() and getProjectCrs() != crs:
            geo = QgsGeometry(error.geo)
            transform = QgsCoordinateTransform(crs, QgsProject().instance().crs(), QgsProject.instance())
            geo.transform(transform)

        if geo.type() == QgsWkbTypes.PointGeometry:
            p = geo.asPoint()
            bufferCrs = getProjectCrs() if isProjectCrsEnabled() else crs
            b = 2000 if not bufferCrs.isGeographic() else 2000 / 100000  # buffer
            extent = QgsRectangle(p.x() - b, p.y() - b, p.x() + b, p.y() + b)
        else:  # line
            extent = geo.boundingBox()
            extent.scale(2)
        self.mapCanvas.setExtent(extent)
        self.mapCanvas.refresh(); class LrsQualityLayer(QgsVectorLayer):
    def __init__(self, crs):
        uri = "LineString?crs=%s" % crsString(crs)
        provider = QgsProviderRegistry.instance().createProvider('memory', uri)
        provider.addAttributes(LRS_QUALITY_FIELDS.toList())
        uri = provider.dataSourceUri()
        super(LrsQualityLayer, self).__init__(uri, 'LRS quality', 'memory')

        # min, max, color, label
        styles = [
            [0, 10, QColor(Qt.green), '0 - 10 % error'],
            [10, 30, QColor(Qt.blue), '10 - 30 % error'],
            [30, 1000000, QColor(Qt.red), '> 30 % error']
        ]
        ranges = []
        for style in styles:
            symbol = QgsSymbol.defaultSymbol(QgsWkbTypes.LineGeometry)
            symbol.setColor(style[2])
            range = QgsRendererRange(style[0], style[1], symbol, style[3])
            ranges.append(range)

        renderer = QgsGraduatedSymbolRenderer('err_perc', ranges)
        self.setRenderer(renderer) class LrsQualityLayerManager(LrsLayerManager):
    def __init__(self, layer):
        super(LrsQualityLayerManager, self).__init__(layer)

    def update(self, errorUpdates):
        # debug ( "%s" % errorUpdates )
        if not self.layer:
            return

        # delete
        self.deleteChecksums(errorUpdates['removedQualityChecksums'])

        # no update, only remove and add

        # add new
        self.addFeatures(errorUpdates['addedQualityFeatures'], errorUpdates['crs']) class LrsEvents(QObject):
    def __init__(self, lrs, progressBar=None):
        # debug( "LrsEvents.__init__")
        # self.iface = iface
        self.lrs = lrs  # Lrs object
        self.progressBar = progressBar

    @staticmethod
    def is_null(value):
        return value is None or (isinstance(value, QVariant) and value.isNull())

    def create(self, layer, featuresSelect, routeFieldName, startFieldName, endFieldName, errorFieldName, outputName, startOffsetFieldName=None, endOffsetFieldName=None):
        # create new layer
        geometryType = "MultiLineString" if endFieldName else "Point"
        uri = geometryType
        uri += "?crs=%s" % crsString(self.lrs.crs)
        provider = QgsProviderRegistry.instance().createProvider('memory', uri)
        # Because memory provider (QGIS 2.4) fails to parse PostGIS type names (like int8, float, float8 ...)
        # and negative length and precision we overwrite type names according to types and reset length and precision
        fieldsList = layer.fields().toList()
        fixFields(fieldsList)
        provider.addAttributes(fieldsList)
        if errorFieldName:
            provider.addAttributes([QgsField(errorFieldName, QVariant.String, "string"), ])
        uri = provider.dataSourceUri()
        # debug('uri: %s' % uri)

        outputLayer = QgsVectorLayer(uri, outputName, 'memory')
        if not outputLayer.isValid():
            QMessageBox.information(self, 'Information', 'Cannot create memory layer with uri %s' % uri)

        checkFields(layer, outputLayer)

        # Not sure why attributes were set again here, the attributes are already in uri
        # outputLayer.startEditing()  # to add fields
        # for field in layer.fields():
        #    if not outputLayer.addAttribute(field):
        #        QMessageBox.information(self, 'Information', 'Cannot add attribute %s' % field.name())

        # if errorFieldName:
        #    outputLayer.addAttribute(QgsField(errorFieldName, QVariant.String, "string"))

        # outputLayer.commitChanges()

        # It may happen that event goes slightly outside available lrs because of
        # decimal number inaccuracy. Thus we set tolerance used to try to find nearest point event within that
        # tolerance and skip smaller linear event errors (gaps)
        # 0.1m is too much and less than 0.01 m does not make sense in standard GIS
        #eventTolerance = convertDistanceUnits(0.01, LrsUnits.METER, self.lrs.measureUnit)

        # For LrsLayer with existing measure we don't know measure units, but it is little probable that it is
        # more than km or miles (if we stay on Earth) 0.0001 km = 10 cm tolerance should not be too much for
        # meters and it should sufficient for km
        # TODO: add tolerance widget?
        eventTolerance = 0.0001

        outputFeatures = []
        fields = outputLayer.fields()
        #debug("create featuresSelect = %s" % featuresSelect)
        if featuresSelect == SELECTED_FEATURES:
            featuresIterator = layer.getSelectedFeatures()
            total = layer.selectedFeatureCount()
        else:
            featuresIterator = layer.getFeatures()
            total = layer.featureCount()
        count = 0
        for feature in featuresIterator:
            #debug("create feature.id = %s" % feature.id())
            routeId = feature[routeFieldName]
            start = feature[startFieldName]
            end = feature[endFieldName] if endFieldName else None
            # debug ( "event routeId = %s start = %s end = %s" % ( routeId, start, end ) )
            # Offset
            startOffset = feature[startOffsetFieldName] if startOffsetFieldName else 0.0
            endOffset = feature[endOffsetFieldName] if endOffsetFieldName else 0.0

            outputFeature = QgsFeature(fields)  # fields must exist during feature life!
            for field in layer.fields():
                if outputFeature.fields().indexFromName(field.name()) >= 0:
                    outputFeature[field.name()] = feature[field.name()]

            geo = None
            if endFieldName:
                if self.is_null(start) or self.is_null(end):
                    error = 'measure is null'
                else:
                    line, error = self.lrs.eventMultiPolyLine(routeId, start, end, eventTolerance, startOffset, endOffset)
                    if line:
                        geo = QgsGeometry.fromMultiPolylineXY(line)
            else:
                if self.is_null(start):
                    error = 'measure is null'
                else:
                    point, error = self.lrs.eventPointXY(routeId, start, eventTolerance, startOffset)
                    if point:
                        geo = QgsGeometry(QgsPoint(point))

            if geo:
                outputFeature.setGeometry(geo)

            if errorFieldName and error:
                outputFeature[errorFieldName] = error

            outputFeatures.append(outputFeature)

            count += 1
            percent = 100 * count / total
            if self.progressBar:
                self.progressBar.setValue(percent)

        outputLayer.dataProvider().addFeatures(outputFeatures)

        QgsProject.instance().addMapLayers([outputLayer, ])

        if self.progressBar:
            self.progressBar.hide() class LrsCalib(LrsBase):
    progressChanged = pyqtSignal(str, float, name='progressChanged')
    updateErrors = pyqtSignal(dict, name='updateErrors')
    edited = pyqtSignal(name='edited')

    # progress counts
    CURRENT = 1  # current progress count
    TOTAL = 2  # sum of steps to do
    NLINES = 3  # number of lines
    NPOINTS = 4  # number of points
    NROUTES = 5  # number of routes

    # calibration steps
    REGISTERING_LINES = 1
    REGISTERING_POINTS = 2
    CALIBRATING_ROUTES = 3

    stateLabels = {
        REGISTERING_LINES: 'Registering lines',
        REGISTERING_POINTS: 'Registering points',
        CALIBRATING_ROUTES: 'Calibrating routes',
    }

    def __init__(self, lineLayer, lineRouteField, pointLayer, pointRouteField, pointMeasureField, **kwargs):
        super(LrsCalib, self).__init__(**kwargs)

        self.lineLayer = lineLayer
        self.lineRouteField = lineRouteField
        self.pointLayer = pointLayer
        self.pointRouteField = pointRouteField
        self.pointMeasureField = pointMeasureField
        # selectionMode: all,include,exclude selection
        self.selectionMode = kwargs.get('selectionMode', 'all')
        # selection is list of route ids to be included/excluded
        self.selection = []
        for routeId in kwargs.get('selection', []):
            self.selection.append(normalizeRouteId(routeId))
        # max lines gaps snap
        self.snap = kwargs.get('snap', 0.0)
        # threshold - max distance between point and line in canvas CRS units
        self.threshold = kwargs.get('threshold', 10.0)
        self.parallelMode = kwargs.get('parallelMode', 'error')
        self.crs = kwargs.get('crs')


        self.distanceArea = QgsDistanceArea()
        # QgsDistanceArea.setSourceCrs( QgsCoordinateReferenceSystem ) is missing in SIP in at least QGIS 2.0
        self.distanceArea.setSourceCrs(self.crs, QgsProject.instance().transformContext())
        if self.crs.mapUnits() == QgsUnitTypes.DistanceDegrees:
            ellipsoid = self.crs.ellipsoidAcronym()
            if not ellipsoid: ellipsoid = "WGS84"
            self.distanceArea.setEllipsoid(ellipsoid)

        # extrapolate LRS before/after calibration points
        self.extrapolate = kwargs.get('extrapolate', False)

        # stored line route id QgsField to know type
        self.routeField = None

        self.pointLayer.editingStarted.connect(self.pointLayerEditingStarted)
        self.pointLayer.editingStopped.connect(self.pointLayerEditingStopped)
        self.pointEditBuffer = None
        if self.pointLayer.editBuffer():  # layer is already in editing mode
            self.pointLayerEditingStarted()

        self.lineLayer.editingStarted.connect(self.lineLayerEditingStarted)
        self.lineLayer.editingStopped.connect(self.lineLayerEditingStopped)
        self.lineEditBuffer = None
        if self.lineLayer.editBuffer():
            self.lineLayerEditingStarted()

        self.lines = {}  # dict of LrsLine with fid as key
        self.points = {}  # dict of LrsPoint with fid as key

        self.errors = []  # LrsError list

        self.progressCounts = {}

        # Numbers of features/lines/points currently not used because did not 
        # correspond well to list/layer of errors
        self.stats = {}  # statistics
        self.statsNames = (
            # ( 'lineFeatures', 'Total number of line features' ), # may be multilinestrings
            # ( 'lineFeaturesIncluded', 'Number of included line features' ), # selected
            # ( 'lines', 'Total number of line strings' ), # may be parts of multi
            # ( 'linesIncluded', 'Number of included line strings' ),
            # ( 'pointFeatures', 'Total number of point features' ), #may be multipoint
            # ( 'pointFeaturesIncluded', 'Number of included point features' ), # selected
            # ( 'points', 'Total number of points' ), # may be parts of multi
            # ( 'pointsIncluded', 'Number of included points' ), # selected
            # ( 'pointsOk', 'Number of included points successfully used in LRS' ),
            # ( 'pointsError', 'Number of included points with error' ),
            ('length', 'Total length of all lines'),
            ('lengthIncluded', 'Length of included lines'),
            ('lengthOk', 'Length of successfully created LRS'),
            # ( 'lengthError', 'Length of included lines without LRS' ),
        )

        self.lineTransform = None
        if self.crs and self.crs != lineLayer.crs():
            self.lineTransform = QgsCoordinateTransform(lineLayer.crs(), self.crs,
                                                        QgsProject.instance())

        self.pointTransform = None
        if self.crs and self.crs != pointLayer.crs():
            self.pointTransform = QgsCoordinateTransform(pointLayer.crs(), self.crs,
                                                         QgsProject.instance())

        self.wasEdited = False  # true if layers were edited since calibration

        QgsProject.instance().layersWillBeRemoved.connect(self.layersWillBeRemoved)

    def __del__(self):
        self.disconnect()

    def pointLayerDisconnect(self):
        if not self.pointLayer: return
        self.pointLayerEditingDisconnect()
        self.pointLayer.editingStarted.disconnect(self.pointLayerEditingStarted)
        self.pointLayer.editingStopped.disconnect(self.pointLayerEditingStopped)

    def lineLayerDisconnect(self):
        if not self.lineLayer: return
        self.lineLayerEditingDisconnect()
        self.lineLayer.editingStarted.disconnect(self.lineLayerEditingStarted)
        self.lineLayer.editingStopped.disconnect(self.lineLayerEditingStopped)

    def disconnect(self):
        QgsProject.instance().layersWillBeRemoved.disconnect(self.layersWillBeRemoved)
        self.pointLayerDisconnect()
        self.lineLayerDisconnect()

    def layersWillBeRemoved(self, layerIdList):
        project = QgsProject.instance()
        for id in layerIdList:
            if self.pointLayer and self.pointLayer.id() == id:
                self.pointLayerDisconnect()
                self.pointLayer = None
            if self.lineLayer and self.lineLayer.id() == id:
                self.lineLayerDisconnect()
                self.lineLayer = None

    # ------------------- COMMON -------------------
    # get route by id, create it if does not exist
    # routeId does not have to be normalized
    def getRoute(self, routeId):
        normalId = normalizeRouteId(routeId)
        # debug ( 'normalId = %s orig type = %s' % (normalId, type(routeId) ) )
        if normalId not in self.routes:
            self.routes[normalId] = LrsCalibRoute(self.lineLayer, routeId, self.snap, self.threshold, self.crs,
                                                  self.measureUnit, self.distanceArea, parallelMode=self.parallelMode)
        return self.routes[normalId]


    # test if process route according to selectionMode and selection
    def routeIdSelected(self, routeId):
        routeId = normalizeRouteId(routeId)
        if self.selectionMode == 'all':
            return True
        elif self.selectionMode == 'include':
            return routeId in self.selection
        elif self.selectionMode == 'exclude':
            return routeId not in self.selection

    # ------------------- GENERATE (CALIBRATE) -------------------

    def updateProgressTotal(self):
        cnts = self.progressCounts
        cnts[self.TOTAL] = cnts[self.NLINES]
        cnts[self.TOTAL] += cnts[self.NPOINTS]
        cnts[self.TOTAL] += cnts[self.NROUTES]  # calibrate routes
        # debug ("%s" % cnts )

    # increase progress, called after each step (line, point...)
    def progressStep(self, state):
        self.progressCounts[self.CURRENT] = self.progressCounts.get(self.CURRENT, 0) + 1
        percent = 100 * self.progressCounts[self.CURRENT] / self.progressCounts[self.TOTAL]
        # debug ( "percent = %s %s / %s" % (percent, self.progressCounts[self.CURRENT], self.progressCounts[self.TOTAL] ) )
        self.progressChanged.emit(self.stateLabels[state], percent)

    def calibrate(self):
        # debug ( 'Lrs.calibrate' )
        self.progressChanged.emit(self.stateLabels[self.REGISTERING_LINES], 0)

        self.points = {}
        self.lines = {}
        self.errors = []  # reset

        self.stats = {}
        for s in self.statsNames:
            self.stats[s[0]] = 0

        field = self.lineLayer.fields().field(self.lineRouteField)
        self.routeField = QgsField(field.name(), field.type(), field.typeName(), field.length(), field.precision())

        self.progressCounts = {}
        # we don't know progressTotal at the beginning, but we can estimate it
        self.progressCounts[self.NLINES] = self.lineLayer.featureCount()
        self.progressCounts[self.NPOINTS] = self.pointLayer.featureCount()
        # estimation (precise later when routes are built)
        self.progressCounts[self.NROUTES] = self.progressCounts[self.NLINES]
        self.updateProgressTotal()

        self.registerLines()
        self.registerPoints()
        for route in self.routes.values():
            route.calibrate(self.extrapolate)
            self.progressStep(self.CALIBRATING_ROUTES)

            # count stats
        for route in self.routes.values():
            # self.stats['pointsOk'] += len ( route.getGoodMilestones() )
            self.stats['lengthOk'] += route.getGoodLength()

            # self.stats['pointsError'] = self.stats['pointsIncluded'] - self.stats['pointsOk']

    def isCalibrated(self):
        return len(self.routes) > 0

    # -------------------------------- register / unregister features ----------------------

    def registerLineFeature(self, feature):
        routeId = feature[self.lineRouteField]
        if routeId == '' or routeId == NULL: routeId = None
        # debug ( "fid = %s routeId = %s" % ( feature.id(), routeId ) )

        if not self.routeIdSelected(routeId):
            return None

        geo = feature.geometry()
        if geo:
            if self.lineTransform:
                geo.transform(self.lineTransform)

        route = self.getRoute(routeId)
        line = LrsLine(feature.id(), routeId, geo)
        self.lines[feature.id()] = line
        route.addLine(line)
        return line

    def unregisterLineByFid(self, fid):
        line = self.lines[fid]
        route = self.getRoute(line.routeId)
        route.removeLine(fid)
        del self.lines[fid]

    def registerLines(self):
        self.routes = {}
        for feature in self.lineLayer.getFeatures():
            line = self.registerLineFeature(feature)
            # self.stats['lineFeatures'] += 1
            length = 0
            if feature.geometry():
                length = self.distanceArea.measureLength(feature.geometry())
            self.stats['length'] += length
            if line:
                # self.stats['lineFeaturesIncluded'] += 1
                # self.stats['linesIncluded'] += line.getNumParts()
                self.stats['lengthIncluded'] += length
            self.progressStep(self.REGISTERING_LINES)
            # precise number of routes
        self.progressCounts[self.NROUTES] = len(self.routes)
        self.updateProgressTotal()

    # returns LrsPoint
    def registerPointFeature(self, feature):
        routeId = feature[self.pointRouteField]
        if routeId == '' or routeId == NULL:
            routeId = None

        if not self.routeIdSelected(routeId):
            return None

        measure = feature[self.pointMeasureField]
        if measure == NULL: measure = None
        if measure is not None:
            # convert to float to don't care later about operations with integers
            measure = float(measure)
        # debug ( "fid = %s routeId = %s measure = %s" % ( feature.id(), routeId, measure ) )
        geo = feature.geometry()
        if geo:
            if self.pointTransform:
                geo.transform(self.pointTransform)

        point = LrsPoint(feature.id(), routeId, measure, geo)
        self.points[feature.id()] = point
        route = self.getRoute(routeId)
        route.addPoint(point)
        return point

    def unregisterPointByFid(self, fid):
        point = self.points[fid]
        route = self.getRoute(point.routeId)
        route.removePoint(fid)
        del self.points[fid]

    def registerPoints(self):
        for feature in self.pointLayer.getFeatures():
            point = self.registerPointFeature(feature)
            # self.stats['pointFeatures'] += 1
            # if point:
            # self.stats['pointFeaturesIncluded'] += 1
            # self.stats['pointsIncluded'] += point.getNumParts()
            self.progressStep(self.REGISTERING_POINTS)
            # route total may increase (e.g. orphans)
        self.progressCounts[self.NROUTES] = len(self.routes)
        self.updateProgressTotal()

    # -------------------33

    def getRouteIds(self):
        ids = []
        for route in self.routes.values():
            if route.routeId is not None:
                ids.append(route.routeId)

        ids.sort()
        return ids

    def getErrors(self):
        errors = list(self.errors)
        for route in self.routes.values():
            errors.extend(route.getErrors())
        return errors

    def getParts(self):
        parts = []
        for route in self.routes.values():
            parts.extend(route.parts)
        return parts

    def getSegments(self):
        segments = []
        for route in self.routes.values():
            segments.extend(route.getSegments())
        return segments

    def getQualityFeatures(self):
        features = []
        for route in self.routes.values():
            features.extend(route.getQualityFeatures())
        return features

    # ----------------------------- Editing ---------------------------------
    def pointLayerEditingStarted(self):
        self.pointEditBuffer = self.pointLayer.editBuffer()
        self.pointEditBuffer.featureAdded.connect(self.pointFeatureAdded)
        self.pointEditBuffer.featureDeleted.connect(self.pointFeatureDeleted)
        # some versions of PyQt fail (Win build) with new style connection if the signal has multiple params
        # self.pointEditBuffer.geometryChanged.connect( self.pointGeometryChanged )
        # QObject.connect(self.pointEditBuffer, SIGNAL("geometryChanged(QgsFeatureId, QgsGeometry &)"),
        #                self.pointGeometryChanged)
        # Not working:
        #self.pointEditBuffer.geometryChanged["QgsFeatureId, QgsGeometry"].connect(self.pointGeometryChanged)
        # Trying simple connect hoping that it works also on Windows
        self.pointEditBuffer.geometryChanged.connect(self.pointGeometryChanged)
        self.pointEditBuffer.attributeValueChanged.connect(self.pointAttributeValueChanged)

    def pointLayerEditingStopped(self):
        self.pointEditBuffer = None

    def pointLayerEditingDisconnect(self):
        if self.pointEditBuffer:
            self.pointEditBuffer.featureAdded.disconnect(self.pointFeatureAdded)
            self.pointEditBuffer.featureDeleted.disconnect(self.pointFeatureDeleted)
            self.pointEditBuffer.geometryChanged.disconnect(self.pointGeometryChanged)
            self.pointEditBuffer.attributeValueChanged.disconnect(self.pointAttributeValueChanged)

    def lineLayerEditingStarted(self):
        self.lineEditBuffer = self.lineLayer.editBuffer()
        self.lineEditBuffer.featureAdded.connect(self.lineFeatureAdded)
        self.lineEditBuffer.featureDeleted.connect(self.lineFeatureDeleted)
        # some versions of PyQt fail (Win build) with new style connection if the signal has multiple params
        # self.lineEditBuffer.geometryChanged.connect( self.lineGeometryChanged )
        # QObject.connect(self.lineEditBuffer, SIGNAL("geometryChanged(QgsFeatureId, QgsGeometry &)"),
        #                self.lineGeometryChanged)
        # Not working:
        #self.lineEditBuffer.geometryChanged["QgsFeatureId, QgsGeometry"].connect(self.lineGeometryChanged)
        # Trying simple connect hoping that it works also on Windows
        self.lineEditBuffer.geometryChanged.connect(self.lineGeometryChanged)
        self.lineEditBuffer.attributeValueChanged.connect(self.lineAttributeValueChanged)

    def lineLayerEditingStopped(self):
        self.lineEditBuffer = None

    def lineLayerEditingDisconnect(self):
        if self.lineEditBuffer:
            self.lineEditBuffer.featureAdded.disconnect(self.lineFeatureAdded)
            self.lineEditBuffer.featureDeleted.disconnect(self.lineFeatureDeleted)
            self.lineEditBuffer.geometryChanged.disconnect(self.lineGeometryChanged)
            self.lineEditBuffer.attributeValueChanged.disconnect(self.lineAttributeValueChanged)

    def setEdited(self):
        self.wasEdited = True
        self.edited.emit()

    def getEdited(self):
        return self.wasEdited

    def emitUpdateErrors(self, errorUpdates):
        errorUpdates['crs'] = self.crs
        self.updateErrors.emit(errorUpdates)

    # Warning: featureAdded is called first with temporary (negative fid)
    # then, when changes are commited, featureDeleted is called with that 
    # temporary id and featureAdded with real new id,
    # if changes are rollbacked, only featureDeleted is called

    # ------------------- point edit -------------------
    def pointFeatureAdded(self, fid):
        # added features have temporary negative id
        # debug ( "feature added fid %s" % fid )
        self.setEdited()
        feature = getLayerFeature(self.pointLayer, fid)
        point = self.registerPointFeature(feature)  # returns LrsPoint
        if not point: return  # route id not in selection

        route = self.getRoute(point.routeId)
        errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
        self.emitUpdateErrors(errorUpdates)

    def pointFeatureDeleted(self, fid):
        # debug ( "feature deleted fid %s" % fid )
        self.setEdited()
        # deleted feature cannot be read anymore from layer
        point = self.points.get(fid)
        if not point: return  # route id not in selection

        route = self.getRoute(point.routeId)
        self.unregisterPointByFid(fid)
        errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
        self.emitUpdateErrors(errorUpdates)

    def pointGeometryChanged(self, fid, geo):
        # debug ( "geometry changed fid %s" % fid )
        self.setEdited()

        # remove old
        point = self.points.get(fid)
        if not point: return  # route id not in selection

        route = self.getRoute(point.routeId)
        self.unregisterPointByFid(fid)

        # add new
        feature = getLayerFeature(self.pointLayer, fid)
        self.registerPointFeature(feature)

        errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
        self.emitUpdateErrors(errorUpdates)

    def pointAttributeValueChanged(self, fid, attIdx, value):
        # debug ( "attribute changed fid = %s attIdx = %s value = %s " % (fid, attIdx, value) )
        self.setEdited()

        fields = self.pointLayer.fields()
        routeIdx = fields.indexFromName(self.pointRouteField)
        measureIdx = fields.indexFromName(self.pointMeasureField)
        # debug ( "routeIdx = %s measureIdx = %s" % ( routeIdx, measureIdx) )

        if attIdx == routeIdx or attIdx == measureIdx:
            point = self.points.get(fid)
            if point:  # was in selection
                route = self.getRoute(point.routeId)
                self.unregisterPointByFid(fid)

                if attIdx == routeIdx:
                    # recalibrate old
                    errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
                    self.emitUpdateErrors(errorUpdates)

            feature = getLayerFeature(self.pointLayer, fid)
            point = self.registerPointFeature(feature)  # returns LrsPoint
            if point:  # route id in selection
                route = self.getRoute(point.routeId)
                errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
                self.emitUpdateErrors(errorUpdates)

    # --------------------------- line edit ---------------------------
    def lineFeatureAdded(self, fid):
        # added features have temporary negative id
        # debug ( "feature added fid %s" % fid )
        self.setEdited()
        feature = getLayerFeature(self.lineLayer, fid)
        line = self.registerLineFeature(feature)  # returns LrsLine
        if not line: return  # route id not in selection

        route = self.getRoute(line.routeId)
        errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
        self.emitUpdateErrors(errorUpdates)

    def lineFeatureDeleted(self, fid):
        # debug ( "feature deleted fid %s" % fid )
        # deleted feature cannot be read anymore from layer
        self.setEdited()
        line = self.lines.get(fid)
        if not line: return  # route id not in selection

        route = self.getRoute(line.routeId)
        self.unregisterLineByFid(fid)
        errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
        self.emitUpdateErrors(errorUpdates)

    def lineGeometryChanged(self, fid, geo):
        # debug ( "geometry changed fid %s" % fid )
        self.setEdited()

        # remove old
        line = self.lines.get(fid)
        if not line: return  # route id not in selection

        route = self.getRoute(line.routeId)
        self.unregisterLineByFid(fid)

        # add new
        feature = getLayerFeature(self.lineLayer, fid)
        self.registerLineFeature(feature)

        errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
        self.emitUpdateErrors(errorUpdates)

    def lineAttributeValueChanged(self, fid, attIdx, value):
        # debug ( "attribute changed fid = %s attIdx = %s value = %s " % (fid, attIdx, value) )
        self.setEdited()

        fields = self.lineLayer.fields()
        routeIdx = fields.indexFromName(self.lineRouteField)
        # debug ( "routeIdx = %s" % ( routeIdx, measureIdx) )

        if attIdx == routeIdx:
            line = self.lines.get(fid)
            if line:  # was in selection
                route = self.getRoute(line.routeId)
                self.unregisterLineByFid(fid)
                errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
                self.emitUpdateErrors(errorUpdates)

            feature = getLayerFeature(self.lineLayer, fid)

            line = self.registerLineFeature(feature)  # returns LrsLine
            if line:  # route id in selection
                route = self.getRoute(line.routeId)
                errorUpdates = route.calibrateAndGetUpdates(self.extrapolate)
                self.emitUpdateErrors(errorUpdates)

    # ------------------- STATS -------------------
    def getStatsHtmlRow(self, name, label):
        # return "%s : %s<br>" % ( label, self.stats[name] )
        value = self.stats[name]
        # lengths are in map units not in measure units
        # if 'length' in name.lower():
        #    value = formatMeasure( value, self.measureUnit )
        return "<tr><td>%s</td> <td>%s</td></tr>" % (label, value)

    def getStatsHtml(self):
        html = '''<html><head>
                    <style type="text/css">
                      table {
                        border: 1px solid gray;
                        border-spacing: 0px;
                      }
                      td {
                        padding: 5px;
                        border: 1px solid gray;
                        font-size: 10pt;
                      }
                      body {
                        font-size: 10pt;
                      }
                    </style>
                  </head><body>'''
        html += '<table border="1">'

        for s in self.statsNames:
            html += self.getStatsHtmlRow(s[0], s[1])

        html += '</table>'
        html += '<p>Lengths in map units.'
        html += '</body></html>'
        return html

    # get statistics
    def getStat(self, name):
        return self.stats[name] class LrsLayer(LrsBase):
    def __init__(self, layer, **kwargs):
        super(LrsLayer, self).__init__(**kwargs)
        self.layer = layer
        self.crs = layer.crs()
        self.routeFieldName = None  # field name

    def setRouteFieldName(self, routeField):
        self.routeFieldName = routeField

    # load from layer
    def load(self, progressFunction=None):
        #debug("load %s %s" % (self.layer.name(), self.routeFieldName))
        self.reset()
        if not self.routeFieldName:
            return
        total = self.layer.featureCount()
        count = 0
        for feature in self.layer.getFeatures():
            # sleep(1)  # progress debug
            geo = feature.geometry()
            # if geo:
            #     if self.lineTransform:
            #         geo.transform(self.lineTransform)

            routeId = feature[self.routeFieldName]
            route = self.getRoute(routeId)
            #line = LrsLine(feature.id(), routeId, geo)
            #self.lines[feature.id()] = line
            if geo:
                for g in geo.asGeometryCollection():
                    part = LrsLayerPart(g)
                    route.addPart(part)

            count += 1
            percent = 100 * count / total;
            if progressFunction:
                progressFunction(percent)

        for route in self.routes.values():
            route.checkPartOverlaps()

    # get route by id, create it if does not exist
    # routeId does not have to be normalized
    def getRoute(self, routeId):
        normalId = normalizeRouteId(routeId)
        # debug ( 'normalId = %s orig type = %s' % (normalId, type(routeId) ) )
        if normalId not in self.routes:
            self.routes[normalId] = LrsLayerRoute(routeId, parallelMode='error')
        return self.routes[normalId]

    def getRouteIds(self):
        #debug("getRouteIds routeFieldName = %s" % self.routeFieldName)
        if not self.layer or not self.routeFieldName:
            return []
        ids = set()
        for feature in self.layer.getFeatures():
            ids.add(feature[self.routeFieldName])
        ids = list(ids)
        ids.sort()
        return ids class LrsOutput(QObject):
    def __init__(self, iface, lrs, showProgressFunction):
        # debug( "LrsOutput.__init__")
        self.iface = iface
        self.lrs = lrs  # Lrs object
        self.showProgressFunction = showProgressFunction

    def output(self, outputName):
        #geometryType = "MultiLineStringM"
        geometryType = "LineStringM"
        uri = geometryType
        uri += "?crs=%s" % crsString(self.iface.mapCanvas().mapSettings().destinationCrs())
        provider = QgsProviderRegistry.instance().createProvider('memory', uri)

        routeField = self.lrs.routeField
        routeFieldName = routeField.name()
        routeFieldType = "string"
        if routeField.type() == QVariant.Int or routeField.type() == QVariant.UInt \
                or routeField.type() == QVariant.LongLong or routeField.type() == QVariant.ULongLong:
            routeFieldType = "int"
        elif routeField.type() == QVariant.Double:
            routeFieldType = "double"

        provider.addAttributes([
            QgsField(routeFieldName, routeField.type(), routeFieldType),
            QgsField("m_from", QVariant.Double, 'double'),
            QgsField("m_to", QVariant.Double, 'double'),
        ])
        uri = provider.dataSourceUri()
        #debug('uri: %s' % uri)

        outputLayer = QgsVectorLayer(uri, outputName, 'memory')
        outputFeatures = []

        total = len(self.lrs.getParts())
        count = 0
        for part in self.lrs.getParts():
            percent = 100 * count / total
            self.showProgressFunction("Exporting features", percent)
            count += 1
            if not part.records:
                continue
            geo = part.getGeometryWithMeasures()
            if not geo:
                continue

            if routeField.type() == QVariant.Int or routeField.type() == QVariant.Double:
                routeVal = part.routeId
            else:
                routeVal = "%s" % part.routeId

            outputFeature = QgsFeature(outputLayer.fields())  # fields must exist during feature life!
            outputFeature.setGeometry(geo)
            outputFeature[routeFieldName] = routeVal
            outputFeature["m_from"] = part.milestoneMeasureFrom()
            outputFeature["m_to"] = part.milestoneMeasureTo()

            outputFeatures.append(outputFeature)

        outputLayer.dataProvider().addFeatures(outputFeatures)

        QgsProject.instance().addMapLayers([outputLayer, ]) class LrsMeasures(QObject):
    def __init__(self, iface, lrs, progressBar):
        # debug( "LrsMeasures.__init__")
        self.iface = iface
        self.lrs = lrs  # Lrs object
        self.progressBar = progressBar

    def calculate(self, layer, routeFieldName, outputRouteFieldName, measureFieldName, threshold, outputName):
        # get the  lrs layer's route field, to obtain its type
        lrsFields = self.lrs.layer.fields()
        lrsRouteField = lrsFields.at(lrsFields.indexFromName(self.lrs.routeFieldName))

        # create new layer
        # it may happen that memory provider does not support all fields types, see #10, check if fields exists
        # uri = "Point?crs=%s" %  crsString ( self.iface.mapCanvas().mapSettings().destinationCrs() )
        uri = "Point?crs=%s" % crsString(layer.crs())
        provider = QgsProviderRegistry.instance().createProvider('memory', uri)
        fieldsList = layer.fields().toList()
        fixFields(fieldsList)
        provider.addAttributes(fieldsList)
        provider.addAttributes([
            QgsField(outputRouteFieldName, lrsRouteField.type(), lrsRouteField.typeName()),
            QgsField(measureFieldName, QVariant.Double, "double"),
        ])

        uri = provider.dataSourceUri()
        outputLayer = QgsVectorLayer(uri, outputName, 'memory')

        checkFields(layer, outputLayer)

        # Not sure why attributes were set again here, the attributes are already in uri
        # outputLayer.startEditing()  # to add fields
        # for field in layer.fields():
        #     if not outputLayer.addAttribute(field):
        #         QMess        # create new layerageBox.information(self, 'Information', 'Cannot add attribute %s' % field.name())
        #
        # outputLayer.addAttribute(QgsField(outputRouteFieldName, QVariant.String, "string"))
        # outputLayer.addAttribute(QgsField(measureFieldName, QVariant.Double, "double"))
        # outputLayer.commitChanges()

        outputFeatures = []
        fields = outputLayer.fields()
        total = layer.featureCount()
        count = 0
        transform = None
        if layer.crs() != self.lrs.crs:
            transform = QgsCoordinateTransform(layer.crs(), self.lrs.crs, QgsProject.instance())
        for feature in layer.getFeatures():
            featureRouteId = None
            points = []

            geo = feature.geometry()
            if geo:
                if QgsWkbTypes.isSingleType(geo.wkbType()):
                    points = [geo.asPoint()]
                else:
                    points = geo.asMultiPoint()

            # fetch the route, if any, specified by the feature
            if routeFieldName is not None:
                featureRouteId = feature[routeFieldName]
                if (type(featureRouteId) is QVariant) and featureRouteId.isNull():
                    featureRouteId = None
                else:
                    featureRoute = self.lrs.getRouteIfExists(featureRouteId)

            for point in points:
                outputFeature = QgsFeature(fields)  # fields must exist during feature life!
                outputFeature.setGeometry(QgsGeometry.fromPointXY(point))

                for field in layer.fields():
                    if outputFeature.fields().indexFromName(field.name()) >= 0:
                        outputFeature[field.name()] = feature[field.name()]

                if transform:
                    point = transform.transform(point)

                # measure along the feature's route, if it specifies one; otherwise, find the nearest route and
                # measure along it
                if featureRouteId is not None:
                    routeId = featureRouteId
                    measure = featureRoute.pointMeasure(point) if featureRoute is not None else None
                else:
                    routeId, measure = self.lrs.pointMeasure(point, threshold)
                # debug ( "routeId = %s merasure = %s" % (routeId, measure) )

                if routeId is not None:
                    outputFeature[outputRouteFieldName] = routeId
                outputFeature[measureFieldName] = measure

                outputFeatures.append(outputFeature)

            count += 1
            percent = 100 * count / total
            self.progressBar.setValue(percent)

        outputLayer.dataProvider().addFeatures(outputFeatures)

        QgsProject.instance().addMapLayers([outputLayer, ])

        self.progressBar.hide() class LrsComboManager(LrsComboManagerBase):
    def __init__(self, comboOrList, **kwargs):
        super(LrsComboManager, self).__init__(comboOrList, **kwargs)
        self.connectCombos() class LrsFieldComboManager(LrsComboManagerBase):
    fieldNameChanged = pyqtSignal(str)
    fieldNameActivated = pyqtSignal(str)

    def __init__(self, comboOrList, layerComboManager, **kwargs):
        super(LrsFieldComboManager, self).__init__(comboOrList, **kwargs)
        self.types = kwargs.get('types', None)  # QVariant.type
        self.layerComboManager = layerComboManager
        # it is easier/safer to work with layer id instead of with the layer, because
        # it may be deleted in C++ and disconnect fails
        self.layerId = None  # current layer id

        self.connectCombos()
        self.layerComboManager.layerChanged.connect(self.layerChanged)

    def reload(self):
        self.layerChanged(self.layerComboManager.getLayer())

    def __del__(self):
        self.disconnectFromLayer()

    def getFieldName(self):
        idx = self.comboList[0].currentIndex()
        if idx != -1:
            return self.comboList[0].itemData(idx, Qt.UserRole)
        return None

    def disconnectFromLayer(self):
        layer = QgsProject.instance().mapLayer(self.layerId)
        if layer:
            layer.attributeAdded.disconnect(self.resetFields)
            layer.attributeDeleted.disconnect(self.resetFields)

    def layerChanged(self, layer):
        #debug("layerChanged settingsName = %s" % self.settingsName)
        if not QgsProject:
            return

        self.disconnectFromLayer()

        self.layerId = layer.id() if layer else None
        # debug ("layerId = %s" % layerId)

        self.resetFields()

        # layer = QgsProject.instance().mapLayer(self.layerId)
        if layer:
            layer.attributeAdded.connect(self.resetFields)
            layer.attributeDeleted.connect(self.resetFields)

    def resetFields(self):
        #debug("resetFields settingsName = %s" % self.settingsName)
        layer = QgsProject.instance().mapLayer(self.layerId)
        if not layer:
            for combo in self.comboList:
                combo.clear()
                return

        #debug("resetFields layer = %s" % layer.name())

        # Add none item
        if self.allowNone:
            item = self.findItemByData(None)
            if not item:
                item = QStandardItem("-----")
                item.setData(None, Qt.UserRole)
                self.model.appendRow(item)

        fieldsNames = []
        for idx, field in enumerate(layer.fields()):
            if self.types and not field.type() in self.types: continue
            fieldsNames.append(field.name())

        # delete removed
        for i in range(self.model.rowCount() - 1, -1, -1):
            fieldName = self.model.item(i).data(Qt.UserRole)
            if self.allowNone and fieldName is None: continue
            if not fieldName in fieldsNames:
                self.model.removeRows(i, 1)

        # add new fields
        for idx, field in enumerate(layer.fields()):
            #debug("resetFields %s %s %s" % (field.name(), field.type(), self.types))
            if self.types and not field.type() in self.types: continue
            fieldName = field.name()
            fieldLabel = layer.attributeDisplayName(idx)

            item = self.findItemByData(fieldName)
            if not item:  # add new
                item = QStandardItem(fieldLabel)
                item.setData(fieldName, Qt.UserRole)
                self.model.appendRow(item)
            else:  # update text
                item.setText(fieldLabel)

        self.proxy.sort(0)

    def currentIndexChanged(self, idx):
        #debug("LrsFieldComboManager currentIndexChanged idx = %s settingsName = %s" % (idx, self.settingsName))
        super(LrsFieldComboManager, self).currentIndexChanged(idx)
        self.fieldNameChanged.emit(self.getFieldName())

    def activated(self, idx):
        #self.debug("LrsFieldComboManager activated idx = %s value = %s" % (idx, self.getFieldName()))
        self.fieldNameActivated.emit(self.getFieldName()) class LrsLayerComboManager(LrsComboManagerBase):
    layerChanged = pyqtSignal(QgsMapLayer)

    def __init__(self, comboOrList, **kwargs):
        super(LrsLayerComboManager, self).__init__(comboOrList, **kwargs)
        self.geometryType = kwargs.get('geometryType', None)  # QgsWkbTypes.GeometryType
        self.geometryHasM = kwargs.get('geometryHasM', False)  # has measure

        self.connectCombos()  # connect to this class method

        QgsProject.instance().layersAdded.connect(self.canvasLayersChanged)
        QgsProject.instance().layersRemoved.connect(self.canvasLayersChanged)
        # nameChanged is emitted by layer, see canvasLayersChanged

    def __del__(self):
        if not QgsProject:
            return
        QgsProject.instance().layersAdded.disconnect(self.canvasLayersChanged)
        QgsProject.instance().layersRemoved.disconnect(self.canvasLayersChanged)

    def currentIndexChanged(self, idx):
        #debug("LrsLayerComboManager currentIndexChanged idx = %s" % idx)
        super(LrsLayerComboManager, self).currentIndexChanged(idx)
        self.layerChanged.emit(self.getLayer())

    def layerId(self):
        idx = self.comboList[0].currentIndex()
        if idx != -1:
            return self.comboList[0].itemData(idx, Qt.UserRole)
        return None

    def getLayer(self):
        if not QgsProject:
            return
        lId = self.layerId()
        return QgsProject.instance().mapLayer(lId)

    def canvasLayersChanged(self):
        #self.debug("LrsLayerComboManager currentIndexChanged")
        self.reload()

    def reload(self):
        # https://qgis.org/api/classQgsMapLayerComboBox.html#af4d245f67261e82719290ca028224b3c
        #self.debug("LrsLayerComboManager reload")

        if not QgsProject:
            return
        # layers = []
        # for layer in QgsProject.instance().mapLayers().values():
        #     print(layer)
        #     if layer.type() != QgsMapLayer.VectorLayer:
        #         continue
        #     if self.geometryType is not None and layer.geometryType() != self.geometryType:
        #         continue
        #     if self.geometryHasM and not QgsWkbTypes.hasM(layer.wkbType()):
        #         continue
        #     layers.append(layer)

        # delete removed layers
        for i in range(self.model.rowCount() - 1, -1, -1):
            lid = self.model.item(i).data(Qt.UserRole)
            if lid not in QgsProject.instance().mapLayers().keys():
                # debug("canvasLayersChanged remove lid = %s" % lid)
                self.model.removeRows(i, 1)

        # add new layers
        for layer in QgsProject.instance().mapLayers().values():
            # print(layer)
            if layer.type() != QgsMapLayer.VectorLayer:
                continue
            if self.geometryType is not None and layer.geometryType() != self.geometryType:
                continue
            if self.geometryHasM and not QgsWkbTypes.hasM(layer.wkbType()):
                continue

            start = self.model.index(0, 0, QModelIndex())
            indexes = self.model.match(start, Qt.UserRole, layer.id(), Qt.MatchFixedString)
            if len(indexes) == 0:  # add new
                item = QStandardItem(layer.name())
                item.setData(layer.id(), Qt.UserRole)
                self.model.appendRow(item)
                layer.nameChanged.connect(self.canvasLayersChanged)
            else:  # update text
                index = indexes[0]
                item = self.model.item(index.row(), index.column())
                item.setText(layer.name())

        self.proxy.sort(0) class LrsUnitComboManager(LrsComboManagerBase):
    def __init__(self, comboOrList, **kwargs):
        kwargs['sort'] = False
        super(LrsUnitComboManager, self).__init__(comboOrList, **kwargs)

        for unit in [LrsUnits.METER, LrsUnits.KILOMETER, LrsUnits.FEET, LrsUnits.MILE]:
            item = QStandardItem(LrsUnits.unitName(unit))
            item.setData(unit, Qt.UserRole)
            self.model.appendRow(item)

        self.reset()

    def unit(self):
        idx = self.comboList[0].currentIndex()
        if idx != -1:
            return self.comboList[0].itemData(idx, Qt.UserRole)
        return LrsUnits.UNKNOWN

    def writeToProject(self):
        name = LrsUnits.unitName(self.unit())
        QgsProject.instance().writeEntry(PROJECT_PLUGIN_NAME, self.settingsName, name)

    def readFromProject(self):
        name = QgsProject.instance().readEntry(PROJECT_PLUGIN_NAME, self.settingsName)[0]

        unit = LrsUnits.unitFromName(name)
        idx = self.comboList[0].findData(unit, Qt.UserRole)
        #debug( "readFromProject settingsName = %s name = %s idx = %s" % ( self.settingsName, name, idx) )
        if idx != -1:
            for combo in self.comboList:
                combo.setCurrentIndex(idx)
        else:
            self.reset()
from PyQt5.QtCore import QVariant, QCoreApplication
from qgis.gui import QgsHighlight
from ..lrs.error.lrserrorlayermanager import LrsErrorLayerManager
from ..lrs.error.lrserrorlinelayer import LrsErrorLineLayer
from ..lrs.error.lrserrormodel import LrsErrorModel
from ..lrs.error.lrserrorpointlayer import LrsErrorPointLayer
from ..lrs.error.lrserrorvisualizer import LrsErrorVisualizer
from ..lrs.error.lrsqualitylayer import LrsQualityLayer
from ..lrs.error.lrsqualitylayermanager import LrsQualityLayerManager
from ..lrs.lrsevents import LrsEvents
from ..lrs.lrscalib import LrsCalib
from ..lrs.lrslayer import LrsLayer
from ..lrs.lrsoutput import LrsOutput
from ..lrs.lrsmeasures import LrsMeasures
from .lrscombomanager import LrsComboManager
from .lrscombomanagerbase import *
from .lrsfieldcombomanager import LrsFieldComboManager
from .lrslayercombomanager import LrsLayerComboManager
from .lrsselectiondialog import *
from .lrsunitcombomanager import LrsUnitComboManager
from .lrswidgetmanager import *
from .ui_lrsdockwidget import Ui_LrsDockWidget
    import psycopg2
    import psycopg2.extensions
        self.resetMeasureButtons()
        self.updateMeasureUnits()
        self.enableTabs()

    def isCalibrated(self):
        return self.lrs is not None and self.lrs.isCalibrated()

    def resetGenButtons(self):
        self.genCreateOutputButton.setEnabled(
            self.isCalibrated() and len(self.genOutputNameLineEdit.text().strip()) > 0)

    def createLrsOutput(self):
        output = LrsOutput(self.iface, self.lrs, self.showGenProgress)
        output.output(self.genOutputNameLineEdit.text().strip())
        self.hideGenProgress()

    def showGenProgress(self, label, percent):
        self.genProgressFrame.show()
        self.genProgressLabel.setText(label)
        self.genProgressBar.setValue(percent)

    def hideGenProgress(self):
        self.genProgressFrame.hide()

    # ------------------------------- ERRORS -------------------------------
    def updateErrors(self, errorUpdates):
        # #debug ( "updateErrors" )
        # because SingleSelection does not allow to deselect row, we have to clear selection manually
        index = self.getSelectedErrorIndex()
        if index:
            rows = self.errorModel.rowsToBeRemoved(errorUpdates)
            selected = index.row()
            if selected in rows:
                self.errorView.selectionModel().clear()
        self.errorModel.updateErrors(errorUpdates)
        self.errorSelectionChanged()
        self.updateErrorLayers(errorUpdates)
        self.updateQualityLayer(errorUpdates)

    # def errorSelectionChanged(self, selected, deselected ):
    def errorSelectionChanged(self):
        error = self.getSelectedError()
        self.errorVisualizer.highlight(error, self.lrs.crs)
        self.errorZoomButton.setEnabled(error is not None)

    def getSelectedErrorIndex(self):
        sm = self.errorView.selectionModel()
        if not sm.hasSelection():
            return None
        index = sm.selection().indexes()[0]
        index = self.sortErrorModel.mapToSource(index)
        return index

    def getSelectedError(self):
        index = self.getSelectedErrorIndex()
        if not index:
            return None
        return self.errorModel.getError(index)

    def errorZoom(self):
        error = self.getSelectedError()
        if not error:
            return
        self.errorVisualizer.zoom(error, self.lrs.crs)

        # add new error layers to map

    def addErrorLayers(self):
        project = QgsProject.instance()

        if self.errorLineLayer is None:
            self.errorLineLayer = LrsErrorLineLayer(self.lrs.crs)
            self.errorLineLayerManager = LrsErrorLayerManager(self.errorLineLayer)
            self.errorLineLayer.renderer().symbol().setColor(QColor(Qt.red))
            self.resetErrorLineLayer()
            QgsProject.instance().addMapLayers([self.errorLineLayer, ])
            project.writeEntry(PROJECT_PLUGIN_NAME, "errorLineLayerId", self.errorLineLayer.id())

        if self.errorPointLayer is None:
            self.errorPointLayer = LrsErrorPointLayer(self.lrs.crs)
            self.errorPointLayerManager = LrsErrorLayerManager(self.errorPointLayer)
            self.errorPointLayer.renderer().symbol().setColor(QColor(Qt.red))
            self.resetErrorPointLayer()
            QgsProject.instance().addMapLayers([self.errorPointLayer, ])
            project.writeEntry(PROJECT_PLUGIN_NAME, "errorPointLayerId", self.errorPointLayer.id())

    # reset error layers content (features)
    def resetErrorLayers(self):
        # #debug ( "resetErrorLayers" )
        self.resetErrorPointLayer()
        self.resetErrorLineLayer()

    def updateErrorLayers(self, errorUpdates):
        if self.errorPointLayerManager:
            self.errorPointLayerManager.updateErrors(errorUpdates)
        if self.errorLineLayerManager:
            self.errorLineLayerManager.updateErrors(errorUpdates)

    def updateQualityLayer(self, errorUpdates):
        if self.qualityLayerManager:
            self.qualityLayerManager.update(errorUpdates)

    def resetErrorPointLayer(self):
        # debug("resetErrorPointLayer %s" % self.errorPointLayer)
        if self.errorPointLayerManager is None:
            return
        self.errorPointLayerManager.clear()
        errors = self.lrs.getErrors()
        self.errorPointLayerManager.addErrors(errors, self.lrs.crs)

    def resetErrorLineLayer(self):
        if self.errorLineLayerManager is None:
            return
        self.errorLineLayerManager.clear()
        errors = self.lrs.getErrors()
        self.errorLineLayerManager.addErrors(errors, self.lrs.crs)

    def addQualityLayer(self):
        if not self.qualityLayer:
            self.qualityLayer = LrsQualityLayer(self.lrs.crs)

            self.qualityLayerManager = LrsQualityLayerManager(self.qualityLayer)