def close_gaps(df, tolerance): """Close gaps in LineString geometry where it should be contiguous. Snaps both lines to a centroid of a gap in between. """ geom = df.geometry.values.data coords = pygeos.get_coordinates(geom) indices = pygeos.get_num_coordinates(geom) # generate a list of start and end coordinates and create point geometries edges = [0] i = 0 for ind in indices: ix = i + ind edges.append(ix - 1) edges.append(ix) i = ix edges = edges[:-1] points = pygeos.points(np.unique(coords[edges], axis=0)) buffered = pygeos.buffer(points, tolerance) dissolved = pygeos.union_all(buffered) exploded = [ pygeos.get_geometry(dissolved, i) for i in range(pygeos.get_num_geometries(dissolved)) ] centroids = pygeos.centroid(exploded) snapped = pygeos.snap(geom, pygeos.union_all(centroids), tolerance) return snapped
def test_get_parts(geom): expected_num_parts = pygeos.get_num_geometries(geom) expected_parts = pygeos.get_geometry(geom, range(0, expected_num_parts)) parts = pygeos.get_parts(geom) assert len(parts) == expected_num_parts assert np.all(pygeos.equals_exact(parts, expected_parts))
def explode(df): """Explodes multipart geometries to single parts. Attributes are copied to each individual geometry. NOTE: Faster method not yet supported in pygeos, in https://github.com/pygeos/pygeos/pull/130 This branch must be checked out and built for this functionality. Parameters ---------- df : GeoDataFrame Returns ------- GeoDataFrame """ # Fast method: # ix, parts = pg.get_parts(df.geometry.values.data) # series = pd.Series(parts, index=df.index[ix], name="geometry") # return df.drop(columns=["geometry"]).join(series) # Slower method geometries = df.geometry.values.data ix = [] parts = [] for i in range(len(df)): num_parts = pg.get_num_geometries(geometries[i]) ix.extend(np.repeat(df.index[i], num_parts)) parts.extend(pg.get_geometry(geometries[i], range(num_parts))) return gp.GeoDataFrame({ "geometry": parts }, index=ix, crs=df.crs).join(df.drop(columns=["geometry"]))
def time_get_parts_python(self): """Python / ufuncs version of get_parts""" parts = [] for i in range(len(self.multipolygons)): num_parts = pygeos.get_num_geometries(self.multipolygons[i]) parts.append(pygeos.get_geometry(self.multipolygons[i], range(num_parts))) parts = np.concatenate(parts)
def get_angles(collection, return_indices=False): """ Get the angles pertaining to each vertex of a set of polygons. This assumes the input are polygons. Arguments --------- ga : pygeos geometry array array of polygons/multipolygons return_indices : bool (Default: False) whether to return the indices relating each geometry to a polygon Returns ------- angles between triples of points on each geometry, as well as the indices relating angles to input geometries (if requested). See the Notes for information on the shape of angles and indices. Notes ------- If a geometry has n coordinates and k parts, the array will be n - k. If each geometry has n_i coordinates, then let N be a vector storing those counts (computed, for example, using pygeos.get_num_coordinates(ga)). Likewise, let K be a vector storing the number of parts each geometry has, k_i (computed, for example, using pygeos.get_num_geometries(ga)) Then, the output is of shape (N - K).sum() """ ga = _cast(collection) exploded = pygeos.get_parts(ga) coords = pygeos.get_coordinates(exploded) n_coords_per_geom = pygeos.get_num_coordinates(exploded) n_parts_per_geom = pygeos.get_num_geometries(exploded) angles = numpy.asarray(_get_angles(coords, n_coords_per_geom)) if return_indices: return angles, numpy.repeat( numpy.arange(len(ga)), pygeos.get_num_coordinates(ga) - pygeos.get_num_geometries(ga), ) else: return angles
def test_get_parts_geometry_collection_multi(): """On the first pass, the individual Multi* geometry objects are returned from the collection. On the second pass, the individual singular geometry objects within those are returned. """ geom = pygeos.geometrycollections([multi_point, multi_line_string, multi_polygon]) expected_num_parts = pygeos.get_num_geometries(geom) expected_parts = pygeos.get_geometry(geom, range(0, expected_num_parts)) parts = pygeos.get_parts(geom) assert len(parts) == expected_num_parts assert np.all(pygeos.equals_exact(parts, expected_parts)) expected_subparts = [] for g in np.asarray(expected_parts): for i in range(0, pygeos.get_num_geometries(g)): expected_subparts.append(pygeos.get_geometry(g, i)) subparts = pygeos.get_parts(parts) assert len(subparts) == len(expected_subparts) assert np.all(pygeos.equals_exact(subparts, expected_subparts))
def test_get_parts_array(): # note: this also verifies that None is handled correctly # in the mix; internally it returns -1 for count of geometries geom = np.array([None, empty_line_string, multi_point, point, multi_polygon]) expected_parts = [] for g in geom: for i in range(0, pygeos.get_num_geometries(g)): expected_parts.append(pygeos.get_geometry(g, i)) parts = pygeos.get_parts(geom) assert len(parts) == len(expected_parts) assert np.all(pygeos.equals_exact(parts, expected_parts))
def test_get_parts_return_index(): geom = np.array([multi_point, point, multi_polygon]) expected_parts = [] expected_index = [] for i, g in enumerate(geom): for j in range(0, pygeos.get_num_geometries(g)): expected_parts.append(pygeos.get_geometry(g, j)) expected_index.append(i) parts, index = pygeos.get_parts(geom, return_index=True) assert len(parts) == len(expected_parts) assert np.all(pygeos.equals_exact(parts, expected_parts)) assert np.array_equal(index, expected_index)
def close_gaps(gdf, tolerance): """Close gaps in LineString geometry where it should be contiguous. Snaps both lines to a centroid of a gap in between. Parameters ---------- gdf : GeoDataFrame, GeoSeries GeoDataFrame or GeoSeries containing LineString representation of a network. tolerance : float nodes within a tolerance will be snapped together Returns ------- GeoSeries See also -------- momepy.extend_lines momepy.remove_false_nodes """ geom = gdf.geometry.values.data coords = pygeos.get_coordinates(geom) indices = pygeos.get_num_coordinates(geom) # generate a list of start and end coordinates and create point geometries edges = [0] i = 0 for ind in indices: ix = i + ind edges.append(ix - 1) edges.append(ix) i = ix edges = edges[:-1] points = pygeos.points(np.unique(coords[edges], axis=0)) buffered = pygeos.buffer(points, tolerance / 2) dissolved = pygeos.union_all(buffered) exploded = [ pygeos.get_geometry(dissolved, i) for i in range(pygeos.get_num_geometries(dissolved)) ] centroids = pygeos.centroid(exploded) snapped = pygeos.snap(geom, pygeos.union_all(centroids), tolerance) return gpd.GeoSeries(snapped, crs=gdf.crs)
def explode(series): """Convert multipart geometries to a list of geometries Parameters ---------- series : Series Returns ------- Series """ return series.apply( lambda g: [pg.get_geometry(g, i) for i in range(0, pg.get_num_geometries(g))] )
def to_dict(geometry): """Convert pygeos Geometry object to a dictionary representation. Equivalent to structure of GeoJSON. Parameters ---------- geometry : pygeos Geometry object (singular) Returns ------- dict GeoJSON dict representation of geometry """ geometry = pg.normalize(geometry) def get_ring_coords(polygon): # outer ring must be reversed to be counterclockwise[::-1] coords = [pg.get_coordinates(pg.get_exterior_ring(polygon)).tolist()] for i in range(pg.get_num_interior_rings(polygon)): # inner rings must be reversed to be clockwise[::-1] coords.append( pg.get_coordinates(pg.get_interior_ring(polygon, i)).tolist()) return coords geom_type = GEOJSON_TYPE[pg.get_type_id(geometry)] coords = [] if geom_type == "MultiPolygon": coords = [] geoms = pg.get_geometry(geometry, range(pg.get_num_geometries(geometry))) for geom in geoms: coords.append(get_ring_coords(geom)) elif geom_type == "Polygon": coords = get_ring_coords(geometry) else: raise NotImplementedError("Not built") return {"type": geom_type, "coordinates": coords}
def _clean_multi_geometries(object_array): """Cleanup a sequence of geometries to remove multi geometries. Args: object_array (numpy.ndarray): The object array to cleanup Returns: numpy.ndarray: Cleaned-up object array. """ # Handle multi-geometries geometries = object_array[:, 0] num_geometries = get_num_geometries(geometries) for index in np.nonzero(num_geometries > 1)[0]: split_geometries = [ np.concatenate((get_geometry(geometries[index], i), object_array[index, 1:])) for i in range(num_geometries[index]) ] object_array[index] = split_geometries[0] object_array = np.concatenate((object_array, split_geometries[1:, :])) return geometries
def second_areal_moment(collection): """ Using equation listed on en.wikipedia.org/Second_Moment_of_area, the second moment of area is actually the cross-moment of area between the X and Y dimensions: I_xy = (1/24)\sum^{i=N}^{i=1} (x_iy_{i+1} + 2*x_iy_i + 2*x_{i+1}y_{i+1} + x_{i+1}y_i)(x_iy_i - x_{i+1}y_i) where x_i, y_i is the current point and x_{i+1}, y_{i+1} is the next point, and where x_{n+1} = x_1, y_{n+1} = 1. This relation is known as the: - second moment of area - moment of inertia of plane area - area moment of inertia - second area moment and is *not* the mass moment of inertia, a property of the distribution of mass around a shape. """ ga = _cast(collection) result = numpy.zeros(len(ga)) n_holes_per_geom = pygeos.get_num_interior_rings(ga) for i, geometry in enumerate(ga): n_holes = n_holes_per_geom[i] for hole_ix in range(n_holes): hole = pygeos.get_coordinates(pygeos.get_interior_ring( ga, hole_ix)) result[i] -= _second_moa_ring(hole) n_parts = pygeos.get_num_geometries(geometry) for part in pygeos.get_parts(geometry): result[i] += _second_moa_ring(pygeos.get_coordinates(part)) # must divide everything by 24 and flip if polygon is clockwise. signflip = numpy.array([-1, 1])[pygeos.is_ccw(ga).astype(int)] return result * (1 / 24) * signflip
def occult(lines: LineCollection, tolerance: float) -> LineCollection: """ Remove occulted lines. The order of the geometries in 'lines' matters, see example below. 'tolerance' controls the distance tolerance between the first and last points of a geometry to consider it closed. Examples: $ vpype line 0 0 5 5 rect 2 2 1 1 occult show # line is occulted by rect $ vpype rect 2 2 1 1 line 0 0 5 5 occult show # line is NOT occulted by rect, as the line is drawn after the rectangle. """ line_arr = np.array( [pygeos.linestrings(list(zip(line.real, line.imag))) for line in lines] ) for i, line in enumerate(line_arr): coords = pygeos.get_coordinates(line) if math.hypot(coords[-1, 0] - coords[0, 0], coords[-1, 1] - coords[0, 1]) < tolerance: tree = pygeos.STRtree(line_arr[:i]) p = pygeos.polygons(coords) geom_idx = tree.query(p, predicate="intersects") line_arr[geom_idx] = pygeos.set_operations.difference(line_arr[geom_idx], p) new_lines = LineCollection() for geom in line_arr: for i in range(pygeos.get_num_geometries(geom)): coords = pygeos.get_coordinates(pygeos.get_geometry(geom, i)) new_lines.append(coords[:, 0] + coords[:, 1] * 1j) return new_lines
def test_delaunay_triangles_only_edges(): original = Geometry("MULTIPOINT (50 30, 60 30, 100 100, 10 150, 110 120)") actual = pygeos.delaunay_triangles(original, only_edges=True) assert pygeos.get_num_geometries(actual) == 7
def test_create_collection_skips_none(func, sub_geom): actual = func([sub_geom, None, None, sub_geom]) assert pygeos.get_num_geometries(actual) == 2
def test_create_collection(func, sub_geom): actual = func([sub_geom, sub_geom]) assert pygeos.get_num_geometries(actual) == 2
def test_get_num_geometries(): actual = pygeos.get_num_geometries(all_types + (None, )).tolist() assert actual == [1, 1, 1, 1, 2, 1, 2, 2, 0, 0]
def test_voronoi_polygons(): original = Geometry("MULTIPOINT (50 30, 60 30, 100 100, 10 150, 110 120)") actual = pygeos.voronoi_polygons(original) assert pygeos.get_num_geometries(actual) == 5
def test_voronoi_polygons_only_edges(): # example from PostGIS docs original = Geometry("MULTIPOINT (50 30, 60 30, 100 100, 10 150, 110 120)") actual = pygeos.voronoi_polygons(original, only_edges=True) assert pygeos.get_num_geometries(actual) == 7
def test_get_geometry_collection(geom): n = pygeos.get_num_geometries(geom) actual = pygeos.get_geometry(geom, [0, -n, n, -(n + 1)]) assert pygeos.equals(actual[0], actual[1]).all() assert pygeos.is_missing(actual[2:4]).all()