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

Retain unique attribute values when consolidating nodes #1144

Merged
merged 7 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Read the v2 [migration guide](https://github.com/gboeing/osmnx/issues/1123)
- remove save_graph_xml function's node_tags, node_attrs, edge_tags, edge_attrs, merge_edges, oneway, api_version, and precision parameters (#1135)
- make save_graph_xml function accept only an unsimplified MultiDiGraph as its input data (#1135)
- replace save_graph_xml function's edge_tag_aggs tuple parameter with way_tag_aggs dict parameter (#1135)
- make consolidate_intersections function retain unique attribute values when consolidating nodes (#1144)
- add OSM junction and railway tags to the default settings.useful_tags_node (#1144)
- fix graph projection creating useless lat and lon node attributes (#1144)
- make optional function parameters keyword-only throughout package (#1134)
- make dist function parameters required rather than optional throughout package (#1134)
- make which_result function parameter consistently able to accept a list throughout package (#1113)
Expand Down
18 changes: 10 additions & 8 deletions osmnx/_osm_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,14 @@ def _save_graph_xml(
else:
gdf[col] = gdf[col].fillna(value)

# warn user if graph is projected then remove lat/lon gdf_nodes columns if
# they exist, as x/y cols will be saved as lat/lon node attributes instead
# warn user if graph is projected
if projection.is_projected(G.graph["crs"]):
msg = (
"Graph should be unprojected: the existing lat-lon node attributes will "
"be discarded and the projected x-y coordinates will be saved as lat-lon "
"node attributes instead. Project your graph back to lat-lon to avoid this."
"Graph should be unprojected: the existing projected x-y coordinates "
"will be saved as lat-lon node attributes. Project your graph back to "
"lat-lon to avoid this."
)
warn(msg, category=UserWarning, stacklevel=2)
for col in set(gdf_nodes.columns) & {"lat", "lon"}:
gdf_nodes = gdf_nodes.drop(columns=[col])

# transform nodes gdf to meet OSM XML spec
# 1) reset index (osmid) then rename osmid, x, and y columns
Expand Down Expand Up @@ -279,7 +276,12 @@ def _add_nodes_xml(
node_element = SubElement(parent, "node", attrib=attrs)

# add each node tag dict as its own SubElement of the node SubElement
tags = ({"k": k, "v": str(node[k])} for k in node_tags & node.keys() if pd.notna(node[k]))
# for vals that are non-null (or list if node consolidation was done)
tags = (
{"k": k, "v": str(node[k])}
for k in node_tags & node.keys()
if isinstance(node[k], list) or pd.notna(node[k])
)
for tag in tags:
_ = SubElement(node_element, "tag", attrib=tag)

Expand Down
2 changes: 0 additions & 2 deletions osmnx/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,6 @@ def load_graphml(
default_node_dtypes = {
"elevation": float,
"elevation_res": float,
"lat": float,
"lon": float,
"osmid": int,
"street_count": int,
"x": float,
Expand Down
6 changes: 0 additions & 6 deletions osmnx/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,6 @@ def project_graph(
# STEP 1: PROJECT THE NODES
gdf_nodes = utils_graph.graph_to_gdfs(G, edges=False)

# create new lat/lon columns to preserve lat/lon for later reference if
# cols do not already exist (ie, don't overwrite in later re-projections)
if "lon" not in gdf_nodes.columns or "lat" not in gdf_nodes.columns:
gdf_nodes["lon"] = gdf_nodes["x"]
gdf_nodes["lat"] = gdf_nodes["y"]

# project the nodes GeoDataFrame and extract the projected x/y values
gdf_nodes_proj = project_gdf(gdf_nodes, to_crs=to_crs)
gdf_nodes_proj["x"] = gdf_nodes_proj["geometry"].x
Expand Down
4 changes: 2 additions & 2 deletions osmnx/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
API repeatedly for the same request. Default is `True`.
useful_tags_node : list[str]
OSM "node" tags to add as graph node attributes, when present in the data
retrieved from OSM. Default is `["highway", "ref"]`.
retrieved from OSM. Default is `["highway", "junction", "railway", "ref"]`.
useful_tags_way : list[str]
OSM "way" tags to add as graph edge attributes, when present in the data
retrieved from OSM. Default is `["access", "area", "bridge", "est_width",
Expand Down Expand Up @@ -162,7 +162,7 @@
requests_kwargs: dict[str, Any] = {}
requests_timeout: float = 180
use_cache: bool = True
useful_tags_node: list[str] = ["highway", "ref"]
useful_tags_node: list[str] = ["highway", "junction", "railway", "ref"]
useful_tags_way: list[str] = [
"access",
"area",
Expand Down
32 changes: 22 additions & 10 deletions osmnx/simplification.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from shapely.geometry import Point
from shapely.geometry import Polygon

from . import settings
from . import stats
from . import utils
from . import utils_graph
Expand Down Expand Up @@ -511,7 +512,7 @@ def _merge_nodes_geometric(G: nx.MultiDiGraph, tolerance: float) -> gpd.GeoSerie
return gpd.GeoSeries(merged.geoms, crs=G.graph["crs"])


def _consolidate_intersections_rebuild_graph( # noqa: PLR0912,PLR0915
def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915
G: nx.MultiDiGraph,
tolerance: float,
reconnect_edges: bool, # noqa: FBT001
Expand Down Expand Up @@ -563,7 +564,9 @@ def _consolidate_intersections_rebuild_graph( # noqa: PLR0912,PLR0915
# attach each node to its cluster of merged nodes. first get the original
# graph's node points then spatial join to give each node the label of
# cluster it's within. make cluster labels type string.
node_points = utils_graph.graph_to_gdfs(G, edges=False)[["geometry"]]
node_points = utils_graph.graph_to_gdfs(G, edges=False)
cols = set(node_points.columns).intersection(["geometry", *settings.useful_tags_node])
node_points = node_points[list(cols)]
gdf = gpd.sjoin(node_points, node_clusters, how="left", predicate="within")
gdf = gdf.drop(columns="geometry").rename(columns={"index_right": "cluster"})
gdf["cluster"] = gdf["cluster"].astype(str)
Expand Down Expand Up @@ -608,14 +611,23 @@ def _consolidate_intersections_rebuild_graph( # noqa: PLR0912,PLR0915
osmid = osmids[0]
H.add_node(cluster_label, osmid_original=osmid, **G.nodes[osmid])
else:
# if cluster is multiple merged nodes, create one new node to
# represent them
H.add_node(
cluster_label,
osmid_original=str(osmids),
x=nodes_subset["x"].iloc[0],
y=nodes_subset["y"].iloc[0],
)
# if cluster is multiple merged nodes, create one new node with
# attributes to represent them
node_attrs = {
"osmid_original": osmids,
"x": nodes_subset["x"].iloc[0],
"y": nodes_subset["y"].iloc[0],
}
for col in set(nodes_subset.columns).intersection(settings.useful_tags_node):
# get the unique non-null values (we won't add null attrs)
unique_vals = list(set(nodes_subset[col].dropna()))
if len(unique_vals) == 1:
# if there's 1 unique value for this attribute, keep it
node_attrs[col] = unique_vals[0]
elif len(unique_vals) > 1:
# if there are multiple unique values, keep all uniques
node_attrs[col] = unique_vals
H.add_node(cluster_label, **node_attrs)

# calculate street_count attribute for all nodes lacking it
null_nodes = [n for n, sc in H.nodes(data="street_count") if sc is None]
Expand Down