diff --git a/CHANGELOG.md b/CHANGELOG.md index cd47c2133..9316e23cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - formally support Python 3.12 (#1082) - fix Windows-specific character encoding issue when reading XML files (#1084) +- rename add_node_elevations_google function's max_locations_per_batch parameter, with deprecation warning (#1088) +- move add_node_elevations_google function's url_template parameter to settings module, with deprecation warning (#1088) ## 1.7.1 (2023-10-29) diff --git a/osmnx/elevation.py b/osmnx/elevation.py index f0f7c2719..cecf23cf9 100644 --- a/osmnx/elevation.py +++ b/osmnx/elevation.py @@ -164,18 +164,19 @@ def add_node_elevations_raster(G, filepath, band=1, cpus=None): def add_node_elevations_google( G, api_key=None, - max_locations_per_batch=350, + batch_size=350, pause=0, + max_locations_per_batch=None, precision=None, - url_template="https://maps.googleapis.com/maps/api/elevation/json?locations={}&key={}", -): # pragma: no cover + url_template=None, +): """ - Add `elevation` (meters) attribute to each node using a web service. + Add an `elevation` (meters) attribute to each node using a web service. By default, this uses the Google Maps Elevation API but you can optionally use an equivalent API with the same interface and response format, such as - Open Topo Data. The Google Maps Elevation API requires an API key but - other providers may not. + Open Topo Data, via the `settings` module's `elevation_url_template`. The + Google Maps Elevation API requires an API key but other providers may not. For a free local alternative see the `add_node_elevations_raster` function. See also the `add_edge_grades` function. @@ -186,25 +187,34 @@ def add_node_elevations_google( input graph api_key : string a valid API key, can be None if the API does not require a key - max_locations_per_batch : int + batch_size : int max number of coordinate pairs to submit in each API call (if this is too high, the server will reject the request because its character limit exceeds the max allowed) pause : float time to pause between API calls, which can be increased if you get rate limited + max_locations_per_batch : int + deprecated, do not use precision : int deprecated, do not use url_template : string - a URL string template for the API endpoint, containing exactly two - parameters: `locations` and `key`; for example, for Open Topo Data: - `"https://api.opentopodata.org/v1/aster30m?locations={}&key={}"` + deprecated, do not use Returns ------- G : networkx.MultiDiGraph graph with node elevation attributes """ + if max_locations_per_batch is None: + max_locations_per_batch = batch_size + else: + warn( + "the `max_locations_per_batch` parameter is deprecated and will be " + "removed in a future release, use the `batch_size` parameter instead", + stacklevel=2, + ) + if precision is None: precision = 3 else: @@ -213,6 +223,16 @@ def add_node_elevations_google( stacklevel=2, ) + if url_template is None: + url_template = settings.elevation_url_template + else: + warn( + "the `url_template` parameter is deprecated and will be removed " + "in a future release, configure the `settings` module's " + "`elevation_url_template` instead", + stacklevel=2, + ) + # make a pandas series of all the nodes' coordinates as 'lat,lon' # round coordinates to 5 decimal places (approx 1 meter) to be able to fit # in more locations per API call @@ -223,22 +243,25 @@ def add_node_elevations_google( domain = _downloader._hostname_from_url(url_template) utils.log(f"Requesting node elevations from {domain!r} in {n_calls} request(s)") - # break the series of coordinates into chunks of size max_locations_per_batch + # break the series of coordinates into chunks of max_locations_per_batch # API format is locations=lat,lon|lat,lon|lat,lon|lat,lon... results = [] for i in range(0, len(node_points), max_locations_per_batch): chunk = node_points.iloc[i : i + max_locations_per_batch] locations = "|".join(chunk) - url = url_template.format(locations, api_key) + url = url_template.format(locations=locations, key=api_key) # download and append these elevation results to list of all results response_json = _elevation_request(url, pause) - results.extend(response_json["results"]) + if "results" in response_json and len(response_json["results"]) > 0: + results.extend(response_json["results"]) + else: + raise InsufficientResponseError(str(response_json)) # sanity check that all our vectors have the same number of elements msg = f"Graph has {len(G):,} nodes and we received {len(results):,} results from {domain!r}" utils.log(msg) - if not (len(results) == len(G) == len(node_points)): + if not (len(results) == len(G) == len(node_points)): # pragma: no cover err_msg = f"{msg}\n{response_json}" raise InsufficientResponseError(err_msg) diff --git a/osmnx/settings.py b/osmnx/settings.py index d9d35dd30..7d44c96c7 100644 --- a/osmnx/settings.py +++ b/osmnx/settings.py @@ -48,6 +48,12 @@ Endpoint to resolve DNS-over-HTTPS if local DNS resolution fails. Set to None to disable DoH, but see `downloader._config_dns` documentation for caveats. Default is: `"https://8.8.8.8/resolve?name={hostname}"` +elevation_url_template : string + Endpoint of the Google Maps Elevation API (or equivalent), containing + exactly two parameters: `locations` and `key`. Default is: + `"https://maps.googleapis.com/maps/api/elevation/json?locations={locations}&key={key}"` + One example of an alternative equivalent would be Open Topo Data: + `"https://api.opentopodata.org/v1/aster30m?locations={locations}&key={key}"` imgs_folder : string or pathlib.Path Path to folder in which to save plotted images by default. Default is `"./images"`. @@ -92,7 +98,7 @@ Edge tags for for saving .osm XML files with `save_graph_xml` function. Default is `["highway", "lanes", "maxspeed", "name", "oneway"]`. overpass_endpoint : string - The base API url to use for overpass queries. Default is + The base API url to use for Overpass queries. Default is `"https://overpass-api.de/api"`. overpass_rate_limit : bool If True, check the Overpass server status endpoint for how long to @@ -140,6 +146,9 @@ default_referer = "OSMnx Python package (https://github.com/gboeing/osmnx)" default_user_agent = "OSMnx Python package (https://github.com/gboeing/osmnx)" doh_url_template = "https://8.8.8.8/resolve?name={hostname}" +elevation_url_template = ( + "https://maps.googleapis.com/maps/api/elevation/json?locations={locations}&key={key}" +) imgs_folder = "./images" log_console = False log_file = False diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index e4c1d280c..db7555afc 100755 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -216,16 +216,29 @@ def test_osm_xml(): def test_elevation(): """Test working with elevation data.""" G = ox.graph_from_address(address=address, dist=500, dist_type="bbox", network_type="bike") - rasters = list(Path("tests/input_data").glob("elevation*.tif")) # add node elevations from Google (fails without API key) with pytest.raises(ox._errors.InsufficientResponseError): - _ = ox.elevation.add_node_elevations_google(G, api_key="") + _ = ox.elevation.add_node_elevations_google( + G, + api_key="", + max_locations_per_batch=350, + precision=2, + url_template=ox.settings.elevation_url_template, + ) + + # add node elevations from Open Topo Data (works without API key) + ox.settings.elevation_url_template = ( + "https://api.opentopodata.org/v1/aster30m?locations={locations}&key={key}" + ) + _ = ox.elevation.add_node_elevations_google(G, batch_size=100, pause=0.01) # add node elevations from a single raster file (some nodes will be null) + rasters = list(Path("tests/input_data").glob("elevation*.tif")) G = ox.elevation.add_node_elevations_raster(G, rasters[0], cpus=1) + assert pd.notnull(pd.Series(dict(G.nodes(data="elevation")))).any() - # add node elevations from multiple raster files + # add node elevations from multiple raster files (no nodes should be null) G = ox.elevation.add_node_elevations_raster(G, rasters) assert pd.notnull(pd.Series(dict(G.nodes(data="elevation")))).all()