Ejemplo n.º 1
0
    def processAlgorithm(self, parameters, context, feedback):
        segments_layer = self.parameterAsLayer(parameters, self.IN_SEGMENTS,
                                               context)

        t = self.parameterAsDouble(parameters, self.THRESHOLD, context)

        index = QgsSpatialIndex()  # Spatial index

        index.addFeatures(segments_layer.getFeatures())

        todel = []
        for ln in segments_layer.getFeatures():
            index.deleteFeature(ln)

            cands = index.intersects(ln.geometry().boundingBox())

            ln1 = ln.geometry()
            for ca in cands:
                totest = segments_layer.getFeature(ca)
                ln2 = totest.geometry()

                are_equal = self.equal_segments(ln1, ln2, t)
                if are_equal:
                    index.deleteFeature(totest)
                    todel.append(ca)

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            segments_layer.fields(
            ),  # QgsFields() for an empty fields list or source_lines.fields()
            QgsWkbTypes.MultiLineString,
            segments_layer.sourceCrs())

        if sink is None:
            raise QgsProcessingException(
                self.invalidSinkError(parameters, self.OUTPUT))

        for feature in segments_layer.getFeatures():
            if feature.id() not in todel:
                sink.addFeature(feature, QgsFeatureSink.FastInsert)

        return {"OUTPUT": dest_id}
