134 lines
6.0 KiB
Python
134 lines
6.0 KiB
Python
# mapbox
|
|
from iso3166 import countries
|
|
from uritemplate import URITemplate
|
|
|
|
from mapbox.errors import InvalidCountryCodeError, InvalidPlaceTypeError
|
|
from mapbox.services.base import Service
|
|
|
|
|
|
class Geocoder(Service):
|
|
"""Access to the Geocoding API V5"""
|
|
|
|
api_name = 'geocoding'
|
|
api_version = 'v5'
|
|
precision = {'reverse': 5, 'proximity': 3}
|
|
|
|
def __init__(self, name='mapbox.places', access_token=None, cache=None,
|
|
host=None):
|
|
"""Constructs a Geocoding Service object.
|
|
|
|
:param name: name of a geocoding dataset.
|
|
:param access_token: Mapbox access token string.
|
|
:param cache: CacheControl cache instance (Dict or FileCache).
|
|
"""
|
|
self.name = name
|
|
super(Geocoder, self).__init__(access_token=access_token, cache=cache,
|
|
host=host)
|
|
|
|
def _validate_country_codes(self, ccs):
|
|
"""Validate country code filters for use in requests."""
|
|
for cc in ccs:
|
|
if cc not in self.country_codes:
|
|
raise InvalidCountryCodeError(cc)
|
|
return {'country': ",".join(ccs)}
|
|
|
|
def _validate_place_types(self, types):
|
|
"""Validate place types and return a mapping for use in requests."""
|
|
for pt in types:
|
|
if pt not in self.place_types:
|
|
raise InvalidPlaceTypeError(pt)
|
|
return {'types': ",".join(types)}
|
|
|
|
def forward(self, address, types=None, lon=None, lat=None,
|
|
country=None, bbox=None, limit=None, languages=None):
|
|
"""Returns a Requests response object that contains a GeoJSON
|
|
collection of places matching the given address.
|
|
|
|
`response.geojson()` returns the geocoding result as GeoJSON.
|
|
`response.status_code` returns the HTTP API status code.
|
|
|
|
Place results may be constrained to those of one or more types
|
|
or be biased toward a given longitude and latitude.
|
|
|
|
See: https://www.mapbox.com/api-documentation/search/#geocoding."""
|
|
uri = URITemplate(self.baseuri + '/{dataset}/{query}.json').expand(
|
|
dataset=self.name, query=address.encode('utf-8'))
|
|
params = {}
|
|
if country:
|
|
params.update(self._validate_country_codes(country))
|
|
if types:
|
|
params.update(self._validate_place_types(types))
|
|
if lon is not None and lat is not None:
|
|
params.update(proximity='{0},{1}'.format(
|
|
round(float(lon), self.precision.get('proximity', 3)),
|
|
round(float(lat), self.precision.get('proximity', 3))))
|
|
if languages:
|
|
params.update(language=','.join(languages))
|
|
if bbox is not None:
|
|
params.update(bbox='{0},{1},{2},{3}'.format(*bbox))
|
|
if limit is not None:
|
|
params.update(limit='{0}'.format(limit))
|
|
resp = self.session.get(uri, params=params)
|
|
self.handle_http_error(resp)
|
|
|
|
# for consistency with other services
|
|
def geojson():
|
|
return resp.json()
|
|
resp.geojson = geojson
|
|
|
|
return resp
|
|
|
|
def reverse(self, lon, lat, types=None, limit=None):
|
|
"""Returns a Requests response object that contains a GeoJSON
|
|
collection of places near the given longitude and latitude.
|
|
|
|
`response.geojson()` returns the geocoding result as GeoJSON.
|
|
`response.status_code` returns the HTTP API status code.
|
|
|
|
See: https://www.mapbox.com/api-documentation/search/#reverse-geocoding."""
|
|
uri = URITemplate(self.baseuri + '/{dataset}/{lon},{lat}.json').expand(
|
|
dataset=self.name,
|
|
lon=str(round(float(lon), self.precision.get('reverse', 5))),
|
|
lat=str(round(float(lat), self.precision.get('reverse', 5))))
|
|
params = {}
|
|
|
|
if types:
|
|
types = list(types)
|
|
params.update(self._validate_place_types(types))
|
|
|
|
if limit is not None:
|
|
if not types or len(types) != 1:
|
|
raise InvalidPlaceTypeError(
|
|
'Specify a single type when using limit with reverse geocoding')
|
|
params.update(limit='{0}'.format(limit))
|
|
|
|
resp = self.session.get(uri, params=params)
|
|
self.handle_http_error(resp)
|
|
|
|
# for consistency with other services
|
|
def geojson():
|
|
return resp.json()
|
|
resp.geojson = geojson
|
|
|
|
return resp
|
|
|
|
@property
|
|
def country_codes(self):
|
|
"""A list of valid country codes"""
|
|
return [c.alpha2.lower() for c in countries]
|
|
|
|
@property
|
|
def place_types(self):
|
|
"""A mapping of place type names to descriptions"""
|
|
return {
|
|
'address': "A street address with house number. Examples: 1600 Pennsylvania Ave NW, 1051 Market St, Oberbaumstrasse 7.",
|
|
'country': "Sovereign states and other political entities. Examples: United States, France, China, Russia.",
|
|
'place': "City, town, village or other municipality relevant to a country's address or postal system. Examples: Cleveland, Saratoga Springs, Berlin, Paris.",
|
|
'locality': "A smaller area within a place that possesses official status and boundaries. Examples: Oakleigh (Melbourne)",
|
|
'neighborhood': "A smaller area within a place, often without formal boundaries. Examples: Montparnasse, Downtown, Haight-Ashbury.",
|
|
'poi': "Places of interest including commercial venues, major landmarks, parks, and other features. Examples: Subway Restaurant, Yosemite National Park, Statue of Liberty.",
|
|
'poi.landmark': "Places of interest that are particularly notable or long-lived like parks, places of worship and museums. A strict subset of the poi place type. Examples: Yosemite National Park, Statue of Liberty.",
|
|
'postcode': "Postal code, varies by a country's postal system. Examples: 20009, CR0 3RL.",
|
|
'district': "Second order administrative division. Only used when necessary. Examples: Tianjin, Beijing",
|
|
'region': "First order administrative divisions within a country, usually provinces or states. Examples: California, Ontario, Essonne."}
|