def moment_of_inertia(collection): """ Computes the moment of inertia of the polygon. This treats each boundary point as a point-mass of 1. Thus, for constant unit mass at each boundary point, the MoI of this pointcloud is \sum_i d_{i,c}^2 where c is the centroid of the polygon Altman's OS_1 measure, cited in Boyce and Clark (1964), also used in Weaver and Hess (1963). """ ga = _cast(collection) coords = pygeos.get_coordinates(ga) geom_ixs = numpy.repeat(numpy.arange(len(ga)), pygeos.get_num_coordinates(ga)) centroids = pygeos.get_coordinates(pygeos.centroid(ga))[geom_ixs] squared_euclidean = numpy.sum((coords - centroids)**2, axis=1) dists = (pandas.DataFrame.from_dict( dict(d2=squared_euclidean, geom_ix=geom_ixs)).groupby("geom_ix").d2.sum()).values return pygeos.area(ga) / numpy.sqrt(2 * dists)
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 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 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 test_get_num_coordinates(): actual = pygeos.get_num_coordinates(all_types + (None, )).tolist() assert actual == [1, 3, 5, 5, 2, 2, 10, 3, 0, 0]
def extend_lines(gdf, tolerance, target=None, barrier=None, extension=0): """ Extends lines from gdf to istelf or target within a set tolerance Extends unjoined ends of LineString segments to join with other segments or target. If ``target`` is passed, extend lines to target. Otherwise extend lines to itself. If ``barrier`` is passed, each extended line is checked for intersection with ``barrier``. If they intersect, extended line is not returned. This can be useful if you don't want to extend street network segments through buildings. Parameters ---------- gdf : GeoDataFrame GeoDataFrame containing LineString geometry tolerance : float tolerance in snapping (by how much could be each segment extended). target : GeoDataFrame, GeoSeries target geometry to which ``gdf`` gets extended. Has to be (Multi)LineString geometry. barrier : GeoDataFrame, GeoSeries extended line is not used if it intersects barrier extension : float by how much to extend line beyond the snapped geometry. Useful when creating enclosures to avoid floating point imprecision. Returns ------- GeoDataFrame GeoDataFrame of with extended geometry See also -------- momepy.close_gaps momepy.remove_false_nodes """ # explode to avoid MultiLineStrings # double reset index due to the bug in GeoPandas explode df = gdf.reset_index(drop=True).explode().reset_index(drop=True) if target is None: target = df itself = True else: itself = False # get underlying pygeos geometry geom = df.geometry.values.data # extract array of coordinates and number per geometry 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)) # query LineString geometry to identify points intersecting 2 geometries tree = pygeos.STRtree(geom) inp, res = tree.query_bulk(points, predicate="intersects") unique, counts = np.unique(inp, return_counts=True) ends = np.unique(res[np.isin(inp, unique[counts == 1])]) new_geoms = [] # iterate over cul-de-sac-like segments and attempt to snap them to street network for line in ends: l_coords = pygeos.get_coordinates(geom[line]) start = pygeos.points(l_coords[0]) end = pygeos.points(l_coords[-1]) first = list(tree.query(start, predicate="intersects")) second = list(tree.query(end, predicate="intersects")) first.remove(line) second.remove(line) t = target if not itself else target.drop(line) if first and not second: snapped = _extend_line(l_coords, t, tolerance) if (barrier is not None and barrier.sindex.query(pygeos.linestrings(snapped), predicate="intersects").size > 0): new_geoms.append(geom[line]) else: if extension == 0: new_geoms.append(pygeos.linestrings(snapped)) else: new_geoms.append( pygeos.linestrings( _extend_line(snapped, t, extension, snap=False))) elif not first and second: snapped = _extend_line(np.flip(l_coords, axis=0), t, tolerance) if (barrier is not None and barrier.sindex.query(pygeos.linestrings(snapped), predicate="intersects").size > 0): new_geoms.append(geom[line]) else: if extension == 0: new_geoms.append(pygeos.linestrings(snapped)) else: new_geoms.append( pygeos.linestrings( _extend_line(snapped, t, extension, snap=False))) elif not first and not second: one_side = _extend_line(l_coords, t, tolerance) one_side_e = _extend_line(one_side, t, extension, snap=False) snapped = _extend_line(np.flip(one_side_e, axis=0), t, tolerance) if (barrier is not None and barrier.sindex.query(pygeos.linestrings(snapped), predicate="intersects").size > 0): new_geoms.append(geom[line]) else: if extension == 0: new_geoms.append(pygeos.linestrings(snapped)) else: new_geoms.append( pygeos.linestrings( _extend_line(snapped, t, extension, snap=False))) df.iloc[ends, df.columns.get_loc(df.geometry.name)] = new_geoms return df
def remove_false_nodes(gdf): """ Clean topology of existing LineString geometry by removal of nodes of degree 2. Parameters ---------- gdf : GeoDataFrame, GeoSeries, array of pygeos geometries (Multi)LineString data of street network Returns ------- gdf : GeoDataFrame, GeoSeries See also -------- momepy.extend_lines momepy.close_gaps """ if isinstance(gdf, (gpd.GeoDataFrame, gpd.GeoSeries)): # explode to avoid MultiLineStrings # double reset index due to the bug in GeoPandas explode df = gdf.reset_index(drop=True).explode().reset_index(drop=True) # get underlying pygeos geometry geom = df.geometry.values.data else: geom = gdf # extract array of coordinates and number per geometry 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)) # query LineString geometry to identify points intersecting 2 geometries tree = pygeos.STRtree(geom) inp, res = tree.query_bulk(points, predicate="intersects") unique, counts = np.unique(inp, return_counts=True) merge = res[np.isin(inp, unique[counts == 2])] if len(merge) > 0: # filter duplications and create a dictionary with indication of components to # be merged together dups = [ item for item, count in collections.Counter(merge).items() if count > 1 ] split = np.split(merge, len(merge) / 2) components = {} for i, a in enumerate(split): if a[0] in dups or a[1] in dups: if a[0] in components.keys(): i = components[a[0]] elif a[1] in components.keys(): i = components[a[1]] components[a[0]] = i components[a[1]] = i # iterate through components and create new geometries new = [] for c in set(components.values()): keys = [] for item in components.items(): if item[1] == c: keys.append(item[0]) new.append(pygeos.line_merge(pygeos.union_all(geom[keys]))) # remove incorrect geometries and append fixed versions df = df.drop(merge) final = gpd.GeoSeries(new).explode().reset_index(drop=True) if isinstance(gdf, gpd.GeoDataFrame): return df.append( gpd.GeoDataFrame({df.geometry.name: final}, geometry=df.geometry.name), ignore_index=True, ) return df.append(final, ignore_index=True)
def line_to_line(gdf, target, tolerance): """ Extends lines from gdf to target within a set tolerance """ # explode to avoid MultiLineStrings # double reset index due to the bug in GeoPandas explode df = gdf.reset_index(drop=True).explode().reset_index(drop=True) # get underlying pygeos geometry geom = df.geometry.values.data # extract array of coordinates and number per geometry 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)) # query LineString geometry to identify points intersecting 2 geometries tree = pygeos.STRtree(geom) inp, res = tree.query_bulk(points, predicate="intersects") unique, counts = np.unique(inp, return_counts=True) ends = np.unique(res[np.isin(inp, unique[counts == 1])]) new_geoms = [] # iterate over cul-de-sac-like segments and attempt to snap them to street network for line in ends: l_coords = pygeos.get_coordinates(geom[line]) start = pygeos.points(l_coords[0]) end = pygeos.points(l_coords[-1]) first = list(tree.query(start, predicate="intersects")) second = list(tree.query(end, predicate="intersects")) first.remove(line) second.remove(line) if first and not second: snapped = extend_line(l_coords, target, tolerance) new_geoms.append( pygeos.linestrings( extend_line(snapped, target, 0.00001, snap=False))) elif not first and second: snapped = extend_line(np.flip(l_coords, axis=0), target, tolerance) new_geoms.append( pygeos.linestrings( extend_line(snapped, target, 0.00001, snap=False))) elif not first and not second: one_side = extend_line(l_coords, target, tolerance) one_side_e = extend_line(one_side, target, 0.00001, snap=False) snapped = extend_line(np.flip(one_side_e, axis=0), target, tolerance) new_geoms.append( pygeos.linestrings( extend_line(snapped, target, 0.00001, snap=False))) df = df.drop(ends) final = gpd.GeoSeries(new_geoms).explode().reset_index(drop=True) return df.append( gpd.GeoDataFrame({df.geometry.name: final}, geometry=df.geometry.name), ignore_index=True, )