Ejemplo n.º 2
0
class EdgeCluster():
    def __init__(self, edges, initial_step_size, iterations, cycles,
                 compatibility):
        self.S = initial_step_size  # Weighting factor (needs to be cached, because will be decreased in every cycle)
        self.I = iterations  # Number of iterations per cycle (needs to be cached, because will be decreased in every cycle)
        self.edges = edges  # Edges to bundle in this cluster
        self.edge_lengths = []  # Array to cache edge lenghts
        self.E = len(edges)  # Number of edges
        self.EP = 2  # Current number of edge points
        self.SP = 0  # Current number of subdivision points
        self.compatibility = compatibility
        self.cycles = cycles
        self.compatibility_matrix = np.zeros(
            shape=(self.E, self.E))  # Compatibility matrix
        self.direction_matrix = np.zeros(
            shape=(self.E, self.E))  # Encodes direction of edge pairs
        self.N = (2**cycles) + 1  # Maximum number of points per edge
        self.epm_x = np.zeros(shape=(self.E,
                                     self.N))  # Bundles edges (x-values)
        self.epm_y = np.zeros(shape=(self.E,
                                     self.N))  # Bundles edges (y-values)

        self.segments = {}  # dict of Edges containing {id_seg: seg}
        self.allSegments = {}  # {id_seg: [seg as Edge, MainEdge id]}

    def get_size(self):
        return len(self.edges)

    def get_segments(self, edge):
        return self.segments[edge.id()]

    def create_segments(self, weight_index, feedback):
        self.index = QgsSpatialIndex()
        seg_id = 0
        for i, edge in enumerate(self.edges):
            if feedback.isCanceled(): return
            feedback.setProgress(100.0 * (i + 1) / len(self.edges))
            geom = edge.geometry()
            attrs = edge.attributes()
            polyline = geom.asPolyline()
            segments = {}
            for j in range(0, len(polyline) - 1):
                feat = QgsFeature()
                feat.setId(seg_id)
                seg_id += 1
                g = QgsGeometry.fromPolylineXY([polyline[j], polyline[j + 1]])
                feat.setGeometry(g)
                feat.setAttributes(attrs + [j])  #Add the rank of the segment
                segEdge = Edge(feat, weight_index)
                self.allSegments[segEdge.id()] = [segEdge, edge.id()]
                self.index.addFeature(segEdge)
                segments[segEdge.id()] = segEdge
            self.segments[edge.id()] = segments

    def collapse_lines(self, max_distance, feedback):
        for i, edge1 in enumerate(self.edges):
            feedback.setProgress(100.0 * (i + 1) / len(self.edges))
            if feedback.isCanceled(): return
            segments1 = self.get_segments(edge1)
            for key1, segment1 in segments1.items():
                geom1 = segment1.geometry()
                # get other edges in the vicinty
                tolerance = min(max_distance, geom1.length() / 2)
                ids = self.index.intersects(
                    geom1.buffer(tolerance, 4).boundingBox())
                # feedback.setProgressText("{0} matching segments with tolerance of {1}".format(len(ids), tolerance))
                keys2pop = []
                for id in ids:
                    edge2_id = self.allSegments[id][1]
                    if edge2_id == edge1.id(): continue
                    segment2 = self.allSegments[id][0]
                    geom2 = segment2.geometry()
                    d0 = geom1.vertexAt(0).distance(geom2.vertexAt(0))
                    d1 = geom1.vertexAt(1).distance(geom2.vertexAt(1))
                    if d0 <= (tolerance / 2) and d1 <= (tolerance / 2):
                        segment1.increase_weight(segment2.get_weight())
                        keys2pop.append(id)
                    d0 = geom1.vertexAt(0).distance(geom2.vertexAt(1))
                    d1 = geom1.vertexAt(1).distance(geom2.vertexAt(0))
                    if d0 <= (tolerance / 2) and d1 <= (tolerance / 2):
                        segment1.increase_weight(segment2.get_weight())
                        keys2pop.append(id)
                for id in keys2pop:
                    self.index.deleteFeature(self.allSegments[id][0])
                    self.segments[self.allSegments[id][1]].pop(id)
                    self.allSegments.pop(id)

    def compute_compatibilty_matrix(self, feedback):
        """
        Compatibility is stored in a matrix (rows = edges, columns = edges).
        Every coordinate in the matrix tells whether the two edges (r,c)/(c,r)
        are compatible, or not. The diagonal is always zero, and the other fields
        are filled with either -1 (not compatible) or 1 (compatible).
        The matrix is symmetric.
        """
        feedback.setProgressText("Compute compatibility matrix")
        edges_as_geom = []
        edges_as_vect = []
        for e_idx, edge in enumerate(self.edges):
            if feedback.isCanceled(): return
            geom = edge.geometry()
            edges_as_geom.append(geom)
            edges_as_vect.append(
                QgsVector(
                    geom.vertexAt(1).x() - geom.vertexAt(0).x(),
                    geom.vertexAt(1).y() - geom.vertexAt(0).y()))
            self.edge_lengths.append(edges_as_vect[e_idx].length())

        progress = 0
        for i in range(self.E - 1):
            if feedback.isCanceled(): return
            feedback.setProgress(100.0 * (i + 1) / (self.E - 1))
            for j in range(i + 1, self.E):
                if feedback.isCanceled(): return
                # Parameters
                lavg = (self.edge_lengths[i] + self.edge_lengths[j]) / 2.0
                dot = edges_as_vect[i].normalized(
                ) * edges_as_vect[j].normalized()

                # Angle compatibility
                angle_comp = abs(dot)

                # Scale compatibility
                scale_comp = 2.0 / (
                    lavg / min(self.edge_lengths[i], self.edge_lengths[j]) +
                    max(self.edge_lengths[i], self.edge_lengths[j]) / lavg)

                # Position compatibility
                i0 = edges_as_geom[i].vertexAt(0)
                i1 = edges_as_geom[i].vertexAt(1)
                j0 = edges_as_geom[j].vertexAt(0)
                j1 = edges_as_geom[j].vertexAt(1)
                e1_mid = QgsPoint((i0.x() + i1.x()) / 2.0,
                                  (i0.y() + i1.y()) / 2.0)
                e2_mid = QgsPoint((j0.x() + j1.x()) / 2.0,
                                  (j0.y() + j1.y()) / 2.0)
                diff = QgsVector(e2_mid.x() - e1_mid.x(),
                                 e2_mid.y() - e1_mid.y())
                pos_comp = lavg / (lavg + diff.length())

                # Visibility compatibility
                mid_E1 = edges_as_geom[i].centroid()
                mid_E2 = edges_as_geom[j].centroid()
                #dist = mid_E1.distance(mid_E2)
                I0 = MiscUtils.project_point_on_line(j0, edges_as_geom[i])
                I1 = MiscUtils.project_point_on_line(j1, edges_as_geom[i])
                mid_I = QgsGeometry.fromPolyline([I0, I1]).centroid()
                dist_I = I0.distance(I1)
                if dist_I == 0.0:
                    visibility1 = 0.0
                else:
                    visibility1 = max(
                        0, 1 - ((2 * mid_E1.distance(mid_I)) / dist_I))
                J0 = MiscUtils.project_point_on_line(i0, edges_as_geom[j])
                J1 = MiscUtils.project_point_on_line(i1, edges_as_geom[j])
                mid_J = QgsGeometry.fromPolyline([J0, J1]).centroid()
                dist_J = J0.distance(J1)
                if dist_J == 0.0:
                    visibility2 = 0.0
                else:
                    visibility2 = max(
                        0, 1 - ((2 * mid_E2.distance(mid_J)) / dist_J))
                visibility_comp = min(visibility1, visibility2)

                # Compatibility score
                comp_score = angle_comp * scale_comp * pos_comp * visibility_comp

                # Fill values into the matrix (1 = yes, -1 = no) and use matrix symmetry (i/j = j/i)
                if comp_score >= self.compatibility:
                    self.compatibility_matrix[i, j] = 1
                    self.compatibility_matrix[j, i] = 1
                else:
                    self.compatibility_matrix[i, j] = -1
                    self.compatibility_matrix[j, i] = -1

                # Store direction
                distStart1 = j0.distance(i0)
                distStart2 = j1.distance(i0)
                if distStart1 > distStart2:
                    self.direction_matrix[i, j] = -1
                    self.direction_matrix[j, i] = -1
                else:
                    self.direction_matrix[i, j] = 1
                    self.direction_matrix[j, i] = 1

    def force_directed_eb(self, feedback):
        """ Force-directed edge bundling """
        if feedback.isCanceled(): return
        # Create compatibility matrix
        self.compute_compatibilty_matrix(feedback)
        feedback.setCurrentStep(2)
        if feedback.isCanceled(): return

        for e_idx, edge in enumerate(self.edges):
            vertices = edge.geometry().asPolyline()
            self.epm_x[e_idx, 0] = vertices[0].x()
            self.epm_y[e_idx, 0] = vertices[0].y()
            self.epm_x[e_idx, self.N - 1] = vertices[1].x()
            self.epm_y[e_idx, self.N - 1] = vertices[1].y()

        # For each cycle
        feedback.setProgressText('Compute force-directed layout')
        for c in range(self.cycles):
            if feedback.isCanceled(): return
            # New number of subdivision points
            current_num = self.EP
            currentindeces = []
            for i in range(current_num):
                idx = int(
                    (float(i) / float(current_num - 1)) * float(self.N - 1))
                currentindeces.append(idx)
            self.SP += 2**c
            self.EP = self.SP + 2
            edgeindeces = []
            newindeces = []
            for i in range(self.EP):
                idx = int((float(i) / float(self.EP - 1)) * float(self.N - 1))
                edgeindeces.append(idx)
                if idx not in currentindeces:
                    newindeces.append(idx)
            pointindeces = edgeindeces[1:self.EP - 1]

            # Calculate position of new points
            for idx in newindeces:
                if feedback.isCanceled(): return
                i = int((float(idx) / float(self.N - 1)) * float(self.EP - 1))
                left = i - 1
                leftidx = int(
                    (float(left) / float(self.EP - 1)) * float(self.N - 1))
                right = i + 1
                rightidx = int(
                    (float(right) / float(self.EP - 1)) * float(self.N - 1))
                self.epm_x[:, idx] = (self.epm_x[:, leftidx] +
                                      self.epm_x[:, rightidx]) / 2.0
                self.epm_y[:, idx] = (self.epm_y[:, leftidx] +
                                      self.epm_y[:, rightidx]) / 2.0

            # Needed for spring forces
            KP0 = np.zeros(shape=(self.E, 1))
            KP0[:, 0] = np.asarray(self.edge_lengths)
            KP = K / (KP0 * (self.EP - 1))

            # For all iterations (number decreased in every cycle)
            for iteration in range(self.I):
                if feedback.isCanceled(): return
                if (iteration + 1) % 5 == 0:
                    feedback.pushCommandInfo(
                        "Cycle {0} and Iteration {1}".format(
                            c + 1, iteration + 1))
                    feedback.setProgress(100.0 * (iteration + 1) / self.I)
                # Spring forces
                middlepoints_x = self.epm_x[:, pointindeces]
                middlepoints_y = self.epm_y[:, pointindeces]
                neighbours_left_x = self.epm_x[:, edgeindeces[0:self.EP - 2]]
                neighbours_left_y = self.epm_y[:, edgeindeces[0:self.EP - 2]]
                neighbours_right_x = self.epm_x[:, edgeindeces[2:self.EP]]
                neighbours_right_y = self.epm_y[:, edgeindeces[2:self.EP]]
                springforces_x = (neighbours_left_x - middlepoints_x +
                                  neighbours_right_x - middlepoints_x) * KP
                springforces_y = (neighbours_left_y - middlepoints_y +
                                  neighbours_right_y - middlepoints_y) * KP

                # Electrostatic forces
                electrostaticforces_x = np.zeros(shape=(self.E, self.SP))
                electrostaticforces_y = np.zeros(shape=(self.E, self.SP))
                # Loop through all edges
                for e_idx, edge in enumerate(self.edges):
                    if feedback.isCanceled(): return
                    # Loop through compatible edges
                    comp_list = np.where(
                        self.compatibility_matrix[:, e_idx] > 0)
                    for other_idx in np.nditer(comp_list, ['zerosize_ok']):
                        if feedback.isCanceled(): return
                        otherindeces = pointindeces[:]
                        if self.direction_matrix[e_idx, other_idx] < 0:
                            otherindeces.reverse()
                        # Distance between points
                        subtr_x = self.epm_x[
                            other_idx, otherindeces] - self.epm_x[e_idx,
                                                                  pointindeces]
                        subtr_y = self.epm_y[
                            other_idx, otherindeces] - self.epm_y[e_idx,
                                                                  pointindeces]
                        distance = np.sqrt(
                            np.add(np.multiply(subtr_x, subtr_x),
                                   np.multiply(subtr_y, subtr_y)))
                        flocal_x = map(forcecalcx, subtr_x, subtr_y, distance)
                        flocal_y = map(forcecalcy, subtr_x, subtr_y, distance)
                        # Sum of forces
                        electrostaticforces_x[e_idx, :] += np.array(
                            list(flocal_x))
                        electrostaticforces_y[e_idx, :] += np.array(
                            list(flocal_y))

                # Compute total forces
                force_x = (springforces_x + electrostaticforces_x) * self.S
                force_y = (springforces_y + electrostaticforces_y) * self.S

                # Compute new point positions
                self.epm_x[:, pointindeces] += force_x
                self.epm_y[:, pointindeces] += force_y

            # Adjustments for next cycle
            self.S = self.S * sdc  # Decrease weighting factor
            self.I = int(round(self.I * idc))  # Decrease iterations

        feedback.setProgressText("Ongoing final calculations")
        for e_idx in range(self.E):
            if feedback.isCanceled(): return
            # Create a new polyline out of the line array
            line = map(lambda p, q: QgsPoint(p, q), self.epm_x[e_idx],
                       self.epm_y[e_idx])
            self.edges[e_idx].setGeometry(QgsGeometry.fromPolyline(line))
