First implementation of the true Mapbox isochrones

This commit is contained in:
antoniocarlon
2020-01-17 17:04:43 +01:00
parent d2be601049
commit 2a68291da7
15 changed files with 546 additions and 2 deletions

View File

@@ -2,4 +2,5 @@ from routing import MapboxRouting, MapboxRoutingResponse
from geocoder import MapboxGeocoder
from bulk_geocoder import MapboxBulkGeocoder
from isolines import MapboxIsolines, MapboxIsochronesResponse
from true_isolines import MapboxTrueIsolines, MapboxTrueIsochronesResponse
from matrix_client import MapboxMatrixClient

View File

@@ -0,0 +1,142 @@
import json
import requests
from uritemplate import URITemplate
from cartodb_services.tools.exceptions import ServiceException
from cartodb_services.tools.qps import qps_retry
from cartodb_services.tools import Coordinate
BASEURI = ('https://api.mapbox.com/isochrone/v1/mapbox/{profile}/{coordinates}?contours_minutes={contours_minutes}&access_token={apikey}')
PROFILE_DRIVING = 'driving'
PROFILE_CYCLING = 'cycling'
PROFILE_WALKING = 'walking'
DEFAULT_PROFILE = PROFILE_DRIVING
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
}
VALID_PROFILES = [PROFILE_DRIVING,
PROFILE_CYCLING,
PROFILE_WALKING]
ENTRY_FEATURES = 'features'
ENTRY_GEOMETRY = 'geometry'
class MapboxTrueIsolines():
'''
Python wrapper for Mapbox based isolines.
'''
def __init__(self, apikey, logger, service_params=None):
service_params = service_params or {}
self._apikey = apikey
self._logger = logger
def _uri(self, origin, time_range, profile=DEFAULT_PROFILE):
uri = URITemplate(BASEURI).expand(apikey=self._apikey,
coordinates=origin,
contours_minutes=time_range,
profile=profile)
return uri
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_coordinates(self, boundary):
return [Coordinate(c[0], c[1]) for c in boundary]
def _parse_isochrone_service(self, response):
json_response = json.loads(response)
coordinates = []
if json_response:
for feature in json_response[ENTRY_FEATURES]:
geometry = feature[ENTRY_GEOMETRY]
coordinates.append(self._parse_coordinates(geometry))
return coordinates
@qps_retry(qps=5, provider='mapbox_iso')
def _calculate_isoline(self, origin, time_ranges,
profile=DEFAULT_PROFILE):
origin = '{lon},{lat}'.format(lat=origin.latitude,
lon=origin.longitude)
time_ranges.sort()
time_ranges_seconds = ','.join([str(round(t/60)) for t in time_ranges])
uri = self._uri(origin, time_ranges_seconds, profile)
try:
response = requests.get(uri)
if response.status_code == requests.codes.ok:
isolines = []
coordinates = self._parse_isochrone_service(response.text)
for t, c in zip(time_ranges, coordinates):
isolines.append(MapboxTrueIsochronesResponse(c, t))
return isolines
elif response.status_code == requests.codes.bad_request:
return []
elif response.status_code == requests.codes.unprocessable_entity:
return []
else:
raise ServiceException(response.status_code, response)
except requests.Timeout as te:
# In case of timeout we want to stop the job because the server
# could be down
self._logger.error('Timeout connecting to Mapbox isochrone service',
te)
raise ServiceException('Error getting isochrone data from Mapbox',
None)
except requests.ConnectionError as ce:
# Don't raise the exception to continue with the geocoding job
self._logger.error('Error connecting to Mapbox isochrone service',
exception=ce)
return []
def calculate_isochrone(self, origin, time_ranges,
profile=DEFAULT_PROFILE):
self._validate_profile(profile)
return self._calculate_isoline(origin=origin,
time_ranges=time_ranges,
profile=profile)
def calculate_isodistance(self, origin, distance_range,
profile=DEFAULT_PROFILE):
self._validate_profile(profile)
max_speed = MAX_SPEEDS[profile]
time_range = distance_range / max_speed
return self._calculate_isoline(origin=origin,
time_ranges=[time_range],
profile=profile)[0].coordinates
class MapboxTrueIsochronesResponse:
def __init__(self, coordinates, duration):
self._coordinates = coordinates
self._duration = duration
@property
def coordinates(self):
return self._coordinates
@property
def duration(self):
return self._duration

View File

