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 quota import QuotaService
from user import UserMetricsService from user import UserMetricsService

View File

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

View File

@@ -1 +1,2 @@
from redis_tools import RedisConnection 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_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):

View File

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

View File

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

View File

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

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