Ejemplo n.º 3
0
    def processAlgorithm(
            self,  # pylint: disable=missing-function-docstring,too-many-statements,too-many-branches,too-many-locals
            parameters,
            context,
            feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)

        if source is None:
            raise QgsProcessingException(
                self.invalidSourceError(parameters, self.INPUT))

        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT,
                                               context, source.fields(),
                                               QgsWkbTypes.LineString,
                                               source.sourceCrs())
        if sink is None:
            raise QgsProcessingException(
                self.invalidSinkError(parameters, self.OUTPUT))

        roundabout_expression_string = self.parameterAsExpression(
            parameters, self.EXPRESSION, context)

        # step 1 - find all roundabouts
        exp = QgsExpression(roundabout_expression_string)
        expression_context = self.createExpressionContext(
            parameters, context, source)
        exp.prepare(expression_context)

        roundabouts = []
        not_roundabouts = {}
        not_roundabout_index = QgsSpatialIndex()

        total = 10.0 / source.featureCount() if source.featureCount() else 0
        features = source.getFeatures()

        _id = 1
        for current, feature in enumerate(features):
            if feedback.isCanceled():
                break

            def add_feature(f, _id, geom, is_roundabout):
                output_feature = QgsFeature(f)
                output_feature.setGeometry(geom)
                output_feature.setId(_id)
                if is_roundabout:
                    roundabouts.append(output_feature)
                else:
                    not_roundabouts[output_feature.id()] = output_feature
                    not_roundabout_index.addFeature(output_feature)

            expression_context.setFeature(feature)

            is_roundabout = exp.evaluate(expression_context)
            if not feature.geometry().wkbType() == QgsWkbTypes.LineString:
                geom = feature.geometry()
                for p in geom.parts():
                    add_feature(feature, _id, QgsGeometry(p.clone()),
                                is_roundabout)
                    _id += 1
            else:
                add_feature(feature, _id, feature.geometry(), is_roundabout)
                _id += 1

            # Update the progress bar
            feedback.setProgress(int(current * total))

        feedback.pushInfo(
            self.tr('Found {} roundabout parts'.format(len(roundabouts))))
        feedback.pushInfo(
            self.tr('Found {} not roundabouts'.format(len(not_roundabouts))))

        if feedback.isCanceled():
            return {self.OUTPUT: dest_id}

        all_roundabouts = QgsGeometry.unaryUnion(
            [r.geometry() for r in roundabouts])
        feedback.setProgress(20)
        all_roundabouts = all_roundabouts.mergeLines()
        feedback.setProgress(25)

        total = 70.0 / all_roundabouts.constGet().numGeometries(
        ) if all_roundabouts.isMultipart() else 1

        for current, roundabout in enumerate(all_roundabouts.parts()):
            touching = not_roundabout_index.intersects(
                roundabout.boundingBox())
            if not touching:
                continue

            if feedback.isCanceled():
                break

            roundabout_engine = QgsGeometry.createGeometryEngine(roundabout)
            roundabout_engine.prepareGeometry()
            roundabout_geom = QgsGeometry(roundabout.clone())
            roundabout_centroid = roundabout_geom.centroid()

            other_points = []

            # find all touching roads, and move the touching part to the centroid
            for t in touching:
                touching_geom = not_roundabouts[t].geometry()
                touching_road = touching_geom.constGet().clone()
                if not roundabout_engine.touches(touching_road):
                    # print('not touching!!')
                    continue

                # work out if start or end of line touched the roundabout
                nearest = roundabout_geom.nearestPoint(touching_geom)
                _, v = touching_geom.closestVertexWithContext(
                    nearest.asPoint())

                if v == 0:
                    # started at roundabout
                    other_points.append((touching_road.endPoint(), True, t))
                else:
                    # ended at roundabout
                    other_points.append((touching_road.startPoint(), False, t))

            if not other_points:
                continue

            # see if any incoming segments originate at the same place ("V" patterns)
            averaged = set()
            for point1, started_at_roundabout1, id1 in other_points:
                if id1 in averaged:
                    continue

                if feedback.isCanceled():
                    break

                parts_to_average = [id1]
                for point2, _, id2 in other_points:
                    if id2 == id1:
                        continue

                    if point2 != point1:
                        # todo tolerance?
                        continue

                    parts_to_average.append(id2)

                if len(parts_to_average) == 1:
                    # not a <O pattern, just a round coming straight to the roundabout
                    line = not_roundabouts[id1].geometry().constGet().clone()
                    if started_at_roundabout1:
                        # extend start of line to roundabout centroid
                        line.moveVertex(QgsVertexId(0, 0, 0),
                                        roundabout_centroid.constGet())
                    else:
                        # extend end of line to roundabout centroid
                        line.moveVertex(
                            QgsVertexId(0, 0,
                                        line.numPoints() - 1),
                            roundabout_centroid.constGet())

                    not_roundabout_index.deleteFeature(
                        not_roundabouts[parts_to_average[0]])
                    not_roundabouts[parts_to_average[0]].setGeometry(
                        QgsGeometry(line))
                    not_roundabout_index.addFeature(
                        not_roundabouts[parts_to_average[0]])

                elif len(parts_to_average) == 2:
                    # <O pattern
                    src_part, other_part = parts_to_average  # pylint: disable=unbalanced-tuple-unpacking
                    averaged.add(src_part)
                    averaged.add(other_part)

                    averaged_line = GeometryUtils.average_linestrings(
                        not_roundabouts[src_part].geometry().constGet(),
                        not_roundabouts[other_part].geometry().constGet())

                    if started_at_roundabout1:
                        # extend start of line to roundabout centroid
                        averaged_line.moveVertex(
                            QgsVertexId(0, 0, 0),
                            roundabout_centroid.constGet())
                    else:
                        # extend end of line to roundabout centroid
                        averaged_line.moveVertex(
                            QgsVertexId(0, 0,
                                        averaged_line.numPoints() - 1),
                            roundabout_centroid.constGet())

                    not_roundabout_index.deleteFeature(
                        not_roundabouts[src_part])
                    not_roundabouts[src_part].setGeometry(
                        QgsGeometry(averaged_line))
                    not_roundabout_index.addFeature(not_roundabouts[src_part])

                    not_roundabout_index.deleteFeature(
                        not_roundabouts[other_part])
                    del not_roundabouts[other_part]

            feedback.setProgress(25 + int(current * total))

        total = 5.0 / len(not_roundabouts)
        current = 0
        for _, f in not_roundabouts.items():
            if feedback.isCanceled():
                break

            sink.addFeature(f, QgsFeatureSink.FastInsert)
            current += 1
            feedback.setProgress(95 + int(current * total))

        return {self.OUTPUT: dest_id}
