@wemap/routers 6.2.2 → 7.0.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.
- package/assets/biocbon-bergere-rdc-network.osm +163 -0
- package/assets/gare-de-lest-network-pp-bounds.osm +1615 -0
- package/dist/wemap-routers.es.js +1811 -695
- package/dist/wemap-routers.es.js.map +1 -1
- package/index.js +13 -5
- package/package.json +9 -6
- package/src/Constants.js +4 -2
- package/src/ItineraryInfoManager.spec.js +2 -2
- package/src/Utils.js +0 -77
- package/src/model/Itinerary.js +41 -5
- package/src/model/Itinerary.spec.js +91 -0
- package/src/model/Itinerary.type.spec.js +3 -78
- package/src/model/Leg.js +89 -19
- package/src/model/Leg.spec.js +110 -0
- package/src/model/Leg.type.spec.js +48 -0
- package/src/model/LevelChange.js +14 -24
- package/src/model/LevelChange.spec.js +78 -0
- package/src/model/LevelChange.type.spec.js +26 -0
- package/src/model/RouterResponse.js +70 -1
- package/src/model/RouterResponse.spec.js +85 -0
- package/src/model/RouterResponse.type.spec.js +7 -4
- package/src/model/Step.js +45 -6
- package/src/model/Step.spec.js +100 -0
- package/src/model/Step.type.spec.js +52 -0
- package/src/remote/RemoteRouter.js +31 -0
- package/src/remote/RemoteRouterManager.js +84 -0
- package/src/remote/RemoteRouterOptions.js +25 -0
- package/src/remote/RemoteRouterServerUnreachable.js +10 -0
- package/src/remote/RemoteRouterUtils.js +78 -0
- package/src/remote/RoutingModeCorrespondanceNotFound.js +18 -0
- package/src/remote/cityway/CitywayRemoteRouter.js +386 -0
- package/src/{cityway/CitywayUtils.spec.js → remote/cityway/CitywayRemoteRouter.spec.js} +19 -18
- package/src/remote/deutsche-bahn/DeutscheBahnRemoteRouter.js +143 -0
- package/src/{deutsche-bahn/DeutscheBahnRouterUtils.spec.js → remote/deutsche-bahn/DeutscheBahnRemoteRouter.spec.js} +7 -6
- package/src/remote/idfm/IdfmRemoteRouter.js +432 -0
- package/src/{idfm/IdfmUtils.spec.js → remote/idfm/IdfmRemoteRouter.spec.js} +7 -6
- package/src/remote/idfm/IdfmRemoteRouterTokenError.js +6 -0
- package/src/remote/osrm/OsrmRemoteRouter.js +331 -0
- package/src/{osrm/OsrmUtils.spec.js → remote/osrm/OsrmRemoteRouter.spec.js} +9 -15
- package/src/remote/otp/OtpRemoteRouter.js +222 -0
- package/src/{otp/OtpUtils.spec.js → remote/otp/OtpRemoteRouter.spec.js} +10 -9
- package/src/remote/wemap-meta/WemapMetaRemoteRouter.js +57 -0
- package/src/remote/wemap-meta/WemapMetaRemoteRouter.spec.js +22 -0
- package/src/remote/wemap-meta/WemapMetaRemoteRouterOptions.js +36 -0
- package/src/remote/wemap-meta/WemapMetaRemoteRouterPayload.js +44 -0
- package/src/wemap/WemapRouter.js +6 -0
- package/src/wemap/WemapRouterUtils.js +10 -4
- package/src/wemap/WemapStepsGeneration.js +36 -9
- package/src/wemap-meta/IOMap.js +191 -0
- package/src/wemap-meta/WemapMetaRouter.js +314 -0
- package/src/wemap-meta/WemapMetaRouter.spec.js +119 -0
- package/src/wemap-meta/WemapMetaRouterOptions.js +20 -0
- package/src/cityway/CitywayUtils.js +0 -252
- package/src/deutsche-bahn/DeutscheBahnRouterUtils.js +0 -91
- package/src/idfm/IdfmUtils.js +0 -247
- package/src/osrm/OsrmUtils.js +0 -269
- package/src/otp/OtpUtils.js +0 -150
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chai from 'chai';
|
|
2
|
+
|
|
3
|
+
import { Coordinates } from '@wemap/geo';
|
|
4
|
+
|
|
5
|
+
import LevelChange from './LevelChange.js';
|
|
6
|
+
import Step from './Step.js';
|
|
7
|
+
import checkLevelChangeType from './LevelChange.type.spec.js';
|
|
8
|
+
|
|
9
|
+
const { expect } = chai;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const isNullOrNumber = val => val === null || typeof val === 'number';
|
|
13
|
+
const isNullOrString = val => val === null || typeof val === 'string';
|
|
14
|
+
const isNullOrObject = val => val === null || typeof val === 'object';
|
|
15
|
+
const isNullOrLevelChange = val => val === null || val instanceof LevelChange;
|
|
16
|
+
|
|
17
|
+
const isUndefinedOrBoolean = val => typeof val === 'undefined' || typeof val === 'boolean';
|
|
18
|
+
const isUndefinedOrString = val => typeof val === 'undefined' || typeof val === 'string';
|
|
19
|
+
|
|
20
|
+
const stepExtraProperties = ['subwayEntrance', 'subwayEntranceRef'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Step} step
|
|
24
|
+
*/
|
|
25
|
+
export default function checkStepType(step) {
|
|
26
|
+
|
|
27
|
+
expect(step).instanceOf(Step);
|
|
28
|
+
expect(step.firstStep).be.a('boolean');
|
|
29
|
+
expect(step.lastStep).be.a('boolean');
|
|
30
|
+
expect(step.number).be.a('number');
|
|
31
|
+
expect(step.coords).instanceOf(Coordinates);
|
|
32
|
+
expect(step.angle).be.a('number');
|
|
33
|
+
expect(step.previousBearing).be.a('number');
|
|
34
|
+
expect(step.nextBearing).be.a('number');
|
|
35
|
+
expect(step.distance).be.a('number');
|
|
36
|
+
expect(step.duration).satisfies(isNullOrNumber);
|
|
37
|
+
expect(step.name).satisfies(isNullOrString);
|
|
38
|
+
expect(step.levelChange).satisfies(isNullOrLevelChange);
|
|
39
|
+
if (step.levelChange) {
|
|
40
|
+
checkLevelChangeType(step.levelChange);
|
|
41
|
+
}
|
|
42
|
+
expect(step._idCoordsInLeg).be.a('number');
|
|
43
|
+
|
|
44
|
+
expect(step.extras).satisfies(isNullOrObject);
|
|
45
|
+
if (step.extras !== null) {
|
|
46
|
+
for (const key of Object.keys(step.extras)) {
|
|
47
|
+
expect(stepExtraProperties).includes(key);
|
|
48
|
+
}
|
|
49
|
+
expect(step.extras.subwayEntrance).satisfies(isUndefinedOrBoolean);
|
|
50
|
+
expect(step.extras.subwayEntranceRef).satisfies(isUndefinedOrString);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Coordinates } from '@wemap/geo';
|
|
2
|
+
|
|
3
|
+
import RouterResponse from '../model/RouterResponse.js';
|
|
4
|
+
import RemoteRouterOptions from './RemoteRouterOptions.js';
|
|
5
|
+
|
|
6
|
+
class RemoteRouter {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the router name
|
|
10
|
+
* @type {string} the router name
|
|
11
|
+
*/
|
|
12
|
+
get rname() {
|
|
13
|
+
return 'Unknown';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @abstract
|
|
18
|
+
* @param {string} endpointUrl
|
|
19
|
+
* @param {string} mode see Constants.ROUTING_MODE
|
|
20
|
+
* @param {Array<Coordinates>} waypoints
|
|
21
|
+
* @param {RemoteRouterOptions} options
|
|
22
|
+
* @returns {!RouterResponse}
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line no-unused-vars
|
|
25
|
+
async getItineraries(endpointUrl, mode, waypoints, options = new RemoteRouterOptions()) {
|
|
26
|
+
throw new Error('RemoteRouter "' + this.rname + '" does not @override getItineraries()');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default RemoteRouter;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Coordinates } from '@wemap/geo';
|
|
2
|
+
|
|
3
|
+
import RouterResponse from '../model/RouterResponse.js';
|
|
4
|
+
import RemoteRouter from './RemoteRouter.js';
|
|
5
|
+
import CitywayRemoteRouter from './cityway/CitywayRemoteRouter.js';
|
|
6
|
+
import DeutscheBahnRemoteRouter from './deutsche-bahn/DeutscheBahnRemoteRouter.js';
|
|
7
|
+
import IdfmRemoteRouter from './idfm/IdfmRemoteRouter.js';
|
|
8
|
+
import OsrmRemoteRouter from './osrm/OsrmRemoteRouter.js';
|
|
9
|
+
import OtpRemoteRouter from './otp/OtpRemoteRouter.js';
|
|
10
|
+
import RemoteRouterOptions from './RemoteRouterOptions.js';
|
|
11
|
+
import RemoteRouterServerUnreachable from './RemoteRouterServerUnreachable.js';
|
|
12
|
+
import RoutingModeCorrespondanceNotFound from './RoutingModeCorrespondanceNotFound.js';
|
|
13
|
+
import IdfmRemoteRouterTokenError from './idfm/IdfmRemoteRouterTokenError.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Singleton
|
|
17
|
+
*/
|
|
18
|
+
class RemoteRouterManager {
|
|
19
|
+
|
|
20
|
+
/** @type {RemoteRouter[]} */
|
|
21
|
+
remoteRouters = [
|
|
22
|
+
CitywayRemoteRouter,
|
|
23
|
+
DeutscheBahnRemoteRouter,
|
|
24
|
+
IdfmRemoteRouter,
|
|
25
|
+
OsrmRemoteRouter,
|
|
26
|
+
OtpRemoteRouter
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @returns {RemoteRouter}
|
|
32
|
+
*/
|
|
33
|
+
getRouterByName(name) {
|
|
34
|
+
return this.remoteRouters.find(remoteRouter => remoteRouter.rname === name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {!string} endpointUrl
|
|
39
|
+
* @param {!string} mode see Constants.ROUTING_MODE
|
|
40
|
+
* @param {Array<Coordinates>} waypoints
|
|
41
|
+
* @param {?RemoteRouterOptions} options
|
|
42
|
+
* @returns {!RouterResponse}
|
|
43
|
+
* @throws {RemoteRouterServerUnreachable}
|
|
44
|
+
* @throws {RoutingModeCorrespondanceNotFound}
|
|
45
|
+
* @throws {IdfmRemoteRouterTokenError}
|
|
46
|
+
*/
|
|
47
|
+
async getItineraries(name, endpointUrl, mode, waypoints, options = new RemoteRouterOptions()) {
|
|
48
|
+
const router = this.getRouterByName(name);
|
|
49
|
+
if (!router) {
|
|
50
|
+
throw new Error(`Unknown "${this.rname}" remote router`);
|
|
51
|
+
}
|
|
52
|
+
return router.getItineraries(endpointUrl, mode, waypoints, options);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {!{name: string, endpointUrl: string}[]} remoteRouters
|
|
58
|
+
* @param {!string} mode see Constants.ROUTING_MODE
|
|
59
|
+
* @param {!Array<Coordinates>} waypoints
|
|
60
|
+
* @param {?RemoteRouterOptions} options
|
|
61
|
+
* @returns {!RouterResponse}
|
|
62
|
+
* @throws {RemoteRouterServerUnreachable}
|
|
63
|
+
* @throws {RoutingModeCorrespondanceNotFound}
|
|
64
|
+
* @throws {IdfmRemoteRouterTokenError}
|
|
65
|
+
*/
|
|
66
|
+
async getItinerariesWithFallback(remoteRouters, mode, waypoints, options = new RemoteRouterOptions()) {
|
|
67
|
+
let routerResponse;
|
|
68
|
+
for (const { name, endpointUrl } of remoteRouters) {
|
|
69
|
+
routerResponse = await this.getItineraries(name, endpointUrl, mode, waypoints, options);
|
|
70
|
+
if (routerResponse.itineraries.length) {
|
|
71
|
+
return routerResponse;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!routerResponse) {
|
|
75
|
+
routerResponse = new RouterResponse();
|
|
76
|
+
routerResponse.from = waypoints[0];
|
|
77
|
+
routerResponse.to = waypoints[waypoints.length];
|
|
78
|
+
}
|
|
79
|
+
return routerResponse;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default new RemoteRouterManager();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class RemoteRouterOptions {
|
|
2
|
+
|
|
3
|
+
/** @type {boolean} */
|
|
4
|
+
useStairs = true;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {object}
|
|
8
|
+
*/
|
|
9
|
+
toJson() {
|
|
10
|
+
return { useStairs: this.useStairs };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object}
|
|
15
|
+
* @returns {RemoteRouterOptions}
|
|
16
|
+
*/
|
|
17
|
+
static fromJson(json) {
|
|
18
|
+
const obj = new RemoteRouterOptions();
|
|
19
|
+
obj.useStairs = json.useStairs;
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default RemoteRouterOptions;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { diffAngle } from '@wemap/maths';
|
|
2
|
+
|
|
3
|
+
import Itinerary from '../model/Itinerary.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Itinerary} itinerary
|
|
8
|
+
*/
|
|
9
|
+
export function generateStepsMetadata(itinerary) {
|
|
10
|
+
|
|
11
|
+
let counter = 1;
|
|
12
|
+
|
|
13
|
+
itinerary.legs.forEach((leg, legId) => {
|
|
14
|
+
leg.steps.forEach((step, stepId) => {
|
|
15
|
+
|
|
16
|
+
if (counter === 1) {
|
|
17
|
+
step.firstStep = true;
|
|
18
|
+
}
|
|
19
|
+
if (legId === itinerary.legs.length - 1
|
|
20
|
+
&& stepId === leg.steps.length - 1) {
|
|
21
|
+
step.lastStep = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
step.number = counter++;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Generate previousBearing, nextBearing and angle
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
let coordsBeforeStep;
|
|
32
|
+
if (step._idCoordsInLeg > 0) {
|
|
33
|
+
coordsBeforeStep = leg.coords[step._idCoordsInLeg - 1];
|
|
34
|
+
} else if (legId === 0) {
|
|
35
|
+
coordsBeforeStep = itinerary.from;
|
|
36
|
+
} else {
|
|
37
|
+
coordsBeforeStep = itinerary.legs[legId - 1].to.coords;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let coordsAfterStep;
|
|
41
|
+
if (step._idCoordsInLeg !== leg.coords.length - 1) {
|
|
42
|
+
coordsAfterStep = leg.coords[step._idCoordsInLeg + 1];
|
|
43
|
+
} else if (legId === itinerary.legs.length - 1) {
|
|
44
|
+
coordsAfterStep = itinerary.to;
|
|
45
|
+
} else {
|
|
46
|
+
coordsAfterStep = itinerary.legs[legId + 1].from.coords;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
step.previousBearing = coordsBeforeStep.bearingTo(step.coords);
|
|
50
|
+
step.nextBearing = step.coords.bearingTo(coordsAfterStep);
|
|
51
|
+
step.angle = diffAngle(step.previousBearing, step.nextBearing + Math.PI);
|
|
52
|
+
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create and return a date with a given timezone
|
|
60
|
+
* @param {number} year
|
|
61
|
+
* @param {number} month
|
|
62
|
+
* @param {number} day
|
|
63
|
+
* @param {number} hours
|
|
64
|
+
* @param {number} minutes
|
|
65
|
+
* @param {number} seconds
|
|
66
|
+
* @param {string} timeZone - timezone name (e.g. 'Europe/Paris')
|
|
67
|
+
*/
|
|
68
|
+
export function dateWithTimeZone(year, month, day, hour, minute, second, timeZone = 'Europe/Paris') {
|
|
69
|
+
const date = new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
70
|
+
|
|
71
|
+
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
|
72
|
+
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timeZone }));
|
|
73
|
+
const offset = utcDate.getTime() - tzDate.getTime();
|
|
74
|
+
|
|
75
|
+
date.setTime(date.getTime() + offset);
|
|
76
|
+
|
|
77
|
+
return date;
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default class RoutingModeCorrespondanceNotFound extends Error {
|
|
2
|
+
|
|
3
|
+
/** @type {!string} */
|
|
4
|
+
routerName;
|
|
5
|
+
|
|
6
|
+
/** @type {!string} */
|
|
7
|
+
routingMode;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {!string} routerName
|
|
11
|
+
* @param {!string} routingMode
|
|
12
|
+
*/
|
|
13
|
+
constructor(routerName, routingMode) {
|
|
14
|
+
this.routerName = routerName;
|
|
15
|
+
this.routingMode = routingMode;
|
|
16
|
+
super(`routing mode "${routingMode}" correspondance not found for router "${routerName}"`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/* eslint-disable max-depth */
|
|
2
|
+
/* eslint-disable max-statements */
|
|
3
|
+
import { Coordinates } from '@wemap/geo';
|
|
4
|
+
import Logger from '@wemap/logger';
|
|
5
|
+
|
|
6
|
+
import RemoteRouter from '../RemoteRouter.js';
|
|
7
|
+
import Itinerary from '../../model/Itinerary.js';
|
|
8
|
+
import Leg from '../../model/Leg.js';
|
|
9
|
+
import RouterResponse from '../../model/RouterResponse.js';
|
|
10
|
+
import Step from '../../model/Step.js';
|
|
11
|
+
import { generateStepsMetadata, dateWithTimeZone } from '../RemoteRouterUtils.js';
|
|
12
|
+
import Constants from '../../Constants.js';
|
|
13
|
+
import RemoteRouterServerUnreachable from '../RemoteRouterServerUnreachable.js';
|
|
14
|
+
import RoutingModeCorrespondanceNotFound from '../RoutingModeCorrespondanceNotFound.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Input mode correspondance
|
|
18
|
+
*/
|
|
19
|
+
const inputModeCorrespondance = new Map();
|
|
20
|
+
inputModeCorrespondance.set(Constants.ROUTING_MODE.CAR, 'Car');
|
|
21
|
+
inputModeCorrespondance.set(Constants.ROUTING_MODE.WALK, 'Walk');
|
|
22
|
+
inputModeCorrespondance.set(Constants.ROUTING_MODE.BIKE, 'Bike');
|
|
23
|
+
inputModeCorrespondance.set(Constants.ROUTING_MODE.BUS, 'PT');
|
|
24
|
+
inputModeCorrespondance.set(Constants.ROUTING_MODE.MULTI, 'PT');
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List of all routing modes supported by the API
|
|
29
|
+
*/
|
|
30
|
+
const routingModeCorrespondance = new Map();
|
|
31
|
+
routingModeCorrespondance.set('WALK', Constants.ROUTING_MODE.WALK);
|
|
32
|
+
routingModeCorrespondance.set('BICYCLE', Constants.ROUTING_MODE.BIKE);
|
|
33
|
+
routingModeCorrespondance.set('TRAMWAY', Constants.ROUTING_MODE.TRAM);
|
|
34
|
+
routingModeCorrespondance.set('METRO', Constants.ROUTING_MODE.METRO);
|
|
35
|
+
routingModeCorrespondance.set('FUNICULAR', Constants.ROUTING_MODE.FUNICULAR);
|
|
36
|
+
routingModeCorrespondance.set('BUS', Constants.ROUTING_MODE.BUS);
|
|
37
|
+
routingModeCorrespondance.set('COACH', Constants.ROUTING_MODE.BUS);
|
|
38
|
+
routingModeCorrespondance.set('SCHOOL', Constants.ROUTING_MODE.BUS);
|
|
39
|
+
routingModeCorrespondance.set('BUS_PMR', Constants.ROUTING_MODE.BUS);
|
|
40
|
+
routingModeCorrespondance.set('MINIBUS', Constants.ROUTING_MODE.BUS);
|
|
41
|
+
routingModeCorrespondance.set('TROLLEY_BUS', Constants.ROUTING_MODE.BUS);
|
|
42
|
+
routingModeCorrespondance.set('TAXIBUS', Constants.ROUTING_MODE.BUS);
|
|
43
|
+
routingModeCorrespondance.set('SHUTTLE', Constants.ROUTING_MODE.BUS);
|
|
44
|
+
routingModeCorrespondance.set('TRAIN', Constants.ROUTING_MODE.TRAIN);
|
|
45
|
+
routingModeCorrespondance.set('HST', Constants.ROUTING_MODE.TRAIN);
|
|
46
|
+
routingModeCorrespondance.set('LOCAL_TRAIN', Constants.ROUTING_MODE.TRAIN);
|
|
47
|
+
routingModeCorrespondance.set('AIR', Constants.ROUTING_MODE.AIRPLANE);
|
|
48
|
+
routingModeCorrespondance.set('FERRY', Constants.ROUTING_MODE.BOAT);
|
|
49
|
+
routingModeCorrespondance.set('TAXI', Constants.ROUTING_MODE.UNKNOWN);
|
|
50
|
+
routingModeCorrespondance.set('CAR_POOL', Constants.ROUTING_MODE.UNKNOWN);
|
|
51
|
+
routingModeCorrespondance.set('PRIVATE_VEHICLE', Constants.ROUTING_MODE.CAR);
|
|
52
|
+
routingModeCorrespondance.set('SCOOTER', Constants.ROUTING_MODE.MOTO);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* List of all plan trip supported by the API
|
|
56
|
+
* Routing mode UNKNOWN means that the itinerary will not be parsed by the router
|
|
57
|
+
*/
|
|
58
|
+
const planTripType = new Map();
|
|
59
|
+
planTripType.set(0, Constants.ROUTING_MODE.BUS);
|
|
60
|
+
planTripType.set(1, Constants.ROUTING_MODE.WALK);
|
|
61
|
+
planTripType.set(2, Constants.ROUTING_MODE.BIKE);
|
|
62
|
+
planTripType.set(3, Constants.ROUTING_MODE.CAR);
|
|
63
|
+
planTripType.set(4, Constants.ROUTING_MODE.UNKNOWN);
|
|
64
|
+
planTripType.set(5, Constants.ROUTING_MODE.UNKNOWN);
|
|
65
|
+
planTripType.set(6, Constants.ROUTING_MODE.UNKNOWN);
|
|
66
|
+
planTripType.set(7, Constants.ROUTING_MODE.UNKNOWN);
|
|
67
|
+
planTripType.set(8, Constants.ROUTING_MODE.UNKNOWN);
|
|
68
|
+
planTripType.set(9, Constants.ROUTING_MODE.UNKNOWN);
|
|
69
|
+
planTripType.set(10, Constants.ROUTING_MODE.UNKNOWN);
|
|
70
|
+
planTripType.set(11, Constants.ROUTING_MODE.UNKNOWN);
|
|
71
|
+
planTripType.set(12, Constants.ROUTING_MODE.UNKNOWN);
|
|
72
|
+
planTripType.set(13, Constants.ROUTING_MODE.UNKNOWN);
|
|
73
|
+
planTripType.set(14, Constants.ROUTING_MODE.UNKNOWN);
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Singleton.
|
|
78
|
+
*/
|
|
79
|
+
class CitywayRemoteRouter extends RemoteRouter {
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @override
|
|
83
|
+
*/
|
|
84
|
+
get rname() {
|
|
85
|
+
return 'cityway';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @override
|
|
91
|
+
* @throws {RoutingModeCorrespondanceNotFound}
|
|
92
|
+
* @throws {RemoteRouterServerUnreachable}
|
|
93
|
+
*/
|
|
94
|
+
async getItineraries(endpointUrl, mode, waypoints) {
|
|
95
|
+
const url = this.getURL(endpointUrl, mode, waypoints);
|
|
96
|
+
const res = await fetch(url);
|
|
97
|
+
if (res.status !== 200) {
|
|
98
|
+
throw new RemoteRouterServerUnreachable(this.rname, url);
|
|
99
|
+
}
|
|
100
|
+
const response = await res.json();
|
|
101
|
+
return this.createRouterResponseFromJson(response);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} endpointUrl
|
|
107
|
+
* @param {string} mode
|
|
108
|
+
* @param {Array<Coordinates>} waypoints
|
|
109
|
+
* @throws {RoutingModeCorrespondanceNotFound}
|
|
110
|
+
*/
|
|
111
|
+
getURL(endpointUrl, mode, waypoints) {
|
|
112
|
+
const citywayMode = inputModeCorrespondance.get(mode);
|
|
113
|
+
if (!citywayMode) {
|
|
114
|
+
throw new RoutingModeCorrespondanceNotFound(this.rname, mode);
|
|
115
|
+
}
|
|
116
|
+
if (waypoints.length > 2) {
|
|
117
|
+
Logger.warn(`${this.rname} router uses only the first 2 waypoints (asked ${waypoints.length})`);
|
|
118
|
+
}
|
|
119
|
+
const fromPlace = `DepartureLatitude=${waypoints[0].latitude}&DepartureLongitude=${waypoints[0].longitude}`;
|
|
120
|
+
const toPlace = `ArrivalLatitude=${waypoints[1].latitude}&ArrivalLongitude=${waypoints[1].longitude}`;
|
|
121
|
+
const queryMode = `TripModes=${citywayMode}`;
|
|
122
|
+
|
|
123
|
+
const url = new URL(endpointUrl);
|
|
124
|
+
let { search } = url;
|
|
125
|
+
search = (search ? `${search}&` : '?') + `${fromPlace}&${toPlace}&${queryMode}`;
|
|
126
|
+
|
|
127
|
+
return `${url.origin}${url.pathname}${search}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate multi itineraries from Cityway JSON
|
|
132
|
+
* @param {object} json JSON file provided by Cityway.
|
|
133
|
+
* @returns {?RouterResponse}
|
|
134
|
+
* @example https://preprod.api.lia2.cityway.fr/journeyplanner/api/opt/PlanTrips/json?DepartureLatitude=49.51509388236216&DepartureLongitude=0.09341749619366316&ArrivalLatitude=49.5067090188444&ArrivalLongitude=0.1694842115417831&DepartureType=COORDINATES&ArrivalType=COORDINATES
|
|
135
|
+
*/
|
|
136
|
+
createRouterResponseFromJson(json) {
|
|
137
|
+
|
|
138
|
+
if (json.StatusCode !== 200 || !json.Data || !json.Data.length) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const routerResponse = new RouterResponse();
|
|
143
|
+
routerResponse.routerName = this.rname;
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
// Do not know if it the best approach, but it works...
|
|
147
|
+
const allJsonTrips = json.Data.reduce((acc, dataObj) => {
|
|
148
|
+
acc.push(...dataObj.response.trips.Trip.map(trip => ({
|
|
149
|
+
...trip,
|
|
150
|
+
...(dataObj.hasOwnProperty('PlanTripType') ? { PlanTripType: dataObj.PlanTripType } : {})
|
|
151
|
+
})));
|
|
152
|
+
return acc;
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
// eslint-disable-next-line no-labels
|
|
156
|
+
itineraryLoop:
|
|
157
|
+
for (const trip of allJsonTrips) {
|
|
158
|
+
|
|
159
|
+
if (trip.hasOwnProperty('PlanTripType') && planTripType.get(trip.PlanTripType) === Constants.ROUTING_MODE.UNKNOWN) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const itinerary = new Itinerary();
|
|
164
|
+
|
|
165
|
+
itinerary.duration = this.parseDuration(trip.Duration);
|
|
166
|
+
itinerary.startTime = this.jsonDateToTimestamp(trip.Departure.Time);
|
|
167
|
+
itinerary.from = this.jsonToCoordinates(trip.Departure.Site.Position);
|
|
168
|
+
itinerary.endTime = this.jsonDateToTimestamp(trip.Arrival.Time);
|
|
169
|
+
itinerary.to = this.jsonToCoordinates(trip.Arrival.Site.Position);
|
|
170
|
+
|
|
171
|
+
for (const jsonSection of trip.sections.Section) {
|
|
172
|
+
|
|
173
|
+
const jsonLeg = jsonSection.Leg ? jsonSection.Leg : jsonSection.PTRide;
|
|
174
|
+
|
|
175
|
+
const leg = new Leg();
|
|
176
|
+
|
|
177
|
+
leg.mode = routingModeCorrespondance.get(jsonLeg.TransportMode);
|
|
178
|
+
leg.duration = this.parseDuration(jsonLeg.Duration);
|
|
179
|
+
leg.startTime = this.jsonDateToTimestamp(jsonLeg.Departure.Time);
|
|
180
|
+
leg.endTime = this.jsonDateToTimestamp(jsonLeg.Arrival.Time);
|
|
181
|
+
leg.coords = [];
|
|
182
|
+
|
|
183
|
+
if (leg.mode === Constants.ROUTING_MODE.UNKNOWN) {
|
|
184
|
+
// eslint-disable-next-line
|
|
185
|
+
continue itineraryLoop;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (leg.mode === Constants.ROUTING_MODE.WALK
|
|
189
|
+
|| leg.mode === Constants.ROUTING_MODE.BIKE
|
|
190
|
+
|| leg.mode === Constants.ROUTING_MODE.CAR) {
|
|
191
|
+
|
|
192
|
+
leg.from = {
|
|
193
|
+
name: jsonLeg.Departure.Site.Name,
|
|
194
|
+
coords: this.jsonToCoordinates(jsonLeg.Departure.Site.Position)
|
|
195
|
+
};
|
|
196
|
+
leg.to = {
|
|
197
|
+
name: jsonLeg.Arrival.Site.Name,
|
|
198
|
+
coords: this.jsonToCoordinates(jsonLeg.Arrival.Site.Position)
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
leg.steps = [];
|
|
202
|
+
for (const jsonPathLink of jsonLeg.pathLinks.PathLink) {
|
|
203
|
+
const step = new Step();
|
|
204
|
+
let stepCoords;
|
|
205
|
+
if (jsonPathLink.Geometry) {
|
|
206
|
+
stepCoords = this.parseWKTGeometry(jsonPathLink.Geometry);
|
|
207
|
+
} else {
|
|
208
|
+
stepCoords = [leg.from.coords, leg.to.coords];
|
|
209
|
+
}
|
|
210
|
+
step.coords = stepCoords[0];
|
|
211
|
+
step._idCoordsInLeg = leg.coords.length;
|
|
212
|
+
stepCoords.forEach((coords, idx) => {
|
|
213
|
+
if (
|
|
214
|
+
idx !== 0
|
|
215
|
+
|| leg.coords.length === 0
|
|
216
|
+
|| !leg.coords[leg.coords.length - 1].equalsTo(coords)
|
|
217
|
+
) {
|
|
218
|
+
leg.coords.push(coords);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
step.name = jsonPathLink.Departure.Site.Name;
|
|
224
|
+
step.levelChange = null;
|
|
225
|
+
|
|
226
|
+
step.distance = jsonPathLink.Distance;
|
|
227
|
+
|
|
228
|
+
leg.steps.push(step);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} else if (Constants.PUBLIC_TRANSPORT.includes(leg.mode)) {
|
|
232
|
+
|
|
233
|
+
leg.from = {
|
|
234
|
+
name: jsonLeg.Departure.StopPlace.Name,
|
|
235
|
+
coords: this.jsonToCoordinates(jsonLeg.Departure.StopPlace.Position)
|
|
236
|
+
};
|
|
237
|
+
leg.to = {
|
|
238
|
+
name: jsonLeg.Arrival.StopPlace.Name,
|
|
239
|
+
coords: this.jsonToCoordinates(jsonLeg.Arrival.StopPlace.Position)
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let transportName = jsonLeg.Line.Number;
|
|
243
|
+
if (leg.mode === Constants.ROUTING_MODE.TRAM && transportName.toLowerCase().includes('tram')) {
|
|
244
|
+
// In order to remove the "TRAM " prefix.
|
|
245
|
+
transportName = transportName.substr(5);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
leg.transportInfo = {
|
|
249
|
+
name: transportName,
|
|
250
|
+
routeColor: jsonLeg.Line.Color,
|
|
251
|
+
routeTextColor: jsonLeg.Line.TextColor,
|
|
252
|
+
directionName: jsonLeg.Destination
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
for (const jsonStep of jsonLeg.steps.Step) {
|
|
256
|
+
const stepCoords = this.parseWKTGeometry(jsonStep.Geometry);
|
|
257
|
+
stepCoords.forEach((coords, idx) => {
|
|
258
|
+
if (
|
|
259
|
+
idx !== 0
|
|
260
|
+
|| leg.coords.length === 0
|
|
261
|
+
|| !leg.coords[leg.coords.length - 1].equalsTo(coords)
|
|
262
|
+
) {
|
|
263
|
+
leg.coords.push(coords);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const legStep = new Step();
|
|
269
|
+
legStep.coords = leg.coords[0];
|
|
270
|
+
legStep._idCoordsInLeg = 0;
|
|
271
|
+
legStep.name = jsonLeg.Line.Name;
|
|
272
|
+
legStep.levelChange = null;
|
|
273
|
+
legStep.distance = jsonLeg.Distance;
|
|
274
|
+
leg.steps = [legStep];
|
|
275
|
+
} else {
|
|
276
|
+
Logger.warn(`[CitywayParser] Unknown leg mode: ${jsonLeg.TransportMode}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
leg.distance = leg.coords.reduce((acc, coords, idx, arr) => {
|
|
280
|
+
if (idx === 0) {
|
|
281
|
+
return acc;
|
|
282
|
+
}
|
|
283
|
+
return acc + arr[idx - 1].distanceTo(coords);
|
|
284
|
+
}, 0);
|
|
285
|
+
|
|
286
|
+
itinerary.legs.push(leg);
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
routerResponse.itineraries.push(itinerary);
|
|
291
|
+
|
|
292
|
+
itinerary.distance = itinerary.coords.reduce((acc, coords, idx, arr) => {
|
|
293
|
+
if (idx === 0) {
|
|
294
|
+
return acc;
|
|
295
|
+
}
|
|
296
|
+
return acc + arr[idx - 1].distanceTo(coords);
|
|
297
|
+
}, 0);
|
|
298
|
+
|
|
299
|
+
// All legs have to be parsed before computing steps metadata
|
|
300
|
+
generateStepsMetadata(itinerary);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
routerResponse.from = routerResponse.itineraries[0].from;
|
|
304
|
+
routerResponse.to = routerResponse.itineraries[routerResponse.itineraries.length - 1].to;
|
|
305
|
+
|
|
306
|
+
return routerResponse;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {object} json
|
|
311
|
+
* @returns {Coordinates}
|
|
312
|
+
*/
|
|
313
|
+
jsonToCoordinates(json) {
|
|
314
|
+
return new Coordinates(json.Lat, json.Long);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} jsonDate
|
|
319
|
+
* @returns {number}
|
|
320
|
+
*/
|
|
321
|
+
jsonDateToTimestamp(jsonDate) {
|
|
322
|
+
const [dateStr, timeStr] = jsonDate.split(' ');
|
|
323
|
+
const [dayStr, monthStr, yearStr] = dateStr.split('/');
|
|
324
|
+
const [hoursStr, minutesStr, secondsStr] = timeStr.split(':');
|
|
325
|
+
|
|
326
|
+
return dateWithTimeZone(
|
|
327
|
+
Number(yearStr),
|
|
328
|
+
Number(monthStr) - 1,
|
|
329
|
+
Number(dayStr),
|
|
330
|
+
Number(hoursStr),
|
|
331
|
+
Number(minutesStr),
|
|
332
|
+
Number(secondsStr)
|
|
333
|
+
).getTime();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {string} wktGeometry
|
|
338
|
+
* @returns {Coordinates[]}
|
|
339
|
+
*/
|
|
340
|
+
parseWKTGeometry(wktGeometry) {
|
|
341
|
+
const tmpCoordsStr = wktGeometry.match(/LINESTRING \((.*)\)/i);
|
|
342
|
+
const tmpCoordsPt = wktGeometry.match(/POINT \((.*)\)/i);
|
|
343
|
+
if (!tmpCoordsStr && !tmpCoordsPt) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (tmpCoordsPt) {
|
|
348
|
+
const [lng, lat] = tmpCoordsPt[1].split(' ');
|
|
349
|
+
return [new Coordinates(Number(lat), Number(lng))];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return tmpCoordsStr[1].split(',').map(str => {
|
|
353
|
+
const sp = str.trim().split(' ');
|
|
354
|
+
return new Coordinates(Number(sp[1]), Number(sp[0]));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {string} iso8601Duration
|
|
360
|
+
* @see https://stackoverflow.com/a/29153059/2239938
|
|
361
|
+
*/
|
|
362
|
+
parseDuration(iso8601Duration) {
|
|
363
|
+
const iso8601DurationRegex = /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
|
|
364
|
+
|
|
365
|
+
var matches = iso8601Duration.match(iso8601DurationRegex);
|
|
366
|
+
|
|
367
|
+
// const sign = typeof matches[1] === 'undefined' ? '+' : '-',
|
|
368
|
+
const years = typeof matches[2] === 'undefined' ? 0 : Number(matches[2]);
|
|
369
|
+
const months = typeof matches[3] === 'undefined' ? 0 : Number(matches[3]);
|
|
370
|
+
const weeks = typeof matches[4] === 'undefined' ? 0 : Number(matches[4]);
|
|
371
|
+
const days = typeof matches[5] === 'undefined' ? 0 : Number(matches[5]);
|
|
372
|
+
const hours = typeof matches[6] === 'undefined' ? 0 : Number(matches[6]);
|
|
373
|
+
const minutes = typeof matches[7] === 'undefined' ? 0 : Number(matches[7]);
|
|
374
|
+
const seconds = typeof matches[8] === 'undefined' ? 0 : Number(matches[8]);
|
|
375
|
+
|
|
376
|
+
return seconds
|
|
377
|
+
+ minutes * 60
|
|
378
|
+
+ hours * 3600
|
|
379
|
+
+ days * 86400
|
|
380
|
+
+ weeks * (86400 * 7)
|
|
381
|
+
+ months * (86400 * 30)
|
|
382
|
+
+ years * (86400 * 365.25);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export default new CitywayRemoteRouter();
|