def get_adjacents_and_geometries(self, expression=""): """ Auxiliary indexes for adjacency of geometries. Returns: (list) groups of adjacent polygons (dict) feature id: geometry """ parents_per_vertex, geometries = self.get_parents_per_vertex_and_geometries( expression) adjs = [] for (wkt, parents) in parents_per_vertex.items(): point = Point(wkt) if len(parents) > 1: for fid in parents: geom = geometries[fid] (point, ndx, ndxa, ndxb, dist) = geom.closestVertex(point) next = Point(geom.vertexAt(ndxb)) parents_next = parents_per_vertex[next.asWkt()] common = set(x for x in parents if x in parents_next) if len(common) > 1: adjs.append(common) adjs = list(adjs) groups = merge_groups(adjs) return (groups, geometries)
def test_is_inside_part(self): self.ulayer.append(self.fixture, level="M") feat = QgsFeature(self.ulayer.fields()) zone = Geometry.fromPolygonXY([[ Point(357270.987, 3123924.266), Point(357282.643, 3123936.187), Point(357283.703, 3123920.822), Point(357270.987, 3123924.266), ]]) feat.setGeometry(zone) self.assertTrue(self.ulayer.is_inside(feat))
def test_is_inside_false(self): self.ulayer.append(self.fixture, level="M") feat = QgsFeature(self.ulayer.fields()) zone = Geometry.fromPolygonXY([[ Point(357228.335, 3123901.881), Point(357231.779, 3123922.677), Point(357245.555, 3123897.377), Point(357228.335, 3123901.881), ]]) feat.setGeometry(zone) self.assertFalse(self.ulayer.is_inside(feat))
def move_entrance( self, ad, ad_buildings, ad_parts, to_move, to_insert, parents_per_vx, ): """ Auxiliary method to move entrance to the nearest building and part. Don't move and the entrance specification is changed if the new position is not enough close ('remote'), is a corner ('corner'), is in an inner ring ('inner') or is in a wall shared with another building ('shared'). """ point = ad.geometry().asPoint() distance = 9e9 for bu in ad_buildings: bg = bu.geometry() d, c, v = bg.closestSegmentWithContext(point)[:3] if d < distance: (building, distance, closest, vertex) = (bu, d, c, v) bg = building.geometry() bid = building.id() va = Point(bg.vertexAt(vertex - 1)) vb = Point(bg.vertexAt(vertex)) if distance > config.addr_thr**2: ad["spec"] = "remote" elif vertex > len(Geometry.get_multipolygon(bg)[0][0]): ad["spec"] = "inner" elif (closest.sqrDist(va) < config.entrance_thr**2 or closest.sqrDist(vb) < config.entrance_thr**2): ad["spec"] = "corner" elif PolygonLayer.is_shared_segment(parents_per_vx, va, vb, bid): ad["spec"] = "shared" else: dg = Geometry.fromPointXY(closest) to_move[ad.id()] = dg bg.insertVertex(closest.x(), closest.y(), vertex) to_insert[bid] = QgsGeometry(bg) building.setGeometry(bg) for part in ad_parts: pg = part.geometry() r = Geometry.get_multipolygon(pg)[0][0] for i in range(len(r) - 1): vpa = Point(pg.vertexAt(i)) vpb = Point(pg.vertexAt(i + 1)) if va in (vpa, vpb) and vb in (vpa, vpb): pg.insertVertex(closest.x(), closest.y(), i + 1) to_insert[part.id()] = QgsGeometry(pg) part.setGeometry(pg) break
def test_is_inside_full(self): self.ulayer.append(self.fixture, level="M") zone = Geometry.fromPolygonXY([[ Point(357275.888, 3123959.765), Point(357276.418, 3123950.625), Point(357286.220, 3123957.911), Point(357275.888, 3123959.765), ]]) feat = QgsFeature(self.ulayer.fields()) feat.setGeometry(zone) self.assertTrue(self.ulayer.is_inside(feat))
def test_simplify1(self): refs = [ ("8643326CS5284S", Point(358684.62, 3124377.54), True), ("8643326CS5284S", Point(358686.29, 3124376.11), True), ("8643324CS5284S", Point(358677.29, 3124366.64), False), ] self.layer.explode_multi_parts() self.layer.simplify() for ref in refs: building = next(self.layer.search("localId = '%s'" % ref[0])) self.assertEqual( ref[1] in Geometry.get_multipolygon(building)[0][0], ref[2])
def test_add_topological_points(self): refs = [ ("8842708CS5284S", Point(358821.08, 3124205.68), 0), ("8842708CS5284S_part1", Point(358821.08, 3124205.68), 0), ("8942328CS5284S", Point(358857.04, 3124248.6705), 1), ("8942328CS5284S_part3", Point(358857.04, 3124248.6705), 0), ] for ref in refs: building = next(self.layer.search("localId = '%s'" % ref[0])) poly = Geometry.get_multipolygon(building) self.assertNotIn(ref[1], poly[ref[2]][0]) self.layer.topology() for ref in refs: building = next(self.layer.search("localId = '%s'" % ref[0])) poly = Geometry.get_multipolygon(building) self.assertIn(ref[1], poly[ref[2]][0])
def get_overpass_bbox(self, bbox): """Transform bbox to EPSG 4326 and returns str in overpass format.""" if bbox.isEmpty(): bbox = None else: p1 = Geometry.fromPointXY(Point(bbox.xMinimum(), bbox.yMinimum())) p2 = Geometry.fromPointXY(Point(bbox.xMaximum(), bbox.yMaximum())) target_crs = QgsCoordinateReferenceSystem.fromEpsgId(4326) crs_transform = self.get_crs_transform(self.crs(), target_crs) p1.transform(crs_transform) p2.transform(crs_transform) bbox = [ p1.asPoint().y() - config.bbox_buffer, p1.asPoint().x() - config.bbox_buffer, p2.asPoint().y() + config.bbox_buffer, p2.asPoint().x() + config.bbox_buffer, ] bbox = "{:.8f},{:.8f},{:.8f},{:.8f}".format(*bbox) return bbox
def test_read_from_osm(self): layer = HighwayLayer() data = osm.Osm() data.Way(((10, 10), (15, 15)), {"name": "FooBar"}) w2 = data.Way(((20, 20), (30, 30))) data.Relation([w2], {"name": "BarTaz"}) layer.read_from_osm(data) self.assertEqual(layer.featureCount(), 2) names = [feat["name"] for feat in layer.getFeatures()] self.assertIn("BarTaz", names) self.assertIn("FooBar", names) for f in layer.getFeatures(): if f["name"] == "FooBar": self.assertEqual( f.geometry().asPolyline(), [Point(10, 10), Point(15, 15)] ) if f["name"] == "BarTaz": self.assertEqual( f.geometry().asPolyline(), [Point(20, 20), Point(30, 30)] )
def test_get_parents_per_vertex_and_geometries(self): ( parents_per_vertex, geometries, ) = self.layer.get_parents_per_vertex_and_geometries() self.assertEqual(len(geometries), self.layer.featureCount()) self.assertTrue( all([ Geometry.get_multipolygon( geometries[f.id()]) == Geometry.get_multipolygon(f) for f in self.layer.getFeatures() ])) self.assertGreater(len(parents_per_vertex), 0) self.assertTrue( all([ Geometry.fromPointXY(Point(vertex)).intersects(geometries[fid]) for (vertex, fids) in list(parents_per_vertex.items()) for fid in fids ]))
def simplify(self): """ Reduce the number of vertices in a polygon layer. * Delete vertex if the angle with its adjacents is near of the straight angle for less than 'straight_thr' degrees in all its parents. * Delete vertex if the distance to the segment formed by its parents is less than 'cath_thr' meters. """ if log.app_level <= logging.DEBUG: debshp = DebugWriter("debug_simplify.shp", self) killed = 0 to_change = {} # Clean non corners (parents_per_vertex, geometries) = self.get_parents_per_vertex_and_geometries() pbar = self.get_progressbar(_("Simplify"), len(parents_per_vertex)) for wkt, parents in parents_per_vertex.items(): point = Point(wkt) # Test if this vertex is a 'corner' in any of its parent polygons for fid in parents: geom = geometries[fid] (angle, is_acute, is_corner, cath) = point.get_corner_context(geom) debmsg = "angle=%.1f, is_acute=%s, is_corner=%s, cath=%.4f" % ( angle, is_acute, is_corner, cath, ) if is_corner: break msg = "Keep" if not is_corner: killed += 1 # delete the vertex from all its parents. for fid in frozenset(parents): g = QgsGeometry(geometries[fid]) (__, ndx, __, __, __) = g.closestVertex(point) (ndxa, ndxb) = g.adjacentVertices(ndx) v = g.vertexAt(ndx) va = g.vertexAt(ndxa) vb = g.vertexAt(ndxb) invalid_ring = v == va or v == vb or va == vb g.deleteVertex(ndx) msg = "Refused" if Geometry.is_valid(g) and not invalid_ring: parents.remove(fid) geometries[fid] = g to_change[fid] = g msg = "Deleted" if log.app_level <= logging.DEBUG: debshp.add_point(point, msg + " " + debmsg) if len(to_change) > BUFFER_SIZE: self.writer.changeGeometryValues(to_change) to_change = {} pbar.update() pbar.close() if len(to_change) > 0: self.writer.changeGeometryValues(to_change) if killed > 0: log.debug(_("Simplified %d vertices in the '%s' layer"), killed, self.name()) report.values["vertex_simplify_" + self.name()] = killed
def delete_invalid_geometries(self, query_small_area=lambda feat: True): """ Delete invalid geometries. Test if any of it acute angle vertex could be deleted. Also removes zig-zag and spike vertex (see Point.get_spike_context). """ if log.app_level <= logging.DEBUG: debshp = DebugWriter("debug_notvalid.shp", self, QgsFields(), WKBPolygon) debshp2 = DebugWriter("debug_spikes.shp", self) to_change = {} to_clean = [] to_move = {} parts = 0 rings = 0 zz = 0 spikes = 0 geometries = {} msg = _("Delete invalid geometries") pbar = self.get_progressbar(msg, len(geometries)) for feat in self.getFeatures(): fid = feat.id() geom = feat.geometry() badgeom = False pn = 0 for polygon in Geometry.get_multipolygon(geom): f = QgsFeature(QgsFields()) g = Geometry.fromPolygonXY(polygon) if g.area() < config.min_area and query_small_area(feat): parts += 1 geom.deletePart(pn) to_change[fid] = geom f.setGeometry(QgsGeometry(g)) if log.app_level <= logging.DEBUG: debshp.addFeature(f) continue pn += 1 for i, ring in enumerate(polygon): if badgeom: break skip = False for n, v in enumerate(ring[0:-1]): ( angle_v, angle_a, ndx, ndxa, is_acute, is_zigzag, is_spike, vx, ) = Point(v).get_spike_context(geom) if skip or not is_acute: skip = False continue g = Geometry.fromPolygonXY([ring]) f.setGeometry(QgsGeometry(g)) g.deleteVertex(n) if not g.isGeosValid() or g.area() < config.min_area: if i > 0: rings += 1 geom.deleteRing(i) to_change[fid] = geom if log.app_level <= logging.DEBUG: debshp.addFeature(f) else: badgeom = True to_clean.append(fid) if log.app_level <= logging.DEBUG: debshp.addFeature(f) break if len(ring) > 4: # (can delete vertexs) va = Point(geom.vertexAt(ndxa)) if is_zigzag: g = QgsGeometry(geom) if ndxa > ndx: g.deleteVertex(ndxa) g.deleteVertex(ndx) skip = True else: g.deleteVertex(ndx) g.deleteVertex(ndxa) valid = g.isGeosValid() if valid: geom = g zz += 1 to_change[fid] = g if log.app_level <= logging.DEBUG: debshp2.add_point( va, "zza %d %d %d %f" % (fid, ndx, ndxa, angle_a), ) debshp2.add_point( v, "zz %d %d %d %s" % (fid, ndx, len(ring), valid), ) elif is_spike: g = QgsGeometry(geom) to_move[va] = vx g.moveVertex(vx.x(), vx.y(), ndxa) g.deleteVertex(ndx) valid = g.isGeosValid() if valid: spikes += 1 skip = ndxa > ndx geom = g to_change[fid] = g if log.app_level <= logging.DEBUG: debshp2.add_point(vx, "vx %d %d" % (fid, ndx)) debshp2.add_point( va, "va %d %d %d %f" % (fid, ndx, ndxa, angle_a)) debshp2.add_point( v, "v %d %d %d %s" % (fid, ndx, len(ring), valid), ) geometries[fid] = geom if geom.area() < config.min_area and query_small_area(feat): to_clean.append(fid) if fid in to_change: del to_change[fid] pbar.update() pbar.close() if to_move: for fid, geom in geometries.items(): if fid in to_clean: continue n = 0 v = Point(geom.vertexAt(n)) while v.x() != 0 or v.y() != 0: if v in to_move: g = QgsGeometry(geom) vx = to_move[v] if log.app_level <= logging.DEBUG: debshp2.add_point(v, "mv %d %d" % (fid, n)) debshp2.add_point(vx, "mvx %d %d" % (fid, n)) g.moveVertex(vx.x(), vx.y(), n) if g.isGeosValid(): geom = g to_change[fid] = g n += 1 v = Point(geom.vertexAt(n)) if to_change: self.writer.changeGeometryValues(to_change) if parts: msg = _("Deleted %d invalid part geometries in the '%s' layer") log.debug(msg, parts, self.name()) report.values["geom_parts_" + self.name()] = parts if rings: msg = _("Deleted %d invalid ring geometries in the '%s' layer") log.debug(msg, rings, self.name()) report.values["geom_rings_" + self.name()] = rings if to_clean: self.writer.deleteFeatures(to_clean) msg = _("Deleted %d invalid geometries in the '%s' layer") log.debug(msg, len(to_clean), self.name()) report.values["geom_invalid_" + self.name()] = len(to_clean) if zz: msg = _("Deleted %d zig-zag vertices in the '%s' layer") log.debug(msg, zz, self.name()) report.values["vertex_zz_" + self.name()] = zz if spikes: msg = _("Deleted %d spike vertices in the '%s' layer") log.debug(msg, spikes, self.name()) report.values["vertex_spike_" + self.name()] = spikes
def topology(self): """Add to nearest segments each vertex in a polygon layer.""" threshold = self.dist_thr # Distance threshold to create nodes dup_thr = self.dup_thr straight_thr = self.straight_thr tp = 0 td = 0 if log.app_level <= logging.DEBUG: debshp = DebugWriter("debug_topology.shp", self) geometries = { f.id(): QgsGeometry(f.geometry()) for f in self.getFeatures() } index = self.get_index() to_change = {} nodes = set() pbar = self.get_progressbar(_("Topology"), len(geometries)) for (gid, geom) in geometries.items(): if geom.area() < config.min_area: continue for point in frozenset(Geometry.get_outer_vertices(geom)): if point not in nodes: area_of_candidates = Point(point).boundingBox(threshold) fids = index.intersects(area_of_candidates) for fid in fids: g = QgsGeometry(geometries[fid]) (p, ndx, ndxa, ndxb, dist_v) = g.closestVertex(point) (dist_s, closest, vertex) = g.closestSegmentWithContext(point)[:3] va = Point(g.vertexAt(ndxa)) vb = Point(g.vertexAt(ndxb)) note = "" if dist_v == 0: dist_a = va.sqrDist(point) dist_b = vb.sqrDist(point) if dist_a < dup_thr**2: g.deleteVertex(ndxa) note = "dupe refused by isGeosValid" if Geometry.is_valid(g): note = "Merge dup. %.10f %.5f,%.5f->%.5f,%.5f" % ( dist_a, va.x(), va.y(), point.x(), point.y(), ) nodes.add(p) nodes.add(va) td += 1 if dist_b < dup_thr**2: g.deleteVertex(ndxb) note = "dupe refused by isGeosValid" if Geometry.is_valid(g): note = "Merge dup. %.10f %.5f,%.5f->%.5f,%.5f" % ( dist_b, vb.x(), vb.y(), point.x(), point.y(), ) nodes.add(p) nodes.add(vb) td += 1 elif dist_v < dup_thr**2: g.moveVertex(point.x(), point.y(), ndx) note = "dupe refused by isGeosValid" if Geometry.is_valid(g): note = "Merge dup. %.10f %.5f,%.5f->%.5f,%.5f" % ( dist_v, p.x(), p.y(), point.x(), point.y(), ) nodes.add(p) td += 1 elif (dist_s < threshold**2 and closest != va and closest != vb): va = Point(g.vertexAt(vertex)) vb = Point(g.vertexAt(vertex - 1)) angle = abs(point.azimuth(va) - point.azimuth(vb)) note = "Topo refused by angle: %.2f" % angle if abs(180 - angle) <= straight_thr: note = "Topo refused by insertVertex" if g.insertVertex(point.x(), point.y(), vertex): note = "Topo refused by isGeosValid" if Geometry.is_valid(g): note = "Add topo %.6f %.5f,%.5f" % ( dist_s, point.x(), point.y(), ) tp += 1 if note.startswith("Merge") or note.startswith("Add"): to_change[fid] = g geometries[fid] = g if note and log.app_level <= logging.DEBUG: debshp.add_point(point, note) if len(to_change) > BUFFER_SIZE: self.writer.changeGeometryValues(to_change) to_change = {} pbar.update() pbar.close() if len(to_change) > 0: self.writer.changeGeometryValues(to_change) if td: log.debug(_("Merged %d close vertices in the '%s' layer"), td, self.name()) report.values["vertex_close_" + self.name()] = td if tp: log.debug(_("Created %d topological points in the '%s' layer"), tp, self.name()) report.values["vertex_topo_" + self.name()] = tp
def test_add_point(self): layer = HighwayLayer() db = DebugWriter("test", layer, "memory") db.add_point(Point(0, 0), "foobar") db.add_point(Point(0, 0))
def test_delete_invalid_geometries(self): f1 = QgsFeature(self.layer.fields()) g1 = Geometry.fromPolygonXY([[ Point(358794.000, 3124330.000), Point(358794.200, 3124329.800), Point(358794.400, 3124330.000), Point(358794.200, 3124500.000), Point(358794.000, 3124330.000), ]]) f1.setGeometry(g1) f2 = QgsFeature(self.layer.fields()) g2 = Geometry.fromPolygonXY([[ Point(358794.000, 3124330.000), Point(358795.000, 3124331.000), Point(358794.500, 3124500.000), Point(358794.000, 3124330.000), ]]) f2.setGeometry(g2) f3 = QgsFeature(self.layer.fields()) g3 = Geometry.fromPolygonXY([ [ Point(358890.000, 3124329.000), Point(358900.000, 3124329.000), Point(358900.000, 3124501.000), Point(358890.000, 3124501.000), Point(358890.000, 3124330.000), ], [ Point(358894.000, 3124330.000), Point(358895.000, 3124331.000), Point(358894.500, 3124500.000), Point(358894.000, 3124330.000), ], ]) f3.setGeometry(g3) f4 = QgsFeature(self.layer.fields()) g4 = Geometry.fromPolygonXY([[ Point(357400.00, 3124305.00), # spike Point(357405.00, 3124305.04), Point(357404.99, 3124307.60), Point(357405.00, 3124307.40), # zig-zag Point(357405.00, 3124313.00), # spike Point(357405.04, 3124310.00), Point(357407.50, 3124311.00), Point(357409.96, 3124310.00), Point(357410.00, 3124313.00), # spike Point(357410.02, 3124306.00), Point(357410.00, 3124305.00), Point(357400.00, 3124305.00), ]]) f4.setGeometry(g4) f5 = QgsFeature(self.layer.fields()) g5 = Geometry.fromPolygonXY([[ Point(357400.00, 3124305.00), Point(357405.00, 3124305.04), Point(357405.00, 3124310.00), Point(357400.00, 3124310.00), Point(357400.00, 3124305.00), ]]) f5.setGeometry(g5) fc = self.layer.featureCount() self.layer.writer.addFeatures([f1, f2, f3, f4, f5]) self.layer.delete_invalid_geometries() self.assertEqual(fc, self.layer.featureCount() - 3) request = QgsFeatureRequest().setFilterFid(self.layer.featureCount() - 3) f = next(self.layer.getFeatures(request)) mp = Geometry.get_multipolygon(f) self.assertEqual(len(mp[0]), 1) request = QgsFeatureRequest().setFilterFid(self.layer.featureCount() - 2) f = next(self.layer.getFeatures(request)) mp = Geometry.get_multipolygon(f) r = [ (357410.00, 3124305.00), (357405.00, 3124305.00), (357405.00, 3124309.98), (357407.50, 3124311.00), (357410.01, 3124310.02), (357410.02, 3124306.00), (357410.00, 3124305.00), ] self.assertEqual(r, [(round(p.x(), 2), round(p.y(), 2)) for p in mp[0][0]]) request = QgsFeatureRequest().setFilterFid(self.layer.featureCount() - 1) f = next(self.layer.getFeatures(request)) mp = Geometry.get_multipolygon(f) r = [ (357400.00, 3124305.00), (357400.00, 3124310.00), (357405.00, 3124310.00), (357405.00, 3124305.00), (357400.00, 3124305.00), ] self.assertEqual(r, [(round(p.x(), 2), round(p.y(), 2)) for p in mp[0][0]])
def test_difference(self): layer1 = PolygonLayer("Polygon", "test1", "memory") layer2 = PolygonLayer("Polygon", "test2", "memory") g1 = Geometry.fromPolygonXY([[ Point(10, 10), Point(20, 10), Point(20, 20), Point(10, 20), Point(10, 10), ]]) g2 = Geometry.fromPolygonXY([[ Point(30, 10), Point(40, 10), Point(40, 20), Point(30, 20), Point(30, 10), ]]) h1 = Geometry.fromPolygonXY([[ Point(14, 14), Point(16, 14), Point(16, 16), Point(14, 16), Point(14, 14), ]]) h2 = Geometry.fromPolygonXY([[ Point(20, 10), Point(30, 10), Point(30, 20), Point(20, 20), Point(20, 10), ]]) h3 = Geometry.fromPolygonXY([[ Point(38, 10), Point(42, 10), Point(42, 20), Point(38, 20), Point(38, 10), ]]) h4 = Geometry.fromPolygonXY([[ Point(30, 30), Point(40, 30), Point(40, 40), Point(40, 30), Point(30, 30), ]]) r1 = g1.difference(h1) r2 = g2.difference(h3) layer1.writer.addFeatures([QgsFeature() for i in range(2)]) layer1.writer.changeGeometryValues({1: g1, 2: g2}) layer2.writer.addFeatures([QgsFeature() for i in range(4)]) layer2.writer.changeGeometryValues({1: h1, 2: h2, 3: h3, 4: h4}) layer1.difference(layer2) self.assertEqual(layer1.featureCount(), 2) request = QgsFeatureRequest().setFilterFid(1) f1 = next(layer1.getFeatures(request)) request = QgsFeatureRequest().setFilterFid(2) f2 = next(layer1.getFeatures(request)) self.assertEqual(f1.geometry().difference(r1).area(), 0) self.assertEqual(f2.geometry().difference(r2).area(), 0)