Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clean up elevation.add_node_elevations_google function parameters #1088

Merged
merged 3 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
51 changes: 37 additions & 14 deletions osmnx/elevation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)

Expand Down
11 changes: 10 additions & 1 deletion osmnx/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions tests/test_osmnx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down