@wemap/geo 7.2.0 → 7.3.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.
@@ -0,0 +1,3003 @@
1
+ import { wrap, deg2rad, rad2deg, Quaternion, Vector3, positiveMod, Rotations, diffAngleLines } from '@wemap/maths';
2
+ import Logger from '@wemap/logger';
3
+
4
+ const Constants = {
5
+ R_MAJOR: 6378137.0,
6
+ R_MINOR: 6356752.3142,
7
+ EARTH_GRAVITY: 9.80665,
8
+
9
+ /**
10
+ * latitude and longitude epsilon in degrees
11
+ * 1e-8° correspond to ~1mm at latitude = 0
12
+ */
13
+ EPS_DEG_MM: 1e-8,
14
+
15
+ /**
16
+ * epsilon in meters which corresponds to 1 millimeter
17
+ */
18
+ EPS_MM: 1e-3
19
+ };
20
+
21
+ Constants.ELLIPSOID_FLATNESS = (Constants.R_MAJOR - Constants.R_MINOR) / Constants.R_MAJOR;
22
+
23
+ Constants.ECCENTRICITY = Math.sqrt(Constants.ELLIPSOID_FLATNESS * (2 - Constants.ELLIPSOID_FLATNESS));
24
+
25
+ Constants.R_MAJOR_2 = Constants.R_MAJOR * Constants.R_MAJOR;
26
+ Constants.R_MAJOR_4 = Constants.R_MAJOR_2 * Constants.R_MAJOR_2;
27
+ Constants.R_MINOR_2 = Constants.R_MINOR * Constants.R_MINOR;
28
+ Constants.R_MINOR_4 = Constants.R_MINOR_2 * Constants.R_MINOR_2;
29
+ Constants.ECCENTRICITY_2 = Constants.ECCENTRICITY * Constants.ECCENTRICITY;
30
+ Constants.CIRCUMFERENCE = Constants.R_MAJOR * 2 * Math.PI;
31
+
32
+ /**
33
+ * A Level is the representation of a building floor number
34
+ * A level can be a simple number (val) or a range (low, up)
35
+ * To know if a level is a range or a number, isRange can be used
36
+ */
37
+ class Level {
38
+
39
+ /** @type {number?} */
40
+ val = null;
41
+
42
+ /** @type {boolean} */
43
+ isRange = false;
44
+
45
+ /** @type {number?} */
46
+ low = null;
47
+
48
+ /** @type {number?} */
49
+ up = null;
50
+
51
+ /**
52
+ * Level constructor
53
+ * 1 argument: level is not a range and first argument is the level
54
+ * 2 arguments: level is a range
55
+ * @param {number} arg1 if arg2: low value, otherwise: level
56
+ * @param {number} arg2 (optional) up value
57
+ */
58
+ constructor(arg1, arg2) {
59
+ if (typeof arg1 !== 'number' || isNaN(arg1)) {
60
+ throw new Error('first argument is mandatory');
61
+ }
62
+ if (typeof arg2 === 'number' && !isNaN(arg2)) {
63
+ if (arg1 === arg2) {
64
+ this.isRange = false;
65
+ this.val = arg1;
66
+ } else {
67
+ this.isRange = true;
68
+ this.low = Math.min(arg1, arg2);
69
+ this.up = Math.max(arg1, arg2);
70
+ }
71
+ } else if (typeof arg2 === 'undefined') {
72
+ this.isRange = false;
73
+ this.val = arg1;
74
+ } else {
75
+ throw new Error('second argument is not a number');
76
+ }
77
+ }
78
+
79
+ clone() {
80
+ if (this.isRange) {
81
+ return new Level(this.low, this.up);
82
+ }
83
+ return new Level(this.val);
84
+ }
85
+
86
+ /**
87
+ * Create a level from a string
88
+ * @param {string} str level in str format (eg. 1, -2, 1;2, -2;3, 2;-1, 0.5;1 ...)
89
+ */
90
+ static fromString(str) {
91
+
92
+ if (typeof str !== 'string') {
93
+ return null;
94
+ }
95
+
96
+ if (!isNaN(Number(str))) {
97
+ return new Level(parseFloat(str));
98
+ }
99
+
100
+ const splited = str.split(';');
101
+ if (splited.length === 2) {
102
+ if (!isNaN(Number(splited[0])) && !isNaN(Number(splited[1]))) {
103
+ return new Level(parseFloat(splited[0]), parseFloat(splited[1]));
104
+ }
105
+ }
106
+
107
+ Logger.warn('Cannot parse following level: ' + str);
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Returns if the level is inside the given level
113
+ * @param {Level} level the container level
114
+ */
115
+ isInside(level) {
116
+ return Level.contains(level, this);
117
+ }
118
+
119
+ /**
120
+ * Returns if the level is inside the given level
121
+ * @param {Level} level the container level
122
+ */
123
+ contains(level) {
124
+ return Level.contains(this, level);
125
+ }
126
+
127
+
128
+ /**
129
+ * Returns if a level is contained in another
130
+ * @param {Level} container The container level
131
+ * @param {Level} targeted The targeted level
132
+ */
133
+ static contains(container, targeted) {
134
+
135
+ if (container === targeted) {
136
+ return true;
137
+ }
138
+
139
+ if (!container || !targeted) {
140
+ return false;
141
+ }
142
+
143
+ if (!container.isRange) {
144
+ if (targeted.isRange) {
145
+ return false;
146
+ }
147
+ return container.val === targeted.val;
148
+ }
149
+
150
+ return container.up >= (targeted.isRange ? targeted.up : targeted.val)
151
+ && container.low <= (targeted.isRange ? targeted.low : targeted.val);
152
+ }
153
+
154
+ /**
155
+ * Retrieve the intersection of two levels
156
+ * @param {Level} other The other level
157
+ */
158
+ intersect(other) {
159
+ return Level.intersect(this, other);
160
+ }
161
+
162
+ /**
163
+ * Retrieve the intersection of two levels
164
+ * @param {Level} first The first level
165
+ * @param {Level} second The second level
166
+ */
167
+ static intersect(first, second) {
168
+
169
+ if (first === second) {
170
+ if (first instanceof Level) {
171
+ return first.clone();
172
+ }
173
+ return null;
174
+ }
175
+
176
+ if (!second) {
177
+ return null;
178
+ }
179
+
180
+ if (!first) {
181
+ return null;
182
+ }
183
+
184
+ if (first.isRange && !second.isRange) {
185
+ if (first.contains(second)) {
186
+ return second.clone();
187
+ }
188
+ return null;
189
+ }
190
+ if (!first.isRange && second.isRange) {
191
+ if (second.contains(first)) {
192
+ return first.clone();
193
+ }
194
+ return null;
195
+ }
196
+ if (first.isRange && second.isRange) {
197
+ const up = Math.min(first.up, second.up);
198
+ const low = Math.max(first.low, second.low);
199
+ if (up < low) {
200
+ return null;
201
+ }
202
+ return new Level(low, up);
203
+ }
204
+ return first.val === second.val ? first : null;
205
+ }
206
+
207
+ /**
208
+ * Retrieve the union of two levels
209
+ * @param {Level} other The other level
210
+ */
211
+ union(other) {
212
+ return Level.union(this, other);
213
+ }
214
+
215
+ /**
216
+ * Retrieve the union of two levels
217
+ * @param {Level} other The other level
218
+ */
219
+ static union(first, second) {
220
+
221
+ if (first === second) {
222
+ if (first instanceof Level) {
223
+ return first.clone();
224
+ }
225
+ return null;
226
+ }
227
+
228
+ if (!second) {
229
+ return first.clone();
230
+ }
231
+
232
+ if (!first) {
233
+ return second.clone();
234
+ }
235
+
236
+ let low, up;
237
+ if (!first.isRange && !second.isRange) {
238
+ low = Math.min(first.val, second.val);
239
+ up = Math.max(first.val, second.val);
240
+ } else if (first.isRange && !second.isRange) {
241
+ low = Math.min(first.low, second.val);
242
+ up = Math.max(first.up, second.val);
243
+ } else if (!first.isRange && second.isRange) {
244
+ low = Math.min(second.low, first.val);
245
+ up = Math.max(second.up, first.val);
246
+ } else {
247
+ /* if (first.isRange && second.isRange) */
248
+ low = Math.min(second.low, first.low);
249
+ up = Math.max(second.up, first.up);
250
+ }
251
+
252
+ if (low === up) {
253
+ return new Level(low);
254
+ }
255
+ return new Level(low, up);
256
+ }
257
+
258
+ multiplyBy(factor) {
259
+ if (this.isRange) {
260
+ this.low *= factor;
261
+ this.up *= factor;
262
+ } else {
263
+ this.val *= factor;
264
+ }
265
+ return this;
266
+ }
267
+
268
+ toString() {
269
+ return this.isRange ? this.low + ';' + this.up : String(this.val);
270
+ }
271
+
272
+ static equalsTo(first, second) {
273
+
274
+ if (first === second) {
275
+ return true;
276
+ }
277
+ if (!first && !second) {
278
+ return true;
279
+ }
280
+ if (!first && second) {
281
+ return false;
282
+ }
283
+ if (first && !second) {
284
+ return false;
285
+ }
286
+ if (!first.isRange && second.isRange) {
287
+ return false;
288
+ }
289
+ if (first.isRange && !second.isRange) {
290
+ return false;
291
+ }
292
+ if (first.isRange && second.isRange) {
293
+ return first.low === second.low && first.up === second.up;
294
+ }
295
+ // !first.isRange && !second.isRange
296
+ return first.val === second.val;
297
+
298
+ }
299
+
300
+ static diff(first, second) {
301
+
302
+ if (first === null || second === null) {
303
+ return null;
304
+ }
305
+
306
+ if (!first.isRange && !second.isRange) {
307
+ return second.val - first.val;
308
+ }
309
+
310
+ if (first.isRange && !second.isRange) {
311
+ if (first.low === second.val) {
312
+ return second.val - first.up;
313
+ }
314
+ if (first.up === second.val) {
315
+ return second.val - first.low;
316
+ }
317
+ return null;
318
+ }
319
+
320
+ if (second.isRange && !first.isRange) {
321
+ if (first.val === second.low) {
322
+ return second.up - first.val;
323
+ }
324
+ if (first.val === second.up) {
325
+ return second.low - first.val;
326
+ }
327
+ return null;
328
+ }
329
+
330
+ if (Level.equalsTo(first, second)) {
331
+ return 0;
332
+ }
333
+
334
+ return null;
335
+ }
336
+
337
+ }
338
+
339
+ /**
340
+ * A Coordinates position using at least latitude (lat) and longitude (lng).
341
+ * Optionnal fields are: altitude (alt) and level.
342
+ *
343
+ * Basic geo methods are directly accessibles from here:
344
+ * distanceTo, bearingTo, toEcef...
345
+ *
346
+ * /!\ This class has been adapted to use earth as a sphere and not as an ellipsoid
347
+ * /!\ So, this class does not stricly represent WGS84 coordinates anymore
348
+ * /!\ This modifications have been made for computational improvements.
349
+ */
350
+ class Coordinates {
351
+
352
+ autoWrap = true;
353
+
354
+ constructor(lat, lng, alt, level) {
355
+ this.lat = lat;
356
+ this.lng = lng;
357
+ this.alt = alt;
358
+ this.level = level;
359
+ this._ecef = null;
360
+ }
361
+
362
+ get lat() {
363
+ return this._lat;
364
+ }
365
+
366
+ get latitude() {
367
+ return this._lat;
368
+ }
369
+
370
+ get lng() {
371
+ return this._lng;
372
+ }
373
+
374
+ get longitude() {
375
+ return this._lng;
376
+ }
377
+
378
+ /**
379
+ * alt does not denote the altitude of a point but its height from
380
+ * the "level" field (if defined) or from the ground
381
+ */
382
+ get alt() {
383
+ return this._alt;
384
+ }
385
+
386
+ get level() {
387
+ return this._level;
388
+ }
389
+
390
+ set lat(lat) {
391
+ if (typeof lat === 'number' && Math.abs(lat) <= 90) {
392
+ this._lat = lat;
393
+ } else {
394
+ throw new Error('lat argument is not in [-90; 90]');
395
+ }
396
+ this._ecef = null;
397
+ }
398
+
399
+ set latitude(lat) {
400
+ throw new Error('Please use Coordinate#lat setter instead of Coordinate#latitude');
401
+ }
402
+
403
+ set lng(lng) {
404
+ if (typeof lng === 'number') {
405
+ this._lng = lng;
406
+ if (this.autoWrap) {
407
+ this.wrap();
408
+ }
409
+ } else {
410
+ throw new Error('lng argument is not a number');
411
+ }
412
+ this._ecef = null;
413
+ }
414
+
415
+ set longitude(lng) {
416
+ throw new Error('Please use Coordinate#lng setter instead of Coordinate#longitude');
417
+ }
418
+
419
+ set alt(alt) {
420
+ if (typeof alt === 'number') {
421
+ this._alt = alt;
422
+ } else {
423
+ if (typeof alt !== 'undefined' && alt !== null) {
424
+ throw new Error('alt argument is not a finite number');
425
+ }
426
+ this._alt = null;
427
+ }
428
+ this._ecef = null;
429
+ }
430
+
431
+ set level(level) {
432
+ if (level instanceof Level) {
433
+ this._level = level;
434
+ } else {
435
+ if (typeof level !== 'undefined' && level !== null) {
436
+ throw new Error('level argument is not a Level object');
437
+ }
438
+ this._level = null;
439
+ }
440
+ }
441
+
442
+ clone() {
443
+ const output = new Coordinates(this.lat, this.lng, this.alt);
444
+ if (this.level) {
445
+ output.level = this.level.clone();
446
+ }
447
+ return output;
448
+ }
449
+
450
+ wrap() {
451
+ if (this._lng <= -180 || this._lng > 180) {
452
+ this._lng = wrap(this._lng, -180, 180);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Compares two Coordinates
458
+ * @param {Coordinates} pos1 position 1
459
+ * @param {Coordinates} pos2 position 2
460
+ * @param {Number} eps latitude and longitude epsilon in degrees (default: 1e-8 [~1mm at lat=0])
461
+ * @param {Number} epsAlt altitude epsilon in meters (default: 1e-3 [= 1mm])
462
+ */
463
+ static equalsTo(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
464
+
465
+ // Handle null comparison
466
+ if (pos1 === null && pos1 === pos2) {
467
+ return true;
468
+ }
469
+
470
+ if (!(pos1 instanceof Coordinates) || !(pos2 instanceof Coordinates)) {
471
+ return false;
472
+ }
473
+
474
+ return Math.abs(pos2.lat - pos1.lat) < eps
475
+ && Math.abs(pos2.lng - pos1.lng) < eps
476
+ && (pos1.alt === pos2.alt
477
+ || pos1.alt !== null && pos2.alt !== null
478
+ && Math.abs(pos2.alt - pos1.alt) < epsAlt)
479
+ && Level.equalsTo(pos1.level, pos2.level);
480
+ }
481
+
482
+ equalsTo(other) {
483
+ return Coordinates.equalsTo(this, other);
484
+ }
485
+
486
+ destinationPoint(distance, bearing, elevation) {
487
+ const newPoint = this.clone();
488
+ newPoint.move(distance, bearing, elevation);
489
+ return newPoint;
490
+ }
491
+
492
+ // Source: http://www.movable-type.co.uk/scripts/latlong.html#destPoint
493
+ move(distance, bearing, elevation) {
494
+
495
+ const dR = distance / Constants.R_MAJOR;
496
+ const cosDr = Math.cos(dR);
497
+ const sinDr = Math.sin(dR);
498
+
499
+ const phi1 = deg2rad(this.lat);
500
+ const lambda1 = deg2rad(this.lng);
501
+
502
+ const phi2 = Math.asin(
503
+ Math.sin(phi1) * cosDr
504
+ + Math.cos(phi1) * sinDr * Math.cos(bearing)
505
+ );
506
+ const lambda2 = lambda1 + Math.atan2(
507
+ Math.sin(bearing) * sinDr * Math.cos(phi1),
508
+ cosDr - Math.sin(phi1) * Math.sin(phi2)
509
+ );
510
+
511
+ this.lat = rad2deg(phi2);
512
+ this.lng = rad2deg(lambda2);
513
+
514
+ if (typeof elevation === 'number') {
515
+ if (this.alt === null) {
516
+ throw new Error('Point altitude is not defined');
517
+ }
518
+ this.alt += elevation;
519
+ }
520
+
521
+
522
+ return this;
523
+ }
524
+
525
+ /**
526
+ * Returns a distance between two points in meters
527
+ * @param {Coordinates} location2 latitude / longitude point
528
+ * @return {Number} distance in meters
529
+ */
530
+ distanceTo(location2) {
531
+ const lat1 = this.lat;
532
+ const lng1 = this.lng;
533
+
534
+ const lat2 = location2.lat;
535
+ const lng2 = location2.lng;
536
+
537
+ const dlat = deg2rad(lat2 - lat1);
538
+ const dlng = deg2rad(lng2 - lng1);
539
+
540
+ const dlngsin = Math.sin(dlng / 2);
541
+ const dlatsin = Math.sin(dlat / 2);
542
+ const lat1rad = deg2rad(lat1);
543
+ const lat1cos = Math.cos(lat1rad);
544
+ const lat2rad = deg2rad(lat2);
545
+ const lat2cos = Math.cos(lat2rad);
546
+ const angle = dlatsin * dlatsin + lat1cos * lat2cos * dlngsin * dlngsin;
547
+
548
+ // arctangent
549
+ const tangy = Math.sqrt(angle);
550
+ const tangx = Math.sqrt(1 - angle);
551
+ const cosn = 2 * Math.atan2(tangy, tangx);
552
+
553
+ return Constants.R_MAJOR * cosn;
554
+ }
555
+
556
+ static distanceBetween(point1, point2) {
557
+ return point1.distanceTo(point2);
558
+ }
559
+
560
+ bearingTo(location2) {
561
+ const lat1 = deg2rad(this.lat);
562
+ const lat2 = deg2rad(location2.lat);
563
+ const diffLng = deg2rad(location2.lng - this.lng);
564
+
565
+ return Math.atan2(Math.sin(diffLng) * Math.cos(lat2),
566
+ Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(diffLng));
567
+ }
568
+
569
+ static bearingTo(point1, point2) {
570
+ return point1.bearingTo(point2);
571
+ }
572
+
573
+
574
+ /**
575
+ * ECEF Transformations
576
+ * Here we used a light version of ECEF considering earth
577
+ * as a sphere instead of an ellipse
578
+ */
579
+
580
+ get enuToEcefRotation() {
581
+ const rot1 = Quaternion.fromAxisAngle([0, 0, 1], Math.PI / 2 + deg2rad(this.lng));
582
+ const rot2 = Quaternion.fromAxisAngle([1, 0, 0], Math.PI / 2 - deg2rad(this.lat));
583
+ return Quaternion.multiply(rot1, rot2);
584
+ }
585
+
586
+ get ecefToEnuRotation() {
587
+ const rot1 = Quaternion.fromAxisAngle([1, 0, 0], deg2rad(this.lat) - Math.PI / 2);
588
+ const rot2 = Quaternion.fromAxisAngle([0, 0, 1], -deg2rad(this.lng) - Math.PI / 2);
589
+ return Quaternion.multiply(rot1, rot2);
590
+ }
591
+
592
+ // https://gist.github.com/klucar/1536194
593
+ // Adapted for spherical formula
594
+ get ecef() {
595
+
596
+ if (!this._ecef) {
597
+ const lat = deg2rad(this.lat);
598
+ const lng = deg2rad(this.lng);
599
+ const alt = this.alt || 0;
600
+
601
+ const x = (Constants.R_MAJOR + alt) * Math.cos(lat) * Math.cos(lng);
602
+ const y = (Constants.R_MAJOR + alt) * Math.cos(lat) * Math.sin(lng);
603
+ const z = (Constants.R_MAJOR + alt) * Math.sin(lat);
604
+
605
+ this._ecef = [x, y, z];
606
+ }
607
+
608
+ return this._ecef;
609
+ }
610
+
611
+ static fromECEF(ecef) {
612
+
613
+ const x = ecef[0];
614
+ const y = ecef[1];
615
+ const z = ecef[2];
616
+
617
+ const p = Math.sqrt(x ** 2 + y ** 2);
618
+
619
+ let lng = Math.atan2(y, x);
620
+ const lat = Math.atan2(z, p);
621
+ const alt = p / Math.cos(lat) - Constants.R_MAJOR;
622
+
623
+ lng = lng % (2 * Math.PI);
624
+
625
+ const newPoint = new Coordinates(rad2deg(lat), rad2deg(lng), alt);
626
+ newPoint._ecef = ecef;
627
+ return newPoint;
628
+ }
629
+
630
+
631
+ // https://stackoverflow.com/questions/1299567/how-to-calculate-distance-from-a-point-to-a-line-segment-on-a-sphere
632
+ // Adapted to ECEF
633
+ getSegmentProjection(p1, p2) {
634
+
635
+ const a = Vector3.normalize(p1.ecef);
636
+ const b = Vector3.normalize(p2.ecef);
637
+ const c = Vector3.normalize(this.ecef);
638
+
639
+ const G = Vector3.cross(a, b);
640
+ if (Vector3.norm(G) === 0) {
641
+ return null;
642
+ }
643
+
644
+ const F = Vector3.cross(c, G);
645
+ const t = Vector3.normalize(Vector3.cross(G, F));
646
+
647
+ const posECEF = Vector3.multiplyScalar(t, Constants.R_MAJOR);
648
+ const poseCoordinates = Coordinates.fromECEF(posECEF);
649
+
650
+ // poseCoordinates.alt is not 0 here due to the ECEF transformation residual.
651
+ // So if p1.alt and p2.alt are defined we take the middle elevation between p1 and p2.
652
+ // Otherwise we remove alt from projection because the residual has no sense.
653
+ let alt;
654
+ if (p1.alt !== null && p2.alt !== null) {
655
+ // This formula is maybe not the best one.
656
+ alt = (p1.alt + p2.alt) / 2;
657
+ }
658
+ const projection = new Coordinates(poseCoordinates.lat, poseCoordinates.lng,
659
+ alt, Level.intersect(p1.level, p2.level));
660
+
661
+ if (Math.abs((p1.distanceTo(p2) - p1.distanceTo(projection) - p2.distanceTo(projection))) > Constants.EPS_MM) {
662
+ return null;
663
+ }
664
+
665
+ return projection;
666
+ }
667
+
668
+ /**
669
+ * Input / Output
670
+ */
671
+
672
+ toString() {
673
+ let str = '[' + this._lat.toFixed(7) + ', ' + this._lng.toFixed(7);
674
+ if (this._alt !== null) {
675
+ str += ', ' + this._alt.toFixed(2);
676
+ }
677
+ if (this._level !== null) {
678
+ str += ', [' + this._level.toString() + ']';
679
+ }
680
+ str += ']';
681
+ return str;
682
+ }
683
+
684
+ toJson() {
685
+ const output = {
686
+ lat: this.lat,
687
+ lng: this.lng
688
+ };
689
+ if (this.alt !== null) {
690
+ output.alt = this.alt;
691
+ }
692
+ if (this.level !== null) {
693
+ output.level = this.level.toString();
694
+ }
695
+ return output;
696
+ }
697
+
698
+ static fromJson(json) {
699
+ return new Coordinates(json.lat, json.lng, json.alt, Level.fromString(json.level));
700
+ }
701
+
702
+ toCompressedJson() {
703
+ const output = [this.lat, this.lng];
704
+ if (this.alt !== null || this.level !== null) {
705
+ output.push(this.alt);
706
+ }
707
+ if (this.level !== null) {
708
+ output.push(this.level.toString());
709
+ }
710
+ return output;
711
+ }
712
+
713
+ static fromCompressedJson(json) {
714
+ const coords = new Coordinates(json[0], json[1]);
715
+ if (json.length > 2) {
716
+ coords.alt = json[2];
717
+ }
718
+ if (json.length > 3) {
719
+ coords.level = Level.fromString(json[3]);
720
+ }
721
+ return coords;
722
+ }
723
+ }
724
+
725
+ /**
726
+ * A Coordinates User Position is a Coordinates position with specific data related to user (bearing, time, accuracy)
727
+ */
728
+ class UserPosition extends Coordinates {
729
+
730
+ _time = null;
731
+ _accuracy = null;
732
+ _bearing = null;
733
+
734
+ constructor(lat, lng, alt, level, time, accuracy, bearing) {
735
+ super(lat, lng, alt, level);
736
+ this.time = time;
737
+ this.accuracy = accuracy;
738
+ this.bearing = bearing;
739
+ }
740
+
741
+ get time() {
742
+ return this._time;
743
+ }
744
+
745
+ set time(time) {
746
+ if (typeof time === 'number') {
747
+ this._time = time;
748
+ } else {
749
+ if (typeof time !== 'undefined' && time !== null) {
750
+ throw new Error('time argument is not a number');
751
+ }
752
+ this._time = null;
753
+ }
754
+ }
755
+
756
+
757
+ get accuracy() {
758
+ return this._accuracy;
759
+ }
760
+
761
+ set accuracy(accuracy) {
762
+ if (typeof accuracy === 'number' && accuracy >= 0) {
763
+ this._accuracy = accuracy;
764
+ } else {
765
+ if (typeof accuracy !== 'undefined' && accuracy !== null) {
766
+ throw new Error('accuracy argument is not a positive number');
767
+ }
768
+ this._accuracy = null;
769
+ }
770
+ }
771
+
772
+
773
+ get bearing() {
774
+ return this._bearing;
775
+ }
776
+
777
+ set bearing(bearing) {
778
+ if (typeof bearing === 'number') {
779
+ this._bearing = bearing % (2 * Math.PI);
780
+ } else {
781
+ if (typeof bearing !== 'undefined' && bearing !== null) {
782
+ throw new Error('bearing argument is not a number');
783
+ }
784
+ this._bearing = null;
785
+ }
786
+ }
787
+
788
+
789
+ // Create a UserPosition with lat, lng, alt from Coordinates coordinates and
790
+ // other fields from another UserPosition
791
+ static fromCoordinates(coordinates) {
792
+ return new UserPosition(coordinates.lat, coordinates.lng,
793
+ coordinates.alt, coordinates.level);
794
+ }
795
+
796
+ clone() {
797
+ const cloned = UserPosition.fromCoordinates(super.clone());
798
+ cloned.time = this.time;
799
+ cloned.accuracy = this.accuracy;
800
+ cloned.bearing = this.bearing;
801
+ return cloned;
802
+ }
803
+
804
+
805
+ /**
806
+ * Compares two UserPosition
807
+ * @param {UserPosition} pos1 position 1
808
+ * @param {UserPosition} pos2 position 2
809
+ * @param {Number} eps latitude and longitude epsilon in degrees (default: 1e-8 [~1mm at lat=0])
810
+ * @param {Number} epsAlt altitude epsilon in meters (default: 1e-3 [= 1mm])
811
+ */
812
+ static equalsTo(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
813
+
814
+ // Handle null comparison
815
+ if (pos1 === null && pos1 === pos2) {
816
+ return true;
817
+ }
818
+
819
+ if (!(pos1 instanceof UserPosition) || !(pos2 instanceof UserPosition)) {
820
+ return false;
821
+ }
822
+
823
+ if (!super.equalsTo(pos1, pos2, eps, epsAlt)) {
824
+ return false;
825
+ }
826
+
827
+ return pos1.time === pos2.time
828
+ && pos1.accuracy === pos2.accuracy
829
+ && pos1.bearing === pos2.bearing;
830
+ }
831
+
832
+ equalsTo(other) {
833
+ return UserPosition.equalsTo(this, other);
834
+ }
835
+
836
+
837
+ toJson() {
838
+ const output = super.toJson();
839
+ if (this.time !== null) {
840
+ output.time = this.time;
841
+ }
842
+ if (this.accuracy !== null) {
843
+ output.accuracy = this.accuracy;
844
+ }
845
+ if (this.bearing !== null) {
846
+ output.bearing = this.bearing;
847
+ }
848
+ return output;
849
+ }
850
+
851
+ static fromJson(json) {
852
+ const position = UserPosition.fromCoordinates(Coordinates.fromJson(json));
853
+ position.time = json.time;
854
+ position.accuracy = json.accuracy;
855
+ position.bearing = json.bearing;
856
+ return position;
857
+ }
858
+ }
859
+
860
+ /* eslint-disable max-statements */
861
+
862
+ /**
863
+ * Sample a route of Coordinates
864
+ * @param {Array.<Coordinates>} route ordered points
865
+ * @param {*} stepSize step size to sample
866
+ * @param {*} maxLength max route length to sample
867
+ */
868
+ function sampleRoute(route, stepSize = 0.7, startSampling = 0, length = Number.MAX_VALUE) {
869
+
870
+ const endSampling = startSampling + length;
871
+
872
+ const sampledRoute = [];
873
+
874
+ let lastSample = null;
875
+
876
+ let totalDistanceTraveled = 0;
877
+ let distanceToNextSample;
878
+ let startFound = false;
879
+
880
+ for (let segmentIndex = 0; segmentIndex < route.length - 1; segmentIndex++) {
881
+
882
+ const p1 = route[segmentIndex];
883
+ const p2 = route[segmentIndex + 1];
884
+ const segmentSize = p1.distanceTo(p2);
885
+ const segmentBearing = p1.bearingTo(p2);
886
+
887
+ let distanceTraveledOnSegment = 0;
888
+
889
+ if (!startFound) {
890
+ if (startSampling < totalDistanceTraveled + segmentSize) {
891
+ startFound = true;
892
+ distanceToNextSample = startSampling - totalDistanceTraveled;
893
+ } else {
894
+ totalDistanceTraveled += segmentSize;
895
+ continue;
896
+ }
897
+ }
898
+
899
+ lastSample = p1;
900
+ while (distanceTraveledOnSegment + distanceToNextSample < segmentSize
901
+ && totalDistanceTraveled + distanceToNextSample <= endSampling) {
902
+
903
+ const newPoint = lastSample.destinationPoint(distanceToNextSample, segmentBearing);
904
+ newPoint.bearing = segmentBearing;
905
+ sampledRoute.push(newPoint);
906
+ lastSample = newPoint;
907
+
908
+ distanceTraveledOnSegment += distanceToNextSample;
909
+ totalDistanceTraveled += distanceToNextSample;
910
+ distanceToNextSample = stepSize;
911
+ }
912
+
913
+ if (totalDistanceTraveled + distanceToNextSample > endSampling) {
914
+ break;
915
+ }
916
+
917
+ const rest = segmentSize - distanceTraveledOnSegment;
918
+ totalDistanceTraveled += rest;
919
+ distanceToNextSample -= rest;
920
+ }
921
+
922
+ return sampledRoute;
923
+ }
924
+
925
+ /**
926
+ * Trim a route of Coordinates
927
+ * @param {Array.<Coordinates>} route ordered points
928
+ * @param {Coordinates} startPosition position where the trim starts. startPosition has to be on the route.
929
+ * @param {*} maxLength max route length
930
+ */
931
+ function trimRoute(route, startPosition = route[0], length = Number.MAX_VALUE) {
932
+
933
+ const newRoute = [];
934
+ let previousPoint;
935
+
936
+ let currentPointIndex;
937
+ let cumulativeDistance = 0;
938
+
939
+ if (route.length <= 1) {
940
+ throw new Error('Route must have at least 2 points');
941
+ }
942
+
943
+ for (currentPointIndex = 1; currentPointIndex < route.length; currentPointIndex++) {
944
+
945
+ const p1 = route[currentPointIndex - 1];
946
+ const p2 = route[currentPointIndex];
947
+
948
+ if (Coordinates.equalsTo(startPosition, p1)) {
949
+ newRoute.push(p1);
950
+ previousPoint = p1;
951
+ break;
952
+ }
953
+
954
+ const proj = startPosition.getSegmentProjection(p1, p2);
955
+ if (proj && Coordinates.equalsTo(startPosition, proj) && !proj.equalsTo(p2)) {
956
+ newRoute.push(proj);
957
+ previousPoint = proj;
958
+ break;
959
+ }
960
+ }
961
+
962
+ if (!newRoute.length) {
963
+ throw new Error('startPosition is not on the route');
964
+ }
965
+
966
+ while (currentPointIndex < route.length) {
967
+ const currentPoint = route[currentPointIndex];
968
+ const dist = previousPoint.distanceTo(currentPoint);
969
+ if (cumulativeDistance + dist >= length
970
+ || Math.abs(cumulativeDistance + dist - length) <= Constants.EPS_MM) {
971
+ const bearing = previousPoint.bearingTo(currentPoint);
972
+ const remainingLength = length - cumulativeDistance;
973
+ const end = previousPoint.destinationPoint(remainingLength, bearing);
974
+ newRoute.push(end);
975
+ break;
976
+ }
977
+ newRoute.push(currentPoint);
978
+ previousPoint = currentPoint;
979
+ cumulativeDistance += dist;
980
+ currentPointIndex++;
981
+ }
982
+
983
+ return newRoute;
984
+ }
985
+
986
+ /**
987
+ * @param {Coordinates[]} coords
988
+ * @param {number} precision
989
+ * @returns {Coordinates[]}
990
+ */
991
+ function simplifyRoute(coords, precisionAngle = deg2rad(5)) {
992
+
993
+ const isClosed = (coords[0].equalsTo(coords[coords.length - 1]));
994
+
995
+ let newRoute = coords.slice(0, coords.length - (isClosed ? 1 : 0));
996
+
997
+ const len = newRoute.length;
998
+ for (let i = isClosed ? 0 : 1; i < len; i++) {
999
+
1000
+ const p0 = coords[positiveMod(i - 1, len)];
1001
+ const p1 = coords[i];
1002
+ const p2 = coords[positiveMod(i + 1, len)];
1003
+
1004
+ const seg1Dir = p0.bearingTo(p1);
1005
+ const seg2Dir = p1.bearingTo(p2);
1006
+
1007
+ if (Math.abs(seg2Dir - seg1Dir) < precisionAngle) {
1008
+ newRoute = newRoute.filter(coord => coord !== p1);
1009
+ }
1010
+ }
1011
+
1012
+ if (isClosed) {
1013
+ newRoute.push(newRoute[0]);
1014
+ }
1015
+
1016
+ return newRoute;
1017
+ }
1018
+
1019
+ /**
1020
+ * @param {GeolocationPosition} geolocationPosition
1021
+ * @returns {UserPosition}
1022
+ */
1023
+ function geolocationPositionToUserPosition(geolocationPosition) {
1024
+ if (geolocationPosition === null) {
1025
+ return null;
1026
+ }
1027
+
1028
+ const { latitude, longitude, accuracy, heading } = geolocationPosition.coords;
1029
+
1030
+ const userPosition = new UserPosition(latitude, longitude);
1031
+ userPosition.time = geolocationPosition.timestamp;
1032
+ userPosition.accuracy = accuracy;
1033
+ userPosition.bearing = heading ? deg2rad(heading) : null;
1034
+ return userPosition;
1035
+ }
1036
+
1037
+ var Utils = /*#__PURE__*/Object.freeze({
1038
+ __proto__: null,
1039
+ sampleRoute: sampleRoute,
1040
+ trimRoute: trimRoute,
1041
+ simplifyRoute: simplifyRoute,
1042
+ geolocationPositionToUserPosition: geolocationPositionToUserPosition
1043
+ });
1044
+
1045
+ class BoundingBox {
1046
+
1047
+ northEast;
1048
+ southWest;
1049
+
1050
+ constructor(northEast, southWest) {
1051
+ this.northEast = northEast || null;
1052
+ this.southWest = southWest || null;
1053
+
1054
+ if (this.northEast && this.southWest && this.getNorth() < this.getSouth()) {
1055
+ throw new Error('Incorrect bounding box');
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * Returns the geographical coordinate equidistant from the bounding box's corners.
1061
+ *
1062
+ * @returns {Coordinates} The bounding box's center.
1063
+ */
1064
+ get center() {
1065
+ const latCenter = (this.southWest.lat + this.northEast.lat) / 2;
1066
+ const lngCenter = (this.northEast.lng + this.southWest.lng) / 2;
1067
+ return new Coordinates(latCenter, lngCenter);
1068
+ }
1069
+
1070
+ /**
1071
+ * Check if a point is contained in the bounding box.
1072
+ *
1073
+ * @returns {Coordinates} The point to analyze.
1074
+ */
1075
+ contains(point) {
1076
+ return point.lat <= this.northEast.lat
1077
+ && point.lat >= this.southWest.lat
1078
+ && point.lng <= this.northEast.lng
1079
+ && point.lng >= this.southWest.lng;
1080
+ }
1081
+
1082
+ /**
1083
+ * Extend the bounds to include a given LngLat or LngLatBounds.
1084
+ *
1085
+ * @param {Coordinates|BoundingBox} obj object to extend to
1086
+ * @returns {BoundingBox} `this`
1087
+ */
1088
+ extend(obj) {
1089
+ const sw = this.southWest,
1090
+ ne = this.northEast;
1091
+ let sw2, ne2;
1092
+
1093
+ if (obj instanceof Coordinates) {
1094
+ sw2 = obj;
1095
+ ne2 = obj;
1096
+
1097
+ } else if (obj instanceof BoundingBox) {
1098
+ sw2 = obj.southWest;
1099
+ ne2 = obj.northEast;
1100
+
1101
+ if (!sw2 || !ne2) {
1102
+ return this;
1103
+ }
1104
+ } else {
1105
+ throw new Error('Unknown parameter');
1106
+ }
1107
+
1108
+ if (!sw && !ne) {
1109
+ this.southWest = new Coordinates(sw2.lat, sw2.lng);
1110
+ this.northEast = new Coordinates(ne2.lat, ne2.lng);
1111
+
1112
+ } else {
1113
+ this.southWest = new Coordinates(
1114
+ Math.min(sw2.lat, sw.lat),
1115
+ Math.min(sw2.lng, sw.lng)
1116
+ );
1117
+ this.northEast = new Coordinates(
1118
+ Math.max(ne2.lat, ne.lat),
1119
+ Math.max(ne2.lng, ne.lng)
1120
+ );
1121
+ }
1122
+
1123
+ return this;
1124
+ }
1125
+
1126
+ /**
1127
+ * This method extends the bounding box with a value in meters
1128
+ * /*\ This method is not precise as distance differs in function of latitude
1129
+ * @param {!number} measure in meters
1130
+ */
1131
+ extendsWithMeasure(measure) {
1132
+
1133
+ if (typeof measure !== 'number') {
1134
+ throw new Error('measure is not a number');
1135
+ }
1136
+
1137
+ this.northEast = this.northEast
1138
+ .destinationPoint(measure, 0)
1139
+ .move(measure, Math.PI / 2);
1140
+
1141
+ this.southWest = this.southWest.clone()
1142
+ .destinationPoint(measure, -Math.PI / 2)
1143
+ .destinationPoint(measure, Math.PI);
1144
+
1145
+ return this;
1146
+ }
1147
+
1148
+ /**
1149
+ * Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
1150
+ * For example, a ratio of 0.5 extends the bounds by 50% in each direction.
1151
+ * Negative values will retract the bounds.
1152
+ * @param {Number} bufferRatio
1153
+ * @returns {BoundingBox} `this`
1154
+ */
1155
+ pad(bufferRatio) {
1156
+ const sw = this.southWest;
1157
+ const ne = this.northEast;
1158
+
1159
+ const heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio;
1160
+ const widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
1161
+
1162
+ this.southWest = new Coordinates(sw.lat - heightBuffer, sw.lng - widthBuffer);
1163
+ this.northEast = new Coordinates(ne.lat + heightBuffer, ne.lng + widthBuffer);
1164
+
1165
+ return this;
1166
+ }
1167
+
1168
+ /**
1169
+ * Returns the southwest corner of the bounding box.
1170
+ *
1171
+ * @returns {Coordinates} The southwest corner of the bounding box.
1172
+ */
1173
+ getSouthWest() {
1174
+ return this.southWest;
1175
+ }
1176
+
1177
+ /**
1178
+ * Returns the northeast corner of the bounding box.
1179
+ *
1180
+ * @returns {Coordinates} The northeast corner of the bounding box.
1181
+ */
1182
+ getNorthEast() {
1183
+ return this.northEast;
1184
+ }
1185
+
1186
+ /**
1187
+ * Returns the northwest corner of the bounding box.
1188
+ *
1189
+ * @returns {Coordinates} The northwest corner of the bounding box.
1190
+ */
1191
+ getNorthWest() {
1192
+ return new Coordinates(this.getNorth(), this.getWest());
1193
+ }
1194
+
1195
+ /**
1196
+ * Returns the southeast corner of the bounding box.
1197
+ *
1198
+ * @returns {LngLat} The southeast corner of the bounding box.
1199
+ */
1200
+ getSouthEast() {
1201
+ return new Coordinates(this.getSouth(), this.getEast());
1202
+ }
1203
+
1204
+ /**
1205
+ * Returns the west edge of the bounding box.
1206
+ *
1207
+ * @returns {number} The west edge of the bounding box.
1208
+ */
1209
+ getWest() {
1210
+ return this.southWest.lng;
1211
+ }
1212
+
1213
+ /**
1214
+ * Returns the south edge of the bounding box.
1215
+ *
1216
+ * @returns {number} The south edge of the bounding box.
1217
+ */
1218
+ getSouth() {
1219
+ return this.southWest.lat;
1220
+ }
1221
+
1222
+ /**
1223
+ * Returns the east edge of the bounding box.
1224
+ *
1225
+ * @returns {number} The east edge of the bounding box.
1226
+ */
1227
+ getEast() {
1228
+ return this.northEast.lng;
1229
+ }
1230
+
1231
+ /**
1232
+ * Returns the north edge of the bounding box.
1233
+ *
1234
+ * @returns {number} The north edge of the bounding box.
1235
+ */
1236
+ getNorth() {
1237
+ return this.northEast.lat;
1238
+ }
1239
+
1240
+ static equalsTo(bb1, bb2) {
1241
+ return Coordinates.equalsTo(bb1.northEast, bb2.northEast)
1242
+ && Coordinates.equalsTo(bb1.southWest, bb2.southWest);
1243
+ }
1244
+
1245
+ equalsTo(other) {
1246
+ return BoundingBox.equalsTo(this, other);
1247
+ }
1248
+
1249
+ /**
1250
+ * Create a BoundingBox from a WSEN array
1251
+ * @param {Number[4]} bounds a WSEN array
1252
+ * @returns {BoundingBox} the corresponding BoundingBox
1253
+ */
1254
+ static fromArray(bounds) {
1255
+ return new BoundingBox(
1256
+ new Coordinates(bounds[3], bounds[2]),
1257
+ new Coordinates(bounds[1], bounds[0])
1258
+ );
1259
+ }
1260
+
1261
+ /**
1262
+ * Returns the WSEN array
1263
+ * @returns {Number[4]} the WSEN array
1264
+ */
1265
+ toArray() {
1266
+ return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()];
1267
+ }
1268
+ }
1269
+
1270
+ class RelativePosition {
1271
+
1272
+ _x = 0;
1273
+ _y = 0;
1274
+ _z = 0;
1275
+ _time = null;
1276
+ _accuracy = null;
1277
+ _bearing = null;
1278
+
1279
+ constructor(x, y, z, time, accuracy, bearing) {
1280
+ this.x = x;
1281
+ this.y = y;
1282
+ this.z = z;
1283
+ this.time = time;
1284
+ this.accuracy = accuracy;
1285
+ this.bearing = bearing;
1286
+ }
1287
+
1288
+ get x() {
1289
+ return this._x;
1290
+ }
1291
+
1292
+ set x(x) {
1293
+ if (typeof x === 'number') {
1294
+ this._x = x;
1295
+ } else {
1296
+ throw new Error('x argument is not a number');
1297
+ }
1298
+ }
1299
+
1300
+ get y() {
1301
+ return this._y;
1302
+ }
1303
+
1304
+ set y(y) {
1305
+ if (typeof y === 'number') {
1306
+ this._y = y;
1307
+ } else {
1308
+ throw new Error('y argument is not a number');
1309
+ }
1310
+ }
1311
+
1312
+ get z() {
1313
+ return this._z;
1314
+ }
1315
+
1316
+ set z(z) {
1317
+ if (typeof z === 'number') {
1318
+ this._z = z;
1319
+ } else {
1320
+ throw new Error('z argument is not a number');
1321
+ }
1322
+ }
1323
+
1324
+ get time() {
1325
+ return this._time;
1326
+ }
1327
+
1328
+ set time(time) {
1329
+ if (typeof time === 'number') {
1330
+ this._time = time;
1331
+ } else {
1332
+ if (typeof time !== 'undefined' && time !== null) {
1333
+ throw new Error('time argument is not a number');
1334
+ }
1335
+ this._time = null;
1336
+ }
1337
+ }
1338
+
1339
+
1340
+ get accuracy() {
1341
+ return this._accuracy;
1342
+ }
1343
+
1344
+ set accuracy(accuracy) {
1345
+ if (typeof accuracy === 'number' && accuracy >= 0) {
1346
+ this._accuracy = accuracy;
1347
+ } else {
1348
+ if (typeof accuracy !== 'undefined' && accuracy !== null) {
1349
+ throw new Error('accuracy argument is not a positive number');
1350
+ }
1351
+ this._accuracy = null;
1352
+ }
1353
+ }
1354
+
1355
+
1356
+ get bearing() {
1357
+ return this._bearing;
1358
+ }
1359
+
1360
+ set bearing(bearing) {
1361
+ if (typeof bearing === 'number') {
1362
+ this._bearing = bearing % (2 * Math.PI);
1363
+ } else {
1364
+ if (typeof bearing !== 'undefined' && bearing !== null) {
1365
+ throw new Error('bearing argument is not a number');
1366
+ }
1367
+ this._bearing = null;
1368
+ }
1369
+ }
1370
+
1371
+ clone() {
1372
+ return new RelativePosition(this.x, this.y, this.z, this.time, this.accuracy, this.bearing);
1373
+ }
1374
+
1375
+
1376
+ /**
1377
+ * Compares two RelativePosition
1378
+ * @param {RelativePosition} pos1 position 1
1379
+ * @param {RelativePosition} pos2 position 2
1380
+ * @param {Number} eps x, y, z epsilon in meters (default: 1e-3 [= 1mm])
1381
+ */
1382
+ static equalsTo(pos1, pos2, eps = Constants.EPS_MM) {
1383
+
1384
+ // Handle null comparison
1385
+ if (pos1 === null && pos1 === pos2) {
1386
+ return true;
1387
+ }
1388
+
1389
+ if (!(pos1 instanceof RelativePosition) || !(pos2 instanceof RelativePosition)) {
1390
+ return false;
1391
+ }
1392
+
1393
+ return Math.abs(pos2.x - pos1.x) < eps
1394
+ && Math.abs(pos2.y - pos1.y) < eps
1395
+ && Math.abs(pos2.z - pos1.z) < eps
1396
+ && pos1.time === pos2.time
1397
+ && pos1.accuracy === pos2.accuracy
1398
+ && pos1.bearing === pos2.bearing;
1399
+ }
1400
+
1401
+ equalsTo(other) {
1402
+ return RelativePosition.equalsTo(this, other);
1403
+ }
1404
+
1405
+
1406
+ toJson() {
1407
+ const output = {
1408
+ x: this.x,
1409
+ y: this.y,
1410
+ z: this.z
1411
+ };
1412
+
1413
+ if (this.time !== null) {
1414
+ output.time = this.time;
1415
+ }
1416
+ if (this.accuracy !== null) {
1417
+ output.accuracy = this.accuracy;
1418
+ }
1419
+ if (this.bearing !== null) {
1420
+ output.bearing = this.bearing;
1421
+ }
1422
+ return output;
1423
+ }
1424
+
1425
+ static fromJson(json) {
1426
+ return new RelativePosition(json.x, json.y, json.z, json.time, json.accuracy, json.bearing);
1427
+ }
1428
+ }
1429
+
1430
+ /**
1431
+ * Position is defined in EUS (East-Up-South) frame with: x pointing to East, y pointing to Up, z pointing to South
1432
+ * This frame is close to ThreeJS / OpenGL frame.
1433
+ */
1434
+ class GeoRelativePosition extends RelativePosition { }
1435
+
1436
+ class Attitude {
1437
+
1438
+ _quaternion = [1, 0, 0, 0];
1439
+ _heading = null;
1440
+ _eulerAngles = null;
1441
+ _time = null;
1442
+ _accuracy = null;
1443
+
1444
+ /**
1445
+ *
1446
+ * @param {number[]} quaternion
1447
+ * @param {number} time
1448
+ * @param {number} accuracy
1449
+ */
1450
+ constructor(quaternion, time, accuracy) {
1451
+ this.quaternion = quaternion;
1452
+ this.time = time;
1453
+ this.accuracy = accuracy;
1454
+ }
1455
+
1456
+ static unitary() {
1457
+ return new Attitude([1, 0, 0, 0]);
1458
+ }
1459
+
1460
+ /**
1461
+ * @returns {number[]}
1462
+ */
1463
+ get quaternion() {
1464
+ return this._quaternion;
1465
+ }
1466
+
1467
+ /**
1468
+ * @param {number[]} quaternion
1469
+ */
1470
+ set quaternion(quaternion) {
1471
+ if (!Array.isArray(quaternion)
1472
+ || quaternion.length !== 4
1473
+ || Math.abs(1 - Quaternion.norm(quaternion)) > 1e-4) {
1474
+ throw new Error('quaternion is not a unit quaternion');
1475
+ }
1476
+ this._quaternion = quaternion;
1477
+ this._heading = null;
1478
+ this._eulerAngles = null;
1479
+ }
1480
+
1481
+ /**
1482
+ * @returns {number}
1483
+ */
1484
+ get time() {
1485
+ return this._time;
1486
+ }
1487
+
1488
+ /**
1489
+ * @param {number} time
1490
+ */
1491
+ set time(time) {
1492
+ if (typeof time === 'number') {
1493
+ this._time = time;
1494
+ } else {
1495
+ if (typeof time !== 'undefined' && time !== null) {
1496
+ throw new Error('time argument is not a number');
1497
+ }
1498
+ this._time = null;
1499
+ }
1500
+ }
1501
+
1502
+ /**
1503
+ * @returns {number}
1504
+ */
1505
+ get accuracy() {
1506
+ return this._accuracy;
1507
+ }
1508
+
1509
+ /**
1510
+ * @param {number} accuracy
1511
+ */
1512
+ set accuracy(accuracy) {
1513
+ if (typeof accuracy === 'number' && accuracy >= 0 && accuracy <= Math.PI) {
1514
+ this._accuracy = accuracy;
1515
+ } else {
1516
+ if (typeof accuracy !== 'undefined' && accuracy !== null) {
1517
+ throw new Error('accuracy argument (' + accuracy + ') is not in range [0; PI]');
1518
+ }
1519
+ this._accuracy = null;
1520
+ }
1521
+ }
1522
+
1523
+ /** @type {number[]} */
1524
+ get eulerAngles() {
1525
+ if (this._eulerAngles === null) {
1526
+ this._eulerAngles = Rotations.quaternionToEulerZXY(this.quaternion);
1527
+ }
1528
+ return this._eulerAngles;
1529
+ }
1530
+
1531
+ /** @type {number[]} */
1532
+ get eulerAnglesDegrees() {
1533
+ return this.eulerAngles.map(x => rad2deg(x));
1534
+ }
1535
+
1536
+ /** @type {number} */
1537
+ get heading() {
1538
+ if (this._heading === null) {
1539
+ let offset = 0;
1540
+ if (typeof (window) !== 'undefined' && window && window.orientation) {
1541
+ offset = deg2rad(window.orientation);
1542
+ }
1543
+ this._heading = Rotations.getHeadingFromQuaternion(this.quaternion) + offset;
1544
+ }
1545
+ return this._heading;
1546
+ }
1547
+
1548
+ /** @type {number} */
1549
+ get headingDegrees() {
1550
+ return rad2deg(this.heading);
1551
+ }
1552
+
1553
+ /**
1554
+ * Compares two Attitude
1555
+ * @param {Attitude} att1 attitude 1
1556
+ * @param {Attitude} att2 attitude 2
1557
+ */
1558
+ static equalsTo(att1, att2) {
1559
+
1560
+ // Handle null comparison
1561
+ if (att1 === null && att1 === att2) {
1562
+ // TODO not sure to return true here.
1563
+ return true;
1564
+ }
1565
+
1566
+ if (!(att1 instanceof Attitude) || !(att2 instanceof Attitude)) {
1567
+ return false;
1568
+ }
1569
+
1570
+ if (att1 === att2) {
1571
+ return true;
1572
+ }
1573
+
1574
+ return Quaternion.equalsTo(att1.quaternion, att2.quaternion);
1575
+ }
1576
+
1577
+ /**
1578
+ * @param {Attitude} other
1579
+ * @returns {boolean}
1580
+ */
1581
+ equalsTo(other) {
1582
+ return Attitude.equalsTo(this, other);
1583
+ }
1584
+
1585
+ /**
1586
+ * @returns {object}
1587
+ */
1588
+ toJson() {
1589
+ return this.quaternion;
1590
+ }
1591
+
1592
+ /**
1593
+ * @param {object} json
1594
+ * @returns {Attitude}
1595
+ */
1596
+ static fromJson(json) {
1597
+ return new Attitude(json);
1598
+ }
1599
+
1600
+ /**
1601
+ * @returns {Attitude}
1602
+ */
1603
+ clone() {
1604
+ return new Attitude(this.quaternion.slice(0), this.time, this.accuracy);
1605
+ }
1606
+ }
1607
+
1608
+ class AbsoluteHeading {
1609
+
1610
+ _heading = null;
1611
+ _time = null;
1612
+ _accuracy = null;
1613
+
1614
+ /**
1615
+ *
1616
+ * @param {Number} heading
1617
+ * @param {Number} time
1618
+ * @param {Number} accuracy
1619
+ */
1620
+ constructor(heading, time, accuracy) {
1621
+ this.heading = heading;
1622
+ this.time = time;
1623
+ this.accuracy = accuracy;
1624
+ }
1625
+
1626
+ /**
1627
+ * @returns {Number}
1628
+ */
1629
+ get heading() {
1630
+ return this._heading;
1631
+ }
1632
+
1633
+ /**
1634
+ * @param {Number} heading
1635
+ */
1636
+ set heading(heading) {
1637
+ if (typeof heading === 'number') {
1638
+ this._heading = heading;
1639
+ } else {
1640
+ throw new Error('heading argument is not a number');
1641
+ }
1642
+ }
1643
+
1644
+ /**
1645
+ * @returns {Number}
1646
+ */
1647
+ get time() {
1648
+ return this._time;
1649
+ }
1650
+
1651
+ /**
1652
+ * @param {Number} time
1653
+ */
1654
+ set time(time) {
1655
+ if (typeof time === 'number') {
1656
+ this._time = time;
1657
+ } else {
1658
+ if (typeof time !== 'undefined' && time !== null) {
1659
+ throw new Error('time argument is not a number');
1660
+ }
1661
+ this._time = null;
1662
+ }
1663
+ }
1664
+
1665
+ /**
1666
+ * @returns {Number}
1667
+ */
1668
+ get accuracy() {
1669
+ return this._accuracy;
1670
+ }
1671
+
1672
+ /**
1673
+ * @param {Number} accuracy
1674
+ */
1675
+ set accuracy(accuracy) {
1676
+ if (typeof accuracy === 'number' && accuracy >= 0 && accuracy <= Math.PI) {
1677
+ this._accuracy = accuracy;
1678
+ } else {
1679
+ if (typeof accuracy !== 'undefined' && accuracy !== null) {
1680
+ throw new Error('accuracy argument (' + accuracy + ') is not in range [0; PI]');
1681
+ }
1682
+ this._accuracy = null;
1683
+ }
1684
+ }
1685
+
1686
+ /**
1687
+ * @returns {Attitude}
1688
+ */
1689
+ toAttitude() {
1690
+ /**
1691
+ * Heading is given around z-axis in NED frame and our attitude in ENU frame, that is why
1692
+ * -1* is applied to heading.
1693
+ */
1694
+ return new Attitude(
1695
+ Quaternion.fromAxisAngle([0, 0, 1], -this.heading),
1696
+ this.time,
1697
+ this.accuracy
1698
+ );
1699
+ }
1700
+
1701
+
1702
+ /**
1703
+ * Compares two AbsoluteHeading
1704
+ * @param {AbsoluteHeading} heading1 heading 1
1705
+ * @param {AbsoluteHeading} heading2 heading 2
1706
+ */
1707
+ static equalsTo(heading1, heading2) {
1708
+
1709
+ // Handle null comparison
1710
+ if (heading1 === null && heading1 === heading2) {
1711
+ return true;
1712
+ }
1713
+
1714
+ if (!(heading1 instanceof AbsoluteHeading) || !(heading2 instanceof AbsoluteHeading)) {
1715
+ return false;
1716
+ }
1717
+
1718
+ return Math.abs(heading1.heading - heading2.heading) < 1e-8;
1719
+ }
1720
+
1721
+ equalsTo(other) {
1722
+ return AbsoluteHeading.equalsTo(this, other);
1723
+ }
1724
+
1725
+ toJson() {
1726
+ const output = { heading: this.heading };
1727
+ if (this.accuracy !== null) {
1728
+ output.accuracy = this.accuracy;
1729
+ }
1730
+ if (this.time !== null) {
1731
+ output.time = this.time;
1732
+ }
1733
+ return output;
1734
+ }
1735
+
1736
+ static fromJson({
1737
+ heading, time, accuracy
1738
+ }) {
1739
+ return new AbsoluteHeading(heading, time, accuracy);
1740
+ }
1741
+
1742
+ clone() {
1743
+ return new AbsoluteHeading(this.heading, this.time, this.accuracy);
1744
+ }
1745
+ }
1746
+
1747
+ /**
1748
+ * @template T
1749
+ */
1750
+ class GraphNode {
1751
+
1752
+ /** @type {Coordinates} */
1753
+ _coords;
1754
+
1755
+ /** @type {Edge<T>[]} */
1756
+ edges = [];
1757
+
1758
+ /** @type {?T} */
1759
+ builtFrom = null;
1760
+
1761
+ /** @type {boolean} */
1762
+ io = false;
1763
+
1764
+ /**
1765
+ * @param {Coordinates} coords
1766
+ * @param {T} builtFrom
1767
+ */
1768
+ constructor(coords, builtFrom = null) {
1769
+ this.coords = coords;
1770
+ this.builtFrom = builtFrom;
1771
+ this.edges = [];
1772
+ }
1773
+
1774
+ /** @type {!Coordinates} */
1775
+ get coords() {
1776
+ return this._coords;
1777
+ }
1778
+
1779
+ /** @type {!Coordinates} */
1780
+ set coords(coords) {
1781
+ if (!(coords instanceof Coordinates)) {
1782
+ throw new Error('coords is not a Coordinates');
1783
+ }
1784
+ this._coords = coords;
1785
+ }
1786
+
1787
+ /**
1788
+ * @param {GraphNode} other
1789
+ * @returns {number}
1790
+ */
1791
+ distanceTo(other) {
1792
+ return this.coords.distanceTo(other.coords);
1793
+ }
1794
+
1795
+ /**
1796
+ * @param {GraphNode} other
1797
+ * @returns {number}
1798
+ */
1799
+ bearingTo(other) {
1800
+ return this.coords.bearingTo(other.coords);
1801
+ }
1802
+
1803
+ /**
1804
+ * @param {GraphNode} other
1805
+ * @returns {boolean}
1806
+ */
1807
+ equalsTo(other) {
1808
+ return this.coords.equalsTo(other.coords)
1809
+ && this.builtFrom === other.builtFrom;
1810
+ }
1811
+
1812
+ /**
1813
+ * @returns {GraphNode}
1814
+ */
1815
+ clone() {
1816
+ const node = new GraphNode(this.coords);
1817
+ node.edges = this.edges.slice(0);
1818
+ node.builtFrom = this.builtFrom;
1819
+ node.io = this.io;
1820
+ return node;
1821
+ }
1822
+
1823
+ /**
1824
+ * Does not include "edges" and "builtFrom" properties
1825
+ * @returns {object}
1826
+ */
1827
+ toJson() {
1828
+ return this.coords.toCompressedJson();
1829
+ }
1830
+
1831
+ /**
1832
+ * @template T
1833
+ * @returns {GraphNode<T>}
1834
+ */
1835
+ static fromJson(json) {
1836
+ return new GraphNode(Coordinates.fromCompressedJson(json));
1837
+ }
1838
+
1839
+ generateLevelFromEdges() {
1840
+ let tmpLevel = null;
1841
+ for (let i = 0; i < this.edges.length; i++) {
1842
+ const edge = this.edges[i];
1843
+ if (edge.level) {
1844
+ if (!tmpLevel) {
1845
+ tmpLevel = edge.level.clone();
1846
+ } else {
1847
+ tmpLevel = tmpLevel.intersect(edge.level);
1848
+ if (!tmpLevel) {
1849
+ Logger.error('Error: Something bad happend during parsing: We cannot retrieve node level from adjacent ways: ' + this.coords);
1850
+ return false;
1851
+ }
1852
+ }
1853
+ }
1854
+ }
1855
+ this.coords.level = tmpLevel;
1856
+ return true;
1857
+ }
1858
+
1859
+
1860
+ /**
1861
+ * We suppose generateLevelFromEdges() was called before
1862
+ */
1863
+ inferNodeLevelByRecursion() {
1864
+ const { level } = this.coords;
1865
+ if (!level || !level.isRange) {
1866
+ return true;
1867
+ }
1868
+
1869
+ /**
1870
+ * We can infer node level only if this one have one edge attached
1871
+ */
1872
+ if (this.edges.length > 1) {
1873
+ return true;
1874
+ }
1875
+
1876
+ /**
1877
+ * This method looks for single level nodes recursively from a multi-level node
1878
+ * The result of this method is an union of all single level nodes found.
1879
+ * @param {GraphNode} node node to explore
1880
+ * @param {GraphNode[]} visitedNodes list of visited nodes
1881
+ */
1882
+ const lookForLevel = (node, visitedNodes) => {
1883
+
1884
+ visitedNodes.push(node);
1885
+
1886
+ if (!node.coords.level) {
1887
+ return null;
1888
+ }
1889
+
1890
+ if (!node.coords.level.isRange) {
1891
+ return node.coords.level;
1892
+ }
1893
+
1894
+ let tmpLevel = null;
1895
+ for (let i = 0; i < node.edges.length; i++) {
1896
+ const edge = node.edges[i];
1897
+ const otherNode = edge.node1 === node ? edge.node2 : edge.node1;
1898
+ if (!visitedNodes.includes(otherNode)) {
1899
+ tmpLevel = Level.union(lookForLevel(otherNode, visitedNodes), tmpLevel);
1900
+ }
1901
+ }
1902
+ return tmpLevel;
1903
+ };
1904
+
1905
+ const othersLevels = lookForLevel(this, []);
1906
+
1907
+ if (othersLevels !== null) {
1908
+ if (!othersLevels.isRange) {
1909
+ this.coords.level = new Level(othersLevels.val === level.low ? level.up : level.low);
1910
+ return true;
1911
+ }
1912
+ Logger.warn('Level of: ' + this.coords.toString() + ' cannot be decided');
1913
+ return false;
1914
+ }
1915
+
1916
+ return true;
1917
+ }
1918
+
1919
+ /**
1920
+ * We suppose generateLevelFromEdges() was called before
1921
+ */
1922
+ inferNodeLevelByNeighboors() {
1923
+ const { level } = this.coords;
1924
+ if (!level || !level.isRange) {
1925
+ return true;
1926
+ }
1927
+
1928
+ let tmpLevel = null;
1929
+ for (let i = 0; i < this.edges.length; i++) {
1930
+ const edge = this.edges[i];
1931
+ const otherNode = edge.node1 === this ? edge.node2 : edge.node1;
1932
+ tmpLevel = Level.union(otherNode.coords.level, tmpLevel);
1933
+ }
1934
+
1935
+ if (tmpLevel === null || !tmpLevel.isRange) {
1936
+ this.coords.level = new Level(tmpLevel.val === level.low ? level.up : level.low);
1937
+ }
1938
+
1939
+ return true;
1940
+ }
1941
+
1942
+ /**
1943
+ * Set node.io to true for nodes that make the link between
1944
+ * indoor and outdoor edges
1945
+ */
1946
+ checkIO() {
1947
+ this.io = this._coords.level !== null
1948
+ && this.edges.some(edge => edge.level === null);
1949
+ return true;
1950
+ }
1951
+
1952
+ /**
1953
+ * @param {GraphNode[]} nodes
1954
+ */
1955
+ static generateNodesLevels(nodes) {
1956
+ const success = nodes.reduce((acc, node) => acc && node.generateLevelFromEdges(), true);
1957
+ if (!success) {
1958
+ return false;
1959
+ }
1960
+
1961
+ // In some cases, node levels cannot be retrieve just using adjacent edges
1962
+ // (e.g stairs without network at one of its bounds)
1963
+ // To infer this node level, we use inferNodeLevelByRecursion()
1964
+ const res = nodes.reduce((acc, node) => acc
1965
+ && node.inferNodeLevelByNeighboors()
1966
+ && node.inferNodeLevelByRecursion()
1967
+ , true);
1968
+
1969
+ // Finally define nodes that are links between indoor and outdoor
1970
+ nodes.forEach(node => node.checkIO());
1971
+
1972
+ return res;
1973
+ }
1974
+ }
1975
+
1976
+ /**
1977
+ * @template T
1978
+ *
1979
+ * An Edge is a segment composed of two Node
1980
+ * An edge is mostly issued from an OsmWay, but this is not always the case.
1981
+ * For example, edges created by mapmatching.
1982
+ */
1983
+ class GraphEdge {
1984
+
1985
+ /** @type {GraphNode<T>} */
1986
+ _node1 = null;
1987
+
1988
+ /** @type {GraphNode<T>} */
1989
+ _node2 = null;
1990
+
1991
+ /** @type {?Level} */
1992
+ _level = null;
1993
+
1994
+ /** @type {?number} */
1995
+ _bearing;
1996
+
1997
+ /** @type {?number} */
1998
+ _length;
1999
+
2000
+ /** @type {boolean} */
2001
+ _computedSizeAndBearing = false;
2002
+
2003
+ /** @type {?T} */
2004
+ builtFrom = null;
2005
+
2006
+ /** @type {boolean} */
2007
+ isOneway = false;
2008
+
2009
+ /**
2010
+ * @param {!GraphNode} node1
2011
+ * @param {!GraphNode} node2
2012
+ * @param {?Level} level
2013
+ * @param {?T} builtFrom
2014
+ * @param {?string} name
2015
+ */
2016
+ constructor(node1, node2, level = null, builtFrom = null) {
2017
+ this.node1 = node1;
2018
+ this.node2 = node2;
2019
+ this.level = level;
2020
+ this.builtFrom = builtFrom;
2021
+ }
2022
+
2023
+ /** @type {!GraphNode<T>} */
2024
+ get node1() {
2025
+ return this._node1;
2026
+ }
2027
+
2028
+ /** @type {!GraphNode<T>} */
2029
+ set node1(node) {
2030
+
2031
+ if (!(node instanceof GraphNode)) {
2032
+ throw new TypeError('node1 is not a GraphNode');
2033
+ }
2034
+
2035
+ if (this._node1 !== null && this._node2 !== this._node1) {
2036
+ this._node1.edges = this._node1.edges.filter(edge => edge !== this);
2037
+ }
2038
+
2039
+ node.edges.push(this);
2040
+
2041
+ this._node1 = node;
2042
+ this._computedSizeAndBearing = false;
2043
+ }
2044
+
2045
+ /** @type {!GraphNode<T>} */
2046
+ get node2() {
2047
+ return this._node2;
2048
+ }
2049
+
2050
+ /** @type {!GraphNode<T>} */
2051
+ set node2(node) {
2052
+
2053
+ if (!(node instanceof GraphNode)) {
2054
+ throw new TypeError('node2 is not a GraphNode');
2055
+ }
2056
+
2057
+ if (this._node2 !== null && this._node2 !== this._node1) {
2058
+ this._node2.edges = this._node2.edges.filter(edge => edge !== this);
2059
+ }
2060
+
2061
+ node.edges.push(this);
2062
+
2063
+ this._node2 = node;
2064
+ this._computedSizeAndBearing = false;
2065
+ }
2066
+
2067
+ /** @type {?Level} */
2068
+ get level() {
2069
+ return this._level;
2070
+ }
2071
+
2072
+ /** @type {?Level} */
2073
+ set level(level) {
2074
+ if (level instanceof Level) {
2075
+ this._level = level;
2076
+ } else {
2077
+ if (typeof level !== 'undefined' && level !== null) {
2078
+ throw new Error('level argument is not a Level object');
2079
+ }
2080
+ this._level = null;
2081
+ }
2082
+ }
2083
+
2084
+ /**
2085
+ * Get edge bearing from node1 to node2
2086
+ * @type {number}
2087
+ */
2088
+ get bearing() {
2089
+ if (!this._computedSizeAndBearing) {
2090
+ this._computeSizeAndBearing();
2091
+ }
2092
+ return this._bearing;
2093
+ }
2094
+
2095
+ /**
2096
+ * get edge length
2097
+ * @type {number}
2098
+ */
2099
+ get length() {
2100
+ if (!this._computedSizeAndBearing) {
2101
+ this._computeSizeAndBearing();
2102
+ }
2103
+ return this._length;
2104
+ }
2105
+
2106
+ _computeSizeAndBearing() {
2107
+ this._length = this.node1.distanceTo(this.node2);
2108
+ this._bearing = this.node1.bearingTo(this.node2);
2109
+ this._computedSizeAndBearing = true;
2110
+ }
2111
+
2112
+ /**
2113
+ * @param {GraphEdge<T>} other
2114
+ * @returns {boolean}
2115
+ */
2116
+ equalsTo(other) {
2117
+
2118
+ if (this === other) {
2119
+ return true;
2120
+ }
2121
+
2122
+ if (!(other instanceof GraphEdge)) {
2123
+ return false;
2124
+ }
2125
+
2126
+ return other.node1.equalsTo(this.node1)
2127
+ && other.node2.equalsTo(this.node2)
2128
+ && Level.equalsTo(other.level, this.level)
2129
+ && other.isOneway === this.isOneway
2130
+ && other.builtFrom === this.builtFrom;
2131
+ }
2132
+
2133
+ /**
2134
+ * @returns {GraphEdge<T>}
2135
+ */
2136
+ clone() {
2137
+ const edge = new GraphEdge(this.node1, this.node2);
2138
+ edge.level = this.level;
2139
+ edge.isOneway = this.isOneway;
2140
+ edge.builtFrom = this.builtFrom;
2141
+ return edge;
2142
+ }
2143
+
2144
+ }
2145
+
2146
+ /**
2147
+ * @template T
2148
+ * @param {GraphEdge<T>[]} edges
2149
+ * @param {GraphNode<T>} node1
2150
+ * @param {GraphNode<T>} node2
2151
+ * @returns {?GraphEdge<T>}
2152
+ */
2153
+ function getEdgeByNodes(edges, node1, node2) {
2154
+ return edges.find(edge =>
2155
+ node1 === edge.node1 && node2 === edge.node2
2156
+ || node2 === edge.node1 && node1 === edge.node2
2157
+ );
2158
+ }
2159
+
2160
+ var GraphUtils = /*#__PURE__*/Object.freeze({
2161
+ __proto__: null,
2162
+ getEdgeByNodes: getEdgeByNodes
2163
+ });
2164
+
2165
+ /**
2166
+ * @template T
2167
+ *
2168
+ * A typical network with nodes (Node) and edges (Edge)
2169
+ */
2170
+ class Network {
2171
+
2172
+ /** @type {GraphNode<T>[]} */
2173
+ nodes;
2174
+
2175
+ /** @type {GraphEdge<T>[]} */
2176
+ edges;
2177
+
2178
+ /**
2179
+ * @template T
2180
+ * @param {GraphNode<T>[]} nodes
2181
+ * @param {GraphEdge<T>[]} edges
2182
+ */
2183
+ constructor(nodes, edges) {
2184
+ this.nodes = Array.isArray(nodes) ? nodes : [];
2185
+ this.edges = Array.isArray(edges) ? edges : [];
2186
+ }
2187
+
2188
+ /**
2189
+ * @template T
2190
+ * @param {Coordinates} coords
2191
+ * @returns {?GraphNode<T>}
2192
+ */
2193
+ getNodeByCoords(coords) {
2194
+ return this.nodes.find(node => node.coords.equalsTo(coords));
2195
+ }
2196
+
2197
+ /**
2198
+ * @template T
2199
+ * @param {GraphNode<T>} node1
2200
+ * @param {GraphNode<T>} node2
2201
+ * @returns {?GraphEdge<T>}
2202
+ */
2203
+ getEdgeByNodes(node1, node2) {
2204
+ return getEdgeByNodes(this.edges, node1, node2);
2205
+ }
2206
+
2207
+
2208
+ /**
2209
+ * @param {?number} extendedMeasure
2210
+ * @returns {BoundingBox}
2211
+ */
2212
+ getBoundingBox(extendedMeasure) {
2213
+ const boundingBox = this.nodes.reduce(
2214
+ (acc, node) => acc.extend(node.coords), new BoundingBox()
2215
+ );
2216
+ if (extendedMeasure) {
2217
+ boundingBox.extendsWithMeasure(extendedMeasure);
2218
+ }
2219
+ return boundingBox;
2220
+ }
2221
+
2222
+ /**
2223
+ * @param {function} _nodeToStringFn
2224
+ * @param {function} _edgeToStringFn
2225
+ * @returns {string}
2226
+ */
2227
+ toDetailedString(_nodeToStringFn, _edgeToStringFn) {
2228
+
2229
+ let nodeToStringFn = _nodeToStringFn;
2230
+ if (!nodeToStringFn) {
2231
+ nodeToStringFn = node => `${node.builtFrom}`;
2232
+ }
2233
+
2234
+ let edgeToStringFn = _edgeToStringFn;
2235
+ if (!_edgeToStringFn) {
2236
+ edgeToStringFn = edge => `${edge.builtFrom}`;
2237
+ }
2238
+
2239
+ let output
2240
+ = '--- Network ---\n'
2241
+ + `Nodes: ${this.nodes.length}\n`
2242
+ + `Edges: ${this.edges.length}\n`
2243
+ + '---\n'
2244
+ + 'Nodes\n';
2245
+ this.nodes.forEach(node => {
2246
+ output += `${nodeToStringFn(node)} [edges: ${node.edges.length}]\n`;
2247
+ });
2248
+ output += '---\n'
2249
+ + 'Edges\n';
2250
+ this.edges.forEach(edge => {
2251
+ output += `${edgeToStringFn(edge)} `;
2252
+ output += `[${nodeToStringFn(edge.node1)} -- ${nodeToStringFn(edge.node2)}]\n`;
2253
+ });
2254
+ output += '---';
2255
+ return output;
2256
+ }
2257
+
2258
+ /**
2259
+ * @returns {object}
2260
+ */
2261
+ toCompressedJson() {
2262
+ return {
2263
+ nodes: this.nodes.map(node => node.toJson()),
2264
+ edges: this.edges.map(edge => {
2265
+ const output = [
2266
+ this.nodes.indexOf(edge.node1),
2267
+ this.nodes.indexOf(edge.node2)
2268
+ ];
2269
+ if (edge.level !== null) {
2270
+ output.push(edge.level.toString());
2271
+ }
2272
+ if (edge.isOneway) {
2273
+ if (edge.level === null) {
2274
+ output.push(null);
2275
+ }
2276
+ output.push(true);
2277
+ }
2278
+ return output;
2279
+ })
2280
+ };
2281
+ }
2282
+
2283
+ /**
2284
+ * @param {object} json
2285
+ * @returns {Network}
2286
+ */
2287
+ static fromCompressedJson(json) {
2288
+
2289
+ const network = new Network();
2290
+
2291
+ network.nodes = json.nodes.map(GraphNode.fromJson);
2292
+
2293
+ network.edges = json.edges.map(jsonEdge => {
2294
+ const edge = new GraphEdge(
2295
+ network.nodes[jsonEdge[0]],
2296
+ network.nodes[jsonEdge[1]]
2297
+ );
2298
+ if (jsonEdge.length > 2 && jsonEdge[2] !== null) {
2299
+ edge.level = Level.fromString(jsonEdge[2]);
2300
+ }
2301
+ if (jsonEdge.length > 3 && jsonEdge[3]) {
2302
+ edge.isOneway = true;
2303
+ }
2304
+ return edge;
2305
+ });
2306
+
2307
+ return network;
2308
+ }
2309
+
2310
+
2311
+ /**
2312
+ * Convert Array of Coordinates array to a network
2313
+ * @param {Coordinates[][]} segments
2314
+ * @returns {Network}
2315
+ */
2316
+ static fromCoordinates(segments) {
2317
+
2318
+ const network = new Network();
2319
+
2320
+ const getOrCreateNode = coords =>
2321
+ network.nodes.find(_coords => _coords.equalsTo(coords)) || new GraphNode(coords);
2322
+
2323
+
2324
+ const createEdgeFromNodes = (node1, node2) =>
2325
+ new GraphEdge(node1, node2, Level.union(node1.coords.level, node2.coords.level));
2326
+
2327
+ for (const segment of segments) {
2328
+
2329
+ let previousNode = null;
2330
+ for (const coords of segment) {
2331
+ const currentNode = getOrCreateNode(coords);
2332
+
2333
+ if (previousNode) {
2334
+ const edge = createEdgeFromNodes(currentNode, previousNode);
2335
+ network.edges.push(edge);
2336
+ }
2337
+
2338
+ network.nodes.push(currentNode);
2339
+ previousNode = currentNode;
2340
+ }
2341
+ }
2342
+
2343
+ return network;
2344
+ }
2345
+
2346
+ /**
2347
+ * Create edges From MultiLevel Itinerary for a given level
2348
+ * @param {Level} targetLevel level for selection.
2349
+ * @param {Boolean} useMultiLevelEdges use segments which intersect both levels (stairs, elevators...)
2350
+ * @returns {GraphEdge[]} Ordered edges
2351
+ */
2352
+ getEdgesAtLevel(targetLevel, useMultiLevelEdges = true) {
2353
+ return this.edges.filter(
2354
+ ({ level }) => useMultiLevelEdges
2355
+ ? Level.intersect(targetLevel, level) !== null
2356
+ : Level.contains(targetLevel, level)
2357
+ );
2358
+ }
2359
+
2360
+ }
2361
+
2362
+ class GraphProjection {
2363
+
2364
+ /** @type {Coordinates} */
2365
+ origin;
2366
+
2367
+ /** @type {number} */
2368
+ distanceFromNearestElement;
2369
+
2370
+ /** @type {Coordinates} */
2371
+ projection;
2372
+
2373
+ /** @type {Node|Edge} */
2374
+ nearestElement;
2375
+
2376
+ }
2377
+
2378
+ /* eslint-disable complexity */
2379
+
2380
+ class MapMatching {
2381
+
2382
+ /** @type {Network} */
2383
+ _network = null;
2384
+
2385
+ /** @type {number} */
2386
+ _maxDistance = Number.MAX_VALUE;
2387
+
2388
+ /** @type {number} */
2389
+ _maxAngleBearing = Math.PI;
2390
+
2391
+ /**
2392
+ * Constructor of Map-matching
2393
+ * @param {?Network} network
2394
+ */
2395
+ constructor(network) {
2396
+ this.network = network;
2397
+ }
2398
+
2399
+ /** @type {!number} */
2400
+ set maxAngleBearing(maxAngleBearing) {
2401
+ this._maxAngleBearing = maxAngleBearing;
2402
+ }
2403
+
2404
+ /** @type {!number} */
2405
+ set maxDistance(maxDistance) {
2406
+ this._maxDistance = maxDistance;
2407
+ }
2408
+
2409
+ /** @type {!number} */
2410
+ get maxAngleBearing() {
2411
+ return this._maxAngleBearing;
2412
+ }
2413
+
2414
+ /** @type {!number} */
2415
+ get maxDistance() {
2416
+ return this._maxDistance;
2417
+ }
2418
+
2419
+ /** @type {?Network} */
2420
+ get network() {
2421
+ return this._network;
2422
+ }
2423
+
2424
+ /** @type {?Network} */
2425
+ set network(network) {
2426
+ if (network instanceof Network) {
2427
+ this._network = network;
2428
+ } else {
2429
+ if (typeof network !== 'undefined' && network !== null) {
2430
+ throw new Error('network argument is not a Network object');
2431
+ }
2432
+ this._network = null;
2433
+ }
2434
+ }
2435
+
2436
+ /**
2437
+ * Check if the specified edge and its nodes can be used for projection
2438
+ * @returns {boolean} an array of two elements.
2439
+ * First is true if projection will be used on the specified edge, false otherwise.
2440
+ * Second is true if projection will be used on the nodes of the specified edge, false otherwise.
2441
+ *
2442
+ * @param {GraphEdge} edge
2443
+ * @param {Coordinates} location
2444
+ * @param {boolean} useBearing
2445
+ * @param {boolean} useMultiLevelSegments
2446
+ * @param {Function} acceptEdgeFn
2447
+ */
2448
+ _shouldProjectOnEdgeAndNodes(edge, location, useBearing, useMultiLevelSegments, acceptEdgeFn) {
2449
+
2450
+ if (!acceptEdgeFn(edge)) {
2451
+ // if edge selection is not verified
2452
+ return [false, false, false];
2453
+ }
2454
+
2455
+ let checkNode1 = true;
2456
+ let checkNode2 = true;
2457
+ let checkEdge = true;
2458
+
2459
+ if (
2460
+ // Verify if edge level only if one of both is defined
2461
+ (location.level || edge.level)
2462
+ && (
2463
+ // if edge level intersect location level
2464
+ !Level.intersect(location.level, edge.level)
2465
+ // ignore MultiLevelSegments if option used
2466
+ || (!useMultiLevelSegments && edge.level && edge.level.isRange)
2467
+ )) {
2468
+ checkEdge = false;
2469
+ }
2470
+
2471
+ if (!Level.equalsTo(location.level, edge.node1.coords.level) && !edge.node1.io) {
2472
+ checkNode1 = false;
2473
+ }
2474
+
2475
+ if (!Level.equalsTo(location.level, edge.node2.coords.level) && !edge.node2.io) {
2476
+ checkNode2 = false;
2477
+ }
2478
+
2479
+ if (useBearing) {
2480
+ // if mapmatching bearing is enabled do not use nodes matching
2481
+ if (checkEdge) {
2482
+ // condition for optimisation
2483
+ const diffAngle = diffAngleLines(edge.bearing, location.bearing);
2484
+ if (diffAngle > this._maxAngleBearing) {
2485
+ // Do not try to project if angle is greater than the threshold
2486
+ checkEdge = false;
2487
+ }
2488
+ }
2489
+ checkNode1 = false;
2490
+ checkNode2 = false;
2491
+ }
2492
+
2493
+ return [checkEdge, checkNode1, checkNode2];
2494
+ }
2495
+
2496
+
2497
+ /**
2498
+ * @param {Coordinates} fromCoordinates
2499
+ * @param {Coordinates} toCoordinates
2500
+ */
2501
+ static _assignLatLngLevel(fromCoordinates, toCoordinates) {
2502
+ toCoordinates.lat = fromCoordinates.lat;
2503
+ toCoordinates.lng = fromCoordinates.lng;
2504
+ toCoordinates.level = fromCoordinates.level;
2505
+ }
2506
+
2507
+ /**
2508
+ * @param {Edge} _edge
2509
+ * @param {Coordinates} _projection
2510
+ */
2511
+ static _updateProjectionLevelFromEdge = (_edge, _projection) => {
2512
+ if (_edge.level) {
2513
+ _projection.level = _edge.level.clone();
2514
+ }
2515
+ };
2516
+
2517
+ /**
2518
+ * @param {!Coordinates} location
2519
+ * @param {boolean} useDistance
2520
+ * @param {boolean} useBearing
2521
+ * @param {boolean} useMultiLevelSegments
2522
+ * @param {function} acceptEdgeFn
2523
+ * @returns {GraphProjection}
2524
+ */
2525
+ getProjection(location, useDistance = false, useBearing = false,
2526
+ useMultiLevelSegments = true, acceptEdgeFn = () => true) {
2527
+
2528
+ if (this.network === null) {
2529
+ throw new Error('Network has not been set yet');
2530
+ }
2531
+
2532
+ if (!(location instanceof Coordinates)) {
2533
+ throw new TypeError('location is not an instance of Coordinates');
2534
+ }
2535
+
2536
+ if (useBearing && (!location.bearing || !this._maxAngleBearing)) {
2537
+ return null;
2538
+ }
2539
+
2540
+ const projection = new GraphProjection();
2541
+ projection.origin = location;
2542
+ projection.distanceFromNearestElement = Number.MAX_VALUE;
2543
+ projection.projection = location.clone();
2544
+
2545
+ const isProjectionBetter = (distanceOfNewProjection) => {
2546
+ return distanceOfNewProjection < projection.distanceFromNearestElement
2547
+ && (!useDistance || distanceOfNewProjection <= this._maxDistance);
2548
+ };
2549
+
2550
+ for (let i = 0; i < this.network.edges.length; i++) {
2551
+ const edge = this.network.edges[i];
2552
+
2553
+ const [checkEdge, checkNode1, checkNode2] = this._shouldProjectOnEdgeAndNodes(
2554
+ edge, location, useBearing, useMultiLevelSegments, acceptEdgeFn);
2555
+
2556
+ if (checkNode1) {
2557
+
2558
+ const distNode1 = location.distanceTo(edge.node1.coords);
2559
+ if (isProjectionBetter(distNode1) || distNode1 <= Constants.EPS_MM) {
2560
+ projection.distanceFromNearestElement = distNode1;
2561
+ projection.nearestElement = edge.node1;
2562
+ MapMatching._assignLatLngLevel(edge.node1.coords, projection.projection);
2563
+ MapMatching._updateProjectionLevelFromEdge(edge, projection.projection);
2564
+
2565
+ if (distNode1 <= Constants.EPS_MM
2566
+ && location.level === edge.node1.coords.level) {
2567
+ break;
2568
+ }
2569
+ }
2570
+ }
2571
+
2572
+ if (checkNode2) {
2573
+
2574
+ const distNode2 = location.distanceTo(edge.node2.coords);
2575
+ if (isProjectionBetter(distNode2) || distNode2 <= Constants.EPS_MM) {
2576
+
2577
+ projection.distanceFromNearestElement = distNode2;
2578
+ projection.nearestElement = edge.node2;
2579
+ MapMatching._assignLatLngLevel(edge.node2.coords, projection.projection);
2580
+ MapMatching._updateProjectionLevelFromEdge(edge, projection.projection);
2581
+
2582
+ if (distNode2 <= Constants.EPS_MM
2583
+ && location.level === edge.node2.coords.level) {
2584
+ break;
2585
+ }
2586
+ }
2587
+ }
2588
+
2589
+ if (checkEdge) {
2590
+ const segmentProjection = location.getSegmentProjection(edge.node1.coords, edge.node2.coords);
2591
+ if (segmentProjection) {
2592
+ const distEdge = location.distanceTo(segmentProjection);
2593
+ if (isProjectionBetter(distEdge)) {
2594
+ projection.distanceFromNearestElement = distEdge;
2595
+ projection.nearestElement = edge;
2596
+ MapMatching._assignLatLngLevel(segmentProjection, projection.projection);
2597
+ MapMatching._updateProjectionLevelFromEdge(edge, projection.projection);
2598
+ }
2599
+ }
2600
+ }
2601
+ }
2602
+
2603
+
2604
+ if (!projection.nearestElement) {
2605
+ return null;
2606
+ }
2607
+
2608
+ if (projection.projection instanceof UserPosition) {
2609
+ projection.projection.accuracy += projection.distanceFromNearestElement;
2610
+ }
2611
+ return projection;
2612
+ }
2613
+
2614
+ }
2615
+
2616
+ /**
2617
+ * @template T
2618
+ */
2619
+ class GraphItinerary {
2620
+
2621
+ /** @type {Coordinates} */
2622
+ start;
2623
+
2624
+ /** @type {Coordinates} */
2625
+ end;
2626
+
2627
+ /** @type {GraphNode<T>[]} */
2628
+ nodes;
2629
+
2630
+ /** @type {GraphEdge<T>[]} */
2631
+ edges;
2632
+
2633
+ /** @type {number[]} */
2634
+ edgesWeights;
2635
+
2636
+
2637
+ /**
2638
+ * @template T
2639
+ * @param {GraphNode<T>[]} networkNodes
2640
+ * @param {number[]} edgesWeights
2641
+ * @returns {GraphItinerary<T>}
2642
+ */
2643
+ static fromNetworkNodes(networkNodes, edgesWeights) {
2644
+ const itinerary = new GraphItinerary();
2645
+ itinerary.edgesWeights = edgesWeights;
2646
+
2647
+ itinerary.nodes = networkNodes.map(node => {
2648
+ const newNode = node.clone();
2649
+
2650
+ // Remove node edges, they will be added later.
2651
+ newNode.edges = [];
2652
+
2653
+ // Let consider io nodes level = null for GraphItinerary
2654
+ if (newNode.io) {
2655
+ newNode.coords = newNode.coords.clone();
2656
+ newNode.coords.level = null;
2657
+ }
2658
+
2659
+ return newNode;
2660
+ });
2661
+
2662
+ itinerary.edges = [];
2663
+ networkNodes.forEach((node, idx, arr) => {
2664
+ if (idx === 0) {
2665
+ return;
2666
+ }
2667
+
2668
+ // Retrieve network edge
2669
+ const prevNode = arr[idx - 1];
2670
+ const edge = getEdgeByNodes(prevNode.edges, prevNode, node);
2671
+
2672
+ // Create itinerary edge
2673
+ const newEdge = new GraphEdge(
2674
+ itinerary.nodes[idx - 1],
2675
+ itinerary.nodes[idx],
2676
+ edge.level,
2677
+ edge.builtFrom
2678
+ );
2679
+ newEdge.isOneway = edge.isOneway;
2680
+ itinerary.edges.push(newEdge);
2681
+ });
2682
+
2683
+ return itinerary;
2684
+
2685
+ }
2686
+ }
2687
+
2688
+ class NoRouteFoundError extends Error {
2689
+
2690
+ constructor(start, end, details) {
2691
+ super();
2692
+ this.start = start;
2693
+ this.end = end;
2694
+ this.details = details;
2695
+ }
2696
+
2697
+ get startStr() {
2698
+ if (this.start instanceof GraphNode) {
2699
+ return `Node ${this.start.coords.toString()}`;
2700
+ }
2701
+
2702
+ // if (this.start instanceof Coordinates) {
2703
+ return this.start.toString();
2704
+ }
2705
+
2706
+ get endStr() {
2707
+ if (this.end instanceof GraphNode) {
2708
+ return `Node ${this.end.coords.toString()}`;
2709
+ }
2710
+
2711
+ // if (this.end instanceof Coordinates) {
2712
+ return this.end.toString();
2713
+ }
2714
+
2715
+ get message() {
2716
+ let message = `No route found from ${this.startStr} to ${this.endStr}.`;
2717
+ if (this.details) {
2718
+ message += ` Details: ${this.details}`;
2719
+ }
2720
+ return message;
2721
+ }
2722
+ }
2723
+
2724
+ /**
2725
+ * @template T
2726
+ */
2727
+ class GraphRouterOptions {
2728
+
2729
+ /** @type {number} in meters */
2730
+ projectionMaxDistance = 50;
2731
+
2732
+ /** @type {function(GraphEdge<T>):boolean} */
2733
+ weightEdgeFn = edge => edge.length;
2734
+
2735
+ /** @type {function(GraphEdge<T>):boolean} */
2736
+ acceptEdgeFn = () => true;
2737
+
2738
+ }
2739
+
2740
+ /**
2741
+ * @template T
2742
+ * @abstract
2743
+ */
2744
+ class GraphRouter {
2745
+
2746
+ /** @type {!Network<T>} */
2747
+ _network;
2748
+
2749
+ /**
2750
+ * @param {!Network<T>} network
2751
+ */
2752
+ constructor(network) {
2753
+ this._network = network;
2754
+ this._mapMatching = new MapMatching(network);
2755
+ }
2756
+
2757
+ /**
2758
+ * @template T
2759
+ * @param {!GraphNode<T>|!Coordinates} start
2760
+ * @param {!GraphNode<T>|!Coordinates} end
2761
+ * @param {GraphRouterOptions} _options
2762
+ * @returns {GraphItinerary<T>}
2763
+ */
2764
+ getShortestPath(start, end, options = new GraphRouterOptions()) {
2765
+
2766
+ if (!(start instanceof GraphNode) && !(start instanceof Coordinates)) {
2767
+ throw new Error('Unknown start type');
2768
+ }
2769
+
2770
+ if (!(end instanceof GraphNode) && !(end instanceof Coordinates)) {
2771
+ throw new Error('Unknown end type');
2772
+ }
2773
+
2774
+ const { acceptEdgeFn, weightEdgeFn, projectionMaxDistance } = options;
2775
+ this._mapMatching.maxDistance = projectionMaxDistance;
2776
+
2777
+ const createdNodes = [];
2778
+
2779
+ const retrieveOrCreateNearestNode = point => {
2780
+ if (point instanceof GraphNode) {
2781
+ return point;
2782
+ }
2783
+
2784
+ const closeNode = this._network.getNodeByCoords(point);
2785
+ if (closeNode) {
2786
+ return closeNode;
2787
+ }
2788
+
2789
+ const proj = this._mapMatching.getProjection(point, true, false, false, acceptEdgeFn);
2790
+ if (!proj) {
2791
+ throw new NoRouteFoundError(start, end,
2792
+ `Point ${point.toString()} is too far from the network `
2793
+ + `> ${this._mapMatching.maxDistance.toFixed(0)} meters`
2794
+ );
2795
+ }
2796
+ if (proj.nearestElement instanceof GraphNode) {
2797
+ return proj.nearestElement;
2798
+ }
2799
+ // if (proj.nearestElement instanceof Edge)
2800
+ const nodeCreated = this.createNodeInsideEdge(
2801
+ proj.nearestElement,
2802
+ proj.projection
2803
+ );
2804
+ createdNodes.push(nodeCreated);
2805
+ return nodeCreated;
2806
+ };
2807
+
2808
+ const removeCreatedNodes = () => {
2809
+ while (createdNodes.length) {
2810
+ this.removeNodeFromPreviouslyCreatedEdge(createdNodes.pop());
2811
+ }
2812
+ };
2813
+
2814
+ const startNode = retrieveOrCreateNearestNode(start);
2815
+ const endNode = retrieveOrCreateNearestNode(end);
2816
+
2817
+ let graphItinerary;
2818
+
2819
+ if (startNode === endNode) {
2820
+ graphItinerary = GraphItinerary.fromNetworkNodes([startNode], []);
2821
+ } else {
2822
+ graphItinerary = this.getShortestPathBetweenGraphNodes(
2823
+ startNode,
2824
+ endNode,
2825
+ acceptEdgeFn,
2826
+ weightEdgeFn
2827
+ );
2828
+ }
2829
+
2830
+ graphItinerary.start = start instanceof GraphNode ? start.coords : start;
2831
+ graphItinerary.end = end instanceof GraphNode ? end.coords : end;
2832
+
2833
+ removeCreatedNodes();
2834
+
2835
+ if (!graphItinerary.nodes.length) {
2836
+ throw new NoRouteFoundError(start, end);
2837
+ }
2838
+
2839
+ return graphItinerary;
2840
+
2841
+ }
2842
+
2843
+ /**
2844
+ * @param {GraphEdge<T>} edge
2845
+ * @param {Coordinates} point
2846
+ */
2847
+ createNodeInsideEdge(edge, point) {
2848
+ const a = edge.node1;
2849
+ const b = edge.node2;
2850
+
2851
+ const m = new GraphNode(point);
2852
+ m.coords.level = edge.level;
2853
+ m.builtFrom = edge.builtFrom;
2854
+
2855
+ const u = edge.clone();
2856
+ u.node1 = a;
2857
+ u.node2 = m;
2858
+
2859
+ const v = edge.clone();
2860
+ v.node1 = m;
2861
+ v.node2 = b;
2862
+
2863
+ a.edges = a.edges.filter(_edge => _edge !== edge);
2864
+ b.edges = b.edges.filter(_edge => _edge !== edge);
2865
+
2866
+ this._network.nodes.push(m);
2867
+ this._network.edges.push(u, v);
2868
+
2869
+ this._network.edges = this._network.edges.filter(
2870
+ _edge => _edge !== edge
2871
+ );
2872
+
2873
+ return m;
2874
+ }
2875
+
2876
+ /**
2877
+ * @param {GraphNode<T>} _node
2878
+ */
2879
+ removeNodeFromPreviouslyCreatedEdge(_node) {
2880
+ const u = _node.edges[0];
2881
+ const v = _node.edges[1];
2882
+
2883
+ u.node1.edges = u.node1.edges.filter(edge => edge !== u);
2884
+ v.node1.edges = v.node1.edges.filter(edge => edge !== v);
2885
+
2886
+ const oldEdge = u.clone();
2887
+ oldEdge.node1 = u.node1;
2888
+ oldEdge.node2 = v.node2;
2889
+ this._network.edges.push(oldEdge);
2890
+
2891
+ this._network.nodes = this._network.nodes.filter(node => node !== _node);
2892
+ this._network.edges = this._network.edges.filter(
2893
+ edge => edge !== u && edge !== v
2894
+ );
2895
+ }
2896
+
2897
+ getShortestPathBetweenGraphNodes(start, end, acceptEdgeFn, weightFn) {
2898
+ const distanceMap = {},
2899
+ vertexList = {},
2900
+ vertexNodes = {},
2901
+ parentVertices = {},
2902
+ path = [];
2903
+ let vertexId = 1;
2904
+
2905
+ // Initially, we assume each vertex is unreachable
2906
+ this._network.nodes.forEach(vertex => {
2907
+
2908
+ // Generate Unique Router Id
2909
+ vertex.uniqueRouterId = vertexId;
2910
+ vertexNodes[vertexId] = vertex;
2911
+
2912
+ distanceMap[vertexId] = Infinity;
2913
+ vertexList[vertexId] = true;
2914
+
2915
+ vertexId++;
2916
+ });
2917
+
2918
+ // The cost from the starting vertex to the starting vertex is 0
2919
+ distanceMap[start.uniqueRouterId] = 0;
2920
+
2921
+ // check each vertex
2922
+ while (Object.keys(vertexList).length > 0) {
2923
+ const current = Number(
2924
+ Object.keys(vertexList).reduce((_checking, vertex) => {
2925
+ return distanceMap[_checking] > distanceMap[vertex]
2926
+ ? vertex
2927
+ : _checking;
2928
+ }, Object.keys(vertexList)[0])
2929
+ );
2930
+
2931
+ // all the vertices accessible from current vertex
2932
+ this._network.edges
2933
+ .filter(edge => {
2934
+
2935
+ if (!acceptEdgeFn(edge)) {
2936
+ return false;
2937
+ }
2938
+
2939
+ const from = edge.node1.uniqueRouterId,
2940
+ to = edge.node2.uniqueRouterId;
2941
+ // are these vertices joined?
2942
+ return from === current || to === current;
2943
+ })
2944
+ // for each vertex we can reach
2945
+ .forEach(edge => {
2946
+ let to, from, reversed = false;
2947
+ // determine the direction of travel
2948
+ if (edge.node1.uniqueRouterId === current) {
2949
+ to = edge.node2.uniqueRouterId;
2950
+ from = edge.node1.uniqueRouterId;
2951
+ } else {
2952
+ to = edge.node1.uniqueRouterId;
2953
+ from = edge.node2.uniqueRouterId;
2954
+ reversed = true;
2955
+ }
2956
+
2957
+ if (edge.isOneway && reversed) {
2958
+ return;
2959
+ }
2960
+
2961
+ // distance is how far we travelled to reach the
2962
+ // current vertex, plus cost of travel the next(to)
2963
+ const distance = distanceMap[current] + weightFn(edge);
2964
+
2965
+ // if we have found a cheaper path
2966
+ // update the hash of costs
2967
+ // and record which vertex we came from
2968
+ if (distanceMap[to] > distance) {
2969
+ distanceMap[to] = distance;
2970
+ parentVertices[to] = from;
2971
+ }
2972
+ });
2973
+
2974
+ // remove vertex so we don't revisit it
2975
+ delete vertexList[current];
2976
+ }
2977
+
2978
+ const edgesWeights = [];
2979
+
2980
+ // now we have the most efficient paths for all vertices
2981
+ // build the path for the user specified vertex(end)
2982
+ let endId = end.uniqueRouterId;
2983
+ while (parentVertices[endId]) {
2984
+ path.unshift(vertexNodes[endId]);
2985
+ edgesWeights.unshift(distanceMap[endId] - distanceMap[parentVertices[endId]]);
2986
+ endId = parentVertices[endId];
2987
+ }
2988
+ if (path.length !== 0) {
2989
+ path.unshift(start);
2990
+ }
2991
+
2992
+ // Remove Unique Router Id
2993
+ this._network.nodes.forEach(vertex => {
2994
+ delete vertex.uniqueRouterId;
2995
+ });
2996
+
2997
+ // This clone the itinerary and temporary nodes
2998
+ return GraphItinerary.fromNetworkNodes(path, edgesWeights);
2999
+ }
3000
+ }
3001
+
3002
+ export { AbsoluteHeading, Attitude, BoundingBox, Constants, Coordinates, GeoRelativePosition, GraphEdge, GraphItinerary, GraphNode, GraphProjection, GraphRouter, GraphRouterOptions, GraphUtils, Level, MapMatching, Network, NoRouteFoundError, RelativePosition, UserPosition, Utils };
3003
+ //# sourceMappingURL=wemap-geo.es.js.map