From 8c0af7d51d4f547bae8f758f02d132c45bd83200 Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 27 Dec 2017 13:43:14 +0100 Subject: [PATCH] Pure Python implementation of clients for Mapbox services --- .../cartodb_services/mapbox/__init__.py | 5 + .../cartodb_services/mapbox/exceptions.py | 18 +++ .../cartodb_services/mapbox/geocoder.py | 59 +++++++ .../cartodb_services/mapbox/isolines.py | 146 ++++++++++++++++++ .../cartodb_services/mapbox/matrix_client.py | 73 +++++++++ .../cartodb_services/mapbox/routing.py | 107 +++++++++++++ .../cartodb_services/tools/coordinates.py | 20 ++- .../cartodb_services/tools/spherical.py | 17 ++ .../python/cartodb_services/requirements.txt | 1 + .../test/test_mapboxgeocoder.py | 24 +++ .../test/test_mapboxisoline.py | 36 +++++ .../test/test_mapboxmatrix.py | 54 +++++++ .../test/test_mapboxrouting.py | 61 ++++++++ 13 files changed, 620 insertions(+), 1 deletion(-) create mode 100644 server/lib/python/cartodb_services/cartodb_services/mapbox/__init__.py create mode 100644 server/lib/python/cartodb_services/cartodb_services/mapbox/exceptions.py create mode 100644 server/lib/python/cartodb_services/cartodb_services/mapbox/geocoder.py create mode 100644 server/lib/python/cartodb_services/cartodb_services/mapbox/isolines.py create mode 100644 server/lib/python/cartodb_services/cartodb_services/mapbox/matrix_client.py create mode 100644 server/lib/python/cartodb_services/cartodb_services/mapbox/routing.py create mode 100644 server/lib/python/cartodb_services/cartodb_services/tools/spherical.py create mode 100644 server/lib/python/cartodb_services/test/test_mapboxgeocoder.py create mode 100644 server/lib/python/cartodb_services/test/test_mapboxisoline.py create mode 100644 server/lib/python/cartodb_services/test/test_mapboxmatrix.py create mode 100644 server/lib/python/cartodb_services/test/test_mapboxrouting.py diff --git a/server/lib/python/cartodb_services/cartodb_services/mapbox/__init__.py b/server/lib/python/cartodb_services/cartodb_services/mapbox/__init__.py new file mode 100644 index 0000000..32849af --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/mapbox/__init__.py @@ -0,0 +1,5 @@ +from routing import MapboxRouting, MapboxRoutingResponse +from geocoder import MapboxGeocoder +from isolines import MapboxIsolines +from matrix_client import MapboxMatrixClient +from exceptions import ServiceException diff --git a/server/lib/python/cartodb_services/cartodb_services/mapbox/exceptions.py b/server/lib/python/cartodb_services/cartodb_services/mapbox/exceptions.py new file mode 100644 index 0000000..c2573d0 --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/mapbox/exceptions.py @@ -0,0 +1,18 @@ +''' +Exceptions for the Mapbox services Python wrapper. +''' + + +class ServiceException(Exception): + ''' + Exception to be raised if any Service problem is found. + ''' + + def __init__(self, code, message): + self.code = code + self.message = message + + def __str__(self): + return repr('ServiceException ({code}): {message}'.format( + code=self.code, + message=self.message)) diff --git a/server/lib/python/cartodb_services/cartodb_services/mapbox/geocoder.py b/server/lib/python/cartodb_services/cartodb_services/mapbox/geocoder.py new file mode 100644 index 0000000..6187f55 --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/mapbox/geocoder.py @@ -0,0 +1,59 @@ +''' +Python client for the Mapbox Geocoder service. +''' + +import json +import requests +from mapbox import Geocoder +from cartodb_services.metrics import Traceable +from cartodb_services.mapbox.exceptions import ServiceException + +ACCESS_TOKEN = 'pk.eyJ1IjoiYWNhcmxvbiIsImEiOiJjamJuZjQ1Zjc0Ymt4Mnh0YmFrMmhtYnY4In0.gt9cw0VeKc3rM2mV5pcEmg' + +EPHEMERAL_GEOCODER = 'mapbox.places' +PERMANENT_GEOCODER = 'mapbox.places-permanent' +DEFAULT_GEOCODER = EPHEMERAL_GEOCODER + +ENTRY_FEATURES = 'features' +ENTRY_CENTER = 'center' +ENTRY_GEOMETRY = 'geometry' +ENTRY_COORDINATES = 'coordinates' +ENTRY_TYPE = 'type' +TYPE_POINT = 'Point' + + +class MapboxGeocoder(Traceable): + ''' + Python wrapper for the Mapbox Geocoder service. + ''' + + def __init__(self, token=ACCESS_TOKEN, name=DEFAULT_GEOCODER): + self._token = token + self._geocoder = Geocoder(access_token=self._token, name=name) + + def _parse_geocoder_response(self, response): + json_response = json.loads(response) + feature = json_response[ENTRY_FEATURES][0] + + return self._extract_lng_lat_from_feature(feature) + + def _extract_lng_lat_from_feature(self, feature): + geometry = feature[ENTRY_GEOMETRY] + if geometry[ENTRY_TYPE] == TYPE_POINT: + location = geometry[ENTRY_COORDINATES] + else: + location = feature[ENTRY_CENTER] + + longitude = location[0] + latitude = location[1] + return [longitude, latitude] + + def geocode(self, address, country=None): + response = self._geocoder.forward(address=address, + country=country, + limit=1) + + if response.status_code == requests.codes.ok: + return self._parse_geocoder_response(response.text) + else: + raise ServiceException(response.status_code, response.content) diff --git a/server/lib/python/cartodb_services/cartodb_services/mapbox/isolines.py b/server/lib/python/cartodb_services/cartodb_services/mapbox/isolines.py new file mode 100644 index 0000000..2d18059 --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/mapbox/isolines.py @@ -0,0 +1,146 @@ +''' +Python implementation for Mapbox services based isolines. +Uses the Mapbox Time Matrix service. +''' + +import json +from cartodb_services.tools.spherical import (get_angles, + calculate_dest_location) +from cartodb_services.mapbox.matrix_client import (validate_profile, + DEFAULT_PROFILE, + PROFILE_WALKING, + PROFILE_DRIVING, + PROFILE_CYCLING, + ENTRY_DURATIONS) + +MAX_SPEEDS = { + PROFILE_WALKING: 3.3333333, # In m/s, assuming 12km/h walking speed + PROFILE_CYCLING: 16.67, # In m/s, assuming 60km/h max speed + PROFILE_DRIVING: 41.67 # In m/s, assuming 140km/h max speed +} + +DEFAULT_NUM_ANGLES = 24 +DEFAULT_MAX_ITERS = 5 +DEFAULT_TOLERANCE = 0.1 + +MATRIX_NUM_ANGLES = DEFAULT_NUM_ANGLES +MATRIX_MAX_ITERS = DEFAULT_MAX_ITERS +MATRIX_TOLERANCE = DEFAULT_TOLERANCE + +UNIT_FACTOR_ISOCHRONE = 1.0 +UNIT_FACTOR_ISODISTANCE = 1000.0 +DEFAULT_UNIT_FACTOR = UNIT_FACTOR_ISOCHRONE + + +class MapboxIsolines(): + ''' + Python wrapper for Mapbox services based isolines. + ''' + + def __init__(self, matrix_client, routing_client): + self._matrix_client = matrix_client + self._routing_client = routing_client + + def _calculate_matrix_cost(self, origin, targets, isorange, + profile=DEFAULT_PROFILE, + unit_factor=UNIT_FACTOR_ISOCHRONE, + number_of_angles=MATRIX_NUM_ANGLES): + response = self._matrix_client.matrix([origin] + targets, + profile) + json_response = json.loads(response) + + costs = [None] * number_of_angles + + for idx, cost in enumerate(json_response[ENTRY_DURATIONS][0][1:]): + if cost: + costs[idx] = cost * unit_factor + else: + costs[idx] = isorange + + return costs + + def calculate_isochrone(self, origin, time_range, + profile=DEFAULT_PROFILE): + validate_profile(profile) + + max_speed = MAX_SPEEDS[profile] + upper_rmax = max_speed * time_range # an upper bound for the radius + + return self.calculate_isoline(origin=origin, + isorange=time_range, + upper_rmax=upper_rmax, + cost_method=self._calculate_matrix_cost, + profile=profile, + unit_factor=UNIT_FACTOR_ISOCHRONE, + number_of_angles=MATRIX_NUM_ANGLES, + max_iterations=MATRIX_MAX_ITERS, + tolerance=MATRIX_TOLERANCE) + + def calculate_isodistance(self, origin, distance_range, + profile=DEFAULT_PROFILE): + validate_profile(profile) + + max_speed = MAX_SPEEDS[profile] + time_range = distance_range / max_speed + + return self.calculate_isochrone(origin=origin, + time_range=time_range, + profile=profile) + + def calculate_isoline(self, origin, isorange, upper_rmax, + cost_method=_calculate_matrix_cost, + profile=DEFAULT_PROFILE, + unit_factor=DEFAULT_UNIT_FACTOR, + number_of_angles=DEFAULT_NUM_ANGLES, + max_iterations=DEFAULT_MAX_ITERS, + tolerance=DEFAULT_TOLERANCE): + # Formally, a solution is an array of {angle, radius, lat, lon, cost} + # with cardinality number_of_angles + # we're looking for a solution in which + # abs(cost - isorange) / isorange <= TOLERANCE + + # Initial setup + angles = get_angles(number_of_angles) + rmax = [upper_rmax] * number_of_angles + rmin = [0.0] * number_of_angles + location_estimates = [calculate_dest_location(origin, a, + upper_rmax / 2.0) + for a in angles] + + # Iterate to refine the first solution + for i in xrange(0, max_iterations): + # Calculate the "actual" cost for each location estimate. + # NOTE: sometimes it cannot calculate the cost and returns None. + # Just assume isorange and stop the calculations there + + costs = cost_method(origin=origin, targets=location_estimates, + isorange=isorange, profile=profile, + unit_factor=unit_factor, + number_of_angles=number_of_angles) + + errors = [(cost - isorange) / float(isorange) for cost in costs] + max_abs_error = max([abs(e) for e in errors]) + if max_abs_error <= tolerance: + # good enough, stop there + break + + # let's refine the solution, binary search + for j in xrange(0, number_of_angles): + + if abs(errors[j]) > tolerance: + if errors[j] > 0: + rmax[j] = (rmax[j] + rmin[j]) / 2.0 + else: + rmin[j] = (rmax[j] + rmin[j]) / 2.0 + + location_estimates[j] = calculate_dest_location(origin, + angles[j], + (rmax[j] + rmin[j]) / 2.0) + + # delete points that got None + location_estimates_filtered = [] + for i, c in enumerate(costs): + if c != isorange: + location_estimates_filtered.append(location_estimates[i]) + + return location_estimates_filtered diff --git a/server/lib/python/cartodb_services/cartodb_services/mapbox/matrix_client.py b/server/lib/python/cartodb_services/cartodb_services/mapbox/matrix_client.py new file mode 100644 index 0000000..404b906 --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/mapbox/matrix_client.py @@ -0,0 +1,73 @@ +''' +Python client for the Mapbox Time Matrix service. +''' + +import requests +from cartodb_services.metrics import Traceable +from cartodb_services.tools.coordinates import (validate_coordinates, + marshall_coordinates) +from exceptions import ServiceException + +ACCESS_TOKEN = 'pk.eyJ1IjoiYWNhcmxvbiIsImEiOiJjamJuZjQ1Zjc0Ymt4Mnh0YmFrMmhtYnY4In0.gt9cw0VeKc3rM2mV5pcEmg' + +BASEURI = ('https://api.mapbox.com/directions-matrix/v1/mapbox/{profile}/' + '{coordinates}' + '?access_token={token}' + '&sources=0' # Set the first coordinate as source... + '&destinations=all') # ...and the rest as destinations + +NUM_COORDINATES_MIN = 2 # https://www.mapbox.com/api-documentation/#matrix +NUM_COORDINATES_MAX = 25 # https://www.mapbox.com/api-documentation/#matrix + +PROFILE_DRIVING_TRAFFIC = 'driving-traffic' +PROFILE_DRIVING = 'driving' +PROFILE_CYCLING = 'cycling' +PROFILE_WALKING = 'walking' +DEFAULT_PROFILE = PROFILE_DRIVING + +VALID_PROFILES = [PROFILE_DRIVING_TRAFFIC, + PROFILE_DRIVING, + PROFILE_CYCLING, + PROFILE_WALKING] + +ENTRY_DURATIONS = 'durations' + + +def validate_profile(profile): + if profile not in VALID_PROFILES: + raise ValueError('{profile} is not a valid profile. ' + 'Valid profiles are: {valid_profiles}'.format( + profile=profile, + valid_profiles=', '.join( + [x for x in VALID_PROFILES]))) + + +class MapboxMatrixClient(Traceable): + ''' + Python wrapper for the Mapbox Time Matrix service. + ''' + + def __init__(self, token=ACCESS_TOKEN): + self.token = token + + def _uri(self, coordinates, profile=DEFAULT_PROFILE): + return BASEURI.format(profile=profile, coordinates=coordinates, + token=self.token) + + def _parse_matrix_response(self, response): + return response + + def matrix(self, coordinates, profile=DEFAULT_PROFILE): + validate_profile(profile) + validate_coordinates(coordinates, + NUM_COORDINATES_MIN, NUM_COORDINATES_MAX) + + coords = marshall_coordinates(coordinates) + + uri = self._uri(coords, profile) + response = requests.get(uri) + + if response.status_code == requests.codes.ok: + return self._parse_matrix_response(response.text) + else: + raise ServiceException(response.status_code, response.content) diff --git a/server/lib/python/cartodb_services/cartodb_services/mapbox/routing.py b/server/lib/python/cartodb_services/cartodb_services/mapbox/routing.py new file mode 100644 index 0000000..c3a6e0e --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/mapbox/routing.py @@ -0,0 +1,107 @@ +''' +Python client for the Mapbox Routing service. +''' + +import json +import requests +from cartodb_services.metrics import Traceable +from cartodb_services.tools import PolyLine +from cartodb_services.tools.coordinates import (validate_coordinates, + marshall_coordinates) +from cartodb_services.mapbox.exceptions import ServiceException + +ACCESS_TOKEN = 'pk.eyJ1IjoiYWNhcmxvbiIsImEiOiJjamJuZjQ1Zjc0Ymt4Mnh0YmFrMmhtYnY4In0.gt9cw0VeKc3rM2mV5pcEmg' + +BASEURI = ('https://api.mapbox.com/directions/v5/mapbox/{profile}/' + '{coordinates}' + '?access_token={token}' + '&overview={overview}') + +NUM_WAYPOINTS_MIN = 2 # https://www.mapbox.com/api-documentation/#directions +NUM_WAYPOINTS_MAX = 25 # https://www.mapbox.com/api-documentation/#directions + +PROFILE_DRIVING_TRAFFIC = 'driving-traffic' +PROFILE_DRIVING = 'driving' +PROFILE_CYCLING = 'cycling' +PROFILE_WALKING = 'walking' +DEFAULT_PROFILE = PROFILE_DRIVING + +DEFAULT_OVERVIEW = 'full' + +VALID_PROFILES = [PROFILE_DRIVING_TRAFFIC, + PROFILE_DRIVING, + PROFILE_CYCLING, + PROFILE_WALKING] + +ENTRY_ROUTES = 'routes' +ENTRY_GEOMETRY = 'geometry' +ENTRY_DURATION = 'duration' +ENTRY_DISTANCE = 'distance' + + +class MapboxRouting(Traceable): + ''' + Python wrapper for the Mapbox Routing service. + ''' + + def __init__(self, token=ACCESS_TOKEN): + self._token = token + + def _uri(self, coordinates, profile=DEFAULT_PROFILE, + overview=DEFAULT_OVERVIEW): + return BASEURI.format(profile=profile, coordinates=coordinates, + token=self._token, overview=overview) + + def _validate_profile(self, profile): + if profile not in VALID_PROFILES: + raise ValueError('{profile} is not a valid profile. ' + 'Valid profiles are: {valid_profiles}'.format( + profile=profile, + valid_profiles=', '.join( + [x for x in VALID_PROFILES]))) + + def _parse_routing_response(self, response): + json_response = json.loads(response) + route = json_response[ENTRY_ROUTES][0] # Force the first route + + geometry = PolyLine().decode(route[ENTRY_GEOMETRY]) + distance = route[ENTRY_DISTANCE] + duration = route[ENTRY_DURATION] + + return MapboxRoutingResponse(geometry, distance, duration) + + def directions(self, waypoints, profile=DEFAULT_PROFILE): + self._validate_profile(profile) + validate_coordinates(waypoints, NUM_WAYPOINTS_MIN, NUM_WAYPOINTS_MAX) + + coordinates = marshall_coordinates(waypoints) + + uri = self._uri(coordinates, profile) + response = requests.get(uri) + + if response.status_code == requests.codes.ok: + return self._parse_routing_response(response.text) + elif response.status_code == requests.codes.bad_request: + return MapboxRoutingResponse(None, None, None) + else: + raise ServiceException(response.status_code, response.content) + + +class MapboxRoutingResponse: + + 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 diff --git a/server/lib/python/cartodb_services/cartodb_services/tools/coordinates.py b/server/lib/python/cartodb_services/cartodb_services/tools/coordinates.py index 759b02e..20483c4 100644 --- a/server/lib/python/cartodb_services/cartodb_services/tools/coordinates.py +++ b/server/lib/python/cartodb_services/cartodb_services/tools/coordinates.py @@ -17,5 +17,23 @@ class Coordinate: def to_json(self): return "{{\"lon\": {0},\"lat\": {1}}}".format(self._longitude, self._latitude) + def __str__(self): - return "{0}, {1}".format(self._longitude, self._latitude) \ No newline at end of file + return "{0}, {1}".format(self._longitude, self._latitude) + + +def validate_coordinates(coordinates, + num_coordinates_min, num_coordinates_max): + if not coordinates: + raise ValueError('Invalid (empty) coordinates.') + + if len(coordinates) < num_coordinates_min \ + or len(coordinates) > num_coordinates_max: + raise ValueError('Invalid number of coordinates. ' + 'Must be between {min} and {max}'.format( + min=num_coordinates_min, + max=num_coordinates_max)) + +def marshall_coordinates(coordinates): + return ';'.join([str(coordinate).replace(' ', '') + for coordinate in coordinates]) diff --git a/server/lib/python/cartodb_services/cartodb_services/tools/spherical.py b/server/lib/python/cartodb_services/cartodb_services/tools/spherical.py new file mode 100644 index 0000000..496acda --- /dev/null +++ b/server/lib/python/cartodb_services/cartodb_services/tools/spherical.py @@ -0,0 +1,17 @@ +from math import cos, sin, pi, radians, degrees, asin, atan2 +from cartodb_services.tools import Coordinate + +EARTH_RADIUS_METERS = 6367444 + + +def get_angles(number_of_angles): # Angle in radians + step = (2.0 * pi) / number_of_angles + return [(x * step) for x in xrange(0, number_of_angles)] + +def calculate_dest_location(origin, angle, radius): # Angle in radians + origin_lat_radians = radians(origin.latitude) + origin_long_radians = radians(origin.longitude) + dest_lat_radians = asin(sin(origin_lat_radians) * cos(radius / EARTH_RADIUS_METERS) + cos(origin_lat_radians) * sin(radius / EARTH_RADIUS_METERS) * cos(angle)) + dest_lng_radians = origin_long_radians + atan2(sin(angle) * sin(radius / EARTH_RADIUS_METERS) * cos(origin_lat_radians), cos(radius / EARTH_RADIUS_METERS) - sin(origin_lat_radians) * sin(dest_lat_radians)) + + return Coordinate(degrees(dest_lng_radians), degrees(dest_lat_radians)) diff --git a/server/lib/python/cartodb_services/requirements.txt b/server/lib/python/cartodb_services/requirements.txt index 74693fe..b628aef 100644 --- a/server/lib/python/cartodb_services/requirements.txt +++ b/server/lib/python/cartodb_services/requirements.txt @@ -6,6 +6,7 @@ rollbar==0.13.2 # Dependency for googlemaps package requests==2.9.1 rratelimit==0.0.4 +mapbox # Test mock==1.3.0 diff --git a/server/lib/python/cartodb_services/test/test_mapboxgeocoder.py b/server/lib/python/cartodb_services/test/test_mapboxgeocoder.py new file mode 100644 index 0000000..0b0d8dd --- /dev/null +++ b/server/lib/python/cartodb_services/test/test_mapboxgeocoder.py @@ -0,0 +1,24 @@ +import unittest +from cartodb_services.mapbox import MapboxGeocoder +from cartodb_services.mapbox import ServiceException + +INVALID_TOKEN = 'invalid_token' +VALID_ADDRESS = 'Calle Siempreviva 3, Valladolid' +WELL_KNOWN_LONGITUDE = -4.730947 +WELL_KNOWN_LATITUDE = 41.668654 + + +class MapboxGeocoderTestCase(unittest.TestCase): + def setUp(self): + self.geocoder = MapboxGeocoder() + + def test_invalid_token(self): + invalid_geocoder = MapboxGeocoder(token=INVALID_TOKEN) + with self.assertRaises(ServiceException): + invalid_geocoder.geocode(VALID_ADDRESS) + + def test_valid_request(self): + place = self.geocoder.geocode(VALID_ADDRESS) + + self.assertEqual(place[0], WELL_KNOWN_LONGITUDE) + self.assertEqual(place[1], WELL_KNOWN_LATITUDE) diff --git a/server/lib/python/cartodb_services/test/test_mapboxisoline.py b/server/lib/python/cartodb_services/test/test_mapboxisoline.py new file mode 100644 index 0000000..05737b7 --- /dev/null +++ b/server/lib/python/cartodb_services/test/test_mapboxisoline.py @@ -0,0 +1,36 @@ +import unittest +from cartodb_services.mapbox.isolines import MapboxIsolines +from cartodb_services.mapbox.matrix_client import DEFAULT_PROFILE +from cartodb_services.mapbox.matrix_client import MapboxMatrixClient +from cartodb_services.mapbox.routing import MapboxRouting +from cartodb_services.tools import Coordinate +from cartodb_services.tools.coordinates import (validate_coordinates, + marshall_coordinates) + +VALID_ORIGIN = Coordinate(-73.989, 40.733) + + +class MapboxIsolinesTestCase(unittest.TestCase): + + def setUp(self): + matrix_client = MapboxMatrixClient() + routing_client = MapboxRouting() + self.mapbox_isolines = MapboxIsolines(matrix_client, routing_client) + + def test_calculate_isochrone(self): + time_range = 10 * 60 # 10 minutes + solution = self.mapbox_isolines.calculate_isochrone( + origin=VALID_ORIGIN, + profile=DEFAULT_PROFILE, + time_range=time_range) + + assert solution + + def test_calculate_isodistance(self): + distance_range = 10000 + solution = self.mapbox_isolines.calculate_isodistance( + origin=VALID_ORIGIN, + profile=DEFAULT_PROFILE, + distance_range=distance_range) + + assert solution diff --git a/server/lib/python/cartodb_services/test/test_mapboxmatrix.py b/server/lib/python/cartodb_services/test/test_mapboxmatrix.py new file mode 100644 index 0000000..4aa3a33 --- /dev/null +++ b/server/lib/python/cartodb_services/test/test_mapboxmatrix.py @@ -0,0 +1,54 @@ +import unittest +from cartodb_services.mapbox import MapboxMatrixClient +from cartodb_services.mapbox.matrix_client import DEFAULT_PROFILE +from cartodb_services.mapbox import ServiceException +from cartodb_services.tools import Coordinate + +INVALID_TOKEN = 'invalid_token' +VALID_ORIGIN = Coordinate(-73.989, 40.733) +VALID_TARGET = Coordinate(-74, 40.733) +VALID_COORDINATES = [VALID_ORIGIN] + [VALID_TARGET] +NUM_COORDINATES_MAX = 25 +INVALID_COORDINATES_EMPTY = [] +INVALID_COORDINATES_MIN = [VALID_ORIGIN] +INVALID_COORDINATES_MAX = [VALID_ORIGIN] + \ + [VALID_TARGET + for x in range(0, NUM_COORDINATES_MAX + 1)] +VALID_PROFILE = DEFAULT_PROFILE +INVALID_PROFILE = 'invalid_profile' + + +class MapboxMatrixTestCase(unittest.TestCase): + def setUp(self): + self.matrix_client = MapboxMatrixClient() + + def test_invalid_profile(self): + with self.assertRaises(ValueError): + self.matrix_client.matrix(VALID_COORDINATES, + INVALID_PROFILE) + + def test_invalid_coordinates_empty(self): + with self.assertRaises(ValueError): + self.matrix_client.matrix(INVALID_COORDINATES_EMPTY, + VALID_PROFILE) + + def test_invalid_coordinates_max(self): + with self.assertRaises(ValueError): + self.matrix_client.matrix(INVALID_COORDINATES_MAX, + VALID_PROFILE) + + def test_invalid_coordinates_min(self): + with self.assertRaises(ValueError): + self.matrix_client.matrix(INVALID_COORDINATES_MIN, + VALID_PROFILE) + + def test_invalid_token(self): + invalid_matrix = MapboxMatrixClient(token=INVALID_TOKEN) + with self.assertRaises(ServiceException): + invalid_matrix.matrix(VALID_COORDINATES, + VALID_PROFILE) + + def test_valid_request(self): + distance_matrix = self.matrix_client.matrix(VALID_COORDINATES, + VALID_PROFILE) + assert distance_matrix diff --git a/server/lib/python/cartodb_services/test/test_mapboxrouting.py b/server/lib/python/cartodb_services/test/test_mapboxrouting.py new file mode 100644 index 0000000..118e97a --- /dev/null +++ b/server/lib/python/cartodb_services/test/test_mapboxrouting.py @@ -0,0 +1,61 @@ +import unittest +from cartodb_services.mapbox import MapboxRouting +from cartodb_services.mapbox.routing import DEFAULT_PROFILE +from cartodb_services.mapbox import ServiceException +from cartodb_services.tools import Coordinate + +INVALID_TOKEN = 'invalid_token' +VALID_WAYPOINTS = [Coordinate(-73.989, 40.733), Coordinate(-74, 40.733)] +NUM_WAYPOINTS_MAX = 25 +INVALID_WAYPOINTS_EMPTY = [] +INVALID_WAYPOINTS_MIN = [Coordinate(-73.989, 40.733)] +INVALID_WAYPOINTS_MAX = [Coordinate(-73.989, 40.733) + for x in range(0, NUM_WAYPOINTS_MAX + 2)] +VALID_PROFILE = DEFAULT_PROFILE +INVALID_PROFILE = 'invalid_profile' + +WELL_KNOWN_SHAPE = [(40.73312, -73.98891), (40.73353, -73.98987), + (40.73398, -73.99095), (40.73453, -73.99227), + (40.73531, -73.99412), (40.73467, -73.99459), + (40.73442, -73.99477), (40.73435, -73.99482), + (40.73403, -73.99505), (40.73344, -73.99549), + (40.73286, -73.9959), (40.73226, -73.99635), + (40.73186, -73.99664), (40.73147, -73.99693), + (40.73141, -73.99698), (40.73147, -73.99707), + (40.73219, -73.99856), (40.73222, -73.99861), + (40.73293, -74.00007), (40.733, -74.00001)] +WELL_KNOWN_LENGTH = 1317.9 + + +class MapboxRoutingTestCase(unittest.TestCase): + def setUp(self): + self.routing = MapboxRouting() + + def test_invalid_profile(self): + with self.assertRaises(ValueError): + self.routing.directions(VALID_WAYPOINTS, INVALID_PROFILE) + + def test_invalid_waypoints_empty(self): + with self.assertRaises(ValueError): + self.routing.directions(INVALID_WAYPOINTS_EMPTY, VALID_PROFILE) + + def test_invalid_waypoints_min(self): + with self.assertRaises(ValueError): + self.routing.directions(INVALID_WAYPOINTS_MIN, VALID_PROFILE) + + def test_invalid_waypoints_max(self): + with self.assertRaises(ValueError): + self.routing.directions(INVALID_WAYPOINTS_MAX, VALID_PROFILE) + + def test_invalid_token(self): + invalid_routing = MapboxRouting(token=INVALID_TOKEN) + with self.assertRaises(ServiceException): + invalid_routing.directions(VALID_WAYPOINTS, + VALID_PROFILE) + + def test_valid_request(self): + route = self.routing.directions(VALID_WAYPOINTS, VALID_PROFILE) + + self.assertEqual(route.shape, WELL_KNOWN_SHAPE) + self.assertEqual(route.length, WELL_KNOWN_LENGTH) + assert route.duration # The duration may change between executions