@@ -4,6 +4,7 @@ import fetch from 'isomorphic-fetch'
44import { decode as decodePolyline } from 'polyline'
55import { isEqual as coordinatesAreEqual } from '@conveyal/lonlat'
66import lineString from 'turf-linestring'
7+ import lineSliceAlong from '@turf/line-slice-along'
78
89import 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 ( / W a y p o i n t ( \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+ */
101184export 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
123200export 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