def testGetGeometry(self): idx = QgsSpatialIndex() idx2 = QgsSpatialIndex(QgsSpatialIndex.FlagStoreFeatureGeometries) fid = 0 for y in range(5): for x in range(10, 15): ft = QgsFeature() ft.setId(fid) ft.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y))) idx.addFeature(ft) idx2.addFeature(ft) fid += 1 # not storing geometries, a keyerror should be raised with self.assertRaises(KeyError): idx.geometry(-100) with self.assertRaises(KeyError): idx.geometry(1) with self.assertRaises(KeyError): idx.geometry(2) with self.assertRaises(KeyError): idx.geometry(1000) self.assertEqual(idx2.geometry(1).asWkt(1), 'Point (11 0)') self.assertEqual(idx2.geometry(2).asWkt(1), 'Point (12 0)') with self.assertRaises(KeyError): idx2.geometry(-100) with self.assertRaises(KeyError): idx2.geometry(1000)
def processAlgorithm(self, parameters, context, feedback): pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) crs = self.parameterAsCrs(parameters, self.TARGET_CRS, context) bbox = self.parameterAsExtent(parameters, self.EXTENT, context, crs) extent = QgsGeometry().fromRect(bbox) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, crs) if sink is None: raise QgsProcessingException( self.invalidSinkError(parameters, self.OUTPUT)) nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() points = dict() random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break rx = bbox.xMinimum() + bbox.width() * random.random() ry = bbox.yMinimum() + bbox.height() * random.random() p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if geom.within(extent) and \ vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(int(nPoints * total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo( self.tr( 'Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.')) return {self.OUTPUT: dest_id}
def testIndex(self): idx = QgsSpatialIndex() fid = 0 for y in range(5, 15, 5): for x in range(5, 25, 5): ft = QgsFeature() ft.setId(fid) ft.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y))) idx.addFeature(ft) fid += 1 # intersection test rect = QgsRectangle(7.0, 3.0, 17.0, 13.0) fids = idx.intersects(rect) myExpectedValue = 4 myValue = len(fids) myMessage = 'Expected: %s Got: %s' % (myExpectedValue, myValue) self.assertEqual(myValue, myExpectedValue, myMessage) fids.sort() myMessage = ('Expected: %s\nGot: %s\n' % ([1, 2, 5, 6], fids)) assert fids == [1, 2, 5, 6], myMessage # nearest neighbor test fids = idx.nearestNeighbor(QgsPointXY(8.75, 6.25), 3) myExpectedValue = 0 myValue = len(fids) myMessage = 'Expected: %s Got: %s' % (myExpectedValue, myValue) fids.sort() myMessage = ('Expected: %s\nGot: %s\n' % ([0, 1, 5], fids)) assert fids == [0, 1, 5], myMessage
def create_spatial_index(layer): # Select all features along with their attributes all_features = {feature.id(): feature for (feature) in layer.getFeatures()} # Create spatial index spatial_index = QgsSpatialIndex() for f in all_features.values(): spatial_index.addFeature(f) return spatial_index
def compute_graph(features, feedback, create_id_graph=False, min_distance=0): """ compute topology from a layer/field """ s = Graph(sort_graph=False) id_graph = None if create_id_graph: id_graph = Graph(sort_graph=True) # skip features without geometry features_with_geometry = { f_id: f for (f_id, f) in features.items() if f.hasGeometry() } total = 70.0 / len( features_with_geometry) if features_with_geometry else 1 index = QgsSpatialIndex() i = 0 for feature_id, f in features_with_geometry.items(): if feedback.isCanceled(): break g = f.geometry() if min_distance > 0: g = g.buffer(min_distance, 5) engine = QgsGeometry.createGeometryEngine(g.constGet()) engine.prepareGeometry() feature_bounds = g.boundingBox() # grow bounds a little so we get touching features feature_bounds.grow(feature_bounds.width() * 0.01) intersections = index.intersects(feature_bounds) for l2 in intersections: f2 = features_with_geometry[l2] if engine.intersects(f2.geometry().constGet()): s.add_edge(f.id(), f2.id()) s.add_edge(f2.id(), f.id()) if id_graph: id_graph.add_edge(f.id(), f2.id()) index.addFeature(f) i += 1 feedback.setProgress(int(i * total)) for feature_id, f in features_with_geometry.items(): if feedback.isCanceled(): break if feature_id not in s.node_edge: s.add_edge(feature_id, None) return s, id_graph
def buildSpatialIndexAndIdDict(self, inputLyr): """ creates a spatial index for the centroid layer """ spatialIdx = QgsSpatialIndex() idDict = {} for feat in inputLyr.getFeatures(): spatialIdx.addFeature(feat) idDict[feat.id()] = feat return spatialIdx, idDict
def processAlgorithm(self, parameters, context, feedback): pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) crs = self.parameterAsCrs(parameters, self.TARGET_CRS, context) bbox = self.parameterAsExtent(parameters, self.EXTENT, context, crs) extent = QgsGeometry().fromRect(bbox) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, crs) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() points = dict() random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break rx = bbox.xMinimum() + bbox.width() * random.random() ry = bbox.yMinimum() + bbox.height() * random.random() p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if geom.within(extent) and \ vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(int(nPoints * total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo(self.tr('Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.')) return {self.OUTPUT: dest_id}
def createIndex(layer): provider = layer.dataProvider() caps = provider.capabilities() if caps & QgsVectorDataProvider.CreateSpatialIndex: feat = QgsFeature() index = QgsSpatialIndex() fit = provider.getFeatures() while fit.nextFeature(feat): index.addFeature(feat) return index else: return None
def get_nearest_segmentId(layer, QgsPointXY, set_progress_funk): spIndex = QgsSpatialIndex() all_f = layer.featureCount() for feature in layer.getFeatures(): progress = 47 + (int(feature.id()) * 10) / all_f set_progress_funk(progress) spIndex.addFeature(feature) nearestIds = spIndex.nearestNeighbor(QgsPointXY, 1) nearest_feature = layer.getFeatures(QgsFeatureRequest().setFilterFid( nearestIds[0])) ftr = QgsFeature() nearest_feature.nextFeature(ftr) return ftr["fid"]
def compute_graph(features, feedback, create_id_graph=False, min_distance=0): """ compute topology from a layer/field """ s = Graph(sort_graph=False) id_graph = None if create_id_graph: id_graph = Graph(sort_graph=True) # skip features without geometry features_with_geometry = {f_id: f for (f_id, f) in features.items() if f.hasGeometry()} total = 70.0 / len(features_with_geometry) if features_with_geometry else 1 index = QgsSpatialIndex() i = 0 for feature_id, f in features_with_geometry.items(): if feedback.isCanceled(): break g = f.geometry() if min_distance > 0: g = g.buffer(min_distance, 5) engine = QgsGeometry.createGeometryEngine(g.constGet()) engine.prepareGeometry() feature_bounds = g.boundingBox() # grow bounds a little so we get touching features feature_bounds.grow(feature_bounds.width() * 0.01) intersections = index.intersects(feature_bounds) for l2 in intersections: f2 = features_with_geometry[l2] if engine.intersects(f2.geometry().constGet()): s.add_edge(f.id(), f2.id()) s.add_edge(f2.id(), f.id()) if id_graph: id_graph.add_edge(f.id(), f2.id()) index.addFeature(f) i += 1 feedback.setProgress(int(i * total)) for feature_id, f in features_with_geometry.items(): if feedback.isCanceled(): break if feature_id not in s.node_edge: s.add_edge(feature_id, None) return s, id_graph
def _assignFuel(self, points): index = QgsSpatialIndex() for feat in self._fuel_layer.getFeatures(): index.addFeature(feat) intersecting_fuels = dict() intersecting_points = list() for pt in points: pt.fuelModel = '1' geom = pt.feature.geometry() intersects = index.intersects(geom.boundingBox()) if len(intersects) > 0: request = QgsFeatureRequest() request.setFilterFids([intersects[0]]) fuels = [f for f in self._fuel_layer.getFeatures(request)] for fuel in fuels: if not fuel.id() in intersecting_fuels: intersecting_fuels[fuel.id()] = fuel intersecting_points.append(pt) if len(intersecting_fuels) > 0: for pt in intersecting_points: for k, feat in intersecting_fuels.items(): if feat.geometry().contains(pt.feature.geometry()): pt.fuelModel = feat.attributes()[1] break
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException( self.invalidSourceError(parameters, self.INPUT)) strategy = self.parameterAsEnum(parameters, self.STRATEGY, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) expression = QgsExpression( self.parameterAsString(parameters, self.EXPRESSION, context)) if expression.hasParserError(): raise QgsProcessingException(expression.parserErrorString()) expressionContext = self.createExpressionContext( parameters, context, source) expression.prepare(expressionContext) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs(), QgsFeatureSink.RegeneratePrimaryKey) if sink is None: raise QgsProcessingException( self.invalidSinkError(parameters, self.OUTPUT)) da = QgsDistanceArea() da.setSourceCrs(source.sourceCrs(), context.transformContext()) da.setEllipsoid(context.project().ellipsoid()) total = 100.0 / source.featureCount() if source.featureCount() else 0 current_progress = 0 for current, f in enumerate(source.getFeatures()): if feedback.isCanceled(): break if not f.hasGeometry(): continue current_progress = total * current feedback.setProgress(current_progress) expressionContext.setFeature(f) value = expression.evaluate(expressionContext) if expression.hasEvalError(): feedback.pushInfo( self.tr('Evaluation error for feature ID {}: {}').format( f.id(), expression.evalErrorString())) continue fGeom = f.geometry() engine = QgsGeometry.createGeometryEngine(fGeom.constGet()) engine.prepareGeometry() bbox = fGeom.boundingBox() if strategy == 0: pointCount = int(value) else: pointCount = int(round(value * da.measureArea(fGeom))) if pointCount == 0: feedback.pushInfo( "Skip feature {} as number of points for it is 0.".format( f.id())) continue index = QgsSpatialIndex() points = dict() nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 feature_total = total / pointCount if pointCount else 1 random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break rx = bbox.xMinimum() + bbox.width() * random.random() ry = bbox.yMinimum() + bbox.height() * random.random() p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if engine.contains(geom.constGet()) and \ vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(current_progress + int(nPoints * feature_total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo( self.tr('Could not generate requested number of random ' 'points. Maximum number of attempts exceeded.')) feedback.setProgress(100) return {self.OUTPUT: dest_id}
from qgis.core import QgsFeatureRequest import csv from qgis.utils import iface layer = iface.activeLayer() iter = layer.getFeatures() grid = [f for f in iter] groups = dict() file = open('/home3/jaume/farmacies-2020-03-14.csv') farmacies = csv.reader(file, delimiter=',') i = 0 index = QgsSpatialIndex() for feat in grid: index.addFeature(feat) groups[feat.id()] = list() for farmacia in farmacies: if i > 0: punt = QgsGeometry.fromPointXY( QgsPointXY(float(farmacia[16]), float(farmacia[17]))) intersects = index.intersects(punt.boundingBox()) if len(intersects) > 0: request = QgsFeatureRequest() request.setFilterFids(intersects) feats = [f for f in layer.getFeatures(request)] found = False for feat in feats: if punt.intersects(feat.geometry()): groups[feat.id()].append(farmacia)
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) MaxTriesPerPoint = self.parameterAsDouble(parameters, self.MAXTRIESPERPOINT, context) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs(), QgsFeatureSink.RegeneratePrimaryKey) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) totNPoints = 0 # The total number of points generated featureCount = source.featureCount() total = 100.0 / (pointCount * featureCount) if pointCount else 1 random.seed() index = QgsSpatialIndex() points = dict() da = QgsDistanceArea() da.setSourceCrs(source.sourceCrs(), context.transformContext()) da.setEllipsoid(context.project().ellipsoid()) maxIterations = pointCount * MaxTriesPerPoint for f in source.getFeatures(): lineGeoms = [] lineCount = 0 fGeom = f.geometry() feedback.pushInfo('fGeom: ' + str(fGeom)) totLineLength = da.measureLength(fGeom) feedback.pushInfo('fGeom totLineLength: ' + str(totLineLength)) # Explode multi part if fGeom.isMultipart(): for aLine in fGeom.asMultiPolyline(): lineGeoms.append(aLine) #lines = fGeom.asMultiPolyline() # pick random line #lineId = random.randint(0, len(lines) - 1) #vertices = lines[lineId] else: lineGeoms.append(fGeom.asPolyline()) #vertices = fGeom.asPolyline() feedback.pushInfo('lineGeoms: ' + str(lineGeoms)) # Generate points on the line geometry / geometries nPoints = 0 nIterations = 0 while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break #feedback.pushInfo('nIterations: ' + str(nIterations)) # Get the random "position" for this point randomLength = random.random() * totLineLength feedback.pushInfo('randomLength: ' + str(randomLength)) currLength = 0 prefLength = 0 # Go through the parts for l in lineGeoms: if feedback.isCanceled(): break currGeom = QgsGeometry.fromPolylineXY(l) #lineLength = da.measureLength(QgsGeometry.fromPolylineXY(l)) lineLength = da.measureLength(currGeom) prevLength = currLength currLength += lineLength feedback.pushInfo('l lineLength: ' + str(lineLength) + ' currLength: ' + str(currLength)) vertices = l # Skip if this is not the "selected" part if currLength < randomLength: continue #randomLength -= currLength #vertices = QgsGeometry.fromPolylineXY(l) feedback.pushInfo('l/vertices: ' + str(vertices)) #randomLength = random.random() * lineLength #distanceToVertex(vid) # find the segment for the new point # and calculate the offset (remainDistance) on that segment remainDist = randomLength - prevLength feedback.pushInfo('remainDist1: ' + str(remainDist)) if len(vertices) == 2: vid = 0 #remainDist = randomLength - currLength else: vid = 0 #while (fGeom.distanceToVertex(vid)) < randomLength: #while (currGeom.distanceToVertex(vid)) < randomLength: currDist = currGeom.distanceToVertex(vid) prevDist = currDist while currDist < remainDist and vid < len(vertices): vid += 1 prevDist = currDist currDist = currGeom.distanceToVertex(vid) feedback.pushInfo('currdist: ' + str(currDist) + ' vid: ' + str(vid)) if vid == len(vertices): feedback.pushInfo('**** vid = len(vertices)! ****') vid -= 1 feedback.pushInfo('currdist2: ' + str(currDist) + ' vid: ' + str(vid)) remainDist = remainDist - prevDist feedback.pushInfo('remainDist2: ' + str(remainDist)) if remainDist <= 0: continue startPoint = vertices[vid] endPoint = vertices[vid + 1] length = da.measureLine(startPoint, endPoint) # if remainDist > minDistance: d = remainDist / (length - remainDist) rx = (startPoint.x() + d * endPoint.x()) / (1 + d) ry = (startPoint.y() + d * endPoint.y()) / (1 + d) # generate random point p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(totNPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', totNPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 totNPoints += 1 feedback.setProgress(int(totNPoints * total)) break nIterations += 1 if nPoints < pointCount: #feedback.pushInfo(self.tr('Could not generate requested number of random points. ' feedback.reportError(self.tr('Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.'), False) return {self.OUTPUT: dest_id, self.OUTPUT_POINTS: nPoints}
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs()) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 featureCount = source.featureCount() total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() points = dict() da = QgsDistanceArea() da.setSourceCrs(source.sourceCrs(), context.transformContext()) da.setEllipsoid(context.project().ellipsoid()) request = QgsFeatureRequest() random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break # pick random feature fid = random.randint(0, featureCount - 1) f = next(source.getFeatures(request.setFilterFid(fid).setSubsetOfAttributes([]))) fGeom = f.geometry() if fGeom.isMultipart(): lines = fGeom.asMultiPolyline() # pick random line lineId = random.randint(0, len(lines) - 1) vertices = lines[lineId] else: vertices = fGeom.asPolyline() # pick random segment if len(vertices) == 2: vid = 0 else: vid = random.randint(0, len(vertices) - 2) startPoint = vertices[vid] endPoint = vertices[vid + 1] length = da.measureLine(startPoint, endPoint) dist = length * random.random() if dist > minDistance: d = dist / (length - dist) rx = (startPoint.x() + d * endPoint.x()) / (1 + d) ry = (startPoint.y() + d * endPoint.y()) / (1 + d) # generate random point p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(int(nPoints * total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo(self.tr('Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.')) return {self.OUTPUT: dest_id}
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) strategy = self.parameterAsEnum(parameters, self.STRATEGY, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) expression = QgsExpression(self.parameterAsString(parameters, self.EXPRESSION, context)) if expression.hasParserError(): raise QgsProcessingException(expression.parserErrorString()) expressionContext = self.createExpressionContext(parameters, context, source) expression.prepare(expressionContext) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs(), QgsFeatureSink.RegeneratePrimaryKey) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) da = QgsDistanceArea() da.setSourceCrs(source.sourceCrs(), context.transformContext()) da.setEllipsoid(context.project().ellipsoid()) total = 100.0 / source.featureCount() if source.featureCount() else 0 current_progress = 0 for current, f in enumerate(source.getFeatures()): if feedback.isCanceled(): break if not f.hasGeometry(): continue current_progress = total * current feedback.setProgress(current_progress) expressionContext.setFeature(f) value = expression.evaluate(expressionContext) if expression.hasEvalError(): feedback.pushInfo( self.tr('Evaluation error for feature ID {}: {}').format(f.id(), expression.evalErrorString())) continue fGeom = f.geometry() engine = QgsGeometry.createGeometryEngine(fGeom.constGet()) engine.prepareGeometry() bbox = fGeom.boundingBox() if strategy == 0: pointCount = int(value) else: pointCount = int(round(value * da.measureArea(fGeom))) if pointCount == 0: feedback.pushInfo("Skip feature {} as number of points for it is 0.".format(f.id())) continue index = QgsSpatialIndex() points = dict() nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 feature_total = total / pointCount if pointCount else 1 random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break rx = bbox.xMinimum() + bbox.width() * random.random() ry = bbox.yMinimum() + bbox.height() * random.random() p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if engine.contains(geom.constGet()) and \ vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(current_progress + int(nPoints * feature_total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo(self.tr('Could not generate requested number of random ' 'points. Maximum number of attempts exceeded.')) feedback.setProgress(100) return {self.OUTPUT: dest_id}
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): 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)) features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([])) total = 100.0 / source.featureCount() if source.featureCount() else 0 geoms = dict() null_geom_features = set() index = QgsSpatialIndex() for current, f in enumerate(features): if feedback.isCanceled(): break if not f.hasGeometry(): null_geom_features.add(f.id()) continue geoms[f.id()] = f.geometry() index.addFeature(f) feedback.setProgress(int(0.10 * current * total)) # takes about 10% of time # start by assuming everything is unique, and chop away at this list unique_features = dict(geoms) current = 0 removed = 0 for feature_id, geometry in geoms.items(): if feedback.isCanceled(): break if feature_id not in unique_features: # feature was already marked as a duplicate continue candidates = index.intersects(geometry.boundingBox()) candidates.remove(feature_id) for candidate_id in candidates: if candidate_id not in unique_features: # candidate already marked as a duplicate (not sure if this is possible, # since it would mean the current feature would also have to be a duplicate! # but let's be safe!) continue if geometry.isGeosEqual(geoms[candidate_id]): # candidate is a duplicate of feature del unique_features[candidate_id] removed += 1 current += 1 feedback.setProgress(int(0.80 * current * total) + 10) # takes about 80% of time # now, fetch all the feature attributes for the unique features only # be super-smart and don't re-fetch geometries distinct_geoms = set(unique_features.keys()) output_feature_ids = distinct_geoms.union(null_geom_features) total = 100.0 / len(output_feature_ids) if output_feature_ids else 1 request = QgsFeatureRequest().setFilterFids(list(output_feature_ids)).setFlags(QgsFeatureRequest.NoGeometry) for current, f in enumerate(source.getFeatures(request)): if feedback.isCanceled(): break # use already fetched geometry if f.id() not in null_geom_features: f.setGeometry(unique_features[f.id()]) sink.addFeature(f, QgsFeatureSink.FastInsert) feedback.setProgress(int(0.10 * current * total) + 90) # takes about 10% of time feedback.pushInfo(self.tr('{} duplicate features removed'.format(removed))) return {self.OUTPUT: dest_id, self.DUPLICATE_COUNT: removed, self.RETAINED_COUNT: len(output_feature_ids)}
class RasterHandler(QObject): """Raster layer handler.""" raster_changed = pyqtSignal(object) def __init__(self, layer, uc=None, debug=False): super(RasterHandler, self).__init__() self.layer = layer self.uc = uc self.logger = get_logger() if debug else None self.provider = layer.dataProvider() self.bands_nr = self.layer.bandCount() self.bands_range = range(1, self.bands_nr + 1) self.active_bands = [1] self.project = QgsProject.instance() self.crs_transform = None if self.project.crs() == self.layer.crs() else \ QgsCoordinateTransform(self.project.crs(), self.layer.crs(), self.project) self.data_types = None self.nodata_values = None self.pixel_size_x = self.layer.rasterUnitsPerPixelX() self.pixel_size_y = self.layer.rasterUnitsPerPixelY() self.raster_cols = self.layer.width() self.raster_rows = self.layer.height() self.layer_extent = self.provider.extent() self.min_x = self.layer_extent.xMinimum() self.min_y = self.layer_extent.yMinimum() self.max_x = self.layer_extent.xMaximum() self.max_y = self.layer_extent.yMaximum() self.origin_x = self.min_x self.origin_y = self.max_y self.first_pixel_x = self.min_x + self.pixel_size_x / 2. # x coord of upper left pixel center self.first_pixel_y = self.max_y - self.pixel_size_y / 2. # y self.cell_centers = None # dict of coordinates of currently selected cells centers {(row, col): (x, y)} self.cell_exp_val = None # dict of evaluated expressions for cells centers {(row, col): value} self.cell_pts_layer = None # point memory layer with selected cells centers self.selecting_geoms = None # dictionary of selecting geometries {id: geometry} self.spatial_index = None # spatial index of selecting geometries extents self.block_row_min = None # range of indices of the raster block to modify self.block_row_max = None self.block_col_min = None self.block_col_max = None self.selected_cells = None # list of selected cells as tuples of global indices (row, cell) self.selected_cells_feats = None # {(row, cell): feature} self.total_geometry = None self.all_touched_cells = None self.exp_field_idx = None self.get_data_types() self.get_nodata_values() def get_data_types(self): self.data_types = [] for nr in self.bands_range: self.data_types.append(self.provider.dataType(nr)) def write_supported(self): msg = "" supported = True for nr in self.bands_range: if self.provider.dataType( nr) == 0 or self.provider.dataType(nr) > 7: msg = f"{dtypes[self.provider.dataType(nr)]['name']} (band {nr})" supported = False return supported, msg def get_nodata_values(self): self.nodata_values = [] for nr in self.bands_range: if self.provider.sourceHasNoDataValue(nr): self.nodata_values.append(self.provider.sourceNoDataValue(nr)) self.provider.setUseSourceNoDataValue(nr, True) # no nodata defined in the raster source else: # check if user defined any nodata values if self.provider.userNoDataValues(nr): # get min nodata value from the first user nodata range nd_ranges = self.provider.userNoDataValues(nr) self.nodata_values.append(nd_ranges[0].min()) else: # leave nodata undefined self.nodata_values.append(None) def select(self, geometries, all_touched_cells=True, transform=True): """ For the geometries list, find selected cells. If all_touched_cells is True, all cells touching a geometry will be selected. Otherwise, a geometry must intersect a cell center to select it. """ if self.logger: self.logger.debug( f"Selecting cells for geometries: {[g.asWkt() for g in geometries]}" ) if not geometries: self.uc.bar_warn("Select some raster cells!") return self.selecting_geoms = dict() self.selected_cells = [] self.spatial_index = QgsSpatialIndex() self.total_geometry = QgsGeometry() dxy = 0.001 geoms = [] for nr, geom in enumerate(geometries): if not geom.isGeosValid(): continue sgeom = QgsGeometry(geom) if self.crs_transform and transform: try: res = sgeom.transform(self.crs_transform) if not res == QgsGeometry.Success: raise QgsCsException(repr(res)) except QgsCsException as err: msg = "Raster transformation failed! Check the raster projection settings." if self.uc: self.uc.bar_warn(msg, dur=5) msg += repr(err) if self.logger: self.logger.warning(msg) return self.selecting_geoms[nr] = sgeom self.spatial_index.addFeature(nr, sgeom.boundingBox()) geoms.append(sgeom) self.total_geometry = QgsGeometry.unaryUnion(geoms) if self.logger: self.logger.debug( f"Total selecting geometry bbox: {self.total_geometry.boundingBox()}" ) self.block_row_min, self.block_row_max, self.block_col_min, self.block_col_max = \ self.extent_to_cell_indices(self.total_geometry.boundingBox()) half_pix_x = self.pixel_size_x / 2. half_pix_y = self.pixel_size_y / 2. self.cell_centers = dict() for row in range(self.block_row_min, self.block_row_max + 1): for col in range(self.block_col_min, self.block_col_max + 1): pt_x = self.first_pixel_x + col * self.pixel_size_x pt_y = self.first_pixel_y - row * self.pixel_size_y if all_touched_cells: bbox = QgsRectangle(pt_x - half_pix_x, pt_y - half_pix_y, pt_x + half_pix_x, pt_y + half_pix_y) else: bbox = QgsRectangle(pt_x, pt_y, pt_x + dxy, pt_y + dxy) sel_inter = self.spatial_index.intersects(bbox) for sel_geom_id in sel_inter: g = self.selecting_geoms[sel_geom_id] if g.intersects(bbox): self.selected_cells.append((row, col)) self.cell_centers[(row, col)] = (pt_x, pt_y) if self.logger: self.logger.debug( f"Nr of cells selected: {len(self.selected_cells)}") def create_cell_pts_layer(self): """For current block extent, create memory point layer with a feature in each selected cell.""" crs_str = self.layer.crs().authid().lower() fields_def = "field=row:int&field=col:int" self.cell_pts_layer = QgsVectorLayer( f"Point?crs={crs_str}&{fields_def}", "Temp raster cell points", "memory") fields = self.cell_pts_layer.dataProvider().fields() feats = [] for row_col, xy in self.cell_centers.items(): row, col = row_col x, y = xy feat = QgsFeature(fields) feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y))) feat["row"] = row feat["col"] = col feats.append(feat) self.cell_pts_layer.dataProvider().addFeatures(feats) self.selected_cells_feats = dict() # {(row, cell): feat} for feat in self.cell_pts_layer.getFeatures(): self.selected_cells_feats[(feat["row"], feat["col"])] = feat.id() def write_block(self, const_values=None, low_pass_filter=False): """ Construct raster block for each band, apply the values and write to file. If const_values are given (a list of const values for each band) they are used for each selected cell. In other case the memory layer with values calculated for each cell selected will be used. Alternatively, selected cells values can be filtered using low-pass 3x3 filter. """ if self.logger: vals = f"const values ({const_values})" if const_values else "expression values." self.logger.debug(f"Writing blocks with {vals}") if not self.provider.isEditable(): res = self.provider.setEditable(True) if not res: if self.uc: self.uc.show_warn('QGIS can\'t modify this type of raster') return None if self.logger: self.logger.debug("Calculating block origin coordinates...") b_orig_x, b_orig_y = self.index_to_point(self.block_row_min, self.block_col_min) cols = self.block_col_max - self.block_col_min + 1 rows = self.block_row_max - self.block_row_min + 1 b_end_x = b_orig_x + cols * self.pixel_size_x b_end_y = b_orig_y - rows * self.pixel_size_y block_bbox = QgsRectangle(b_orig_x, b_end_y, b_end_x, b_orig_y) if self.logger: self.logger.debug(f"Block bbox: {block_bbox.toString()}") self.logger.debug( f"Nr of cells in the block: rows={rows}, cols={cols}") old_blocks = [] new_blocks = [] cell_values = dict() if const_values is None and not low_pass_filter: for feat in self.cell_pts_layer.getFeatures(): cell_values[feat.id()] = feat.attribute(self.exp_field_idx) for band_nr in self.active_bands: block = self.provider.block(band_nr, block_bbox, cols, rows) new_blocks.append(block) block_data = block.data().data() old_block = QgsRasterBlock(self.data_types[band_nr - 1], cols, rows) old_block.setData(block_data) for abs_row, abs_col in self.selected_cells: row = abs_row - self.block_row_min col = abs_col - self.block_col_min if const_values: idx = band_nr - 1 if len(self.active_bands) > 1 else 0 new_val = const_values[idx] elif low_pass_filter: # the filter is applied for cells inside the block only if block.height() < 3 or block.width() < 3: # the selected block is too small for filtering -> keep the old value new_val = None else: new_val = low_pass_filtered( old_block, row, col, self.nodata_values[band_nr - 1]) else: # set the expression value feat_id = self.selected_cells_feats[(abs_row, abs_col)] if cell_values[feat_id] is not None: new_val = None if math.isnan(cell_values[feat_id]) or \ cell_values[feat_id] is None else cell_values[feat_id] else: new_val = None new_val = old_block.value(row, col) if new_val is None else new_val set_res = block.setValue(row, col, new_val) if self.logger: self.logger.debug( f"Setting block value for band {band_nr}, row {row}, col: {col}: {set_res}" ) old_blocks.append(old_block) band_res = self.provider.writeBlock(block, band_nr, self.block_col_min, self.block_row_min) if self.logger: self.logger.debug( f"Writing block for band {band_nr}: {band_res}") self.provider.setEditable(False) change = RasterChange(self.active_bands, self.block_row_min, self.block_col_min, old_blocks, new_blocks) self.raster_changed.emit(change) return True def write_block_undo(self, data): """Write blocks from the undo / redo stack.""" if self.logger: self.logger.debug(f"Writing blocks from undo") if not self.provider.isEditable(): res = self.provider.setEditable(True) bands, row_min, col_min, blocks = data for band_nr in bands: idx = band_nr - 1 if len(bands) > 1 else 0 block = blocks[idx] band_res = self.provider.writeBlock(block, band_nr, col_min, row_min) if self.logger: self.logger.debug( f"Writing undo/redo block for band {band_nr}: {band_res}") self.provider.setEditable(False) def extent_to_cell_indices(self, extent): """Return x and y raster cell indices ranges for the extent.""" col_min, row_max = self.point_to_index( (extent.xMinimum(), extent.yMinimum())) col_max, row_min = self.point_to_index( (extent.xMaximum(), extent.yMaximum())) if self.logger: self.logger.debug( f"Cell ranges for extent {extent.toString(precision=3)} = row_min: {row_min}, " + f"row_max: {row_max}, col_min: {col_min}, col_max: {col_max}") return row_min, row_max, col_min, col_max def index_to_point(self, row, col, upper_left=True): """Return cell upper left corner or cell center coordinates.""" x0 = self.origin_x if upper_left else self.first_pixel_x y0 = self.origin_y if upper_left else self.first_pixel_y x, y = x0 + col * self.pixel_size_x, y0 - row * self.pixel_size_y if self.logger: self.logger.debug(f"Coords for ({row}, {col}) = ({x}, {y}) (x, y)") return x, y def point_to_index(self, coords): """ Return raster cell indices for the coordinates. If it falls outside of the layer extent, then the first or last index is returned. """ if self.origin_x <= coords[0] <= self.max_x: x_offset = coords[0] - self.origin_x col = math.floor(x_offset / self.pixel_size_x) elif coords[0] < self.origin_x: col = 0 else: col = self.raster_cols - 1 if self.min_y <= coords[1] <= self.origin_y: y_offset = self.origin_y - coords[1] row = math.floor(y_offset / self.pixel_size_y) elif coords[1] > self.origin_y: row = 0 else: row = self.raster_rows - 1 return col, row
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, # 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 index.addFeature(feature) roads[feature.id()] = feature feedback.setProgress(int(current * total)) total = 90.0 / len(roads) current = 0 removed = 0 for _id, f in roads.items(): if feedback.isCanceled(): break current += 1 if f.geometry().length() >= threshold: sink.addFeature(f, QgsFeatureSink.FastInsert) feedback.setProgress(10 + int(current * total)) continue # we mark identify a cross road because either side is touched by at least two other features # with matching identifier attributes candidate_attrs = [f.attributes()[i] for i in field_indices] touching_candidates = index.intersects(f.geometry().boundingBox()) if not f.geometry().isMultipart(): candidate = f.geometry().constGet().clone() else: if f.geometry().constGet().numGeometries() > 1: raise QgsProcessingException( self.tr('Only single-part geometries are supported')) candidate = f.geometry().constGet().geometryN(0).clone() candidate_start = candidate.startPoint() candidate_end = candidate.endPoint() start_engine = QgsGeometry.createGeometryEngine(candidate_start) end_engine = QgsGeometry.createGeometryEngine(candidate_end) touching_start_count = 0 touching_end_count = 0 for t in touching_candidates: if t == _id: continue other = roads[t] other_attrs = [other.attributes()[i] for i in field_indices] if other_attrs != candidate_attrs: continue if other.geometry().length() < threshold: continue if start_engine.intersects(roads[t].geometry().constGet()): touching_start_count += 1 if end_engine.intersects(roads[t].geometry().constGet()): touching_end_count += 1 if touching_start_count >= 2 and touching_end_count >= 2: break feedback.setProgress(10 + int(current * total)) if touching_start_count >= 2 and touching_end_count >= 2: # kill it removed += 1 else: sink.addFeature(f, QgsFeatureSink.FastInsert) feedback.pushInfo(self.tr('Removed {} cross roads'.format(removed))) 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) 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 index.addFeature(feature) roads[feature.id()] = feature feedback.setProgress(int(current * total)) total = 90.0 / len(roads) removed = 0 current = 0 for _id, f in roads.items(): if feedback.isCanceled(): break current += 1 if f.geometry().length() >= threshold: sink.addFeature(f, QgsFeatureSink.FastInsert) feedback.setProgress(10 + int(current * total)) continue touching_candidates = index.intersects(f.geometry().boundingBox()) if len(touching_candidates) == 1: # small street, touching nothing but itself -- kill it! removed += 1 feedback.setProgress(10 + int(current * total)) continue if not f.geometry().isMultipart(): candidate = f.geometry().constGet().clone() else: if f.geometry().constGet().numGeometries() > 1: raise QgsProcessingException( self.tr('Only single-part geometries are supported')) candidate = f.geometry().constGet().geometryN(0).clone() candidate_start = candidate.startPoint() candidate_end = candidate.endPoint() start_engine = QgsGeometry.createGeometryEngine(candidate_start) end_engine = QgsGeometry.createGeometryEngine(candidate_end) touching_start = False touching_end = False for t in touching_candidates: if t == _id: continue if start_engine.intersects(roads[t].geometry().constGet()): touching_start = True if end_engine.intersects(roads[t].geometry().constGet()): touching_end = True if touching_start and touching_end: break feedback.setProgress(10 + int(current * total)) if touching_start and touching_end: # keep it, it joins two roads sink.addFeature(f, QgsFeatureSink.FastInsert) continue removed += 1 feedback.pushInfo(self.tr('Removed {} cul-de-sacs'.format(removed))) return {self.OUTPUT: dest_id}
class ContourTool(object): def updateReference(self, referenceLayer): """ Updates the reference layer and updates the spatial index """ self.first_value = None self.reference = referenceLayer self.populateIndex() def populateIndex(self): """ Populates the spatial index """ #spatial index self.index = QgsSpatialIndex() for feat in self.reference.getFeatures(): self.index.addFeature(feat) def getCandidates(self, bbox): """ Gets candidates using the spatial index to speedup the process """ #features that might satisfy the query ids = self.index.intersects(bbox) candidates = [] for id in ids: candidates.append( next( self.reference.getFeatures( QgsFeatureRequest().setFilterFid(id)))) return candidates def getFeatures(self, geom): """ Gets the features that intersect geom to be updated """ #features that satisfy the query ret = [] rect = geom.boundingBox() candidates = self.getCandidates(rect) for candidate in candidates: featGeom = candidate.geometry() if featGeom.intersects(geom): ret.append(candidate) return ret def getKey(self, item): """ Gets the key """ return item[0] def sortFeatures(self, geom, features): """ Sorts features according to the distance """ #sorting by distance distances = [] firstPoint = geom.asPolyline()[0] pointGeom = QgsGeometry.fromPointXY(firstPoint) for intersected in features: intersection = geom.intersection(intersected.geometry()) if intersection.type() == QgsWkbTypes.PointGeometry: distance = intersection.distance(pointGeom) distances.append((distance, intersected)) ordered = sorted(distances, key=self.getKey) #returning a list of tuples (distance, feature) return ordered def reproject(self, geom, canvasCrs): """ Reprojects geom to the reference layer crs """ destCrs = self.reference.crs() if canvasCrs.authid() != destCrs.authid(): coordinateTransformer = QgsCoordinateTransform( canvasCrs, destCrs, QgsProject.instance()) geom.transform(coordinateTransformer) def setFirstValue(self, value): self.first_value = value def assignValues(self, attribute, pace, geom, canvasCrs): """ Assigns attribute values to all features that intersect geom. """ self.reproject(geom, canvasCrs) features = self.getFeatures(geom) if len(features) == 0: return -2 ordered = self.sortFeatures(geom, features) if len(ordered) == 0: return -1 self.reference.startEditing() #the first feature must have the initial value already assigned first_feature = ordered[0][1] #getting the filed index that must be updated fieldIndex = self.reference.fields().indexFromName(attribute) #getting the initial value first_value = first_feature.attribute(attribute) if not first_value: first_value_dlg = ContourValue(self) retorno = first_value_dlg.exec_() if self.first_value: id = first_feature.id() first_value = self.first_value if not self.reference.changeAttributeValue( id, fieldIndex, self.first_value): return 0 else: return -3 self.first_value = None for i in range(1, len(ordered)): #value to be adjusted value = first_value + pace * i #feature that will be updated feature = ordered[i][1] #feature id that will be updated id = feature.id() #actual update in the layer if not self.reference.changeAttributeValue(id, fieldIndex, value): return 0 return 1
def processAlgorithm(self, parameters, context, feedback): #pylint: disable=unused-argument,missing-docstring layer = self.parameterAsSource(parameters, self.INPUT, context) # Step 1 feedback.setProgressText(self.tr("[1/4] Get Line Endpoints ...")) total = 100.0 / layer.featureCount() if layer.featureCount() else 0 coordinates = list() @simple_linestring_op def extract_coordinates(polyline): """ Extract endpoints coordinates """ a = polyline[0] b = polyline[-1] coordinates.append(tuple(a)) coordinates.append(tuple(b)) for current, feature in enumerate(layer.getFeatures()): if feedback.isCanceled(): break extract_coordinates(feature.geometry()) feedback.setProgress(int(total * current)) # Step 2 feedback.setProgressText(self.tr("[2/4] Quantize coordinates ...")) coordinates = np.array(coordinates) minx = np.min(coordinates[:, 0]) miny = np.min(coordinates[:, 1]) maxx = np.max(coordinates[:, 0]) maxy = np.max(coordinates[:, 1]) quantization = 1e8 kx = (minx == maxx) and 1 or (maxx - minx) ky = (miny == maxy) and 1 or (maxy - miny) sx = kx / quantization sy = ky / quantization coordinates = np.int32( np.round((coordinates - (minx, miny)) / (sx, sy))) # Step 3 feedback.setProgressText(self.tr("[3/4] Build Endpoints Index ...")) fields = QgsFields() fields.append(QgsField('GID', type=QVariant.Int, len=10)) (sink, nodes_id) = self.parameterAsSink(parameters, self.NODES, context, fields, QgsWkbTypes.Point, layer.sourceCrs()) point_index = QgsSpatialIndex() # point_list = list() coordinates_map = dict() gid = 0 total = 100.0 / len(coordinates) for i, coordinate in enumerate(coordinates): if feedback.isCanceled(): break c = tuple(coordinate) if c not in coordinates_map: coordinates_map[c] = i # point_list.append(c) geometry = QgsGeometry.fromPointXY( QgsPointXY(c[0] * sx + minx, c[1] * sy + miny)) point_feature = QgsFeature() point_feature.setId(gid) point_feature.setAttributes([gid]) point_feature.setGeometry(geometry) point_index.addFeature(point_feature) sink.addFeature(point_feature) gid = gid + 1 feedback.setProgress(int(total * i)) del coordinates del coordinates_map # Step 4 feedback.setProgressText( self.tr("[4/4] Output Lines with Node Attributes ...")) fields = QgsFields(layer.fields()) fields.append(QgsField('NODEA', QVariant.Int, len=10)) fields.append(QgsField('NODEB', QVariant.Int, len=10)) (sink, output_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, layer.wkbType(), layer.sourceCrs()) def nearest(point): """ Return the nearest point in the point index """ for candidate in point_index.nearestNeighbor(point, 1): return candidate return None @simple_linestring_op def output_simple_features(polyline, feature): """ Split multi-polylines into simple polyline if required, match endpoints into the node index, and output one or more stream features with node attributes """ a = polyline[0] b = polyline[-1] simple_geom = QgsGeometry.fromPolylineXY(polyline) out_feature = QgsFeature() out_feature.setGeometry(simple_geom) out_feature.setAttributes(feature.attributes() + [nearest(a), nearest(b)]) sink.addFeature(out_feature) total = 100.0 / layer.featureCount() if layer.featureCount() else 0 for current, feature in enumerate(layer.getFeatures()): if feedback.isCanceled(): break geom = feature.geometry() if not geom.isMultipart(): polyline = geom.asPolyline() a = polyline[0] b = polyline[-1] out_feature = QgsFeature() out_feature.setGeometry(geom) out_feature.setAttributes(feature.attributes() + [nearest(a), nearest(b)]) sink.addFeature(out_feature) else: output_simple_features(geom, feature) feedback.setProgress(int(total * current)) return {self.OUTPUT: output_id, self.NODES: nodes_id}
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException( self.invalidSourceError(parameters, self.INPUT)) proximity = self.parameterAsDouble(parameters, self.PROXIMITY, context) radius = self.parameterAsDouble(parameters, self.DISTANCE, context) horizontal = self.parameterAsBool(parameters, self.HORIZONTAL, context) (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)) features = source.getFeatures() total = 100.0 / source.featureCount() if source.featureCount() else 0 def searchRect(p): return QgsRectangle(p.x() - proximity, p.y() - proximity, p.x() + proximity, p.y() + proximity) index = QgsSpatialIndex() # NOTE: this is a Python port of QgsPointDistanceRenderer::renderFeature. If refining this algorithm, # please port the changes to QgsPointDistanceRenderer::renderFeature also! clustered_groups = [] group_index = {} group_locations = {} for current, f in enumerate(features): if feedback.isCanceled(): break if not f.hasGeometry(): continue point = f.geometry().asPoint() other_features_within_radius = index.intersects(searchRect(point)) if not other_features_within_radius: index.addFeature(f) group = [f] clustered_groups.append(group) group_index[f.id()] = len(clustered_groups) - 1 group_locations[f.id()] = point else: # find group with closest location to this point (may be more than one within search tolerance) min_dist_feature_id = other_features_within_radius[0] min_dist = group_locations[min_dist_feature_id].distance(point) for i in range(1, len(other_features_within_radius)): candidate_id = other_features_within_radius[i] new_dist = group_locations[candidate_id].distance(point) if new_dist < min_dist: min_dist = new_dist min_dist_feature_id = candidate_id group_index_pos = group_index[min_dist_feature_id] group = clustered_groups[group_index_pos] # calculate new centroid of group old_center = group_locations[min_dist_feature_id] group_locations[min_dist_feature_id] = QgsPointXY( (old_center.x() * len(group) + point.x()) / (len(group) + 1.0), (old_center.y() * len(group) + point.y()) / (len(group) + 1.0)) # add to a group clustered_groups[group_index_pos].append(f) group_index[f.id()] = group_index_pos feedback.setProgress(int(current * total)) current = 0 total = 100.0 / len(clustered_groups) if clustered_groups else 1 feedback.setProgress(0) fullPerimeter = 2 * math.pi for group in clustered_groups: if feedback.isCanceled(): break count = len(group) if count == 1: sink.addFeature(group[0], QgsFeatureSink.FastInsert) else: angleStep = fullPerimeter / count if count == 2 and horizontal: currentAngle = math.pi / 2 else: currentAngle = 0 old_point = group_locations[group[0].id()] for f in group: if feedback.isCanceled(): break sinusCurrentAngle = math.sin(currentAngle) cosinusCurrentAngle = math.cos(currentAngle) dx = radius * sinusCurrentAngle dy = radius * cosinusCurrentAngle # we want to keep any existing m/z values point = f.geometry().constGet().clone() point.setX(old_point.x() + dx) point.setY(old_point.y() + dy) f.setGeometry(QgsGeometry(point)) sink.addFeature(f, QgsFeatureSink.FastInsert) currentAngle += angleStep current += 1 feedback.setProgress(int(current * total)) return {self.OUTPUT: dest_id}
def _append_pipes(self, params, out): out.extend(InpFile.build_section_keyword(Pipe.section_name)) out.append(InpFile.build_section_header(Pipe.section_header)) # out.append(InpFile.build_dashline(Pipe.section_header)) pipe_fts = params.pipes_vlay.getFeatures() # Build nodes spatial index sindex = QgsSpatialIndex() for feat in params.junctions_vlay.getFeatures(): #sindex.insertFeature(feat) sindex.addFeature(feat) for feat in params.reservoirs_vlay.getFeatures(): #sindex.insertFeature(feat) sindex.addFeature(feat) for feat in params.tanks_vlay.getFeatures(): #sindex.insertFeature(feat) sindex.addFeature(feat) for pipe_ft in pipe_fts: eid = pipe_ft.attribute(Pipe.field_name_eid) # Find start/end nodes # adj_nodes = NetworkUtils.find_start_end_nodes(params, pipe_ft.geometry()) adj_nodes = NetworkUtils.find_start_end_nodes_sindex( params, sindex, pipe_ft.geometry()) start_node_id = adj_nodes[0].attribute(Junction.field_name_eid) end_node_id = adj_nodes[1].attribute(Junction.field_name_eid) length = pipe_ft.attribute(Pipe.field_name_length) #length_units = pipe_ft.attribute(QPipe.field_name_length_units) diameter = pipe_ft.attribute(Pipe.field_name_diameter) #diameter_units = pipe_ft.attribute(QPipe.field_name_diameter_units) roughness = pipe_ft.attribute(Pipe.field_name_roughness) minor_loss = pipe_ft.attribute(Pipe.field_name_minor_loss) status = pipe_ft.attribute(Pipe.field_name_status) description = pipe_ft.attribute(Pipe.field_name_description) tag_name = pipe_ft.attribute(Pipe.field_name_tag) #num_edu = int(pipe_ft.attribute(QPipe.field_name_num_edu)) #zone_id = int(pipe_ft.attribute(QPipe.field_name_zone_id)) #velocity = float(pipe_ft.attribute(QPipe.field_name_velocity)) #velocity_units = pipe_ft.attribute(QPipe.field_name_velocity_units) #friction_loss = float(pipe_ft.attribute(QPipe.field_name_frictionloss)) #friction_loss_units = pipe_ft.attribute(QPipe.field_name_frictionloss_units) # Line line = InpFile.pad(eid, InpFile.pad_19) line += InpFile.pad(start_node_id, InpFile.pad_19) line += InpFile.pad(end_node_id, InpFile.pad_19) if not isinstance(length, unicode): line += InpFile.pad('{0:.2f}'.format(length), InpFile.pad_19) else: line += InpFile.pad(length, InpFile.pad_19) #line += InpFile.pad(length_units, InpFile.pad_19) if not isinstance(diameter, unicode): line += InpFile.pad('{0:.2f}'.format(diameter), InpFile.pad_19) else: line += InpFile.pad(diameter, InpFile.pad_19) #line += InpFile.pad(diameter_units, InpFile.pad_19) if not isinstance(roughness, unicode): line += InpFile.pad('{0:.2f}'.format(roughness), InpFile.pad_19) else: line += InpFile.pad(roughness, InpFile.pad_19) if not isinstance(minor_loss, unicode): line += InpFile.pad('{0:.2f}'.format(minor_loss), InpFile.pad_19) else: line += InpFile.pad(minor_loss, InpFile.pad_19) line += InpFile.pad(status, InpFile.pad_19) #line += InpFile.pad(num_edu, InpFile.pad_19) #line += InpFile.pad(zone_id, InpFile.pad_19) #line += InpFile.pad(velocity, InpFile.pad_19) #line += InpFile.pad(velocity_units, InpFile.pad_19) #line += InpFile.pad(frictionloss, InpFile.pad_19) #line += InpFile.pad(frictionloss_units, InpFile.pad_19) if description is not None and description != '': line += ';' + description if tag_name is not None and tag_name != NULL and tag_name != '': self.tags.append(Tag(Tag.element_type_link, eid, tag_name)) #if not isinstance(num_edu, unicode): # line += InpFile.pad('{0:.2f}'.format(num_edu), InpFile.pad_19) #else: # line += InpFile.pad(num_edu, InpFile.pad_19) # # if zone_id != NULL: # if not isinstance(zone_id, unicode): # line += InpFile.pad('{0:.2f}'.format(zone_id), InpFile.pad_19) # else: # line += InpFile.pad(zone_id, InpFile.pad_19) # else: # zone_id = 0 out.append(line)
def processAlgorithm(self, 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)) features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes([])) total = 100.0 / source.featureCount() if source.featureCount() else 0 geoms = dict() index = QgsSpatialIndex() for current, f in enumerate(features): if feedback.isCanceled(): break geoms[f.id()] = f.geometry() index.addFeature(f) feedback.setProgress(int(0.10 * current * total)) # takes about 10% of time # start by assuming everything is unique, and chop away at this list unique_features = dict(geoms) current = 0 for feature_id, geometry in geoms.items(): if feedback.isCanceled(): break if feature_id not in unique_features: # feature was already marked as a duplicate continue candidates = index.intersects(geometry.boundingBox()) candidates.remove(feature_id) for candidate_id in candidates: if candidate_id not in unique_features: # candidate already marked as a duplicate (not sure if this is possible, # since it would mean the current feature would also have to be a duplicate! # but let's be safe!) continue if geometry.isGeosEqual(geoms[candidate_id]): # candidate is a duplicate of feature del unique_features[candidate_id] current += 1 feedback.setProgress(int(0.80 * current * total) + 10) # takes about 80% of time total = 100.0 / len(unique_features) if unique_features else 1 # now, fetch all the feature attributes for the unique features only # be super-smart and don't re-fetch geometries request = QgsFeatureRequest().setFilterFids(list(unique_features.keys())).setFlags(QgsFeatureRequest.NoGeometry) for current, f in enumerate(source.getFeatures(request)): if feedback.isCanceled(): break # use already fetched geometry f.setGeometry(unique_features[f.id()]) sink.addFeature(f, QgsFeatureSink.FastInsert) feedback.setProgress(int(0.10 * current * total) + 90) # takes about 10% of time return {self.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, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException( self.invalidSourceError(parameters, self.INPUT)) pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) bbox = source.sourceExtent() sourceIndex = QgsSpatialIndex(source, feedback) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs()) if sink is None: raise QgsProcessingException( self.invalidSinkError(parameters, self.OUTPUT)) nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() points = dict() random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break rx = bbox.xMinimum() + bbox.width() * random.random() ry = bbox.yMinimum() + bbox.height() * random.random() p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) ids = sourceIndex.intersects(geom.buffer(5, 5).boundingBox()) if len(ids) > 0 and \ vector.checkMinDistance(p, index, minDistance, points): request = QgsFeatureRequest().setFilterFids( ids).setSubsetOfAttributes([]) for f in source.getFeatures(request): if feedback.isCanceled(): break tmpGeom = f.geometry() if geom.within(tmpGeom): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(int(nPoints * total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo( self.tr( 'Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.')) return {self.OUTPUT: dest_id}
class GsCollection: """Class used for managing the QgsFeature spatially. QgsSpatialIndex class is used to store and retrieve the features. """ __slots__ = ('_spatial_index', '_dict_qgs_segment', '_id_qgs_segment') def __init__(self): """Constructor that initialize the GsCollection. """ self._spatial_index = QgsSpatialIndex() self._dict_qgs_segment = { } # Contains a reference to the original geometry self._id_qgs_segment = 0 def _get_next_id_segment(self): """Increment the id of the segment. :return: Value of the next ID :rtype: int """ self._id_qgs_segment += 1 return self._id_qgs_segment def _create_rectangle(self, geom_id, qgs_geom): """Creates a new QgsRectangle to load in the QgsSpatialIndex. :param: geom_id: Integer ID of the geometry :param: qgs_geom: QgsGeometry to use for bounding box extraction :return: The feature created :rtype: QgsFeature """ id_segment = self._get_next_id_segment() self._dict_qgs_segment[id_segment] = ( geom_id, qgs_geom) # Reference to the RbGeom ID and geometry return id_segment, qgs_geom.boundingBox() def add_features(self, rb_geoms, feedback): """Add a RbGeom object in the spatial index. For the LineString geometries. The geometry is broken into each line segment that are individually loaded in the QgsSpatialIndex. This strategy accelerate the validation of the spatial constraints. :param: rb_geoms: List of RbGeom to load in the QgsSpatialIndex :feedback: QgsFeedback handle used to update the progress bar """ progress_bar = ProgressBar(feedback, len(rb_geoms), "Building internal structure...") for val, rb_geom in enumerate(rb_geoms): progress_bar.set_value(val) qgs_rectangles = [] if rb_geom.qgs_geom.wkbType() == QgsWkbTypes.Point: qgs_rectangles.append( self._create_rectangle(rb_geom.id, rb_geom.qgs_geom)) else: qgs_points = rb_geom.qgs_geom.constGet().points() for i in range(0, (len(qgs_points) - 1)): qgs_geom = QgsGeometry( QgsLineString(qgs_points[i], qgs_points[i + 1])) qgs_rectangles.append( self._create_rectangle(rb_geom.id, qgs_geom)) for geom_id, qgs_rectangle in qgs_rectangles: self._spatial_index.addFeature(geom_id, qgs_rectangle) return def get_segment_intersect(self, qgs_geom_id, qgs_rectangle, qgs_geom_subline): """Find the feature that intersects the bounding box. Once the line string intersecting the bounding box are found. They are separated into 2 lists. The first one being the line string with the same id (same line) the second one all the others line string. :param qgs_geom_id: ID of the line string that is being simplified :param qgs_rectangle: QgsRectangle used for feature intersection :param qgs_geom_subline: LineString used to remove line segment superimposed to this line string :return: Two lists of line string segment. First: Line string with same id; Second all the others :rtype: tuple of 2 lists """ qgs_geoms_with_itself = [] qgs_geoms_with_others = [] qgs_rectangle.grow( Epsilon.ZERO_RELATIVE * 100.) # Always increase the b_box to avoid degenerated b_box ids = self._spatial_index.intersects(qgs_rectangle) for geom_id in ids: target_qgs_geom_id, target_qgs_geom = self._dict_qgs_segment[ geom_id] if target_qgs_geom_id is None: # Nothing to do; segment was deleted pass else: if target_qgs_geom_id == qgs_geom_id: # Test that the segment is not part of qgs_subline if not target_qgs_geom.within(qgs_geom_subline): qgs_geoms_with_itself.append(target_qgs_geom) else: qgs_geoms_with_others.append(target_qgs_geom) return qgs_geoms_with_itself, qgs_geoms_with_others def _delete_segment(self, qgs_geom_id, qgs_pnt0, qgs_pnt1): """Delete a line segment in the spatial index based on start/end points. To minimise the number of feature returned we search for a very small bounding box located in the middle of the line segment. Usually only one line segment is returned. :param qgs_geom_id: Integer ID of the geometry :param qgs_pnt0 : QgsPoint start point of the target line segment. :param qgs_pnt1 : QgsPoint end point of the target line segment. """ qgs_geom_to_delete = QgsGeometry(QgsLineString(qgs_pnt0, qgs_pnt1)) qgs_mid_point = QgsGeometryUtils.midpoint(qgs_pnt0, qgs_pnt1) qgs_rectangle = qgs_mid_point.boundingBox() qgs_rectangle.grow(Epsilon.ZERO_RELATIVE * 100) deleted = False ids = self._spatial_index.intersects(qgs_rectangle) for geom_id in ids: target_qgs_geom_id, target_qgs_geom = self._dict_qgs_segment[ geom_id] # Extract id and geometry if qgs_geom_id == target_qgs_geom_id: # Only check for the same ID if target_qgs_geom.equals( qgs_geom_to_delete): # Check if it's the same geometry deleted = True self._dict_qgs_segment[geom_id] = ( None, None) # Delete from the internal structure break if not deleted: raise Exception( QgsProcessingException("Internal structure corruption...")) return def _delete_vertex(self, rb_geom, v_id_start, v_id_end): """Delete consecutive vertex in the line and update the spatial index. When a vertex in a line string is deleted. Two line segments are deleted and one line segment is created in the spatial index. Cannot delete the first/last vertex of a line string :param rb_geom: LineString object to update. :param v_id_start: start of the vertex to delete. :param v_id_end: end of the vertex to delete. """ is_closed = rb_geom.qgs_geom.constGet().isClosed() v_ids_to_del = list(range(v_id_start, v_id_end + 1)) if v_id_start == 0 and is_closed: # Special case for closed line where we simulate a circular array nbr_vertice = rb_geom.qgs_geom.constGet().numPoints() v_ids_to_del.insert(0, nbr_vertice - 2) else: v_ids_to_del.insert(0, v_ids_to_del[0] - 1) v_ids_to_del.append(v_ids_to_del[-1] + 1) # Delete the line segment in the spatial index for i in range(len(v_ids_to_del) - 1): qgs_pnt0 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[i]) qgs_pnt1 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[i + 1]) self._delete_segment(rb_geom.id, qgs_pnt0, qgs_pnt1) # Add the new line segment in the spatial index qgs_pnt0 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[0]) qgs_pnt1 = rb_geom.qgs_geom.vertexAt(v_ids_to_del[-1]) qgs_geom_segment = QgsGeometry(QgsLineString(qgs_pnt0, qgs_pnt1)) geom_id, qgs_rectangle = self._create_rectangle( rb_geom.id, qgs_geom_segment) self._spatial_index.addFeature(geom_id, qgs_rectangle) # Delete the vertex in the line string geometry for v_id_to_del in reversed(range(v_id_start, v_id_end + 1)): rb_geom.qgs_geom.deleteVertex(v_id_to_del) if v_id_start == 0 and is_closed: # Special case for closed line where we simulate a circular array nbr_vertice = rb_geom.qgs_geom.constGet().numPoints() qgs_pnt_first = rb_geom.qgs_geom.vertexAt(0) rb_geom.qgs_geom.insertVertex(qgs_pnt_first, nbr_vertice - 1) rb_geom.qgs_geom.deleteVertex(nbr_vertice) return def delete_vertex(self, rb_geom, v_id_start, v_id_end): """Manage deletion of consecutives vertex. If v_id_start is greater than v_id_end the delete is broken into up to 3 calls :param rb_geom: LineString object to update. :param v_id_start: start of the vertex to delete. :param v_id_end: end of the vertex to delete. """ num_points = rb_geom.qgs_geom.constGet().numPoints() # Manage closes line where first/last vertice are the same if v_id_start == num_points - 1: v_id_start = 0 # Last point is the same as the first vertice if v_id_end == -1: v_id_end = num_points - 2 # Preceding point the first/last vertice if v_id_start <= v_id_end: self._delete_vertex(rb_geom, v_id_start, v_id_end) else: self._delete_vertex(rb_geom, v_id_start, num_points - 2) self._delete_vertex(rb_geom, 0, 0) if v_id_end > 0: self._delete_vertex(rb_geom, 1, v_id_end) # lst_vertex_to_del = list(range(v_id_start, num_points)) + list(range(0, v_id_end+1)) # for vertex_to_del in lst_vertex_to_del: # self._delete_vertex(rb_geom, vertex_to_del, vertex_to_del) # num_points = rb_geom.qgs_geom.constGet().numPoints() # lst_vertex_to_del = list(range(v_id_start, num_points)) + list(range(0, v_id_end + 1)) # for vertex_to_del in lst_vertex_to_del: # self._delete_vertex(rb_geom, vertex_to_del, vertex_to_del) def add_vertex(self, rb_geom, bend_i, bend_j, qgs_geom_new_subline): """Update the line segment in the spatial index :param rb_geom: RbGeom line to update :param bend_i: Start of the bend to delete :param bend_j: End of the bend to delete (always bend_i + 1) :param qgs_geom_new_subline: New sub line string to add in the spatial index :return: """ # Delete the base of the bend qgs_pnt0 = rb_geom.qgs_geom.vertexAt(bend_i) qgs_pnt1 = rb_geom.qgs_geom.vertexAt(bend_j) self._delete_segment(rb_geom.id, qgs_pnt0, qgs_pnt1) qgs_points = qgs_geom_new_subline.constGet().points() tmp_qgs_points = qgs_points[1:-1] # Drop first/last item # Insert the new vertex in the QgsGeometry. Work reversely to facilitate insertion for qgs_point in reversed(tmp_qgs_points): rb_geom.qgs_geom.insertVertex(qgs_point, bend_j) # Add the new segment in the spatial container for i in range(len(qgs_points) - 1): qgs_geom_segment = QgsGeometry( QgsLineString(qgs_points[i], qgs_points[i + 1])) geom_id, qgs_rectangle = self._create_rectangle( rb_geom.id, qgs_geom_segment) self._spatial_index.addFeature(geom_id, qgs_rectangle) return def validate_integrity(self, rb_geoms): """This method is used to validate the data structure at the end of the process This method is executed only when requested and for debug purpose only. It's validating the data structure by removing element from it the data structure is unusable after. Validate integrity must be the last operation before ending the program as it destroy the data structure... :param rb_geoms: Geometry contained in the spatial container :return: Flag indicating if the structure is valid. True: is valid; False: is not valid :rtype: Boolean """ is_structure_valid = True # from the geometry remove all the segment in the spatial index. for rb_geom in rb_geoms: qgs_line_string = rb_geom.qgs_geom.constGet() if qgs_line_string.wkbType() == QgsWkbTypes.LineString: qgs_points = qgs_line_string.points() for i in range(len(qgs_points) - 1): self._delete_segment(rb_geom.id, qgs_points[i], qgs_points[i + 1]) if is_structure_valid: # Verify that there are no other feature in the spatial index; except for QgsPoint qgs_rectangle = QgsRectangle(-sys.float_info.max, -sys.float_info.max, sys.float_info.max, sys.float_info.max) feat_ids = self._spatial_index.intersects(qgs_rectangle) for feat_id in feat_ids: qgs_geom = self._spatial_index.geometry(feat_id) if qgs_geom.wkbType() == QgsWkbTypes.Point: pass else: # Error is_structure_valid = False return is_structure_valid
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) proximity = self.parameterAsDouble(parameters, self.PROXIMITY, context) radius = self.parameterAsDouble(parameters, self.DISTANCE, context) horizontal = self.parameterAsBool(parameters, self.HORIZONTAL, context) (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)) features = source.getFeatures() total = 100.0 / source.featureCount() if source.featureCount() else 0 def searchRect(p): return QgsRectangle(p.x() - proximity, p.y() - proximity, p.x() + proximity, p.y() + proximity) index = QgsSpatialIndex() # NOTE: this is a Python port of QgsPointDistanceRenderer::renderFeature. If refining this algorithm, # please port the changes to QgsPointDistanceRenderer::renderFeature also! clustered_groups = [] group_index = {} group_locations = {} for current, f in enumerate(features): if feedback.isCanceled(): break if not f.hasGeometry(): continue point = f.geometry().asPoint() other_features_within_radius = index.intersects(searchRect(point)) if not other_features_within_radius: index.addFeature(f) group = [f] clustered_groups.append(group) group_index[f.id()] = len(clustered_groups) - 1 group_locations[f.id()] = point else: # find group with closest location to this point (may be more than one within search tolerance) min_dist_feature_id = other_features_within_radius[0] min_dist = group_locations[min_dist_feature_id].distance(point) for i in range(1, len(other_features_within_radius)): candidate_id = other_features_within_radius[i] new_dist = group_locations[candidate_id].distance(point) if new_dist < min_dist: min_dist = new_dist min_dist_feature_id = candidate_id group_index_pos = group_index[min_dist_feature_id] group = clustered_groups[group_index_pos] # calculate new centroid of group old_center = group_locations[min_dist_feature_id] group_locations[min_dist_feature_id] = QgsPointXY((old_center.x() * len(group) + point.x()) / (len(group) + 1.0), (old_center.y() * len(group) + point.y()) / (len(group) + 1.0)) # add to a group clustered_groups[group_index_pos].append(f) group_index[f.id()] = group_index_pos feedback.setProgress(int(current * total)) current = 0 total = 100.0 / len(clustered_groups) if clustered_groups else 1 feedback.setProgress(0) fullPerimeter = 2 * math.pi for group in clustered_groups: if feedback.isCanceled(): break count = len(group) if count == 1: sink.addFeature(group[0], QgsFeatureSink.FastInsert) else: angleStep = fullPerimeter / count if count == 2 and horizontal: currentAngle = math.pi / 2 else: currentAngle = 0 old_point = group_locations[group[0].id()] for f in group: if feedback.isCanceled(): break sinusCurrentAngle = math.sin(currentAngle) cosinusCurrentAngle = math.cos(currentAngle) dx = radius * sinusCurrentAngle dy = radius * cosinusCurrentAngle # we want to keep any existing m/z values point = f.geometry().constGet().clone() point.setX(old_point.x() + dx) point.setY(old_point.y() + dy) f.setGeometry(QgsGeometry(point)) sink.addFeature(f, QgsFeatureSink.FastInsert) currentAngle += angleStep current += 1 feedback.setProgress(int(current * total)) return {self.OUTPUT: dest_id}
def initialize_from_qepanet(self): """ get system details from the qgis vector layers (a lot of this taken from in_writer.py in qepanet) """ #Initalize data Matrices and vectors for calculations C = [ ] #Connection matrix: describes how each pipe the pipes are connected together Pipe_props_raw = [ ] #Pipe Property Matrix: stores properties of each pipes as # [ id ] Pipe_nodes_raw = [ ] #Pipe Nodes: stores pipe start and end nodes to build thet connection matrix Node_props_raw = [ ] #Node Property Matrix: stores properties of each junction as # [ id, elev, zone_end ] # Build nodes spatial index sindex = QgsSpatialIndex() res = [] res_elev = [] j = 0 r = 0 t = 0 pipe_fts = self.qgisparams.pipes_vlay.getFeatures() junc_fts = self.qgisparams.junctions_vlay.getFeatures() res_fts = self.qgisparams.reservoirs_vlay.getFeatures() #tank_fts = self.qgisparams.tanks_vlay.getFeatures() #pump_fts = self.qgisparams.pumps_vlay.getFeatures() for feat in junc_fts: sindex.addFeature(feat) j += 1 for feat in res_fts: sindex.addFeature(feat) res.append(feat.attribute(Reservoir.field_name_eid)) res_elev.append(feat.attribute(Reservoir.field_name_elev)) r += 1 #logger.progress("Reservoir: "+str(res)) # for feat in tank_fts: # sindex.addFeature(feat) # t+=1 if len( res ) != 1: #check to see if there is a single reservoir. Analysis assumes that all end nodes are pumps pump to a single outlet logger.error( "The number of reserviors in the network must equal 1.") for pipe_ft in pipe_fts: eid = pipe_ft.attribute(Pipe.field_name_eid) # Find start/end nodes # adj_nodes = NetworkUtils.find_start_end_nodes(params, pipe_ft.geometry()) adj_nodes = NetworkUtils.find_start_end_nodes_sindex( self.qgisparams, sindex, pipe_ft.geometry()) start_node_id = adj_nodes[0].attribute(Junction.field_name_eid) end_node_id = adj_nodes[1].attribute(Junction.field_name_eid) Pipe_nodes_raw.append([eid, start_node_id, end_node_id]) #length = pipe_ft.attribute(Pipe.field_name_length) #diameter = pipe_ft.attribute(Pipe.field_name_diameter) #roughness = pipe_ft.attribute(Pipe.field_name_roughness) #minor_loss = pipe_ft.attribute(Pipe.field_name_minor_loss) #status = pipe_ft.attribute(Pipe.field_name_status) #description = pipe_ft.attribute(Pipe.field_name_description) #tag_name = pipe_ft.attribute(Pipe.field_name_tag) #num_edu = pipe_ft.attribute(Pipe.field_name_num_edu) Pipe_props_raw.append([eid]) #reset the iterator for junc_fts junc_fts = self.qgisparams.junctions_vlay.getFeatures() for junc_ft in junc_fts: eid = junc_ft.attribute(Junction.field_name_eid) elev = junc_ft.attribute(Junction.field_name_elev) zone_end = junc_ft.attribute(QJunction.field_name_zone_end) Node_props_raw.append([eid, elev, zone_end]) Pipe_nodes = pd.DataFrame( Pipe_nodes_raw, columns=['Pipe ID', 'Node 1 ID', 'Node 2 ID']) Pipe_props = pd.DataFrame(Pipe_props_raw, columns=['Pipe ID'], dtype='float') Node_props = pd.DataFrame( Node_props_raw, columns=['Node ID', 'Elevation [ft]', 'Zone End'], dtype='float') #logger.progress("Node_props:\n", Node_props) #logger.progress(str(Pipe_nodes)) #logger.progress("Node_props: \n", Node_props) #logger.progress(Node_props.dtypes) #logger.progress("'Pipe_nodes' and 'Pipe_props' vectors populated...") return [Pipe_props, Node_props, Pipe_nodes, res, res_elev]
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) bbox = source.sourceExtent() sourceIndex = QgsSpatialIndex(source, feedback) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs()) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() points = dict() random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break rx = bbox.xMinimum() + bbox.width() * random.random() ry = bbox.yMinimum() + bbox.height() * random.random() p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) ids = sourceIndex.intersects(geom.buffer(5, 5).boundingBox()) if len(ids) > 0 and \ vector.checkMinDistance(p, index, minDistance, points): request = QgsFeatureRequest().setFilterFids(ids).setSubsetOfAttributes([]) for f in source.getFeatures(request): if feedback.isCanceled(): break tmpGeom = f.geometry() if geom.within(tmpGeom): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(int(nPoints * total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo(self.tr('Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.')) return {self.OUTPUT: dest_id}
def check(self, check_pipe_conns, check_node_conns): #TODO: rewrite to use a pandas dataframe sindex = QgsSpatialIndex() pipe_fts = self.qgisparams.pipes_vlay.getFeatures() junc_fts = self.qgisparams.junctions_vlay.getFeatures() res_fts = self.qgisparams.reservoirs_vlay.getFeatures() # tank_fts = self.qgisparams.tanks_vlay.getFeatures() # pump_fts = self.qgisparams.pumps_vlay.getFeatures() logger.progress("%%%%%%%%%%% BEGIN CHECK OF PSS SYSTEM %%%%%%%%%%%") #get system details from the qgis vector layers (a lot of this taken from in_writer.py in qepanet) #logger.progress("pipe_lst: "+str(np.asarray(pipe_lst).shape)) # Build nodes spatial index #sindex = QgsSpatialIndex() res = [] j = 0 r = 0 t = 0 l = 0 p = 0 has_error = False num_entity_err = False logger.progress("Summary of System Components:") for feat in junc_fts: sindex.addFeature(feat) j += 1 logger.progress(str(j) + " Junctions") for feat in res_fts: sindex.addFeature(feat) res.append(feat.attribute(Reservoir.field_name_eid)) r += 1 logger.progress(str(r) + " Reservoirs") # for feat in tank_fts: # sindex.addFeature(feat) # t+=1 # logger.progress(str(t)+" Tanks") for feat in pipe_fts: l += 1 logger.progress(str(l) + " Pipes") # for feat in pump_fts: # p+=1 # logger.progress(str(p)+" Pumps") if len( res ) != 1: #check to see if there is a single reservoir. Analysis assumes that all end nodes are pumps pump to a single outlet logger.error( "The number of reserviors in the network must equal 1.") has_error = True if j != l: #check to see if the number of pipes equals the number of junctions. This is true for a directed tree graph with a reservior as its end node. logger.error( "The number of junctions is not equal to the number of pipes.") #has_error = True #commented to continue calc to get a more detailed error in "create_conn_matrix() num_entity_err = True #reset QGIS iterators pipe_fts = self.qgisparams.pipes_vlay.getFeatures() junc_fts = self.qgisparams.junctions_vlay.getFeatures() res_fts = self.qgisparams.reservoirs_vlay.getFeatures() # tank_fts = self.qgisparams.tanks_vlay.getFeatures() # pump_fts = self.qgisparams.pumps_vlay.getFeatures() node_lst = [junc_fts, res_fts] node_Class = [Junction, Reservoir] pipe_lst = [pipe_fts] pipe_Class = [Pipe] disconn_pipes = [] disconn_juncs = [] if check_pipe_conns is True: logger.progress("Cycling through all pipes to check connections..." ) #TODO: This is not working properly Pipe_nodes = [] #List of start and end nodes for each pipe all_pipes = [] #TODO: update to include all layers and check if each layer is valid for i in range(len(pipe_lst)): for ft in pipe_lst[i]: #logger.progress("Checking pipe connection...") eid = ft.attribute(pipe_Class[i].field_name_eid) if num_entity_err == True: all_pipes.append(eid) #sindex = self.qgisparams.nodes_sindex # Find start/end nodes adj_nodes = NetworkUtils.find_start_end_nodes_sindex( self.qgisparams, sindex, ft.geometry()) #adj_nodes = NetworkUtils.find_start_end_nodes(self.qgisparams, ft.geometry()) #TODO: Determine why the 'sindex' version does not work found_nodes = [] #try: start_node_id = adj_nodes[0].attribute( Junction.field_name_eid ) #TODO: Get Reservoir eid name in case it is different. end_node_id = adj_nodes[1].attribute( Junction.field_name_eid) found_nodes.append(start_node_id) found_nodes.append(end_node_id) if start_node_id == end_node_id: logger.error( "Pipe " + eid + " is connected to itself. Connected nodes are " + start_node_id + " and " + end_node_id + ".") has_error = True #check for any very short pipes: #TODO: This is not working correctly short_pipes_nodes = [] if float(ft.attribute(Pipe.field_name_length) ) < 2: #TODO: units are assumed to be ft logger.error("Pipe " + eid + " is very short. Connected nodes are" + start_node_id + " and " + end_node_id + ".") if len(short_pipes) != 0: start_node = False end_node = False for i in range(len(short_pipes_nodes)): if start_node_id == short_pipe_nodes[i]: short_pipe_nodes.append(start_node_id) start_node = True if end_node_id == short_pipe_nodes[i]: short_pipe_nodes.append(end_node_id) end_node == True if (start_node == True) and (end_node == True): break has_error = True if len(short_pipe_nodes) > 0: self.select_qgis_feature( self.qgisparams.junctions_vlay, short_pipe_nodes) Pipe_nodes.append([eid, start_node_id, end_node_id]) # except: # print_str = "Connected nodes are" # found_node = False # if start_node_id: # print_str += " "+start_node_id # found_node = True # if end_node_id: # print_str += " "+end_node_id # found_node = True # if found_node is False: # print_str = "No nodes were found for pipe" # logger.error("Pipe "+eid+" has less than 2 nodes connected. "+print_str+".") # disconn_pipes.append(eid) # has_error = True if len(disconn_pipes) > 0: self.select_qgis_features( self.qgisparams.pipes_vlay, disconn_pipes) #TODO: assumes the vector layer is Pipes else: logger.progress("Pipe connection checks were not performed.") #logger.progress(str(Pipe_nodes)) if check_node_conns is True: if check_pipe_conns is False: logger.warning( "Pipe checks must be performed before checking nodes.") return [False, False, l, j] logger.progress("Cycling through all nodes to check connections..." ) #TODO: This is not working properly for i in range(len(node_lst)): for ft in node_lst[i]: #logger.progress("Checking node connection...") num_conn = 0 eid = ft.attribute(node_Class[i].field_name_eid) for j in range(len(found_nodes)): if found_nodes[j] == eid: num_conn += 1 logger.progress("Connections for Node " + eid + ": " + str(num_conn)) if num_conn == 0: logger.error("The node " + eid + " is not connected to any pipes.") disconn_juncs.append(eid) has_error = True if len(disconn_juncs) > 0: self.select_qgis_features( self.qgisparams.junctions_vlay, disconn_juncs ) #TODO: assumes the vector layer is Junctions else: logger.progress("Node connection checks were not performed.") if num_entity_err: logger.progress( "An Error was found! The number of pipes does not match the number of junctions. Additional details will be provided after parsing through the system." ) elif has_error == True: logger.error( "Error(s) were found. Check the system definition and correct.", stop=True) else: logger.progress("There are no errors found in the system!") logger.progress("%%%%%%%%%%% END CHECK OF PSS SYSTEM %%%%%%%%%%%") #return [has_error, num_entity_err, l, j] #Note: 'PSS.' needed so dervived classes do not overwrite existing values PSS.sysGeomError = has_error PSS.numEntityErr = num_entity_err PSS.nPipes = l PSS.nNodes = j
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException( self.invalidSourceError(parameters, self.INPUT)) pointCount = self.parameterAsDouble(parameters, self.POINTS_NUMBER, context) minDistance = self.parameterAsDouble(parameters, self.MIN_DISTANCE, context) fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 10, 0)) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Point, source.sourceCrs()) if sink is None: raise QgsProcessingException( self.invalidSinkError(parameters, self.OUTPUT)) nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 featureCount = source.featureCount() total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() points = dict() da = QgsDistanceArea() da.setSourceCrs(source.sourceCrs(), context.transformContext()) da.setEllipsoid(context.project().ellipsoid()) request = QgsFeatureRequest() random.seed() while nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break # pick random feature fid = random.randint(0, featureCount - 1) f = next( source.getFeatures( request.setFilterFid(fid).setSubsetOfAttributes([]))) fGeom = f.geometry() if fGeom.isMultipart(): lines = fGeom.asMultiPolyline() # pick random line lineId = random.randint(0, len(lines) - 1) vertices = lines[lineId] else: vertices = fGeom.asPolyline() # pick random segment if len(vertices) == 2: vid = 0 else: vid = random.randint(0, len(vertices) - 2) startPoint = vertices[vid] endPoint = vertices[vid + 1] length = da.measureLine(startPoint, endPoint) dist = length * random.random() if dist > minDistance: d = dist / (length - dist) rx = (startPoint.x() + d * endPoint.x()) / (1 + d) ry = (startPoint.y() + d * endPoint.y()) / (1 + d) # generate random point p = QgsPointXY(rx, ry) geom = QgsGeometry.fromPointXY(p) if vector.checkMinDistance(p, index, minDistance, points): f = QgsFeature(nPoints) f.initAttributes(1) f.setFields(fields) f.setAttribute('id', nPoints) f.setGeometry(geom) sink.addFeature(f, QgsFeatureSink.FastInsert) index.addFeature(f) points[nPoints] = p nPoints += 1 feedback.setProgress(int(nPoints * total)) nIterations += 1 if nPoints < pointCount: feedback.pushInfo( self.tr( 'Could not generate requested number of random points. ' 'Maximum number of attempts exceeded.')) return {self.OUTPUT: dest_id}
def resampleLayer(self, inputLayer, fieldsToSample, weight_by, inputIdField): ''' Disaggregates the polygon properties named in <fieldsToSample> from <inputLayer> at self.outputLayer features :param qgsVectorLayer with data to disaggregate: :param List of fields that should be downscaled spatially: :param Attribute of the OUTPUT shapefile by which to weight the resampled values that fall in the same input feature. *** value extracted from each inputLayer polygon will be multiplied by the fraction of that polygon intersected :param inputIdField: Field name of INPUT layer that contains unique identifiers for each entry :return: resampled layer ''' # Make sure the fields to sample are actually present in the file. Throw exception if not extantFields = get_field_names(inputLayer) missing = list(set(fieldsToSample).difference(extantFields)) if len(missing) > 0: raise ValueError( 'The input shapefile %s is missing the following attributes: %s' % (str(inputLayer.dataProvider().dataSourceUri()), str(fieldsToSample))) # Create spatial index: assume input layer has more features than output inputIndex = QgsSpatialIndex() # Determine which input features intersect the bounding box of the output feature, # then do a real intersection. for feat in inputLayer.getFeatures(): inputIndex.addFeature(feat) # If the inputLayer and outputLayer spatial units are the same, then disaggregation does not need to happen. if sameFeatures(inputLayer, self.outputLayer): return inputLayer # record what was used to label features #if self.logger is not None: # self.logger.addEvent('Disagg', None, None, None, 'Resampling fields ' + str(fieldsToSample) + ', weighting by ' + str(weight_by)) # Clone the output layer (populate this with [dis]aggregated data) newShapeFile = duplicateVectorLayer(self.outputLayer, targetEPSG=self.templateEpsgCode) # Keep track of which field name has which field index fieldIndices = {} newShapeFile.startEditing() existingFields = get_field_names(self.outputLayer) numFields = len(newShapeFile.dataProvider().fields()) numFieldsAdded = 0 for field in fieldsToSample: if field in existingFields: fieldIndices[field] = existingFields.index(field) else: newShapeFile.addAttribute(QgsField(field, QVariant.Double)) newShapeFile.updateFields() fieldIndices[field] = numFields + numFieldsAdded numFieldsAdded += 1 newShapeFile.commitChanges() newShapeFile.updateExtents() # Get read-across between so feature ID can be ascertained from name according to chosen ID field t = shapefile_attributes(newShapeFile)[self.templateIdField] def intorstring(x): try: return int(x) except: return str(x) readAcross = pd.Series(index=list(map(intorstring, t.values)), data=list(map(intorstring, t.index))) t = None # Get areas of input shapefile intersected by output shapefile, and proportions covered, and attribute vals intersectedAreas = intersecting_amounts(fieldsToSample, inputIndex, inputLayer, newShapeFile, inputIdField, self.templateIdField) # Work out disaggregation factor baed on area intersected # Use "big" totals of weightings if the same attribute present in the input data file total_weightings = {} # Assume no "big" totals are available #if type(weight_by) not in [str, unicode]: # raise ValueError('Weighting attribute name not a string or unicode variable') if weight_by in get_field_names(inputLayer): atts = shapefile_attributes(inputLayer)[weight_by] total_weightings = { weight_by: {idx: atts[idx] for idx in atts.index} } self.logger.addEvent( 'Disagg', None, None, None, 'Found attribute ' + str(weight_by) + ' in shapefile to be disaggregated. ' 'Assuming this is the sum of ' + str(weight_by) + ' in the output features') else: # It's not in the input file: Record what happened in the log if weight_by is not None: self.logger.addEvent( 'Disagg', None, None, None, 'Total of Weighting Attribute ' + str(weight_by) + ' not found in original shapefile, so calculating it from the output areas' ) else: self.logger.addEvent( 'Disagg', None, None, None, 'No weighting attribute specified so disaggregated weighting by intersected feature area only' ) if weight_by is None: disagg = disaggregate_weightings(intersectedAreas, newShapeFile, weight_by, total_weightings, self.templateIdField)['_AREA_'] else: disagg = disaggregate_weightings(intersectedAreas, newShapeFile, weight_by, total_weightings, self.templateIdField)[weight_by] # Select successfully identified output areas newShapeFile.selectByIds(list(readAcross[list(disagg.keys())])) selectedOutputFeatures = newShapeFile.selectedFeatures() newShapeFile.startEditing() # Apply disaggregation to features for outputFeat in selectedOutputFeatures: # For each output feature # Select the relevant features from the input layer area_weightings = { inputAreaId: disagg[outputFeat[self.templateIdField]][inputAreaId] for inputAreaId in list(disagg[outputFeat[ self.templateIdField]].keys()) } # Calculate area-weighted average to get a single value for each output area for field in fieldsToSample: # The values to disaggregate in all regions touching this output feature input_values = { inputAreaId: intersectedAreas[outputFeat[ self.templateIdField]][inputAreaId][field] for inputAreaId in list(intersectedAreas[outputFeat[ self.templateIdField]].keys()) } # If an output area is influenced by multiple input areas, and a subset of these is invalid, # assign them zero for i in list(input_values.keys()): try: input_values[i] = float(input_values[i]) except: input_values[i] = 0 # Combine values in all input regions touching this output feature. If disagg_weightings missed one out it's because no intersection or NULL data. # Any value intersecting an output area with NULL weighting will be excluded outputAreasToUse = set(input_values.keys()).intersection( list(area_weightings.keys())) weighted_average = np.sum( np.array([ input_values[in_id] * float(area_weightings[in_id]) for in_id in list(outputAreasToUse) ])) newShapeFile.changeAttributeValue(outputFeat.id(), fieldIndices[field], float(weighted_average)) newShapeFile.commitChanges() newShapeFile.selectByIds([]) # De-select all features return newShapeFile