-
Notifications
You must be signed in to change notification settings - Fork 636
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for non-free MapQuest API (#399)
- Loading branch information
1 parent
f2640e0
commit a049b4b
Showing
4 changed files
with
266 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
from geopy.compat import quote, string_compare, urlencode | ||
from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder | ||
from geopy.location import Location | ||
from geopy.point import Point | ||
from geopy.util import logger | ||
|
||
__all__ = ("MapQuest", ) | ||
|
||
|
||
class MapQuest(Geocoder): | ||
"""Geocoder using the non-free MapQuest API. | ||
Documentation at: | ||
https://developer.mapquest.com/documentation/geocoding-api/ | ||
MapQuest provides two Geocoding APIs: | ||
- :class:`geopy.geocoders.OpenMapQuest` API which is free and is based on open data. | ||
- :class:`geopy.geocoders.MapQuest` API which is paid and is based on non-free licensed data. | ||
This class provides support for using the "non-free" version of MapQuest. To use the open-source version use the :class:`geopy.geocoders.OpenMapQuest` api instead. | ||
""" | ||
geocode_path = '/geocoding/v1/address' | ||
reverse_path = '/geocoding/v1/reverse' | ||
|
||
def __init__( | ||
self, | ||
api_key, | ||
format_string=None, | ||
scheme=None, | ||
timeout=DEFAULT_SENTINEL, | ||
proxies=DEFAULT_SENTINEL, | ||
user_agent=None, | ||
ssl_context=DEFAULT_SENTINEL, | ||
domain='www.mapquestapi.com', | ||
): | ||
""" | ||
:param str api_key: The API key required by Mapquest to perform | ||
geocoding requests. API keys are managed through MapQuest's "Manage Keys" | ||
page (https://developer.mapquest.com/user/me/apps). | ||
:param str format_string: | ||
See :attr:`geopy.geocoders.options.default_format_string`. | ||
:param str scheme: | ||
See :attr:`geopy.geocoders.options.default_scheme`. | ||
:param int timeout: | ||
See :attr:`geopy.geocoders.options.default_timeout`. | ||
:param dict proxies: | ||
See :attr:`geopy.geocoders.options.default_proxies`. | ||
:param str user_agent: | ||
See :attr:`geopy.geocoders.options.default_user_agent`. | ||
:type ssl_context: :class:`ssl.SSLContext` | ||
:param ssl_context: | ||
See :attr:`geopy.geocoders.options.default_ssl_context`. | ||
:param str domain: base api domain for mapquest | ||
""" | ||
super(MapQuest, self).__init__( | ||
format_string=format_string, | ||
scheme=scheme, | ||
timeout=timeout, | ||
proxies=proxies, | ||
user_agent=user_agent, | ||
ssl_context=ssl_context, | ||
) | ||
|
||
self.api_key = api_key | ||
self.domain = domain.strip('/') | ||
self.api = "%s://%s" % (self.scheme, self.domain) | ||
|
||
def _parse_json(self, json, exactly_one=True): | ||
'''Returns location, (latitude, longitude) from json feed.''' | ||
features = json['results'][0]['locations'] | ||
|
||
if features == []: | ||
return None | ||
|
||
def parse_location(feature): | ||
addr_keys = [ | ||
'street', | ||
'adminArea6', | ||
'adminArea5', | ||
'adminArea4', | ||
'adminArea3', | ||
'adminArea2', | ||
'adminArea1', | ||
'postalCode' | ||
] | ||
|
||
location = [] | ||
|
||
for k in addr_keys: | ||
if k in feature and feature[k]: | ||
location.append(feature[k]) | ||
return ", ".join(location) | ||
|
||
def parse_feature(feature): | ||
location = parse_location(feature) | ||
longitude = feature['latLng']['lng'] | ||
latitude = feature['latLng']['lat'] | ||
return Location(location, (latitude, longitude), feature) | ||
|
||
if exactly_one: | ||
return parse_feature(features[0]) | ||
else: | ||
return [parse_feature(feature) for feature in features] | ||
|
||
def geocode( | ||
self, | ||
query, | ||
exactly_one=True, | ||
timeout=DEFAULT_SENTINEL, | ||
country=None, | ||
bounds=None | ||
): | ||
""" | ||
Return a location point by address | ||
:param str query: The address or query you wish to geocode. | ||
:param bool exactly_one: Return one result or a list of results, if | ||
available. | ||
:param int timeout: Time, in seconds, to wait for the geocoding service | ||
to respond before raising a :class:`geopy.exc.GeocoderTimedOut` | ||
exception. Set this only if you wish to override, on this call | ||
only, the value set during the geocoder's initialization. | ||
:param bounds: The bounding box of the viewport within which | ||
to bias geocode results more prominently. | ||
Example: ``[Point(22, 180), Point(-22, -180)]``. | ||
:type bounds: list or tuple of 2 items of :class:`geopy.point.Point` or | ||
``(latitude, longitude)`` or ``"%(latitude)s, %(longitude)s"``. | ||
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if | ||
``exactly_one=False``. | ||
""" | ||
params = {} | ||
params['key'] = self.api_key | ||
params['location'] = query | ||
|
||
if bounds: | ||
params['boundingBox'] = self._format_bounding_box( | ||
bounds, "%(lat2)s,%(lon1)s,%(lat1)s,%(lon2)s" | ||
) | ||
|
||
url = self.api + self.geocode_path + "?" + urlencode(params) | ||
|
||
logger.debug("%s.geocode: %s", self.__class__.__name__, url) | ||
|
||
return self._parse_json( | ||
self._call_geocoder(url, timeout=timeout), exactly_one | ||
) | ||
|
||
def reverse( | ||
self, | ||
query, | ||
exactly_one=True, | ||
timeout=DEFAULT_SENTINEL | ||
): | ||
""" | ||
Return an address by location point. | ||
:param query: The coordinates for which you wish to obtain the | ||
closest human-readable addresses. | ||
:type query: :class:`geopy.point.Point`, list or tuple of ``(latitude, | ||
longitude)``, or string as ``"%(latitude)s, %(longitude)s"``. | ||
:param bool exactly_one: Return one result or a list of results, if | ||
available. | ||
:param int timeout: Time, in seconds, to wait for the geocoding service | ||
to respond before raising a :class:`geopy.exc.GeocoderTimedOut` | ||
exception. Set this only if you wish to override, on this call | ||
only, the value set during the geocoder's initialization. | ||
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if | ||
``exactly_one=False``. | ||
""" | ||
params = {} | ||
params['key'] = self.api_key | ||
|
||
point = self._coerce_point_to_string(query, "%(lat)s,%(lon)s") | ||
params['location'] = point | ||
|
||
url = self.api + self.reverse_path + "?" + urlencode(params) | ||
|
||
logger.debug("%s.reverse: %s", self.__class__.__name__, url) | ||
|
||
return self._parse_json( | ||
self._call_geocoder(url, timeout=timeout), exactly_one | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import unittest | ||
|
||
from geopy.compat import u | ||
from geopy.geocoders import MapQuest | ||
from geopy.point import Point | ||
from test.geocoders.util import GeocoderTestBase, env | ||
|
||
|
||
@unittest.skipUnless( | ||
bool(env.get('MAPQUEST_KEY')), | ||
"No MAPQUEST_KEY env variable set" | ||
) | ||
class MapQuestTestCase(GeocoderTestBase): | ||
@classmethod | ||
def setUpClass(cls): | ||
cls.geocoder = MapQuest(api_key=env['MAPQUEST_KEY'], timeout=3) | ||
|
||
def test_geocode(self): | ||
self.geocode_run( | ||
{"query": "435 north michigan ave, chicago il 60611 usa"}, | ||
{"latitude": 41.89036, "longitude": -87.624043}, | ||
) | ||
|
||
def test_unicode_name(self): | ||
self.geocode_run( | ||
{"query": u("\u6545\u5bab")}, | ||
{"latitude": 25.0968, "longitude": 121.54714}, | ||
) | ||
|
||
def test_reverse(self): | ||
new_york_point = Point(40.75376406311989, -73.98489005863667) | ||
location = self.reverse_run( | ||
{"query": new_york_point, "exactly_one": True}, | ||
{"latitude": 40.7537640, "longitude": -73.98489, "delta": 1}, | ||
) | ||
self.assertIn("New York", location.address) | ||
|
||
def test_zero_results(self): | ||
self.geocode_run( | ||
{"query": ''}, | ||
{}, | ||
expect_failure=True, | ||
) | ||
|
||
def test_geocode_bbox(self): | ||
self.geocode_run( | ||
{ | ||
"query": "435 north michigan ave, chicago il 60611 usa", | ||
"bounds": [Point(35.227672, -103.271484), | ||
Point(48.603858, -74.399414)] | ||
}, | ||
{"latitude": 41.890, "longitude": -87.624}, | ||
) | ||
|
||
def test_geocode_raw(self): | ||
result = self.geocode_run({"query": "New York"}, {}) | ||
self.assertTrue(isinstance(result.raw, dict)) | ||
self.assertEqual(result.raw['latLng'], {'lat': 40.713054, 'lng': -74.007228}) | ||
|
||
def test_geocode_exactly_one_true(self): | ||
list_result = self.geocode_run({"query": "New York", "exactly_one": False}, {}) | ||
self.assertTrue(isinstance(list_result, list)) |