Example #1
0
 def convertToPolygon(self, geom):
     if QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.PointGeometry and geom.constGet().nCoordinates() < 3:
         raise QgsProcessingException(
             self.tr('Cannot convert from Point to Polygon').format(QgsWkbTypes.displayString(geom.wkbType())))
     elif QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.PointGeometry:
         # multipoint with at least 3 points
         # TODO: mega inefficient - needs rework when geometry iterators land
         # (but at least it doesn't lose Z/M values)
         points = []
         for g in geom.constGet().coordinateSequence():
             for r in g:
                 for p in r:
                     points.append(p)
         linestring = QgsLineString(points)
         linestring.close()
         p = QgsPolygon()
         p.setExteriorRing(linestring)
         return [QgsGeometry(p)]
     elif QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.LineGeometry:
         if QgsWkbTypes.isMultiType(geom):
             parts = []
             for i in range(geom.constGet().numGeometries()):
                 p = QgsPolygon()
                 linestring = geom.constGet().geometryN(i).clone()
                 linestring.close()
                 p.setExteriorRing(linestring)
                 parts.append(QgsGeometry(p))
             return QgsGeometry.collectGeometry(parts)
         else:
             # linestring to polygon
             p = QgsPolygon()
             linestring = geom.constGet().clone()
             linestring.close()
             p.setExteriorRing(linestring)
             return [QgsGeometry(p)]
     else:
         #polygon
         if QgsWkbTypes.isMultiType(geom):
             return geom.asGeometryCollection()
         else:
             return [geom]
Example #2
0
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        if source is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))

        if QgsWkbTypes.isMultiType(source.wkbType()):
            raise QgsProcessingException(self.tr('Input point layer is a MultiPoint layer - first convert to single points before using this algorithm.'))

        source_field = self.parameterAsString(parameters, self.INPUT_FIELD, context)
        target_source = self.parameterAsSource(parameters, self.TARGET, context)
        if target_source is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.TARGET))

        if QgsWkbTypes.isMultiType(target_source.wkbType()):
            raise QgsProcessingException(self.tr('Target point layer is a MultiPoint layer - first convert to single points before using this algorithm.'))

        target_field = self.parameterAsString(parameters, self.TARGET_FIELD, context)
        same_source_and_target = parameters[self.INPUT] == parameters[self.TARGET]
        matType = self.parameterAsEnum(parameters, self.MATRIX_TYPE, context)
        nPoints = self.parameterAsInt(parameters, self.NEAREST_POINTS, context)

        if nPoints < 1:
            nPoints = target_source.featureCount()

        if matType == 0:
            # Linear distance matrix
            return self.linearMatrix(parameters, context, source, source_field, target_source, target_field, same_source_and_target,
                                     matType, nPoints, feedback)
        elif matType == 1:
            # Standard distance matrix
            return self.regularMatrix(parameters, context, source, source_field, target_source, target_field,
                                      nPoints, feedback)
        elif matType == 2:
            # Summary distance matrix
            return self.linearMatrix(parameters, context, source, source_field, target_source, target_field, same_source_and_target,
                                     matType, nPoints, feedback)
Example #3
0
 def convertToLineStrings(self, geom):
     if QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.PointGeometry:
         raise QgsProcessingException(
             self.tr('Cannot convert from {0} to LineStrings').format(QgsWkbTypes.displayString(geom.wkbType())))
     elif QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.LineGeometry:
         if QgsWkbTypes.isMultiType(geom.wkbType()):
             return geom.asGeometryCollection()
         else:
             #line to line
             return [geom]
     else:
         # polygons to lines
         # we just use the boundary here - that consists of all rings in the (multi)polygon
         boundary = QgsGeometry(geom.constGet().boundary())
         # boundary will be multipart
         return boundary.asGeometryCollection()
Example #4
0
 def convertToMultiLineStrings(self, geom):
     if QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.PointGeometry:
         raise QgsProcessingException(
             self.tr('Cannot convert from {0} to MultiLineStrings').format(QgsWkbTypes.displayString(geom.wkbType())))
     elif QgsWkbTypes.geometryType(geom.wkbType()) == QgsWkbTypes.LineGeometry:
         if QgsWkbTypes.isMultiType(geom.wkbType()):
             return [geom]
         else:
             # line to multiLine
             ml = QgsMultiLineString()
             ml.addGeometry(geom.constGet().clone())
             return [QgsGeometry(ml)]
     else:
         # polygons to multilinestring
         # we just use the boundary here - that consists of all rings in the (multi)polygon
         return [QgsGeometry(geom.constGet().boundary())]
Example #5
0
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        if source is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))

        method = self.parameterAsEnum(parameters, self.METHOD, context)

        wkb_type = source.wkbType()
        fields = source.fields()

        new_fields = QgsFields()
        if QgsWkbTypes.geometryType(wkb_type) == QgsWkbTypes.PolygonGeometry:
            new_fields.append(QgsField('area', QVariant.Double))
            new_fields.append(QgsField('perimeter', QVariant.Double))
        elif QgsWkbTypes.geometryType(wkb_type) == QgsWkbTypes.LineGeometry:
            new_fields.append(QgsField('length', QVariant.Double))
            if not QgsWkbTypes.isMultiType(source.wkbType()):
                new_fields.append(QgsField('straightdis', QVariant.Double))
                new_fields.append(QgsField('sinuosity', QVariant.Double))
        else:
            new_fields.append(QgsField('xcoord', QVariant.Double))
            new_fields.append(QgsField('ycoord', QVariant.Double))
            if QgsWkbTypes.hasZ(source.wkbType()):
                self.export_z = True
                new_fields.append(QgsField('zcoord', QVariant.Double))
            if QgsWkbTypes.hasM(source.wkbType()):
                self.export_m = True
                new_fields.append(QgsField('mvalue', QVariant.Double))

        fields = QgsProcessingUtils.combineFields(fields, new_fields)
        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
                                               fields, wkb_type, source.sourceCrs())
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        coordTransform = None

        # Calculate with:
        # 0 - layer CRS
        # 1 - project CRS
        # 2 - ellipsoidal

        self.distance_area = QgsDistanceArea()
        if method == 2:
            self.distance_area.setSourceCrs(source.sourceCrs(), context.transformContext())
            self.distance_area.setEllipsoid(context.project().ellipsoid())
        elif method == 1:
            coordTransform = QgsCoordinateTransform(source.sourceCrs(), context.project().crs(), context.project())

        features = source.getFeatures()
        total = 100.0 / source.featureCount() if source.featureCount() else 0
        for current, f in enumerate(features):
            if feedback.isCanceled():
                break

            outFeat = f
            attrs = f.attributes()
            inGeom = f.geometry()
            if inGeom:
                if coordTransform is not None:
                    inGeom.transform(coordTransform)

                if inGeom.type() == QgsWkbTypes.PointGeometry:
                    attrs.extend(self.point_attributes(inGeom))
                elif inGeom.type() == QgsWkbTypes.PolygonGeometry:
                    attrs.extend(self.polygon_attributes(inGeom))
                else:
                    attrs.extend(self.line_attributes(inGeom))

            # ensure consistent count of attributes - otherwise null
            # geometry features will have incorrect attribute length
            # and provider may reject them
            if len(attrs) < len(fields):
                attrs += [NULL] * (len(fields) - len(attrs))

            outFeat.setAttributes(attrs)
            sink.addFeature(outFeat, QgsFeatureSink.FastInsert)

            feedback.setProgress(int(current * total))

        return {self.OUTPUT: dest_id}
    def load_complex_gml(self, xml_uri, is_remote, attributes = {}, geometry_mapping = None, logger = None, swap_xy = False):
        """
        :param xml_uri: the XML URI
        :param is_remote: True if it has to be fetched by http
        :param attributes: { 'attr1' : ( '//xpath/expression', QVariant.Int ) }
        :param geometry_mapping: XPath expression to a gml geometry node
        :param swap_xy: True if X/Y coordinates must be swapped
        :returns: the created layer
        """
        try:
            if is_remote:
                xml_src = remote_open_from_qgis(xml_uri)
            else:
                # Open the file in binary mode, this means returning bytes
                # instead of a string whose encoding would have to be interpreted
                # it is up to the XML parser to determine which encoding it is
                xml_src = open(xml_uri, 'rb')
            src = ComplexFeatureSource(xml_src, attributes, geometry_mapping, logger)

            attr_list = [ (k, v[1]) for k, v in attributes.items() ]

            layers = {}
            features = {}
            layer_geom_type = {}
            for id, fid, qgsgeoms, xml, attrs in src.getFeatures(swap_xy):
                # layer creation
                if qgsgeoms == []:
                    if "" not in layers:
                        layer = self._create_layer('none', None, attr_list, src.title, "nogeom")
                        self._add_properties_to_layer(layer, xml_uri, is_remote, attributes, geometry_mapping)
                        layers["nogeom"] = layer
                else:
                    for (qgsgeom, srid), tag in qgsgeoms:
                        if tag in layers:
                            continue
                        type2d = QgsWkbTypes.flatType(qgsgeom.wkbType())
                        typemap = {QgsWkbTypes.Point: 'point',
                                   QgsWkbTypes.MultiPoint: 'multipoint',
                                   QgsWkbTypes.LineString: 'linestring',
                                   QgsWkbTypes.MultiLineString: 'multilinestring',
                                   QgsWkbTypes.Polygon: 'polygon',
                                   QgsWkbTypes.MultiPolygon: 'multipolygon',
                                   QgsWkbTypes.CompoundCurve: 'compoundcurve',
                                   QgsWkbTypes.CircularString: 'compoundcurve',
                                   QgsWkbTypes.MultiCurve: 'multicurve',
                                   QgsWkbTypes.CurvePolygon: 'curvepolygon',
                                   QgsWkbTypes.MultiSurface: 'multisurface'}
                        if qgsgeom and type2d in typemap:
                            title = "{} ({})".format(src.title, no_prefix(tag))
                            layer = self._create_layer(typemap[QgsWkbTypes.multiType(type2d)], srid, attr_list, title, no_prefix(tag))
                        else:
                            raise RuntimeError("Unsupported geometry type {}".format(qgsgeom.wkbType()))
                        self._add_properties_to_layer(layer, xml_uri, is_remote, attributes, geometry_mapping)                        
                        layers[tag] = layer

                # collect features
                f = QgsFeature(layer.dataProvider().fields(), id)
                f.setAttribute("id", str(id))
                f.setAttribute("fid", fid)
                for k, v in attrs.items():
                    r = f.setAttribute(k, v)
                for g, tag in qgsgeoms:
                    if tag not in features:
                        features[tag] = []
                    fcopy = QgsFeature(f)
                    fcopy.setAttribute("_xml_", ET.tostring(xml).decode('utf8'))
                    if g:
                        qgsgeom, _ = g
                        if QgsWkbTypes.isMultiType(layers[tag].wkbType()) and QgsWkbTypes.isSingleType(qgsgeom.wkbType()):
                            # force multi
                            qgsgeom.convertToMultiType()
                        fcopy.setGeometry(qgsgeom)
                    features[tag].append(fcopy)

            # write features
            for tag, f in features.items():
                if len(f) > 0:
                    layer = layers[tag]
                    layer.startEditing()
                    layer.addFeatures(f)
                    layer.commitChanges()
        finally:
            xml_src.close()

        # Set the styl for polygons coming from boundedBy
        for tag_name, layer in layers.items():
            if tag_name.endswith("boundedBy"):
                layer.loadNamedStyle(os.path.join(os.path.dirname(__file__), "..", "gui", "bounded_by_style.qml"))
        return layers
