From b570069f1e5723ab04e66c4d69950b1db5bf4718 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 29 Jan 2018 09:56:48 -0500 Subject: [PATCH] fix(routing): Use GraphHopper for follow streets routing Mapzen (and thus the valhalla routing service) is no longer in operation. This addresses #60, but needs to be cherry-picked into a separate branch for merging into dev. refs #60 --- configurations/default/env.yml.tmp | 1 + lib/scenario-editor/utils/valhalla.js | 89 +++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/configurations/default/env.yml.tmp b/configurations/default/env.yml.tmp index 0e78866bb..e2f6b16b8 100644 --- a/configurations/default/env.yml.tmp +++ b/configurations/default/env.yml.tmp @@ -5,3 +5,4 @@ MAPBOX_ACCESS_TOKEN: test-access-token MAPBOX_MAP_ID: mapbox.streets MAPBOX_ATTRIBUTION: © Mapbox © OpenStreetMap Improve this map # R5_URL: http://localhost:8080 +GRAPH_HOPPER_KEY: graph-hopper-routing-key diff --git a/lib/scenario-editor/utils/valhalla.js b/lib/scenario-editor/utils/valhalla.js index 1facd1ffd..3c3f14fa4 100644 --- a/lib/scenario-editor/utils/valhalla.js +++ b/lib/scenario-editor/utils/valhalla.js @@ -4,6 +4,7 @@ import fetch from 'isomorphic-fetch' import {decode as decodePolyline} from 'polyline' import {isEqual as coordinatesAreEqual} from '@conveyal/lonlat' import lineString from 'turf-linestring' +import lineSliceAlong from '@turf/line-slice-along' import type { Coordinates, @@ -77,10 +78,64 @@ type ValhallaResponse = { } } +/** + * Convert GraphHopper routing JSON response to polyline. + */ +function handleGraphHopperRouting (json, individualLegs = false) { + if (json && json.paths && json.paths[0]) { + const decodedPolyline = decodePolyline(json.paths[0].points) + .map(coordinate => ([coordinate[1], coordinate[0]])) + // console.log('decoded polyline', json.paths[0].points, decodedPolyline) + if (individualLegs) { + // Reconstruct individual legs from the instructions. NOTE: we do not simply + // use the waypoints found in the response because for lines that share + // street segments, slicing on these points results in unpredictable splits. + // Slicing the line along distances is much more reliable. + const segments = [] + const waypointDistances = [0] + let distance = 0 + json.paths[0].instructions.forEach(instruction => { + // Iterate over the instructions, accumulating distance and storing the + // distance at each waypoint encountered. + if (instruction.text.match(/Waypoint (\d+)/)) { + // console.log(`adding waypoint ${waypointDistances.length} at ${distance} meters`) + waypointDistances.push(distance) + } else { + distance += instruction.distance + } + }) + // Add last distance measure. + // FIXME: Should this just be the length of the entire line? + // console.log(waypointDistances, json.paths[0].distance) + waypointDistances.push(distance) + const decodedLineString = lineString(decodedPolyline) + if (waypointDistances.length > 2) { + for (var i = 1; i < waypointDistances.length; i++) { + const slicedSegment = lineSliceAlong( + decodedLineString, + waypointDistances[i - 1] / 1000, + waypointDistances[i] / 1000 + ) + segments.push(slicedSegment.geometry.coordinates) + } + // console.log('individual legs', segments) + return segments + } else { + // FIXME does this work for two input points? + return [decodedPolyline] + } + } else { + return decodedPolyline + } + } else { + return null + } +} + /** * Convert Mapzen routing JSON response to polyline. */ -function handleMapzenRouting (json, individualLegs = false) { +export function handleMapzenRouting (json, individualLegs = false) { if (json && json.trip) { const legArray = json.trip.legs.map((leg, index) => { return decodePolyline(leg.shape) @@ -118,6 +173,14 @@ export function routeWithMapzen (points: Array): ?Promise res.json()) } +/** + * Route between two or more points using external routing service. + * @param {[type]} points array of two or more LatLng points + * @param {[type]} individualLegs whether to return coordinates as set of + * distinct segments for each pair of points + * @param {[type]} useMapzen FIXME: not implemented. boolean to select service to use. + * @return {[type]} Array of coordinates or Array of arrays of coordinates. + */ export async function polyline ( points: Array, individualLegs: boolean = false, @@ -125,13 +188,12 @@ export async function polyline ( ): Promise> { let json try { - json = await routeWithMapzen(points) + json = await routeWithGraphHopper(points) } catch (e) { console.log(e) return null } - const geometry = handleMapzenRouting(json, individualLegs) - // console.log(geometry) + const geometry = handleGraphHopperRouting(json, individualLegs) return geometry } @@ -179,6 +241,25 @@ export async function getSegment ( return geometry } +/** + * Call GraphHopper routing service with lat/lng coordinates. + */ +export function routeWithGraphHopper (points: Array): ?Promise { + // https://graphhopper.com/api/1/route?point=49.932707,11.588051&point=50.3404,11.64705&vehicle=car&debug=true&&type=json + if (points.length < 2) { + console.warn('need at least two points to route with graphhopper', points) + return null + } + if (!process.env.GRAPH_HOPPER_KEY) { + throw new Error('GRAPH_HOPPER_KEY not set') + } + const GRAPH_HOPPER_KEY: string = process.env.GRAPH_HOPPER_KEY + const locations = points.map(p => (`point=${p.lat},${p.lng}`)).join('&') + return fetch( + `https://graphhopper.com/api/1/route?${locations}&key=${GRAPH_HOPPER_KEY}&vehicle=car&debug=true&&type=json` + ).then(res => res.json()) +} + /** * Call Mapbox routing service with set of lat/lng coordinates. */