Ejemplo n.º 4
0
    def processAlgorithm(
            self,  # pylint: disable=missing-function-docstring,too-many-statements,too-many-branches,too-many-locals
            parameters,
            context,
            feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)

        if source is None:
            raise QgsProcessingException(
                self.invalidSourceError(parameters, self.INPUT))

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

        threshold = self.parameterAsDouble(parameters, self.THRESHOLD, context)
        fields = self.parameterAsFields(parameters, self.FIELDS, context)
        field_indices = [source.fields().lookupField(f) for f in fields]
        index = QgsSpatialIndex()
        roads = {}

        total = 10.0 / source.featureCount() if source.featureCount() else 0
        features = source.getFeatures()

        for current, feature in enumerate(features):
            if feedback.isCanceled():
                break

            if feature.geometry().isMultipart():
                if feature.geometry().constGet().numGeometries() > 1:
                    raise QgsProcessingException(
                        self.tr('Only single-part geometries are supported'))
                part1 = feature.geometry().constGet().geometryN(0).clone()
                feature.setGeometry(part1)

            index.addFeature(feature)
            roads[feature.id()] = feature

            feedback.setProgress(int(current * total))

        collapsed = {}
        processed = set()

        total = 85.0 / len(roads)
        current = 0
        for _id, f in roads.items():
            if feedback.isCanceled():
                break

            current += 1
            feedback.setProgress(10 + current * total)

            if _id in processed:
                continue

            box = f.geometry().boundingBox()
            box.grow(threshold)

            similar_candidates = index.intersects(box)
            if not similar_candidates:
                collapsed[_id] = f
                processed.add(_id)
                continue

            candidate = f.geometry()
            candidate_attrs = [f.attributes()[i] for i in field_indices]

            parts = []

            for t in similar_candidates:
                if t == _id:
                    continue

                other = roads[t]
                other_attrs = [other.attributes()[i] for i in field_indices]
                if other_attrs != candidate_attrs:
                    continue

                dist = candidate.hausdorffDistance(other.geometry())
                if dist < threshold:
                    parts.append(t)

            if len(parts) == 0:
                collapsed[_id] = f
                continue

            # todo fix this
            if len(parts) > 1:
                continue
            assert len(parts) == 1, len(parts)

            other = roads[parts[0]].geometry()
            averaged = QgsGeometry(
                GeometryUtils.average_linestrings(candidate.constGet(),
                                                  other.constGet()))

            # reconnect touching lines
            bbox = candidate.boundingBox()
            bbox.combineExtentWith(other.boundingBox())
            touching_candidates = index.intersects(bbox)

            for touching_candidate in touching_candidates:
                if touching_candidate in (_id, parts[0]):
                    continue

                # print(touching_candidate)

                touching_candidate_geom = roads[touching_candidate].geometry()
                # either the start or end of touching_candidate_geom touches candidate
                start = QgsGeometry(
                    touching_candidate_geom.constGet().startPoint())
                end = QgsGeometry(
                    touching_candidate_geom.constGet().endPoint())

                moved_start = False
                moved_end = False
                for cc in [candidate, other]:
                    #  if start.touches(cc):
                    start_line = start.shortestLine(cc)
                    if start_line.length() < 0.00000001:
                        # start touches, move to touch averaged line
                        averaged_line = start.shortestLine(averaged)
                        new_start = averaged_line.constGet().endPoint()
                        touching_candidate_geom.get().moveVertex(
                            QgsVertexId(0, 0, 0), new_start)
                        # print('moved start')
                        moved_start = True
                        continue
                    end_line = end.shortestLine(cc)
                    if end_line.length() < 0.00000001:
                        # endtouches, move to touch averaged line
                        averaged_line = end.shortestLine(averaged)
                        new_end = averaged_line.constGet().endPoint()
                        touching_candidate_geom.get().moveVertex(
                            QgsVertexId(
                                0, 0,
                                touching_candidate_geom.constGet().numPoints()
                                - 1), new_end)
                        # print('moved end')
                        moved_end = True
                        # break

                index.deleteFeature(roads[touching_candidate])
                if moved_start and moved_end:
                    if touching_candidate in collapsed:
                        del collapsed[touching_candidate]
                    processed.add(touching_candidate)
                else:
                    roads[touching_candidate].setGeometry(
                        touching_candidate_geom)
                    index.addFeature(roads[touching_candidate])
                    if touching_candidate in collapsed:
                        collapsed[touching_candidate].setGeometry(
                            touching_candidate_geom)

            index.deleteFeature(f)
            index.deleteFeature(roads[parts[0]])

            ff = QgsFeature(roads[parts[0]])
            ff.setGeometry(averaged)
            index.addFeature(ff)
            roads[ff.id()] = ff

            ff = QgsFeature(f)
            ff.setGeometry(averaged)
            index.addFeature(ff)
            roads[_id] = ff

            collapsed[_id] = ff
            processed.add(_id)
            processed.add(parts[0])

        total = 5.0 / len(processed)
        current = 0
        for _, f in collapsed.items():
            if feedback.isCanceled():
                break

            sink.addFeature(f, QgsFeatureSink.FastInsert)
            current += 1
            feedback.setProgress(95 + int(current * total))

        return {self.OUTPUT: dest_id}
