Python library supports Mapzen routing

This commit is contained in:
Mario de Frutos
2016-02-23 12:35:12 +01:00
parent 2a807af6df
commit d2e73a69fa
13 changed files with 272 additions and 5 deletions

View File

@@ -0,0 +1 @@
from routing import MapzenRouting, MapzenRoutingResponse

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -1 +1,2 @@
from redis_tools import RedisConnection
from coordinates import Coordinate

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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')