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}
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))
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}
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}
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'}
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
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'}