Ejemplo n.º 5
0
    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """

        # Retrieve the feature source and sink. The 'dest_id' variable is used
        # to uniquely identify the feature sink, and must be included in the
        # dictionary returned by the processAlgorithm function.
        couche_lines = self.parameterAsVectorLayer(parameters, self.LINES, context)
        couche_points = self.parameterAsVectorLayer(parameters, self.NODES, context)
        radius=self.parameterAsDouble(parameters,self.RAYON,context)

        # Compute the number of steps to display within the progress bar and
        # get features from source
        delta=float(radius)
        index=QgsSpatialIndex()
        lines=couche_lines.getFeatures()
        for i in lines:
            if i.geometry().isMultipart():
                i.setGeometry(QgsGeometry.fromPolylineXY(i.geometry().asMultiPolyline()[0]))
            index.insertFeature(i)

        couche_lines.startEditing()
        couche_lines.beginEditCommand(self.tr("Split polylines at connections"))
        points=couche_points.getFeatures()
        nb=couche_points.featureCount()
        feedback.setProgressText(self.tr("Connecting points to lines..."))
        for pos,pt in enumerate(points):
            feedback.setProgress(pos*100.0/nb)
            ptg=pt.geometry()
            if ptg.isMultipart():
                ptg=QgsGeometry.fromPoint(ptg.asMultiPoint()[0])
            coor=ptg.asPoint()
            nearest=index.intersects(QgsRectangle(coor.x()-delta,coor.y()-delta,coor.x()+delta,coor.y()+delta))
            dmin=1e38
            if len(nearest)>0:
                for n in nearest:
                    f=couche_lines.getFeatures(request=QgsFeatureRequest(n))
                    for g in f:
                        d=g.geometry().distance(pt.geometry())
                        if d<=dmin:
                            dmin=d
                            gmin=g
                            gid=g.id()
                g=gmin
                if g.geometry().distance(pt.geometry())<delta:
                    a=g.geometry().closestSegmentWithContext(ptg.asPoint())
                    if not(a[2]==0):
                        geom=g.geometry()
                        geom.convertToSingleType()
                        geom_id=g.id()
                        att=g.attributes()
                        connexion=QgsFeature()
                        connexion.setGeometry(QgsGeometry.fromPolylineXY([ptg.asPoint(),a[1]]))
                        connexion.setAttributes(att)
                        couche_lines.addFeature(connexion)
                        geom.insertVertex(a[1][0],a[1][1],a[2])
                        geoma=geom.asPolyline()[:a[2]+1]
                        geomb=geom.asPolyline()[a[2]:]
                        feedback.setProgressText(unicode(geomb))
                        fa=QgsFeature()
                        fa.setGeometry(QgsGeometry.fromPolylineXY(geoma))
                        fa.setAttributes(att)
                        couche_lines.addFeature(fa)
                        index.insertFeature(fa)
                        fb=QgsFeature()
                        fb.setGeometry(QgsGeometry.fromPolylineXY(geomb))
                        fb.setAttributes(att)
                        couche_lines.addFeature(fb)
                        index.insertFeature(fb)
                        couche_lines.deleteFeature(g.id())
                        index.deleteFeature(g)
        couche_lines.endEditCommand()
        return {self.LINES: 'OK'}
Ejemplo n.º 6
0
class sGraph(QObject):
    finished = pyqtSignal(object)
    error = pyqtSignal(Exception, str)
    progress = pyqtSignal(float)
    warning = pyqtSignal(str)
    killed = pyqtSignal(bool)

    def __init__(self, edges={}, nodes={}):
        QObject.__init__(self)
        self.sEdges = edges
        self.sNodes = nodes  # can be empty
        self.total_progress = 0
        self.step = 0

        if len(self.sEdges) == 0:
            self.edge_id = 0
            self.sNodesCoords = {}
            self.node_id = 0
        else:
            self.edge_id = max(self.sEdges.keys())
            self.node_id = max(self.sNodes.keys())
            self.sNodesCoords = {snode.getCoords(): snode.id for snode in list(self.sNodes.values())}

        self.edgeSpIndex = QgsSpatialIndex()
        self.ndSpIndex = QgsSpatialIndex()
        res = [self.edgeSpIndex.addFeature(sedge.feature) for sedge in list(self.sEdges.values())]
        del res

        self.errors = []
        # breakages, orphans, merges, snaps, duplicate, points, mlparts
        self.unlinks = []
        self.points = []
        self.multiparts = []

    # graph from feat iter
    # updates the id
    def load_edges(self, feat_iter, angle_threshold):

        for f in feat_iter:

            if self.killed is True:
                break

            # add edge
            geometry = f.geometry().simplify(angle_threshold)
            geometry_pl = geometry.asPolyline()
            startpoint = geometry_pl[0]
            endpoint = geometry_pl[-1]
            start = self.load_point(startpoint)
            end = self.load_point(endpoint)
            snodes = [start, end]
            self.edge_id += 1
            self.update_topology(snodes[0], snodes[1], self.edge_id)

            f.setId(self.edge_id)
            f.setGeometry(geometry)
            sedge = sEdge(self.edge_id, f, snodes)
            self.sEdges[self.edge_id] = sedge

        return

    # pseudo graph from feat iter (only clean features - ids are fixed)
    def load_edges_w_o_topology(self, clean_feat_iter):

        for f in clean_feat_iter:

            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            # add edge
            sedge = sEdge(f.id(), f, [])
            self.sEdges[f.id()] = sedge
            self.edgeSpIndex.addFeature(f)

        self.edge_id = f.id()
        return

    # find existing or generate new node
    def load_point(self, point):
        try:
            node_id = self.sNodesCoords[(point[0], point[1])]
        except KeyError:
            self.node_id += 1
            node_id = self.node_id
            feature = QgsFeature()
            feature.setId(node_id)
            feature.setAttributes([node_id])
            feature.setGeometry(QgsGeometry.fromPointXY(point))
            self.sNodesCoords[(point[0], point[1])] = node_id
            snode = sNode(node_id, feature, [], [])
            self.sNodes[self.node_id] = snode
        return node_id

    # store topology
    def update_topology(self, node1, node2, edge):
        self.sNodes[node1].topology.append(node2)
        self.sNodes[node1].adj_edges.append(edge)
        self.sNodes[node2].topology.append(node1)
        self.sNodes[node2].adj_edges.append(edge)
        return

    # delete point
    def delete_node(self, node_id):
        del self.sNodes[node_id]
        return True

    def remove_edge(self, nodes, e):
        self.sNodes[nodes[0]].adj_edges.remove(e)
        self.sNodes[nodes[0]].topology.remove(nodes[1])
        self.sNodes[nodes[1]].adj_edges.remove(e)  # if self loop - removed twice
        self.sNodes[nodes[1]].topology.remove(nodes[0])  # if self loop - removed twice
        del self.sEdges[e]
        # spIndex self.edgeSpIndex.deleteFeature(self.sEdges[e].feature)
        return

    # create graph (broken_features_iter)
    # can be applied to edges w-o topology for speed purposes
    def break_features_iter(self, getUnlinks, angle_threshold, fix_unlinks=False):

        for sedge in list(self.sEdges.values()):

            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            f = sedge.feature
            f_geom = f.geometry()
            pl = f_geom.asPolyline()
            lines = [line for line in self.edgeSpIndex.intersects(f_geom.boundingBox()) if line != f.id()]

            # self intersections
            # include first and last
            self_intersections = uf.getSelfIntersections(pl)

            # common vertices
            intersections = list(itertools.chain.from_iterable(
                [set(pl[1:-1]).intersection(set(self.sEdges[line].feature.geometry().asPolyline())) for line in lines]))
            intersections += self_intersections
            intersections = (set(intersections))

            if len(intersections) > 0:
                # broken features iterator
                # errors
                for pnt in intersections:
                    err_f = QgsFeature(error_feat)
                    err_f.setGeometry(QgsGeometry.fromPointXY(pnt))
                    err_f.setAttributes(['broken'])
                    self.errors.append(err_f)
                vertices_indices = uf.find_vertex_indices(pl, intersections)
                for start, end in zip(vertices_indices[:-1], vertices_indices[1:]):
                    broken_feat = QgsFeature(f)
                    broken_geom = QgsGeometry.fromPolylineXY(pl[start:end + 1]).simplify(angle_threshold)
                    broken_feat.setGeometry(broken_geom)
                    yield broken_feat
            else:
                simpl_geom = f.geometry().simplify(angle_threshold)
                f.setGeometry(simpl_geom)
                yield f

    def fix_unlinks(self):

        self.edgeSpIndex = QgsSpatialIndex()
        self.step = self.step / 2.0

        for e in list(self.sEdges.values()):
            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            self.edgeSpIndex.addFeature(e.feature)

        for sedge in list(self.sEdges.values()):

            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            f = sedge.feature
            f_geom = f.geometry()
            pl = f_geom.asPolyline()
            lines = [line for line in self.edgeSpIndex.intersects(f_geom.boundingBox()) if line != f.id()]
            lines = [line for line in lines if f_geom.crosses(self.sEdges[line].feature.geometry())]
            for line in lines:
                crossing_points = f_geom.intersection(self.sEdges[line].feature.geometry())
                if crossing_points.type() == QgsWkbTypes.PointGeometry:
                    if not crossing_points.isMultipart():
                        if crossing_points.asPoint() in pl[1:-1]:
                            edge_geometry = self.sEdges[sedge.id].feature.geometry()
                            edge_geometry.moveVertex(crossing_points.asPoint().x() + 1,
                                                     crossing_points.asPoint().y() + 1,
                                                     pl.index(crossing_points.asPoint()))
                            self.sEdges[sedge.id].feature.setGeometry(edge_geometry)
                    else:
                        for p in crossing_points.asMultiPoint():
                            if p in pl[1:-1]:
                                edge_geometry = self.sEdges[sedge.id].feature.geometry()
                                edge_geometry.moveVertex(p.x() + 1,
                                                         p.y() + 1,
                                                         pl.index(p))
                                self.sEdges[sedge.id].feature.setGeometry(edge_geometry)
            # TODO: exclude vertices - might be in one of the lines

        return

    def con_comp_iter(self, group_dictionary):
        components_passed = set([])
        for id in list(group_dictionary.keys()):

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            if {id}.isdisjoint(components_passed):
                group = [[id]]
                candidates = ['dummy', 'dummy']
                while len(candidates) > 0:
                    flat_group = group[:-1] + group[-1]
                    candidates = [set(group_dictionary[last_visited_node]).difference(set(flat_group)) for
                                  last_visited_node in group[-1]]
                    candidates = list(set(itertools.chain.from_iterable(candidates)))
                    group = flat_group + [candidates]
                    components_passed.update(set(candidates))
                yield group[:-1]

    # group points based on proximity - spatial index is not updated
    def snap_endpoints(self, snap_threshold):
        QgsMessageLog.logMessage('starting snapping', level=Qgis.Critical)
        res = [self.ndSpIndex.addFeature(snode.feature) for snode in list(self.sNodes.values())]
        filtered_nodes = {}
        # exclude nodes where connectivity = 2 - they will be merged
        self.step = self.step / float(2)
        for node in [n for n in list(self.sNodes.values()) if n.adj_edges != 2]:
            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            # find nodes within x distance
            node_geom = node.feature.geometry()
            nodes = [nd for nd in self.ndSpIndex.intersects(node_geom.buffer(snap_threshold, 10).boundingBox()) if
                     nd != node.id and node_geom.distance(self.sNodes[nd].feature.geometry()) <= snap_threshold]
            if len(nodes) > 0:
                filtered_nodes[node.id] = nodes

        QgsMessageLog.logMessage('continuing snapping', level=Qgis.Critical)
        self.step = (len(filtered_nodes) * self.step) / float(len(self.sNodes))
        for group in self.con_comp_iter(filtered_nodes):

            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            # find con_edges
            con_edges = set(itertools.chain.from_iterable([self.sNodes[node].adj_edges for node in group]))

            # collapse nodes to node
            merged_node_id, centroid_point = self.collapse_to_node(group)

            # update connected edges and their topology
            for edge in con_edges:
                sedge = self.sEdges[edge]
                start, end = sedge.nodes
                # if existing self loop
                if start == end:  # and will be in group
                    if sedge.feature.geometry().length() <= snap_threshold:  # short self-loop
                        self.remove_edge((start, end), edge)
                    else:
                        self.sEdges[edge].replace_start(self.node_id, centroid_point)
                        self.update_topology(merged_node_id, merged_node_id, edge)
                        self.sNodes[end].topology.remove(start)
                        self.sEdges[edge].replace_end(self.node_id, centroid_point)
                        self.sNodes[start].topology.remove(end)
                    # self.sNodes[start].topology.remove(end)
                # if becoming self loop (if one intermediate vertex - turns back on itself)
                elif start in group and end in group:
                    if (len(sedge.feature.geometry().asPolyline()) <= 3
                            or sedge.feature.geometry().length() <= snap_threshold):
                        self.remove_edge((start, end), edge)
                    else:
                        self.sEdges[edge].replace_start(self.node_id, centroid_point)
                        self.sEdges[edge].replace_end(self.node_id, centroid_point)
                        self.update_topology(merged_node_id, merged_node_id, edge)
                        self.sNodes[end].topology.remove(start)
                        self.sNodes[start].topology.remove(end)
                # if only start
                elif start in group:
                    self.sEdges[edge].replace_start(self.node_id, centroid_point)
                    self.sNodes[merged_node_id].topology.append(end)
                    self.sNodes[merged_node_id].adj_edges.append(edge)
                    self.sNodes[end].topology.append(merged_node_id)
                    self.sNodes[end].topology.remove(start)
                # if only end
                elif end in group:
                    self.sEdges[edge].replace_end(self.node_id, centroid_point)
                    self.sNodes[merged_node_id].topology.append(start)
                    self.sNodes[merged_node_id].adj_edges.append(edge)
                    self.sNodes[start].topology.append(merged_node_id)
                    self.sNodes[start].topology.remove(end)

            # errors
            for node in group:
                err_f = QgsFeature(error_feat)
                err_f.setGeometry(self.sNodes[node].feature.geometry())
                err_f.setAttributes(['snapped'])
                self.errors.append(err_f)

            # delete old nodes
            res = [self.delete_node(item) for item in group]

        return

    def collapse_to_node(self, group):

        # create new node, coords
        self.node_id += 1
        feat = QgsFeature()
        centroid = (
            QgsGeometry.fromMultiPointXY([self.sNodes[nd].feature.geometry().asPoint() for nd in group])).centroid()
        feat.setGeometry(centroid)
        feat.setAttributes([self.node_id])
        feat.setId(self.node_id)
        snode = sNode(self.node_id, feat, [], [])
        self.sNodes[self.node_id] = snode
        self.ndSpIndex.addFeature(feat)

        return self.node_id, centroid.asPoint()

    # TODO add agg_cost
    def route_nodes(self, group, step):
        count = 1
        group = [group]
        while count <= step:
            last_visited = group[-1]
            group = group[:-1] + group[-1]
            con_nodes = set(itertools.chain.from_iterable(
                [self.sNodes[last_node].topology for last_node in last_visited])).difference(group)
            group += [con_nodes]
            count += 1
            for nd in con_nodes:
                yield count - 1, nd

    def route_edges(self, group, step):
        count = 1
        group = [group]
        while count <= step:
            last_visited = group[-1]
            group = group[:-1] + group[-1]
            con_edges = set(
                itertools.chain.from_iterable([self.sNodes[last_node].topology for last_node in last_visited]))
            con_nodes = [con_node for con_node in con_nodes if con_node not in group]
            group += [con_nodes]
            count += 1
            # TODO: return circles
            for dg in con_edges:
                yield count - 1, nd, dg

    # TODO: snap_geometries (not endpoints)
    # TODO: extend

    def clean_dupl(self, group_edges, snap_threshold, parallel=False):

        self.total_progress += self.step
        self.progress.emit(self.total_progress)

        # keep line with minimum length
        # TODO: add distance centroids
        lengths = [self.sEdges[e].feature.geometry().length() for e in group_edges]
        sorted_edges = [x for _, x in sorted(zip(lengths, group_edges))]
        min_len = min(lengths)

        # if parallel is False:
        prl_dist_threshold = 0
        # else:
        #    dist_threshold = snap_threshold
        for e in sorted_edges[1:]:
            # delete line
            if abs(self.sEdges[e].feature.geometry().length() - min_len) <= prl_dist_threshold:
                for p in set([self.sNodes[n].feature.geometry() for n in self.sEdges[e].nodes]):
                    err_f = QgsFeature(error_feat)
                    err_f.setGeometry(p)
                    err_f.setAttributes(['duplicate'])
                    self.errors.append(err_f)
                self.remove_edge(self.sEdges[e].nodes, e)
        return

    def clean_multipart(self, e):

        self.total_progress += self.step
        self.progress.emit(self.total_progress)

        # only used in the last cleaning iteration - only updates self.sEdges and spIndex (allowed to be used once in the end)
        # nodes are not added to the new edges

        multi_poly = e.feature.geometry().asMultiPolyline()

        for singlepart in multi_poly:
            # create new edge and update spIndex
            single_geom = QgsGeometry.fromPolylineXY(singlepart)
            single_feature = QgsFeature(e.feature)
            single_feature.setGeometry(single_geom)
            self.edge_id += 1
            single_feature.setId(self.edge_id)
            self.sEdges[self.edge_id] = sEdge(self.edge_id, single_feature, [])
            self.edgeSpIndex.addFeature(single_feature)

            if len(multi_poly) >= 1:
                # add points as multipart errors if there was actually more than one line
                for p in single_geom.asPolyline():
                    err_f = QgsFeature(error_feat)
                    err_f.setGeometry(QgsGeometry.fromPointXY(p))
                    err_f.setAttributes(['multipart'])
                    self.errors.append(err_f)

        # delete old feature - spIndex

        self.edgeSpIndex.deleteFeature(self.sEdges[e.id].feature)
        del self.sEdges[e.id]

        return

    def clean_orphan(self, e):

        self.total_progress += self.step
        self.progress.emit(self.total_progress)

        nds = e.nodes
        snds = self.sNodes[nds[0]], self.sNodes[nds[1]]
        # connectivity of both endpoints 1
        # if parallel - A:[B,B]
        # if selfloop to line - A: [A,A, C]
        # if selfloop
        # if selfloop and parallel
        if len(set(snds[0].topology)) == len(set(snds[1].topology)) == 1 and len(set(snds[0].adj_edges)) == 1:
            del self.sEdges[e.id]
            for nd in set(nds):
                err_f = QgsFeature(error_feat)
                err_f.setGeometry(self.sNodes[nd].feature.geometry())
                err_f.setAttributes(['orphan'])
                self.errors.append(err_f)
                del self.sNodes[nd]
        return True

    # find duplicate geometries
    # find orphans

    def clean(self, duplicates, orphans, snap_threshold, closed_polylines, multiparts=False):
        # clean duplicates - delete longest from group using snap threshold
        step_original = float(self.step)
        if duplicates:
            input = [(e.id, frozenset(e.nodes)) for e in list(self.sEdges.values())]
            groups = defaultdict(list)
            for v, k in input: groups[k].append(v)

            dupl_candidates = dict([nodes_edges for nodes_edges in list(groups.items()) if len(nodes_edges[1]) > 1])

            self.step = (len(dupl_candidates) * self.step) / float(len(self.sEdges))
            for (nodes, group_edges) in list(dupl_candidates.items()):

                if self.killed is True:
                    break

                self.total_progress += self.step
                self.progress.emit(self.total_progress)
                self.clean_dupl(group_edges, snap_threshold, False)

        self.step = step_original
        # clean orphans
        if orphans:
            for e in list(self.sEdges.values()):

                if self.killed is True:
                    break

                self.total_progress += self.step
                self.progress.emit(self.total_progress)

                self.clean_orphan(e)

        # clean orphan closed polylines
        elif closed_polylines:

            for e in list(self.sEdges.values()):

                if self.killed is True:
                    break

                self.total_progress += self.step
                self.progress.emit(self.total_progress)

                if len(set(e.nodes)) == 1:
                    self.clean_orphan(e)

        # break multiparts
        if multiparts:
            for e in list(self.sEdges.values()):

                if self.killed is True:
                    break

                self.total_progress += self.step
                self.progress.emit(self.total_progress)

                if e.feature.geometry().type() == QgsWkbTypes.LineGeometry and \
                        e.feature.geometry().isMultipart():
                    self.clean_multipart(e)

        return

    # merge

    def merge_b_intersections(self, angle_threshold):

        # special cases: merge parallels (becomes orphan)
        # do not merge two parallel self loops

        edges_passed = set([])

        for e in self.edge_edges_iter():
            if {e}.isdisjoint(edges_passed):
                edges_passed.update({e})
                group_nodes, group_edges = self.route_polylines(e)
                if group_edges:
                    edges_passed.update({group_edges[-1]})
                    self.merge_edges(group_nodes, group_edges, angle_threshold)
        return

    def merge_collinear(self, collinear_threshold, angle_threshold=0):

        filtered_nodes = dict([id_nd for id_nd in list(graph.sNodes.items()) if
                               len(id_nd[1].topology) == 2 and len(id_nd[1].adj_edges) == 2])
        filtered_nodes = dict([id_nd1 for id_nd1 in list(filtered_nodes.items()) if
                               uf.angle_3_points(graph.sNodes[id_nd1[1].topology[0]].feature.geometry().asPoint(),
                                                 id_nd1[1].feature.geometry().asPoint(), graph.sNodes[
                                                     id_nd1[1].topology[
                                                         1]].feature.geometry().asPoint()) <= collinear_threshold])
        filtered_nodes = {id: nd.adj_edges for id, nd in list(filtered_nodes.items())}
        filtered_edges = {}
        for k, v in list(filtered_nodes.items()):
            try:
                filtered_edges[v[0]].append(v[1])
            except KeyError:
                filtered_edges[v[0]] = [v[1]]
            try:
                filtered_edges[v[1]].append(v[0])
            except KeyError:
                filtered_edges[v[1]] = [v[0]]

        self.step = (len(filtered_edges) * self.step) / float(len(self.sEdges))

        for group in self.collinear_comp_iter(filtered_edges):
            nodes = [self.sEdges[e].nodes for e in group]
            for idx, pair in enumerate(nodes[:-1]):
                if pair[0] in nodes[idx + 1]:
                    nodes[idx] = pair[::-1]
            if nodes[-1][1] in nodes[-2]:
                nodes[-1] = nodes[-1][::-1]
            nodes = [n[0] for n in nodes] + [nodes[-1][-1]]
            self.merge_edges(nodes, group, angle_threshold)
        return

    def collinear_comp_iter(self, group_dictionary):
        components_passed = set([])
        for id, top in list(group_dictionary.items()):

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            if {id}.isdisjoint(components_passed) and len(top) != 2:
                group = [[id]]
                candidates = ['dummy', 'dummy']
                while len(candidates) > 0:
                    flat_group = group[:-1] + group[-1]
                    candidates = [set(group_dictionary[last_visited_node]).difference(set(flat_group)) for
                                  last_visited_node in group[-1]]
                    candidates = list(set(itertools.chain.from_iterable(candidates)))
                    group = flat_group + [candidates]
                    components_passed.update(set(candidates))
                yield group[:-1]

    def edge_edges_iter(self):
        # what if two parallel edges at the edge - should become self loop
        for nd_id, nd in list(self.sNodes.items()):

            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            con_edges = nd.adj_edges
            if len(nd.topology) != 2 and len(con_edges) != 2:  # not set to include parallels and self loops
                for e in con_edges:
                    yield e

    def route_polylines(self, startedge):
        # if edge has been passed
        startnode, endnode = self.sEdges[startedge].nodes
        if len(self.sNodes[endnode].topology) != 2:  # not set to account for self loops
            startnode, endnode = endnode, startnode
        group_nodes = [startnode, endnode]
        group_edges = [startedge]
        while len(set(self.sNodes[group_nodes[-1]].adj_edges)) == 2:
            last_visited = group_nodes[-1]
            if last_visited in self.sNodes[last_visited].topology:  # to account for self loops
                break
            con_edge = set(self.sNodes[last_visited].adj_edges).difference(set(group_edges)).pop()
            con_node = [n for n in self.sEdges[con_edge].nodes if n != last_visited][0]  # to account for self loops
            group_nodes.append(con_node)
            group_edges.append(con_edge)
        if len(group_nodes) > 2:
            return group_nodes, group_edges
        else:
            return None, None

    def generate_unlinks(self):  # for osm or other

        # spIndex # TODO change OTF - insert/delete feature
        self.edgeSpIndex = QgsSpatialIndex()

        self.step = self.step / float(4)
        for e in list(self.sEdges.values()):
            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            self.edgeSpIndex.addFeature(e.feature)

        unlinks_id = 0
        self.step = float(3.0) * self.step
        for id, e in list(self.sEdges.items()):

            if self.killed is True:
                break

            self.total_progress += self.step
            self.progress.emit(self.total_progress)

            f_geom = e.feature.geometry()
            # to avoid duplicate unlinks - id > line
            lines = [line for line in self.edgeSpIndex.intersects(f_geom.boundingBox()) if
                     f_geom.crosses(self.sEdges[line].feature.geometry()) and id > line]

            for line in lines:
                crossing_points = f_geom.intersection(self.sEdges[line].feature.geometry())
                # in some cases the startpoint or endpoint is returned - exclude
                if crossing_points.type() == QgsWkbTypes.PointGeometry:
                    if not crossing_points.isMultipart():
                        un_f = QgsFeature(unlink_feat)
                        un_f.setGeometry(crossing_points)
                        un_f.setId(unlinks_id)
                        un_f.setAttributes([unlinks_id])
                        unlinks_id += 1
                        self.unlinks.append(un_f)
                    else:
                        for p in crossing_points.asMultiPoint():
                            if p not in f_geom.asPolyline():
                                un_f = QgsFeature(unlink_feat)
                                un_f.setGeometry(QgsGeometry.fromPointXY(p))
                                un_f.setId(unlinks_id)
                                un_f.setAttributes([unlinks_id])
                                unlinks_id += 1
                                self.unlinks.append(un_f)
        return

    # TODO: features added - pass through clean_iterator (can be ml line)
    def merge_edges(self, group_nodes, group_edges, angle_threshold):

        geoms = [self.sEdges[e].feature.geometry() for e in group_edges]
        lengths = [g.length() for g in geoms]
        max_len = max(lengths)

        # merge edges
        self.edge_id += 1
        feat = QgsFeature()
        # attributes from longest
        longest_feat = self.sEdges[group_edges[lengths.index(max_len)]].feature
        feat.setAttributes(longest_feat.attributes())
        merged_geom = uf.merge_geoms(geoms, angle_threshold)
        if merged_geom.type() == QgsWkbTypes.LineGeometry:
            if not merged_geom.isMultipart():
                p0 = merged_geom.asPolyline()[0]
                p1 = merged_geom.asPolyline()[-1]
            else:
                p0 = merged_geom.asMultiPolyline()[0][0]
                p1 = merged_geom.asMultiPolyline()[-1][-1]

        # special case - if self loop breaks at intersection of other line & then merged back on old self loop point
        # TODO: include in merged_geoms functions to make indepedent
        selfloop_point = self.sNodes[group_nodes[0]].feature.geometry().asPoint()
        if p0 == p1 and p0 != selfloop_point:
            merged_points = geoms[0].asPolyline()
            geom1 = self.sEdges[group_edges[0]].feature.geometry().asPolyline()
            if not geom1[0] == selfloop_point:
                merged_points = merged_points[::-1]
            for geom in geoms[1:]:
                points = geom.asPolyline()
                if not points[0] == merged_points[-1]:
                    merged_points += (points[::-1])[1:]
                else:
                    merged_points += points[1:]
            merged_geom = QgsGeometry.fromPolylineXY(merged_points)
            if merged_geom.wkbType() != QgsWkbTypes.LineString:
                print('ml', merged_geom.wkbType())

        feat.setGeometry(merged_geom)
        feat.setId(self.edge_id)

        if p0 == self.sNodes[group_nodes[0]].feature.geometry().asPoint():
            merged_edge = sEdge(self.edge_id, feat, [group_nodes[0], group_nodes[-1]])
        else:
            merged_edge = sEdge(self.edge_id, feat, [group_nodes[-1], group_nodes[0]])
        self.sEdges[self.edge_id] = merged_edge

        # update ends
        self.sNodes[group_nodes[0]].topology.remove(group_nodes[1])
        self.update_topology(group_nodes[0], group_nodes[-1], self.edge_id)
        # if group_nodes == [group_nodes[0], group_nodes[1], group_nodes[0]]:
        self.sNodes[group_nodes[-1]].topology.remove(group_nodes[-2])
        self.sNodes[group_nodes[0]].adj_edges.remove(group_edges[0])
        self.sNodes[group_nodes[-1]].adj_edges.remove(group_edges[-1])

        # middle nodes del
        for nd in group_nodes[1:-1]:
            err_f = QgsFeature(error_feat)
            err_f.setGeometry(self.sNodes[nd].feature.geometry())
            err_f.setAttributes(['merged'])
            self.errors.append(err_f)
            del self.sNodes[nd]

        # del edges
        for e in group_edges:
            del self.sEdges[e]

        return

    def simplify_circles(self):
        roundabouts = NULL
        short = NULL
        res = [self.collapse_to_node(group) for group in con_components(roundabouts + short)]
        return

    def simplify_parallel_lines(self):
        dual_car = NULL
        res = [self.collapse_to_medial_axis(group) for group in con_components(dual_car)]
        pass

    def collapse_to_medial_axis(self):
        pass

    def simplify_angle(self, max_angle_threshold):
        pass

    def catchment_iterator(self, origin_point, closest_edge, cost_limit, origin_name):
        # find closest line
        edge_geom = self.sEdges[closest_edge].feature.geometry()
        nodes = set(self.sEdges[closest_edge].nodes)

        # endpoints
        branches = []
        shortest_line = origin_point.shortestLine(edge_geom)
        point_on_line = shortest_line.intersection(edge_geom)
        fraction = edge_geom.lineLocatePoint(point_on_line)
        fractions = [fraction, 1 - fraction]
        degree = 0
        for node, fraction in zip(nodes, fractions):
            branches.append((None, node, closest_edge, self.sNodes[node].feature.geometry().distance(point_on_line),))

        for k in list(self.sEdges.keys()):
            self.sEdges[k].visited[origin_name] = None

        self.sEdges[closest_edge].visited[origin_name] = True

        while len(branches) > 0:
            branches = [nbr for (org, dest, edge, agg_cost) in branches if agg_cost < cost_limit and dest != [] for nbr
                        in self.get_next_edges(dest, agg_cost, origin_name)]

            # fraction = 1 - ((agg_cost - cost_limit) / float(cost_limit))
            # degree += 1

    def get_next_edges(self, old_dest, agg_cost, origin_name):
        new_origin = old_dest[0]
        new_branches = []
        for edg in set(self.sNodes[new_origin].adj_edges):
            sedge = self.sEdges[edg]
            if sedge.visited[origin_name] is None:
                sedge.visited[origin_name] = new_origin
                new_agg_cost = agg_cost + sedge.len
                sedge.agg_cost[origin_name] = new_agg_cost
                self.sEdges[edg] = sedge
                new_dest = [n for n in sedge.nodes if n != new_origin]
                new_branches.append((new_origin, new_dest, edg, new_agg_cost))
        return new_branches

    def kill(self):
        self.killed = True
Ejemplo n.º 7
0
    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """

        # Retrieve the feature source and sink. The 'dest_id' variable is used
        # to uniquely identify the feature sink, and must be included in the
        # dictionary returned by the processAlgorithm function.
        couche_lines = self.parameterAsVectorLayer(parameters, self.LINES, context)
        couche_points = self.parameterAsVectorLayer(parameters, self.NODES, context)
        radius=self.parameterAsDouble(parameters,self.RAYON,context)

        # Compute the number of steps to display within the progress bar and
        # get features from source
        delta=float(radius)
        index=QgsSpatialIndex()
        lines=couche_lines.getFeatures()
        for i in lines:
            if i.geometry().isMultipart():
                i.setGeometry(QgsGeometry.fromPolylineXY(i.geometry().asMultiPolyline()[0]))
            index.insertFeature(i)

        couche_lines.startEditing()
        couche_lines.beginEditCommand(self.tr("Split polylines at connections"))
        points=couche_points.getFeatures()
        nb=couche_points.featureCount()
        feedback.setProgressText(self.tr("Connecting points to lines..."))
        for pos,pt in enumerate(points):
            feedback.setProgress(pos*100.0/nb)
            ptg=pt.geometry()
            if ptg.isMultipart():
                ptg=QgsGeometry.fromPoint(ptg.asMultiPoint()[0])
            coor=ptg.asPoint()
            nearest=index.intersects(QgsRectangle(coor.x()-delta,coor.y()-delta,coor.x()+delta,coor.y()+delta))
            dmin=1e38
            if len(nearest)>0:
                for n in nearest:
                    f=couche_lines.getFeatures(request=QgsFeatureRequest(n))
                    for g in f:
                        d=g.geometry().distance(pt.geometry())
                        if d<=dmin:
                            dmin=d
                            gmin=g
                            gid=g.id()
                g=gmin
                if g.geometry().distance(pt.geometry())<delta:
                    a=g.geometry().closestSegmentWithContext(ptg.asPoint())
                    if not(a[2]==0):
                        geom=g.geometry()
                        geom.convertToSingleType()
                        geom_id=g.id()
                        att=g.attributes()
                        connexion=QgsFeature()
                        connexion.setGeometry(QgsGeometry.fromPolylineXY([ptg.asPoint(),a[1]]))
                        connexion.setAttributes(att)
                        couche_lines.addFeature(connexion)
                        geom.insertVertex(a[1][0],a[1][1],a[2])
                        geoma=geom.asPolyline()[:a[2]+1]
                        geomb=geom.asPolyline()[a[2]:]
                        feedback.setProgressText(unicode(geomb))
                        fa=QgsFeature()
                        fa.setGeometry(QgsGeometry.fromPolylineXY(geoma))
                        fa.setAttributes(att)
                        couche_lines.addFeature(fa)
                        index.insertFeature(fa)
                        fb=QgsFeature()
                        fb.setGeometry(QgsGeometry.fromPolylineXY(geomb))
                        fb.setAttributes(att)
                        couche_lines.addFeature(fb)
                        index.insertFeature(fb)
                        couche_lines.deleteFeature(g.id())
                        index.deleteFeature(g)
        couche_lines.commitChanges()
        couche_lines.endEditCommand()
        return {self.LINES: 'OK'}