Python library supports Mapzen routing
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
from routing import MapzenRouting, MapzenRoutingResponse
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class WrongParams(Exception):
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr('Wrong parameters passed: ' + json.dumps(self.value))
|
||||||
|
|
||||||
|
|
||||||
|
class MalformedResult(Exception):
|
||||||
|
def __str__(self):
|
||||||
|
return repr('Result structure is malformed')
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from polyline.codec import PolylineCodec
|
||||||
|
|
||||||
|
from exceptions import WrongParams
|
||||||
|
from cartodb_services.tools import Coordinate
|
||||||
|
|
||||||
|
|
||||||
|
class MapzenRouting:
|
||||||
|
'A Mapzen Routing wrapper for python'
|
||||||
|
|
||||||
|
PRODUCTION_ROUTING_BASE_URL = 'https://valhalla.mapzen.com/route'
|
||||||
|
|
||||||
|
ACCEPTED_MODES = {
|
||||||
|
"walk": "pedestrian",
|
||||||
|
"car": "auto",
|
||||||
|
"public_transport": "bus",
|
||||||
|
"bicycle": "bicycle"
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTO_SHORTEST = 'auto_shortest'
|
||||||
|
|
||||||
|
OPTIONAL_PARAMS = [
|
||||||
|
'mode_type',
|
||||||
|
]
|
||||||
|
|
||||||
|
METRICS_UNITS = 'kilometers'
|
||||||
|
IMPERIAL_UNITS = 'miles'
|
||||||
|
|
||||||
|
def __init__(self, app_key, base_url=PRODUCTION_ROUTING_BASE_URL):
|
||||||
|
self._app_key = app_key
|
||||||
|
self._url = base_url
|
||||||
|
|
||||||
|
def calculate_route_point_to_point(self, origin, destination, mode,
|
||||||
|
options=[], units=METRICS_UNITS):
|
||||||
|
parsed_options = self.__parse_options(options)
|
||||||
|
mode_param = self.__parse_mode_param(mode, parsed_options)
|
||||||
|
directions = self.__parse_directions(origin, destination)
|
||||||
|
json_request_params = self.__parse_json_parameters(directions,
|
||||||
|
mode_param,
|
||||||
|
units)
|
||||||
|
request_params = self.__parse_request_parameters(json_request_params)
|
||||||
|
response = requests.get(self._url, params=request_params)
|
||||||
|
if response.status_code == requests.codes.ok:
|
||||||
|
return self.__parse_routing_response(response.text)
|
||||||
|
else:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
def __parse_options(self, options):
|
||||||
|
return dict(option.split('=') for option in options)
|
||||||
|
|
||||||
|
def __parse_request_parameters(self, json_request):
|
||||||
|
request_options = {"json": json_request}
|
||||||
|
request_options.update({'api_key': self._app_key})
|
||||||
|
|
||||||
|
return request_options
|
||||||
|
|
||||||
|
def __parse_json_parameters(self, directions, mode, units):
|
||||||
|
json_options = directions
|
||||||
|
json_options.update({'costing': self.ACCEPTED_MODES[mode]})
|
||||||
|
json_options.update({"directions_options": {'units': units,
|
||||||
|
'narrative': False}})
|
||||||
|
|
||||||
|
return json.dumps(json_options)
|
||||||
|
|
||||||
|
def __parse_directions(self, origin, destination):
|
||||||
|
return {"locations": [
|
||||||
|
{"lon": origin.longitude, "lat": origin.latitude},
|
||||||
|
{"lon": destination.longitude, "lat": destination.latitude}
|
||||||
|
]}
|
||||||
|
|
||||||
|
def __parse_routing_response(self, response):
|
||||||
|
try:
|
||||||
|
parsed_json_response = json.loads(response)
|
||||||
|
legs = parsed_json_response['trip']['legs'][0]
|
||||||
|
shape = PolylineCodec().decode(legs['shape'])
|
||||||
|
length = legs['summary']['length']
|
||||||
|
duration = legs['summary']['time']
|
||||||
|
routing_response = MapzenRoutingResponse(shape, length, duration)
|
||||||
|
|
||||||
|
return routing_response
|
||||||
|
except IndexError:
|
||||||
|
return []
|
||||||
|
except KeyError:
|
||||||
|
raise MalformedResult()
|
||||||
|
|
||||||
|
def __parse_mode_param(self, mode, options):
|
||||||
|
if mode in self.ACCEPTED_MODES:
|
||||||
|
mode_source = self.ACCEPTED_MODES[mode]
|
||||||
|
else:
|
||||||
|
raise WrongParams("{0} is not an accepted mode type".format(mode))
|
||||||
|
|
||||||
|
if mode == self.ACCEPTED_MODES['car'] and 'mode_type' in options and \
|
||||||
|
options['mode_type'] == 'shortest':
|
||||||
|
mode = self.AUTO_SHORTEST
|
||||||
|
|
||||||
|
return mode
|
||||||
|
|
||||||
|
|
||||||
|
class MapzenRoutingResponse:
|
||||||
|
|
||||||
|
def __init__(self, shape, length, duration):
|
||||||
|
self._shape = shape
|
||||||
|
self._length = length
|
||||||
|
self._duration = duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self):
|
||||||
|
return self._shape
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return self._length
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duration(self):
|
||||||
|
return self._duration
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import plpy
|
||||||
|
|
||||||
|
|
||||||
|
def polyline_to_linestring(polyline):
|
||||||
|
"""Convert a Mapzen polyline shape to a PostGIS multipolygon"""
|
||||||
|
coordinates = []
|
||||||
|
for point in polyline:
|
||||||
|
# Divide by 10 because mapzen uses one more decimal than the
|
||||||
|
# google standard (https://mapzen.com/documentation/turn-by-turn/decoding/)
|
||||||
|
coordinates.append("%s %s" % (point[1]/10, point[0]/10))
|
||||||
|
wkt_coordinates = ','.join(coordinates)
|
||||||
|
|
||||||
|
sql = "SELECT ST_GeomFromText('LINESTRING({0})', 4326) as geom".format(wkt_coordinates)
|
||||||
|
geometry = plpy.execute(sql, 1)[0]['geom']
|
||||||
|
|
||||||
|
return geometry
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
from config import GeocoderConfig, IsolinesRoutingConfig, InternalGeocoderConfig, ConfigException
|
from config import GeocoderConfig, IsolinesRoutingConfig, InternalGeocoderConfig, RoutingConfig, ConfigException
|
||||||
from quota import QuotaService
|
from quota import QuotaService
|
||||||
from user import UserMetricsService
|
from user import UserMetricsService
|
||||||
|
|||||||
@@ -27,6 +27,27 @@ class ServiceConfig(object):
|
|||||||
def organization(self):
|
def organization(self):
|
||||||
return self._orgname
|
return self._orgname
|
||||||
|
|
||||||
|
class RoutingConfig(ServiceConfig):
|
||||||
|
|
||||||
|
ROUTING_CONFIG_KEYS = ['username', 'orgname', 'mapzen_app_key']
|
||||||
|
MAPZEN_APP_KEY = 'mapzen_app_key'
|
||||||
|
USERNAME_KEY = 'username'
|
||||||
|
ORGNAME_KEY = 'orgname'
|
||||||
|
|
||||||
|
def __init__(self, redis_connection, username, orgname=None,
|
||||||
|
mapzen_app_key=None):
|
||||||
|
super(RoutingConfig, self).__init__(redis_connection, username,
|
||||||
|
orgname)
|
||||||
|
self._mapzen_app_key = mapzen_app_key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_type(self):
|
||||||
|
return 'routing_mapzen'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapzen_app_key(self):
|
||||||
|
return self._mapzen_app_key
|
||||||
|
|
||||||
|
|
||||||
class IsolinesRoutingConfig(ServiceConfig):
|
class IsolinesRoutingConfig(ServiceConfig):
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
from redis_tools import RedisConnection
|
from redis_tools import RedisConnection
|
||||||
|
from coordinates import Coordinate
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class Coordinate:
|
||||||
|
"""Class that represents a generic form of coordinates to be used
|
||||||
|
by the services """
|
||||||
|
|
||||||
|
def __init__(self, longitude, latitude):
|
||||||
|
self._longitude = longitude
|
||||||
|
self._latitude = latitude
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latitude(self):
|
||||||
|
return self._latitude
|
||||||
|
|
||||||
|
@property
|
||||||
|
def longitude(self):
|
||||||
|
return self._longitude
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return "{{\"lon\": {0},\"lat\": {1}}}".format(self._longitude,
|
||||||
|
self._latitude)
|
||||||
@@ -6,8 +6,6 @@ class RedisConnection:
|
|||||||
|
|
||||||
REDIS_DEFAULT_USER_DB = 5
|
REDIS_DEFAULT_USER_DB = 5
|
||||||
REDIS_DEFAULT_TIMEOUT = 2 #seconds
|
REDIS_DEFAULT_TIMEOUT = 2 #seconds
|
||||||
#REDIS_SENTINEL_DEFAULT_PORT = 26379
|
|
||||||
#REDIS_DEFAULT_PORT = 6379
|
|
||||||
|
|
||||||
def __init__(self, sentinel_master_id, redis_host, redis_port,
|
def __init__(self, sentinel_master_id, redis_host, redis_port,
|
||||||
redis_db=REDIS_DEFAULT_USER_DB, **kwargs):
|
redis_db=REDIS_DEFAULT_USER_DB, **kwargs):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ python-dateutil==2.2
|
|||||||
googlemaps==2.4.2
|
googlemaps==2.4.2
|
||||||
# Dependency for googlemaps package
|
# Dependency for googlemaps package
|
||||||
requests<=2.9.1
|
requests<=2.9.1
|
||||||
|
polyline==1.1
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
mock==1.3.0
|
mock==1.3.0
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from setuptools import setup, find_packages
|
|||||||
setup(
|
setup(
|
||||||
name='cartodb_services',
|
name='cartodb_services',
|
||||||
|
|
||||||
version='0.2.0',
|
version='0.3.0',
|
||||||
|
|
||||||
description='CartoDB Services API Python Library',
|
description='CartoDB Services API Python Library',
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ class HereMapsRoutingIsolineTestCase(unittest.TestCase):
|
|||||||
u'32.9699707,0.9462833'])
|
u'32.9699707,0.9462833'])
|
||||||
|
|
||||||
def test_calculate_isochrone_with_valid_params(self, req_mock):
|
def test_calculate_isochrone_with_valid_params(self, req_mock):
|
||||||
print self.isoline_url
|
|
||||||
url = "{0}?start=geo%2133.0%2C1.0&mode=shortest%3Bcar".format(self.isoline_url)
|
url = "{0}?start=geo%2133.0%2C1.0&mode=shortest%3Bcar".format(self.isoline_url)
|
||||||
req_mock.register_uri('GET', url, text=self.GOOD_RESPONSE)
|
req_mock.register_uri('GET', url, text=self.GOOD_RESPONSE)
|
||||||
response = self.routing.calculate_isochrone('geo!33.0,1.0', 'car',
|
response = self.routing.calculate_isochrone('geo!33.0,1.0', 'car',
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import requests_mock
|
||||||
|
import re
|
||||||
|
from nose.tools import assert_raises
|
||||||
|
from urlparse import urlparse, parse_qs
|
||||||
|
|
||||||
|
from cartodb_services.mapzen import MapzenRouting, MapzenRoutingResponse
|
||||||
|
from cartodb_services.mapzen.exceptions import WrongParams
|
||||||
|
from cartodb_services.tools import Coordinate
|
||||||
|
|
||||||
|
requests_mock.Mocker.TEST_PREFIX = 'test_'
|
||||||
|
|
||||||
|
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
class MapzenRoutingTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
GOOD_SHAPE = [(38.5, -120.2), (43.2, -126.4)]
|
||||||
|
|
||||||
|
GOOD_RESPONSE = """{{
|
||||||
|
"id": "ethervoid-route",
|
||||||
|
"trip": {{
|
||||||
|
"status": 0,
|
||||||
|
"status_message": "Found route between points",
|
||||||
|
"legs": [
|
||||||
|
{{
|
||||||
|
"shape": "_p~iF~ps|U_~t[~|yd@",
|
||||||
|
"summary": {{
|
||||||
|
"length": 444.59,
|
||||||
|
"time": 16969
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
],
|
||||||
|
"units": "kilometers",
|
||||||
|
"summary": {{
|
||||||
|
"length": 444.59,
|
||||||
|
"time": 16969
|
||||||
|
}},
|
||||||
|
"locations": [
|
||||||
|
{{
|
||||||
|
"lon": -120.2,
|
||||||
|
"lat": 38.5,
|
||||||
|
"type": "break"
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
"lon": -126.4,
|
||||||
|
"lat": 43.2,
|
||||||
|
"type": "break"
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
}}""".format(GOOD_SHAPE)
|
||||||
|
|
||||||
|
MALFORMED_RESPONSE = """{"manolo": "escobar"}"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.routing = MapzenRouting('api_key')
|
||||||
|
self.url = MapzenRouting.PRODUCTION_ROUTING_BASE_URL
|
||||||
|
|
||||||
|
def test_calculate_simple_routing_with_valid_params(self, req_mock):
|
||||||
|
req_mock.register_uri('GET', requests_mock.ANY, text=self.GOOD_RESPONSE)
|
||||||
|
origin = Coordinate('-120.2','38.5')
|
||||||
|
destination = Coordinate('-126.4','43.2')
|
||||||
|
response = self.routing.calculate_route_point_to_point(origin, destination,'car')
|
||||||
|
|
||||||
|
self.assertEqual(response.shape, self.GOOD_SHAPE)
|
||||||
|
self.assertEqual(response.length, 444.59)
|
||||||
|
self.assertEqual(response.duration, 16969)
|
||||||
|
|
||||||
|
def test_uknown_mode_raise_exception(self, req_mock):
|
||||||
|
req_mock.register_uri('GET', requests_mock.ANY, text=self.GOOD_RESPONSE)
|
||||||
|
origin = Coordinate('-120.2','38.5')
|
||||||
|
destination = Coordinate('-126.4','43.2')
|
||||||
|
|
||||||
|
assert_raises(WrongParams, self.routing.calculate_route_point_to_point, origin, destination, 'unknown')
|
||||||
Reference in New Issue
Block a user