Skip to content

Commit

Permalink
Merge pull request #1144 from gboeing/nodes
Browse files Browse the repository at this point in the history
Retain unique attribute values when consolidating nodes
  • Loading branch information
gboeing authored Mar 13, 2024
2 parents ea397d3 + 06c4ff0 commit 1f5604a
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 28 deletions.
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

0 comments on commit 1f5604a

Please sign in to comment.