Skip to content

Commit fda749c

Browse files
committed
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 Conflicts: lib/scenario-editor/utils/valhalla.js
1 parent 9409ed4 commit fda749c

File tree

2 files changed

+154
-15
lines changed

2 files changed

+154
-15
lines changed

configurations/default/env.yml.tmp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ MAPBOX_ACCESS_TOKEN: test-access-token
55
MAPBOX_MAP_ID: mapbox.streets
66
MAPBOX_ATTRIBUTION: <a href="https://www.mapbox.com/about/maps/" target="_blank">&copy; Mapbox &copy; OpenStreetMap</a> <a href="https://www.mapbox.com/map-feedback/" target="_blank">Improve this map</a>
77
# R5_URL: http://localhost:8080
8+
GRAPH_HOPPER_KEY: graph-hopper-routing-key

lib/scenario-editor/utils/valhalla.js

Lines changed: 153 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fetch from 'isomorphic-fetch'
44
import {decode as decodePolyline} from 'polyline'
55
import {isEqual as coordinatesAreEqual} from '@conveyal/lonlat'
66
import lineString from 'turf-linestring'
7+
import lineSliceAlong from '@turf/line-slice-along'
78

89
import type {
910
Coordinates,
@@ -77,7 +78,81 @@ type ValhallaResponse = {
7778
}
7879
}
7980

