@wemap/routers 6.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/bureaux-wemap-montpellier-network.osm +162 -0
- package/assets/gare-de-lyon-extract.osm +174 -0
- package/assets/itinerary-deutsche-bahn-1.json +368 -0
- package/assets/itinerary-grenoble-otp-1.json +1536 -0
- package/assets/itinerary-grenoble-otp-2.json +1092 -0
- package/assets/itinerary-lehavre-cityway-1.json +6799 -0
- package/assets/itinerary-lehavre-cityway-2.json +2133 -0
- package/assets/itinerary-lehavre-cityway-3.json +12577 -0
- package/assets/itinerary-lehavre-cityway-4.json +1451 -0
- package/assets/itinerary-lehavre-cityway-5.json +5925 -0
- package/assets/itinerary-montpellier-osrm-3.json +1 -0
- package/assets/itinerary-montpellier-outdoor-without-steps.json +110 -0
- package/assets/itinerary-montpellier-outdoor.json +513 -0
- package/assets/itinerary-with-duplicate-nodes.json +110 -0
- package/assets/network-conveying-backward.osm +74 -0
- package/assets/network-elevator.osm +48 -0
- package/assets/network-simple.osm +27 -0
- package/assets/network-with-modifiers.osm +39 -0
- package/assets/one-way.osm +46 -0
- package/dist/wemap-osm.es.js +2012 -0
- package/dist/wemap-osm.es.js.map +1 -0
- package/index.js +24 -0
- package/package.json +35 -0
- package/src/ItineraryInfoManager.js +148 -0
- package/src/ItineraryInfoManager.spec.js +54 -0
- package/src/Utils.js +64 -0
- package/src/cityway/CitywayUtils.js +240 -0
- package/src/cityway/CitywayUtils.spec.js +109 -0
- package/src/deutsche-bahn/DeutscheBahnRouterUtils.js +91 -0
- package/src/deutsche-bahn/DeutscheBahnRouterUtils.spec.js +54 -0
- package/src/model/Itinerary.js +197 -0
- package/src/model/Itinerary.type.spec.js +105 -0
- package/src/model/ItineraryInfo.js +34 -0
- package/src/model/Leg.js +113 -0
- package/src/model/LevelChange.js +65 -0
- package/src/model/RouterResponse.js +19 -0
- package/src/model/RouterResponse.type.spec.js +24 -0
- package/src/model/Step.js +118 -0
- package/src/osrm/OsrmUtils.js +269 -0
- package/src/osrm/OsrmUtils.spec.js +457 -0
- package/src/otp/OtpUtils.js +150 -0
- package/src/otp/OtpUtils.spec.js +97 -0
- package/src/wemap/WemapNetworkUtils.js +195 -0
- package/src/wemap/WemapNetworkUtils.spec.js +109 -0
- package/src/wemap/WemapRouter.js +27 -0
- package/src/wemap/WemapRouter.spec.js +221 -0
- package/src/wemap/WemapRouterOptions.js +32 -0
- package/src/wemap/WemapRouterUtils.js +46 -0
- package/src/wemap/WemapStepsGeneration.js +104 -0
|
@@ -0,0 +1,240 @@
|
|
|
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 Itinerary from '../model/Itinerary.js';
|
|
7
|
+
import Leg from '../model/Leg.js';
|
|
8
|
+
import RouterResponse from '../model/RouterResponse.js';
|
|
9
|
+
import Step from '../model/Step.js';
|
|
10
|
+
import { generateStepsMetadata } from '../Utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} json
|
|
14
|
+
* @returns {Coordinates}
|
|
15
|
+
*/
|
|
16
|
+
export function jsonToCoordinates(json) {
|
|
17
|
+
return new Coordinates(json.Lat, json.Long);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} jsonDate
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
function jsonDateToTimestamp(jsonDate) {
|
|
25
|
+
const [dateStr, timeStr] = jsonDate.split(' ');
|
|
26
|
+
const [dayStr, monthStr, yearStr] = dateStr.split('/');
|
|
27
|
+
const [hoursStr, minutesStr, secondsStr] = timeStr.split(':');
|
|
28
|
+
const date = new Date(
|
|
29
|
+
Number(yearStr), Number(monthStr) - 1, Number(dayStr),
|
|
30
|
+
Number(hoursStr), Number(minutesStr), Number(secondsStr)
|
|
31
|
+
);
|
|
32
|
+
return date.getTime();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} wktGeometry
|
|
37
|
+
* @returns {Coordinates[]}
|
|
38
|
+
*/
|
|
39
|
+
function parseWKTGeometry(wktGeometry) {
|
|
40
|
+
const tmpCoordsStr = wktGeometry.match(/LINESTRING \((.*)\)/i);
|
|
41
|
+
if (!tmpCoordsStr) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return tmpCoordsStr[1].split(',').map(str => {
|
|
45
|
+
const sp = str.trim().split(' ');
|
|
46
|
+
return new Coordinates(Number(sp[1]), Number(sp[0]));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} iso8601Duration
|
|
52
|
+
* @see https://stackoverflow.com/a/29153059/2239938
|
|
53
|
+
*/
|
|
54
|
+
function parseDuration(iso8601Duration) {
|
|
55
|
+
const iso8601DurationRegex = /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
|
|
56
|
+
|
|
57
|
+
var matches = iso8601Duration.match(iso8601DurationRegex);
|
|
58
|
+
|
|
59
|
+
// const sign = typeof matches[1] === 'undefined' ? '+' : '-',
|
|
60
|
+
const years = typeof matches[2] === 'undefined' ? 0 : Number(matches[2]);
|
|
61
|
+
const months = typeof matches[3] === 'undefined' ? 0 : Number(matches[3]);
|
|
62
|
+
const weeks = typeof matches[4] === 'undefined' ? 0 : Number(matches[4]);
|
|
63
|
+
const days = typeof matches[5] === 'undefined' ? 0 : Number(matches[5]);
|
|
64
|
+
const hours = typeof matches[6] === 'undefined' ? 0 : Number(matches[6]);
|
|
65
|
+
const minutes = typeof matches[7] === 'undefined' ? 0 : Number(matches[7]);
|
|
66
|
+
const seconds = typeof matches[8] === 'undefined' ? 0 : Number(matches[8]);
|
|
67
|
+
|
|
68
|
+
return seconds
|
|
69
|
+
+ minutes * 60
|
|
70
|
+
+ hours * 3600
|
|
71
|
+
+ days * 86400
|
|
72
|
+
+ weeks * (86400 * 7)
|
|
73
|
+
+ months * (86400 * 30)
|
|
74
|
+
+ years * (86400 * 365.25);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate multi itineraries from Cityway JSON
|
|
79
|
+
* @param {object} json JSON file provided by Cityway.
|
|
80
|
+
* @returns {?RouterResponse}
|
|
81
|
+
* @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
|
|
82
|
+
*/
|
|
83
|
+
export function createRouterResponseFromJson(json) {
|
|
84
|
+
|
|
85
|
+
if (json.StatusCode !== 200 || !json.Data || !json.Data.length) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const routerResponse = new RouterResponse();
|
|
90
|
+
routerResponse.routerName = 'cityway';
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// Do not know if it the best approach, but it works...
|
|
94
|
+
const allJsonTrips = json.Data.reduce((acc, dataObj) => {
|
|
95
|
+
acc.push(...dataObj.response.trips.Trip);
|
|
96
|
+
return acc;
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
for (const trip of allJsonTrips) {
|
|
100
|
+
|
|
101
|
+
const itinerary = new Itinerary();
|
|
102
|
+
|
|
103
|
+
itinerary.duration = parseDuration(trip.Duration);
|
|
104
|
+
itinerary.startTime = jsonDateToTimestamp(trip.Departure.Time);
|
|
105
|
+
itinerary.from = jsonToCoordinates(trip.Departure.Site.Position);
|
|
106
|
+
itinerary.endTime = jsonDateToTimestamp(trip.Arrival.Time);
|
|
107
|
+
itinerary.to = jsonToCoordinates(trip.Arrival.Site.Position);
|
|
108
|
+
routerResponse.itineraries.push(itinerary);
|
|
109
|
+
|
|
110
|
+
for (const jsonSection of trip.sections.Section) {
|
|
111
|
+
|
|
112
|
+
const jsonLeg = jsonSection.Leg ? jsonSection.Leg : jsonSection.PTRide;
|
|
113
|
+
|
|
114
|
+
const leg = new Leg();
|
|
115
|
+
|
|
116
|
+
leg.mode = jsonLeg.TransportMode;
|
|
117
|
+
leg.duration = parseDuration(jsonLeg.Duration);
|
|
118
|
+
leg.startTime = jsonDateToTimestamp(jsonLeg.Departure.Time);
|
|
119
|
+
leg.endTime = jsonDateToTimestamp(jsonLeg.Arrival.Time);
|
|
120
|
+
leg.coords = [];
|
|
121
|
+
|
|
122
|
+
if (leg.mode === 'WALK' || leg.mode === 'BICYCLE') {
|
|
123
|
+
|
|
124
|
+
leg.from = {
|
|
125
|
+
name: jsonLeg.Departure.Site.Name,
|
|
126
|
+
coords: jsonToCoordinates(jsonLeg.Departure.Site.Position)
|
|
127
|
+
};
|
|
128
|
+
leg.to = {
|
|
129
|
+
name: jsonLeg.Arrival.Site.Name,
|
|
130
|
+
coords: jsonToCoordinates(jsonLeg.Arrival.Site.Position)
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
leg.steps = [];
|
|
134
|
+
for (const jsonPathLink of jsonLeg.pathLinks.PathLink) {
|
|
135
|
+
const step = new Step();
|
|
136
|
+
let stepCoords;
|
|
137
|
+
if (jsonPathLink.Geometry) {
|
|
138
|
+
stepCoords = parseWKTGeometry(jsonPathLink.Geometry);
|
|
139
|
+
} else {
|
|
140
|
+
stepCoords = [leg.from.coords, leg.to.coords];
|
|
141
|
+
}
|
|
142
|
+
step.coords = stepCoords[0];
|
|
143
|
+
step._idCoordsInLeg = leg.coords.length;
|
|
144
|
+
stepCoords.forEach((coords, idx) => {
|
|
145
|
+
if (
|
|
146
|
+
idx !== 0
|
|
147
|
+
|| leg.coords.length === 0
|
|
148
|
+
|| !leg.coords[leg.coords.length - 1].equalsTo(coords)
|
|
149
|
+
) {
|
|
150
|
+
leg.coords.push(coords);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
step.name = jsonPathLink.Departure.Site.Name;
|
|
156
|
+
step.levelChange = null;
|
|
157
|
+
|
|
158
|
+
step.distance = jsonPathLink.Distance;
|
|
159
|
+
|
|
160
|
+
leg.steps.push(step);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} else if (leg.mode === 'BUS' || leg.mode === 'TRAMWAY' || leg.mode === 'FUNICULAR') {
|
|
164
|
+
|
|
165
|
+
leg.from = {
|
|
166
|
+
name: jsonLeg.Departure.StopPlace.Name,
|
|
167
|
+
coords: jsonToCoordinates(jsonLeg.Departure.StopPlace.Position)
|
|
168
|
+
};
|
|
169
|
+
leg.to = {
|
|
170
|
+
name: jsonLeg.Arrival.StopPlace.Name,
|
|
171
|
+
coords: jsonToCoordinates(jsonLeg.Arrival.StopPlace.Position)
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let transportName = jsonLeg.Line.Number;
|
|
175
|
+
if (leg.mode === 'TRAMWAY') {
|
|
176
|
+
leg.mode = 'TRAM';
|
|
177
|
+
// In order to remove the "TRAM " prefix.
|
|
178
|
+
transportName = transportName.substr(5);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
leg.transportInfo = {
|
|
182
|
+
name: transportName,
|
|
183
|
+
routeColor: jsonLeg.Line.Color,
|
|
184
|
+
routeTextColor: jsonLeg.Line.TextColor,
|
|
185
|
+
directionName: jsonLeg.Destination
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (const jsonStep of jsonLeg.steps.Step) {
|
|
189
|
+
const stepCoords = parseWKTGeometry(jsonStep.Geometry);
|
|
190
|
+
stepCoords.forEach((coords, idx) => {
|
|
191
|
+
if (
|
|
192
|
+
idx !== 0
|
|
193
|
+
|| leg.coords.length === 0
|
|
194
|
+
|| !leg.coords[leg.coords.length - 1].equalsTo(coords)
|
|
195
|
+
) {
|
|
196
|
+
leg.coords.push(coords);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const legStep = new Step();
|
|
202
|
+
legStep.coords = leg.coords[0];
|
|
203
|
+
legStep._idCoordsInLeg = 0;
|
|
204
|
+
legStep.name = jsonLeg.Line.Name;
|
|
205
|
+
legStep.levelChange = null;
|
|
206
|
+
legStep.distance = jsonLeg.Distance;
|
|
207
|
+
leg.steps = [legStep];
|
|
208
|
+
} else {
|
|
209
|
+
Logger.warn(`[CitywayParser] Unknown leg mode: ${leg.mode}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
leg.distance = leg.coords.reduce((acc, coords, idx, arr) => {
|
|
213
|
+
if (idx === 0) {
|
|
214
|
+
return acc;
|
|
215
|
+
}
|
|
216
|
+
return acc + arr[idx - 1].distanceTo(coords);
|
|
217
|
+
}, 0);
|
|
218
|
+
|
|
219
|
+
itinerary.legs.push(leg);
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
itinerary.distance = itinerary.coords.reduce((acc, coords, idx, arr) => {
|
|
224
|
+
if (idx === 0) {
|
|
225
|
+
return acc;
|
|
226
|
+
}
|
|
227
|
+
return acc + arr[idx - 1].distanceTo(coords);
|
|
228
|
+
}, 0);
|
|
229
|
+
|
|
230
|
+
// All legs have to be parsed before computing steps metadata
|
|
231
|
+
generateStepsMetadata(itinerary);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
routerResponse.from = routerResponse.itineraries[0].from;
|
|
235
|
+
routerResponse.to = routerResponse.itineraries[routerResponse.itineraries.length - 1].to;
|
|
236
|
+
|
|
237
|
+
return routerResponse;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
import chai from 'chai';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
import { Coordinates } from '@wemap/geo';
|
|
8
|
+
|
|
9
|
+
import { createRouterResponseFromJson } from './CitywayUtils.js';
|
|
10
|
+
|
|
11
|
+
import { verifyRouterResponseData } from '../model/RouterResponse.type.spec.js';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const { expect } = chai;
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
describe('CitywayUtils - createRouterResponseFromJson', () => {
|
|
19
|
+
|
|
20
|
+
it('RouterResponse - 1', () => {
|
|
21
|
+
|
|
22
|
+
const filePath = path.resolve(__dirname, '../../assets/itinerary-lehavre-cityway-1.json');
|
|
23
|
+
const fileString = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
const json = JSON.parse(fileString);
|
|
25
|
+
|
|
26
|
+
const routerResponse = createRouterResponseFromJson(json);
|
|
27
|
+
verifyRouterResponseData(routerResponse);
|
|
28
|
+
|
|
29
|
+
expect(routerResponse.routerName).equal('cityway');
|
|
30
|
+
expect(routerResponse.itineraries.length).equal(3);
|
|
31
|
+
expect(routerResponse.from.equalsTo(new Coordinates(49.515093882362159, 0.093417496193663158))).true;
|
|
32
|
+
expect(routerResponse.to.equalsTo(new Coordinates(49.5067090188444, 0.16948421154178309))).true;
|
|
33
|
+
|
|
34
|
+
const itinerary1 = routerResponse.itineraries[0];
|
|
35
|
+
expect(itinerary1.distance).to.be.closeTo(6887, 1);
|
|
36
|
+
expect(itinerary1.duration).equal(2379);
|
|
37
|
+
// Do not work because of the input time format
|
|
38
|
+
// expect(itinerary1.startTime).equal(1620659156000);
|
|
39
|
+
// expect(itinerary1.endTime).equal(1620661535000);
|
|
40
|
+
expect(itinerary1.legs.length).equal(5);
|
|
41
|
+
|
|
42
|
+
const itinerary1leg1 = itinerary1.legs[0];
|
|
43
|
+
// Do not work because of the input time format
|
|
44
|
+
// expect(itinerary1leg1.startTime).equal(1620659156000);
|
|
45
|
+
// expect(itinerary1leg1.endTime).equal(1620659340000);
|
|
46
|
+
expect(itinerary1leg1.distance).to.be.closeTo(200.14, 0.1);
|
|
47
|
+
expect(itinerary1leg1.mode).equal('WALK');
|
|
48
|
+
expect(itinerary1leg1.transportInfo).is.null;
|
|
49
|
+
expect(itinerary1leg1.from.name).equal('RUE DU QUARTIER NEUF');
|
|
50
|
+
expect(itinerary1leg1.from.coords.equalsTo(new Coordinates(49.515093882362159, 0.093417496193663158))).true;
|
|
51
|
+
expect(itinerary1leg1.to.name).equal('T. Gautier');
|
|
52
|
+
expect(itinerary1leg1.to.coords.equalsTo(new Coordinates(49.5147550229, 0.0911025378))).true;
|
|
53
|
+
|
|
54
|
+
const itinerary1leg2 = itinerary1.legs[1];
|
|
55
|
+
expect(itinerary1leg2.mode).equal('BUS');
|
|
56
|
+
expect(itinerary1leg2.transportInfo).is.not.null;
|
|
57
|
+
expect(itinerary1leg2.transportInfo.name).equal('03');
|
|
58
|
+
expect(itinerary1leg2.transportInfo.routeColor).equal('3FA435');
|
|
59
|
+
expect(itinerary1leg2.transportInfo.routeTextColor).is.null;
|
|
60
|
+
expect(itinerary1leg2.transportInfo.directionName).equal('Graville');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('RouterResponse - 2', () => {
|
|
64
|
+
const filePath = path.resolve(__dirname, '../../assets/itinerary-lehavre-cityway-2.json');
|
|
65
|
+
const fileString = fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
const json = JSON.parse(fileString);
|
|
67
|
+
|
|
68
|
+
const routerResponse = createRouterResponseFromJson(json);
|
|
69
|
+
verifyRouterResponseData(routerResponse);
|
|
70
|
+
|
|
71
|
+
expect(routerResponse.itineraries.length).equal(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('RouterResponse - 3', () => {
|
|
75
|
+
const filePath = path.resolve(__dirname, '../../assets/itinerary-lehavre-cityway-3.json');
|
|
76
|
+
const fileString = fs.readFileSync(filePath, 'utf8');
|
|
77
|
+
const json = JSON.parse(fileString);
|
|
78
|
+
|
|
79
|
+
const routerResponse = createRouterResponseFromJson(json);
|
|
80
|
+
verifyRouterResponseData(routerResponse);
|
|
81
|
+
|
|
82
|
+
expect(routerResponse.itineraries[0].legs[1].mode).equal('FUNICULAR');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('RouterResponse - 4', () => {
|
|
86
|
+
const filePath = path.resolve(__dirname, '../../assets/itinerary-lehavre-cityway-4.json');
|
|
87
|
+
const fileString = fs.readFileSync(filePath, 'utf8');
|
|
88
|
+
const json = JSON.parse(fileString);
|
|
89
|
+
|
|
90
|
+
const routerResponse = createRouterResponseFromJson(json);
|
|
91
|
+
verifyRouterResponseData(routerResponse);
|
|
92
|
+
|
|
93
|
+
expect(routerResponse.itineraries[0].legs[0].mode).equal('BICYCLE');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('RouterResponse - 5', () => {
|
|
97
|
+
const filePath = path.resolve(__dirname, '../../assets/itinerary-lehavre-cityway-5.json');
|
|
98
|
+
const fileString = fs.readFileSync(filePath, 'utf8');
|
|
99
|
+
const json = JSON.parse(fileString);
|
|
100
|
+
|
|
101
|
+
const routerResponse = createRouterResponseFromJson(json);
|
|
102
|
+
verifyRouterResponseData(routerResponse);
|
|
103
|
+
|
|
104
|
+
expect(routerResponse.itineraries[1].legs[1].mode).equal('TRAM');
|
|
105
|
+
expect(routerResponse.itineraries[1].legs[1].transportInfo.name).equal('B');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
});
|
|
109
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
|
|
3
|
+
import { Coordinates, GraphEdge, GraphItinerary, GraphNode, Level } from '@wemap/geo';
|
|
4
|
+
import { OsmElement, OsmNode, OsmWay } from '@wemap/osm';
|
|
5
|
+
|
|
6
|
+
import Itinerary from '../model/Itinerary.js';
|
|
7
|
+
import RouterResponse from '../model/RouterResponse.js';
|
|
8
|
+
import { generateStepsMetadata } from '../Utils.js';
|
|
9
|
+
import StepsGeneration from '../wemap/WemapStepsGeneration.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate multi itineraries from the DB JSON
|
|
13
|
+
* @param {object} json JSON file provided by the DB.
|
|
14
|
+
* @param {Coordinates} from itinerary start
|
|
15
|
+
* @param {Coordinates} to itinerary end
|
|
16
|
+
* @returns {?RouterResponse}
|
|
17
|
+
*/
|
|
18
|
+
export function createRouterResponseFromJson(json, from, to) {
|
|
19
|
+
|
|
20
|
+
const { segments: jsonSegments } = json;
|
|
21
|
+
|
|
22
|
+
if (!jsonSegments) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const routerResponse = new RouterResponse();
|
|
27
|
+
routerResponse.routerName = 'deutsche-bahn';
|
|
28
|
+
|
|
29
|
+
routerResponse.from = from;
|
|
30
|
+
routerResponse.to = to;
|
|
31
|
+
|
|
32
|
+
/** @type {GraphEdge<OsmElement>[]} */
|
|
33
|
+
const edges = [];
|
|
34
|
+
|
|
35
|
+
/** @type {GraphNode<OsmElement>[]} */
|
|
36
|
+
const nodes = [];
|
|
37
|
+
|
|
38
|
+
/** @type {number[]} */
|
|
39
|
+
const edgesWeights = [];
|
|
40
|
+
|
|
41
|
+
let id = 1;
|
|
42
|
+
for (const jsonSegment of jsonSegments) {
|
|
43
|
+
|
|
44
|
+
const level = new Level(jsonSegment.fromLevel, jsonSegment.toLevel);
|
|
45
|
+
const osmWay = new OsmWay(id++, null, level);
|
|
46
|
+
|
|
47
|
+
for (const jsonPoint of jsonSegment.polyline) {
|
|
48
|
+
const coord = new Coordinates(jsonPoint.lat, jsonPoint.lon, null, level);
|
|
49
|
+
|
|
50
|
+
if (nodes.length !== 0
|
|
51
|
+
&& nodes[nodes.length - 1].coords.equalsTo(coord)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const osmNode = new OsmNode(id++, coord);
|
|
56
|
+
const node = new GraphNode(osmNode.coords, osmNode);
|
|
57
|
+
|
|
58
|
+
if (nodes.length !== 0) {
|
|
59
|
+
const prevNode = nodes[nodes.length - 1];
|
|
60
|
+
const edge = new GraphEdge(prevNode, node, level, osmWay);
|
|
61
|
+
edges.push(edge);
|
|
62
|
+
edgesWeights.push(prevNode.coords.distanceTo(osmNode));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
nodes.push(node);
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @type {GraphItinerary<OsmElement>} */
|
|
71
|
+
const graphItinerary = new GraphItinerary();
|
|
72
|
+
graphItinerary.nodes = nodes;
|
|
73
|
+
graphItinerary.edges = edges;
|
|
74
|
+
graphItinerary.edgesWeights = edgesWeights;
|
|
75
|
+
graphItinerary.start = nodes[0].coords;
|
|
76
|
+
graphItinerary.end = nodes[nodes.length - 1].coords;
|
|
77
|
+
|
|
78
|
+
const points = nodes.map(node => node.coords);
|
|
79
|
+
const itinerary = Itinerary.fromOrderedCoordinates(points, from, to);
|
|
80
|
+
itinerary.legs[0].steps = StepsGeneration.fromGraphItinerary(graphItinerary);
|
|
81
|
+
itinerary.legs[0].steps.map((step, idx) => (step._idCoordsInLeg = idx));
|
|
82
|
+
|
|
83
|
+
// All legs have to be parsed before computing steps metadata
|
|
84
|
+
generateStepsMetadata(itinerary);
|
|
85
|
+
|
|
86
|
+
routerResponse.itineraries.push(itinerary);
|
|
87
|
+
|
|
88
|
+
return routerResponse;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
import chai from 'chai';
|
|
3
|
+
import chaiAlmost from 'chai-almost';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
import { Coordinates, Level } from '@wemap/geo';
|
|
9
|
+
|
|
10
|
+
import { createRouterResponseFromJson } from './DeutscheBahnRouterUtils.js';
|
|
11
|
+
|
|
12
|
+
import { verifyRouterResponseData } from '../model/RouterResponse.type.spec.js';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const { expect } = chai;
|
|
16
|
+
chai.use(chaiAlmost(0.1));
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
describe('OtpUtils - createRouterResponseFromJson', () => {
|
|
21
|
+
|
|
22
|
+
it('RouterResponse - 1', () => {
|
|
23
|
+
|
|
24
|
+
const filePath = path.resolve(__dirname, '../../assets/itinerary-deutsche-bahn-1.json');
|
|
25
|
+
const fileString = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
const json = JSON.parse(fileString);
|
|
27
|
+
|
|
28
|
+
const from = new Coordinates(52.5258473, 13.3683657, null, new Level(-10));
|
|
29
|
+
const to = new Coordinates(52.52499085853664, 13.369467296949914, null, new Level(0));
|
|
30
|
+
|
|
31
|
+
const routerResponse = createRouterResponseFromJson(json, from, to);
|
|
32
|
+
verifyRouterResponseData(routerResponse);
|
|
33
|
+
|
|
34
|
+
expect(routerResponse.routerName).equal('deutsche-bahn');
|
|
35
|
+
expect(routerResponse.itineraries.length).equal(1);
|
|
36
|
+
expect(routerResponse.from.equalsTo(from)).true;
|
|
37
|
+
expect(routerResponse.to.equalsTo(to)).true;
|
|
38
|
+
|
|
39
|
+
const itinerary1 = routerResponse.itineraries[0];
|
|
40
|
+
expect(itinerary1.coords.length).equal(77);
|
|
41
|
+
expect(itinerary1.distance).almost.equal(129.673);
|
|
42
|
+
expect(itinerary1.duration).almost.equal(93.365);
|
|
43
|
+
|
|
44
|
+
const itinerary1leg1 = itinerary1.legs[0];
|
|
45
|
+
expect(itinerary1leg1.distance).almost.equal(129.673);
|
|
46
|
+
expect(itinerary1leg1.mode).equal('WALK');
|
|
47
|
+
expect(itinerary1leg1.from.name).is.null;
|
|
48
|
+
expect(itinerary1leg1.from.coords.equalsTo(from)).true;
|
|
49
|
+
expect(itinerary1leg1.to.name).is.null;
|
|
50
|
+
expect(itinerary1leg1.to.coords.equalsTo(to)).true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
});
|
|
54
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
import { Coordinates, Network } from '@wemap/geo';
|
|
3
|
+
import Leg from './Leg.js';
|
|
4
|
+
import Step from './Step.js';
|
|
5
|
+
import { getDurationFromLength } from '../Utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Main attributes are:
|
|
9
|
+
* nodes: the ordered list of Node
|
|
10
|
+
* edges: the ordered list of Edge
|
|
11
|
+
* start: the start point (Coordinates)
|
|
12
|
+
* end: the end point (Coordinates)
|
|
13
|
+
* length: the route length
|
|
14
|
+
*/
|
|
15
|
+
class Itinerary {
|
|
16
|
+
|
|
17
|
+
/** @type {!Coordinates} */
|
|
18
|
+
from;
|
|
19
|
+
|
|
20
|
+
/** @type {!Coordinates} */
|
|
21
|
+
to;
|
|
22
|
+
|
|
23
|
+
/** @type {!number} */
|
|
24
|
+
distance;
|
|
25
|
+
|
|
26
|
+
/** @type {!number} */
|
|
27
|
+
duration;
|
|
28
|
+
|
|
29
|
+
/** @type {?number} */
|
|
30
|
+
startTime = null;
|
|
31
|
+
|
|
32
|
+
/** @type {?number} */
|
|
33
|
+
endTime = null;
|
|
34
|
+
|
|
35
|
+
/** @type {!(Leg[])} */
|
|
36
|
+
legs = [];
|
|
37
|
+
|
|
38
|
+
/** @type {?Coordinates[]} */
|
|
39
|
+
_coords = null;
|
|
40
|
+
|
|
41
|
+
set coords(_) {
|
|
42
|
+
throw new Error('Itinerary.coords cannot be set. They are calculated from Itinerary.legs.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {!(Coordinates[])} */
|
|
46
|
+
get coords() {
|
|
47
|
+
if (!this._coords) {
|
|
48
|
+
// Returns the coordinates contained in all legs and remove duplicates between array
|
|
49
|
+
this._coords = this.legs.reduce((acc, val) => {
|
|
50
|
+
const isDuplicate = acc.length && val.coords.length && acc[acc.length - 1].equalsTo(val.coords[0]);
|
|
51
|
+
acc.push(...val.coords.slice(isDuplicate ? 1 : 0));
|
|
52
|
+
return acc;
|
|
53
|
+
}, []);
|
|
54
|
+
}
|
|
55
|
+
return this._coords;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
set steps(_) {
|
|
59
|
+
throw new Error('Itinerary.step cannot be set. They are calculated from Itinerary.legs.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {!(Step[])} */
|
|
63
|
+
get steps() {
|
|
64
|
+
return this.legs.map(leg => leg.steps).flat();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @returns {Network}
|
|
69
|
+
*/
|
|
70
|
+
toNetwork() {
|
|
71
|
+
return Network.fromCoordinates([this.coords]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {Itinerary[]} itineraries
|
|
76
|
+
* @returns {Itinerary}
|
|
77
|
+
*/
|
|
78
|
+
static fromItineraries(...itineraries) {
|
|
79
|
+
const itinerary = new Itinerary();
|
|
80
|
+
itinerary.from = itineraries[0].from;
|
|
81
|
+
itinerary.to = itineraries[itineraries.length - 1].to;
|
|
82
|
+
itinerary.distance = 0;
|
|
83
|
+
itinerary.duration = 0;
|
|
84
|
+
itinerary.legs = [];
|
|
85
|
+
|
|
86
|
+
itineraries.forEach(_itinerary => {
|
|
87
|
+
itinerary.distance += _itinerary.distance;
|
|
88
|
+
itinerary.duration += _itinerary.duration;
|
|
89
|
+
itinerary.legs.push(..._itinerary.legs);
|
|
90
|
+
itinerary.legs.forEach(leg => {
|
|
91
|
+
leg.steps[0].firstStep = false;
|
|
92
|
+
leg.steps[leg.steps.length - 1].lastStep = false;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
itinerary.legs[0].steps[0].firstStep = true;
|
|
97
|
+
const lastLeg = itinerary.legs[itinerary.legs.length - 1];
|
|
98
|
+
lastLeg.steps[lastLeg.steps.length - 1].lastStep = true;
|
|
99
|
+
|
|
100
|
+
return itinerary;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert lat/lng/level points to Itinerary
|
|
105
|
+
* @param {number[][]} points 2D points array of lat/lng/level (level is optional)
|
|
106
|
+
* @param {Coordinates} from
|
|
107
|
+
* @param {Coordinates} to
|
|
108
|
+
* @param {string} mode
|
|
109
|
+
* @returns {Itinerary}
|
|
110
|
+
*/
|
|
111
|
+
static fromOrderedPointsArray(points, start, end) {
|
|
112
|
+
|
|
113
|
+
const pointToCoordinates = point => new Coordinates(point[0], point[1], null, point[2]);
|
|
114
|
+
|
|
115
|
+
return this.fromOrderedCoordinates(
|
|
116
|
+
points.map(pointToCoordinates),
|
|
117
|
+
pointToCoordinates(start),
|
|
118
|
+
pointToCoordinates(end)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert ordered Coordinates to Itinerary
|
|
124
|
+
* @param {Coordinates[]} points
|
|
125
|
+
* @param {Coordinates} from
|
|
126
|
+
* @param {Coordinates} to
|
|
127
|
+
* @param {string} mode
|
|
128
|
+
* @returns {Itinerary}
|
|
129
|
+
*/
|
|
130
|
+
static fromOrderedCoordinates(points, from, to, mode = 'WALK') {
|
|
131
|
+
|
|
132
|
+
const itinerary = new Itinerary();
|
|
133
|
+
itinerary.from = from;
|
|
134
|
+
itinerary.to = to;
|
|
135
|
+
|
|
136
|
+
const leg = new Leg();
|
|
137
|
+
leg.mode = mode;
|
|
138
|
+
leg.from = { name: null, coords: from };
|
|
139
|
+
leg.to = { name: null, coords: to };
|
|
140
|
+
|
|
141
|
+
leg.coords = points;
|
|
142
|
+
leg.distance = points.reduce((acc, coords, idx, arr) => {
|
|
143
|
+
if (idx !== 0) {
|
|
144
|
+
return acc + arr[idx - 1].distanceTo(coords);
|
|
145
|
+
}
|
|
146
|
+
return acc;
|
|
147
|
+
}, 0);
|
|
148
|
+
leg.duration = getDurationFromLength(leg.distance);
|
|
149
|
+
itinerary.legs.push(leg);
|
|
150
|
+
|
|
151
|
+
itinerary.distance = leg.distance;
|
|
152
|
+
itinerary.duration = leg.duration;
|
|
153
|
+
|
|
154
|
+
return itinerary;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @returns {object}
|
|
159
|
+
*/
|
|
160
|
+
toJson() {
|
|
161
|
+
const output = {
|
|
162
|
+
from: this.from.toCompressedJson(),
|
|
163
|
+
to: this.to.toCompressedJson(),
|
|
164
|
+
distance: this.distance,
|
|
165
|
+
duration: this.duration,
|
|
166
|
+
legs: this.legs.map(leg => leg.toJson())
|
|
167
|
+
};
|
|
168
|
+
if (this.startTime !== null) {
|
|
169
|
+
output.startTime = this.startTime;
|
|
170
|
+
}
|
|
171
|
+
if (this.endTime !== null) {
|
|
172
|
+
output.endTime = this.endTime;
|
|
173
|
+
}
|
|
174
|
+
return output;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {object} json
|
|
179
|
+
* @returns {Itinerary}
|
|
180
|
+
*/
|
|
181
|
+
static fromJson(json) {
|
|
182
|
+
const itinerary = new Itinerary();
|
|
183
|
+
itinerary.from = Coordinates.fromCompressedJson(json.from);
|
|
184
|
+
itinerary.to = Coordinates.fromCompressedJson(json.to);
|
|
185
|
+
itinerary.distance = json.distance;
|
|
186
|
+
itinerary.duration = json.duration;
|
|
187
|
+
itinerary.legs = json.legs.map(Leg.fromJson);
|
|
188
|
+
if (json.startTime) {
|
|
189
|
+
itinerary.startTime = json.startTime;
|
|
190
|
+
}
|
|
191
|
+
if (json.endTime) {
|
|
192
|
+
itinerary.endTime = json.endTime;
|
|
193
|
+
}
|
|
194
|
+
return itinerary;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export default Itinerary;
|