Skip to content

Commit

Permalink
Merge pull request #506 from gboeing/feature
Browse files Browse the repository at this point in the history
new geocoding module
  • Loading branch information
gboeing authored Jun 20, 2020
2 parents 1633b83 + e039471 commit f956d2e
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 28 deletions.
16 changes: 8 additions & 8 deletions docs/source/osmnx.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------

Expand Down Expand Up @@ -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
------------------

Expand Down
3 changes: 2 additions & 1 deletion osmnx/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
25 changes: 17 additions & 8 deletions osmnx/boundaries.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Create GeoDataFrames of place boundaries."""

import logging as lg
import warnings

import geopandas as gpd

Expand All @@ -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
----------
Expand All @@ -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")
Expand Down Expand Up @@ -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
----------
Expand All @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions osmnx/footprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
161 changes: 161 additions & 0 deletions osmnx/geocoding.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 4 additions & 4 deletions osmnx/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")

Expand Down
6 changes: 3 additions & 3 deletions osmnx/pois.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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)

Expand Down
9 changes: 8 additions & 1 deletion osmnx/utils_geo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Geospatial utility functions."""

import math
import warnings
from collections import OrderedDict

import numpy as np
Expand All @@ -21,7 +22,7 @@

def geocode(query):
"""
Geocode a query string to (lat, lng) with the Nominatim geocoder.
Use `geocoding.geocode()` instead (deprecated).
Parameters
----------
Expand All @@ -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"
Expand Down

0 comments on commit f956d2e

Please sign in to comment.