@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.
Files changed (49) hide show
  1. package/assets/bureaux-wemap-montpellier-network.osm +162 -0
  2. package/assets/gare-de-lyon-extract.osm +174 -0
  3. package/assets/itinerary-deutsche-bahn-1.json +368 -0
  4. package/assets/itinerary-grenoble-otp-1.json +1536 -0
  5. package/assets/itinerary-grenoble-otp-2.json +1092 -0
  6. package/assets/itinerary-lehavre-cityway-1.json +6799 -0
  7. package/assets/itinerary-lehavre-cityway-2.json +2133 -0
  8. package/assets/itinerary-lehavre-cityway-3.json +12577 -0
  9. package/assets/itinerary-lehavre-cityway-4.json +1451 -0
  10. package/assets/itinerary-lehavre-cityway-5.json +5925 -0
  11. package/assets/itinerary-montpellier-osrm-3.json +1 -0
  12. package/assets/itinerary-montpellier-outdoor-without-steps.json +110 -0
  13. package/assets/itinerary-montpellier-outdoor.json +513 -0
  14. package/assets/itinerary-with-duplicate-nodes.json +110 -0
  15. package/assets/network-conveying-backward.osm +74 -0
  16. package/assets/network-elevator.osm +48 -0
  17. package/assets/network-simple.osm +27 -0
  18. package/assets/network-with-modifiers.osm +39 -0
  19. package/assets/one-way.osm +46 -0
  20. package/dist/wemap-osm.es.js +2012 -0
  21. package/dist/wemap-osm.es.js.map +1 -0
  22. package/index.js +24 -0
  23. package/package.json +35 -0
  24. package/src/ItineraryInfoManager.js +148 -0
  25. package/src/ItineraryInfoManager.spec.js +54 -0
  26. package/src/Utils.js +64 -0
  27. package/src/cityway/CitywayUtils.js +240 -0
  28. package/src/cityway/CitywayUtils.spec.js +109 -0
  29. package/src/deutsche-bahn/DeutscheBahnRouterUtils.js +91 -0
  30. package/src/deutsche-bahn/DeutscheBahnRouterUtils.spec.js +54 -0
  31. package/src/model/Itinerary.js +197 -0
  32. package/src/model/Itinerary.type.spec.js +105 -0
  33. package/src/model/ItineraryInfo.js +34 -0
  34. package/src/model/Leg.js +113 -0
  35. package/src/model/LevelChange.js +65 -0
  36. package/src/model/RouterResponse.js +19 -0
  37. package/src/model/RouterResponse.type.spec.js +24 -0
  38. package/src/model/Step.js +118 -0
  39. package/src/osrm/OsrmUtils.js +269 -0
  40. package/src/osrm/OsrmUtils.spec.js +457 -0
  41. package/src/otp/OtpUtils.js +150 -0
  42. package/src/otp/OtpUtils.spec.js +97 -0
  43. package/src/wemap/WemapNetworkUtils.js +195 -0
  44. package/src/wemap/WemapNetworkUtils.spec.js +109 -0
  45. package/src/wemap/WemapRouter.js +27 -0
  46. package/src/wemap/WemapRouter.spec.js +221 -0
  47. package/src/wemap/WemapRouterOptions.js +32 -0
  48. package/src/wemap/WemapRouterUtils.js +46 -0
  49. 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;