Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node-specific tolerances to intersection consolidation #1160

Merged
merged 18 commits into from
Apr 25, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions osmnx/simplification.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import geopandas as gpd
import networkx as nx
import numpy as np
from shapely.geometry import LineString
gboeing marked this conversation as resolved.
Show resolved Hide resolved
from shapely.geometry import MultiPolygon
from shapely.geometry import Point
Expand Down Expand Up @@ -405,7 +406,7 @@ def simplify_graph( # noqa: C901


def consolidate_intersections(
G, tolerance=10, rebuild_graph=True, dead_ends=False, reconnect_edges=True
G, tolerance=10, rebuild_graph=True, dead_ends=False, reconnect_edges=True, tolerance_column=None
):
"""
Consolidate intersections comprising clusters of nearby nodes.
Expand All @@ -418,6 +419,11 @@ def consolidate_intersections(
Note the tolerance represents a per-node buffering radius: for example, to
consolidate nodes within 10 meters of each other, use tolerance=5.

It's also possible to specify difference tolerances for each node. This can
be done by adding an attribute to each node with contains the tolerance, and
passing the name of that argument as tolerance_column argument. If a node
does not have a value in the tolerance_column, the default tolerance is used.

When rebuild_graph=False, it uses a purely geometrical (and relatively
fast) algorithm to identify "geometrically close" nodes, merge them, and
return just the merged intersections' centroids. When rebuild_graph=True,
Expand Down Expand Up @@ -457,6 +463,10 @@ def consolidate_intersections(
edge length attributes; if False, returned graph has no edges (which
is faster if you just need topologically consolidated intersection
counts).
tolerance_column : str, optional
The name of the column in the nodes GeoDataFrame that contains
individual tolerance values for each node. If None, the default
tolerance is used for all nodes.

Returns
-------
Expand All @@ -481,18 +491,18 @@ def consolidate_intersections(
return G

# otherwise
return _consolidate_intersections_rebuild_graph(G, tolerance, reconnect_edges)
return _consolidate_intersections_rebuild_graph(G, tolerance, reconnect_edges, tolerance_column)

# otherwise, if we're not rebuilding the graph
if not G:
# if graph has no nodes, just return empty GeoSeries
return gpd.GeoSeries(crs=G.graph["crs"])

# otherwise, return the centroids of the merged intersection polygons
return _merge_nodes_geometric(G, tolerance).centroid
return _merge_nodes_geometric(G, tolerance, tolerance_column).centroid


def _merge_nodes_geometric(G, tolerance):
def _merge_nodes_geometric(G, tolerance, tolerance_column):
"""
Geometrically merge nodes within some distance of each other.

Expand All @@ -509,15 +519,25 @@ def _merge_nodes_geometric(G, tolerance):
merged : GeoSeries
the merged overlapping polygons of the buffered nodes
"""
# buffer nodes GeoSeries then get unary union to merge overlaps
merged = convert.graph_to_gdfs(G, edges=False)["geometry"].buffer(tolerance).unary_union
gdf_nodes = convert.graph_to_gdfs(G, edges=False)

if tolerance_column and tolerance_column in gdf_nodes.columns:
# Use the values from the tolerance_column as an array for buffering
buffer_distances = gdf_nodes[tolerance_column].values
# If a node does not have a value in the tolerance_column, use the default tolerance
buffer_distances[np.isnan(buffer_distances)] = tolerance
# Buffer nodes to the specified distances and merge them
merged = gdf_nodes['geometry'].buffer(distance=buffer_distances).unary_union
EwoutH marked this conversation as resolved.
Show resolved Hide resolved
else:
# Use the default tolerance for all nodes
merged = gdf_nodes['geometry'].buffer(distance=tolerance).unary_union

# if only a single node results, make it iterable to convert to GeoSeries
merged = MultiPolygon([merged]) if isinstance(merged, Polygon) else merged
gboeing marked this conversation as resolved.
Show resolved Hide resolved
return gpd.GeoSeries(merged.geoms, crs=G.graph["crs"])


def _consolidate_intersections_rebuild_graph(G, tolerance=10, reconnect_edges=True):
def _consolidate_intersections_rebuild_graph(G, tolerance=10, reconnect_edges=True, tolerance_column=None):
"""
Consolidate intersections comprising clusters of nearby nodes.

Expand Down Expand Up @@ -556,7 +576,7 @@ def _consolidate_intersections_rebuild_graph(G, tolerance=10, reconnect_edges=Tr
# STEP 1
# buffer nodes to passed-in distance and merge overlaps. turn merged nodes
# into gdf and get centroids of each cluster as x, y
node_clusters = gpd.GeoDataFrame(geometry=_merge_nodes_geometric(G, tolerance))
node_clusters = gpd.GeoDataFrame(geometry=_merge_nodes_geometric(G, tolerance, tolerance_column))
centroids = node_clusters.centroid
node_clusters["x"] = centroids.x
node_clusters["y"] = centroids.y
Expand Down
Loading