Skip to content

Commit

Permalink
Merge pull request #1088 from gboeing/elev
Browse files Browse the repository at this point in the history
clean up elevation.add_node_elevations_google function parameters
  • Loading branch information
gboeing authored Nov 27, 2023
2 parents 3882e33 + dfc2678 commit 7293c9c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 18 deletions.
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

0 comments on commit 7293c9c

Please sign in to comment.