@wemap/routers 12.8.9 → 12.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,445 @@
1
+ import { Coordinates, Utils as GeoUtils } from '@wemap/geo';
2
+ import Logger from '@wemap/logger';
3
+
4
+ import RemoteRouter from '../RemoteRouter.js';
5
+ import Itinerary from '../../model/Itinerary.js';
6
+ import Leg, { TransportInfo } from '../../model/Leg.js';
7
+ import { dateWithTimeZone } from '../RemoteRouterUtils.js';
8
+ import { TransitMode, areTransitAndTravelModeConsistent } from '../../model/TransitMode.js';
9
+ import StepsBuilder from '../../model/StepsBuilder.js';
10
+ import { type MinStepInfo } from '../../model/Step.js';
11
+ import { type RouterRequest } from '../../model/RouterRequest.js';
12
+ import { RemoteRoutingError } from '../../RoutingError.js';
13
+ import { NavitiaJson, NavitiaCoordinates, NavitiaSection, NavitiaIntermediateStep } from './types';
14
+
15
+ /**
16
+ * List of all modes supported by the API
17
+ * http://doc.navitia.io/#physical-mode
18
+ */
19
+
20
+ const transitModeCorrespondance = new Map<string, TransitMode>();
21
+ transitModeCorrespondance.set('Air', 'AIRPLANE');
22
+ transitModeCorrespondance.set('Boat', 'BOAT');
23
+ transitModeCorrespondance.set('Bus', 'BUS');
24
+ transitModeCorrespondance.set('BusRapidTransit', 'BUS');
25
+ transitModeCorrespondance.set('Coach', 'BUS');
26
+ transitModeCorrespondance.set('Ferry', 'FERRY');
27
+ transitModeCorrespondance.set('Funicular', 'FUNICULAR');
28
+ transitModeCorrespondance.set('LocalTrain', 'TRAIN');
29
+ transitModeCorrespondance.set('LongDistanceTrain', 'TRAIN');
30
+ transitModeCorrespondance.set('Metro', 'METRO');
31
+ transitModeCorrespondance.set('Métro', 'METRO');
32
+ transitModeCorrespondance.set('RailShuttle', 'TRAIN');
33
+ transitModeCorrespondance.set('RapidTransit', 'BUS');
34
+ transitModeCorrespondance.set('Shuttle', 'BUS');
35
+ transitModeCorrespondance.set('SuspendedCableCar', 'FUNICULAR');
36
+ transitModeCorrespondance.set('Taxi', 'TAXI');
37
+ transitModeCorrespondance.set('Train', 'TRAIN');
38
+ transitModeCorrespondance.set('RER', 'TRAIN');
39
+ transitModeCorrespondance.set('Tramway', 'TRAM');
40
+ transitModeCorrespondance.set('walking', 'WALK');
41
+ transitModeCorrespondance.set('bike', 'BIKE');
42
+
43
+ /**
44
+ * List of transports modes
45
+ */
46
+ const TRANSPORT_IDS = [
47
+ 'physical_mode:Air',
48
+ 'physical_mode:Boat',
49
+ 'physical_mode:Bus',
50
+ 'physical_mode:BusRapidTransit',
51
+ 'physical_mode:Coach',
52
+ 'physical_mode:Ferry',
53
+ 'physical_mode:Funicular',
54
+ 'physical_mode:LocalTrain',
55
+ 'physical_mode:LongDistanceTrain',
56
+ 'physical_mode:Metro',
57
+ 'physical_mode:RailShuttle',
58
+ 'physical_mode:RapidTransit',
59
+ 'physical_mode:Shuttle',
60
+ 'physical_mode:SuspendedCableCar',
61
+ 'physical_mode:Taxi',
62
+ 'physical_mode:Train',
63
+ 'physical_mode:Tramway'
64
+ ];
65
+
66
+ function jsonToCoordinates(json: NavitiaCoordinates) {
67
+ return new Coordinates(Number(json.lat), Number(json.lon));
68
+ }
69
+
70
+ function last<T>(array: T[]) {
71
+ return array[array.length - 1];
72
+ }
73
+
74
+ /**
75
+ * stringDate (e.g. 20211117T104516)
76
+ */
77
+ function dateStringToTimestamp(stringDate: string, timeZone: string) {
78
+ const yearStr = stringDate.substr(0, 4);
79
+ const monthStr = stringDate.substr(4, 2);
80
+ const dayStr = stringDate.substr(6, 2);
81
+ const hoursStr = stringDate.substr(9, 2);
82
+ const minutesStr = stringDate.substr(11, 2);
83
+ const secondsStr = stringDate.substr(13, 2);
84
+
85
+ return dateWithTimeZone(
86
+ Number(yearStr),
87
+ Number(monthStr) - 1,
88
+ Number(dayStr),
89
+ Number(hoursStr),
90
+ Number(minutesStr),
91
+ Number(secondsStr),
92
+ timeZone
93
+ ).getTime();
94
+ }
95
+
96
+ // The api key should be defined in the routingurl as api_key=XXXXXXXX
97
+
98
+ /**
99
+ * Singleton.
100
+ */
101
+ class NavitiaRemoteRouter extends RemoteRouter {
102
+
103
+ get rname() { return 'navitia' as const; }
104
+
105
+ async getItineraries(endpointUrl: string, routerRequest: RouterRequest) {
106
+ const url = this.getURL(endpointUrl, routerRequest);
107
+
108
+ // The api key should be defined in the routingurl as api_key=XXXXXXXX
109
+ const api_key = url.searchParams.get('api_key');
110
+ if (!api_key) {
111
+ throw RemoteRoutingError.missingApiKey(this.rname);
112
+ }
113
+
114
+ const res = await (fetch(url, {
115
+ method: 'GET',
116
+ headers: {
117
+ 'Authorization': api_key,
118
+ }
119
+ }).catch(() => {
120
+ throw RemoteRoutingError.unreachableServer(this.rname, url.toString());
121
+ }));
122
+
123
+
124
+ const jsonResponse: NavitiaJson = await res.json().catch(() => {
125
+ throw RemoteRoutingError.responseNotParsing(this.rname, url.toString());
126
+ });
127
+
128
+ // When Navitia failed to calculate an itinerary (ie. start or end
129
+ // point is far from network), it respond a 404 with an error message
130
+ // or a 200 with an error message
131
+ if (jsonResponse && jsonResponse.error) {
132
+ throw RemoteRoutingError.notFound(this.rname, jsonResponse.error.message);
133
+ }
134
+
135
+ const itineraries = this.parseResponse(jsonResponse);
136
+
137
+ const sameModeFound = itineraries.some((itinerary) => areTransitAndTravelModeConsistent(itinerary.transitMode, routerRequest.travelMode));
138
+
139
+ if (!sameModeFound) {
140
+ throw RemoteRoutingError.notFound(
141
+ this.rname,
142
+ 'Selected mode of transport was not found for this itinerary.'
143
+ )
144
+ }
145
+
146
+ return itineraries;
147
+ }
148
+
149
+ getURL(endpointUrl: string, routerRequest: RouterRequest) {
150
+ const { origin, destination, waypoints, travelMode } = routerRequest;
151
+
152
+ if ((waypoints || []).length > 0) {
153
+ Logger.warn(`${this.rname} router uses only the first 2 waypoints (asked ${waypoints?.length})`);
154
+ }
155
+
156
+ const url = new URL(endpointUrl);
157
+
158
+ const coreParams = new URLSearchParams();
159
+ coreParams.set('from', `${origin.longitude};${origin.latitude}`);
160
+ coreParams.set('to', `${destination.longitude};${destination.latitude}`);
161
+ coreParams.set('data_freshness', 'realtime');
162
+
163
+ if (routerRequest.itineraryModifiers?.avoidStairs) {
164
+ coreParams.set('wheelchair', 'true')
165
+ }
166
+
167
+ let queryParams: URLSearchParams = new URLSearchParams();
168
+ switch (travelMode) {
169
+ case 'WALK':
170
+ queryParams = this.getWalkingQuery();
171
+ break;
172
+ case 'BIKE':
173
+ queryParams = this.getBikeQuery();
174
+ break;
175
+ case 'CAR':
176
+ queryParams = this.getCarQuery();
177
+ break;
178
+ default:
179
+ break;
180
+ }
181
+
182
+ [coreParams, queryParams].map(params => {
183
+ for (const pair of params.entries()) {
184
+ url.searchParams.append(pair[0], pair[1]);
185
+ }
186
+ });
187
+
188
+ return url;
189
+ }
190
+
191
+ getCarQuery() {
192
+ const urlSearchParams = new URLSearchParams();
193
+
194
+ TRANSPORT_IDS.forEach((id) => {
195
+ urlSearchParams.append('forbidden_uris[]', id);
196
+ });
197
+
198
+ urlSearchParams.append('first_section_mode[]', 'walking');
199
+ urlSearchParams.append('first_section_mode[]', 'car');
200
+ urlSearchParams.append('last_section_mode[]', 'walking');
201
+ urlSearchParams.append('last_section_mode[]', 'car');
202
+
203
+ return urlSearchParams;
204
+ }
205
+
206
+ getWalkingQuery() {
207
+ const urlSearchParams = new URLSearchParams();
208
+
209
+ TRANSPORT_IDS.forEach((id) => {
210
+ urlSearchParams.append('forbidden_uris[]', id);
211
+ });
212
+
213
+ urlSearchParams.append('first_section_mode[]', 'walking');
214
+ urlSearchParams.append('last_section_mode[]', 'walking');
215
+
216
+ return urlSearchParams;
217
+ }
218
+
219
+ getBikeQuery() {
220
+ const urlSearchParams = new URLSearchParams();
221
+
222
+ TRANSPORT_IDS.forEach((id) => {
223
+ urlSearchParams.append('forbidden_uris[]', id);
224
+ });
225
+
226
+ urlSearchParams.append('first_section_mode[]', 'bike');
227
+ urlSearchParams.append('last_section_mode[]', 'bike');
228
+
229
+ return urlSearchParams;
230
+ }
231
+
232
+
233
+ getSectionCoords(section: NavitiaSection) {
234
+ let from: NavitiaCoordinates | undefined;
235
+ let to: NavitiaCoordinates | undefined;
236
+
237
+ if ('stop_point' in section.from) {
238
+ from = section.from.stop_point.coord;
239
+ } else if ('address' in section.from) {
240
+ from = section.from.address.coord;
241
+ } else {
242
+ from = section.from.poi.coord;
243
+ }
244
+
245
+ if ('stop_point' in section.to) {
246
+ to = section.to.stop_point.coord;
247
+ } else if ('address' in section.to) {
248
+ to = section.to.address.coord;
249
+ } else {
250
+ to = section.to.poi.coord;
251
+ }
252
+
253
+ return {
254
+ from: jsonToCoordinates(from),
255
+ to: jsonToCoordinates(to)
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Since the navitia API does not provide coords for each step, we need to compute them
261
+ * We trim the coordinates of the leg with the distance of each step and keep the last result as the coords of the step
262
+ * @param {Leg} leg
263
+ */
264
+ findStepsCoord(legCoords: Coordinates[], steps: NavitiaIntermediateStep[]) {
265
+ const coords = legCoords;
266
+
267
+ const duplicatedCoords = [...coords];
268
+ let previousStep = steps[0];
269
+ let accumulatedIndex = 0;
270
+ const outputSteps = [];
271
+
272
+ for (const [idx, step] of steps.entries()) {
273
+ let newCoords: Coordinates;
274
+
275
+ let _idCoordsInLeg;
276
+
277
+ if (idx === 0) {
278
+ _idCoordsInLeg = 0;
279
+ newCoords = coords[0];
280
+ } else if (idx === steps.length - 1) {
281
+ _idCoordsInLeg = coords.length - 1;
282
+ newCoords = last(coords);
283
+ } else if (duplicatedCoords.length === 1) {
284
+ accumulatedIndex++;
285
+
286
+ _idCoordsInLeg = accumulatedIndex;
287
+
288
+ newCoords = duplicatedCoords[0];
289
+
290
+ coords[_idCoordsInLeg] = newCoords;
291
+ } else {
292
+ const result = GeoUtils.trimRoute(duplicatedCoords, duplicatedCoords[0], previousStep.distance);
293
+ accumulatedIndex += result.length - 1;
294
+
295
+ duplicatedCoords.splice(0, result.length - 1);
296
+
297
+ _idCoordsInLeg = accumulatedIndex;
298
+
299
+ newCoords = last(result);
300
+
301
+ coords[_idCoordsInLeg] = newCoords;
302
+ }
303
+
304
+ outputSteps.push({
305
+ ...step,
306
+ coords: newCoords
307
+ });
308
+
309
+ previousStep = step;
310
+ }
311
+
312
+ return outputSteps;
313
+ }
314
+
315
+ findStepCoords(step: NavitiaSection['path'][number], section: NavitiaSection) {
316
+ if ('instruction_start_coordinate' in step) {
317
+ return jsonToCoordinates(step.instruction_start_coordinate);
318
+ }
319
+
320
+ const via = section.vias?.find(via => via.id === step.via_uri);
321
+ if (via) {
322
+ return jsonToCoordinates(via.access_point.coord);
323
+ }
324
+ }
325
+
326
+ parseResponse(json: NavitiaJson) {
327
+
328
+ if (!json || !json.journeys) {
329
+ throw RemoteRoutingError.notFound(this.rname, json.error?.message);
330
+ }
331
+
332
+ const itineraries = [];
333
+
334
+ const timeZone = json.context.timezone;
335
+
336
+ for (const jsonItinerary of json.journeys) {
337
+
338
+ const legs = [];
339
+
340
+ for (const jsonSection of jsonItinerary.sections) {
341
+
342
+ if (jsonSection.type === 'waiting' || jsonSection.type === 'transfer') {
343
+ continue;
344
+ }
345
+
346
+ const { from: startSection, to: endSection } = this.getSectionCoords(jsonSection);
347
+
348
+ // A section can have multiple same coordinates, we need to remove them
349
+ let existingCoords: string[] = [];
350
+ const legCoords = jsonSection.geojson.coordinates.reduce((acc, [lon, lat]) => {
351
+ if (!existingCoords.includes(`${lon}-${lat}`)) {
352
+ existingCoords = existingCoords.concat(`${lon}-${lat}`);
353
+ acc.push(new Coordinates(lat, lon));
354
+ }
355
+ return acc;
356
+ }, [] as Coordinates[]);
357
+
358
+ const stepsBuilder = new StepsBuilder().setStart(startSection).setEnd(endSection).setPathCoords(legCoords);
359
+ let transportInfo: TransportInfo | undefined;
360
+ let transitMode = transitModeCorrespondance.get(jsonSection.mode) as TransitMode;
361
+
362
+ if (jsonSection.path) {
363
+ const useNavitiaSteps = jsonSection.path.every(step => 'instruction_start_coordinate' in step || step.via_uri);
364
+ const navitiaIntermediateSteps = [];
365
+
366
+ for (const jsonPathLink of jsonSection.path) {
367
+ let coords: Coordinates;
368
+ if (useNavitiaSteps) {
369
+ coords = this.findStepCoords(jsonPathLink, jsonSection)!;
370
+
371
+ const intermediateStep = {
372
+ name: jsonPathLink.name,
373
+ distance: jsonPathLink.length,
374
+ coords
375
+ };
376
+
377
+ stepsBuilder.addStepInfo(intermediateStep);
378
+ } else {
379
+ navitiaIntermediateSteps.push({
380
+ name: jsonPathLink.name,
381
+ distance: jsonPathLink.length,
382
+ });
383
+ }
384
+ }
385
+
386
+ if (!useNavitiaSteps) {
387
+ stepsBuilder.setStepsInfo(this.findStepsCoord(legCoords, navitiaIntermediateSteps));
388
+ }
389
+ }
390
+
391
+ if (jsonSection.type === 'public_transport') {
392
+ transportInfo = {
393
+ name: jsonSection.display_informations.code,
394
+ routeColor: jsonSection.display_informations.color,
395
+ routeTextColor: jsonSection.display_informations.text_color,
396
+ directionName: jsonSection.display_informations.direction
397
+ };
398
+
399
+ transitMode = transitModeCorrespondance.get(jsonSection.display_informations.physical_mode) as TransitMode;
400
+
401
+ const legStep: MinStepInfo = {
402
+ coords: legCoords[0],
403
+ name: transportInfo.directionName,
404
+ distance: jsonSection.geojson.properties[0].length
405
+ };
406
+ stepsBuilder.setStepsInfo([legStep]);
407
+ }
408
+
409
+ const leg = new Leg({
410
+ transitMode,
411
+ duration: jsonSection.duration,
412
+ startTime: dateStringToTimestamp(jsonSection.departure_date_time, timeZone),
413
+ endTime: dateStringToTimestamp(jsonSection.arrival_date_time, timeZone),
414
+ start: {
415
+ name: jsonSection.from.name,
416
+ coords: startSection
417
+ },
418
+ end: {
419
+ name: jsonSection.to.name,
420
+ coords: endSection
421
+ },
422
+ coords: legCoords,
423
+ transportInfo,
424
+ steps: stepsBuilder.build()
425
+ });
426
+ legs.push(leg);
427
+ }
428
+
429
+ const itinerary = new Itinerary({
430
+ duration: jsonItinerary.duration,
431
+ startTime: dateStringToTimestamp(jsonItinerary.departure_date_time, timeZone),
432
+ endTime: dateStringToTimestamp(jsonItinerary.arrival_date_time, timeZone),
433
+ origin: this.getSectionCoords(jsonItinerary.sections[0]).from,
434
+ destination: this.getSectionCoords(last(jsonItinerary.sections)).to,
435
+ legs
436
+ });
437
+
438
+ itineraries.push(itinerary);
439
+ }
440
+
441
+ return itineraries;
442
+ }
443
+ }
444
+
445
+ export default new NavitiaRemoteRouter();
@@ -0,0 +1,73 @@
1
+
2
+ export type NavitiaCoordinates = { lat: number | string, lon: number | string };
3
+ export type NavitiaPath = {
4
+ name: string,
5
+ length: number
6
+ } & ({ instruction_start_coordinate: NavitiaCoordinates } | { via_uri: string });
7
+
8
+ export type NavitiaWaypoint = { name: string }
9
+ & (
10
+ { stop_point: { coord: NavitiaCoordinates } } |
11
+ { address: { coord: NavitiaCoordinates } } |
12
+ { poi: { coord: NavitiaCoordinates } }
13
+ )
14
+
15
+ export type NavitiaSection = {
16
+ id: string;
17
+ type: 'waiting' | 'transfer' | string,
18
+ mode: string,
19
+ departure_date_time: string,
20
+ arrival_date_time: string,
21
+ duration: number,
22
+ geojson: {
23
+ coordinates: [number, number][],
24
+ properties: { length: number }[]
25
+ },
26
+ path: NavitiaPath[],
27
+ display_informations: {
28
+ code: string,
29
+ color: string,
30
+ text_color: string,
31
+ direction: string,
32
+ physical_mode: string
33
+ },
34
+ from: NavitiaWaypoint,
35
+ to: NavitiaWaypoint,
36
+ vias?: {
37
+ id: string;
38
+ name: string;
39
+ access_point: {
40
+ id: string;
41
+ name: string;
42
+ coord: {
43
+ lon: string;
44
+ lat: string;
45
+ },
46
+ embedded_type: string;
47
+ },
48
+ is_entrance: boolean,
49
+ is_exit: boolean,
50
+ length: number,
51
+ traversal_time: number,
52
+ pathway_mode: number
53
+ }[]
54
+ }
55
+ export type NavitiaJson = {
56
+ journeys: {
57
+ duration: number,
58
+ departure_date_time: string,
59
+ arrival_date_time: string,
60
+ sections: NavitiaSection[]
61
+ }[],
62
+ context: {
63
+ timezone: string
64
+ };
65
+ error: {
66
+ message?: string;
67
+ }
68
+ };
69
+
70
+ export type NavitiaIntermediateStep = {
71
+ name: string,
72
+ distance: number
73
+ };
@@ -37,7 +37,7 @@ describe('OsrmRemoteRouter - parseResponse', () => {
37
37
  const itinerary1 = itineraries[0];
38
38
  expect(itinerary1.origin.equals(origin)).true;
39
39
  expect(itinerary1.destination.equals(destination)).true;
40
- expect(itinerary1.distance).closeTo(400, 1);
40
+ expect(itinerary1.distance).closeTo(429, 1);
41
41
  expect(itinerary1.duration).equal(287.8);
42
42
  expect(itinerary1.startTime).is.null;
43
43
  expect(itinerary1.endTime).is.null;
@@ -196,7 +196,8 @@ export default class CustomGraphMap {
196
196
 
197
197
  getRouteInsideMap(start: Coordinates, end: Coordinates, options: GraphRouterOptions) {
198
198
  // Call the Wemap router to get the shortest path
199
- return this.router.calculateShortestPath(start, end, options).route();
199
+ const route = this.router.calculateShortestPath(start, end, options).route();
200
+ return route.hasRoute ? route : null;
200
201
  }
201
202
 
202
203
  getTripInsideMap(waypoints: Coordinates[], options: GraphRouterOptions) {