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 user import UserMetricsService
|
||||
|
||||
@@ -27,6 +27,27 @@ class ServiceConfig(object):
|
||||
def organization(self):
|
||||
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):
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
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_TIMEOUT = 2 #seconds
|
||||
#REDIS_SENTINEL_DEFAULT_PORT = 26379
|
||||
#REDIS_DEFAULT_PORT = 6379
|
||||
|
||||
def __init__(self, sentinel_master_id, redis_host, redis_port,
|
||||
redis_db=REDIS_DEFAULT_USER_DB, **kwargs):
|
||||
|
||||
@@ -5,6 +5,7 @@ python-dateutil==2.2
|
||||
googlemaps==2.4.2
|
||||
# Dependency for googlemaps package
|
||||
requests<=2.9.1
|
||||
polyline==1.1
|
||||
|
||||
# Test
|
||||
mock==1.3.0
|
||||
|
||||
@@ -10,7 +10,7 @@ from setuptools import setup, find_packages
|
||||
setup(
|
||||
name='cartodb_services',
|
||||
|
||||
version='0.2.0',
|
||||
version='0.3.0',
|
||||
|
||||
description='CartoDB Services API Python Library',
|
||||
|
||||
|
||||
@@ -148,7 +148,6 @@ class HereMapsRoutingIsolineTestCase(unittest.TestCase):
|
||||
u'32.9699707,0.9462833'])
|
||||
|
||||
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)
|
||||
req_mock.register_uri('GET', url, text=self.GOOD_RESPONSE)
|
||||
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