diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9f006ce..fe9f20ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/osmnx/_osm_xml.py b/osmnx/_osm_xml.py index 89c549b77..304986e2d 100644 --- a/osmnx/_osm_xml.py +++ b/osmnx/_osm_xml.py @@ -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 @@ -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) diff --git a/osmnx/io.py b/osmnx/io.py index 3817e730f..fc9dfe7a0 100644 --- a/osmnx/io.py +++ b/osmnx/io.py @@ -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, diff --git a/osmnx/projection.py b/osmnx/projection.py index f6461c040..187d17401 100644 --- a/osmnx/projection.py +++ b/osmnx/projection.py @@ -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 diff --git a/osmnx/settings.py b/osmnx/settings.py index 4695e01b2..248ed6d07 100644 --- a/osmnx/settings.py +++ b/osmnx/settings.py @@ -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", @@ -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", diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 7de7ee908..4068e1204 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -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 @@ -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 @@ -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) @@ -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]