@wemap/routers 12.8.5 → 12.8.6

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/index.ts CHANGED
@@ -33,6 +33,7 @@ export { default as OtpRemoteRouter } from './src/remote/otp/OtpRemoteRouter.js'
33
33
  export { default as CitywayRemoteRouter } from './src/remote/cityway/CitywayRemoteRouter.js';
34
34
  export { default as DeutscheBahnRemoteRouter } from './src/remote/deutsche-bahn/DeutscheBahnRemoteRouter.js';
35
35
  export { default as IdfmRemoteRouter } from './src/remote/idfm/IdfmRemoteRouter.js';
36
+ export { default as GeoveloRemoteRouter } from './src/remote/geovelo/GeoveloRemoteRouter.js';
36
37
  export { default as WemapMultiRemoteRouter } from './src/remote/wemap-multi/WemapMultiRemoteRouter.js';
37
38
  export { default as RemoteRouterManager, type RemoteRouterName, type RoutingFallbackStrategy } from './src/remote/RemoteRouterManager.js';
38
39
 
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "directory": "packages/routers"
13
13
  },
14
14
  "name": "@wemap/routers",
15
- "version": "12.8.5",
15
+ "version": "12.8.6",
16
16
  "bugs": {
17
17
  "url": "https://github.com/wemap/wemap-modules-js/issues"
18
18
  },
@@ -52,5 +52,5 @@
52
52
  },
53
53
  "./helpers/*": "./helpers/*"
54
54
  },
55
- "gitHead": "3acfe134ade4d53dcec76009c7c421573c9f211c"
55
+ "gitHead": "9b7c7370f24c721dc136b32470ff96ef9df99042"
56
56
  }