Example #7
0
def make_features_compatible(new_features, input_layer):
    """Try to make the new features compatible with old features by:

    - converting single to multi part
    - dropping additional attributes
    - adding back M/Z values
    - drop Z/M
    - convert multi part to single part

    :param new_features: new features
    :type new_features: list of QgsFeatures
    :param input_layer: input layer
    :type input_layer: QgsVectorLayer
    :return: modified features
    :rtype: list of QgsFeatures
    """

    input_wkb_type = input_layer.wkbType()
    result_features = []
    for new_f in new_features:
        # Fix attributes
        if new_f.fields().count() > 0:
            attributes = []
            for field in input_layer.fields():
                if new_f.fields().indexFromName(field.name()) >= 0:
                    attributes.append(new_f[field.name()])
                else:
                    attributes.append(None)
            f = QgsFeature(input_layer.fields())
            f.setAttributes(attributes)
            f.setGeometry(new_f.geometry())
            new_f = f
        else:
            lendiff = len(new_f.attributes()) - len(input_layer.fields())
            if lendiff > 0:
                f = QgsFeature(input_layer.fields())
                f.setGeometry(new_f.geometry())
                f.setAttributes(new_f.attributes()[:len(input_layer.fields())])
                new_f = f
            elif lendiff < 0:
                f = QgsFeature(input_layer.fields())
                f.setGeometry(new_f.geometry())
                attributes = new_f.attributes() + [None for i in range(-lendiff)]
                f.setAttributes(attributes)
                new_f = f

        # Check if we need geometry manipulation
        new_f_geom_type = QgsWkbTypes.geometryType(new_f.geometry().wkbType())
        new_f_has_geom = new_f_geom_type not in (QgsWkbTypes.UnknownGeometry, QgsWkbTypes.NullGeometry)
        input_layer_has_geom = input_wkb_type not in (QgsWkbTypes.NoGeometry, QgsWkbTypes.Unknown)

        # Drop geometry if layer is geometry-less
        if not input_layer_has_geom and new_f_has_geom:
            f = QgsFeature(input_layer.fields())
            f.setAttributes(new_f.attributes())
            new_f = f
            result_features.append(new_f)
            continue  # skip the rest

        if input_layer_has_geom and new_f_has_geom and \
                new_f.geometry().wkbType() != input_wkb_type:  # Fix geometry
            # Single -> Multi
            if (QgsWkbTypes.isMultiType(input_wkb_type) and not
                    new_f.geometry().isMultipart()):
                new_geom = new_f.geometry()
                new_geom.convertToMultiType()
                new_f.setGeometry(new_geom)
            # Drop Z/M
            if (new_f.geometry().constGet().is3D() and not QgsWkbTypes.hasZ(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().dropZValue()
                new_f.setGeometry(new_geom)
            if (new_f.geometry().constGet().isMeasure() and not QgsWkbTypes.hasM(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().dropMValue()
                new_f.setGeometry(new_geom)
            # Add Z/M back (set it to 0)
            if (not new_f.geometry().constGet().is3D() and QgsWkbTypes.hasZ(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().addZValue(0.0)
                new_f.setGeometry(new_geom)
            if (not new_f.geometry().constGet().isMeasure() and QgsWkbTypes.hasM(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().addMValue(0.0)
                new_f.setGeometry(new_geom)
            # Multi -> Single
            if (not QgsWkbTypes.isMultiType(input_wkb_type) and
                    new_f.geometry().isMultipart()):
                g = new_f.geometry()
                g2 = g.constGet()
                for i in range(g2.partCount()):
                    # Clone or crash!
                    g4 = QgsGeometry(g2.geometryN(i).clone())
                    f = QgsVectorLayerUtils.createFeature(input_layer, g4, {i: new_f.attribute(i) for i in range(new_f.fields().count())})
                    result_features.append(f)
            else:
                result_features.append(new_f)
        else:
            result_features.append(new_f)
    return result_features
Example #8
0
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        method = self.parameterAsEnum(parameters, self.METHOD, context)

        wkb_type = source.wkbType()
        fields = source.fields()

        new_fields = QgsFields()
        if QgsWkbTypes.geometryType(wkb_type) == QgsWkbTypes.PolygonGeometry:
            new_fields.append(QgsField('area', QVariant.Double))
            new_fields.append(QgsField('perimeter', QVariant.Double))
        elif QgsWkbTypes.geometryType(wkb_type) == QgsWkbTypes.LineGeometry:
            new_fields.append(QgsField('length', QVariant.Double))
            if not QgsWkbTypes.isMultiType(source.wkbType()):
                new_fields.append(QgsField('straightdis', QVariant.Double))
                new_fields.append(QgsField('sinuosity', QVariant.Double))
        else:
            new_fields.append(QgsField('xcoord', QVariant.Double))
            new_fields.append(QgsField('ycoord', QVariant.Double))
            if QgsWkbTypes.hasZ(source.wkbType()):
                self.export_z = True
                new_fields.append(QgsField('zcoord', QVariant.Double))
            if QgsWkbTypes.hasM(source.wkbType()):
                self.export_m = True
                new_fields.append(QgsField('mvalue', QVariant.Double))

        fields = QgsProcessingUtils.combineFields(fields, new_fields)
        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT,
                                               context, fields, wkb_type,
                                               source.sourceCrs())

        coordTransform = None

        # Calculate with:
        # 0 - layer CRS
        # 1 - project CRS
        # 2 - ellipsoidal

        self.distance_area = QgsDistanceArea()
        if method == 2:
            self.distance_area.setSourceCrs(source.sourceCrs(),
                                            context.transformContext())
            self.distance_area.setEllipsoid(context.project().ellipsoid())
        elif method == 1:
            coordTransform = QgsCoordinateTransform(source.sourceCrs(),
                                                    context.project().crs(),
                                                    context.project())

        features = source.getFeatures()
        total = 100.0 / source.featureCount() if source.featureCount() else 0
        for current, f in enumerate(features):
            if feedback.isCanceled():
                break

            outFeat = f
            attrs = f.attributes()
            inGeom = f.geometry()
            if inGeom:
                if coordTransform is not None:
                    inGeom.transform(coordTransform)

                if inGeom.type() == QgsWkbTypes.PointGeometry:
                    attrs.extend(self.point_attributes(inGeom))
                elif inGeom.type() == QgsWkbTypes.PolygonGeometry:
                    attrs.extend(self.polygon_attributes(inGeom))
                else:
                    attrs.extend(self.line_attributes(inGeom))

            # ensure consistent count of attributes - otherwise null
            # geometry features will have incorrect attribute length
            # and provider may reject them
            if len(attrs) < len(fields):
                attrs += [NULL] * (len(fields) - len(attrs))

            outFeat.setAttributes(attrs)
            sink.addFeature(outFeat, QgsFeatureSink.FastInsert)

            feedback.setProgress(int(current * total))

        return {self.OUTPUT: dest_id}
Example #9
0
    def processAlgorithm(self, progress):
        layerA = dataobjects.getObjectFromUri(self.getParameterValue(self.INPUT_A))
        splitLayer = dataobjects.getObjectFromUri(self.getParameterValue(self.INPUT_B))

        sameLayer = self.getParameterValue(self.INPUT_A) == self.getParameterValue(self.INPUT_B)
        fieldList = layerA.fields()

        writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(fieldList,
                                                                     layerA.wkbType(), layerA.crs())

        spatialIndex = QgsSpatialIndex()
        splitGeoms = {}
        request = QgsFeatureRequest()
        request.setSubsetOfAttributes([])

        for aSplitFeature in vector.features(splitLayer, request):
            splitGeoms[aSplitFeature.id()] = aSplitFeature.geometry()
            spatialIndex.insertFeature(aSplitFeature)
            # honor the case that user has selection on split layer and has setting "use selection"

        outFeat = QgsFeature()
        features = vector.features(layerA)

        if len(features) == 0:
            total = 100
        else:
            total = 100.0 / float(len(features))

        multiGeoms = 0 # how many multi geometries were encountered

        for current, inFeatA in enumerate(features):
            inGeom = inFeatA.geometry()

            if inGeom.isMultipart():
                multiGeoms += 1
                # MultiGeometries are not allowed because the result of a splitted part cannot be clearly defined:
                # 1) add both new parts as new features
                # 2) store one part as a new feature and the other one as part of the multi geometry
                # 2a) which part should be which, seems arbitrary
            else:
                attrsA = inFeatA.attributes()
                outFeat.setAttributes(attrsA)
                inGeoms = [inGeom]
                lines = spatialIndex.intersects(inGeom.boundingBox())

                if len(lines) > 0:  # has intersection of bounding boxes
                    splittingLines = []

                    engine = QgsGeometry.createGeometryEngine(inGeom.geometry())
                    engine.prepareGeometry()

                    for i in lines:
                        try:
                            splitGeom = splitGeoms[i]
                        except:
                            continue

                        # check if trying to self-intersect
                        if sameLayer:
                            if inFeatA.id() == i:
                                continue

                        if engine.intersects(splitGeom.geometry()):
                            splittingLines.append(splitGeom)

                    if len(splittingLines) > 0:
                        for splitGeom in splittingLines:
                            splitterPList = None
                            outGeoms = []

                            split_geom_engine = QgsGeometry.createGeometryEngine(splitGeom.geometry())
                            split_geom_engine.prepareGeometry()

                            while len(inGeoms) > 0:
                                inGeom = inGeoms.pop()

                                if split_geom_engine.intersects(inGeom.geometry()):
                                    inPoints = vector.extractPoints(inGeom)
                                    if splitterPList == None:
                                        splitterPList = vector.extractPoints(splitGeom)

                                    try:
                                        result, newGeometries, topoTestPoints = inGeom.splitGeometry(splitterPList, False)
                                    except:
                                        ProcessingLog.addToLog(ProcessingLog.LOG_WARNING,
                                                               self.tr('Geometry exception while splitting'))
                                        result = 1

                                    # splitGeometry: If there are several intersections
                                    # between geometry and splitLine, only the first one is considered.
                                    if result == 0:  # split occurred
                                        if inPoints == vector.extractPoints(inGeom):
                                            # bug in splitGeometry: sometimes it returns 0 but
                                            # the geometry is unchanged
                                            QgsMessageLog.logMessage("appending")
                                            outGeoms.append(inGeom)
                                        else:
                                            inGeoms.append(inGeom)

                                            for aNewGeom in newGeometries:
                                                inGeoms.append(aNewGeom)
                                    else:
                                        QgsMessageLog.logMessage("appending else")
                                        outGeoms.append(inGeom)
                                else:
                                    outGeoms.append(inGeom)

                            inGeoms = outGeoms

                for aGeom in inGeoms:
                    passed = True

                    if QgsWkbTypes.geometryType( aGeom.wkbType() )  == QgsWkbTypes.LineGeometry \
                            and not QgsWkbTypes.isMultiType(aGeom.wkbType()):
                        passed = len(aGeom.asPolyline()) > 2

                        if not passed:
                            passed = (len(aGeom.asPolyline()) == 2 and
                                      aGeom.asPolyline()[0] != aGeom.asPolyline()[1])
                            # sometimes splitting results in lines of zero length

                    if passed:
                        outFeat.setGeometry(aGeom)
                        writer.addFeature(outFeat)

            progress.setPercentage(int(current * total))

        if multiGeoms > 0:
            ProcessingLog.addToLog(ProcessingLog.LOG_INFO,
                                   self.tr('Feature geometry error: %s input features ignored due to multi-geometry.') % str(multiGeoms))

        del writer
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.PrmInputLayer,
                                        context)
        decimate_by_distance = self.parameterAsBool(parameters,
                                                    self.PrmDecimateByDistance,
                                                    context)
        decimate_by_time = self.parameterAsBool(parameters,
                                                self.PrmDecimateByTime,
                                                context)
        preserve_final_pt = self.parameterAsBool(parameters,
                                                 self.PrmPreserveFinalPoint,
                                                 context)
        min_distance = self.parameterAsDouble(parameters, self.PrmMinDistance,
                                              context)
        units = self.parameterAsInt(parameters, self.PrmUnitsOfMeasure,
                                    context)
        min_time = self.parameterAsDouble(parameters, self.PrmMinTime, context)
        time_units = self.parameterAsInt(parameters, self.PrmTimeUnits,
                                         context)
        is_or_condition = self.parameterAsInt(parameters,
                                              self.PrmTwoConditionResponnse,
                                              context)
        fields = source.fields()

        if self.PrmTimeField not in parameters or parameters[
                self.PrmTimeField] is None or parameters[
                    self.PrmTimeField] == '':
            time_field = None
            time_idx = None
        else:
            time_field = self.parameterAsString(parameters, self.PrmTimeField,
                                                context)
            time_idx = fields.indexOf(time_field)

        if decimate_by_time and time_field is None:
            msg = tr('Please select a DateTime field when decimating by time')
            feedback.reportError(msg)
            raise QgsProcessingException(msg)

        if self.PrmOrderField not in parameters or parameters[
                self.PrmOrderField] is None or parameters[
                    self.PrmOrderField] == '':
            order_field = None
        else:
            order_field = self.parameterAsString(parameters,
                                                 self.PrmOrderField, context)

        if self.PrmGroupField not in parameters or parameters[
                self.PrmGroupField] is None or parameters[
                    self.PrmGroupField] == '':
            group_field = None
        else:
            group_field = self.parameterAsString(parameters,
                                                 self.PrmGroupField, context)

        wkbtype = source.wkbType()

        if QgsWkbTypes.isMultiType(wkbtype):
            msg = tr('Only supports single part Point geometry')
            feedback.reportError(msg)
            raise QgsProcessingException(msg)

        layercrs = source.sourceCrs()

        (sink, dest_id) = self.parameterAsSink(parameters, self.PrmOutputLayer,
                                               context, fields, wkbtype,
                                               layercrs)

        if layercrs != epsg4326:
            transto4326 = QgsCoordinateTransform(layercrs, epsg4326,
                                                 QgsProject.instance())

        num_pts = source.featureCount()
        total = 100.0 / num_pts if num_pts else 0
        min_time_s = self.convert_time_to_s(min_time, time_units)
        min_distance = min_distance * conversionToMeters(units)
        if group_field:
            grp_indx = fields.indexOf(group_field)
            groups = source.uniqueValues(grp_indx)
            request = QgsFeatureRequest()
            if order_field:
                request.addOrderBy(order_field)
            index = 0
            for group in groups:
                filter = '"{}" = \'{}\''.format(group_field, group)
                request.setFilterExpression(filter)
                iterator = source.getFeatures(request)
                last_decimated = False
                first_feature = True
                for feature in iterator:
                    index += 1
                    if feedback.isCanceled():
                        break
                    pt = feature.geometry().asPoint()
                    if layercrs != epsg4326:  # Convert to 4326
                        pt4326 = transto4326.transform(pt)
                    else:
                        pt4326 = pt

                    if first_feature:
                        first_feature = False
                        sink.addFeature(feature)
                        pt_last = pt4326
                        if decimate_by_time:
                            last_time = feature[time_idx]
                    else:
                        d_keep = True
                        t_keep = True
                        if decimate_by_distance:
                            gline = geod.InverseLine(pt_last.y(), pt_last.x(),
                                                     pt4326.y(), pt4326.x())
                            if gline.s13 < min_distance:
                                d_keep = False
                        if decimate_by_time:
                            cur_time = feature[time_idx]
                            try:
                                diff = abs(
                                    cur_time.toMSecsSinceEpoch() -
                                    last_time.toMSecsSinceEpoch()) / 1000.0
                                if diff < min_time_s:
                                    t_keep = False
                            except Exception:
                                pass

                        if is_or_condition and (d_keep or t_keep):
                            if decimate_by_distance:
                                pt_last = pt4326
                            if decimate_by_time:
                                last_time = cur_time
                            last_decimated = False
                            sink.addFeature(feature)
                        elif d_keep and t_keep:
                            if decimate_by_distance:
                                pt_last = pt4326
                            if decimate_by_time:
                                last_time = cur_time
                            last_decimated = False
                            sink.addFeature(feature)
                        else:
                            last_decimated = True

                    if index % 100:
                        feedback.setProgress(int(index * total))

                if preserve_final_pt and last_decimated:
                    # feature contains the last item from the above for loop which is valid in Python
                    sink.addFeature(feature)
        else:
            if order_field:
                request = QgsFeatureRequest()
                request.addOrderBy(order_field)
                iterator = source.getFeatures(request)
            else:
                iterator = source.getFeatures()
            for cnt, feature in enumerate(iterator):
                if feedback.isCanceled():
                    break
                pt = feature.geometry().asPoint()
                if layercrs != epsg4326:  # Convert to 4326
                    pt4326 = transto4326.transform(pt)
                else:
                    pt4326 = pt
                if cnt == 0:  # This is the first point so it is saved
                    sink.addFeature(feature)
                    pt_last = pt4326
                    if decimate_by_time:
                        last_time = feature[time_idx]
                else:
                    d_keep = True
                    t_keep = True
                    if decimate_by_distance:
                        gline = geod.InverseLine(pt_last.y(), pt_last.x(),
                                                 pt4326.y(), pt4326.x())
                        if gline.s13 < min_distance:
                            d_keep = False
                    if decimate_by_time:
                        cur_time = feature[time_idx]
                        try:
                            diff = abs(cur_time.toMSecsSinceEpoch() -
                                       last_time.toMSecsSinceEpoch()) / 1000.0
                            if diff < min_time_s:
                                t_keep = False
                        except Exception:
                            pass
                    if is_or_condition and (d_keep or t_keep):
                        if decimate_by_distance:
                            pt_last = pt4326
                        if decimate_by_time:
                            last_time = cur_time
                        sink.addFeature(feature)
                    elif d_keep and t_keep:
                        if decimate_by_distance:
                            pt_last = pt4326
                        if decimate_by_time:
                            last_time = cur_time
                        sink.addFeature(feature)
                    elif preserve_final_pt and (cnt == num_pts - 1):
                        sink.addFeature(feature)
                if cnt % 100:
                    feedback.setProgress(int(cnt * total))

        return {self.PrmOutputLayer: dest_id}
    def processAlgorithm(self, parameters, context, feedback):  #pylint: disable=unused-argument,missing-docstring

        layer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
        resolution = self.parameterAsDouble(parameters, self.RESOLUTION,
                                            context)
        lmax = self.parameterAsDouble(parameters, self.LMAX, context)
        # max_angle = self.parameterAsDouble(parameters, self.MAX_ANGLE, context)

        if QgsWkbTypes.isMultiType(layer.wkbType()):
            feedback.reportError(
                self.tr('Multipart geometries are not currently supported'),
                True)
            return {}

        (axis_sink, axis_id) = self.parameterAsSink(parameters, self.FLOW_AXIS,
                                                    context, layer.fields(),
                                                    layer.wkbType(),
                                                    layer.sourceCrs())

        fields = QgsFields(layer.fields())
        fields.append(QgsField('BENDID', QVariant.Int, len=10))
        fields.append(QgsField('NPTS', QVariant.Int, len=4))
        fields.append(QgsField('LBEND', QVariant.Double, len=10, prec=2))
        fields.append(QgsField('LWAVE', QVariant.Double, len=10, prec=2))
        fields.append(QgsField('SINUO', QVariant.Double, len=6, prec=4))
        fields.append(QgsField('AMPLI', QVariant.Double, len=10, prec=4))
        fields.append(QgsField('OMEG0', QVariant.Double, len=10, prec=8))
        fields.append(QgsField('OMEG1', QVariant.Double, len=10, prec=6))
        # QgsField('RCURV', QVariant.Double, len=10, prec=3)

        (segment_sink,
         segment_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
                                            fields, layer.wkbType(),
                                            layer.sourceCrs())

        fields = QgsFields()
        fields.append(QgsField('GID', QVariant.Int, len=10))
        fields.append(QgsField('ANGLE', QVariant.Double, len=10, prec=6))
        fields.append(QgsField('INTERDIST', QVariant.Double, len=10, prec=6))

        (inflection_sink,
         inflection_id) = self.parameterAsSink(parameters,
                                               self.INFLECTION_POINTS, context,
                                               fields, QgsWkbTypes.Point,
                                               layer.sourceCrs())

        fields = QgsFields()
        fields.append(QgsField('BENDID', QVariant.Int, len=10))
        fields.append(QgsField('AMPLI', QVariant.Double, len=10, prec=4))

        (stem_sink, stem_id) = self.parameterAsSink(parameters, self.STEMS,
                                                    context, fields,
                                                    QgsWkbTypes.LineString,
                                                    layer.sourceCrs())

        def write_axis_segment(fid, p0, p1, feature):

            new_feature = QgsFeature()
            new_feature.setGeometry(QgsGeometry.fromPolylineXY([p0, p1]))
            new_feature.setAttributes(feature.attributes() + [
                fid
                # clamp_angle(angle)
            ])
            axis_sink.addFeature(new_feature)

        def write_segment(fid, points, feature):

            bend = Bend(points)

            new_feature = QgsFeature()
            new_feature.setGeometry(QgsGeometry.fromPolylineXY(points))
            new_feature.setAttributes(feature.attributes() + [
                fid,
                bend.npoints(),
                bend.length(),
                bend.wavelength(),
                bend.sinuosity(),
                bend.amplitude(),
                bend.omega_origin(),
                bend.omega_end()
                # bend.curvature_radius()
            ])
            segment_sink.addFeature(new_feature)

            stem, stem_idx = bend.max_amplitude_stem()

            if stem is None:
                # ProcessingLog.addToLog(ProcessingLog.LOG_INFO, str(points))
                return

            stem_feature = QgsFeature()
            stem_feature.setGeometry(stem)
            stem_feature.setAttributes([fid, stem.length()])
            stem_sink.addFeature(stem_feature)

        def write_inflection_point(point_id, point, angle, interdistance):

            new_feature = QgsFeature()
            new_feature.setGeometry(QgsGeometry.fromPointXY(point))
            new_feature.setAttributes([point_id, angle, interdistance])
            inflection_sink.addFeature(new_feature)

        total = 100.0 / layer.featureCount() if layer.featureCount() else 0
        fid = 0
        point_id = 0
        # Total count of detected inflection points
        detected = 0
        # Total count of retained inflection points
        retained = 0

        for current, feature in enumerate(layer.getFeatures()):

            points = feature.geometry().asPolyline()
            points_iterator = iter(points)
            a = next(points_iterator)
            b = next(points_iterator)
            current_sign = 0
            current_segment = [a]
            current_axis_direction = None

            bends = list()
            inflection_points = list()

            def axis_angle(entry):

                if entry.previous is None or entry.next is None:
                    return 0.0

                a = inflection_points[entry.previous]
                b = inflection_points[entry.index]
                c = inflection_points[entry.next]
                ab = qgs_vector(a, b)
                bc = qgs_vector(b, c)

                return clamp_angle(math.degrees(ab.angle(bc)))

            def interdistance(entry):

                l1 = entry.previous and qgs_vector(
                    inflection_points[entry.previous],
                    inflection_points[entry.index]).length() or 0.0
                l2 = entry.next and qgs_vector(
                    inflection_points[entry.index],
                    inflection_points[entry.next]).length() or 0.0

                return l1 + l2

            # write_inflection_point(point_id, a)
            point_id = point_id + 1

            for c in points_iterator:

                sign = angle_sign(a, b, c)

                if current_sign * sign < 0:

                    p0 = current_segment[0]
                    pi = QgsPointXY(0.5 * (a.x() + b.x()),
                                    0.5 * (a.y() + b.y()))
                    current_segment.append(pi)

                    if current_axis_direction:
                        angle = current_axis_direction.angle(qgs_vector(
                            p0, pi)) * 180 / math.pi
                    else:
                        angle = 0.0

                    # write_axis_segment(fid, p0, pi, feature, angle)
                    # write_segment(fid, current_segment, feature)
                    # write_inflection_point(point_id, pi)

                    bend = Bend(current_segment)
                    bends.append(bend)
                    inflection_points.append(p0)

                    current_sign = sign
                    current_segment = [pi, b]
                    current_axis_direction = qgs_vector(p0, pi)
                    fid = fid + 1
                    point_id = point_id + 1

                else:

                    current_segment.append(b)

                if current_sign == 0:
                    current_sign = sign

                a, b = b, c

            p0 = current_segment[0]

            if current_axis_direction:
                angle = current_axis_direction.angle(qgs_vector(
                    p0, b)) * 180 / math.pi
            else:
                angle = 0.0

            # write_axis_segment(fid, p0, b, feature, angle)
            current_segment.append(b)

            # write_segment(fid, current_segment, feature)
            # write_inflection_point(point_id, b)
            bend = Bend(current_segment)
            bends.append(bend)
            inflection_points.append(p0)
            inflection_points.append(b)

            fid = fid + 1
            point_id = point_id + 1

            detected = detected + len(inflection_points)

            # ProcessingLog.addToLog(ProcessingLog.LOG_INFO, 'Inflections points = %d' % len(inflection_points))
            # ProcessingLog.addToLog(ProcessingLog.LOG_INFO, 'Bends = %d' % len(bends))

            # Filter out smaller bends

            entries = list()

            entry = QueueEntry(0)
            entry.previous = None
            entry.next = 1
            entry.priority = float('inf')
            entry.interdistance = float('inf')
            entries.append(entry)

            for k in range(1, len(inflection_points) - 1):

                entry = QueueEntry(k)
                entry.previous = k - 1
                entry.next = k + 1
                entry.priority = bends[k -
                                       1].amplitude() + bends[k].amplitude()
                entry.interdistance = qgs_vector(inflection_points[k-1], inflection_points[k]).length() + \
                                      qgs_vector(inflection_points[k], inflection_points[k+1]).length()
                entries.append(entry)

            k = len(inflection_points) - 1
            entry = QueueEntry(k)
            entry.previous = k - 1
            entry.next = None
            entry.priority = float('inf')
            entry.interdistance = float('inf')
            entries.append(entry)

            queue = list(entries)
            heapify(queue)

            while queue:

                entry = heappop(queue)
                k = entry.index

                # ProcessingLog.addToLog(ProcessingLog.LOG_INFO, 'Entry = %s' % entry)

                if entry.priority > 2 * resolution:
                    break

                if entry.duplicate or entry.removed:
                    continue

                if entry.interdistance > lmax:
                    continue

                previous_entry = entries[entry.previous]
                next_entry = entries[entry.next]

                if previous_entry.previous is None:
                    continue

                if next_entry.next is None:
                    continue

                new_entry = QueueEntry(k)

                entries[previous_entry.previous].next = k
                new_entry.previous = previous_entry.previous

                entries[next_entry.next].previous = k
                new_entry.next = next_entry.next

                before_bend = Bend.merge(bends[new_entry.previous],
                                         bends[entry.previous])
                after_bend = Bend.merge(bends[k], bends[entry.next])

                bends[new_entry.previous] = before_bend
                bends[k] = after_bend

                new_entry.priority = before_bend.amplitude(
                ) + after_bend.amplitude()
                new_entry.interdistance = qgs_vector(inflection_points[new_entry.previous], inflection_points[k]).length() + \
                                      qgs_vector(inflection_points[k], inflection_points[new_entry.next]).length()

                heappush(queue, new_entry)

                entries[k] = new_entry
                previous_entry.removed = True
                next_entry.removed = True
                entry.duplicate = True

            # Filter out large axis angles

            # if max_angle > 0.0:

            #     index = 0
            #     queue = list()

            #     while True:

            #         entry = entries[index]

            #         if entry.previous is None or entry.next is None:

            #             entry.priority = 0.0
            #             queue.append(entry)

            #             if entry.next is None:
            #                 break
            #             else:
            #                 index = entry.next
            #                 continue

            #         # -priority to sort entries from large to small angle
            #         entry.priority = -abs(axis_angle(entry))
            #         queue.append(entry)

            #         index = entry.next

            #     heapify(queue)

            #     while queue:

            #         entry = heappop(queue)
            #         angle = -entry.priority

            #         if angle < max_angle:
            #             break

            #         if entry.duplicate or entry.removed:
            #             continue

            #         if entry.previous is None or entry.next is None:
            #             continue

            #         if entry.interdistance > 2*lmax:
            #             continue

            #         previous_entry = entries[entry.previous]
            #         next_entry = entries[entry.next]

            #         merged_bend = Bend.merge(bends[entry.previous], bends[entry.index])
            #         bends[entry.previous] = merged_bend

            #         previous_entry.duplicate = True
            #         new_previous_entry = QueueEntry(previous_entry.index)
            #         new_previous_entry.previous = previous_entry.previous
            #         new_previous_entry.next = entry.next
            #         angle = axis_angle(new_previous_entry)
            #         dist = new_previous_entry.interdistance = interdistance(new_previous_entry)
            #         new_previous_entry.priority = -abs(angle)
            #         entries[new_previous_entry.index] = new_previous_entry
            #         heappush(queue, new_previous_entry)

            #         next_entry.duplicate = True
            #         new_next_entry = QueueEntry(next_entry.index)
            #         new_next_entry.previous = entry.previous
            #         new_next_entry.next = next_entry.next
            #         angle = axis_angle(new_next_entry)
            #         dist = new_next_entry.interdistance = interdistance(new_next_entry)
            #         new_next_entry.priority = -abs(angle)
            #         entries[new_next_entry.index] = new_next_entry
            #         heappush(queue, new_next_entry)

            #         entry.removed = True

            # Output results

            index = 0

            while True:

                entry = entries[index]
                point = inflection_points[index]

                if entry.next is None:

                    point_id = point_id + 1
                    angle = axis_angle(entry)
                    dist = interdistance(entry)
                    write_inflection_point(point_id, point, angle, dist)
                    retained = retained + 1
                    break

                bend = bends[index]
                point_id = point_id + 1
                fid = fid + 1

                # ProcessingLog.addToLog(ProcessingLog.LOG_INFO, 'Points = %s' % bend.points)
                angle = axis_angle(entry)
                dist = interdistance(entry)
                write_inflection_point(point_id, point, angle, dist)
                retained = retained + 1
                write_axis_segment(fid, bend.p_origin, bend.p_end, feature)
                write_segment(fid, bend.points, feature)

                index = entry.next

            feedback.setProgress(int(current * total))

        feedback.pushInfo('Retained inflection points = %d / %d' %
                          (retained, detected))

        return {
            self.OUTPUT: segment_id,
            self.FLOW_AXIS: axis_id,
            self.INFLECTION_POINTS: inflection_id,
            self.STEMS: stem_id
        }
Example #12
0
def make_features_compatible(new_features, input_layer):
    """Try to make the new features compatible with old features by:

    - converting single to multi part
    - dropping additional attributes
    - adding back M/Z values
    - drop Z/M
    - convert multi part to single part

    :param new_features: new features
    :type new_features: list of QgsFeatures
    :param input_layer: input layer
    :type input_layer: QgsVectorLayer
    :return: modified features
    :rtype: list of QgsFeatures
    """

    input_wkb_type = input_layer.wkbType()
    result_features = []
    for new_f in new_features:
        # Fix attributes
        if new_f.fields().count() > 0:
            attributes = []
            for field in input_layer.fields():
                if new_f.fields().indexFromName(field.name()) >= 0:
                    attributes.append(new_f[field.name()])
                else:
                    attributes.append(None)
            f = QgsFeature(input_layer.fields())
            f.setAttributes(attributes)
            f.setGeometry(new_f.geometry())
            new_f = f
        else:
            lendiff = len(new_f.attributes()) - len(input_layer.fields())
            if lendiff > 0:
                f = QgsFeature(input_layer.fields())
                f.setGeometry(new_f.geometry())
                f.setAttributes(new_f.attributes()[:len(input_layer.fields())])
                new_f = f
            elif lendiff < 0:
                f = QgsFeature(input_layer.fields())
                f.setGeometry(new_f.geometry())
                attributes = new_f.attributes() + [
                    None for i in range(-lendiff)
                ]
                f.setAttributes(attributes)
                new_f = f

        # Check if we need geometry manipulation
        new_f_geom_type = QgsWkbTypes.geometryType(new_f.geometry().wkbType())
        new_f_has_geom = new_f_geom_type not in (QgsWkbTypes.UnknownGeometry,
                                                 QgsWkbTypes.NullGeometry)
        input_layer_has_geom = input_wkb_type not in (QgsWkbTypes.NoGeometry,
                                                      QgsWkbTypes.Unknown)

        # Drop geometry if layer is geometry-less
        if not input_layer_has_geom and new_f_has_geom:
            f = QgsFeature(input_layer.fields())
            f.setAttributes(new_f.attributes())
            new_f = f
            result_features.append(new_f)
            continue  # skip the rest

        if input_layer_has_geom and new_f_has_geom and \
                new_f.geometry().wkbType() != input_wkb_type:  # Fix geometry
            # Single -> Multi
            if (QgsWkbTypes.isMultiType(input_wkb_type)
                    and not new_f.geometry().isMultipart()):
                new_geom = new_f.geometry()
                new_geom.convertToMultiType()
                new_f.setGeometry(new_geom)
            # Drop Z/M
            if (new_f.geometry().constGet().is3D()
                    and not QgsWkbTypes.hasZ(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().dropZValue()
                new_f.setGeometry(new_geom)
            if (new_f.geometry().constGet().isMeasure()
                    and not QgsWkbTypes.hasM(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().dropMValue()
                new_f.setGeometry(new_geom)
            # Add Z/M back (set it to 0)
            if (not new_f.geometry().constGet().is3D()
                    and QgsWkbTypes.hasZ(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().addZValue(0.0)
                new_f.setGeometry(new_geom)
            if (not new_f.geometry().constGet().isMeasure()
                    and QgsWkbTypes.hasM(input_wkb_type)):
                new_geom = new_f.geometry()
                new_geom.get().addMValue(0.0)
                new_f.setGeometry(new_geom)
            # Multi -> Single
            if (not QgsWkbTypes.isMultiType(input_wkb_type)
                    and new_f.geometry().isMultipart()):
                g = new_f.geometry()
                g2 = g.constGet()
                for i in range(g2.partCount()):
                    # Clone or crash!
                    g4 = QgsGeometry(g2.geometryN(i).clone())
                    f = QgsVectorLayerUtils.createFeature(
                        input_layer, g4, {
                            i: new_f.attribute(i)
                            for i in range(new_f.fields().count())
                        })
                    result_features.append(f)
            else:
                result_features.append(new_f)
        else:
            result_features.append(new_f)
    return result_features
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        source_fields_parameter = self.parameterAsFields(
            parameters, self.INPUT_FIELD, context)
        target = self.parameterAsVectorLayer(parameters, self.OUTPUT, context)
        target_fields_parameter = self.parameterAsFields(
            parameters, self.OUTPUT_FIELD, context)
        action_on_duplicate = self.parameterAsEnum(parameters,
                                                   self.ACTION_ON_DUPLICATE,
                                                   context)

        results = {
            self.OUTPUT: None,
            self.APPENDED_COUNT: None,
            self.UPDATED_COUNT: None,
            self.SKIPPED_COUNT: None
        }

        target_value_dict = dict()
        source_field_unique_values = ''
        target_field_unique_values = ''
        source_field_type = None
        target_field_type = None

        if source_fields_parameter:
            source_field_unique_values = source_fields_parameter[0]
            source_field_type = source.fields().field(
                source_field_unique_values).type()

        if target_fields_parameter:
            target_field_unique_values = target_fields_parameter[0]
            target_field_type = target.fields().field(
                target_field_unique_values).type()

        if source_field_type != target_field_type:
            feedback.pushInfo(
                "\nWARNING: Source and target fields to compare have different field types."
            )

        if source_field_unique_values and target_field_unique_values and action_on_duplicate == self.NO_ACTION:
            feedback.reportError(
                "\nWARNING: Since you have chosen source and target fields to compare, you need to choose a valid action to apply on duplicate features before running this algorithm."
            )
            return results

        if action_on_duplicate != self.NO_ACTION and not (
                source_field_unique_values and target_field_unique_values):
            feedback.reportError(
                "\nWARNING: Since you have chosen an action on duplicate features, you need to choose both source and target fields for comparing values before running this algorithm."
            )
            return results

        caps = target.dataProvider().capabilities()
        if not (caps & QgsVectorDataProvider.AddFeatures):
            feedback.reportError(
                "\nWARNING: The target layer does not support appending features to it! Choose another target layer."
            )
            return results

        if action_on_duplicate == self.UPDATE_EXISTING_FEATURE and not (
                caps & QgsVectorDataProvider.ChangeAttributeValues
                and caps & QgsVectorDataProvider.ChangeGeometries):
            feedback.reportError(
                "\nWARNING: The target layer does not support updating its features! Choose another action for duplicate features or choose another target layer."
            )
            return results

        editable_before = False
        if target.isEditable():
            editable_before = True
            feedback.reportError(
                "\nWARNING: You need to close the edit session on layer '{}' before running this algorithm."
                .format(target.name()))
            return results

        # Define a mapping between source and target layer
        mapping = dict()
        for target_idx in target.fields().allAttributesList():
            target_field = target.fields().field(target_idx)
            source_idx = source.fields().indexOf(target_field.name())
            if source_idx != -1:
                mapping[target_idx] = source_idx

        # Build dict of target field values so that we can search easily later {value1: [id1, id2], ...}
        if target_field_unique_values:
            for f in target.getFeatures():
                if f[target_field_unique_values] in target_value_dict:
                    target_value_dict[f[target_field_unique_values]].append(
                        int(f.id()))
                else:
                    target_value_dict[f[target_field_unique_values]] = [
                        int(f.id())
                    ]

        # Prepare features for the Copy and Paste
        results[self.APPENDED_COUNT] = 0
        total = 100.0 / source.featureCount() if source.featureCount() else 0
        features = source.getFeatures()
        destType = target.geometryType()
        destIsMulti = QgsWkbTypes.isMultiType(target.wkbType())
        new_features = list()
        updated_features = dict()
        updated_geometries = dict()
        updated_features_count = 0
        updated_geometries_count = 0
        skipped_features_count = 0  # To properly count features that were skipped
        duplicate_features_set = set(
        )  # To properly count features that were updated
        for current, in_feature in enumerate(features):
            if feedback.isCanceled():
                break

            target_feature_exists = False
            duplicate_target_value = None

            # If skip is the action, skip as soon as possible
            if source_field_unique_values:
                duplicate_target, duplicate_target_value = self.find_duplicate_value(
                    in_feature[source_field_unique_values], source_field_type,
                    target_value_dict, target_field_type)
                if duplicate_target:
                    if action_on_duplicate == self.SKIP_FEATURE:
                        request = QgsFeatureRequest(
                            target_value_dict[duplicate_target_value]
                        )  # Get target feature ids
                        request.setFlags(QgsFeatureRequest.NoGeometry)
                        request.setSubsetOfAttributes(
                            [])  # Note: this adds a new flag
                        skipped_features_count += len(
                            [f for f in target.getFeatures(request)])
                        continue

                    target_feature_exists = True

            attrs = {
                target_idx: in_feature[source_idx]
                for target_idx, source_idx in mapping.items()
            }

            geom = QgsGeometry()

            if in_feature.hasGeometry() and target.isSpatial():
                # Convert geometry to match destination layer
                # Adapted from QGIS qgisapp.cpp, pasteFromClipboard()
                geom = in_feature.geometry()

                if not geom.isNull():
                    if destType != QgsWkbTypes.UnknownGeometry:
                        newGeometry = geom.convertToType(destType, destIsMulti)

                        if newGeometry.isNull():
                            continue  # Couldn't convert
                        geom = newGeometry

                    # Avoid intersection if enabled in digitize settings
                    geom.avoidIntersections(
                        QgsProject.instance().avoidIntersectionsLayers())

            if target_feature_exists and action_on_duplicate == self.UPDATE_EXISTING_FEATURE:
                for t_f in target.getFeatures(
                        target_value_dict[duplicate_target_value]):
                    duplicate_features_set.add(t_f.id())
                    updated_features[t_f.id()] = attrs
                    if target.isSpatial():
                        updated_geometries[t_f.id()] = geom
            else:  # Append
                new_feature = QgsVectorLayerUtils().createFeature(
                    target, geom, attrs)
                new_features.append(new_feature)

            feedback.setProgress(int(current * total))

        # Do the Copy and Paste
        res_add_features = False
        try:
            with edit(target):
                target.beginEditCommand("Appending/Updating features...")

                if updated_features:
                    for k, v in updated_features.items():
                        if target.changeAttributeValues(k, v):
                            updated_features_count += 1
                        else:
                            feedback.reportError(
                                "\nERROR: Target feature (id={}) couldn't be updated to the following attributes: {}."
                                .format(k, v))

                if updated_geometries:
                    for k, v in updated_geometries.items():
                        if target.changeGeometry(k, v):
                            updated_geometries_count += 1
                        else:
                            feedback.reportError(
                                "\nERROR: Target feature's geometry (id={}) couldn't be updated."
                                .format(k))

                if new_features:
                    res_add_features = target.addFeatures(new_features)

                target.endEditCommand()
        except QgsEditError as e:
            if not editable_before:
                # Let's close the edit session to prepare for a next run
                target.rollBack()

            feedback.reportError(
                "\nERROR: No features could be appended/updated to/in '{}', because of the following error:\n{}\n"
                .format(target.name(), repr(e)))
            return results

        if action_on_duplicate == self.SKIP_FEATURE:
            feedback.pushInfo(
                "\nSKIPPED FEATURES: {} duplicate features were skipped while copying features to '{}'!"
                .format(skipped_features_count, target.name()))
            results[self.SKIPPED_COUNT] = skipped_features_count

        if action_on_duplicate == self.UPDATE_EXISTING_FEATURE:
            feedback.pushInfo(
                "\nUPDATED FEATURES: {} out of {} duplicate features were updated while copying features to '{}'!"
                .format(updated_features_count, len(duplicate_features_set),
                        target.name()))
            results[self.UPDATED_COUNT] = updated_features_count

        if not new_features:
            feedback.pushInfo(
                "\nFINISHED WITHOUT APPENDED FEATURES: There were no features to append to '{}'."
                .format(target.name()))
        else:
            if res_add_features:
                feedback.pushInfo(
                    "\nAPPENDED FEATURES: {} out of {} features from input layer were successfully appended to '{}'!"
                    .format(len(new_features), source.featureCount(),
                            target.name()))
                results[self.APPENDED_COUNT] = len(new_features)
            else:  # TODO do we really need this else message below?
                feedback.reportError(
                    "\nERROR: The {} features from input layer could not be appended to '{}'. Sometimes this might be due to NOT NULL constraints that are not met."
                    .format(source.featureCount(), target.name()))

        results[self.OUTPUT] = target
        return results
Example #14
0
    def add_topological_vertices(self, layer1, layer2, id_field=ID_FIELD):
        """
        Modify layer1 adding vertices that are in layer2 and not in layer1

        Ideally, we could pass the whole layer2 as parameter for
        addTopologicalPoints or, at least, pass one multi-geometry containing
        all geometries from layer2. However, onthe one side, the
        addTopologicalPoints function doesn't support a layer as parameter and,
        on the other side, there is a bug in the function that doesn't let it
        work with multi-geometries. That's why we need to traverse the whole
        layer2 in search for its individual geometries. We do use a SpatialIndex
        nonetheless, to improve efficiency.
        """
        if QgsWkbTypes.isMultiType(layer2.wkbType()):
            layer2 = processing.run("native:multiparttosingleparts", {
                'INPUT': layer2,
                'OUTPUT': 'memory:'
            })['OUTPUT']

        if layer2.geometryType() == QgsWkbTypes.PolygonGeometry:
            layer2 = processing.run("ladm_col:polygonstolines", {
                'INPUT': layer2,
                'OUTPUT': 'memory:'
            })['OUTPUT']

        geom_added = list()
        index = QgsSpatialIndex(layer2)
        dict_features_layer2 = None
        candidate_features = None
        id_field_idx1 = layer1.fields().indexFromName(id_field)
        request1 = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx1])
        id_field_idx2 = layer2.fields().indexFromName(id_field)
        request2 = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx2])

        with edit(layer1):
            edit_layer = QgsVectorLayerEditUtils(layer1)
            dict_features_layer2 = {
                feature.id(): feature
                for feature in layer2.getFeatures(request2)
            }

            for feature in layer1.getFeatures(request1):
                bbox = feature.geometry().boundingBox()
                candidate_ids = index.intersects(bbox)
                candidate_features = [
                    dict_features_layer2[candidate_id]
                    for candidate_id in candidate_ids
                ]
                intersect_features = list()

                for candidate_feature in candidate_features:
                    if candidate_feature.geometry().intersects(
                            feature.geometry()):
                        intersect_features.append(candidate_feature)

                for intersect_feature in intersect_features:
                    if intersect_feature.id() not in geom_added:
                        edit_layer.addTopologicalPoints(
                            intersect_feature.geometry())
                        geom_added.append(intersect_feature.id())

        # free up memory
        del candidate_features
        del dict_features_layer2
        gc.collect()
Example #15
0
    def processAlgorithm(self, progress):
        layerA = dataobjects.getObjectFromUri(
            self.getParameterValue(self.INPUT_A))
        splitLayer = dataobjects.getObjectFromUri(
            self.getParameterValue(self.INPUT_B))

        sameLayer = self.getParameterValue(
            self.INPUT_A) == self.getParameterValue(self.INPUT_B)
        fieldList = layerA.fields()

        writer = self.getOutputFromName(self.OUTPUT).getVectorWriter(
            fieldList, layerA.wkbType(), layerA.crs())

        spatialIndex = QgsSpatialIndex()
        splitGeoms = {}
        request = QgsFeatureRequest()
        request.setSubsetOfAttributes([])

        for aSplitFeature in vector.features(splitLayer, request):
            splitGeoms[aSplitFeature.id()] = aSplitFeature.geometry()
            spatialIndex.insertFeature(aSplitFeature)
            # honor the case that user has selection on split layer and has setting "use selection"

        outFeat = QgsFeature()
        features = vector.features(layerA)

        if len(features) == 0:
            total = 100
        else:
            total = 100.0 / float(len(features))

        multiGeoms = 0  # how many multi geometries were encountered

        for current, inFeatA in enumerate(features):
            inGeom = inFeatA.geometry()

            if inGeom.isMultipart():
                multiGeoms += 1
                # MultiGeometries are not allowed because the result of a splitted part cannot be clearly defined:
                # 1) add both new parts as new features
                # 2) store one part as a new feature and the other one as part of the multi geometry
                # 2a) which part should be which, seems arbitrary
            else:
                attrsA = inFeatA.attributes()
                outFeat.setAttributes(attrsA)
                inGeoms = [inGeom]
                lines = spatialIndex.intersects(inGeom.boundingBox())

                if len(lines) > 0:  # has intersection of bounding boxes
                    splittingLines = []

                    engine = QgsGeometry.createGeometryEngine(
                        inGeom.geometry())
                    engine.prepareGeometry()

                    for i in lines:
                        try:
                            splitGeom = splitGeoms[i]
                        except:
                            continue

                        # check if trying to self-intersect
                        if sameLayer:
                            if inFeatA.id() == i:
                                continue

                        if engine.intersects(splitGeom.geometry()):
                            splittingLines.append(splitGeom)

                    if len(splittingLines) > 0:
                        for splitGeom in splittingLines:
                            splitterPList = None
                            outGeoms = []

                            split_geom_engine = QgsGeometry.createGeometryEngine(
                                splitGeom.geometry())
                            split_geom_engine.prepareGeometry()

                            while len(inGeoms) > 0:
                                inGeom = inGeoms.pop()

                                if split_geom_engine.intersects(
                                        inGeom.geometry()):
                                    inPoints = vector.extractPoints(inGeom)
                                    if splitterPList == None:
                                        splitterPList = vector.extractPoints(
                                            splitGeom)

                                    try:
                                        result, newGeometries, topoTestPoints = inGeom.splitGeometry(
                                            splitterPList, False)
                                    except:
                                        ProcessingLog.addToLog(
                                            ProcessingLog.LOG_WARNING,
                                            self.
                                            tr('Geometry exception while splitting'
                                               ))
                                        result = 1

                                    # splitGeometry: If there are several intersections
                                    # between geometry and splitLine, only the first one is considered.
                                    if result == 0:  # split occurred
                                        if inPoints == vector.extractPoints(
                                                inGeom):
                                            # bug in splitGeometry: sometimes it returns 0 but
                                            # the geometry is unchanged
                                            QgsMessageLog.logMessage(
                                                "appending")
                                            outGeoms.append(inGeom)
                                        else:
                                            inGeoms.append(inGeom)

                                            for aNewGeom in newGeometries:
                                                inGeoms.append(aNewGeom)
                                    else:
                                        QgsMessageLog.logMessage(
                                            "appending else")
                                        outGeoms.append(inGeom)
                                else:
                                    outGeoms.append(inGeom)

                            inGeoms = outGeoms

                for aGeom in inGeoms:
                    passed = True

                    if QgsWkbTypes.geometryType( aGeom.wkbType() )  == QgsWkbTypes.LineGeometry \
                            and not QgsWkbTypes.isMultiType(aGeom.wkbType()):
                        passed = len(aGeom.asPolyline()) > 2

                        if not passed:
                            passed = (len(aGeom.asPolyline()) == 2
                                      and aGeom.asPolyline()[0] !=
                                      aGeom.asPolyline()[1])
                            # sometimes splitting results in lines of zero length

                    if passed:
                        outFeat.setGeometry(aGeom)
                        writer.addFeature(outFeat)

            progress.setPercentage(int(current * total))

        if multiGeoms > 0:
            ProcessingLog.addToLog(
                ProcessingLog.LOG_INFO,
                self.
                tr('Feature geometry error: %s input features ignored due to multi-geometry.'
                   ) % str(multiGeoms))

        del writer
Example #16
0
    def testWriteShapefileWithSingleConversion(self):
        """Check writing geometries from a POLYGON ESRI shapefile does not
        convert to multi when "forceSinglePartGeometryType" options is TRUE
        also checks failing cases.

        OGR provider always report MULTI for POLYGON and LINESTRING, but if we set
        the import option "forceSinglePartGeometryType" the writer must respect the
        actual single-part type if the features in the data provider are actually single
        and not multi.
        """

        ml = QgsVectorLayer(('Polygon?crs=epsg:4326&field=id:int'), 'test',
                            'memory')

        provider = ml.dataProvider()
        ft = QgsFeature()
        ft.setGeometry(
            QgsGeometry.fromWkt('Polygon ((0 0, 0 1, 1 1, 1 0, 0 0))'))
        ft.setAttributes([1])
        res, features = provider.addFeatures([ft])

        dest_file_name = os.path.join(self.basetestpath, 'multipart.shp')
        write_result, error_message = QgsVectorLayerExporter.exportLayer(
            ml, dest_file_name, 'ogr', ml.crs(), False,
            {"driverName": "ESRI Shapefile"})
        self.assertEqual(write_result, QgsVectorLayerExporter.NoError,
                         error_message)

        # Open the newly created layer
        shapefile_layer = QgsVectorLayer(dest_file_name)

        dest_singlepart_file_name = os.path.join(self.basetestpath,
                                                 'singlepart.gpkg')
        write_result, error_message = QgsVectorLayerExporter.exportLayer(
            shapefile_layer, dest_singlepart_file_name, 'ogr',
            shapefile_layer.crs(), False, {
                "forceSinglePartGeometryType": True,
                "driverName": "GPKG",
            })
        self.assertEqual(write_result, QgsVectorLayerExporter.NoError,
                         error_message)

        # Load result layer and check that it's NOT MULTI
        single_layer = QgsVectorLayer(dest_singlepart_file_name)
        self.assertTrue(single_layer.isValid())
        self.assertTrue(QgsWkbTypes.isSingleType(single_layer.wkbType()))

        # Now save the shapfile layer into a gpkg with no force options
        dest_multipart_file_name = os.path.join(self.basetestpath,
                                                'multipart.gpkg')
        write_result, error_message = QgsVectorLayerExporter.exportLayer(
            shapefile_layer, dest_multipart_file_name, 'ogr',
            shapefile_layer.crs(), False, {
                "forceSinglePartGeometryType": False,
                "driverName": "GPKG",
            })
        self.assertEqual(write_result, QgsVectorLayerExporter.NoError,
                         error_message)
        # Load result layer and check that it's MULTI
        multi_layer = QgsVectorLayer(dest_multipart_file_name)
        self.assertTrue(multi_layer.isValid())
        self.assertTrue(QgsWkbTypes.isMultiType(multi_layer.wkbType()))

        # Failing case: add a real multi to the shapefile and try to force to single
        self.assertTrue(shapefile_layer.startEditing())
        ft = QgsFeature()
        ft.setGeometry(
            QgsGeometry.fromWkt(
                'MultiPolygon (((0 0, 0 1, 1 1, 1 0, 0 0)), ((-10 -10,-10 -9,-9 -9,-10 -10)))'
            ))
        ft.setAttributes([2])
        self.assertTrue(shapefile_layer.addFeatures([ft]))
        self.assertTrue(shapefile_layer.commitChanges())

        dest_multipart_failure_file_name = os.path.join(
            self.basetestpath, 'multipart_failure.gpkg')
        write_result, error_message = QgsVectorLayerExporter.exportLayer(
            shapefile_layer, dest_multipart_failure_file_name, 'ogr',
            shapefile_layer.crs(), False, {
                "forceSinglePartGeometryType": True,
                "driverName": "GPKG",
            })
        self.assertTrue(QgsWkbTypes.isMultiType(multi_layer.wkbType()))
        self.assertEqual(
            write_result, QgsVectorLayerExporter.ErrFeatureWriteFailed,
            "Failed to transform a feature with ID '1' to single part. Writing stopped."
        )