Skip to content

Commit

Permalink
Add TomTom and AzureMaps geocoders (#312)
Browse files Browse the repository at this point in the history
  • Loading branch information
KostyaEsmukov committed Jul 6, 2018
1 parent ca4216b commit 13920dd
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 0 deletions.
12 changes: 12 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Geocoders

.. automethod:: __init__

.. autoclass:: geopy.geocoders.AzureMaps
:members:
:inherited-members:
:show-inheritance:

.. automethod:: __init__

.. autoclass:: geopy.geocoders.Baidu
:members:

Expand Down Expand Up @@ -100,6 +107,11 @@ Geocoders

.. automethod:: __init__

.. autoclass:: geopy.geocoders.TomTom
:members:

.. automethod:: __init__

.. autoclass:: geopy.geocoders.What3Words
:members:

Expand Down
6 changes: 6 additions & 0 deletions geopy/geocoders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
# Also don't forget to pull up the list of geocoders
# in the docs: docs/index.rst
"ArcGIS",
"AzureMaps",
"Baidu",
"Bing",
"DataBC",
Expand All @@ -103,12 +104,14 @@
"Pelias",
"Photon",
"LiveAddress",
"TomTom",
"What3Words",
"Yandex",
)


from geopy.geocoders.arcgis import ArcGIS
from geopy.geocoders.azure import AzureMaps
from geopy.geocoders.baidu import Baidu
from geopy.geocoders.bing import Bing
from geopy.geocoders.databc import DataBC
Expand All @@ -124,6 +127,7 @@
from geopy.geocoders.photon import Photon
from geopy.geocoders.pickpoint import PickPoint
from geopy.geocoders.smartystreets import LiveAddress
from geopy.geocoders.tomtom import TomTom
from geopy.geocoders.what3words import What3Words
from geopy.geocoders.yandex import Yandex

Expand All @@ -133,6 +137,7 @@

SERVICE_TO_GEOCODER = {
"arcgis": ArcGIS,
"azure": AzureMaps,
"baidu": Baidu,
"bing": Bing,
"databc": DataBC,
Expand All @@ -149,6 +154,7 @@
"pelias": Pelias,
"photon": Photon,
"liveaddress": LiveAddress,
"tomtom": TomTom,
"what3words": What3Words,
"yandex": Yandex,
}
Expand Down
75 changes: 75 additions & 0 deletions geopy/geocoders/azure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

from geopy.geocoders.base import DEFAULT_SENTINEL
from geopy.geocoders.tomtom import TomTom

__all__ = ("AzureMaps", )


class AzureMaps(TomTom):
"""AzureMaps geocoder based on TomTom.
Documentation at:
https://docs.microsoft.com/en-us/azure/azure-maps/index
"""

geocode_path = '/search/address/json'
reverse_path = '/search/address/reverse/json'

def __init__(
self,
subscription_key,
format_string=None,
scheme=None,
timeout=DEFAULT_SENTINEL,
proxies=DEFAULT_SENTINEL,
user_agent=None,
ssl_context=DEFAULT_SENTINEL,
domain='atlas.microsoft.com',
):
"""
:param str subscription_key: Azure Maps subscription key.
: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`.
"""
super(AzureMaps, self).__init__(
api_key=subscription_key,
format_string=format_string,
scheme=scheme,
timeout=timeout,
proxies=proxies,
user_agent=user_agent,
ssl_context=ssl_context,
domain=domain,
)

def _geocode_params(self, formatted_query):
return {
'api-version': '1.0',
'subscription-key': self.api_key,
'query': formatted_query,
}

def _reverse_params(self, position):
return {
'api-version': '1.0',
'subscription-key': self.api_key,
'query': position,
}
200 changes: 200 additions & 0 deletions geopy/geocoders/tomtom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
from geopy.compat import urlencode, quote
from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder
from geopy.location import Location
from geopy.util import logger

__all__ = ("TomTom", )