@@ -0,0 +1,54 @@
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 { verifyStepsCoherence } from '../../model/Itinerary.spec.js';
10
+ import { areTransitAndTravelModeConsistent } from '../../model/TransitMode.js';
11
+ import GeoveloRemoteRouter from './GeoveloRemoteRouter.js';
12
+
13
+ const { expect } = chai;
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const assetsPath = path.resolve(__dirname, '../../../assets');
17
+
18
+ describe.only('GeoveloRouter - parseResponse', () => {
19
+
20
+ it('Itineraries - 1', () => {
21
+
22
+ const filePath = path.resolve(assetsPath, 'geovelo-montpellier.json');
23
+ const fileString = fs.readFileSync(filePath, 'utf8');
24
+ const json = JSON.parse(fileString);
25
+
26
+ const itineraries = GeoveloRemoteRouter.parseResponse(json);
27
+ itineraries.forEach(verifyStepsCoherence);
28
+
29
+ expect(itineraries.length).equal(3);
30
+
31
+ const itinerary1 = itineraries[0];
32
+ expect(itinerary1.origin.equals(new Coordinates(43.596949, 3.877772))).true;
33
+ expect(itinerary1.destination.equals(new Coordinates(43.609609, 3.914684))).true;
34
+ expect(itinerary1.distance).to.be.closeTo(4029, 1);
35
+ expect(itinerary1.duration).equal(1053);
36
+ expect(itinerary1.transitMode).equal('BIKE');
37
+ // Do not work because of the input time format
38
+ expect(itinerary1.startTime).equal(1643623200000);
39
+ expect(itinerary1.endTime).equal(1643624253000);
40
+ expect(itinerary1.legs.length).equal(1);
41
+
42
+ expect(areTransitAndTravelModeConsistent(itinerary1.transitMode, 'BIKE')).is.true;
43
+
44
+ const itinerary1leg1 = itinerary1.legs[0];
45
+ // Do not work because of the input time format
46
+ expect(itinerary1leg1.startTime).equal(1643623200000);
47
+ expect(itinerary1leg1.endTime).equal(1643624253000);
48
+ expect(itinerary1leg1.distance).to.be.closeTo(4029, 1);
49
+ expect(itinerary1leg1.transitMode).equal('BIKE');
50
+ expect(itinerary1leg1.transportInfo).is.null;
51
+ expect(itinerary1leg1.start.coords.distanceTo(new Coordinates(43.596949, 3.877772))).to.be.closeTo(0, 1);
52
+ expect(itinerary1leg1.end.coords.distanceTo(new Coordinates(43.609609, 3.914684))).to.be.closeTo(0, 1);
53
+ });
54
+ });
@@ -0,0 +1,290 @@
1
+ /* eslint-disable max-statements */
2
+ import polyline from '@mapbox/polyline';
3
+ import { Coordinates } from '@wemap/geo';
4
+
5
+ import Itinerary from '../../model/Itinerary.js';
6
+ import Leg, { TransportInfo } from '../../model/Leg.js';
7
+ import StepsBuilder from '../../model/StepsBuilder.js';
8
+ import RemoteRouter from '../RemoteRouter.js';
9
+ import type { TransitMode } from '../../model/TransitMode.js';
10
+ import { RouterRequest } from '../../model/RouterRequest.js';
11
+ import { RemoteRoutingError } from '../../RoutingError.js';
12
+
13
+ type DistanceDetails = {
14
+ total: number;
15
+ normalRoads: number;
16
+ recommendedRoads: number;
17
+ discouragedRoads: number;
18
+ cycleway: number;
19
+ greenway: number;
20
+ lane: number;
21
+ livingstreet: number;
22
+ sharebusway: number;
23
+ footway: number;
24
+ pedestrian: number;
25
+ opposite: number;
26
+ steps: number;
27
+ zone30: number;
28
+ residential: number;
29
+ };
30
+
31
+ type Waypoint = {
32
+ longitude: number;
33
+ latitude: number;
34
+ title?: string | null;
35
+ };
36
+
37
+ type Instruction = Array<string | number>;
38
+
39
+ type SectionWithInstructions = {
40
+ transportMode: 'BIKE' | 'PEDESTRIAN';
41
+ duration: number;
42
+ waypointsIndices: number[] | null;
43
+ geometry: string;
44
+ estimatedDatetimeOfDeparture: string;
45
+ estimatedDatetimeOfArrival: string;
46
+ details: SectionDetails;
47
+ waypoints: Waypoint[];
48
+ };
49
+
50
+ type SectionDetails = {
51
+ distances: DistanceDetails;
52
+ instructions: Instruction[];
53
+ profile: string;
54
+ direction: string;
55
+ verticalGain: number;
56
+ verticalLoss: number;
57
+ calories: number;
58
+ elevations: string | null;
59
+ ridesets: any[];
60
+ averageSpeed: number;
61
+ bikeType: string;
62
+ };
63
+
64
+ type Route = {
65
+ sections: SectionWithInstructions[];
66
+ distances: {
67
+ total: number;
68
+ normalRoads: number;
69
+ recommendedRoads: number;
70
+ discouragedRoads: number;
71
+ };
72
+ title: string;
73
+ waypoints: Waypoint[];
74
+ estimatedDatetimeOfDeparture: string;
75
+ estimatedDatetimeOfArrival: string;
76
+ duration: number;
77
+ id: string;
78
+ };
79
+
80
+ type Routes = Route[];
81
+
82
+ type QueryParams = {
83
+ instructions: boolean;
84
+ elevations: boolean;
85
+ geometry: boolean;
86
+ single_result: boolean;
87
+ bike_stations: boolean;
88
+ objects_as_ids: boolean;
89
+ merge_instructions: boolean;
90
+ show_pushing_bike_instructions: boolean;
91
+ }
92
+
93
+ type RequestBodyParams = {
94
+ datetimeOfDeparture?: string;
95
+ datetimeOfArrival?: string;
96
+ bikeDetails?: {
97
+ profile: string;
98
+ bikeType: string;
99
+ averageSpeed: number;
100
+ eBike: boolean;
101
+ bikeStations: Array<{ from: number; to: number }>;
102
+ };
103
+ transportModes: Array<'BIKE' | 'PEDESTRIAN'>;
104
+ waypoints: Waypoint[];
105
+ }
106
+
107
+ function unpackJSON(data: any[][]) {
108
+ const headers = data[0];
109
+
110
+ return data.slice(1).map(row => {
111
+ const obj: any = {};
112
+ headers.forEach((header, index) => {
113
+ obj[header] = row[index];
114
+ });
115
+ return obj;
116
+ });
117
+ }
118
+
119
+ const transitModeCorrespondance = new Map<string, TransitMode>();
120
+ transitModeCorrespondance.set('BIKE', 'BIKE');
121
+ transitModeCorrespondance.set('PEDESTRIAN', 'WALK');
122
+
123
+ const apiKey = 'qWHj6ax6DMttG8DX6tH9CQARaiTgQ1Di';
124
+
125
+ function waypointToCoordinates(waypoint: Waypoint) {
126
+ return new Coordinates(waypoint.latitude, waypoint.longitude);
127
+ }
128
+
129
+ function last<T>(array: T[]) {
130
+ return array[array.length - 1];
131
+ }
132
+
133
+ /**
134
+ * Singleton.
135
+ */
136
+ class GeoveloRemoteRouter extends RemoteRouter {
137
+
138
+ /**
139
+ * @override
140
+ */
141
+ get rname() { return 'geovelo' as const; }
142
+
143
+
144
+ async getItineraries(endpointUrl: string, routerRequest: RouterRequest) {
145
+ const queryParams = this.getQueryParams();
146
+ const bodyParams = this.getBodyParams(routerRequest);
147
+
148
+ const url = new URL(endpointUrl);
149
+
150
+ for (const [key, value] of Object.entries(queryParams)) {
151
+ url.searchParams.append(key, value.toString());
152
+ }
153
+
154
+ const res = await (fetch(url, {
155
+ method: 'POST',
156
+ headers: { apiKey },
157
+ body: JSON.stringify(bodyParams)
158
+ }).catch(() => {
159
+ throw RemoteRoutingError.unreachableServer(this.rname, url.toString());
160
+ }));
161
+
162
+
163
+ const jsonResponse: Routes = await res.json().catch(() => {
164
+ throw RemoteRoutingError.responseNotParsing(this.rname, url.toString());
165
+ });
166
+
167
+ // When Geovélo failed to calculate an itinerary (ie. start or end
168
+ // point is far from network), it respond with empty routes
169
+ if (!jsonResponse || jsonResponse.length === 0) {
170
+ throw RemoteRoutingError.notFound(this.rname, 'No itineraries found.');
171
+ }
172
+
173
+ const itineraries = this.parseResponse(jsonResponse);
174
+
175
+ return itineraries;
176
+ }
177
+
178
+ getQueryParams(): QueryParams {
179
+ return {
180
+ instructions: true,
181
+ elevations: false,
182
+ geometry: true,
183
+ single_result: false,
184
+ bike_stations: false,
185
+ objects_as_ids: false,
186
+ merge_instructions: true,
187
+ show_pushing_bike_instructions: false
188
+ };
189
+ }
190
+
191
+ getBodyParams(routerRequest: RouterRequest): RequestBodyParams {
192
+ const { origin, destination, waypoints } = routerRequest;
193
+ let computedWaypoints: Waypoint[] = [];
194
+ if (waypoints && waypoints.length > 1) {
195
+ computedWaypoints = waypoints.map((waypoint) => ({
196
+ longitude: waypoint.longitude,
197
+ latitude: waypoint.latitude
198
+ }));
199
+ } else {
200
+ computedWaypoints = [
201
+ { latitude: origin.latitude, longitude: origin.longitude },
202
+ { latitude: destination.latitude, longitude: destination.longitude }
203
+ ]
204
+ }
205
+
206
+ return {
207
+ transportModes: ['BIKE'],
208
+ waypoints: computedWaypoints
209
+ };
210
+ }
211
+
212
+ parseResponse(json: Routes) {
213
+ if (!json || !json.length) {
214
+ throw RemoteRoutingError.notFound(this.rname);
215
+ }
216
+
217
+ const itineraries = [];
218
+
219
+ for (const route of json) {
220
+
221
+ const legs = [];
222
+
223
+ for (const section of route.sections) {
224
+ const from = waypointToCoordinates(section.waypoints[0]);
225
+ const to = waypointToCoordinates(last(section.waypoints));
226
+ const geometry = polyline.toGeoJSON(section.geometry, 6);
227
+
228
+ // A section can have multiple same coordinates, we need to remove them
229
+ let existingCoords: string[] = [];
230
+ const legCoords = geometry.coordinates.reduce((acc, [lon, lat]) => {
231
+ if (!existingCoords.includes(`${lon}-${lat}`)) {
232
+ existingCoords = existingCoords.concat(`${lon}-${lat}`);
233
+ acc.push(new Coordinates(lat, lon));
234
+ }
235
+ return acc;
236
+ }, [] as Coordinates[]);
237
+
238
+ const stepsBuilder = new StepsBuilder().setStart(from).setEnd(to).setPathCoords(legCoords);
239
+ const transitMode = transitModeCorrespondance.get(section.transportMode) as TransitMode;
240
+
241
+ const unpackedIntructions = unpackJSON(section.details.instructions);
242
+
243
+ for (const instruction of unpackedIntructions) {
244
+ const { geometryIndex, roadLength, roadName } = instruction;
245
+ const coordinates = geometry.coordinates[geometryIndex];
246
+
247
+ const intermediateStep = {
248
+ name: roadName,
249
+ distance: roadLength,
250
+ coords: new Coordinates(coordinates[1], coordinates[0])
251
+ };
252
+
253
+ stepsBuilder.addStepInfo(intermediateStep);
254
+ }
255
+
256
+
257
+ const leg = new Leg({
258
+ transitMode,
259
+ duration: section.duration,
260
+ startTime: new Date(section.estimatedDatetimeOfDeparture).getTime(),
261
+ endTime: new Date(section.estimatedDatetimeOfArrival).getTime(),
262
+ start: {
263
+ coords: from
264
+ },
265
+ end: {
266
+ coords: to
267
+ },
268
+ coords: legCoords,
269
+ steps: stepsBuilder.build()
270
+ });
271
+ legs.push(leg);
272
+ }
273
+
274
+ const itinerary = new Itinerary({
275
+ duration: route.duration,
276
+ startTime: new Date(route.estimatedDatetimeOfDeparture).getTime(),
277
+ endTime: new Date(route.estimatedDatetimeOfArrival).getTime(),
278
+ origin: waypointToCoordinates(route.waypoints[0]),
279
+ destination: waypointToCoordinates(last(route.waypoints)),
280
+ legs
281
+ });
282
+
283
+ itineraries.push(itinerary);
284
+ }
285
+
286
+ return itineraries;
287
+ }
288
+ }
289
+
290
+ export default new GeoveloRemoteRouter();
@@ -11,6 +11,7 @@ import { areTransitAndTravelModeConsistent, type TransitMode } from '../../model
11
11
  import { type MinStepInfo } from '../../model/Step.js';
12
12
  import { RouterRequest } from '../../model/RouterRequest.js';
13
13
  import { RemoteRoutingError } from '../../RoutingError.js';
14
+ import GeoveloRemoteRouter from '../geovelo/GeoveloRemoteRouter.js';
14
15
 
15
16
  type IdfmCoordinates = { lat: number | string, lon: number | string };
16
17
  type IdfmPath = {
@@ -182,6 +183,12 @@ class IdfmRemoteRouter extends RemoteRouter {
182
183
 
183
184
 
184
185
  async getItineraries(endpointUrl: string, routerRequest: RouterRequest) {
186
+ const { travelMode } = routerRequest;
187
+ if (travelMode === 'BIKE') {
188
+ // IDFM does not support bike routing, we use Geovelo instead
189
+ return GeoveloRemoteRouter.getItineraries('https://idfm.getwemap.com/marketplace/computedroutes', routerRequest);
190
+ }
191
+
185
192
  const url = this.getURL(endpointUrl, routerRequest);
186
193
 
187
194
  const res = await (fetch(url, {