Skip to content

Commit

Permalink
Add support for non-free MapQuest API (#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
pratheekrebala committed May 9, 2020
1 parent f2640e0 commit a049b4b
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 1 deletion.
5 changes: 4 additions & 1 deletion geopy/geocoders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"Here",
"IGNFrance",
"MapBox",
"MapQuest",
"OpenCage",
"OpenMapQuest",
"PickPoint",
Expand Down Expand Up @@ -164,6 +165,7 @@
from geopy.geocoders.here import Here
from geopy.geocoders.ignfrance import IGNFrance
from geopy.geocoders.mapbox import MapBox
from geopy.geocoders.mapquest import MapQuest
from geopy.geocoders.opencage import OpenCage
from geopy.geocoders.openmapquest import OpenMapQuest
from geopy.geocoders.osm import Nominatim
Expand Down Expand Up @@ -191,6 +193,7 @@
"here": Here,
"ignfrance": IGNFrance,
"mapbox": MapBox,
"mapquest": MapQuest,
"opencage": OpenCage,
"openmapquest": OpenMapQuest,
"pickpoint": PickPoint,
Expand All @@ -200,7 +203,7 @@
"liveaddress": LiveAddress,
"tomtom": TomTom,
"what3words": What3Words,
"yandex": Yandex,
"yandex": Yandex
}


Expand Down
194 changes: 194 additions & 0 deletions geopy/geocoders/mapquest.py
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
)
6 changes: 6 additions & 0 deletions geopy/geocoders/openmapquest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ class OpenMapQuest(Nominatim):
Documentation at:
https://developer.mapquest.com/documentation/open/
MapQuest provides two Geocoding APIs:
- "OpenMapQuest" API which is free and is based on open data.
- "MapQuest" API which is paid and is based on non-free licensed data.
This class provides support for using the free version of MapQuest. To use the non-free version use the :class:`geopy.geocoders.MapQuest` api instead.
.. versionchanged:: 1.17.0
OpenMapQuest now extends the Nominatim class.
"""
Expand Down
62 changes: 62 additions & 0 deletions test/geocoders/mapquest.py
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))

0 comments on commit a049b4b

Please sign in to comment.