@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.
- package/dist/wemap-geo.es.js +3003 -0
- package/dist/wemap-geo.es.js.map +1 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/src/coordinates/Coordinates.js +3 -3
- package/src/coordinates/GeoRef.js +39 -0
|
@@ -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
|