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 6 commits
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
38 changes: 33 additions & 5 deletions osmnx/simplification.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@ def consolidate_intersections(
dead_ends: bool = False,
reconnect_edges: bool = True,
node_attr_aggs: dict[str, Any] | None = None,
tolerance_attribute=None,
gboeing marked this conversation as resolved.
Show resolved Hide resolved
) -> nx.MultiDiGraph | gpd.GeoSeries:

"""
Consolidate intersections comprising clusters of nearby nodes.

Expand All @@ -463,6 +465,11 @@ def consolidate_intersections(
Note `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_attribute argument. If a node
gboeing marked this conversation as resolved.
Show resolved Hide resolved
does not have a value in the tolerance_attribute, the default tolerance is used.

When `rebuild_graph` is False, it uses a purely geometric (and relatively
fast) algorithm to identify "geometrically close" nodes, merge them, and
return the merged intersections' centroids. When `rebuild_graph` is True,
Expand Down Expand Up @@ -507,6 +514,9 @@ def consolidate_intersections(
(anything accepted as an argument by `pandas.agg`). Node attributes
not in `node_attr_aggs` will contain the unique values across the
merged nodes. If None, defaults to `{"elevation": numpy.mean}`.
tolerance_attribute : str, optional
The name of the attribute that contains individual tolerance values for
each node. If None, the default tolerance is used for all nodes.

Returns
-------
Expand Down Expand Up @@ -536,6 +546,7 @@ def consolidate_intersections(
tolerance,
reconnect_edges,
node_attr_aggs,
tolerance_attribute,
)

# otherwise, if we're not rebuilding the graph
Expand All @@ -544,10 +555,10 @@ def consolidate_intersections(
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_attribute).centroid


def _merge_nodes_geometric(G: nx.MultiDiGraph, tolerance: float) -> gpd.GeoSeries:
def _merge_nodes_geometric(G: nx.MultiDiGraph, tolerance: float, tolerance_attribute: str | None = None) -> gpd.GeoSeries:
"""
Geometrically merge nodes within some distance of each other.

Expand All @@ -558,14 +569,25 @@ def _merge_nodes_geometric(G: nx.MultiDiGraph, tolerance: float) -> gpd.GeoSerie
tolerance
Buffer nodes to this distance (in graph's geometry's units) then merge
overlapping polygons into a single polygon via unary union operation.
tolerance_attribute : str, optional
The name of the attribute that contains individual tolerance values for
each node. If None, the default tolerance is used for all nodes.

Returns
-------
merged
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_attribute and tolerance_attribute in gdf_nodes.columns:
# If a node does not have a value in the tolerance_attribute, use the default tolerance
buffer_distances = gdf_nodes[tolerance_attribute].fillna(tolerance)
# Buffer nodes to the specified distances and merge them
merged = gdf_nodes["geometry"].buffer(distance=buffer_distances).unary_union
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
Expand All @@ -577,6 +599,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915
tolerance: float,
reconnect_edges: bool, # noqa: FBT001
node_attr_aggs: dict[str, Any] | None,
tolerance_attribute: str | None = None,
) -> nx.MultiDiGraph:
"""
Consolidate intersections comprising clusters of nearby nodes.
Expand Down Expand Up @@ -611,6 +634,9 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915
(anything accepted as an argument by `pandas.agg`). Node attributes
not in `node_attr_aggs` will contain the unique values across the
merged nodes. If None, defaults to `{"elevation": numpy.mean}`.
tolerance_attribute : str, optional
The name of the attribute that contains individual tolerance values for
each node. If None, the default tolerance is used for all nodes.

Returns
-------
Expand All @@ -625,7 +651,9 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915
# 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_attribute)
)
centroids = node_clusters.centroid
node_clusters["x"] = centroids.x
node_clusters["y"] = centroids.y
Expand Down
Loading