@wemap/geo 9.0.1 → 9.0.2

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