diff --git a/docs/source/osmnx.rst b/docs/source/osmnx.rst index 662ed94fe..d58cf1f46 100644 --- a/docs/source/osmnx.rst +++ b/docs/source/osmnx.rst @@ -9,14 +9,6 @@ osmnx.bearing module :undoc-members: :show-inheritance: -osmnx.boundaries module ------------------------ - -.. automodule:: osmnx.boundaries - :members: - :undoc-members: - :show-inheritance: - osmnx.distance module --------------------- @@ -57,6 +49,14 @@ osmnx.footprints module :undoc-members: :show-inheritance: +osmnx.geocoding module +---------------------- + +.. automodule:: osmnx.geocoding + :members: + :undoc-members: + :show-inheritance: + osmnx.graph module ------------------ diff --git a/osmnx/_api.py b/osmnx/_api.py index 7d5453503..48be3eeac 100644 --- a/osmnx/_api.py +++ b/osmnx/_api.py @@ -15,6 +15,8 @@ from .footprints import footprints_from_place from .footprints import footprints_from_point from .footprints import footprints_from_polygon +from .geocoding import geocode +from .geocoding import geocode_to_gdf from .graph import graph_from_address from .graph import graph_from_bbox from .graph import graph_from_place @@ -46,7 +48,6 @@ from .utils import config from .utils import log from .utils import ts -from .utils_geo import geocode from .utils_graph import get_undirected from .utils_graph import graph_from_gdfs from .utils_graph import graph_to_gdfs diff --git a/osmnx/boundaries.py b/osmnx/boundaries.py index 774e2dbec..07a00621a 100644 --- a/osmnx/boundaries.py +++ b/osmnx/boundaries.py @@ -1,6 +1,7 @@ """Create GeoDataFrames of place boundaries.""" import logging as lg +import warnings import geopandas as gpd @@ -12,10 +13,7 @@ def gdf_from_place(query, which_result=1, buffer_dist=None): """ - Create a GeoDataFrame from a single place name query. - - Geocode the query with Nominatim then turn it into a GeoDataFrame with - a geometry column. + Use `geocoding.geocode_to_gdf()` instead (deprecated). Parameters ---------- @@ -30,6 +28,13 @@ def gdf_from_place(query, which_result=1, buffer_dist=None): ------- gdf : geopandas.GeoDataFrame """ + msg = ( + "The `boundaries` module has been deprecated and will be removed " + "in a future relase. Use the `geocoding` module's `geocode_to_gdf` " + "function instead." + ) + warnings.warn(msg) + # ensure query type if not isinstance(query, (str, dict)): raise ValueError("query must be a dict or a string") @@ -87,10 +92,7 @@ def gdf_from_place(query, which_result=1, buffer_dist=None): def gdf_from_places(queries, which_results=None, buffer_dist=None): """ - Create a GeoDataFrame from a list of place name queries. - - Geocode the queries with Nominatim then turn result into GeoDataFrame with - a geometry column. + Use `geocoding.geocode_to_gdf()` instead (deprecated). Parameters ---------- @@ -107,6 +109,13 @@ def gdf_from_places(queries, which_results=None, buffer_dist=None): ------- gdf : geopandas.GeoDataFrame """ + msg = ( + "The `boundaries` module has been deprecated and will be removed " + "in a future relase. Use the `geocoding` module's `geocode_to_gdf` " + "function instead." + ) + warnings.warn(msg) + # create an empty GeoDataFrame then append each result as a new row, # checking for the presence of which_results gdf = gpd.GeoDataFrame() diff --git a/osmnx/footprints.py b/osmnx/footprints.py index 76716f092..869e828cb 100644 --- a/osmnx/footprints.py +++ b/osmnx/footprints.py @@ -6,8 +6,8 @@ from shapely.geometry import Polygon from shapely.ops import polygonize -from . import boundaries from . import downloader +from . import geocoding from . import projection from . import settings from . import utils @@ -427,7 +427,7 @@ def footprints_from_address(address, dist=1000, footprint_type="building", retai other custom settings via ox.config(). """ # geocode the address string to a (lat, lng) point - point = utils_geo.geocode(query=address) + point = geocoding.geocode(query=address) # get footprints within distance of this point return footprints_from_point(point, dist, footprint_type, retain_invalid) @@ -464,7 +464,7 @@ def footprints_from_place(place, footprint_type="building", retain_invalid=False You can configure the Overpass server timeout, memory allocation, and other custom settings via ox.config(). """ - city = boundaries.gdf_from_place(place, which_result=which_result) + city = geocoding.geocode_to_gdf(place, which_result=which_result) polygon = city["geometry"].iloc[0] return footprints_from_polygon(polygon, footprint_type, retain_invalid) diff --git a/osmnx/geocoding.py b/osmnx/geocoding.py new file mode 100644 index 000000000..96609495c --- /dev/null +++ b/osmnx/geocoding.py @@ -0,0 +1,161 @@ +"""Geocode queries and create GeoDataFrames of place boundaries.""" + +import logging as lg +from collections import OrderedDict + +import geopandas as gpd + +from . import downloader +from . import projection +from . import settings +from . import utils + + +def geocode(query): + """ + Geocode a query string to (lat, lng) with the Nominatim geocoder. + + Parameters + ---------- + query : string + the query string to geocode + + Returns + ------- + point : tuple + the (lat, lng) coordinates returned by the geocoder + """ + # define the parameters + params = OrderedDict() + params["format"] = "json" + params["limit"] = 1 + params[ + "dedupe" + ] = 0 # prevent OSM from deduping results so we get precisely 'limit' # of results + params["q"] = query + response_json = downloader.nominatim_request(params=params) + + # if results were returned, parse lat and long out of the result + if len(response_json) > 0 and "lat" in response_json[0] and "lon" in response_json[0]: + lat = float(response_json[0]["lat"]) + lng = float(response_json[0]["lon"]) + point = (lat, lng) + utils.log(f'Geocoded "{query}" to {point}') + return point + else: + raise Exception(f'Nominatim geocoder returned no results for query "{query}"') + + +def geocode_to_gdf(query, which_result=1, buffer_dist=None): + """ + Geocode a query or queries to a GeoDataFrame with the Nominatim geocoder. + + Geometry column contains place boundaries if they exist in OpenStreetMap. + Query can be a string or dict, or a list of strings/dicts to send to the + geocoder. If query is a list, then which_result should be a list of the + same length. + + Parameters + ---------- + query : string or dict or list + query string or structured dict to geocode/download + which_result : int or list + max number of results to return and which to process upon receipt; if + passing a list then it must be same length as query + buffer_dist : float + distance to buffer around the place geometry, in meters + + Returns + ------- + gdf : geopandas.GeoDataFrame + """ + # if caller passed a list of queries but a scalar which_result value, then + # turn which_result into a list with same length as query list + if isinstance(query, list) and isinstance(which_result, int): + which_result = [which_result] * len(query) + + # turn query and which_result into lists if they're not already + if not isinstance(query, list): + query = [query] + if not isinstance(which_result, list): + which_result = [which_result] + + # ensure same length + if len(query) != len(which_result): + raise ValueError("which_result length must equal query length") + + # ensure query type + for q in query: + if not isinstance(q, (str, dict)): + raise ValueError("each query must be a dict or a string") + + # geocode each query and add to GeoDataFrame as a new row + gdf = gpd.GeoDataFrame() + for q, wr in zip(query, which_result): + gdf_tmp = _geocode_query_to_gdf(q, wr) + gdf = gdf.append(gdf_tmp) + + # reset GeoDataFrame index and set its CRS + gdf = gdf.reset_index(drop=True) + gdf.crs = settings.default_crs + + # if buffer_dist was passed in, project the geometry to UTM, buffer it in + # meters, then project it back to lat-lng + if buffer_dist is not None and len(gdf) > 0: + gdf_utm = projection.project_gdf(gdf) + gdf_utm["geometry"] = gdf_utm["geometry"].buffer(buffer_dist) + gdf = projection.project_gdf(gdf_utm, to_latlong=True) + utils.log(f"Buffered GeoDataFrame to {buffer_dist} meters") + + utils.log(f"Created GeoDataFrame with {len(gdf)} rows from {len(query)} queries") + return gdf + + +def _geocode_query_to_gdf(query, which_result=1): + """ + Geocode a single place query to a GeoDataFrame. + + Parameters + ---------- + query : string or dict + query string or structured dict to geocode/download + which_result : int + max number of results to return and which to process upon receipt + + Returns + ------- + gdf : geopandas.GeoDataFrame + """ + data = downloader._osm_polygon_download(query, limit=which_result) + + if len(data) >= which_result: + # extract data elements from the JSON response + result = data[which_result - 1] + bbox_south, bbox_north, bbox_west, bbox_east = [float(x) for x in result["boundingbox"]] + geometry = result["geojson"] + place = result["display_name"] + features = [ + { + "type": "Feature", + "geometry": geometry, + "properties": { + "place_name": place, + "bbox_north": bbox_north, + "bbox_south": bbox_south, + "bbox_east": bbox_east, + "bbox_west": bbox_west, + }, + } + ] + + # if we got an unexpected geometry type (like a point), log a warning + if geometry["type"] not in ["Polygon", "MultiPolygon"]: + utils.log(f'OSM returned a {geometry["type"]} as the geometry', level=lg.WARNING) + + # create the GeoDataFrame + return gpd.GeoDataFrame.from_features(features) + else: + # if no data returned (or fewer results than which_result) + msg = f'OSM returned no results (or fewer than which_result) for query "{query}"' + utils.log(msg, level=lg.WARNING) + return gpd.GeoDataFrame() diff --git a/osmnx/graph.py b/osmnx/graph.py index 778921229..2deab8808 100644 --- a/osmnx/graph.py +++ b/osmnx/graph.py @@ -9,9 +9,9 @@ from shapely.geometry import MultiPolygon from shapely.geometry import Polygon -from . import boundaries from . import distance from . import downloader +from . import geocoding from . import projection from . import settings from . import simplification @@ -238,7 +238,7 @@ def graph_from_address( other custom settings via ox.config(). """ # geocode the address string to a (lat, lng) point - point = utils_geo.geocode(query=address) + point = geocoding.geocode(query=address) # then create a graph from this point G = graph_from_point( @@ -324,12 +324,12 @@ def graph_from_place( if isinstance(query, (str, dict)): # if it is a string (place name) or dict (structured place query), then # it is a single place - gdf_place = boundaries.gdf_from_place( + gdf_place = geocoding.geocode_to_gdf( query, which_result=which_result, buffer_dist=buffer_dist ) elif isinstance(query, list): # if it is a list, it contains multiple places to get - gdf_place = boundaries.gdf_from_places(query, buffer_dist=buffer_dist) + gdf_place = geocoding.geocode_to_gdf(query, buffer_dist=buffer_dist) else: raise TypeError("query must be dict, string, or list of strings") diff --git a/osmnx/pois.py b/osmnx/pois.py index 7a913a7a0..07ced1acd 100644 --- a/osmnx/pois.py +++ b/osmnx/pois.py @@ -5,8 +5,8 @@ from shapely.geometry import Point from shapely.geometry import Polygon -from . import boundaries from . import downloader +from . import geocoding from . import settings from . import utils from . import utils_geo @@ -440,7 +440,7 @@ def pois_from_address(address, tags, dist=1000): other custom settings via ox.config(). """ # geocode the address string to a (lat, lng) point - point = utils_geo.geocode(query=address) + point = geocoding.geocode(query=address) return pois_from_point(point=point, tags=tags, dist=dist) @@ -475,7 +475,7 @@ def pois_from_place(place, tags, which_result=1): You can configure the Overpass server timeout, memory allocation, and other custom settings via ox.config(). """ - city = boundaries.gdf_from_place(place, which_result=which_result) + city = geocoding.geocode_to_gdf(place, which_result=which_result) polygon = city["geometry"].iloc[0] return pois_from_polygon(polygon, tags) diff --git a/osmnx/utils_geo.py b/osmnx/utils_geo.py index ec08309e9..240dc5bbd 100644 --- a/osmnx/utils_geo.py +++ b/osmnx/utils_geo.py @@ -1,6 +1,7 @@ """Geospatial utility functions.""" import math +import warnings from collections import OrderedDict import numpy as np @@ -21,7 +22,7 @@ def geocode(query): """ - Geocode a query string to (lat, lng) with the Nominatim geocoder. + Use `geocoding.geocode()` instead (deprecated). Parameters ---------- @@ -33,6 +34,12 @@ def geocode(query): point : tuple the (lat, lng) coordinates returned by the geocoder """ + msg = ( + "The `geocode` function has been moved from `utils_geo` to the " + "new `geocoding` module. Access it there accordingly." + ) + warnings.warn(msg) + # define the parameters params = OrderedDict() params["format"] = "json"