Merge pull request #216 from CartoDB/mapzen-isolines-reloaded
Add mapzen isolines
This commit is contained in:
commit
5c20866277
@ -103,6 +103,26 @@
|
|||||||
- { name: range, type: "integer[]" }
|
- { name: range, type: "integer[]" }
|
||||||
- { name: options, type: "text[]", default: 'ARRAY[]::text[]' }
|
- { name: options, type: "text[]", default: 'ARRAY[]::text[]' }
|
||||||
|
|
||||||
|
- name: cdb_mapzen_isochrone
|
||||||
|
return_type: SETOF cdb_dataservices_client.isoline
|
||||||
|
multi_row: true
|
||||||
|
multi_field: true
|
||||||
|
params:
|
||||||
|
- { name: source, type: "geometry(Geometry, 4326)" }
|
||||||
|
- { name: mode, type: text }
|
||||||
|
- { name: range, type: "integer[]" }
|
||||||
|
- { name: options, type: "text[]", default: 'ARRAY[]::text[]' }
|
||||||
|
|
||||||
|
- name: cdb_mapzen_isodistance
|
||||||
|
return_type: SETOF cdb_dataservices_client.isoline
|
||||||
|
multi_row: true
|
||||||
|
multi_field: true
|
||||||
|
params:
|
||||||
|
- { name: source, type: "geometry(Geometry, 4326)" }
|
||||||
|
- { name: mode, type: text }
|
||||||
|
- { name: range, type: "integer[]" }
|
||||||
|
- { name: options, type: "text[]", default: 'ARRAY[]::text[]' }
|
||||||
|
|
||||||
- name: cdb_route_point_to_point
|
- name: cdb_route_point_to_point
|
||||||
return_type: cdb_dataservices_client.simple_route
|
return_type: cdb_dataservices_client.simple_route
|
||||||
multi_field: true
|
multi_field: true
|
||||||
|
@ -26,6 +26,20 @@ RETURNS boolean AS $$
|
|||||||
return True
|
return True
|
||||||
$$ LANGUAGE plpythonu SECURITY DEFINER;
|
$$ LANGUAGE plpythonu SECURITY DEFINER;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION cdb_dataservices_server._get_mapzen_isolines_config(username text, orgname text)
|
||||||
|
RETURNS boolean AS $$
|
||||||
|
cache_key = "user_mapzen_isolines_routing_config_{0}".format(username)
|
||||||
|
if cache_key in GD:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
from cartodb_services.metrics import MapzenIsolinesRoutingConfig
|
||||||
|
plpy.execute("SELECT cdb_dataservices_server._connect_to_redis('{0}')".format(username))
|
||||||
|
redis_conn = GD["redis_connection_{0}".format(username)]['redis_metadata_connection']
|
||||||
|
mapzen_isolines_config = MapzenIsolinesRoutingConfig(redis_conn, plpy, username, orgname)
|
||||||
|
GD[cache_key] = mapzen_isolines_config
|
||||||
|
return True
|
||||||
|
$$ LANGUAGE plpythonu SECURITY DEFINER;
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION cdb_dataservices_server._get_internal_geocoder_config(username text, orgname text)
|
CREATE OR REPLACE FUNCTION cdb_dataservices_server._get_internal_geocoder_config(username text, orgname text)
|
||||||
RETURNS boolean AS $$
|
RETURNS boolean AS $$
|
||||||
cache_key = "user_internal_geocoder_config_{0}".format(username)
|
cache_key = "user_internal_geocoder_config_{0}".format(username)
|
||||||
|
@ -53,3 +53,74 @@ RETURNS SETOF cdb_dataservices_server.isoline AS $$
|
|||||||
finally:
|
finally:
|
||||||
quota_service.increment_total_service_use()
|
quota_service.increment_total_service_use()
|
||||||
$$ LANGUAGE plpythonu SECURITY DEFINER;
|
$$ LANGUAGE plpythonu SECURITY DEFINER;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION cdb_dataservices_server._cdb_mapzen_isolines(
|
||||||
|
username TEXT,
|
||||||
|
orgname TEXT,
|
||||||
|
isotype TEXT,
|
||||||
|
source geometry(Geometry, 4326),
|
||||||
|
mode TEXT,
|
||||||
|
data_range integer[],
|
||||||
|
options text[])
|
||||||
|
RETURNS SETOF cdb_dataservices_server.isoline AS $$
|
||||||
|
import json
|
||||||
|
from cartodb_services.mapzen import MatrixClient
|
||||||
|
from cartodb_services.mapzen import MapzenIsolines
|
||||||
|
from cartodb_services.metrics import QuotaService
|
||||||
|
|
||||||
|
redis_conn = GD["redis_connection_{0}".format(username)]['redis_metrics_connection']
|
||||||
|
user_mapzen_isolines_routing_config = GD["user_mapzen_isolines_routing_config_{0}".format(username)]
|
||||||
|
|
||||||
|
# -- Check the quota
|
||||||
|
quota_service = QuotaService(user_mapzen_isolines_routing_config, redis_conn)
|
||||||
|
if not quota_service.check_user_quota():
|
||||||
|
plpy.error('You have reached the limit of your quota')
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MatrixClient(user_mapzen_isolines_routing_config.mapzen_matrix_api_key)
|
||||||
|
mapzen_isolines = MapzenIsolines(client)
|
||||||
|
|
||||||
|
if source:
|
||||||
|
lat = plpy.execute("SELECT ST_Y('%s') AS lat" % source)[0]['lat']
|
||||||
|
lon = plpy.execute("SELECT ST_X('%s') AS lon" % source)[0]['lon']
|
||||||
|
origin = {'lat': lat, 'lon': lon}
|
||||||
|
else:
|
||||||
|
raise Exception('source is NULL')
|
||||||
|
|
||||||
|
# -- TODO Support options properly
|
||||||
|
isolines = {}
|
||||||
|
if isotype == 'isodistance':
|
||||||
|
for r in data_range:
|
||||||
|
isoline = mapzen_isolines.calculate_isodistance(origin, mode, r)
|
||||||
|
isolines[r] = (isoline)
|
||||||
|
elif isotype == 'isochrone':
|
||||||
|
for r in data_range:
|
||||||
|
isoline = mapzen_isolines.calculate_isochrone(origin, mode, r)
|
||||||
|
isolines[r] = (isoline)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for r in data_range:
|
||||||
|
|
||||||
|
# -- TODO encapsulate this block into a func/method
|
||||||
|
locations = isolines[r] + [ isolines[r][0] ] # close the polygon repeating the first point
|
||||||
|
wkt_coordinates = ','.join(["%f %f" % (l['lon'], l['lat']) for l in locations])
|
||||||
|
sql = "SELECT ST_MPolyFromText('MULTIPOLYGON((({0})))', 4326) as geom".format(wkt_coordinates)
|
||||||
|
multipolygon = plpy.execute(sql, 1)[0]['geom']
|
||||||
|
|
||||||
|
result.append([source, r, multipolygon])
|
||||||
|
|
||||||
|
quota_service.increment_success_service_use()
|
||||||
|
quota_service.increment_isolines_service_use(len(isolines))
|
||||||
|
return result
|
||||||
|
except BaseException as e:
|
||||||
|
import sys, traceback
|
||||||
|
type_, value_, traceback_ = sys.exc_info()
|
||||||
|
quota_service.increment_failed_service_use()
|
||||||
|
error_msg = 'There was an error trying to obtain isolines using mapzen: {0}'.format(e)
|
||||||
|
plpy.debug(traceback.format_tb(traceback_))
|
||||||
|
raise e
|
||||||
|
#plpy.error(error_msg)
|
||||||
|
finally:
|
||||||
|
quota_service.increment_total_service_use()
|
||||||
|
$$ LANGUAGE plpythonu SECURITY DEFINER;
|
||||||
|
@ -19,3 +19,23 @@ RETURNS SETOF cdb_dataservices_server.isoline AS $$
|
|||||||
|
|
||||||
return isolines
|
return isolines
|
||||||
$$ LANGUAGE plpythonu;
|
$$ LANGUAGE plpythonu;
|
||||||
|
|
||||||
|
-- mapzen isodistance
|
||||||
|
CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_mapzen_isodistance(username TEXT, orgname TEXT, source geometry(Geometry, 4326), mode TEXT, range integer[], options text[] DEFAULT array[]::text[])
|
||||||
|
RETURNS SETOF cdb_dataservices_server.isoline AS $$
|
||||||
|
plpy.execute("SELECT cdb_dataservices_server._connect_to_redis('{0}')".format(username))
|
||||||
|
redis_conn = GD["redis_connection_{0}".format(username)]['redis_metrics_connection']
|
||||||
|
plpy.execute("SELECT cdb_dataservices_server._get_mapzen_isolines_config({0}, {1})".format(plpy.quote_nullable(username), plpy.quote_nullable(orgname)))
|
||||||
|
user_isolines_config = GD["user_mapzen_isolines_routing_config_{0}".format(username)]
|
||||||
|
type = 'isodistance'
|
||||||
|
|
||||||
|
mapzen_plan = plpy.prepare("SELECT cdb_dataservices_server._cdb_mapzen_isolines($1, $2, $3, $4, $5, $6, $7) as isoline; ", ["text", "text", "text", "geometry(Geometry, 4326)", "text", "integer[]", "text[]"])
|
||||||
|
result = plpy.execute(mapzen_plan, [username, orgname, type, source, mode, range, options])
|
||||||
|
isolines = []
|
||||||
|
for element in result:
|
||||||
|
isoline = element['isoline']
|
||||||
|
isoline = isoline.translate(None, "()").split(',')
|
||||||
|
isolines.append(isoline)
|
||||||
|
|
||||||
|
return isolines
|
||||||
|
$$ LANGUAGE plpythonu;
|
||||||
|
@ -19,3 +19,24 @@ RETURNS SETOF cdb_dataservices_server.isoline AS $$
|
|||||||
|
|
||||||
return isolines
|
return isolines
|
||||||
$$ LANGUAGE plpythonu;
|
$$ LANGUAGE plpythonu;
|
||||||
|
|
||||||
|
|
||||||
|
-- mapzen isochrones
|
||||||
|
CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_mapzen_isochrone(username TEXT, orgname TEXT, source geometry(Geometry, 4326), mode TEXT, range integer[], options text[] DEFAULT array[]::text[])
|
||||||
|
RETURNS SETOF cdb_dataservices_server.isoline AS $$
|
||||||
|
plpy.execute("SELECT cdb_dataservices_server._connect_to_redis('{0}')".format(username))
|
||||||
|
redis_conn = GD["redis_connection_{0}".format(username)]['redis_metrics_connection']
|
||||||
|
plpy.execute("SELECT cdb_dataservices_server._get_mapzen_isolines_config({0}, {1})".format(plpy.quote_nullable(username), plpy.quote_nullable(orgname)))
|
||||||
|
user_isolines_config = GD["user_mapzen_isolines_routing_config_{0}".format(username)]
|
||||||
|
type = 'isochrone'
|
||||||
|
|
||||||
|
mapzen_plan = plpy.prepare("SELECT cdb_dataservices_server._cdb_mapzen_isolines($1, $2, $3, $4, $5, $6, $7) as isoline; ", ["text", "text", "text", "geometry(Geometry, 4326)", "text", "integer[]", "text[]"])
|
||||||
|
result = plpy.execute(mapzen_plan, [username, orgname, type, source, mode, range, options])
|
||||||
|
isolines = []
|
||||||
|
for element in result:
|
||||||
|
isoline = element['isoline']
|
||||||
|
isoline = isoline.translate(None, "()").split(',') #--TODO what is this for?
|
||||||
|
isolines.append(isoline)
|
||||||
|
|
||||||
|
return isolines
|
||||||
|
$$ LANGUAGE plpythonu;
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
from routing import MapzenRouting, MapzenRoutingResponse
|
from routing import MapzenRouting, MapzenRoutingResponse
|
||||||
|
from isolines import MapzenIsolines
|
||||||
from geocoder import MapzenGeocoder
|
from geocoder import MapzenGeocoder
|
||||||
|
from matrix_client import MatrixClient
|
||||||
|
@ -0,0 +1,160 @@
|
|||||||
|
from math import cos, sin, tan, sqrt, pi, radians, degrees, asin, atan2
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class MapzenIsolines:
|
||||||
|
|
||||||
|
NUMBER_OF_ANGLES = 24
|
||||||
|
MAX_ITERS = 5
|
||||||
|
TOLERANCE = 0.1
|
||||||
|
|
||||||
|
EARTH_RADIUS_METERS = 6367444
|
||||||
|
|
||||||
|
def __init__(self, matrix_client):
|
||||||
|
self._matrix_client = matrix_client
|
||||||
|
|
||||||
|
"""Get an isochrone using mapzen API.
|
||||||
|
|
||||||
|
The implementation tries to sick close to the SQL API:
|
||||||
|
cdb_isochrone(source geometry, mode text, range integer[], [options text[]]) -> SETOF isoline
|
||||||
|
|
||||||
|
But this calculates just one isoline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin dict containing {lat: y, lon: x}
|
||||||
|
transport_mode string, for the moment just "car" or "walk"
|
||||||
|
isorange int range of the isoline in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Array of {lon: x, lat: y} as a representation of the isoline
|
||||||
|
"""
|
||||||
|
def calculate_isochrone(self, origin, transport_mode, time_range):
|
||||||
|
if transport_mode == 'walk':
|
||||||
|
max_speed = 3.3333333 # In m/s, assuming 12km/h walking speed
|
||||||
|
costing_model = 'pedestrian'
|
||||||
|
elif transport_mode == 'car':
|
||||||
|
max_speed = 41.67 # In m/s, assuming 140km/h max speed
|
||||||
|
costing_model = 'auto'
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('car and walk are the only supported modes for the moment')
|
||||||
|
|
||||||
|
upper_rmax = max_speed * time_range # an upper bound for the radius
|
||||||
|
|
||||||
|
return self.calculate_isoline(origin, costing_model, time_range, upper_rmax, 'time')
|
||||||
|
|
||||||
|
"""Get an isodistance using mapzen API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin dict containing {lat: y, lon: x}
|
||||||
|
transport_mode string, for the moment just "car" or "walk"
|
||||||
|
isorange int range of the isoline in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Array of {lon: x, lat: y} as a representation of the isoline
|
||||||
|
"""
|
||||||
|
def calculate_isodistance(self, origin, transport_mode, distance_range):
|
||||||
|
if transport_mode == 'walk':
|
||||||
|
costing_model = 'pedestrian'
|
||||||
|
elif transport_mode == 'car':
|
||||||
|
costing_model = 'auto'
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('car and walk are the only supported modes for the moment')
|
||||||
|
|
||||||
|
upper_rmax = distance_range # an upper bound for the radius, going in a straight line
|
||||||
|
|
||||||
|
return self.calculate_isoline(origin, costing_model, distance_range, upper_rmax, 'distance', 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
"""Get an isoline using mapzen API.
|
||||||
|
|
||||||
|
The implementation tries to sick close to the SQL API:
|
||||||
|
cdb_isochrone(source geometry, mode text, range integer[], [options text[]]) -> SETOF isoline
|
||||||
|
|
||||||
|
But this calculates just one isoline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
origin dict containing {lat: y, lon: x}
|
||||||
|
costing_model string "auto" or "pedestrian"
|
||||||
|
isorange int Range of the isoline in seconds
|
||||||
|
upper_rmax float An upper bound for the binary search
|
||||||
|
cost_variable string Variable to optimize "time" or "distance"
|
||||||
|
unit_factor float A factor to adapt units of isorange (meters) and units of distance (km)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Array of {lon: x, lat: y} as a representation of the isoline
|
||||||
|
"""
|
||||||
|
def calculate_isoline(self, origin, costing_model, isorange, upper_rmax, cost_variable, unit_factor=1.0):
|
||||||
|
|
||||||
|
# NOTE: not for production
|
||||||
|
#logging.basicConfig(level=logging.DEBUG, filename='/tmp/isolines.log')
|
||||||
|
#logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logging.debug('origin = %s' % origin)
|
||||||
|
logging.debug('costing_model = %s' % costing_model)
|
||||||
|
logging.debug('isorange = %d' % isorange)
|
||||||
|
|
||||||
|
# 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 = self._get_angles(self.NUMBER_OF_ANGLES) # array of angles
|
||||||
|
rmax = [upper_rmax] * self.NUMBER_OF_ANGLES
|
||||||
|
rmin = [0.0] * self.NUMBER_OF_ANGLES
|
||||||
|
location_estimates = [self._calculate_dest_location(origin, a, upper_rmax / 2.0) for a in angles]
|
||||||
|
|
||||||
|
# Iterate to refine the first solution
|
||||||
|
for i in xrange(0, self.MAX_ITERS):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
response = self._matrix_client.one_to_many([origin] + location_estimates, costing_model)
|
||||||
|
costs = [None] * self.NUMBER_OF_ANGLES
|
||||||
|
for idx, c in enumerate(response['one_to_many'][0][1:]):
|
||||||
|
if c[cost_variable]:
|
||||||
|
costs[idx] = c[cost_variable]*unit_factor
|
||||||
|
else:
|
||||||
|
costs[idx] = isorange
|
||||||
|
|
||||||
|
logging.debug('i = %d, costs = %s' % (i, costs))
|
||||||
|
|
||||||
|
errors = [(cost - isorange) / float(isorange) for cost in costs]
|
||||||
|
max_abs_error = max([abs(e) for e in errors])
|
||||||
|
if max_abs_error <= self.TOLERANCE:
|
||||||
|
# good enough, stop there
|
||||||
|
break
|
||||||
|
|
||||||
|
# let's refine the solution, binary search
|
||||||
|
for j in xrange(0, self.NUMBER_OF_ANGLES):
|
||||||
|
|
||||||
|
if abs(errors[j]) > self.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] = self._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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: all angles in calculations are in radians
|
||||||
|
def _get_angles(self, number_of_angles):
|
||||||
|
step = (2.0 * pi) / number_of_angles
|
||||||
|
return [(x * step) for x in xrange(0, number_of_angles)]
|
||||||
|
|
||||||
|
def _calculate_dest_location(self, origin, angle, radius):
|
||||||
|
origin_lat_radians = radians(origin['lat'])
|
||||||
|
origin_long_radians = radians(origin['lon'])
|
||||||
|
dest_lat_radians = asin(sin(origin_lat_radians) * cos(radius / self.EARTH_RADIUS_METERS) + cos(origin_lat_radians) * sin(radius / self.EARTH_RADIUS_METERS) * cos(angle))
|
||||||
|
dest_lng_radians = origin_long_radians + atan2(sin(angle) * sin(radius / self.EARTH_RADIUS_METERS) * cos(origin_lat_radians), cos(radius / self.EARTH_RADIUS_METERS) - sin(origin_lat_radians) * sin(dest_lat_radians))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'lon': degrees(dest_lng_radians),
|
||||||
|
'lat': degrees(dest_lat_radians)
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
class MatrixClient:
|
||||||
|
|
||||||
|
"""
|
||||||
|
A minimal client for Mapzen Time-Distance Matrix Service
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
client = MatrixClient('your_api_key')
|
||||||
|
locations = [{"lat":40.744014,"lon":-73.990508},{"lat":40.739735,"lon":-73.979713},{"lat":40.752522,"lon":-73.985015},{"lat":40.750117,"lon":-73.983704},{"lat":40.750552,"lon":-73.993519}]
|
||||||
|
costing = 'pedestrian'
|
||||||
|
|
||||||
|
print client.one_to_many(locations, costing)
|
||||||
|
"""
|
||||||
|
|
||||||
|
ONE_TO_MANY_URL = 'https://matrix.mapzen.com/one_to_many'
|
||||||
|
|
||||||
|
def __init__(self, matrix_key):
|
||||||
|
self._matrix_key = matrix_key
|
||||||
|
|
||||||
|
"""Get distances and times to a set of locations.
|
||||||
|
See https://mapzen.com/documentation/matrix/api-reference/
|
||||||
|
|
||||||
|
Args:
|
||||||
|
locations Array of {lat: y, lon: x}
|
||||||
|
costing Costing model to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict with one_to_many, units and locations
|
||||||
|
"""
|
||||||
|
def one_to_many(self, locations, costing):
|
||||||
|
request_params = {
|
||||||
|
'json': json.dumps({'locations': locations}),
|
||||||
|
'costing': costing,
|
||||||
|
'api_key': self._matrix_key
|
||||||
|
}
|
||||||
|
response = requests.get(self.ONE_TO_MANY_URL, params=request_params)
|
||||||
|
|
||||||
|
response.raise_for_status() # raise exception if not 200 OK
|
||||||
|
|
||||||
|
return response.json()
|
@ -1,3 +1,3 @@
|
|||||||
from config import GeocoderConfig, MapzenGeocoderConfig, IsolinesRoutingConfig, InternalGeocoderConfig, RoutingConfig, ConfigException, ObservatorySnapshotConfig, ObservatoryConfig
|
from config import GeocoderConfig, MapzenGeocoderConfig, IsolinesRoutingConfig, MapzenIsolinesRoutingConfig, InternalGeocoderConfig, RoutingConfig, ConfigException, ObservatorySnapshotConfig, ObservatoryConfig
|
||||||
from quota import QuotaService
|
from quota import QuotaService
|
||||||
from user import UserMetricsService
|
from user import UserMetricsService
|
||||||
|
@ -241,6 +241,40 @@ class IsolinesRoutingConfig(ServiceConfig):
|
|||||||
return self._geocoder_type == self.GOOGLE_GEOCODER
|
return self._geocoder_type == self.GOOGLE_GEOCODER
|
||||||
|
|
||||||
|
|
||||||
|
class MapzenIsolinesRoutingConfig(ServiceConfig):
|
||||||
|
|
||||||
|
PERIOD_END_DATE = 'period_end_date'
|
||||||
|
|
||||||
|
def __init__(self, redis_connection, db_conn, username, orgname=None):
|
||||||
|
super(MapzenIsolinesRoutingConfig, self).__init__(redis_connection, db_conn,
|
||||||
|
username, orgname)
|
||||||
|
try:
|
||||||
|
self._mapzen_matrix_api_key = self._db_config.mapzen_matrix_api_key
|
||||||
|
self._isolines_quota = self._db_config.mapzen_matrix_monthly_quota
|
||||||
|
self._period_end_date = date_parse(self._redis_config[self.PERIOD_END_DATE])
|
||||||
|
except Exception as e:
|
||||||
|
raise ConfigException("Malformed config for Mapzen isolines: {0}".format(e))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_type(self):
|
||||||
|
return 'mapzen_isolines'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def isolines_quota(self):
|
||||||
|
return self._isolines_quota
|
||||||
|
|
||||||
|
@property
|
||||||
|
def soft_isolines_limit(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def period_end_date(self):
|
||||||
|
return self._period_end_date
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapzen_matrix_api_key(self):
|
||||||
|
return self._mapzen_matrix_api_key
|
||||||
|
|
||||||
class InternalGeocoderConfig(ServiceConfig):
|
class InternalGeocoderConfig(ServiceConfig):
|
||||||
|
|
||||||
def __init__(self, redis_connection, db_conn, username, orgname=None):
|
def __init__(self, redis_connection, db_conn, username, orgname=None):
|
||||||
@ -438,6 +472,8 @@ class ServicesDBConfig:
|
|||||||
raise ConfigException('Mapzen configuration missing')
|
raise ConfigException('Mapzen configuration missing')
|
||||||
else:
|
else:
|
||||||
mapzen_conf = json.loads(mapzen_conf_json)
|
mapzen_conf = json.loads(mapzen_conf_json)
|
||||||
|
self._mapzen_matrix_api_key = mapzen_conf['matrix']['api_key']
|
||||||
|
self._mapzen_matrix_quota = mapzen_conf['matrix']['monthly_quota']
|
||||||
self._mapzen_routing_api_key = mapzen_conf['routing']['api_key']
|
self._mapzen_routing_api_key = mapzen_conf['routing']['api_key']
|
||||||
self._mapzen_routing_quota = mapzen_conf['routing']['monthly_quota']
|
self._mapzen_routing_quota = mapzen_conf['routing']['monthly_quota']
|
||||||
self._mapzen_geocoder_api_key = mapzen_conf['geocoder']['api_key']
|
self._mapzen_geocoder_api_key = mapzen_conf['geocoder']['api_key']
|
||||||
@ -492,6 +528,14 @@ class ServicesDBConfig:
|
|||||||
def heremaps_geocoder_cost_per_hit(self):
|
def heremaps_geocoder_cost_per_hit(self):
|
||||||
return self._heremaps_geocoder_cost_per_hit
|
return self._heremaps_geocoder_cost_per_hit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapzen_matrix_api_key(self):
|
||||||
|
return self._mapzen_matrix_api_key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mapzen_matrix_monthly_quota(self):
|
||||||
|
return self._mapzen_matrix_quota
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mapzen_routing_api_key(self):
|
def mapzen_routing_api_key(self):
|
||||||
return self._mapzen_routing_api_key
|
return self._mapzen_routing_api_key
|
||||||
|
@ -70,6 +70,9 @@ class QuotaChecker:
|
|||||||
elif re.match('here_isolines',
|
elif re.match('here_isolines',
|
||||||
self._user_service_config.service_type) is not None:
|
self._user_service_config.service_type) is not None:
|
||||||
return self.__check_isolines_quota()
|
return self.__check_isolines_quota()
|
||||||
|
elif re.match('mapzen_isolines',
|
||||||
|
self._user_service_config.service_type) is not None:
|
||||||
|
return self.__check_isolines_quota()
|
||||||
elif re.match('routing_mapzen',
|
elif re.match('routing_mapzen',
|
||||||
self._user_service_config.service_type) is not None:
|
self._user_service_config.service_type) is not None:
|
||||||
return self.__check_routing_quota()
|
return self.__check_routing_quota()
|
||||||
|
0
server/lib/python/cartodb_services/test/__init__.py
Normal file
0
server/lib/python/cartodb_services/test/__init__.py
Normal file
@ -65,7 +65,7 @@ def _plpy_execute_side_effect(*args, **kwargs):
|
|||||||
if args[0] == "SELECT cartodb.CDB_Conf_GetConf('heremaps_conf') as conf":
|
if args[0] == "SELECT cartodb.CDB_Conf_GetConf('heremaps_conf') as conf":
|
||||||
return [{'conf': '{"geocoder": {"app_id": "app_id", "app_code": "code", "geocoder_cost_per_hit": 1}, "isolines": {"app_id": "app_id", "app_code": "code"}}'}]
|
return [{'conf': '{"geocoder": {"app_id": "app_id", "app_code": "code", "geocoder_cost_per_hit": 1}, "isolines": {"app_id": "app_id", "app_code": "code"}}'}]
|
||||||
elif args[0] == "SELECT cartodb.CDB_Conf_GetConf('mapzen_conf') as conf":
|
elif args[0] == "SELECT cartodb.CDB_Conf_GetConf('mapzen_conf') as conf":
|
||||||
return [{'conf': '{"routing": {"api_key": "api_key_rou", "monthly_quota": 1500000}, "geocoder": {"api_key": "api_key_geo", "monthly_quota": 1500000}}'}]
|
return [{'conf': '{"routing": {"api_key": "api_key_rou", "monthly_quota": 1500000}, "geocoder": {"api_key": "api_key_geo", "monthly_quota": 1500000}, "matrix": {"api_key": "api_key_mat", "monthly_quota": 1500000}}'}]
|
||||||
elif args[0] == "SELECT cartodb.CDB_Conf_GetConf('logger_conf') as conf":
|
elif args[0] == "SELECT cartodb.CDB_Conf_GetConf('logger_conf') as conf":
|
||||||
return [{'conf': '{"geocoder_log_path": "/dev/null"}'}]
|
return [{'conf': '{"geocoder_log_path": "/dev/null"}'}]
|
||||||
elif args[0] == "SELECT cartodb.CDB_Conf_GetConf('data_observatory_conf') as conf":
|
elif args[0] == "SELECT cartodb.CDB_Conf_GetConf('data_observatory_conf') as conf":
|
||||||
|
@ -11,7 +11,7 @@ requests_mock.Mocker.TEST_PREFIX = 'test_'
|
|||||||
|
|
||||||
|
|
||||||
@requests_mock.Mocker()
|
@requests_mock.Mocker()
|
||||||
class GoogleGeocoderTestCase(unittest.TestCase):
|
class MapzenGeocoderTestCase(unittest.TestCase):
|
||||||
MAPZEN_GEOCODER_URL = 'https://search.mapzen.com/v1/search'
|
MAPZEN_GEOCODER_URL = 'https://search.mapzen.com/v1/search'
|
||||||
|
|
||||||
EMPTY_RESPONSE = """{
|
EMPTY_RESPONSE = """{
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import unittest
|
||||||
|
from cartodb_services.mapzen import MapzenIsolines
|
||||||
|
from math import radians, cos, sin, asin, sqrt
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file is basically a sanity test on the algorithm.
|
||||||
|
|
||||||
|
|
||||||
|
It uses a mocked client, which returns the cost based on a very simple model:
|
||||||
|
just proportional to the distance from origin to the target point.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MatrixClientMock():
|
||||||
|
|
||||||
|
def __init__(self, speed):
|
||||||
|
"""
|
||||||
|
Sets up the mock with a speed in km/h
|
||||||
|
"""
|
||||||
|
self._speed = speed
|
||||||
|
|
||||||
|
def one_to_many(self, locations, costing):
|
||||||
|
origin = locations[0]
|
||||||
|
distances = [self._distance(origin, l) for l in locations]
|
||||||
|
response = {
|
||||||
|
'one_to_many': [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'distance': distances[i] * self._speed,
|
||||||
|
'time': distances[i] / self._speed * 3600,
|
||||||
|
'to_index': i,
|
||||||
|
'from_index': 0
|
||||||
|
}
|
||||||
|
for i in xrange(0, len(distances))
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'units': 'km',
|
||||||
|
'locations': [
|
||||||
|
locations
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _distance(self, a, b):
|
||||||
|
"""
|
||||||
|
Calculate the great circle distance between two points
|
||||||
|
on the earth (specified in decimal degrees)
|
||||||
|
http://stackoverflow.com/questions/4913349/haversine-formula-in-python-bearing-and-distance-between-two-gps-points
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
distance in meters
|
||||||
|
"""
|
||||||
|
|
||||||
|
# convert decimal degrees to radians
|
||||||
|
lon1, lat1, lon2, lat2 = map(radians, [a['lon'], a['lat'], b['lon'], b['lat']])
|
||||||
|
|
||||||
|
# haversine formula
|
||||||
|
dlon = lon2 - lon1
|
||||||
|
dlat = lat2 - lat1
|
||||||
|
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
||||||
|
c = 2 * asin(sqrt(a))
|
||||||
|
r = 6371 # Radius of earth in kilometers. Use 3956 for miles
|
||||||
|
return c * r
|
||||||
|
|
||||||
|
|
||||||
|
class MapzenIsolinesTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
speed = 4 # in km/h
|
||||||
|
matrix_client = MatrixClientMock(speed)
|
||||||
|
self.mapzen_isolines = MapzenIsolines(matrix_client)
|
||||||
|
|
||||||
|
def test_calculate_isochrone(self):
|
||||||
|
origin = {"lat":40.744014,"lon":-73.990508}
|
||||||
|
transport_mode = 'walk'
|
||||||
|
isorange = 10 * 60 # 10 minutes
|
||||||
|
solution = self.mapzen_isolines.calculate_isochrone(origin, transport_mode, isorange)
|
Loading…
Reference in New Issue
Block a user