class TomTom(Geocoder):
"""TomTom geocoder.
Documentation at:
https://developer.tomtom.com/online-search/online-search-documentation
"""

geocode_path = '/search/2/geocode/%(query)s.json'
reverse_path = '/search/2/reverseGeocode/%(position)s.json'

def __init__(
self,
api_key,
format_string=None,
scheme=None,
timeout=DEFAULT_SENTINEL,
proxies=DEFAULT_SENTINEL,
user_agent=None,
ssl_context=DEFAULT_SENTINEL,
domain='api.tomtom.com',
):
"""
:param str api_key: TomTom API key.
: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`.
"""
super(TomTom, 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.api = "%s://%s%s" % (self.scheme, domain, self.geocode_path)
self.api_reverse = "%s://%s%s" % (self.scheme, domain, self.reverse_path)

def geocode(
self,
query,
exactly_one=True,
timeout=DEFAULT_SENTINEL,
limit=None,
typeahead=False,
language=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 int limit: Maximum amount of results to return from the service.
Unless exactly_one is set to False, limit will always be 1.
:param bool typeahead: If the "typeahead" flag is set, the query
will be interpreted as a partial input and the search will
enter predictive mode.
:param str language: Language in which search results should be
returned. When data in specified language is not
available for a specific field, default language is used.
List of supported languages (case-insensitive):
https://developer.tomtom.com/online-search/online-search-documentation/supported-languages
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
``exactly_one=False``.
"""
query = self.format_string % query
params = self._geocode_params(query)
params['typeahead'] = '1' if typeahead else '0'

if limit:
params['limit'] = str(int(limit))
if exactly_one:
params['limit'] = '1'

if language:
params['language'] = language

quoted_query = quote(query.encode('utf-8'))
url = "?".join((self.api % dict(query=quoted_query),
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``.
"""
position = self._coerce_point_to_string(query)
params = self._reverse_params(position)

quoted_position = quote(position.encode('utf-8'))
url = "?".join((self.api_reverse % dict(position=quoted_position),
urlencode(params)))
logger.debug("%s.reverse: %s", self.__class__.__name__, url)

return self._parse_reverse_json(
self._call_geocoder(url, timeout=timeout), exactly_one
)

def _geocode_params(self, formatted_query):
return {
'key': self.api_key,
}

def _reverse_params(self, position):
return {
'key': self.api_key,
}

def _parse_json(self, resources, exactly_one):
if not resources or not resources['results']:
return None

if exactly_one:
return self._parse_search_result(resources['results'][0])
else:
return [self._parse_search_result(result)
for result in resources['results']]

def _parse_search_result(self, result):
latitude = result['position']['lat']
longitude = result['position']['lon']
return Location(result['address']['freeformAddress'],
(latitude, longitude), result)

def _parse_reverse_json(self, resources, exactly_one):
if not resources or not resources['addresses']:
return None

if exactly_one:
return self._parse_reverse_result(resources['addresses'][0])
else:
return [self._parse_reverse_result(result)
for result in resources['addresses']]

def _parse_reverse_result(self, result):
latitude, longitude = result['position'].split(',')
return Location(result['address']['freeformAddress'],
(latitude, longitude), result)
17 changes: 17 additions & 0 deletions test/geocoders/azure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import unittest

from geopy.geocoders import AzureMaps
from test.geocoders.util import GeocoderTestBase, env
from test.geocoders.tomtom import BaseTomTomTestCase


@unittest.skipUnless(
bool(env.get('AZURE_SUBSCRIPTION_KEY')),
"No AZURE_SUBSCRIPTION_KEY env variable set"
)
class AzureMapsTestCase(BaseTomTomTestCase, GeocoderTestBase):

@classmethod
def make_geocoder(cls, **kwargs):
return AzureMaps(env['AZURE_SUBSCRIPTION_KEY'], timeout=3,
**kwargs)

0 comments on commit 13920dd

Please sign in to comment.