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

new geocoding module #506

Merged
merged 7 commits into from
Jun 20, 2020
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
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