mobility-toolbox-js 1.7.1 → 1.7.4

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.
@@ -10,6 +10,27 @@ import RoutingAPI from '../../api/routing/RoutingAPI';
10
10
  import Control from '../../common/controls/Control';
11
11
  import RoutingLayer from '../layers/RoutingLayer';
12
12
 
13
+ // Examples for a single hop:
14
+ // basel sbb a station named "basel sbb"
15
+ // ZUE, station "Zürich HB" by its common abbreviation
16
+ // Zürich Hauptbahnhof or HBF Zürich are all valid synonyms für "Zürich HB"
17
+ // @47.37811,8.53935 a station at position 47.37811, 8.53935
18
+ // @47.37811,8.53935$4 track 4 in a station at position 47.37811, 8.53935
19
+ // zürich hb@47.37811,8.53935$8 track 8 in station "Zürich HB" at position 47.37811, 8.53935
20
+ const REGEX_VIA_POINT =
21
+ /^([^@$!\n]*)(@?([\d.]+),([\d.]+))?(\$?([a-zA-Z0-9]{0,2}))$/;
22
+
23
+ // Examples for a single hop:
24
+ //
25
+ // 47.37811,8.53935 a position 47.37811, 8.53935
26
+ const REGEX_VIA_POINT_COORD = /^([\d.]+),([\d.]+)$/;
27
+
28
+ // Examples for a single hop:
29
+ //
30
+ // !8596126 a station with id 8596126
31
+ // !8596126$4 a station with id 8596126
32
+ const REGEX_VIA_POINT_STATION_ID = /^!([^$]*)(\$?([a-zA-Z0-9]{0,2}))$/;
33
+
13
34
  const getFlatCoordinatesFromSegments = (segmentArray) => {
14
35
  const coords = [];
15
36
  segmentArray.forEach((seg) => {
@@ -37,6 +58,8 @@ const getFlatCoordinatesFromSegments = (segmentArray) => {
37
58
  * @classproperty {Array.<Array<graph="osm", minZoom=0, maxZoom=99>>} graphs - Array of routing graphs and min/max zoom levels. If you use the control in combination with the [geOps Maps API](https://developer.geops.io/apis/maps/), you may want to use the optimal level of generalizations: "[['gen4', 0, 8], ['gen3', 8, 9], ['gen2', 9, 11], ['gen1', 11, 13], ['osm', 13, 99]]"
38
59
  * @classproperty {string} mot - Mean of transport to be used for routing.
39
60
  * @classproperty {object} routingApiParams - object of additional parameters to pass to the routing api request.
61
+ * @classproperty {object} snapToClosestStation - If true, the routing will snap the coordinate to the closest station. Default to false.
62
+ * @classproperty {boolean} useRawViaPoints - Experimental property. Wen true, it allows the user to add via points using different kind of string. See "via" parameter defined by the [geOps Routing API](https://developer.geops.io/apis/routing/). Default to false, only array of coordinates and station's id are supported as via points.
40
63
  * @classproperty {RoutingLayer|Layer} routingLayer - Layer for adding route features.
41
64
  * @classproperty {function} onRouteError - Callback on error.
42
65
  * @classproperty {boolean} loading - True if the control is requesting the backend.
@@ -90,6 +113,12 @@ class RoutingControl extends Control {
90
113
  /** @ignore */
91
114
  this.routingApiParams = options.routingApiParams || {};
92
115
 
116
+ /** @ignore */
117
+ this.useRawViaPoints = options.useRawViaPoints || false;
118
+
119
+ /** @ignore */
120
+ this.snapToClosestStation = options.snapToClosestStation || false;
121
+
93
122
  /** @ignore */
94
123
  this.cacheStationData = {};
95
124
 
@@ -106,8 +135,7 @@ class RoutingControl extends Control {
106
135
  this.segments = [];
107
136
 
108
137
  /** @ignore */
109
- this.stopsApiUrl =
110
- options.stopsApiUrl || 'https://api.geops.io/stops/v1/lookup/';
138
+ this.stopsApiUrl = options.stopsApiUrl || 'https://api.geops.io/stops/v1/';
111
139
 
112
140
  /** @ignore */
113
141
  this.api = new RoutingAPI({
@@ -139,8 +167,14 @@ class RoutingControl extends Control {
139
167
 
140
168
  /** @ignore */
141
169
  this.viaPoints = [];
170
+
171
+ /** @ignore */
142
172
  this.onMapClick = this.onMapClick.bind(this);
173
+
174
+ /** @ignore */
143
175
  this.onModifyEnd = this.onModifyEnd.bind(this);
176
+
177
+ /** @ignore */
144
178
  this.onModifyStart = this.onModifyStart.bind(this);
145
179
 
146
180
  /** @ignore */
@@ -173,9 +207,13 @@ class RoutingControl extends Control {
173
207
  * @param {number} index Integer representing the index of the added viaPoint.
174
208
  * @param {number} [overwrite=0] Marks the number of viaPoints that are removed at the specified index on add.
175
209
  */
176
- addViaPoint(coordinates, index = this.viaPoints.length, overwrite = 0) {
210
+ addViaPoint(
211
+ coordinatesOrString,
212
+ index = this.viaPoints.length,
213
+ overwrite = 0,
214
+ ) {
177
215
  /* Add/Insert/Overwrite viapoint and redraw route */
178
- this.viaPoints.splice(index, overwrite, coordinates);
216
+ this.viaPoints.splice(index, overwrite, coordinatesOrString);
179
217
  this.drawRoute();
180
218
  this.dispatchEvent({
181
219
  type: 'change:route',
@@ -247,12 +285,15 @@ class RoutingControl extends Control {
247
285
 
248
286
  const formattedViaPoints = this.viaPoints.map((viaPoint) => {
249
287
  if (Array.isArray(viaPoint)) {
288
+ const projection = this.map.getView().getProjection();
250
289
  // viaPoint is a coordinate
251
290
  // Coordinates need to be reversed as required by the backend RoutingAPI
252
- return [toLonLat(viaPoint)[1], toLonLat(viaPoint)[0]];
291
+ const [lon, lat] = toLonLat(viaPoint, projection);
292
+ return this.snapToClosestStation ? [`@${lat}`, lon] : [lat, lon];
253
293
  }
254
- // viaPoint is a UID
255
- return `!${viaPoint}`;
294
+
295
+ // viaPoint is a string to use as it is
296
+ return this.useRawViaPoints ? viaPoint : `!${viaPoint}`;
256
297
  });
257
298
 
258
299
  this.loading = true;
@@ -344,36 +385,118 @@ class RoutingControl extends Control {
344
385
  }
345
386
 
346
387
  /**
347
- * Draw a via point.
388
+ * Draw a via point. This function can parse all the possibilitiies
348
389
  *
349
390
  * @private
350
391
  */
351
392
  drawViaPoint(viaPoint, idx) {
352
393
  const pointFeature = new Feature();
353
394
  pointFeature.set('viaPointIdx', idx);
395
+
396
+ // The via point is a coordinate using the current map's projection
354
397
  if (Array.isArray(viaPoint)) {
355
398
  pointFeature.setGeometry(new Point(viaPoint));
356
- return this.routingLayer.olLayer.getSource().addFeature(pointFeature);
399
+ this.routingLayer.olLayer.getSource().addFeature(pointFeature);
400
+ return Promise.resolve(pointFeature);
357
401
  }
358
- return fetch(
359
- `${this.stopsApiUrl}${viaPoint.split('$')[0]}?key=${this.stopsApiKey}`,
360
- )
361
- .then((res) => res.json())
362
- .then((stationData) => {
363
- const { coordinates } = stationData.features[0].geometry;
364
- this.cacheStationData[viaPoint] = fromLonLat(coordinates);
365
- pointFeature.setGeometry(new Point(fromLonLat(coordinates)));
366
- this.routingLayer.olLayer.getSource().addFeature(pointFeature);
367
- })
368
- .catch((error) => {
369
- // Dispatch error event and execute error function
370
- this.dispatchEvent({
371
- type: 'error',
372
- target: this,
402
+
403
+ // Possibility to parse:
404
+ //
405
+ // !8596126 a station with id 8596126
406
+ // !8596126$4 a station with id 8596126
407
+ if (!this.useRawViaPoints || REGEX_VIA_POINT_STATION_ID.test(viaPoint)) {
408
+ let stationId;
409
+ let track;
410
+ if (this.useRawViaPoints) {
411
+ [, stationId, , track] = REGEX_VIA_POINT_STATION_ID.exec(viaPoint);
412
+ } else {
413
+ [stationId, track] = viaPoint.split('$');
414
+ }
415
+
416
+ return fetch(
417
+ `${this.stopsApiUrl}lookup/${stationId}?key=${this.stopsApiKey}`,
418
+ )
419
+ .then((res) => res.json())
420
+ .then((stationData) => {
421
+ const { coordinates } = stationData.features[0].geometry;
422
+ this.cacheStationData[viaPoint] = fromLonLat(coordinates);
423
+ pointFeature.set('viaPointTrack', track);
424
+ pointFeature.setGeometry(new Point(fromLonLat(coordinates)));
425
+ this.routingLayer.olLayer.getSource().addFeature(pointFeature);
426
+ return pointFeature;
427
+ })
428
+ .catch((error) => {
429
+ // Dispatch error event and execute error function
430
+ this.dispatchEvent({
431
+ type: 'error',
432
+ target: this,
433
+ });
434
+ this.onRouteError(error, this);
435
+ this.loading = false;
373
436
  });
374
- this.onRouteError(error, this);
375
- this.loading = false;
376
- });
437
+ }
438
+
439
+ // Only when this.useRawViaPoints is true.
440
+ // Possibility to parse:
441
+ //
442
+ // 47.37811,8.53935 a position 47.37811, 8.53935
443
+ if (this.useRawViaPoints && REGEX_VIA_POINT_COORD.test(viaPoint)) {
444
+ const [lat, lon] = REGEX_VIA_POINT_COORD.exec(viaPoint);
445
+ const coordinates = fromLonLat(
446
+ [parseFloat(lon), parseFloat(lat)],
447
+ this.map.getView().getProjection(),
448
+ );
449
+ pointFeature.setGeometry(new Point(coordinates));
450
+ this.routingLayer.olLayer.getSource().addFeature(pointFeature);
451
+ return Promise.resolve(pointFeature);
452
+ }
453
+
454
+ // Only when this.useRawViaPoints is true.
455
+ // It will parse the via point to find some name, id, track coordinates.
456
+ //
457
+ // Possibility to parse:
458
+ //
459
+ // @47.37811,8.53935 a station at position 47.37811, 8.53935
460
+ // @47.37811,8.53935$4 track 4 in a station at position 47.37811, 8.53935
461
+ // zürich hb@47.37811,8.53935$8 track 8 in station "Zürich HB" at position 47.37811, 8.53935
462
+ const [, stationName, , lat, lon, , track] = REGEX_VIA_POINT.exec(viaPoint);
463
+
464
+ if (lon && lat) {
465
+ const coordinates = fromLonLat(
466
+ [parseFloat(lon), parseFloat(lat)],
467
+ this.map.getView().getProjection(),
468
+ );
469
+ pointFeature.set('viaPointTrack', track);
470
+ pointFeature.setGeometry(new Point(coordinates));
471
+ this.routingLayer.olLayer.getSource().addFeature(pointFeature);
472
+ return Promise.resolve(pointFeature);
473
+ }
474
+
475
+ if (stationName) {
476
+ return fetch(
477
+ `${this.stopsApiUrl}?key=${this.stopsApiKey}&q=${stationName}&limit=1`,
478
+ )
479
+ .then((res) => res.json())
480
+ .then((stationData) => {
481
+ const { coordinates } = stationData.features[0].geometry;
482
+ this.cacheStationData[viaPoint] = fromLonLat(coordinates);
483
+ pointFeature.set('viaPointTrack', track);
484
+ pointFeature.setGeometry(new Point(fromLonLat(coordinates)));
485
+ this.routingLayer.olLayer.getSource().addFeature(pointFeature);
486
+ return pointFeature;
487
+ })
488
+ .catch((error) => {
489
+ // Dispatch error event and execute error function
490
+ this.dispatchEvent({
491
+ type: 'error',
492
+ target: this,
493
+ });
494
+ this.onRouteError(error, this);
495
+ this.loading = false;
496
+ return null;
497
+ });
498
+ }
499
+ return Promise.resolve(null);
377
500
  }
378
501
 
379
502
  /**
@@ -1,5 +1,6 @@
1
1
  import fetch from 'jest-fetch-mock';
2
2
  import View from 'ol/View';
3
+ import qs from 'query-string';
3
4
  import Map from '../Map';
4
5
  import RoutingControl from './RoutingControl';
5
6
 
@@ -38,6 +39,8 @@ describe('RoutingControl', () => {
38
39
  test('should be activate by default', () => {
39
40
  const control = new RoutingControl();
40
41
  expect(control.active).toBe(true);
42
+ expect(control.snapToClosestStation).toBe(false);
43
+ expect(control.useRawViaPoints).toBe(false);
41
44
  });
42
45
 
43
46
  test('launch routing and add features', (done) => {
@@ -99,10 +102,10 @@ describe('RoutingControl', () => {
99
102
  .then(() => {
100
103
  // Should use correct URL
101
104
  expect(fetch.mock.calls[0][0]).toEqual(
102
- 'https://foo.ch/a4dca961d199ff76?key=foo',
105
+ 'https://foo.ch/lookup/a4dca961d199ff76?key=foo',
103
106
  );
104
107
  expect(fetch.mock.calls[1][0]).toEqual(
105
- 'https://foo.ch/e3666f03cba06b2b?key=foo',
108
+ 'https://foo.ch/lookup/e3666f03cba06b2b?key=foo',
106
109
  );
107
110
  expect(fetch.mock.calls[2][0]).toEqual(
108
111
  'https://foo.ch/?coord-punish=1000&coord-radius=100&elevation=false&graph=gen5&key=foo&mot=bus&resolve-hops=false&via=%21a4dca961d199ff76%7C%21e3666f03cba06b2b',
@@ -146,4 +149,68 @@ describe('RoutingControl', () => {
146
149
  done();
147
150
  });
148
151
  });
152
+
153
+ test('calls routing api with @ before the coordinates when snapToClosestStation is true', (done) => {
154
+ fetch.mockResponses(
155
+ [JSON.stringify(RoutingControlStation1), { status: 200 }],
156
+ [JSON.stringify(global.fetchRouteResponse), { status: 200 }],
157
+ );
158
+
159
+ const control = new RoutingControl({
160
+ apiKey: 'foo',
161
+ snapToClosestStation: true,
162
+ });
163
+ control.map = map;
164
+ expect(map.getTarget().querySelector('#ol-toggle-routing')).toBeDefined();
165
+ control.viaPoints = [
166
+ [950476.4055933182, 6003322.253698345],
167
+ [950389.0813034325, 6003656.659274571],
168
+ 'e3666f03cba06b2b',
169
+ ];
170
+ control
171
+ .drawRoute(control.viaPoints)
172
+ .then(() => {
173
+ const params = qs.parseUrl(fetch.mock.calls[1][0]).query;
174
+ expect(params.via).toBe(
175
+ '@47.3739194713294,8.538274823394632|@47.37595378493421,8.537490375951839|!e3666f03cba06b2b',
176
+ );
177
+ done();
178
+ })
179
+ .catch(() => {});
180
+ });
181
+
182
+ test('calls routing api with raw via points', (done) => {
183
+ fetch.mockResponses(
184
+ [JSON.stringify(RoutingControlStation1), { status: 200 }],
185
+ [JSON.stringify(RoutingControlStation2), { status: 200 }],
186
+ [JSON.stringify(global.fetchRouteResponse), { status: 200 }],
187
+ );
188
+
189
+ const control = new RoutingControl({
190
+ apiKey: 'foo',
191
+ useRawViaPoints: true,
192
+ });
193
+ control.map = map;
194
+ expect(map.getTarget().querySelector('#ol-toggle-routing')).toBeDefined();
195
+ control.viaPoints = [
196
+ '46.2,7.1',
197
+ '@46.2,7.1',
198
+ '@46.2,7$1',
199
+ 'station name$2', // will send a stops request fo the station name
200
+ 'station name@46.2,7', // will use the coordinate
201
+ 'stationname@46.2,7.7$3', // will use the coordinate
202
+ '!stationid', // will send a stops lookup request fo the station id
203
+ [950389, 6003656],
204
+ ];
205
+ control
206
+ .drawRoute(control.viaPoints)
207
+ .then(() => {
208
+ const params = qs.parseUrl(fetch.mock.calls[2][0]).query;
209
+ expect(params.via).toBe(
210
+ '46.2,7.1|@46.2,7.1|@46.2,7$1|station name$2|station name@46.2,7|stationname@46.2,7.7$3|!stationid|47.375949774398805,8.537489645590679',
211
+ );
212
+ done();
213
+ })
214
+ .catch(() => {});
215
+ });
149
216
  });
@@ -61,6 +61,18 @@ describe('TrajservLayer', () => {
61
61
  expect(clone).toBeInstanceOf(TralisLayer);
62
62
  });
63
63
 
64
+ test('should use the sort function.', () => {
65
+ const fn = () => true;
66
+ const laye = new TralisLayer({
67
+ url: 'ws://localhost:1234',
68
+ apiKey: 'apiKey',
69
+ sort: fn,
70
+ });
71
+ expect(laye).toBeInstanceOf(TralisLayer);
72
+ expect(laye.useDelayStyle).toBe(false);
73
+ expect(laye.sort).toBe(fn);
74
+ });
75
+
64
76
  test('should set a default sort function if useDelayStyle is used.', () => {
65
77
  const laye = new TralisLayer({
66
78
  url: 'ws://localhost:1234',
@@ -82,7 +94,7 @@ describe('TrajservLayer', () => {
82
94
  expect(trajectories).toEqual([red, yellow, cancelled, green2, green, gray]);
83
95
  });
84
96
 
85
- test('should override the defaulrt sort function when useDelayStyle is used.', () => {
97
+ test('should override the default sort function when useDelayStyle is used.', () => {
86
98
  const laye = new TralisLayer({
87
99
  url: 'ws://localhost:1234',
88
100
  apiKey: 'apiKey',
@@ -103,4 +115,31 @@ describe('TrajservLayer', () => {
103
115
  trajectories.sort(laye.sort);
104
116
  expect(trajectories).toEqual([cancelled, green2, red, yellow, green, gray]);
105
117
  });
118
+
119
+ test('should use filter function.', () => {
120
+ const fn = () => true;
121
+ const laye = new TralisLayer({
122
+ url: 'ws://localhost:1234',
123
+ apiKey: 'apiKey',
124
+ useDelayStyle: true,
125
+ filter: fn, // reverse the array
126
+ });
127
+ expect(laye).toBeInstanceOf(TralisLayer);
128
+ expect(laye.useDelayStyle).toBe(true);
129
+ expect(laye.filter).toBe(fn);
130
+ });
131
+
132
+ test('should override filter function if operator, tripNumber, regexPublishedLineName is set.', () => {
133
+ const fn = () => true;
134
+ const laye = new TralisLayer({
135
+ url: 'ws://localhost:1234',
136
+ apiKey: 'apiKey',
137
+ useDelayStyle: true,
138
+ filter: fn, // reverse the array
139
+ publishedLineName: '.*',
140
+ });
141
+ expect(laye).toBeInstanceOf(TralisLayer);
142
+ expect(laye.useDelayStyle).toBe(true);
143
+ expect(laye.filter).not.toBe(fn);
144
+ });
106
145
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mobility-toolbox-js",
3
3
  "license": "MIT",
4
4
  "description": "Toolbox for JavaScript applications in the domains of mobility and logistics.",
5
- "version": "1.7.1",
5
+ "version": "1.7.4",
6
6
  "main": "index.js",
7
7
  "module": "module.js",
8
8
  "dependencies": {