Skip to content

Commit

Permalink
Add maptiler
Browse files Browse the repository at this point in the history
  • Loading branch information
chilfing authored and KostyaEsmukov committed May 10, 2020
1 parent bd4f744 commit ad31b19
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ MapQuest

.. automethod:: __init__

MapTiler
--------

.. autoclass:: geopy.geocoders.MapTiler
:members:

.. automethod:: __init__

OpenCage
--------

Expand Down
3 changes: 3 additions & 0 deletions geopy/geocoders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"IGNFrance",
"MapBox",
"MapQuest",
"MapTiler",
"OpenCage",
"OpenMapQuest",
"PickPoint",
Expand Down Expand Up @@ -166,6 +167,7 @@
from geopy.geocoders.ignfrance import IGNFrance
from geopy.geocoders.mapbox import MapBox
from geopy.geocoders.mapquest import MapQuest
from geopy.geocoders.maptiler import MapTiler
from geopy.geocoders.opencage import OpenCage
from geopy.geocoders.openmapquest import OpenMapQuest
from geopy.geocoders.osm import Nominatim
Expand Down Expand Up @@ -194,6 +196,7 @@
"ignfrance": IGNFrance,
"mapbox": MapBox,
"mapquest": MapQuest,
"maptiler": MapTiler,
"opencage": OpenCage,
"openmapquest": OpenMapQuest,
"pickpoint": PickPoint,
Expand Down
189 changes: 189 additions & 0 deletions geopy/geocoders/maptiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
from geopy.compat import quote, 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__ = ("MapTiler", )


class MapTiler(Geocoder):
"""Geocoder using the MapTiler API.
Documentation at:
https://cloud.maptiler.com/geocoding/
.. versionadded:: 1.21.0
"""

api_path = '/geocoding/%(query)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.maptiler.com',
):
"""
:param str api_key: The API key required by Maptiler to perform
geocoding requests. API keys are managed through Maptiler's account
page (https://cloud.maptiler.com/account/keys).
: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 Maptiler
"""
super(MapTiler, 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%s" % (self.scheme, self.domain, self.api_path)

def _parse_json(self, json, exactly_one=True):
# Returns location, (latitude, longitude) from json feed.
features = json['features']
if not features:
return None

def parse_feature(feature):
location = feature['place_name']
longitude = feature['center'][0]
latitude = feature['center'][1]

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,
proximity=None,
language=None,
bbox=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 proximity: A coordinate to bias local results based on a provided
location.
:type proximity: :class:`geopy.point.Point`, list or tuple of ``(latitude,
longitude)``, or string as ``"%(latitude)s, %(longitude)s"``.
:param str language: Prefer results in specific languages. It's possible
to specify multiple values. e.g. "de,en"
:param bbox: The bounding box of the viewport within which
to bias geocode results more prominently.
Example: ``[Point(22, 180), Point(-22, -180)]``.
:type bbox: 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 = {'key': self.api_key}

query = self.format_string % query
if bbox:
params['bbox'] = self._format_bounding_box(
bbox, "%(lon1)s,%(lat1)s,%(lon2)s,%(lat2)s")

if language:
params['language'] = language

if proximity:
p = Point(proximity)
params['proximity'] = "%s,%s" % (p.longitude, p.latitude)

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,
language=None,
timeout=DEFAULT_SENTINEL,
):
"""
Return an address by location point.
:param str language: Prefer results in specific languages. It's possible
to specify multiple values. e.g. "de,en"
: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 = {'key': self.api_key}

if language:
params['language'] = language

point = self._coerce_point_to_string(query, "%(lon)s,%(lat)s")
quoted_query = quote(point.encode('utf-8'))
url = "?".join((self.api % dict(query=quoted_query),
urlencode(params)))
logger.debug("%s.reverse: %s", self.__class__.__name__, url)
return self._parse_json(
self._call_geocoder(url, timeout=timeout), exactly_one
)
104 changes: 104 additions & 0 deletions test/geocoders/maptiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
import unittest

from geopy.compat import u
from geopy.geocoders import MapTiler
from geopy.location import Location
from geopy.point import Point
from test.geocoders.util import GeocoderTestBase, env


@unittest.skipUnless(
bool(env.get('MAPTILER_KEY')),
"No MAPTILER_KEY env variable set"
)
class MapTilerTestCase(GeocoderTestBase):
@classmethod
def setUpClass(cls):
cls.geocoder = MapTiler(api_key=env['MAPTILER_KEY'], timeout=3)

def test_geocode(self):
self.geocode_run(
{"query": "435 north michigan ave, chicago il 60611 usa"},
{"latitude": 41.890, "longitude": -87.624},
)

def test_unicode_name(self):
self.geocode_run(
{"query": u("Stadelhoferstrasse 8, 8001 Z\u00fcrich")},
{"latitude": 47.36649, "longitude": 8.54855},
)

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": 'asdfasdfasdf'},
{},
expect_failure=True,
)

def test_geocode_outside_bbox(self):
self.geocode_run(
{
"query": "435 north michigan ave, chicago il 60611 usa",
"bbox": [[34.172684, -118.604794],
[34.236144, -118.500938]]
},
{},
expect_failure=True,
)

def test_geocode_bbox(self):
self.geocode_run(
{
"query": "435 north michigan ave, chicago il 60611 usa",
"bbox": [Point(35.227672, -103.271484),
Point(48.603858, -74.399414)]
},
{"latitude": 41.890, "longitude": -87.624},
)

def test_geocode_proximity(self):
self.geocode_run(
{"query": "200 queen street", "proximity": Point(45.3, -66.1)},
{"latitude": 44.038901, "longitude": -64.73052, "delta": 0.1},
)

def test_reverse_language(self):
zurich_point = Point(47.3723, 8.5422)
location = self.reverse_run(
{"query": zurich_point, "exactly_one": True, "language": "ja"},
{"latitude": 47.3723, "longitude": 8.5422, "delta": 1},
)
self.assertIn("チューリッヒ", location.address)

def test_geocode_language(self):
location = self.geocode_run(
{"query": "Zürich", "exactly_one": True, "language": "ja",
"proximity": Point(47.3723, 8.5422)},
{"latitude": 47.3723, "longitude": 8.5422, "delta": 1},
)
self.assertIn("チューリッヒ", location.address)

def test_geocode_raw(self):
result = self.geocode_run({"query": "New York"}, {})
delta = 0.00001
self.assertTrue(isinstance(result.raw, dict))
self.assertAlmostEqual(-73.8784155, result.raw['center'][0], delta=delta)
self.assertAlmostEqual(40.6930727, result.raw['center'][1], delta=delta)
self.assertEqual("relation175905", result.raw['properties']['osm_id'])

def test_geocode_exactly_one_false(self):
list_result = self.geocode_run({"query": "New York", "exactly_one": False}, {})
self.assertTrue(isinstance(list_result, list))

def test_geocode_exactly_one_true(self):
list_result = self.geocode_run({"query": "New York", "exactly_one": True}, {})
self.assertTrue(isinstance(list_result, Location))

0 comments on commit ad31b19

Please sign in to comment.