80-
export function route (points: Array<LatLng>): ?Promise<ValhallaResponse> {
81+
/**
82+
* Convert GraphHopper routing JSON response to polyline.
83+
*/
84+
function handleGraphHopperRouting (json, individualLegs = false) {
85+
if (json && json.paths && json.paths[0]) {
86+
const decodedPolyline = decodePolyline(json.paths[0].points)
87+
.map(coordinate => ([coordinate[1], coordinate[0]]))
88+
// console.log('decoded polyline', json.paths[0].points, decodedPolyline)
89+
if (individualLegs) {
90+
// Reconstruct individual legs from the instructions. NOTE: we do not simply
91+
// use the waypoints found in the response because for lines that share
92+
// street segments, slicing on these points results in unpredictable splits.
93+
// Slicing the line along distances is much more reliable.
94+
const segments = []
95+
const waypointDistances = [0]
96+
let distance = 0
97+
json.paths[0].instructions.forEach(instruction => {
98+
// Iterate over the instructions, accumulating distance and storing the
99+
// distance at each waypoint encountered.
100+
if (instruction.text.match(/Waypoint (\d+)/)) {
101+
// console.log(`adding waypoint ${waypointDistances.length} at ${distance} meters`)
102+
waypointDistances.push(distance)
103+
} else {
104+
distance += instruction.distance
105+
}
106+
})
107+
// Add last distance measure.
108+
// FIXME: Should this just be the length of the entire line?
109+
// console.log(waypointDistances, json.paths[0].distance)
110+
waypointDistances.push(distance)
111+
const decodedLineString = lineString(decodedPolyline)
112+
if (waypointDistances.length > 2) {
113+
for (var i = 1; i < waypointDistances.length; i++) {
114+
const slicedSegment = lineSliceAlong(
115+
decodedLineString,
116+
waypointDistances[i - 1] / 1000,
117+
waypointDistances[i] / 1000
118+
)
119+
segments.push(slicedSegment.geometry.coordinates)
120+
}
121+
// console.log('individual legs', segments)
122+
return segments
123+
} else {
124+
// FIXME does this work for two input points?
125+
return [decodedPolyline]
126+
}
127+
} else {
128+
return decodedPolyline
129+
}
130+
} else {
131+
return null
132+
}
133+
}
134+
135+
/**
136+
* Convert Mapzen routing JSON response to polyline.
137+
*/
138+
export function handleMapzenRouting (json, individualLegs = false) {
139+
if (json && json.trip) {
140+
const legArray = json.trip.legs.map((leg, index) => {
141+
return decodePolyline(leg.shape)
142+
.map((c, index) => [c[1] / 10, c[0] / 10]) // Mapzen or Mapbox is encoding/decoding wrong?
143+
})
144+
return individualLegs
145+
? legArray
146+
: [].concat.apply([], legArray)
147+
} else {
148+
return null
149+
}
150+
}
151+
152+
/**
153+
* Call Mapzen routing service with set of lat/lng coordinates.
154+
*/
155+
export function routeWithMapzen (points: Array<LatLng>): ?Promise<ValhallaResponse> {
81156
if (points.length < 2) {
82157
console.warn('need at least two points to route with mapzen', points)
83158
return null
@@ -98,26 +173,28 @@ export function route (points: Array<LatLng>): ?Promise<ValhallaResponse> {
98173
).then(res => res.json())
99174
}
100175

176+
/**
177+
* Route between two or more points using external routing service.
178+
* @param {[type]} points array of two or more LatLng points
179+
* @param {[type]} individualLegs whether to return coordinates as set of
180+
* distinct segments for each pair of points
181+
* @param {[type]} useMapzen FIXME: not implemented. boolean to select service to use.
182+
* @return {[type]} Array of coordinates or Array of arrays of coordinates.
183+
*/
101184
export async function polyline (
102-
points: Array<LatLng>
185+
points: Array<LatLng>,
186+
individualLegs: boolean = false,
187+
useMapzen: boolean = true
103188
): Promise<?Array<[number, number]>> {
104189
let json
105190
try {
106-
json = await route(points)
191+
json = await routeWithGraphHopper(points)
107192
} catch (e) {
108193
console.log(e)
109194
return null
110195
}
111-
112-
if (json && json.trip) {
113-
const legArray = json.trip.legs.map((leg, index) => {
114-
return decodePolyline(leg.shape)
115-
.map((c, index) => [c[1] / 10, c[0] / 10]) // Mapzen or Mapbox is encoding/decoding wrong?
116-
})
117-
return [].concat.apply([], legArray)
118-
} else {
119-
return null
120-
}
196+
const geometry = handleGraphHopperRouting(json, individualLegs)
197+
return geometry
121198
}
122199

123200
export async function getSegment (
@@ -128,19 +205,24 @@ export async function getSegment (
128205
type: 'LineString',
129206
coordinates: Coordinates
130207
}> {
208+
// Store geometry to be returned here.
131209
let geometry
132210
if (followRoad) {
133-
// if followRoad
211+
// if snapping to streets, use routing service.
134212
const coordinates = await polyline(
135213
points.map(p => ({lng: p[0], lat: p[1]}))
136-
) // [{lng: from[0], lat: from[1]}, {lng: to[0], lat: to[1]}])
214+
)
137215
if (!coordinates) {
216+
// If routing was unsuccessful, default to straight line (if desired by
217+
// caller).
218+
console.warn(`Routing unsuccessful. Returning ${defaultToStraightLine ? 'straight line' : 'null'}.`)
138219
if (defaultToStraightLine) {
139220
geometry = lineString(points).geometry
140221
} else {
141222
return null
142223
}
143224
} else {
225+
// If routing is successful, clean up shape if necessary
144226
const c0 = coordinates[0]
145227
const epsilon = 1e-6
146228
if (!coordinatesAreEqual(c0, points[0], epsilon)) {
@@ -152,7 +234,63 @@ export async function getSegment (
152234
}
153235
}
154236
} else {
237+
// If not snapping to streets, simply generate a line string from input
238+
// coordinates.
155239
geometry = lineString(points).geometry
156240
}
157241
return geometry
158242
}
243+
244+
/**
245+
* Call GraphHopper routing service with lat/lng coordinates.
246+
*/
247+
export function routeWithGraphHopper (points: Array<LatLng>): ?Promise<any> {
248+
// https://graphhopper.com/api/1/route?point=49.932707,11.588051&point=50.3404,11.64705&vehicle=car&debug=true&&type=json
249+
if (points.length < 2) {
250+
console.warn('need at least two points to route with graphhopper', points)
251+
return null
252+
}
253+
if (!process.env.GRAPH_HOPPER_KEY) {
254+
throw new Error('GRAPH_HOPPER_KEY not set')
255+
}
256+
const GRAPH_HOPPER_KEY: string = process.env.GRAPH_HOPPER_KEY
257+
const locations = points.map(p => (`point=${p.lat},${p.lng}`)).join('&')
258+
return fetch(
259+
`https://graphhopper.com/api/1/route?${locations}&key=${GRAPH_HOPPER_KEY}&vehicle=car&debug=true&&type=json`
260+
).then(res => res.json())
261+
}
262+
263+
/**
264+
* Call Mapbox routing service with set of lat/lng coordinates.
265+
*/
266+
export function routeWithMapbox (points: Array<LatLng>): ?Promise<any> {
267+
if (points.length < 2) {
268+
console.warn('need at least two points to route with mapbox', points)
269+
return null
270+
}
271+
if (!process.env.MAPBOX_ACCESS_TOKEN) {
272+
throw new Error('MAPBOX_ACCESS_TOKEN not set')
273+
}
274+
const MAPBOX_ACCESS_TOKEN: string = process.env.MAPBOX_ACCESS_TOKEN
275+
// const locations = points.map(p => ({lon: p.lng, lat: p.lat}))
276+
const locations = points.map(p => (`${p.lng},${p.lat}`)).join(';')
277+
return fetch(
278+
`https://api.mapbox.com/directions/v5/mapbox/driving/${locations}.json?access_token=${MAPBOX_ACCESS_TOKEN}`
279+
).then(res => res.json())
280+
}
281+
282+
// /**
283+
// * Convert Mapbox routing JSON response to polyline.
284+
// */
285+
// function handleMapboxRouting (json) {
286+
// if (json && json.routes && json.routes[0]) {
287+
// // Return decoded polyline on route geometry (by default Mapbox returns a
288+
// // single route with entire geometry contained therein).
289+
// // return json.routes[0].geometry.coordinates
290+
// const decodedPolyline = decodePolyline(json.routes[0].geometry)
291+
// console.log('decoded polyline', json.routes[0].geometry, decodedPolyline)
292+
// return decodedPolyline.map((c, index) => ([c[1], c[0]])) // index === 0 ? c :
293+
// } else {
294+
// return null
295+
// }
296+
// }

0 commit comments

Comments
 (0)