From af5b43aa9c8a5f23f2fca60cecc92cfc269d743f Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Tue, 12 Mar 2024 16:12:12 -0700 Subject: [PATCH 01/11] deprecate simplify_graph function's endpoint_attrs argument --- CHANGELOG.md | 6 +++--- osmnx/simplification.py | 34 +++++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35dc767d6..95c558135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## 1.9.2 (TBD) -- deprecate settings module's default_accept_language, default_referer, default_user_agent, memory, nominatim_endpoint, overpass_endpoint, and timeout settings (#1138) -- deprecate settings module's osm_xml_node_attrs, osm_xml_node_tags, osm_xml_way_attrs, and osm_xml_way_tags settings (#1138) -- deprecate save_graph_xml function's node_tags, node_attrs, edge_tags, edge_attrs, edge_tag_aggs, merge_edges, oneway, api_version, and precision parameters (#1138) +- deprecate settings module's renamed or obsolete settings (#1138) +- deprecate save_graph_xml function's renamed or obsolete parameters (#1138) +- deprecate simplify_graph function's endpoint_attrs argument: renamed to edge_attrs (#1146) ## 1.9.1 (2024-02-01) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 6344a04c0..07b02fbd2 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -228,7 +228,9 @@ def _remove_rings(G, endpoint_attrs): return G -def simplify_graph(G, strict=None, endpoint_attrs=None, remove_rings=True, track_merged=False): # noqa: C901 +def simplify_graph( # noqa: C901 + G, strict=None, edge_attrs=None, endpoint_attrs=None, remove_rings=True, track_merged=False +): """ Simplify a graph's topology by removing interstitial nodes. @@ -243,8 +245,8 @@ def simplify_graph(G, strict=None, endpoint_attrs=None, remove_rings=True, track simplified edges can receive a `merged_edges` attribute that contains a list of all the (u, v) node pairs that were merged together. - Use the `endpoint_attrs` parameter to relax simplification strictness. For - example, `endpoint_attrs=['osmid']` will retain every node whose incident + Use the `edge_attrs` parameter to relax simplification strictness. For + example, `edge_attrs=['osmid']` will retain every node whose incident edges have different OSM IDs. This lets you keep nodes at elbow two-way intersections (but be aware that sometimes individual blocks have multiple OSM IDs within them too). You could also use this parameter to retain @@ -256,11 +258,13 @@ def simplify_graph(G, strict=None, endpoint_attrs=None, remove_rings=True, track input graph strict : bool deprecated, do not use - endpoint_attrs : iterable + edge_attrs : iterable An iterable of edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident edges have different values then each other for any of the - edge attributes in `endpoint_attrs`. + edge attributes in `edge_attrs`. + endpoint_attrs : iterable + deprecated, do not use remove_rings : bool if True, remove isolated self-contained rings that have no endpoints track_merged : bool @@ -273,17 +277,25 @@ def simplify_graph(G, strict=None, endpoint_attrs=None, remove_rings=True, track topologically simplified graph, with a new `geometry` attribute on each simplified edge """ + if endpoint_attrs is not None: + msg = ( + "The `endpoint_attrs` parameter has been deprecated and will be removed " + "in the v2.0.0 release. Use the `edge_attrs` parameter instead." + ) + warn(msg, FutureWarning, stacklevel=2) + edge_attrs = endpoint_attrs + if strict is not None: msg = ( "The `strict` parameter has been deprecated and will be removed in " - "the v2.0.0 release. Use the `endpoint_attrs` parameter instead to " - "relax simplification strictness. For example, `endpoint_attrs=None` " - "reproduces the old `strict=True` behvavior and `endpoint_attrs=['osmid']` " + "the v2.0.0 release. Use the `edge_attrs` parameter instead to " + "relax simplification strictness. For example, `edge_attrs=None` " + "reproduces the old `strict=True` behvavior and `edge_attrs=['osmid']` " "reproduces the old `strict=False` behavior." ) warn(msg, FutureWarning, stacklevel=2) # maintain old behavior if strict is passed during deprecation - endpoint_attrs = None if strict else ["osmid"] + edge_attrs = None if strict else ["osmid"] if "simplified" in G.graph and G.graph["simplified"]: # pragma: no cover msg = "This graph has already been simplified, cannot simplify it again." @@ -302,7 +314,7 @@ def simplify_graph(G, strict=None, endpoint_attrs=None, remove_rings=True, track all_edges_to_add = [] # generate each path that needs to be simplified - for path in _get_paths_to_simplify(G, endpoint_attrs): + for path in _get_paths_to_simplify(G, edge_attrs): # add the interstitial edges we're removing to a list so we can retain # their spatial geometry merged_edges = [] @@ -372,7 +384,7 @@ def simplify_graph(G, strict=None, endpoint_attrs=None, remove_rings=True, track G.remove_nodes_from(set(all_nodes_to_remove)) if remove_rings: - G = _remove_rings(G, endpoint_attrs) + G = _remove_rings(G, edge_attrs) # mark the graph as having been simplified G.graph["simplified"] = True From 71066f357262303ce5018302176edabd8632b6f0 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Wed, 13 Mar 2024 12:32:02 -0700 Subject: [PATCH 02/11] rename edge_attrs to edge_attrs_differ --- CHANGELOG.md | 2 +- osmnx/simplification.py | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95c558135..5b1e45117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - deprecate settings module's renamed or obsolete settings (#1138) - deprecate save_graph_xml function's renamed or obsolete parameters (#1138) -- deprecate simplify_graph function's endpoint_attrs argument: renamed to edge_attrs (#1146) +- deprecate simplify_graph function's endpoint_attrs argument: renamed to edge_attrs_differ (#1146) ## 1.9.1 (2024-02-01) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 07b02fbd2..8eac0194f 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -229,7 +229,12 @@ def _remove_rings(G, endpoint_attrs): def simplify_graph( # noqa: C901 - G, strict=None, edge_attrs=None, endpoint_attrs=None, remove_rings=True, track_merged=False + G, + strict=None, + edge_attrs_differ=None, + endpoint_attrs=None, + remove_rings=True, + track_merged=False, ): """ Simplify a graph's topology by removing interstitial nodes. @@ -245,8 +250,8 @@ def simplify_graph( # noqa: C901 simplified edges can receive a `merged_edges` attribute that contains a list of all the (u, v) node pairs that were merged together. - Use the `edge_attrs` parameter to relax simplification strictness. For - example, `edge_attrs=['osmid']` will retain every node whose incident + Use the `edge_attrs_differ` parameter to relax simplification strictness. For + example, `edge_attrs_differ=['osmid']` will retain every node whose incident edges have different OSM IDs. This lets you keep nodes at elbow two-way intersections (but be aware that sometimes individual blocks have multiple OSM IDs within them too). You could also use this parameter to retain @@ -258,11 +263,11 @@ def simplify_graph( # noqa: C901 input graph strict : bool deprecated, do not use - edge_attrs : iterable + edge_attrs_differ : iterable An iterable of edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident edges have different values then each other for any of the - edge attributes in `edge_attrs`. + edge attributes in `edge_attrs_differ`. endpoint_attrs : iterable deprecated, do not use remove_rings : bool @@ -280,22 +285,22 @@ def simplify_graph( # noqa: C901 if endpoint_attrs is not None: msg = ( "The `endpoint_attrs` parameter has been deprecated and will be removed " - "in the v2.0.0 release. Use the `edge_attrs` parameter instead." + "in the v2.0.0 release. Use the `edge_attrs_differ` parameter instead." ) warn(msg, FutureWarning, stacklevel=2) - edge_attrs = endpoint_attrs + edge_attrs_differ = endpoint_attrs if strict is not None: msg = ( "The `strict` parameter has been deprecated and will be removed in " - "the v2.0.0 release. Use the `edge_attrs` parameter instead to " - "relax simplification strictness. For example, `edge_attrs=None` " - "reproduces the old `strict=True` behvavior and `edge_attrs=['osmid']` " + "the v2.0.0 release. Use the `edge_attrs_differ` parameter instead to " + "relax simplification strictness. For example, `edge_attrs_differ=None` " + "reproduces the old `strict=True` behvavior and `edge_attrs_differ=['osmid']` " "reproduces the old `strict=False` behavior." ) warn(msg, FutureWarning, stacklevel=2) # maintain old behavior if strict is passed during deprecation - edge_attrs = None if strict else ["osmid"] + edge_attrs_differ = None if strict else ["osmid"] if "simplified" in G.graph and G.graph["simplified"]: # pragma: no cover msg = "This graph has already been simplified, cannot simplify it again." @@ -314,7 +319,7 @@ def simplify_graph( # noqa: C901 all_edges_to_add = [] # generate each path that needs to be simplified - for path in _get_paths_to_simplify(G, edge_attrs): + for path in _get_paths_to_simplify(G, edge_attrs_differ): # add the interstitial edges we're removing to a list so we can retain # their spatial geometry merged_edges = [] @@ -384,7 +389,7 @@ def simplify_graph( # noqa: C901 G.remove_nodes_from(set(all_nodes_to_remove)) if remove_rings: - G = _remove_rings(G, edge_attrs) + G = _remove_rings(G, edge_attrs_differ) # mark the graph as having been simplified G.graph["simplified"] = True From 3cd9813f213771663056c04d8de0b03d37a177d9 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Wed, 13 Mar 2024 17:07:42 -0700 Subject: [PATCH 03/11] move functionality then deprecate speed and utils_graph modules for v2 --- docs/source/internals-reference.rst | 8 + docs/source/user-reference.rst | 6 + osmnx/_api.py | 9 +- osmnx/convert.py | 402 +++++++++++++++++++++++ osmnx/routing.py | 271 ++++++++++++++++ osmnx/speed.py | 236 ++------------ osmnx/truncate.py | 62 ++++ osmnx/utils_graph.py | 476 ++++++---------------------- tests/lint_test.sh | 2 +- tests/test_osmnx.py | 25 +- 10 files changed, 881 insertions(+), 616 deletions(-) create mode 100644 osmnx/convert.py diff --git a/docs/source/internals-reference.rst b/docs/source/internals-reference.rst index dc2091d56..e8c9181d8 100644 --- a/docs/source/internals-reference.rst +++ b/docs/source/internals-reference.rst @@ -19,6 +19,14 @@ osmnx.bearing module :private-members: :noindex: +osmnx.convert module +-------------------- + +.. automodule:: osmnx.convert + :members: + :private-members: + :noindex: + osmnx.distance module --------------------- diff --git a/docs/source/user-reference.rst b/docs/source/user-reference.rst index 51a6de9a8..18540a808 100644 --- a/docs/source/user-reference.rst +++ b/docs/source/user-reference.rst @@ -13,6 +13,12 @@ osmnx.bearing module .. automodule:: osmnx.bearing :members: +osmnx.convert module +-------------------- + +.. automodule:: osmnx.convert + :members: + osmnx.distance module --------------------- diff --git a/osmnx/_api.py b/osmnx/_api.py index d034a49a8..74e0c1fd8 100644 --- a/osmnx/_api.py +++ b/osmnx/_api.py @@ -1,8 +1,11 @@ # ruff: noqa: F401 """Expose most common parts of public API directly in package namespace.""" +from . import speed from .bearing import add_edge_bearings from .bearing import orientation_entropy +from .convert import graph_from_gdfs +from .convert import graph_to_gdfs from .distance import nearest_edges from .distance import nearest_nodes from .elevation import add_edge_grades @@ -43,12 +46,12 @@ from .plot import plot_orientation from .projection import project_gdf from .projection import project_graph +from .routing import add_edge_speeds +from .routing import add_edge_travel_times from .routing import k_shortest_paths from .routing import shortest_path from .simplification import consolidate_intersections from .simplification import simplify_graph -from .speed import add_edge_speeds -from .speed import add_edge_travel_times from .stats import basic_stats from .utils import citation from .utils import config @@ -56,5 +59,3 @@ from .utils import ts from .utils_graph import get_digraph from .utils_graph import get_undirected -from .utils_graph import graph_from_gdfs -from .utils_graph import graph_to_gdfs diff --git a/osmnx/convert.py b/osmnx/convert.py new file mode 100644 index 000000000..7299b962c --- /dev/null +++ b/osmnx/convert.py @@ -0,0 +1,402 @@ +"""Convert spatial graphs to/from different data types.""" + +import itertools +from warnings import warn + +import geopandas as gpd +import networkx as nx +import pandas as pd +from shapely.geometry import LineString +from shapely.geometry import Point + +from . import utils + + +def graph_to_gdfs(G, nodes=True, edges=True, node_geometry=True, fill_edge_geometry=True): + """ + Convert a MultiDiGraph to node and/or edge GeoDataFrames. + + This function is the inverse of `graph_from_gdfs`. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + nodes : bool + if True, convert graph nodes to a GeoDataFrame and return it + edges : bool + if True, convert graph edges to a GeoDataFrame and return it + node_geometry : bool + if True, create a geometry column from node x and y attributes + fill_edge_geometry : bool + if True, fill in missing edge geometry fields using nodes u and v + + Returns + ------- + geopandas.GeoDataFrame or tuple + gdf_nodes or gdf_edges or tuple of (gdf_nodes, gdf_edges). gdf_nodes + is indexed by osmid and gdf_edges is multi-indexed by u, v, key + following normal MultiDiGraph structure. + """ + crs = G.graph["crs"] + + if nodes: + if not G.nodes: # pragma: no cover + msg = "graph contains no nodes" + raise ValueError(msg) + + uvk, data = zip(*G.nodes(data=True)) + + if node_geometry: + # convert node x/y attributes to Points for geometry column + node_geoms = (Point(d["x"], d["y"]) for d in data) + gdf_nodes = gpd.GeoDataFrame(data, index=uvk, crs=crs, geometry=list(node_geoms)) + else: + gdf_nodes = gpd.GeoDataFrame(data, index=uvk) + + gdf_nodes.index = gdf_nodes.index.rename("osmid") + utils.log("Created nodes GeoDataFrame from graph") + + if edges: + if not G.edges: # pragma: no cover + msg = "Graph contains no edges" + raise ValueError(msg) + + u, v, k, data = zip(*G.edges(keys=True, data=True)) + + if fill_edge_geometry: + # subroutine to get geometry for every edge: if edge already has + # geometry return it, otherwise create it using the incident nodes + x_lookup = nx.get_node_attributes(G, "x") + y_lookup = nx.get_node_attributes(G, "y") + + def _make_edge_geometry(u, v, data, x=x_lookup, y=y_lookup): + if "geometry" in data: + return data["geometry"] + + # otherwise + return LineString((Point((x[u], y[u])), Point((x[v], y[v])))) + + edge_geoms = map(_make_edge_geometry, u, v, data) + gdf_edges = gpd.GeoDataFrame(data, crs=crs, geometry=list(edge_geoms)) + + else: + gdf_edges = gpd.GeoDataFrame(data) + if "geometry" not in gdf_edges.columns: + # if no edges have a geometry attribute, create null column + gdf_edges = gdf_edges.set_geometry([None] * len(gdf_edges)) + gdf_edges = gdf_edges.set_crs(crs) + + # add u, v, key attributes as index + gdf_edges["u"] = u + gdf_edges["v"] = v + gdf_edges["key"] = k + gdf_edges = gdf_edges.set_index(["u", "v", "key"]) + + utils.log("Created edges GeoDataFrame from graph") + + if nodes and edges: + return gdf_nodes, gdf_edges + + if nodes: + return gdf_nodes + + if edges: + return gdf_edges + + # otherwise + msg = "you must request nodes or edges or both" + raise ValueError(msg) + + +def graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs=None): + """ + Convert node and edge GeoDataFrames to a MultiDiGraph. + + This function is the inverse of `graph_to_gdfs` and is designed to work in + conjunction with it. + + However, you can convert arbitrary node and edge GeoDataFrames as long as + 1) `gdf_nodes` is uniquely indexed by `osmid`, 2) `gdf_nodes` contains `x` + and `y` coordinate columns representing node geometries, 3) `gdf_edges` is + uniquely multi-indexed by `u`, `v`, `key` (following normal MultiDiGraph + structure). This allows you to load any node/edge shapefiles or GeoPackage + layers as GeoDataFrames then convert them to a MultiDiGraph for graph + analysis. Note that any `geometry` attribute on `gdf_nodes` is discarded + since `x` and `y` provide the necessary node geometry information instead. + + Parameters + ---------- + gdf_nodes : geopandas.GeoDataFrame + GeoDataFrame of graph nodes uniquely indexed by osmid + gdf_edges : geopandas.GeoDataFrame + GeoDataFrame of graph edges uniquely multi-indexed by u, v, key + graph_attrs : dict + the new G.graph attribute dict. if None, use crs from gdf_edges as the + only graph-level attribute (gdf_edges must have crs attribute set) + + Returns + ------- + G : networkx.MultiDiGraph + """ + if not ("x" in gdf_nodes.columns and "y" in gdf_nodes.columns): # pragma: no cover + msg = "gdf_nodes must contain x and y columns" + raise ValueError(msg) + + # if gdf_nodes has a geometry attribute set, drop that column (as we use x + # and y for geometry information) and warn the user if the geometry values + # differ from the coordinates in the x and y columns + if hasattr(gdf_nodes, "geometry"): + try: + all_x_match = (gdf_nodes.geometry.x == gdf_nodes["x"]).all() + all_y_match = (gdf_nodes.geometry.y == gdf_nodes["y"]).all() + assert all_x_match + assert all_y_match + except (AssertionError, ValueError): # pragma: no cover + # AssertionError if x/y coords don't match geometry column + # ValueError if geometry column contains non-point geometry types + warn( + "discarding the gdf_nodes geometry column, though its " + "values differ from the coordinates in the x and y columns", + stacklevel=2, + ) + gdf_nodes = gdf_nodes.drop(columns=gdf_nodes.geometry.name) + + # create graph and add graph-level attribute dict + if graph_attrs is None: + graph_attrs = {"crs": gdf_edges.crs} + G = nx.MultiDiGraph(**graph_attrs) + + # add edges and their attributes to graph, but filter out null attribute + # values so that edges only get attributes with non-null values + attr_names = gdf_edges.columns.to_list() + for (u, v, k), attr_vals in zip(gdf_edges.index, gdf_edges.to_numpy()): + data_all = zip(attr_names, attr_vals) + data = {name: val for name, val in data_all if isinstance(val, list) or pd.notna(val)} + G.add_edge(u, v, key=k, **data) + + # add any nodes with no incident edges, since they wouldn't be added above + G.add_nodes_from(set(gdf_nodes.index) - set(G.nodes)) + + # now all nodes are added, so set nodes' attributes + for col in gdf_nodes.columns: + nx.set_node_attributes(G, name=col, values=gdf_nodes[col].dropna()) + + utils.log("Created graph from node/edge GeoDataFrames") + return G + + +def to_digraph(G, weight="length"): + """ + Convert MultiDiGraph to DiGraph. + + Chooses between parallel edges by minimizing `weight` attribute value. + Note: see also `get_undirected` to convert MultiDiGraph to MultiGraph. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + weight : string + attribute value to minimize when choosing between parallel edges + + Returns + ------- + networkx.DiGraph + """ + # make a copy to not mutate original graph object caller passed in + G = G.copy() + to_remove = [] + + # identify all the parallel edges in the MultiDiGraph + parallels = ((u, v) for u, v in G.edges(keys=False) if len(G.get_edge_data(u, v)) > 1) + + # among all sets of parallel edges, remove all except the one with the + # minimum "weight" attribute value + for u, v in set(parallels): + k_min, _ = min(G.get_edge_data(u, v).items(), key=lambda x: x[1][weight]) + to_remove.extend((u, v, k) for k in G[u][v] if k != k_min) + + G.remove_edges_from(to_remove) + utils.log("Converted MultiDiGraph to DiGraph") + + return nx.DiGraph(G) + + +def to_undirected(G): + """ + Convert MultiDiGraph to undirected MultiGraph. + + Maintains parallel edges only if their geometries differ. Note: see also + `get_digraph` to convert MultiDiGraph to DiGraph. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + + Returns + ------- + networkx.MultiGraph + """ + # make a copy to not mutate original graph object caller passed in + G = G.copy() + + # set from/to nodes before making graph undirected + for u, v, d in G.edges(data=True): + d["from"] = u + d["to"] = v + + # add geometry if missing, to compare parallel edges' geometries + if "geometry" not in d: + point_u = (G.nodes[u]["x"], G.nodes[u]["y"]) + point_v = (G.nodes[v]["x"], G.nodes[v]["y"]) + d["geometry"] = LineString([point_u, point_v]) + + # increment parallel edges' keys so we don't retain only one edge of sets + # of true parallel edges when we convert from MultiDiGraph to MultiGraph + G = _update_edge_keys(G) + + # convert MultiDiGraph to MultiGraph, retaining edges in both directions + # of parallel edges and self-loops for now + H = nx.MultiGraph(**G.graph) + H.add_nodes_from(G.nodes(data=True)) + H.add_edges_from(G.edges(keys=True, data=True)) + + # the previous operation added all directed edges from G as undirected + # edges in H. we now have duplicate edges for every bidirectional parallel + # edge or self-loop. so, look through the edges and remove any duplicates. + duplicate_edges = set() + for u1, v1, key1, data1 in H.edges(keys=True, data=True): + # if we haven't already flagged this edge as a duplicate + if (u1, v1, key1) not in duplicate_edges: + # look at every other edge between u and v, one at a time + for key2 in H[u1][v1]: + # don't compare this edge to itself + if key1 != key2: + # compare the first edge's data to the second's + # if they match up, flag the duplicate for removal + data2 = H.edges[u1, v1, key2] + if _is_duplicate_edge(data1, data2): + duplicate_edges.add((u1, v1, key2)) + + H.remove_edges_from(duplicate_edges) + utils.log("Converted MultiDiGraph to undirected MultiGraph") + + return H + + +def _is_duplicate_edge(data1, data2): + """ + Check if two graph edge data dicts have the same osmid and geometry. + + Parameters + ---------- + data1: dict + the first edge's data + data2 : dict + the second edge's data + + Returns + ------- + is_dupe : bool + """ + is_dupe = False + + # if either edge's osmid contains multiple values (due to simplification) + # compare them as sets to see if they contain the same values + osmid1 = set(data1["osmid"]) if isinstance(data1["osmid"], list) else data1["osmid"] + osmid2 = set(data2["osmid"]) if isinstance(data2["osmid"], list) else data2["osmid"] + + # if they contain the same osmid or set of osmids (due to simplification) + if osmid1 == osmid2: + # if both edges have geometry attributes and they match each other + if ("geometry" in data1) and ("geometry" in data2): + if _is_same_geometry(data1["geometry"], data2["geometry"]): + is_dupe = True + + # if neither edge has a geometry attribute + elif ("geometry" not in data1) and ("geometry" not in data2): + is_dupe = True + + # if one edge has geometry attribute but the other doesn't: not dupes + else: + pass + + return is_dupe + + +def _is_same_geometry(ls1, ls2): + """ + Determine if two LineString geometries are the same (in either direction). + + Check both the normal and reversed orders of their constituent points. + + Parameters + ---------- + ls1 : shapely.geometry.LineString + the first LineString geometry + ls2 : shapely.geometry.LineString + the second LineString geometry + + Returns + ------- + bool + """ + # extract coordinates from each LineString geometry + geom1 = [tuple(coords) for coords in ls1.xy] + geom2 = [tuple(coords) for coords in ls2.xy] + + # reverse the first LineString's coordinates' direction + geom1_r = [tuple(reversed(coords)) for coords in ls1.xy] + + # if second geometry matches first in either direction, return True + return geom2 in (geom1, geom1_r) # noqa: PLR6201 + + +def _update_edge_keys(G): + """ + Increment key of one edge of parallel edges that differ in geometry. + + For example, two streets from u to v that bow away from each other as + separate streets, rather than opposite direction edges of a single street. + Increment one of these edge's keys so that they do not match across u, v, + k or v, u, k so we can add both to an undirected MultiGraph. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + + Returns + ------- + G : networkx.MultiDiGraph + """ + # identify all the edges that are duplicates based on a sorted combination + # of their origin, destination, and key. that is, edge uv will match edge vu + # as a duplicate, but only if they have the same key + edges = graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) + edges["uvk"] = ["_".join(sorted([str(u), str(v)]) + [str(k)]) for u, v, k in edges.index] + mask = edges["uvk"].duplicated(keep=False) + dupes = edges[mask].dropna(subset=["geometry"]) + + different_streets = [] + groups = dupes[["geometry", "uvk"]].groupby("uvk") + + # for each group of duplicate edges + for _, group in groups: + # for each pair of edges within this group + for geom1, geom2 in itertools.combinations(group["geometry"], 2): + # if they don't have the same geometry, flag them as different + # streets: flag edge uvk, but not edge vuk, otherwise we would + # increment both their keys and they'll still duplicate each other + if not _is_same_geometry(geom1, geom2): + different_streets.append(group.index[0]) + + # for each unique different street, increment its key to make it unique + for u, v, k in set(different_streets): + new_key = max(list(G[u][v]) + list(G[v][u])) + 1 + G.add_edge(u, v, key=new_key, **G.get_edge_data(u, v, k)) + G.remove_edge(u, v, key=k) + + return G diff --git a/osmnx/routing.py b/osmnx/routing.py index 42811a0f0..024761c7a 100644 --- a/osmnx/routing.py +++ b/osmnx/routing.py @@ -2,15 +2,41 @@ import itertools import multiprocessing as mp +import re from warnings import warn import networkx as nx import numpy as np +import pandas as pd +from . import convert from . import utils from . import utils_graph +def route_to_gdf(G, route, weight="length"): + """ + Return a GeoDataFrame of the edges in a path, in order. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + route : list + list of node IDs constituting the path + weight : string + if there are parallel edges between two nodes, choose lowest weight + + Returns + ------- + gdf_edges : geopandas.GeoDataFrame + GeoDataFrame of the edges + """ + pairs = zip(route[:-1], route[1:]) + uvk = ((u, v, min(G[u][v].items(), key=lambda i: i[1][weight])[0]) for u, v in pairs) + return convert.graph_to_gdfs(G.subgraph(route), nodes=False).loc[uvk] + + def shortest_path(G, orig, dest, weight="length", cpus=1): """ Solve shortest path from origin node(s) to destination node(s). @@ -169,3 +195,248 @@ def _verify_edge_attribute(G, attr): except ValueError as e: msg = f"The edge attribute {attr!r} contains non-numeric values." raise ValueError(msg) from e + + +def add_edge_speeds(G, hwy_speeds=None, fallback=None, precision=None, agg=np.mean): + """ + Add edge speeds (km per hour) to graph as new `speed_kph` edge attributes. + + By default, this imputes free-flow travel speeds for all edges via the + mean `maxspeed` value of the edges of each highway type. For highway types + in the graph that have no `maxspeed` value on any edge, it assigns the + mean of all `maxspeed` values in graph. + + This default mean-imputation can obviously be imprecise, and the user can + override it by passing in `hwy_speeds` and/or `fallback` arguments that + correspond to local speed limit standards. The user can also specify a + different aggregation function (such as the median) to impute missing + values from the observed values. + + If edge `maxspeed` attribute has "mph" in it, value will automatically be + converted from miles per hour to km per hour. Any other speed units should + be manually converted to km per hour prior to running this function, + otherwise there could be unexpected results. If "mph" does not appear in + the edge's maxspeed attribute string, then function assumes kph, per OSM + guidelines: https://wiki.openstreetmap.org/wiki/Map_Features/Units + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + hwy_speeds : dict + dict keys = OSM highway types and values = typical speeds (km per + hour) to assign to edges of that highway type for any edges missing + speed data. Any edges with highway type not in `hwy_speeds` will be + assigned the mean preexisting speed value of all edges of that highway + type. + fallback : numeric + default speed value (km per hour) to assign to edges whose highway + type did not appear in `hwy_speeds` and had no preexisting speed + values on any edge + precision : int + deprecated, do not use + agg : function + aggregation function to impute missing values from observed values. + the default is numpy.mean, but you might also consider for example + numpy.median, numpy.nanmedian, or your own custom function + + Returns + ------- + G : networkx.MultiDiGraph + graph with speed_kph attributes on all edges + """ + if precision is None: + precision = 1 + else: + warn( + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + FutureWarning, + stacklevel=2, + ) + + if fallback is None: + fallback = np.nan + + edges = utils_graph.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) + + # collapse any highway lists (can happen during graph simplification) + # into string values simply by keeping just the first element of the list + edges["highway"] = edges["highway"].map(lambda x: x[0] if isinstance(x, list) else x) + + if "maxspeed" in edges.columns: + # collapse any maxspeed lists (can happen during graph simplification) + # into a single value + edges["maxspeed"] = edges["maxspeed"].apply(_collapse_multiple_maxspeed_values, agg=agg) + + # create speed_kph by cleaning maxspeed strings and converting mph to + # kph if necessary + edges["speed_kph"] = edges["maxspeed"].astype(str).map(_clean_maxspeed).astype(float) + else: + # if no edges in graph had a maxspeed attribute + edges["speed_kph"] = None + + # if user provided hwy_speeds, use them as default values, otherwise + # initialize an empty series to populate with values + hwy_speed_avg = pd.Series(dtype=float) if hwy_speeds is None else pd.Series(hwy_speeds).dropna() + + # for each highway type that caller did not provide in hwy_speeds, impute + # speed of type by taking the mean of the preexisting speed values of that + # highway type + for hwy, group in edges.groupby("highway"): + if hwy not in hwy_speed_avg: + hwy_speed_avg.loc[hwy] = agg(group["speed_kph"]) + + # if any highway types had no preexisting speed values, impute their speed + # with fallback value provided by caller. if fallback=np.nan, impute speed + # as the mean speed of all highway types that did have preexisting values + hwy_speed_avg = hwy_speed_avg.fillna(fallback).fillna(agg(hwy_speed_avg)) + + # for each edge missing speed data, assign it the imputed value for its + # highway type + speed_kph = ( + edges[["highway", "speed_kph"]].set_index("highway").iloc[:, 0].fillna(hwy_speed_avg) + ) + + # all speeds will be null if edges had no preexisting maxspeed data and + # caller did not pass in hwy_speeds or fallback arguments + if pd.isna(speed_kph).all(): + msg = ( + "this graph's edges have no preexisting `maxspeed` attribute " + "values so you must pass `hwy_speeds` or `fallback` arguments." + ) + raise ValueError(msg) + + # add speed kph attribute to graph edges + edges["speed_kph"] = speed_kph.round(precision).to_numpy() + nx.set_edge_attributes(G, values=edges["speed_kph"], name="speed_kph") + + return G + + +def add_edge_travel_times(G, precision=None): + """ + Add edge travel time (seconds) to graph as new `travel_time` edge attributes. + + Calculates free-flow travel time along each edge, based on `length` and + `speed_kph` attributes. Note: run `add_edge_speeds` first to generate the + `speed_kph` attribute. All edges must have `length` and `speed_kph` + attributes and all their values must be non-null. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + precision : int + deprecated, do not use + + Returns + ------- + G : networkx.MultiDiGraph + graph with travel_time attributes on all edges + """ + if precision is None: + precision = 1 + else: + warn( + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + FutureWarning, + stacklevel=2, + ) + + edges = utils_graph.graph_to_gdfs(G, nodes=False) + + # verify edge length and speed_kph attributes exist + if not ("length" in edges.columns and "speed_kph" in edges.columns): # pragma: no cover + msg = "all edges must have `length` and `speed_kph` attributes." + raise KeyError(msg) + + # verify edge length and speed_kph attributes contain no nulls + if pd.isna(edges["length"]).any() or pd.isna(edges["speed_kph"]).any(): # pragma: no cover + msg = "edge `length` and `speed_kph` values must be non-null." + raise ValueError(msg) + + # convert distance meters to km, and speed km per hour to km per second + distance_km = edges["length"] / 1000 + speed_km_sec = edges["speed_kph"] / (60 * 60) + + # calculate edge travel time in seconds + travel_time = distance_km / speed_km_sec + + # add travel time attribute to graph edges + edges["travel_time"] = travel_time.round(precision).to_numpy() + nx.set_edge_attributes(G, values=edges["travel_time"], name="travel_time") + + return G + + +def _clean_maxspeed(maxspeed, agg=np.mean, convert_mph=True): + """ + Clean a maxspeed string and convert mph to kph if necessary. + + If present, splits maxspeed on "|" (which denotes that the value contains + different speeds per lane) then aggregates the resulting values. Invalid + inputs return None. See https://wiki.openstreetmap.org/wiki/Key:maxspeed + for details on values and formats. + + Parameters + ---------- + maxspeed : string + a valid OpenStreetMap way maxspeed value + agg : function + aggregation function if maxspeed contains multiple values (default + is numpy.mean) + convert_mph : bool + if True, convert miles per hour to km per hour + + Returns + ------- + clean_value : string + """ + MILES_TO_KM = 1.60934 + # regex adapted from OSM wiki + pattern = "^([0-9][\\.,0-9]+?)(?:[ ]?(?:km/h|kmh|kph|mph|knots))?$" + values = re.split(r"\|", maxspeed) # creates a list even if it's a single value + try: + clean_values = [] + for value in values: + match = re.match(pattern, value) + clean_value = float(match.group(1).replace(",", ".")) + if convert_mph and "mph" in maxspeed.lower(): + clean_value = clean_value * MILES_TO_KM + clean_values.append(clean_value) + return agg(clean_values) + + except (ValueError, AttributeError): + # if invalid input, return None + return None + + +def _collapse_multiple_maxspeed_values(value, agg): + """ + Collapse a list of maxspeed values to a single value. + + Parameters + ---------- + value : list or string + an OSM way maxspeed value, or a list of them + agg : function + the aggregation function to reduce the list to a single value + + Returns + ------- + agg_value : int + an integer representation of the aggregated value in the list, + converted to kph if original value was in mph. + """ + # if this isn't a list, just return it right back to the caller + if not isinstance(value, list): + return value + + # otherwise, if it is a list, process it + try: + # clean each value in list and convert to kph if it is mph then + # return a single aggregated value + values = [_clean_maxspeed(x) for x in value] + return int(agg(pd.Series(values).dropna())) + except ValueError: + return None diff --git a/osmnx/speed.py b/osmnx/speed.py index 71b019910..cf8282646 100644 --- a/osmnx/speed.py +++ b/osmnx/speed.py @@ -1,255 +1,65 @@ """Calculate graph edge speeds and travel times.""" -import re from warnings import warn -import networkx as nx import numpy as np -import pandas as pd -from . import utils_graph +from . import routing def add_edge_speeds(G, hwy_speeds=None, fallback=None, precision=None, agg=np.mean): """ - Add edge speeds (km per hour) to graph as new `speed_kph` edge attributes. + Do not use: deprecated. - By default, this imputes free-flow travel speeds for all edges via the - mean `maxspeed` value of the edges of each highway type. For highway types - in the graph that have no `maxspeed` value on any edge, it assigns the - mean of all `maxspeed` values in graph. - - This default mean-imputation can obviously be imprecise, and the user can - override it by passing in `hwy_speeds` and/or `fallback` arguments that - correspond to local speed limit standards. The user can also specify a - different aggregation function (such as the median) to impute missing - values from the observed values. - - If edge `maxspeed` attribute has "mph" in it, value will automatically be - converted from miles per hour to km per hour. Any other speed units should - be manually converted to km per hour prior to running this function, - otherwise there could be unexpected results. If "mph" does not appear in - the edge's maxspeed attribute string, then function assumes kph, per OSM - guidelines: https://wiki.openstreetmap.org/wiki/Map_Features/Units + Use the `routing.route_to_gdf` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use hwy_speeds : dict - dict keys = OSM highway types and values = typical speeds (km per - hour) to assign to edges of that highway type for any edges missing - speed data. Any edges with highway type not in `hwy_speeds` will be - assigned the mean preexisting speed value of all edges of that highway - type. + deprecated, do not use fallback : numeric - default speed value (km per hour) to assign to edges whose highway - type did not appear in `hwy_speeds` and had no preexisting speed - values on any edge + deprecated, do not use precision : int deprecated, do not use agg : function - aggregation function to impute missing values from observed values. - the default is numpy.mean, but you might also consider for example - numpy.median, numpy.nanmedian, or your own custom function + deprecated, do not use Returns ------- G : networkx.MultiDiGraph - graph with speed_kph attributes on all edges """ - if precision is None: - precision = 1 - else: - warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", - FutureWarning, - stacklevel=2, - ) - - if fallback is None: - fallback = np.nan - - edges = utils_graph.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) - - # collapse any highway lists (can happen during graph simplification) - # into string values simply by keeping just the first element of the list - edges["highway"] = edges["highway"].map(lambda x: x[0] if isinstance(x, list) else x) - - if "maxspeed" in edges.columns: - # collapse any maxspeed lists (can happen during graph simplification) - # into a single value - edges["maxspeed"] = edges["maxspeed"].apply(_collapse_multiple_maxspeed_values, agg=agg) - - # create speed_kph by cleaning maxspeed strings and converting mph to - # kph if necessary - edges["speed_kph"] = edges["maxspeed"].astype(str).map(_clean_maxspeed).astype(float) - else: - # if no edges in graph had a maxspeed attribute - edges["speed_kph"] = None - - # if user provided hwy_speeds, use them as default values, otherwise - # initialize an empty series to populate with values - hwy_speed_avg = pd.Series(dtype=float) if hwy_speeds is None else pd.Series(hwy_speeds).dropna() - - # for each highway type that caller did not provide in hwy_speeds, impute - # speed of type by taking the mean of the preexisting speed values of that - # highway type - for hwy, group in edges.groupby("highway"): - if hwy not in hwy_speed_avg: - hwy_speed_avg.loc[hwy] = agg(group["speed_kph"]) - - # if any highway types had no preexisting speed values, impute their speed - # with fallback value provided by caller. if fallback=np.nan, impute speed - # as the mean speed of all highway types that did have preexisting values - hwy_speed_avg = hwy_speed_avg.fillna(fallback).fillna(agg(hwy_speed_avg)) - - # for each edge missing speed data, assign it the imputed value for its - # highway type - speed_kph = ( - edges[["highway", "speed_kph"]].set_index("highway").iloc[:, 0].fillna(hwy_speed_avg) + msg = ( + "The `add_edge_speeds` function has moved to the `routing` module. Calling " + "`speed.add_edge_speeds` is deprecated and will be removed in the " + "v2.0.0 release. Call it via `routing.add_edge_speeds` instead." ) - - # all speeds will be null if edges had no preexisting maxspeed data and - # caller did not pass in hwy_speeds or fallback arguments - if pd.isna(speed_kph).all(): - msg = ( - "this graph's edges have no preexisting `maxspeed` attribute " - "values so you must pass `hwy_speeds` or `fallback` arguments." - ) - raise ValueError(msg) - - # add speed kph attribute to graph edges - edges["speed_kph"] = speed_kph.round(precision).to_numpy() - nx.set_edge_attributes(G, values=edges["speed_kph"], name="speed_kph") - - return G + warn(msg, FutureWarning, stacklevel=2) + return routing.add_edge_speeds(G, hwy_speeds, fallback, precision, agg) def add_edge_travel_times(G, precision=None): """ - Add edge travel time (seconds) to graph as new `travel_time` edge attributes. + Do not use: deprecated. - Calculates free-flow travel time along each edge, based on `length` and - `speed_kph` attributes. Note: run `add_edge_speeds` first to generate the - `speed_kph` attribute. All edges must have `length` and `speed_kph` - attributes and all their values must be non-null. + Use the `routing.route_to_gdf` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use precision : int deprecated, do not use Returns ------- G : networkx.MultiDiGraph - graph with travel_time attributes on all edges - """ - if precision is None: - precision = 1 - else: - warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", - FutureWarning, - stacklevel=2, - ) - - edges = utils_graph.graph_to_gdfs(G, nodes=False) - - # verify edge length and speed_kph attributes exist - if not ("length" in edges.columns and "speed_kph" in edges.columns): # pragma: no cover - msg = "all edges must have `length` and `speed_kph` attributes." - raise KeyError(msg) - - # verify edge length and speed_kph attributes contain no nulls - if pd.isna(edges["length"]).any() or pd.isna(edges["speed_kph"]).any(): # pragma: no cover - msg = "edge `length` and `speed_kph` values must be non-null." - raise ValueError(msg) - - # convert distance meters to km, and speed km per hour to km per second - distance_km = edges["length"] / 1000 - speed_km_sec = edges["speed_kph"] / (60 * 60) - - # calculate edge travel time in seconds - travel_time = distance_km / speed_km_sec - - # add travel time attribute to graph edges - edges["travel_time"] = travel_time.round(precision).to_numpy() - nx.set_edge_attributes(G, values=edges["travel_time"], name="travel_time") - - return G - - -def _clean_maxspeed(maxspeed, agg=np.mean, convert_mph=True): - """ - Clean a maxspeed string and convert mph to kph if necessary. - - If present, splits maxspeed on "|" (which denotes that the value contains - different speeds per lane) then aggregates the resulting values. Invalid - inputs return None. See https://wiki.openstreetmap.org/wiki/Key:maxspeed - for details on values and formats. - - Parameters - ---------- - maxspeed : string - a valid OpenStreetMap way maxspeed value - agg : function - aggregation function if maxspeed contains multiple values (default - is numpy.mean) - convert_mph : bool - if True, convert miles per hour to km per hour - - Returns - ------- - clean_value : string - """ - MILES_TO_KM = 1.60934 - # regex adapted from OSM wiki - pattern = "^([0-9][\\.,0-9]+?)(?:[ ]?(?:km/h|kmh|kph|mph|knots))?$" - values = re.split(r"\|", maxspeed) # creates a list even if it's a single value - try: - clean_values = [] - for value in values: - match = re.match(pattern, value) - clean_value = float(match.group(1).replace(",", ".")) - if convert_mph and "mph" in maxspeed.lower(): - clean_value = clean_value * MILES_TO_KM - clean_values.append(clean_value) - return agg(clean_values) - - except (ValueError, AttributeError): - # if invalid input, return None - return None - - -def _collapse_multiple_maxspeed_values(value, agg): """ - Collapse a list of maxspeed values to a single value. - - Parameters - ---------- - value : list or string - an OSM way maxspeed value, or a list of them - agg : function - the aggregation function to reduce the list to a single value - - Returns - ------- - agg_value : int - an integer representation of the aggregated value in the list, - converted to kph if original value was in mph. - """ - # if this isn't a list, just return it right back to the caller - if not isinstance(value, list): - return value - - # otherwise, if it is a list, process it - try: - # clean each value in list and convert to kph if it is mph then - # return a single aggregated value - values = [_clean_maxspeed(x) for x in value] - return int(agg(pd.Series(values).dropna())) - except ValueError: - return None + msg = ( + "The `add_edge_travel_times` function has moved to the `routing` module. Calling " + "`speed.add_edge_travel_times` is deprecated and will be removed in the " + "v2.0.0 release. Call it via `routing.add_edge_travel_times` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return routing.add_edge_travel_times(G, precision) diff --git a/osmnx/truncate.py b/osmnx/truncate.py index a7ab54f10..175000d4e 100644 --- a/osmnx/truncate.py +++ b/osmnx/truncate.py @@ -202,3 +202,65 @@ def truncate_graph_polygon( utils.log("Truncated graph by polygon") return G + + +def remove_isolated_nodes(G): + """ + Remove from a graph all nodes that have no incident edges. + + Parameters + ---------- + G : networkx.MultiDiGraph + graph from which to remove isolated nodes + + Returns + ------- + G : networkx.MultiDiGraph + graph with all isolated nodes removed + """ + # make a copy to not mutate original graph object caller passed in + G = G.copy() + + # get the set of all isolated nodes, then remove them + isolated_nodes = {node for node, degree in G.degree() if degree < 1} + G.remove_nodes_from(isolated_nodes) + utils.log(f"Removed {len(isolated_nodes):,} isolated nodes") + return G + + +def largest_component(G, strongly=False): + """ + Get subgraph of G's largest weakly/strongly connected component. + + Parameters + ---------- + G : networkx.MultiDiGraph + input graph + strongly : bool + if True, return the largest strongly instead of weakly connected + component + + Returns + ------- + G : networkx.MultiDiGraph + the largest connected component subgraph of the original graph + """ + if strongly: + kind = "strongly" + is_connected = nx.is_strongly_connected + connected_components = nx.strongly_connected_components + else: + kind = "weakly" + is_connected = nx.is_weakly_connected + connected_components = nx.weakly_connected_components + + if not is_connected(G): + # get all the connected components in graph then identify the largest + largest_cc = max(connected_components(G), key=len) + n = len(G) + + # induce (frozen) subgraph then unfreeze it by making new MultiDiGraph + G = nx.MultiDiGraph(G.subgraph(largest_cc)) + utils.log(f"Got largest {kind} connected component ({len(G):,} of {n:,} total nodes)") + + return G diff --git a/osmnx/utils_graph.py b/osmnx/utils_graph.py index f5c077968..eb0cf806e 100644 --- a/osmnx/utils_graph.py +++ b/osmnx/utils_graph.py @@ -1,212 +1,98 @@ """Graph utility functions.""" -import itertools from warnings import warn -import geopandas as gpd -import networkx as nx -import pandas as pd -from shapely.geometry import LineString -from shapely.geometry import Point - -from . import utils +from . import convert +from . import routing +from . import truncate def graph_to_gdfs(G, nodes=True, edges=True, node_geometry=True, fill_edge_geometry=True): """ - Convert a MultiDiGraph to node and/or edge GeoDataFrames. + Do not use: deprecated. - This function is the inverse of `graph_from_gdfs`. + Use the `convert.graph_to_gdfs` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use nodes : bool - if True, convert graph nodes to a GeoDataFrame and return it + deprecated, do not use edges : bool - if True, convert graph edges to a GeoDataFrame and return it + deprecated, do not use node_geometry : bool - if True, create a geometry column from node x and y attributes + deprecated, do not use fill_edge_geometry : bool - if True, fill in missing edge geometry fields using nodes u and v + deprecated, do not use Returns ------- geopandas.GeoDataFrame or tuple - gdf_nodes or gdf_edges or tuple of (gdf_nodes, gdf_edges). gdf_nodes - is indexed by osmid and gdf_edges is multi-indexed by u, v, key - following normal MultiDiGraph structure. """ - crs = G.graph["crs"] - - if nodes: - if not G.nodes: # pragma: no cover - msg = "graph contains no nodes" - raise ValueError(msg) - - uvk, data = zip(*G.nodes(data=True)) - - if node_geometry: - # convert node x/y attributes to Points for geometry column - node_geoms = (Point(d["x"], d["y"]) for d in data) - gdf_nodes = gpd.GeoDataFrame(data, index=uvk, crs=crs, geometry=list(node_geoms)) - else: - gdf_nodes = gpd.GeoDataFrame(data, index=uvk) - - gdf_nodes.index = gdf_nodes.index.rename("osmid") - utils.log("Created nodes GeoDataFrame from graph") - - if edges: - if not G.edges: # pragma: no cover - msg = "Graph contains no edges" - raise ValueError(msg) - - u, v, k, data = zip(*G.edges(keys=True, data=True)) - - if fill_edge_geometry: - # subroutine to get geometry for every edge: if edge already has - # geometry return it, otherwise create it using the incident nodes - x_lookup = nx.get_node_attributes(G, "x") - y_lookup = nx.get_node_attributes(G, "y") - - def _make_edge_geometry(u, v, data, x=x_lookup, y=y_lookup): - if "geometry" in data: - return data["geometry"] - - # otherwise - return LineString((Point((x[u], y[u])), Point((x[v], y[v])))) - - edge_geoms = map(_make_edge_geometry, u, v, data) - gdf_edges = gpd.GeoDataFrame(data, crs=crs, geometry=list(edge_geoms)) - - else: - gdf_edges = gpd.GeoDataFrame(data) - if "geometry" not in gdf_edges.columns: - # if no edges have a geometry attribute, create null column - gdf_edges = gdf_edges.set_geometry([None] * len(gdf_edges)) - gdf_edges = gdf_edges.set_crs(crs) - - # add u, v, key attributes as index - gdf_edges["u"] = u - gdf_edges["v"] = v - gdf_edges["key"] = k - gdf_edges = gdf_edges.set_index(["u", "v", "key"]) - - utils.log("Created edges GeoDataFrame from graph") - - if nodes and edges: - return gdf_nodes, gdf_edges - - if nodes: - return gdf_nodes - - if edges: - return gdf_edges - - # otherwise - msg = "you must request nodes or edges or both" - raise ValueError(msg) + msg = ( + "The `graph_to_gdfs` function has moved to the `convert` module. Calling " + "`utils_graph.graph_to_gdfs` is deprecated and will be removed in the " + "v2.0.0 release. Call it via `convert.graph_to_gdfs` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return convert.graph_to_gdfs(G, nodes, edges, node_geometry, fill_edge_geometry) def graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs=None): """ - Convert node and edge GeoDataFrames to a MultiDiGraph. - - This function is the inverse of `graph_to_gdfs` and is designed to work in - conjunction with it. + Do not use: deprecated. - However, you can convert arbitrary node and edge GeoDataFrames as long as - 1) `gdf_nodes` is uniquely indexed by `osmid`, 2) `gdf_nodes` contains `x` - and `y` coordinate columns representing node geometries, 3) `gdf_edges` is - uniquely multi-indexed by `u`, `v`, `key` (following normal MultiDiGraph - structure). This allows you to load any node/edge shapefiles or GeoPackage - layers as GeoDataFrames then convert them to a MultiDiGraph for graph - analysis. Note that any `geometry` attribute on `gdf_nodes` is discarded - since `x` and `y` provide the necessary node geometry information instead. + Use the `convert.graph_from_gdfs` function instead. Parameters ---------- gdf_nodes : geopandas.GeoDataFrame - GeoDataFrame of graph nodes uniquely indexed by osmid + deprecated, do not use gdf_edges : geopandas.GeoDataFrame - GeoDataFrame of graph edges uniquely multi-indexed by u, v, key + deprecated, do not use graph_attrs : dict - the new G.graph attribute dict. if None, use crs from gdf_edges as the - only graph-level attribute (gdf_edges must have crs attribute set) + deprecated, do not use Returns ------- G : networkx.MultiDiGraph """ - if not ("x" in gdf_nodes.columns and "y" in gdf_nodes.columns): # pragma: no cover - msg = "gdf_nodes must contain x and y columns" - raise ValueError(msg) - - # if gdf_nodes has a geometry attribute set, drop that column (as we use x - # and y for geometry information) and warn the user if the geometry values - # differ from the coordinates in the x and y columns - if hasattr(gdf_nodes, "geometry"): - try: - all_x_match = (gdf_nodes.geometry.x == gdf_nodes["x"]).all() - all_y_match = (gdf_nodes.geometry.y == gdf_nodes["y"]).all() - assert all_x_match - assert all_y_match - except (AssertionError, ValueError): # pragma: no cover - # AssertionError if x/y coords don't match geometry column - # ValueError if geometry column contains non-point geometry types - warn( - "discarding the gdf_nodes geometry column, though its " - "values differ from the coordinates in the x and y columns", - stacklevel=2, - ) - gdf_nodes = gdf_nodes.drop(columns=gdf_nodes.geometry.name) - - # create graph and add graph-level attribute dict - if graph_attrs is None: - graph_attrs = {"crs": gdf_edges.crs} - G = nx.MultiDiGraph(**graph_attrs) - - # add edges and their attributes to graph, but filter out null attribute - # values so that edges only get attributes with non-null values - attr_names = gdf_edges.columns.to_list() - for (u, v, k), attr_vals in zip(gdf_edges.index, gdf_edges.to_numpy()): - data_all = zip(attr_names, attr_vals) - data = {name: val for name, val in data_all if isinstance(val, list) or pd.notna(val)} - G.add_edge(u, v, key=k, **data) - - # add any nodes with no incident edges, since they wouldn't be added above - G.add_nodes_from(set(gdf_nodes.index) - set(G.nodes)) - - # now all nodes are added, so set nodes' attributes - for col in gdf_nodes.columns: - nx.set_node_attributes(G, name=col, values=gdf_nodes[col].dropna()) - - utils.log("Created graph from node/edge GeoDataFrames") - return G + msg = ( + "The `graph_from_gdfs` function has moved to the `convert` module. Calling " + "`utils_graph.graph_from_gdfs` is deprecated and will be removed in the " + "v2.0.0 release. Call it via `convert.graph_from_gdfs` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return convert.graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs) def route_to_gdf(G, route, weight="length"): """ - Return a GeoDataFrame of the edges in a path, in order. + Do not use: deprecated. + + Use the `routing.route_to_gdf` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use route : list - list of node IDs constituting the path + deprecated, do not use weight : string - if there are parallel edges between two nodes, choose lowest weight + deprecated, do not use Returns ------- gdf_edges : geopandas.GeoDataFrame - GeoDataFrame of the edges """ - pairs = zip(route[:-1], route[1:]) - uvk = ((u, v, min(G[u][v].items(), key=lambda i: i[1][weight])[0]) for u, v in pairs) - return graph_to_gdfs(G.subgraph(route), nodes=False).loc[uvk] + msg = ( + "The `route_to_gdf` function has moved to the `routing` module. Calling " + "`utils_graph.route_to_gdf` is deprecated and will be removed in the " + "v2.0.0 release. Call it via `routing.route_to_gdf` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return routing.route_to_gdf(G, route, weight) def get_route_edge_attributes( @@ -215,29 +101,28 @@ def get_route_edge_attributes( """ Do not use: deprecated. - Use the `route_to_gdf` function instead. + Use the `routing.route_to_gdf` function instead. Parameters ---------- G : networkx.MultiDiGraph - deprecated + deprecated, do not use route : list - deprecated + deprecated, do not use attribute : string - deprecated + deprecated, do not use minimize_key : string - deprecated + deprecated, do not use retrieve_default : Callable[Tuple[Any, Any], Any] - deprecated + deprecated, do not use Returns ------- attribute_values : list - deprecated """ warn( "The `get_route_edge_attributes` function has been deprecated and will " - "be removed in the v2.0.0 release. Use the `route_to_gdf` function instead.", + "be removed in the v2.0.0 release. Use the `routing.route_to_gdf` function instead.", FutureWarning, stacklevel=2, ) @@ -258,277 +143,96 @@ def get_route_edge_attributes( def remove_isolated_nodes(G): """ - Remove from a graph all nodes that have no incident edges. + Do not use: deprecated. + + Use the `truncate.remove_isolated_nodes` function instead. Parameters ---------- G : networkx.MultiDiGraph - graph from which to remove isolated nodes + deprecated, do not use Returns ------- G : networkx.MultiDiGraph - graph with all isolated nodes removed """ - # make a copy to not mutate original graph object caller passed in - G = G.copy() - - # get the set of all isolated nodes, then remove them - isolated_nodes = {node for node, degree in G.degree() if degree < 1} - G.remove_nodes_from(isolated_nodes) - utils.log(f"Removed {len(isolated_nodes):,} isolated nodes") - return G + msg = ( + "The `remove_isolated_nodes` function has moved to the `truncate` module. Calling " + "`utils_graph.remove_isolated_nodes` is deprecated and will be removed in the " + "v2.0.0 release. Call it via `truncate.remove_isolated_nodes` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return truncate.remove_isolated_nodes(G) def get_largest_component(G, strongly=False): """ - Get subgraph of G's largest weakly/strongly connected component. + Do not use: deprecated. + + Use the `truncate.largest_component` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use strongly : bool - if True, return the largest strongly instead of weakly connected - component + deprecated, do not use Returns ------- G : networkx.MultiDiGraph - the largest connected component subgraph of the original graph """ - if strongly: - kind = "strongly" - is_connected = nx.is_strongly_connected - connected_components = nx.strongly_connected_components - else: - kind = "weakly" - is_connected = nx.is_weakly_connected - connected_components = nx.weakly_connected_components - - if not is_connected(G): - # get all the connected components in graph then identify the largest - largest_cc = max(connected_components(G), key=len) - n = len(G) - - # induce (frozen) subgraph then unfreeze it by making new MultiDiGraph - G = nx.MultiDiGraph(G.subgraph(largest_cc)) - utils.log(f"Got largest {kind} connected component ({len(G):,} of {n:,} total nodes)") - - return G + msg = ( + "The `get_largest_component` function is deprecated and will be removed in the " + "v2.0.0 release. Replace it with `truncate.largest_component` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return truncate.largest_component(G, strongly) def get_digraph(G, weight="length"): """ - Convert MultiDiGraph to DiGraph. + Do not use: deprecated. - Chooses between parallel edges by minimizing `weight` attribute value. - Note: see also `get_undirected` to convert MultiDiGraph to MultiGraph. + Use the `convert.to_digraph` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use weight : string - attribute value to minimize when choosing between parallel edges + deprecated, do not use Returns ------- networkx.DiGraph """ - # make a copy to not mutate original graph object caller passed in - G = G.copy() - to_remove = [] - - # identify all the parallel edges in the MultiDiGraph - parallels = ((u, v) for u, v in G.edges(keys=False) if len(G.get_edge_data(u, v)) > 1) - - # among all sets of parallel edges, remove all except the one with the - # minimum "weight" attribute value - for u, v in set(parallels): - k_min, _ = min(G.get_edge_data(u, v).items(), key=lambda x: x[1][weight]) - to_remove.extend((u, v, k) for k in G[u][v] if k != k_min) - - G.remove_edges_from(to_remove) - utils.log("Converted MultiDiGraph to DiGraph") - - return nx.DiGraph(G) + msg = ( + "The `get_digraph` function is deprecated and will be removed in the " + "v2.0.0 release. Replace it with `convert.to_digraph` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return convert.to_digraph(G, weight) def get_undirected(G): """ - Convert MultiDiGraph to undirected MultiGraph. + Do not use: deprecated. - Maintains parallel edges only if their geometries differ. Note: see also - `get_digraph` to convert MultiDiGraph to DiGraph. + Use the `convert.to_undirected` function instead. Parameters ---------- G : networkx.MultiDiGraph - input graph + deprecated, do not use Returns ------- networkx.MultiGraph """ - # make a copy to not mutate original graph object caller passed in - G = G.copy() - - # set from/to nodes before making graph undirected - for u, v, d in G.edges(data=True): - d["from"] = u - d["to"] = v - - # add geometry if missing, to compare parallel edges' geometries - if "geometry" not in d: - point_u = (G.nodes[u]["x"], G.nodes[u]["y"]) - point_v = (G.nodes[v]["x"], G.nodes[v]["y"]) - d["geometry"] = LineString([point_u, point_v]) - - # increment parallel edges' keys so we don't retain only one edge of sets - # of true parallel edges when we convert from MultiDiGraph to MultiGraph - G = _update_edge_keys(G) - - # convert MultiDiGraph to MultiGraph, retaining edges in both directions - # of parallel edges and self-loops for now - H = nx.MultiGraph(**G.graph) - H.add_nodes_from(G.nodes(data=True)) - H.add_edges_from(G.edges(keys=True, data=True)) - - # the previous operation added all directed edges from G as undirected - # edges in H. we now have duplicate edges for every bidirectional parallel - # edge or self-loop. so, look through the edges and remove any duplicates. - duplicate_edges = set() - for u1, v1, key1, data1 in H.edges(keys=True, data=True): - # if we haven't already flagged this edge as a duplicate - if (u1, v1, key1) not in duplicate_edges: - # look at every other edge between u and v, one at a time - for key2 in H[u1][v1]: - # don't compare this edge to itself - if key1 != key2: - # compare the first edge's data to the second's - # if they match up, flag the duplicate for removal - data2 = H.edges[u1, v1, key2] - if _is_duplicate_edge(data1, data2): - duplicate_edges.add((u1, v1, key2)) - - H.remove_edges_from(duplicate_edges) - utils.log("Converted MultiDiGraph to undirected MultiGraph") - - return H - - -def _is_duplicate_edge(data1, data2): - """ - Check if two graph edge data dicts have the same osmid and geometry. - - Parameters - ---------- - data1: dict - the first edge's data - data2 : dict - the second edge's data - - Returns - ------- - is_dupe : bool - """ - is_dupe = False - - # if either edge's osmid contains multiple values (due to simplification) - # compare them as sets to see if they contain the same values - osmid1 = set(data1["osmid"]) if isinstance(data1["osmid"], list) else data1["osmid"] - osmid2 = set(data2["osmid"]) if isinstance(data2["osmid"], list) else data2["osmid"] - - # if they contain the same osmid or set of osmids (due to simplification) - if osmid1 == osmid2: - # if both edges have geometry attributes and they match each other - if ("geometry" in data1) and ("geometry" in data2): - if _is_same_geometry(data1["geometry"], data2["geometry"]): - is_dupe = True - - # if neither edge has a geometry attribute - elif ("geometry" not in data1) and ("geometry" not in data2): - is_dupe = True - - # if one edge has geometry attribute but the other doesn't: not dupes - else: - pass - - return is_dupe - - -def _is_same_geometry(ls1, ls2): - """ - Determine if two LineString geometries are the same (in either direction). - - Check both the normal and reversed orders of their constituent points. - - Parameters - ---------- - ls1 : shapely.geometry.LineString - the first LineString geometry - ls2 : shapely.geometry.LineString - the second LineString geometry - - Returns - ------- - bool - """ - # extract coordinates from each LineString geometry - geom1 = [tuple(coords) for coords in ls1.xy] - geom2 = [tuple(coords) for coords in ls2.xy] - - # reverse the first LineString's coordinates' direction - geom1_r = [tuple(reversed(coords)) for coords in ls1.xy] - - # if second geometry matches first in either direction, return True - return geom2 in (geom1, geom1_r) # noqa: PLR6201 - - -def _update_edge_keys(G): - """ - Increment key of one edge of parallel edges that differ in geometry. - - For example, two streets from u to v that bow away from each other as - separate streets, rather than opposite direction edges of a single street. - Increment one of these edge's keys so that they do not match across u, v, - k or v, u, k so we can add both to an undirected MultiGraph. - - Parameters - ---------- - G : networkx.MultiDiGraph - input graph - - Returns - ------- - G : networkx.MultiDiGraph - """ - # identify all the edges that are duplicates based on a sorted combination - # of their origin, destination, and key. that is, edge uv will match edge vu - # as a duplicate, but only if they have the same key - edges = graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) - edges["uvk"] = ["_".join(sorted([str(u), str(v)]) + [str(k)]) for u, v, k in edges.index] - mask = edges["uvk"].duplicated(keep=False) - dupes = edges[mask].dropna(subset=["geometry"]) - - different_streets = [] - groups = dupes[["geometry", "uvk"]].groupby("uvk") - - # for each group of duplicate edges - for _, group in groups: - # for each pair of edges within this group - for geom1, geom2 in itertools.combinations(group["geometry"], 2): - # if they don't have the same geometry, flag them as different - # streets: flag edge uvk, but not edge vuk, otherwise we would - # increment both their keys and they'll still duplicate each other - if not _is_same_geometry(geom1, geom2): - different_streets.append(group.index[0]) - - # for each unique different street, increment its key to make it unique - for u, v, k in set(different_streets): - new_key = max(list(G[u][v]) + list(G[v][u])) + 1 - G.add_edge(u, v, key=new_key, **G.get_edge_data(u, v, k)) - G.remove_edge(u, v, key=k) - - return G + msg = ( + "The `get_undirected` function is deprecated and will be removed in the " + "v2.0.0 release. Replace it with `convert.to_undirected` instead." + ) + warn(msg, FutureWarning, stacklevel=2) + return convert.to_undirected(G) diff --git a/tests/lint_test.sh b/tests/lint_test.sh index d572e6bf3..ea529a5dc 100644 --- a/tests/lint_test.sh +++ b/tests/lint_test.sh @@ -16,7 +16,7 @@ pre-commit run --all-files #twine check --strict ./dist/* # build the docs -#make -C ./docs html +make -C ./docs html #python -m sphinx -b linkcheck ./docs/source ./docs/build/linkcheck # run the tests and report the test coverage diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index 218be39ba..10e01c2e0 100755 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -32,6 +32,7 @@ from shapely.geometry import Polygon import osmnx as ox +import osmnx.speed ox.config(log_console=True) ox.settings.log_console = True @@ -270,26 +271,26 @@ def test_routing(): G = ox.graph_from_address(address=address, dist=500, dist_type="bbox", network_type="bike") # give each edge speed and travel time attributes - G = ox.add_edge_speeds(G) + G = ox.speed.add_edge_speeds(G) G = ox.add_edge_speeds(G, hwy_speeds={"motorway": 100}, precision=2) - G = ox.add_edge_travel_times(G) + G = ox.speed.add_edge_travel_times(G) G = ox.add_edge_travel_times(G, precision=2) # test value cleaning - assert ox.speed._clean_maxspeed("100,2") == 100.2 - assert ox.speed._clean_maxspeed("100.2") == 100.2 - assert ox.speed._clean_maxspeed("100 km/h") == 100.0 - assert ox.speed._clean_maxspeed("100 mph") == pytest.approx(160.934) - assert ox.speed._clean_maxspeed("60|100") == 80 - assert ox.speed._clean_maxspeed("60|100 mph") == pytest.approx(128.7472) - assert ox.speed._clean_maxspeed("signal") is None - assert ox.speed._clean_maxspeed("100;70") is None + assert ox.routing._clean_maxspeed("100,2") == 100.2 + assert ox.routing._clean_maxspeed("100.2") == 100.2 + assert ox.routing._clean_maxspeed("100 km/h") == 100.0 + assert ox.routing._clean_maxspeed("100 mph") == pytest.approx(160.934) + assert ox.routing._clean_maxspeed("60|100") == 80 + assert ox.routing._clean_maxspeed("60|100 mph") == pytest.approx(128.7472) + assert ox.routing._clean_maxspeed("signal") is None + assert ox.routing._clean_maxspeed("100;70") is None # test collapsing multiple mph values to single kph value - assert ox.speed._collapse_multiple_maxspeed_values(["25 mph", "30 mph"], np.mean) == 44 + assert ox.routing._collapse_multiple_maxspeed_values(["25 mph", "30 mph"], np.mean) == 44 # test collapsing invalid values: should return None - assert ox.speed._collapse_multiple_maxspeed_values(["mph", "kph"], np.mean) is None + assert ox.routing._collapse_multiple_maxspeed_values(["mph", "kph"], np.mean) is None orig_x = np.array([-122.404771]) dest_x = np.array([-122.401429]) From dfe653c439495fdb6b6135a5549dee3bde3e4d08 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Wed, 13 Mar 2024 17:20:27 -0700 Subject: [PATCH 04/11] update changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1e45117..b38133e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## 1.9.2 (TBD) -- deprecate settings module's renamed or obsolete settings (#1138) -- deprecate save_graph_xml function's renamed or obsolete parameters (#1138) -- deprecate simplify_graph function's endpoint_attrs argument: renamed to edge_attrs_differ (#1146) +- deprecate settings module's renamed or obsolete settings in advance of v2 (#1138) +- deprecate save_graph_xml function's renamed or obsolete parameters in advance of v2 (#1138) +- deprecate simplify_graph function's renamed endpoint_attrs argument in advance of v2 (#1146) +- deprecate speed and utils_graph modules (functionality moved to other modules) in advance of v2 (#1146) ## 1.9.1 (2024-02-01) From f4cf69c0e4f4e92d888b082a62e7a5e02392732e Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Wed, 13 Mar 2024 17:30:52 -0700 Subject: [PATCH 05/11] update changelog --- CHANGELOG.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b38133e5b..4bebc2381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,17 @@ ## 1.9.2 (TBD) -- deprecate settings module's renamed or obsolete settings in advance of v2 (#1138) -- deprecate save_graph_xml function's renamed or obsolete parameters in advance of v2 (#1138) -- deprecate simplify_graph function's renamed endpoint_attrs argument in advance of v2 (#1146) -- deprecate speed and utils_graph modules (functionality moved to other modules) in advance of v2 (#1146) +- deprecate settings module's renamed or obsolete settings (#1138) +- deprecate save_graph_xml function's renamed or obsolete parameters (#1138) +- deprecate simplify_graph function's renamed endpoint_attrs argument (#1146) +- deprecate utils_graph.get_digraph function and replace it with covert.to_digraph function (#1146) +- deprecate utils_graph.get_undirected function and replace it with covert.to_undirected function (#1146) +- deprecate utils_graph.graph_to_gdfs function and replace it with covert.graph_to_gdfs function (#1146) +- deprecate utils_graph.graph_from_gdfs function and replace it with covert.graph_from_gdfs function (#1146) +- deprecate utils_graph.remove_isolated_nodes function and replace it with truncate.remove_isolated_nodes function (#1146) +- deprecate utils_graph.get_largest_component function and replace it with truncate.largest_component function (#1146) +- deprecate utils_graph.route_to_gdf function and replace it with routing.route_to_gdf function (#1146) +- deprecate speed module and move all of its functionality to the routing module (#1146) ## 1.9.1 (2024-02-01) From 2452c014347911c68cf196ec138d452ae87c013e Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Thu, 14 Mar 2024 10:44:16 -0700 Subject: [PATCH 06/11] do not use deprecated functions internally, add migration guide link to deprecation warnings --- osmnx/_downloader.py | 12 ++++--- osmnx/_nominatim.py | 6 ++-- osmnx/_overpass.py | 15 ++++++--- osmnx/bearing.py | 6 ++-- osmnx/convert.py | 4 +-- osmnx/distance.py | 24 +++++++++----- osmnx/elevation.py | 19 +++++++---- osmnx/features.py | 6 ++-- osmnx/folium.py | 6 ++-- osmnx/geocoder.py | 3 +- osmnx/geometries.py | 3 +- osmnx/graph.py | 15 +++++---- osmnx/io.py | 13 ++++---- osmnx/osm_xml.py | 73 ++++++++++++++++++++++++++++++----------- osmnx/plot.py | 22 ++++++++----- osmnx/projection.py | 10 +++--- osmnx/routing.py | 13 ++++---- osmnx/simplification.py | 14 ++++---- osmnx/speed.py | 10 +++--- osmnx/stats.py | 4 +-- osmnx/truncate.py | 18 +++++----- osmnx/utils.py | 3 +- osmnx/utils_geo.py | 10 +++--- osmnx/utils_graph.py | 24 +++++++++----- 24 files changed, 211 insertions(+), 122 deletions(-) diff --git a/osmnx/_downloader.py b/osmnx/_downloader.py index 7a5914edf..14e5e0305 100644 --- a/osmnx/_downloader.py +++ b/osmnx/_downloader.py @@ -157,7 +157,8 @@ def _get_http_headers(user_agent=None, referer=None, accept_language=None): default_accept_language = settings.default_accept_language msg = ( "`settings.default_accept_language` is deprecated and will be removed " - "in the v2.0.0 release: use `settings.http_accept_language` instead" + "in the v2.0.0 release: use `settings.http_accept_language` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -167,7 +168,8 @@ def _get_http_headers(user_agent=None, referer=None, accept_language=None): default_referer = settings.default_referer msg = ( "`settings.default_referer` is deprecated and will be removed in the " - "v2.0.0 release: use `settings.http_referer` instead" + "v2.0.0 release: use `settings.http_referer` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -177,7 +179,8 @@ def _get_http_headers(user_agent=None, referer=None, accept_language=None): default_user_agent = settings.default_user_agent msg = ( "`settings.default_user_agent` is deprecated and will be removed in " - "the v2.0.0 release: use `settings.http_user_agent` instead" + "the v2.0.0 release: use `settings.http_user_agent` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -222,7 +225,8 @@ def _resolve_host_via_doh(hostname): timeout = settings.timeout msg = ( "`settings.timeout` is deprecated and will be removed in the v2.0.0 " - "release: use `settings.requests_timeout` instead" + "release: use `settings.requests_timeout` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) diff --git a/osmnx/_nominatim.py b/osmnx/_nominatim.py index 8119a8dea..0a8fb1c8a 100644 --- a/osmnx/_nominatim.py +++ b/osmnx/_nominatim.py @@ -91,7 +91,8 @@ def _nominatim_request(params, request_type="search", pause=1, error_pause=60): timeout = settings.timeout msg = ( "`settings.timeout` is deprecated and will be removed in the v2.0.0 " - "release: use `settings.requests_timeout` instead" + "release: use `settings.requests_timeout` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -101,7 +102,8 @@ def _nominatim_request(params, request_type="search", pause=1, error_pause=60): nominatim_endpoint = settings.nominatim_endpoint msg = ( "`settings.nominatim_endpoint` is deprecated and will be removed in the " - "v2.0.0 release: use `settings.nominatim_url` instead" + "v2.0.0 release: use `settings.nominatim_url` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) diff --git a/osmnx/_overpass.py b/osmnx/_overpass.py index 18a9db1f6..a6326ff95 100644 --- a/osmnx/_overpass.py +++ b/osmnx/_overpass.py @@ -129,7 +129,8 @@ def _get_overpass_pause(base_endpoint, recursive_delay=5, default_duration=60): timeout = settings.timeout msg = ( "`settings.timeout` is deprecated and will be removed in the " - "v2.0.0 release: use `settings.requests_timeout` instead" + "v2.0.0 release: use `settings.requests_timeout` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -200,7 +201,8 @@ def _make_overpass_settings(): timeout = settings.timeout msg = ( "`settings.timeout` is deprecated and will be removed in the " - "v2.0.0 release: use `settings.requests_timeout` instead" + "v2.0.0 release: use `settings.requests_timeout` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -210,7 +212,8 @@ def _make_overpass_settings(): memory = settings.memory msg = ( "`settings.memory` is deprecated and will be removed in the " - " v2.0.0 release: use `settings.overpass_memory` instead" + " v2.0.0 release: use `settings.overpass_memory` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -408,7 +411,8 @@ def _overpass_request(data, pause=None, error_pause=60): timeout = settings.timeout msg = ( "`settings.timeout` is deprecated and will be removed in the " - "v2.0.0 release: use `settings.requests_timeout` instead" + "v2.0.0 release: use `settings.requests_timeout` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -418,7 +422,8 @@ def _overpass_request(data, pause=None, error_pause=60): overpass_endpoint = settings.overpass_endpoint msg = ( "`settings.overpass_endpoint` is deprecated and will be removed in the " - "v2.0.0 release: use `settings.overpass_url` instead" + "v2.0.0 release: use `settings.overpass_url` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) diff --git a/osmnx/bearing.py b/osmnx/bearing.py index 28e77c7fc..e12bf2d47 100644 --- a/osmnx/bearing.py +++ b/osmnx/bearing.py @@ -81,7 +81,8 @@ def add_edge_bearings(G, precision=None): precision = 1 else: warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -300,7 +301,8 @@ def plot_orientation( """ warn( "The `plot_orientation` function moved to the `plot` module. Calling it " - "via the `bearing` module will raise an exception starting with the v2.0.0 release.", + "via the `bearing` module will raise an exception starting with the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) diff --git a/osmnx/convert.py b/osmnx/convert.py index 7299b962c..095c82b2f 100644 --- a/osmnx/convert.py +++ b/osmnx/convert.py @@ -191,7 +191,7 @@ def to_digraph(G, weight="length"): Convert MultiDiGraph to DiGraph. Chooses between parallel edges by minimizing `weight` attribute value. - Note: see also `get_undirected` to convert MultiDiGraph to MultiGraph. + Note: see also `to_undirected` to convert MultiDiGraph to MultiGraph. Parameters ---------- @@ -228,7 +228,7 @@ def to_undirected(G): Convert MultiDiGraph to undirected MultiGraph. Maintains parallel edges only if their geometries differ. Note: see also - `get_digraph` to convert MultiDiGraph to DiGraph. + `to_digraph` to convert MultiDiGraph to DiGraph. Parameters ---------- diff --git a/osmnx/distance.py b/osmnx/distance.py index 83d12b4b8..d3e0f30e4 100644 --- a/osmnx/distance.py +++ b/osmnx/distance.py @@ -8,11 +8,11 @@ from shapely.geometry import Point from shapely.strtree import STRtree +from . import convert from . import projection from . import routing from . import utils from . import utils_geo -from . import utils_graph # scipy is optional dependency for projected nearest-neighbor search try: @@ -130,7 +130,8 @@ def great_circle_vec(lat1, lng1, lat2, lng2, earth_radius=EARTH_RADIUS_M): """ warn( "The `great_circle_vec` function has been renamed `great_circle`. Calling " - "`great_circle_vec` will raise an error starting in the v2.0.0 release.", + "`great_circle_vec` will raise an error starting in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -162,7 +163,8 @@ def euclidean_dist_vec(y1, x1, y2, x2): """ warn( "The `euclidean_dist_vec` function has been renamed `euclidean`. Calling " - "`euclidean_dist_vec` will raise an error starting in the v2.0.0 release.", + "`euclidean_dist_vec` will raise an error starting in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -208,7 +210,8 @@ def add_edge_lengths(G, precision=None, edges=None): precision = 3 else: warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -279,7 +282,7 @@ def nearest_nodes(G, X, Y, return_dist=False): if np.isnan(X).any() or np.isnan(Y).any(): # pragma: no cover msg = "`X` and `Y` cannot contain nulls" raise ValueError(msg) - nodes = utils_graph.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]] + nodes = convert.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]] if projection.is_projected(G.graph["crs"]): # if projected, use k-d tree for euclidean nearest-neighbor search @@ -356,7 +359,7 @@ def nearest_edges(G, X, Y, interpolate=None, return_dist=False): if np.isnan(X).any() or np.isnan(Y).any(): # pragma: no cover msg = "`X` and `Y` cannot contain nulls" raise ValueError(msg) - geoms = utils_graph.graph_to_gdfs(G, nodes=False)["geometry"] + geoms = convert.graph_to_gdfs(G, nodes=False)["geometry"] # if no interpolation distance was provided if interpolate is None: @@ -376,7 +379,8 @@ def nearest_edges(G, X, Y, interpolate=None, return_dist=False): else: warn( "The `interpolate` parameter has been deprecated and will be " - "removed in the v2.0.0 release.", + "removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -450,7 +454,8 @@ def shortest_path(G, orig, dest, weight="length", cpus=1): """ warn( "The `shortest_path` function has moved to the `routing` module. Calling it " - "via the `distance` module will raise an error starting in the v2.0.0 release.", + "via the `distance` module will raise an error starting in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -486,7 +491,8 @@ def k_shortest_paths(G, orig, dest, k, weight="length"): """ warn( "The `k_shortest_paths` function has moved to the `routing` module. " - "Calling it via the `distance` module will raise an error in the v2.0.0 release.", + "Calling it via the `distance` module will raise an error in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) diff --git a/osmnx/elevation.py b/osmnx/elevation.py index b654b25ae..59c9fd088 100644 --- a/osmnx/elevation.py +++ b/osmnx/elevation.py @@ -12,9 +12,9 @@ import requests from . import _downloader +from . import convert from . import settings from . import utils -from . import utils_graph from ._errors import InsufficientResponseError # rasterio and gdal are optional dependencies for raster querying @@ -54,7 +54,8 @@ def add_edge_grades(G, add_absolute=True, precision=None): precision = 3 else: warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -144,7 +145,7 @@ def add_node_elevations_raster(G, filepath, band=1, cpus=None): gdal.UseExceptions() gdal.BuildVRT(filepath, filepaths).FlushCache() - nodes = utils_graph.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]] + nodes = convert.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]] if cpus == 1: elevs = dict(_query_raster(nodes, filepath, band)) else: @@ -211,7 +212,8 @@ def add_node_elevations_google( else: warn( "The `max_locations_per_batch` parameter is deprecated and will be " - "removed the v2.0.0 release, use the `batch_size` parameter instead", + "removed the v2.0.0 release, use the `batch_size` parameter instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -220,7 +222,8 @@ def add_node_elevations_google( precision = 3 else: warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -231,7 +234,8 @@ def add_node_elevations_google( warn( "The `url_template` parameter is deprecated and will be removed " "in the v2.0.0 release. Configure the `settings` module's " - "`elevation_url_template` instead", + "`elevation_url_template` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -299,7 +303,8 @@ def _elevation_request(url, pause): timeout = settings.timeout msg = ( "`settings.timeout` is deprecated and will be removed in the v2.0.0 " - "release: use `settings.requests_timeout` instead" + "release: use `settings.requests_timeout` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) diff --git a/osmnx/features.py b/osmnx/features.py index 601c23c82..2f783a610 100644 --- a/osmnx/features.py +++ b/osmnx/features.py @@ -121,7 +121,8 @@ def features_from_bbox(north=None, south=None, east=None, west=None, bbox=None, if not (north is None and south is None and east is None and west is None): msg = ( "The `north`, `south`, `east`, and `west` parameters are deprecated and " - "will be removed in the v2.0.0 release. Use the `bbox` parameter instead." + "will be removed in the v2.0.0 release. Use the `bbox` parameter instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) bbox = (north, south, east, west) @@ -269,7 +270,8 @@ def features_from_place(query, tags, which_result=None, buffer_dist=None): if buffer_dist is not None: warn( "The buffer_dist argument has been deprecated and will be removed " - "in the v2.0.0 release. Buffer your query area directly, if desired.", + "in the v2.0.0 release. Buffer your query area directly, if desired. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) diff --git a/osmnx/folium.py b/osmnx/folium.py index dbb224fe0..dcb76c35b 100644 --- a/osmnx/folium.py +++ b/osmnx/folium.py @@ -10,7 +10,7 @@ import json from warnings import warn -from . import utils_graph +from . import convert # folium is an optional dependency for the folium plotting functions try: @@ -67,7 +67,7 @@ def plot_graph_folium( stacklevel=2, ) # create gdf of all graph edges - gdf_edges = utils_graph.graph_to_gdfs(G, nodes=False) + gdf_edges = convert.graph_to_gdfs(G, nodes=False) return _plot_folium(gdf_edges, graph_map, popup_attribute, tiles, zoom, fit_bounds, **kwargs) @@ -124,7 +124,7 @@ def plot_route_folium( # create gdf of the route edges in order node_pairs = zip(route[:-1], route[1:]) uvk = ((u, v, min(G[u][v].items(), key=lambda k: k[1]["length"])[0]) for u, v in node_pairs) - gdf_edges = utils_graph.graph_to_gdfs(G.subgraph(route), nodes=False).loc[uvk] + gdf_edges = convert.graph_to_gdfs(G.subgraph(route), nodes=False).loc[uvk] return _plot_folium(gdf_edges, route_map, popup_attribute, tiles, zoom, fit_bounds, **kwargs) diff --git a/osmnx/geocoder.py b/osmnx/geocoder.py index cb0c2f165..85f972aa5 100644 --- a/osmnx/geocoder.py +++ b/osmnx/geocoder.py @@ -101,7 +101,8 @@ def geocode_to_gdf(query, which_result=None, by_osmid=False, buffer_dist=None): if buffer_dist is not None: warn( "The buffer_dist argument has been deprecated and will be removed " - "in the v2.0.0 release. Buffer your results directly, if desired.", + "in the v2.0.0 release. Buffer your results directly, if desired. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) diff --git a/osmnx/geometries.py b/osmnx/geometries.py index c12a766f7..8b1473622 100644 --- a/osmnx/geometries.py +++ b/osmnx/geometries.py @@ -13,7 +13,8 @@ "The `geometries` module and `geometries_from_X` functions have been " "renamed the `features` module and `features_from_X` functions. Use these " "instead. The `geometries` module and function names are deprecated and " - "will be removed in the v2.0.0 release." + "will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) diff --git a/osmnx/graph.py b/osmnx/graph.py index 1c3b18ce0..2fe072e91 100644 --- a/osmnx/graph.py +++ b/osmnx/graph.py @@ -25,7 +25,6 @@ from . import truncate from . import utils from . import utils_geo -from . import utils_graph from ._errors import CacheOnlyInterruptError from ._errors import InsufficientResponseError from ._version import __version__ @@ -94,7 +93,8 @@ def graph_from_bbox( if not (north is None and south is None and east is None and west is None): msg = ( "The `north`, `south`, `east`, and `west` parameters are deprecated and " - "will be removed in the v2.0.0 release. Use the `bbox` parameter instead." + "will be removed in the v2.0.0 release. Use the `bbox` parameter instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) bbox = (north, south, east, west) @@ -269,7 +269,8 @@ def graph_from_address( warn( "The `return_coords` argument has been deprecated and will be removed in " "the v2.0.0 release. Future behavior will be as though `return_coords=False`. " - "If you want the address's geocoded coordinates, use the `geocode` function.", + "If you want the address's geocoded coordinates, use the `geocode` function. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -367,7 +368,8 @@ def graph_from_place( if buffer_dist is not None: warn( "The buffer_dist argument has been deprecated and will be removed " - "in the v2.0.0 release. Buffer your query area directly, if desired.", + "in the v2.0.0 release. Buffer your query area directly, if desired. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -459,7 +461,8 @@ def graph_from_polygon( else: warn( "The clean_periphery argument has been deprecated and will be removed in " - "the v2.0.0 release. Future behavior will be as though clean_periphery=True.", + "the v2.0.0 release. Future behavior will be as though clean_periphery=True. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -658,7 +661,7 @@ def _create_graph(response_jsons, retain_all=False, bidirectional=False): # retain only the largest connected component if retain_all=False if not retain_all: - G = utils_graph.get_largest_component(G) + G = truncate.largest_component(G) utils.log(f"Created graph with {len(G):,} nodes and {len(G.edges):,} edges") diff --git a/osmnx/io.py b/osmnx/io.py index 84e0aa8c1..4c8be967c 100644 --- a/osmnx/io.py +++ b/osmnx/io.py @@ -9,10 +9,10 @@ import pandas as pd from shapely import wkt +from . import convert from . import osm_xml from . import settings from . import utils -from . import utils_graph def save_graph_geopackage(G, filepath=None, encoding="utf-8", directed=False): @@ -45,9 +45,9 @@ def save_graph_geopackage(G, filepath=None, encoding="utf-8", directed=False): # convert graph to gdfs and stringify non-numeric columns if directed: - gdf_nodes, gdf_edges = utils_graph.graph_to_gdfs(G) + gdf_nodes, gdf_edges = convert.graph_to_gdfs(G) else: - gdf_nodes, gdf_edges = utils_graph.graph_to_gdfs(utils_graph.get_undirected(G)) + gdf_nodes, gdf_edges = convert.graph_to_gdfs(convert.get_undirected(G)) gdf_nodes = _stringify_nonnumeric_cols(gdf_nodes) gdf_edges = _stringify_nonnumeric_cols(gdf_edges) @@ -86,7 +86,8 @@ def save_graph_shapefile(G, filepath=None, encoding="utf-8", directed=False): warn( "The `save_graph_shapefile` function is deprecated and will be removed " "in the v2.0.0 release. Instead, use the `save_graph_geopackage` function " - "to save graphs as GeoPackage files for subsequent GIS analysis.", + "to save graphs as GeoPackage files for subsequent GIS analysis. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -104,9 +105,9 @@ def save_graph_shapefile(G, filepath=None, encoding="utf-8", directed=False): # convert graph to gdfs and stringify non-numeric columns if directed: - gdf_nodes, gdf_edges = utils_graph.graph_to_gdfs(G) + gdf_nodes, gdf_edges = convert.graph_to_gdfs(G) else: - gdf_nodes, gdf_edges = utils_graph.graph_to_gdfs(utils_graph.get_undirected(G)) + gdf_nodes, gdf_edges = convert.graph_to_gdfs(convert.get_undirected(G)) gdf_nodes = _stringify_nonnumeric_cols(gdf_nodes) gdf_edges = _stringify_nonnumeric_cols(gdf_edges) diff --git a/osmnx/osm_xml.py b/osmnx/osm_xml.py index b30a671bc..a54ac7205 100644 --- a/osmnx/osm_xml.py +++ b/osmnx/osm_xml.py @@ -10,9 +10,10 @@ import numpy as np import pandas as pd +from . import convert from . import settings +from . import truncate from . import utils -from . import utils_graph from ._version import __version__ @@ -161,7 +162,8 @@ def save_graph_xml( warn( "The save_graph_xml function has moved from the osm_xml module to the io module. " "osm_xml.save_graph_xml has been deprecated and will be removed in the v2.0.0 " - "release. Access the function via the io module instead.", + "release. Access the function via the io module instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -248,7 +250,8 @@ def _save_graph_xml( # noqa: C901 osm_xml_node_attrs = settings.osm_xml_node_attrs msg = ( "`settings.osm_xml_node_attrs` is deprecated and will be removed " - "in the v2.0.0 release" + "in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -258,7 +261,8 @@ def _save_graph_xml( # noqa: C901 osm_xml_node_tags = settings.osm_xml_node_tags msg = ( "`settings.osm_xml_node_tags` is deprecated and will be removed " - "in the v2.0.0 release" + "in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -268,7 +272,8 @@ def _save_graph_xml( # noqa: C901 osm_xml_way_attrs = settings.osm_xml_way_attrs msg = ( "`settings.osm_xml_way_attrs` is deprecated and will be removed " - "in the v2.0.0 release" + "in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -276,7 +281,10 @@ def _save_graph_xml( # noqa: C901 osm_xml_way_tags = ["highway", "lanes", "maxspeed", "name", "oneway"] else: osm_xml_way_tags = settings.osm_xml_way_tags - msg = "`settings.osm_xml_way_tags` is deprecated and will be removed in the v2.0.0 release" + msg = ( + "`settings.osm_xml_way_tags` is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if node_tags is None: @@ -284,14 +292,18 @@ def _save_graph_xml( # noqa: C901 else: msg = ( "the `node_tags` parameter is deprecated and will be removed in the v2.0.0 release: " - "use `settings.useful_tags_node` instead starting in v2.0.0" + "use `settings.useful_tags_node` instead starting in v2.0.0. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) if node_attrs is None: node_attrs = osm_xml_node_attrs else: - msg = "the `node_attrs` parameter is deprecated and will be removed in the v2.0.0 release" + msg = ( + "the `node_attrs` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if edge_tags is None: @@ -299,26 +311,36 @@ def _save_graph_xml( # noqa: C901 else: msg = ( "the `edge_tags` parameter is deprecated and will be removed in the v2.0.0 release: " - "use `settings.useful_tags_way` instead starting in v2.0.0" + "use `settings.useful_tags_way` instead starting in v2.0.0. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) if edge_attrs is None: edge_attrs = osm_xml_way_attrs else: - msg = "the `edge_attrs` parameter is deprecated and will be removed in the v2.0.0 release" + msg = ( + "the `edge_attrs` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if oneway is None: oneway = False else: - msg = "the `oneway` parameter is deprecated and will be removed in the v2.0.0 release" + msg = ( + "the `oneway` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if merge_edges is None: merge_edges = True else: - msg = "the `merge_edges` parameter is deprecated and will be removed in the v2.0.0 release" + msg = ( + "the `merge_edges` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if edge_tag_aggs is None: @@ -327,27 +349,40 @@ def _save_graph_xml( # noqa: C901 else: msg = ( "the `edge_tag_aggs` parameter is deprecated and will be removed in the v2.0.0 release: " - "use `way_tag_aggs` instead" + "use `way_tag_aggs` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) if api_version is None: api_version = 0.6 else: - msg = "the `api_version` parameter is deprecated and will be removed in the v2.0.0 release" + msg = ( + "the `api_version` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if precision is None: precision = 6 else: - msg = "the `precision` parameter is deprecated and will be removed in the v2.0.0 release" + msg = ( + "the `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) if not isinstance(data, nx.MultiDiGraph): - msg = "the graph to save as XML must be of type MultiDiGraph, starting in v2.0.0" + msg = ( + "the graph to save as XML must be of type MultiDiGraph, starting in v2.0.0. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) elif data.graph.get("simplified", False): - msg = "starting in v2.0.0, graph must be unsimplified to save as OSM XML" + msg = ( + "starting in v2.0.0, graph must be unsimplified to save as OSM XML. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) warn(msg, FutureWarning, stacklevel=2) # default filepath if none was provided @@ -366,7 +401,7 @@ def _save_graph_xml( # noqa: C901 try: gdf_nodes, gdf_edges = data except ValueError: - gdf_nodes, gdf_edges = utils_graph.graph_to_gdfs( + gdf_nodes, gdf_edges = convert.graph_to_gdfs( data, node_geometry=False, fill_edge_geometry=False ) @@ -641,7 +676,7 @@ def _get_unique_nodes_ordered_from_way(df_way_edges): G.add_edges_from(df_way_edges[["u", "v"]].to_numpy()) # copy nodes into new graph - H = utils_graph.get_largest_component(G, strongly=False) + H = truncate.largest_component(G, strongly=False) unique_ordered_nodes = list(nx.topological_sort(H)) num_unique_nodes = len(np.unique(all_nodes)) diff --git a/osmnx/plot.py b/osmnx/plot.py index 49a694cd3..51458aefb 100644 --- a/osmnx/plot.py +++ b/osmnx/plot.py @@ -8,13 +8,13 @@ import pandas as pd from . import bearing +from . import convert from . import graph from . import projection from . import settings from . import simplification from . import utils from . import utils_geo -from . import utils_graph # matplotlib is an optional dependency needed for visualization try: @@ -55,7 +55,8 @@ def get_colors(n, cmap="viridis", start=0.0, stop=1.0, alpha=1.0, return_hex=Fal else: warn( "The `return_hex` parameter has been deprecated and will be removed " - "in the v2.0.0 release.", + "in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -238,12 +239,12 @@ def plot_graph( if max_edge_lw > 0: # plot the edges' geometries - gdf_edges = utils_graph.graph_to_gdfs(G, nodes=False)["geometry"] + gdf_edges = convert.graph_to_gdfs(G, nodes=False)["geometry"] ax = gdf_edges.plot(ax=ax, color=edge_color, lw=edge_linewidth, alpha=edge_alpha, zorder=1) if max_node_size > 0: # scatter plot the nodes' x/y coordinates - gdf_nodes = utils_graph.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]] + gdf_nodes = convert.graph_to_gdfs(G, edges=False, node_geometry=False)[["x", "y"]] ax.scatter( x=gdf_nodes["x"], y=gdf_nodes["y"], @@ -482,7 +483,8 @@ def plot_figure_ground( else: msg = ( "The `edge_color` parameter is deprecated and will be removed in the " - "v2.0.0 release. Use `color` instead." + "v2.0.0 release. Use `color` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -491,7 +493,8 @@ def plot_figure_ground( else: msg = ( "The `smooth_joints` parameter is deprecated and will be removed in the " - "v2.0.0 release. In the future this function will behave as though True." + "v2.0.0 release. In the future this function will behave as though True. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) @@ -510,12 +513,13 @@ def plot_figure_ground( multiplier = 1.2 dep_msg = ( "The `address`, `point`, and `network_type` parameters are deprecated " - "and will be removed in the v2.0.0 release. Pass `G` instead." + "and will be removed in the v2.0.0 release. Pass `G` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) # if G was passed in, plot it centered on its node centroid if G is not None: - gdf_nodes = utils_graph.graph_to_gdfs(G, edges=False, node_geometry=True) + gdf_nodes = convert.graph_to_gdfs(G, edges=False, node_geometry=True) lonlat_point = gdf_nodes.unary_union.centroid.coords[0] point = tuple(reversed(lonlat_point)) @@ -550,7 +554,7 @@ def plot_figure_ground( raise ValueError(msg) # we need an undirected graph to find every edge incident on a node - Gu = utils_graph.get_undirected(G) + Gu = convert.get_undirected(G) # for each edge, get a linewidth according to street type edge_linewidths = [] diff --git a/osmnx/projection.py b/osmnx/projection.py index 6f68cb453..31caab7f3 100644 --- a/osmnx/projection.py +++ b/osmnx/projection.py @@ -2,9 +2,9 @@ import geopandas as gpd +from . import convert from . import settings from . import utils -from . import utils_graph def is_projected(crs): @@ -133,7 +133,7 @@ def project_graph(G, to_crs=None, to_latlong=False): to_crs = settings.default_crs # STEP 1: PROJECT THE NODES - gdf_nodes = utils_graph.graph_to_gdfs(G, edges=False) + gdf_nodes = convert.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) @@ -151,19 +151,19 @@ def project_graph(G, to_crs=None, to_latlong=False): # STEP 2: PROJECT THE EDGES if "simplified" in G.graph and G.graph["simplified"]: # if graph has previously been simplified, project the edge geometries - gdf_edges = utils_graph.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) + gdf_edges = convert.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) gdf_edges_proj = project_gdf(gdf_edges, to_crs=to_crs) else: # if not, you don't have to project these edges because the nodes # contain all the spatial data in the graph (unsimplified edges have # no geometry attributes) - gdf_edges_proj = utils_graph.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False).drop( + gdf_edges_proj = convert.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False).drop( columns=["geometry"] ) # STEP 3: REBUILD GRAPH # turn projected node/edge gdfs into a graph and update its CRS attribute - G_proj = utils_graph.graph_from_gdfs(gdf_nodes_proj, gdf_edges_proj, G.graph) + G_proj = convert.graph_from_gdfs(gdf_nodes_proj, gdf_edges_proj, G.graph) G_proj.graph["crs"] = to_crs utils.log(f"Projected graph with {len(G)} nodes and {len(G.edges)} edges") diff --git a/osmnx/routing.py b/osmnx/routing.py index 024761c7a..a9a907358 100644 --- a/osmnx/routing.py +++ b/osmnx/routing.py @@ -11,7 +11,6 @@ from . import convert from . import utils -from . import utils_graph def route_to_gdf(G, route, weight="length"): @@ -135,7 +134,7 @@ def k_shortest_paths(G, orig, dest, k, weight="length"): is a list of node IDs. """ _verify_edge_attribute(G, weight) - paths_gen = nx.shortest_simple_paths(utils_graph.get_digraph(G, weight), orig, dest, weight) + paths_gen = nx.shortest_simple_paths(convert.to_digraph(G, weight), orig, dest, weight) yield from itertools.islice(paths_gen, 0, k) @@ -249,7 +248,8 @@ def add_edge_speeds(G, hwy_speeds=None, fallback=None, precision=None, agg=np.me precision = 1 else: warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -257,7 +257,7 @@ def add_edge_speeds(G, hwy_speeds=None, fallback=None, precision=None, agg=np.me if fallback is None: fallback = np.nan - edges = utils_graph.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) + edges = convert.graph_to_gdfs(G, nodes=False, fill_edge_geometry=False) # collapse any highway lists (can happen during graph simplification) # into string values simply by keeping just the first element of the list @@ -338,12 +338,13 @@ def add_edge_travel_times(G, precision=None): precision = 1 else: warn( - "The `precision` parameter is deprecated and will be removed in the v2.0.0 release.", + "The `precision` parameter is deprecated and will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) - edges = utils_graph.graph_to_gdfs(G, nodes=False) + edges = convert.graph_to_gdfs(G, nodes=False) # verify edge length and speed_kph attributes exist if not ("length" in edges.columns and "speed_kph" in edges.columns): # pragma: no cover diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 8eac0194f..8d2675fc0 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -10,9 +10,9 @@ from shapely.geometry import Point from shapely.geometry import Polygon +from . import convert from . import stats from . import utils -from . import utils_graph from ._errors import GraphSimplificationError @@ -285,7 +285,8 @@ def simplify_graph( # noqa: C901 if endpoint_attrs is not None: msg = ( "The `endpoint_attrs` parameter has been deprecated and will be removed " - "in the v2.0.0 release. Use the `edge_attrs_differ` parameter instead." + "in the v2.0.0 release. Use the `edge_attrs_differ` parameter instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) edge_attrs_differ = endpoint_attrs @@ -296,7 +297,8 @@ def simplify_graph( # noqa: C901 "the v2.0.0 release. Use the `edge_attrs_differ` parameter instead to " "relax simplification strictness. For example, `edge_attrs_differ=None` " "reproduces the old `strict=True` behvavior and `edge_attrs_differ=['osmid']` " - "reproduces the old `strict=False` behavior." + "reproduces the old `strict=False` behavior. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) # maintain old behavior if strict is passed during deprecation @@ -508,7 +510,7 @@ def _merge_nodes_geometric(G, tolerance): the merged overlapping polygons of the buffered nodes """ # buffer nodes GeoSeries then get unary union to merge overlaps - merged = utils_graph.graph_to_gdfs(G, edges=False)["geometry"].buffer(tolerance).unary_union + merged = convert.graph_to_gdfs(G, edges=False)["geometry"].buffer(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 @@ -563,7 +565,7 @@ def _consolidate_intersections_rebuild_graph(G, tolerance=10, reconnect_edges=Tr # 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 = convert.graph_to_gdfs(G, edges=False)[["geometry"]] 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) @@ -629,7 +631,7 @@ def _consolidate_intersections_rebuild_graph(G, tolerance=10, reconnect_edges=Tr # STEP 6 # create new edge from cluster to cluster for each edge in original graph - gdf_edges = utils_graph.graph_to_gdfs(G, nodes=False) + gdf_edges = convert.graph_to_gdfs(G, nodes=False) for u, v, k, data in G.edges(keys=True, data=True): u2 = gdf.loc[u, "cluster"] v2 = gdf.loc[v, "cluster"] diff --git a/osmnx/speed.py b/osmnx/speed.py index cf8282646..ec555512d 100644 --- a/osmnx/speed.py +++ b/osmnx/speed.py @@ -11,7 +11,7 @@ def add_edge_speeds(G, hwy_speeds=None, fallback=None, precision=None, agg=np.me """ Do not use: deprecated. - Use the `routing.route_to_gdf` function instead. + Use the `routing.add_edge_speeds` function instead. Parameters ---------- @@ -33,7 +33,8 @@ def add_edge_speeds(G, hwy_speeds=None, fallback=None, precision=None, agg=np.me msg = ( "The `add_edge_speeds` function has moved to the `routing` module. Calling " "`speed.add_edge_speeds` is deprecated and will be removed in the " - "v2.0.0 release. Call it via `routing.add_edge_speeds` instead." + "v2.0.0 release. Call it via `routing.add_edge_speeds` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return routing.add_edge_speeds(G, hwy_speeds, fallback, precision, agg) @@ -43,7 +44,7 @@ def add_edge_travel_times(G, precision=None): """ Do not use: deprecated. - Use the `routing.route_to_gdf` function instead. + Use the `routing.add_edge_travel_times` function instead. Parameters ---------- @@ -59,7 +60,8 @@ def add_edge_travel_times(G, precision=None): msg = ( "The `add_edge_travel_times` function has moved to the `routing` module. Calling " "`speed.add_edge_travel_times` is deprecated and will be removed in the " - "v2.0.0 release. Call it via `routing.add_edge_travel_times` instead." + "v2.0.0 release. Call it via `routing.add_edge_travel_times` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return routing.add_edge_travel_times(G, precision) diff --git a/osmnx/stats.py b/osmnx/stats.py index d70824dea..e9f0ae7ce 100644 --- a/osmnx/stats.py +++ b/osmnx/stats.py @@ -19,11 +19,11 @@ import networkx as nx import numpy as np +from . import convert from . import distance from . import projection from . import simplification from . import utils -from . import utils_graph def streets_per_node(G): @@ -352,7 +352,7 @@ def basic_stats(G, area=None, clean_int_tol=None): - `streets_per_node_counts` - see `streets_per_node_counts` function documentation - `streets_per_node_proportions` - see `streets_per_node_proportions` function documentation """ - Gu = utils_graph.get_undirected(G) + Gu = convert.get_undirected(G) stats = {} stats["n"] = len(G.nodes) diff --git a/osmnx/truncate.py b/osmnx/truncate.py index 175000d4e..fa1f7800b 100644 --- a/osmnx/truncate.py +++ b/osmnx/truncate.py @@ -4,9 +4,9 @@ import networkx as nx +from . import convert from . import utils from . import utils_geo -from . import utils_graph def truncate_graph_dist(G, source_node, max_dist=1000, weight="length", retain_all=False): @@ -50,8 +50,8 @@ def truncate_graph_dist(G, source_node, max_dist=1000, weight="length", retain_a # remove any isolated nodes and retain only the largest component (if # retain_all is True) if not retain_all: - G = utils_graph.remove_isolated_nodes(G) - G = utils_graph.get_largest_component(G) + G = remove_isolated_nodes(G) + G = largest_component(G) utils.log(f"Truncated graph by {weight}-weighted network distance") return G @@ -105,7 +105,8 @@ def truncate_graph_bbox( if not (north is None and south is None and east is None and west is None): msg = ( "The `north`, `south`, `east`, and `west` parameters are deprecated and " - "will be removed in the v2.0.0 release. Use the `bbox` parameter instead." + "will be removed in the v2.0.0 release. Use the `bbox` parameter instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) bbox = (north, south, east, west) @@ -156,7 +157,8 @@ def truncate_graph_polygon( if quadrat_width is not None or min_num is not None: warn( "The `quadrat_width` and `min_num` parameters are deprecated and " - "will be removed in the v2.0.0 release.", + "will be removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -164,7 +166,7 @@ def truncate_graph_polygon( utils.log("Identifying all nodes that lie outside the polygon...") # first identify all nodes whose point geometries lie within the polygon - gs_nodes = utils_graph.graph_to_gdfs(G, edges=False)[["geometry"]] + gs_nodes = convert.graph_to_gdfs(G, edges=False)[["geometry"]] to_keep = utils_geo._intersect_index_quadrats(gs_nodes, polygon) if not to_keep: @@ -197,8 +199,8 @@ def truncate_graph_polygon( if not retain_all: # remove any isolated nodes and retain only the largest component - G = utils_graph.remove_isolated_nodes(G) - G = utils_graph.get_largest_component(G) + G = remove_isolated_nodes(G) + G = largest_component(G) utils.log("Truncated graph by polygon") return G diff --git a/osmnx/utils.py b/osmnx/utils.py index cb57f84a1..5a84e0278 100644 --- a/osmnx/utils.py +++ b/osmnx/utils.py @@ -212,7 +212,8 @@ def config( "The `utils.config` function is deprecated and will be removed in " "the v2.0.0 release. Instead, use the `settings` module directly to " "configure a global setting's value. For example, " - "`ox.settings.log_console=True`.", + "`ox.settings.log_console=True`. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) diff --git a/osmnx/utils_geo.py b/osmnx/utils_geo.py index 30b704694..1a4e26d51 100644 --- a/osmnx/utils_geo.py +++ b/osmnx/utils_geo.py @@ -12,10 +12,10 @@ from shapely.geometry import Polygon from shapely.ops import split +from . import convert from . import projection from . import settings from . import utils -from . import utils_graph def sample_points(G, n): @@ -44,7 +44,7 @@ def sample_points(G, n): """ if nx.is_directed(G): # pragma: no cover warn("graph should be undirected to avoid oversampling bidirectional edges", stacklevel=2) - gdf_edges = utils_graph.graph_to_gdfs(G, nodes=False)[["geometry", "length"]] + gdf_edges = convert.graph_to_gdfs(G, nodes=False)[["geometry", "length"]] weights = gdf_edges["length"] / gdf_edges["length"].sum() idx = np.random.default_rng().choice(gdf_edges.index, size=n, p=weights) lines = gdf_edges.loc[idx, "geometry"] @@ -214,7 +214,8 @@ def round_geometry_coords(geom, precision): """ warn( "The `round_geometry_coords` function is deprecated and will be " - "removed in the v2.0.0 release.", + "removed in the v2.0.0 release. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -458,7 +459,8 @@ def bbox_to_poly(north=None, south=None, east=None, west=None, bbox=None): if not (north is None and south is None and east is None and west is None): msg = ( "The `north`, `south`, `east`, and `west` parameters are deprecated and " - "will be removed in the v2.0.0 release. Use the `bbox` parameter instead." + "will be removed in the v2.0.0 release. Use the `bbox` parameter instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) else: diff --git a/osmnx/utils_graph.py b/osmnx/utils_graph.py index eb0cf806e..7b4e990d5 100644 --- a/osmnx/utils_graph.py +++ b/osmnx/utils_graph.py @@ -33,7 +33,8 @@ def graph_to_gdfs(G, nodes=True, edges=True, node_geometry=True, fill_edge_geome msg = ( "The `graph_to_gdfs` function has moved to the `convert` module. Calling " "`utils_graph.graph_to_gdfs` is deprecated and will be removed in the " - "v2.0.0 release. Call it via `convert.graph_to_gdfs` instead." + "v2.0.0 release. Call it via `convert.graph_to_gdfs` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return convert.graph_to_gdfs(G, nodes, edges, node_geometry, fill_edge_geometry) @@ -61,7 +62,8 @@ def graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs=None): msg = ( "The `graph_from_gdfs` function has moved to the `convert` module. Calling " "`utils_graph.graph_from_gdfs` is deprecated and will be removed in the " - "v2.0.0 release. Call it via `convert.graph_from_gdfs` instead." + "v2.0.0 release. Call it via `convert.graph_from_gdfs` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return convert.graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs) @@ -89,7 +91,8 @@ def route_to_gdf(G, route, weight="length"): msg = ( "The `route_to_gdf` function has moved to the `routing` module. Calling " "`utils_graph.route_to_gdf` is deprecated and will be removed in the " - "v2.0.0 release. Call it via `routing.route_to_gdf` instead." + "v2.0.0 release. Call it via `routing.route_to_gdf` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return routing.route_to_gdf(G, route, weight) @@ -122,7 +125,8 @@ def get_route_edge_attributes( """ warn( "The `get_route_edge_attributes` function has been deprecated and will " - "be removed in the v2.0.0 release. Use the `routing.route_to_gdf` function instead.", + "be removed in the v2.0.0 release. Use the `routing.route_to_gdf` function instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123", FutureWarning, stacklevel=2, ) @@ -159,7 +163,8 @@ def remove_isolated_nodes(G): msg = ( "The `remove_isolated_nodes` function has moved to the `truncate` module. Calling " "`utils_graph.remove_isolated_nodes` is deprecated and will be removed in the " - "v2.0.0 release. Call it via `truncate.remove_isolated_nodes` instead." + "v2.0.0 release. Call it via `truncate.remove_isolated_nodes` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return truncate.remove_isolated_nodes(G) @@ -184,7 +189,8 @@ def get_largest_component(G, strongly=False): """ msg = ( "The `get_largest_component` function is deprecated and will be removed in the " - "v2.0.0 release. Replace it with `truncate.largest_component` instead." + "v2.0.0 release. Replace it with `truncate.largest_component` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return truncate.largest_component(G, strongly) @@ -209,7 +215,8 @@ def get_digraph(G, weight="length"): """ msg = ( "The `get_digraph` function is deprecated and will be removed in the " - "v2.0.0 release. Replace it with `convert.to_digraph` instead." + "v2.0.0 release. Replace it with `convert.to_digraph` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return convert.to_digraph(G, weight) @@ -232,7 +239,8 @@ def get_undirected(G): """ msg = ( "The `get_undirected` function is deprecated and will be removed in the " - "v2.0.0 release. Replace it with `convert.to_undirected` instead." + "v2.0.0 release. Replace it with `convert.to_undirected` instead. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" ) warn(msg, FutureWarning, stacklevel=2) return convert.to_undirected(G) From 65c9f8dea038c98db483c630cbbaf16331d769c2 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Thu, 14 Mar 2024 10:44:39 -0700 Subject: [PATCH 07/11] deprecate graph_from_xml tags and polygon function params --- osmnx/features.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osmnx/features.py b/osmnx/features.py index 2f783a610..d466d47a8 100644 --- a/osmnx/features.py +++ b/osmnx/features.py @@ -387,6 +387,15 @@ def features_from_xml(filepath, polygon=None, tags=None, encoding="utf-8"): ------- gdf : geopandas.GeoDataFrame """ + if polygon is not None or tags is not None: + msg = ( + "The `polygon` and `tags` function parameters are deprecated and will " + "be removed in the v2.0.0 release. You can filter the features GeoDataFrame. " + "manually after construction. " + "See the OSMnx v2 migration guide: https://github.com/gboeing/osmnx/issues/1123" + ) + warn(msg, FutureWarning, stacklevel=2) + # transmogrify file of OSM XML data into JSON response_jsons = [osm_xml._overpass_json_from_file(filepath, encoding)] From ade4d1bbb8f6bf99d28d1d62eb22c797a2007fdd Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Thu, 14 Mar 2024 11:14:48 -0700 Subject: [PATCH 08/11] fix function call --- osmnx/io.py | 4 ++-- osmnx/plot.py | 2 +- osmnx/stats.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osmnx/io.py b/osmnx/io.py index 4c8be967c..12ca82fbe 100644 --- a/osmnx/io.py +++ b/osmnx/io.py @@ -47,7 +47,7 @@ def save_graph_geopackage(G, filepath=None, encoding="utf-8", directed=False): if directed: gdf_nodes, gdf_edges = convert.graph_to_gdfs(G) else: - gdf_nodes, gdf_edges = convert.graph_to_gdfs(convert.get_undirected(G)) + gdf_nodes, gdf_edges = convert.graph_to_gdfs(convert.to_undirected(G)) gdf_nodes = _stringify_nonnumeric_cols(gdf_nodes) gdf_edges = _stringify_nonnumeric_cols(gdf_edges) @@ -107,7 +107,7 @@ def save_graph_shapefile(G, filepath=None, encoding="utf-8", directed=False): if directed: gdf_nodes, gdf_edges = convert.graph_to_gdfs(G) else: - gdf_nodes, gdf_edges = convert.graph_to_gdfs(convert.get_undirected(G)) + gdf_nodes, gdf_edges = convert.graph_to_gdfs(convert.to_undirected(G)) gdf_nodes = _stringify_nonnumeric_cols(gdf_nodes) gdf_edges = _stringify_nonnumeric_cols(gdf_edges) diff --git a/osmnx/plot.py b/osmnx/plot.py index 51458aefb..6e1d15dd2 100644 --- a/osmnx/plot.py +++ b/osmnx/plot.py @@ -554,7 +554,7 @@ def plot_figure_ground( raise ValueError(msg) # we need an undirected graph to find every edge incident on a node - Gu = convert.get_undirected(G) + Gu = convert.to_undirected(G) # for each edge, get a linewidth according to street type edge_linewidths = [] diff --git a/osmnx/stats.py b/osmnx/stats.py index e9f0ae7ce..2f7314866 100644 --- a/osmnx/stats.py +++ b/osmnx/stats.py @@ -352,7 +352,7 @@ def basic_stats(G, area=None, clean_int_tol=None): - `streets_per_node_counts` - see `streets_per_node_counts` function documentation - `streets_per_node_proportions` - see `streets_per_node_proportions` function documentation """ - Gu = convert.get_undirected(G) + Gu = convert.to_undirected(G) stats = {} stats["n"] = len(G.nodes) From c93700a8dc6c05b33f23bad67d9670473f0abcda Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Thu, 14 Mar 2024 11:49:11 -0700 Subject: [PATCH 09/11] fix default arg value so deprecation warning can be avoided --- osmnx/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osmnx/plot.py b/osmnx/plot.py index 6e1d15dd2..c777d1601 100644 --- a/osmnx/plot.py +++ b/osmnx/plot.py @@ -26,7 +26,7 @@ cm = colors = plt = colormaps = None # type: ignore[assignment] -def get_colors(n, cmap="viridis", start=0.0, stop=1.0, alpha=1.0, return_hex=False): +def get_colors(n, cmap="viridis", start=0.0, stop=1.0, alpha=1.0, return_hex=None): """ Get `n` evenly-spaced colors from a matplotlib colormap. From 79600463b061e3d411250ef8d16cce95919be2a4 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Thu, 14 Mar 2024 11:54:16 -0700 Subject: [PATCH 10/11] avoid internal deprecation warning --- osmnx/plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osmnx/plot.py b/osmnx/plot.py index c777d1601..282cce044 100644 --- a/osmnx/plot.py +++ b/osmnx/plot.py @@ -537,7 +537,7 @@ def plot_figure_ground( truncate_by_edge=True, return_coords=True, ) - G = simplification.simplify_graph(G, strict=False) + G = simplification.simplify_graph(G, edge_attrs_differ=["osmid"]) elif point is not None: warn(dep_msg, FutureWarning, stacklevel=2) G = graph.graph_from_point( @@ -548,7 +548,7 @@ def plot_figure_ground( simplify=False, truncate_by_edge=True, ) - G = simplification.simplify_graph(G, strict=False) + G = simplification.simplify_graph(G, edge_attrs_differ=["osmid"]) else: # pragma: no cover msg = "You must pass an address or lat-lon point or graph." raise ValueError(msg) From a0b8fa616e171af2692da550c7e4b84cf403e391 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Thu, 14 Mar 2024 13:49:56 -0700 Subject: [PATCH 11/11] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bebc2381..d85b7af56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - deprecate settings module's renamed or obsolete settings (#1138) - deprecate save_graph_xml function's renamed or obsolete parameters (#1138) +- deprecate graph_from_xml tags and polygon function parameters (#1146) - deprecate simplify_graph function's renamed endpoint_attrs argument (#1146) - deprecate utils_graph.get_digraph function and replace it with covert.to_digraph function (#1146) - deprecate utils_graph.get_undirected function and replace it with covert.to_undirected function (#1146)