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 11 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
52 changes: 40 additions & 12 deletions osmnx/simplification.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import geopandas as gpd
import networkx as nx
import numpy as np
from shapely import LineString
from shapely import MultiPolygon
from shapely import Point
from shapely import Polygon
import pandas as pd
from shapely.geometry import LineString
from shapely.geometry import MultiPolygon
from shapely.geometry import Point
from shapely.geometry import Polygon

from . import convert
from . import stats
Expand Down Expand Up @@ -446,7 +447,7 @@
def consolidate_intersections(
G: nx.MultiDiGraph,
*,
tolerance: float = 10,
tolerance: float | dict[int, float] = 10,
rebuild_graph: bool = True,
dead_ends: bool = False,
reconnect_edges: bool = True,
Expand All @@ -463,6 +464,11 @@
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
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 All @@ -487,7 +493,8 @@
A projected graph.
tolerance
Nodes are buffered to this distance (in graph's geometry's units) and
subsequent overlaps are dissolved into a single node.
subsequent overlaps are dissolved into a single node. Can be a float
value or a dictionary mapping node IDs to individual tolerance values.
rebuild_graph
If True, consolidate the nodes topologically, rebuild the graph, and
return as MultiDiGraph. Otherwise, consolidate the nodes geometrically
Expand Down Expand Up @@ -547,7 +554,10 @@
return _merge_nodes_geometric(G, tolerance).centroid


def _merge_nodes_geometric(G: nx.MultiDiGraph, tolerance: float) -> gpd.GeoSeries:
def _merge_nodes_geometric(
G: nx.MultiDiGraph,
tolerance: float | dict[int, float],
) -> gpd.GeoSeries:
"""
Geometrically merge nodes within some distance of each other.

Expand All @@ -558,14 +568,29 @@
tolerance
Buffer nodes to this distance (in graph's geometry's units) then merge
overlapping polygons into a single polygon via unary union operation.
Can be a float value or a dictionary mapping node IDs to individual
tolerance values.

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 isinstance(tolerance, dict):
# Create a Series of tolerances, reindexed to match the nodes
tolerances = pd.Series(tolerance).reindex(gdf_nodes.index)

Check warning on line 583 in osmnx/simplification.py

View check run for this annotation

Codecov / codecov/patch

osmnx/simplification.py#L583

Added line #L583 was not covered by tests
# Buffer nodes to the specified distance
buffered_geoms = gdf_nodes.geometry.buffer(tolerances)

Check warning on line 585 in osmnx/simplification.py

View check run for this annotation

Codecov / codecov/patch

osmnx/simplification.py#L585

Added line #L585 was not covered by tests
# Replace POLYGON EMPTY with original geometries if buffer distance was effectively zero or missing
buffered_geoms = buffered_geoms.fillna(gdf_nodes["geometry"])

Check warning on line 587 in osmnx/simplification.py

View check run for this annotation

Codecov / codecov/patch

osmnx/simplification.py#L587

Added line #L587 was not covered by tests
else:
# Buffer nodes to the specified distance
buffered_geoms = gdf_nodes.geometry.buffer(tolerance)

# Merge overlapping geometries into a single geometry
merged = buffered_geoms.unary_union

# if only a single node results, make it iterable to convert to GeoSeries
merged = MultiPolygon([merged]) if isinstance(merged, Polygon) else merged
Expand All @@ -574,7 +599,7 @@

def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915
G: nx.MultiDiGraph,
tolerance: float,
tolerance: float | dict[int, float],
reconnect_edges: bool, # noqa: FBT001
node_attr_aggs: dict[str, Any] | None,
) -> nx.MultiDiGraph:
Expand All @@ -599,7 +624,8 @@
A projected graph.
tolerance
Nodes are buffered to this distance (in graph's geometry's units) and
subsequent overlaps are dissolved into a single node.
subsequent overlaps are dissolved into a single node. Can be a float
value or a dictionary mapping node IDs to individual tolerance values.
reconnect_edges
If True, reconnect edges (and their geometries) to the consolidated
nodes in rebuilt graph, and update the edge length attributes. If
Expand All @@ -625,7 +651,9 @@
# 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),
)
centroids = node_clusters.centroid
node_clusters["x"] = centroids.x
node_clusters["y"] = centroids.y
Expand Down