@@ -1,6 +1,7 @@
MAPBOX_ROUTING_APIKEY_ROUNDROBIN = 'mapbox_routing_apikey_roundrobin'
MAPBOX_GEOCODER_APIKEY_ROUNDROBIN = 'mapbox_geocoder_apikey_roundrobin'
MAPBOX_ISOLINES_APIKEY_ROUNDROBIN = 'mapbox_isolines_apikey_roundrobin'
MAPBOX_ISO_ISOLINES_APIKEY_ROUNDROBIN = 'mapbox_iso_isolines_apikey_roundrobin'
TRANSPORT_MODE_TO_MAPBOX = {
'car': 'driving',

View File

@@ -224,6 +224,7 @@ class IsolinesRoutingConfig(ServiceConfig):
GEOCODER_PROVIDER_KEY = 'geocoder_provider'
MAPZEN_PROVIDER = 'mapzen'
MAPBOX_PROVIDER = 'mapbox'
MAPBOX_ISO_PROVIDER = 'mapbox'
TOMTOM_PROVIDER = 'tomtom'
HEREMAPS_PROVIDER = 'heremaps'
DEFAULT_PROVIDER = MAPBOX_PROVIDER
@@ -258,6 +259,9 @@ class IsolinesRoutingConfig(ServiceConfig):
self._mapbox_matrix_api_keys = self._db_config.mapbox_matrix_api_keys
self._mapbox_matrix_service_params = db_config.mapbox_matrix_service_params
self._mapbox_isochrones_service_params = db_config.mapbox_isochrones_service_params
elif self._isolines_provider == self.MAPBOX_ISO_PROVIDER:
self._mapbox_iso_isolines_api_keys = self._db_config.mapbox_iso_isolines_api_keys
self._mapbox_iso_isolines_service_params = db_config.mapbox_iso_isolines_service_params
elif self._isolines_provider == self.TOMTOM_PROVIDER:
self._tomtom_isolinesx_api_keys = self._db_config.tomtom_isolines_api_keys
self._tomtom_isolines_service_params = db_config.tomtom_isolines_service_params
@@ -270,6 +274,8 @@ class IsolinesRoutingConfig(ServiceConfig):
return 'mapzen_isolines'
elif self._isolines_provider == self.MAPBOX_PROVIDER:
return 'mapbox_isolines'
elif self._isolines_provider == self.MAPBOX_ISO_PROVIDER:
return 'mapbox_iso_isolines'
elif self._isolines_provider == self.TOMTOM_PROVIDER:
return 'tomtom_isolines'
@@ -333,6 +339,18 @@ class IsolinesRoutingConfig(ServiceConfig):
def mapbox_provider(self):
return self._isolines_provider == self.MAPBOX_PROVIDER
@property
def mapbox_iso_isolines_api_keys(self):
return self._mapbox_iso_isolines_api_keys
@property
def mapbox_iso_isolines_service_params(self):
return self._mapbox_iso_isolines_service_params
@property
def mapbox_iso_provider(self):
return self._isolines_provider == self.MAPBOX_ISO_PROVIDER
@property
def tomtom_isolines_api_keys(self):
return self._tomtom_isolines_api_keys
@@ -599,6 +617,7 @@ class ServicesDBConfig:
self._get_here_config()
self._get_mapzen_config()
self._get_mapbox_config()
self._get_mapbox_iso_config()
self._get_tomtom_config()
self._get_data_observatory_config()
@@ -663,6 +682,16 @@ class ServicesDBConfig:
self._mapbox_geocoder_quota = mapbox_conf['geocoder']['monthly_quota']
self._mapbox_geocoder_service_params = mapbox_conf['geocoder'].get('service', {})
def _get_mapbox_iso_config(self):
mapbox_iso_conf_json = self._get_conf('mapbox_iso_conf')
if not mapbox_iso_conf_json:
raise ConfigException('Mapbox True Isochrones configuration missing')
mapbox_iso_conf = json.loads(mapbox_iso_conf_json)
self._mapbox_iso_isolines_api_keys = mapbox_conf['isolines']['api_keys']
self._mapbox_iso_isolines_quota = tomtom_conf['isolines']['monthly_quota']
self._mapbox_iso_isolines_service_params = tomtom_conf.get('isolines', {}).get('service', {})
def _get_tomtom_config(self):
tomtom_conf_json = self._get_conf('tomtom_conf')
if not tomtom_conf_json:
@@ -812,6 +841,18 @@ class ServicesDBConfig:
def mapbox_geocoder_service_params(self):
return self._mapbox_geocoder_service_params
@property
def mapbox_iso_isolines_api_keys(self):
return self._mapbox_iso_isolines_api_keys
@property
def mapbox_iso_isolines_monthly_quota(self):
return self._mapbox_iso_isolines_quota
@property
def mapbox_iso_isolines_service_params(self):
return self._mapbox_iso_isolines_service_params
@property
def tomtom_isolines_api_keys(self):
return self._tomtom_isolines_api_keys

View File

@@ -22,6 +22,7 @@ class UserMetricsService:
SERVICE_HERE_ISOLINES = 'here_isolines'
SERVICE_MAPZEN_ISOLINES = 'mapzen_isolines'
SERVICE_MAPBOX_ISOLINES = 'mapbox_isolines'
SERVICE_MAPBOX_ISO_ISOLINES = 'mapbox_iso_isolines'
SERVICE_TOMTOM_ISOLINES = 'tomtom_isolines'
SERVICE_MAPZEN_ROUTING = 'routing_mapzen'
SERVICE_MAPBOX_ROUTING = 'routing_mapbox'
@@ -39,6 +40,7 @@ class UserMetricsService:
if service_type in [self.SERVICE_HERE_ISOLINES,
self.SERVICE_MAPZEN_ISOLINES,
self.SERVICE_MAPBOX_ISOLINES,
self.SERVICE_MAPBOX_ISO_ISOLINES,
self.SERVICE_TOMTOM_ISOLINES]:
return self.__used_isolines_quota(service_type, date)
elif service_type in [self.SERVICE_MAPZEN_ROUTING,

View File

@@ -0,0 +1,123 @@
from dateutil.parser import parse as date_parse
from cartodb_services.refactor.service.utils import round_robin
from cartodb_services.mapbox.types import MAPBOX_ISOLINES_APIKEY_ROUNDROBIN
class MapboxTrueIsolinesConfig(object):
"""
Configuration needed to operate the Mapbox directions service.
"""
def __init__(self,
isolines_quota,
soft_isolines_limit,
period_end_date,
cost_per_hit,
log_path,
mapbox_api_keys,
username,
organization,
service_params,
GD):
self._isolines_quota = isolines_quota
self._soft_isolines_limit = soft_isolines_limit
self._period_end_date = period_end_date
self._cost_per_hit = cost_per_hit
self._log_path = log_path
self._mapbox_api_keys = mapbox_api_keys
self._username = username
self._organization = organization
self._service_params = service_params
self._GD = GD
@property
def service_type(self):
return 'mapbox_isolines'
@property
def provider(self):
return 'mapbox'
@property
def is_high_resolution(self):
return True
@property
def isolines_quota(self):
return self._isolines_quota
@property
def soft_isolines_limit(self):
return self._soft_isolines_limit
@property
def period_end_date(self):
return self._period_end_date
@property
def cost_per_hit(self):
return self._cost_per_hit
@property
def log_path(self):
return self._log_path
@property
def mapbox_api_key(self):
return round_robin(self._mapbox_api_keys, self._GD,
MAPBOX_ISO_ISOLINES_APIKEY_ROUNDROBIN)
@property
def username(self):
return self._username
@property
def organization(self):
return self._organization
@property
def service_params(self):
return self._service_params
class MapboxTrueIsolinesConfigBuilder(object):
def __init__(self, server_conf, user_conf, org_conf, username, orgname, GD):
self._server_conf = server_conf
self._user_conf = user_conf
self._org_conf = org_conf
self._username = username
self._orgname = orgname
self._GD = GD
def get(self):
mapbox_server_conf = self._server_conf.get('mapbox_iso_conf')
mapbox_api_keys = mapbox_server_conf['isolines']['api_keys']
mapbox_service_params = mapbox_server_conf['isolines'].get('service', {})
isolines_quota = self._get_quota()
soft_isolines_limit = self._user_conf.get('soft_here_isolines_limit').lower() == 'true'
cost_per_hit = 0
period_end_date_str = self._org_conf.get('period_end_date') or self._user_conf.get('period_end_date')
period_end_date = date_parse(period_end_date_str)
logger_conf = self._server_conf.get('logger_conf')
log_path = logger_conf.get('isolines_log_path', None)
return MapboxTrueIsolinesConfig(isolines_quota,
soft_isolines_limit,
period_end_date,
cost_per_hit,
log_path,
mapbox_api_keys,
self._username,
self._orgname,
mapbox_service_params,
self._GD)
def _get_quota(self):
isolines_quota = self._org_conf.get('here_isolines_quota') or self._user_conf.get('here_isolines_quota')
if isolines_quota is '':
return 0
return int(isolines_quota)