@wemap/geo 9.0.0 → 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,309 +31,351 @@ Constants.CIRCUMFERENCE = Constants.R_MAJOR * 2 * Math.PI;
31
31
 
32
32
  /**
33
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
34
+ * A level can be a simple number or a range (low, up)
35
+ * The range is an array of two numbers
36
36
  */
37
37
  class Level {
38
38
 
39
- /** @type {number?} */
40
- val = null;
41
-
42
39
  /** @type {boolean} */
43
- isRange = false;
44
-
45
- /** @type {number?} */
46
- low = null;
47
-
48
- /** @type {number?} */
49
- up = null;
40
+ VERIFY_TYPING = false;
50
41
 
51
42
  /**
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
43
+ * @param {null|number|[number, number]} level
44
+ * @throws {Error}
57
45
  */
58
- constructor(arg1, arg2) {
59
- if (typeof arg1 !== 'number' || isNaN(arg1)) {
60
- throw new Error('first argument is mandatory');
46
+ static checkType(level) {
47
+ if (level === null) {
48
+ return;
61
49
  }
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);
50
+ if (typeof level === 'number' && !isNaN(level)) {
51
+ return;
52
+ }
53
+ if (Array.isArray(level) && level.length === 2) {
54
+ const [low, up] = level;
55
+ if (typeof low === 'number' && !isNaN(low)
56
+ && typeof up === 'number' && !isNaN(up)) {
57
+ if (low > up || low === up) {
58
+ throw Error(`Invalid level range: [${low}, ${up}]`);
59
+ }
60
+ return;
70
61
  }
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
62
  }
63
+ throw Error(`Unknown level format: ${level}`);
77
64
  }
78
65
 
79
- clone() {
80
- if (this.isRange) {
81
- return new Level(this.low, this.up);
66
+ /**
67
+ * Return true if the level is a range, false otherwise
68
+ * @param {null|number|[number, number]} level
69
+ * @returns {boolean}
70
+ */
71
+ static isRange(level) {
72
+ if (this.VERIFY_TYPING) {
73
+ this.checkType(level);
82
74
  }
83
- return new Level(this.val);
75
+ return Array.isArray(level);
76
+ }
77
+
78
+ /**
79
+ * @param {null|number|[number, number]} level
80
+ * @returns {null|number|[number, number]}
81
+ * @throws {Error}
82
+ */
83
+ static clone(level) {
84
+ if (this.VERIFY_TYPING) {
85
+ this.checkType(level);
86
+ }
87
+ if (level === null) {
88
+ return null;
89
+ }
90
+ if (typeof level === 'number') {
91
+ return level;
92
+ }
93
+
94
+ return [level[0], level[1]];
84
95
  }
85
96
 
86
97
  /**
87
98
  * Create a level from a string
88
99
  * @param {string} str level in str format (eg. 1, -2, 1;2, -2;3, 2;-1, 0.5;1 ...)
100
+ * @returns {null|number|[number, number]}
101
+ * @throws {Error}
89
102
  */
90
103
  static fromString(str) {
91
104
 
92
- if (typeof str !== 'string') {
105
+ if (str === null) {
93
106
  return null;
94
107
  }
95
108
 
109
+ if (typeof str !== 'string') {
110
+ throw Error(`argument must be a string, got ${typeof str}`);
111
+ }
112
+
96
113
  if (!isNaN(Number(str))) {
97
- return new Level(parseFloat(str));
114
+ return parseFloat(str);
98
115
  }
99
116
 
100
117
  const splited = str.split(';');
101
118
  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
- }
119
+ const low = Number(splited[0]);
120
+ const up = Number(splited[1]);
121
+ this.checkType([low, up]);
122
+ return [parseFloat(splited[0]), parseFloat(splited[1])];
105
123
  }
106
124
 
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
+ throw Error(`Cannot parse following level: ${str}`);
125
126
  }
126
127
 
127
128
 
128
129
  /**
129
130
  * Returns if a level is contained in another
130
- * @param {Level} container The container level
131
- * @param {Level} targeted The targeted level
131
+ * @param {null|number|[number, number]} container The container level
132
+ * @param {null|number|[number, number]} targeted The targeted level
132
133
  */
133
134
  static contains(container, targeted) {
135
+ if (this.VERIFY_TYPING) {
136
+ this.checkType(container);
137
+ this.checkType(targeted);
138
+ }
134
139
 
140
+ // Covers null and number
135
141
  if (container === targeted) {
136
142
  return true;
137
143
  }
138
144
 
139
- if (!container || !targeted) {
140
- return false;
141
- }
142
-
143
- if (!container.isRange) {
144
- if (targeted.isRange) {
145
- return false;
145
+ if (Array.isArray(container)) {
146
+ if (Array.isArray(targeted)) {
147
+ return container[0] <= targeted[0] && container[1] >= targeted[1];
146
148
  }
147
- return container.val === targeted.val;
149
+ return container[0] <= targeted && container[1] >= targeted;
148
150
  }
149
-
150
- return container.up >= (targeted.isRange ? targeted.up : targeted.val)
151
- && container.low <= (targeted.isRange ? targeted.low : targeted.val);
151
+ return container <= targeted[0] && container >= targeted[1];
152
152
  }
153
153
 
154
154
  /**
155
155
  * Retrieve the intersection of two levels
156
- * @param {Level} other The other level
156
+ * @param {null|number|[number, number]} first The first level
157
+ * @param {null|number|[number, number]} second The second level
158
+ * @returns {null|number|[number, number]}
157
159
  */
158
- intersect(other) {
159
- return Level.intersect(this, other);
160
- }
160
+ static intersection(first, second) {
161
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) {
162
+ if (this.VERIFY_TYPING) {
163
+ this.checkType(first);
164
+ this.checkType(second);
165
+ }
168
166
 
169
- if (first === second) {
170
- if (first instanceof Level) {
171
- return first.clone();
172
- }
167
+ if (first === null || second === null) {
173
168
  return null;
174
169
  }
175
170
 
176
- if (!second) {
177
- return null;
171
+ if (this.equals(first, second)) {
172
+ return this.clone(first);
178
173
  }
179
174
 
180
- if (!first) {
181
- return null;
175
+ if (typeof first === 'number' && typeof second === 'number') {
176
+ return first === second ? first : null;
182
177
  }
183
178
 
184
- if (first.isRange && !second.isRange) {
185
- if (first.contains(second)) {
186
- return second.clone();
179
+ if (Array.isArray(first) && !Array.isArray(second)) {
180
+ if (this.contains(first, second)) {
181
+ return second;
187
182
  }
188
183
  return null;
189
184
  }
190
- if (!first.isRange && second.isRange) {
191
- if (second.contains(first)) {
192
- return first.clone();
185
+ if (!Array.isArray(first) && Array.isArray(second)) {
186
+ if (this.contains(second, first)) {
187
+ return first;
193
188
  }
194
189
  return null;
195
190
  }
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);
191
+
192
+ // There are two ranges
193
+ const low = Math.max(first[0], second[0]);
194
+ const up = Math.min(first[1], second[1]);
195
+ if (up === low) {
196
+ return up;
203
197
  }
204
- return first.val === second.val ? first : null;
198
+ return up < low ? null : [low, up];
205
199
  }
206
200
 
207
201
  /**
208
- * Retrieve the union of two levels
209
- * @param {Level} other The other level
210
- */
211
- union(other) {
212
- return Level.union(this, other);
202
+ * Retrieve the intersection of two levels
203
+ * @param {null|number|[number, number]} first The first level
204
+ * @param {null|number|[number, number]} second The second level
205
+ * @returns {boolean}
206
+ */
207
+ static intersect(first, second) {
208
+
209
+ if (this.VERIFY_TYPING) {
210
+ this.checkType(first);
211
+ this.checkType(second);
212
+ }
213
+
214
+ if (first === null && second === null) {
215
+ return true;
216
+ }
217
+
218
+ return this.intersection(first, second) !== null;
213
219
  }
214
220
 
215
221
  /**
216
222
  * Retrieve the union of two levels
217
- * @param {Level} other The other level
223
+ * @param {null|number|[number, number]} first The first level
224
+ * @param {null|number|[number, number]} second The scond level
225
+ * @returns {null|number|[number, number]}
218
226
  */
219
227
  static union(first, second) {
220
228
 
229
+ if (this.VERIFY_TYPING) {
230
+ this.checkType(first);
231
+ this.checkType(second);
232
+ }
233
+
221
234
  if (first === second) {
222
- if (first instanceof Level) {
223
- return first.clone();
224
- }
225
- return null;
235
+ return this.clone(first);
226
236
  }
227
237
 
228
- if (!second) {
229
- return first.clone();
238
+ if (second === null) {
239
+ return this.clone(first);
230
240
  }
231
241
 
232
- if (!first) {
233
- return second.clone();
242
+ if (first === null) {
243
+ return this.clone(second);
234
244
  }
235
245
 
236
246
  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);
247
+ if (!Array.isArray(first) && !Array.isArray(second)) {
248
+ low = Math.min(first, second);
249
+ up = Math.max(first, second);
250
+ } else if (Array.isArray(first) && !Array.isArray(second)) {
251
+ low = Math.min(first[0], second);
252
+ up = Math.max(first[1], second);
253
+ } else if (!Array.isArray(first) && Array.isArray(second)) {
254
+ low = Math.min(second[0], first);
255
+ up = Math.max(second[1], first);
246
256
  } else {
247
- /* if (first.isRange && second.isRange) */
248
- low = Math.min(second.low, first.low);
249
- up = Math.max(second.up, first.up);
257
+ /* if (Array.isArray(first) && Array.isArray(second)) */
258
+ low = Math.min(second[0], first[0]);
259
+ up = Math.max(second[1], first[1]);
250
260
  }
251
261
 
252
262
  if (low === up) {
253
- return new Level(low);
263
+ return low;
254
264
  }
255
- return new Level(low, up);
265
+ return [low, up];
256
266
  }
257
267
 
258
- multiplyBy(factor) {
259
- if (this.isRange) {
260
- this.low *= factor;
261
- this.up *= factor;
262
- } else {
263
- this.val *= factor;
268
+ /**
269
+ * Multiply a level by a factor
270
+ * @param {null|number|[number, number]} level the level to multiply
271
+ * @param {number} factor
272
+ * @returns {null|number|[number, number]}
273
+ */
274
+ static multiplyBy(level, factor) {
275
+ if (this.VERIFY_TYPING) {
276
+ this.checkType(level);
264
277
  }
265
- return this;
278
+
279
+ return Array.isArray(level) ? [level[0] * factor, level[1] * factor] : level * factor;
266
280
  }
267
281
 
268
- toString() {
269
- return this.isRange ? this.low + ';' + this.up : String(this.val);
282
+ /**
283
+ * @param {null|number|[number, number]} level
284
+ * @returns {string|null}
285
+ */
286
+ static toString(level) {
287
+ if (this.VERIFY_TYPING) {
288
+ this.checkType(level);
289
+ }
290
+ if (level === null) {
291
+ return null;
292
+ }
293
+ return Array.isArray(level) ? level[0] + ';' + level[1] : String(level);
270
294
  }
271
295
 
272
- static equalsTo(first, second) {
296
+ /**
297
+ * @param {null|number|[number, number]} first
298
+ * @param {null|number|[number, number]} second
299
+ * @returns {boolean}
300
+ */
301
+ static equals(first, second) {
302
+
303
+ if (this.VERIFY_TYPING) {
304
+ this.checkType(first);
305
+ this.checkType(second);
306
+ }
273
307
 
274
308
  if (first === second) {
275
309
  return true;
276
310
  }
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;
311
+
312
+ if (Array.isArray(first) && Array.isArray(second)) {
313
+ return first[0] === second[0] && first[1] === second[1];
294
314
  }
295
- // !first.isRange && !second.isRange
296
- return first.val === second.val;
297
315
 
316
+ return false;
298
317
  }
299
318
 
319
+ /**
320
+ * @param {null|number|[number, number]} first
321
+ * @param {null|number|[number, number]} second
322
+ * @returns {null|number}
323
+ */
300
324
  static diff(first, second) {
301
325
 
326
+ if (this.VERIFY_TYPING) {
327
+ this.checkType(first);
328
+ this.checkType(second);
329
+ }
330
+
302
331
  if (first === null || second === null) {
303
332
  return null;
304
333
  }
305
334
 
306
- if (!first.isRange && !second.isRange) {
307
- return second.val - first.val;
335
+ if (!Array.isArray(first) && !Array.isArray(second)) {
336
+ return second - first;
308
337
  }
309
338
 
310
- if (first.isRange && !second.isRange) {
311
- if (first.low === second.val) {
312
- return second.val - first.up;
339
+ if (Array.isArray(first) && !Array.isArray(second)) {
340
+ if (first[0] === second) {
341
+ return second - first[1];
313
342
  }
314
- if (first.up === second.val) {
315
- return second.val - first.low;
343
+ if (first[1] === second) {
344
+ return second - first[0];
316
345
  }
317
346
  return null;
318
347
  }
319
348
 
320
- if (second.isRange && !first.isRange) {
321
- if (first.val === second.low) {
322
- return second.up - first.val;
349
+ if (Array.isArray(second) && !Array.isArray(first)) {
350
+ if (first === second[0]) {
351
+ return second[1] - first;
323
352
  }
324
- if (first.val === second.up) {
325
- return second.low - first.val;
353
+ if (first === second[1]) {
354
+ return second[0] - first;
326
355
  }
327
356
  return null;
328
357
  }
329
358
 
330
- if (Level.equalsTo(first, second)) {
359
+ if (Level.equals(first, second)) {
331
360
  return 0;
332
361
  }
333
362
 
334
363
  return null;
335
364
  }
336
365
 
366
+ /**
367
+ * @param {null|{isRange: boolean, val?: number, low?: number, up?: number}} legacyLevel
368
+ * @deprecated
369
+ */
370
+ static fromLegacy(legacyLevel) {
371
+ if (legacyLevel === null) {
372
+ return null;
373
+ }
374
+ if (legacyLevel.isRange) {
375
+ return [legacyLevel.low, legacyLevel.up];
376
+ }
377
+ return legacyLevel.val;
378
+ }
337
379
  }
338
380
 
339
381
  /**
@@ -358,7 +400,7 @@ class Coordinates {
358
400
  /** @type {Number|null} */
359
401
  _alt = null;
360
402
 
361
- /** @type {Level|null} */
403
+ /** @type {null|number|[number, number]} */
362
404
  _level = null;
363
405
 
364
406
  /** @type {[Number, Number, Number]|null} */
@@ -370,7 +412,7 @@ class Coordinates {
370
412
  * @param {Number} lat
371
413
  * @param {Number} lng
372
414
  * @param {?(Number|null)} alt
373
- * @param {?(Level|null)} level
415
+ * @param {?(null|number|[number, number])} level
374
416
  */
375
417
  constructor(lat, lng, alt = null, level = null) {
376
418
  this.lat = lat;
@@ -409,7 +451,7 @@ class Coordinates {
409
451
  return this._alt;
410
452
  }
411
453
 
412
- /** @type {Level|null} */
454
+ /** @type {null|number|[number, number]} */
413
455
  get level() {
414
456
  return this._level;
415
457
  }
@@ -460,16 +502,10 @@ class Coordinates {
460
502
  this._ecef = null;
461
503
  }
462
504
 
463
- /** @type {Level|null} */
505
+ /** @type {null|number|[number, number]} */
464
506
  set level(level) {
465
- if (level instanceof Level) {
466
- this._level = level;
467
- } else {
468
- if (typeof level !== 'undefined' && level !== null) {
469
- throw new Error('level argument is not a Level object');
470
- }
471
- this._level = null;
472
- }
507
+ Level.checkType(level);
508
+ this._level = level;
473
509
  }
474
510
 
475
511
  /**
@@ -478,8 +514,8 @@ class Coordinates {
478
514
  */
479
515
  clone() {
480
516
  const output = new Coordinates(this.lat, this.lng, this.alt);
481
- if (this.level) {
482
- output.level = this.level.clone();
517
+ if (this.level !== null) {
518
+ output.level = Level.clone(this.level);
483
519
  }
484
520
  return output;
485
521
  }
@@ -497,7 +533,7 @@ class Coordinates {
497
533
  * @param {Number} eps latitude and longitude epsilon in degrees (default: 1e-8 [~1mm at lat=0])
498
534
  * @param {Number} epsAlt altitude epsilon in meters (default: 1e-3 [= 1mm])
499
535
  */
500
- static equalsTo(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
536
+ static equals(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
501
537
 
502
538
  // Handle null comparison
503
539
  if (pos1 === null && pos1 === pos2) {
@@ -513,15 +549,15 @@ class Coordinates {
513
549
  && (pos1.alt === pos2.alt
514
550
  || pos1.alt !== null && pos2.alt !== null
515
551
  && Math.abs(pos2.alt - pos1.alt) < epsAlt)
516
- && Level.equalsTo(pos1.level, pos2.level);
552
+ && Level.equals(pos1.level, pos2.level);
517
553
  }
518
554
 
519
555
  /**
520
556
  * @param {Coordinates} other
521
557
  * @returns {!Boolean}
522
558
  */
523
- equalsTo(other) {
524
- return Coordinates.equalsTo(this, other);
559
+ equals(other) {
560
+ return Coordinates.equals(this, other);
525
561
  }
526
562
 
527
563
  /**
@@ -738,7 +774,7 @@ class Coordinates {
738
774
  alt = (p1.alt + p2.alt) / 2;
739
775
  }
740
776
  const projection = new Coordinates(poseCoordinates.lat, poseCoordinates.lng,
741
- alt, Level.intersect(p1.level, p2.level));
777
+ alt, Level.intersection(p1.level, p2.level));
742
778
 
743
779
  if (Math.abs((p1.distanceTo(p2) - p1.distanceTo(projection) - p2.distanceTo(projection))) > Constants.EPS_MM) {
744
780
  return null;
@@ -760,7 +796,7 @@ class Coordinates {
760
796
  str += ', ' + this._alt.toFixed(2);
761
797
  }
762
798
  if (this._level !== null) {
763
- str += ', [' + this._level.toString() + ']';
799
+ str += ', [' + Level.toString(this._level) + ']';
764
800
  }
765
801
  str += ']';
766
802
  return str;
@@ -778,7 +814,24 @@ class Coordinates {
778
814
  output.alt = this.alt;
779
815
  }
780
816
  if (this.level !== null) {
781
- output.level = this.level.toString();
817
+ output.level = this.level;
818
+ }
819
+ return output;
820
+ }
821
+
822
+ /**
823
+ * @returns {!Object}
824
+ */
825
+ toLegacyJson() {
826
+ const output = {
827
+ lat: this.lat,
828
+ lng: this.lng
829
+ };
830
+ if (this.alt !== null) {
831
+ output.alt = this.alt;
832
+ }
833
+ if (this.level !== null) {
834
+ output.level = Level.toString(this.level);
782
835
  }
783
836
  return output;
784
837
  }
@@ -788,7 +841,11 @@ class Coordinates {
788
841
  * @returns {!Coordinates}
789
842
  */
790
843
  static fromJson(json) {
791
- return new Coordinates(json.lat, json.lng, json.alt, Level.fromString(json.level));
844
+ if (typeof json.level === 'string') {
845
+ Logger.warn('Still using legacy level format. Please update your project.');
846
+ return new Coordinates(json.lat, json.lng, json.alt, Level.fromString(json.level));
847
+ }
848
+ return new Coordinates(json.lat, json.lng, json.alt, json.level);
792
849
  }
793
850
 
794
851
  /**
@@ -800,7 +857,22 @@ class Coordinates {
800
857
  output.push(this.alt);
801
858
  }
802
859
  if (this.level !== null) {
803
- output.push(this.level.toString());
860
+ output.push(this.level);
861
+ }
862
+ return output;
863
+ }
864
+
865
+ /**
866
+ * @returns {!Object}
867
+ * @deprecated
868
+ */
869
+ toLegacyCompressedJson() {
870
+ const output = [this.lat, this.lng];
871
+ if (this.alt !== null || this.level !== null) {
872
+ output.push(this.alt);
873
+ }
874
+ if (this.level !== null) {
875
+ output.push(Level.toString(this.level));
804
876
  }
805
877
  return output;
806
878
  }
@@ -810,6 +882,26 @@ class Coordinates {
810
882
  * @returns {!Coordinates}
811
883
  */
812
884
  static fromCompressedJson(json) {
885
+ const coords = new Coordinates(json[0], json[1]);
886
+ if (json.length > 2) {
887
+ coords.alt = json[2];
888
+ }
889
+ if (json.length > 3) {
890
+ if (typeof json[3] === 'string') {
891
+ Logger.warn('Still using legacy level format. Please update your project.');
892
+ coords.level = Level.fromString(json[3]);
893
+ } else {
894
+ coords.level = json[3];
895
+ }
896
+ }
897
+ return coords;
898
+ }
899
+
900
+ /**
901
+ * @param {!Object} json
902
+ * @returns {!Coordinates}
903
+ */
904
+ static fromLegacyCompressedJson(json) {
813
905
  const coords = new Coordinates(json[0], json[1]);
814
906
  if (json.length > 2) {
815
907
  coords.alt = json[2];
@@ -908,7 +1000,7 @@ class UserPosition extends Coordinates {
908
1000
  * @param {Number} eps latitude and longitude epsilon in degrees (default: 1e-8 [~1mm at lat=0])
909
1001
  * @param {Number} epsAlt altitude epsilon in meters (default: 1e-3 [= 1mm])
910
1002
  */
911
- static equalsTo(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
1003
+ static equals(pos1, pos2, eps = Constants.EPS_DEG_MM, epsAlt = Constants.EPS_MM) {
912
1004
 
913
1005
  // Handle null comparison
914
1006
  if (pos1 === null && pos1 === pos2) {
@@ -919,7 +1011,7 @@ class UserPosition extends Coordinates {
919
1011
  return false;
920
1012
  }
921
1013
 
922
- if (!super.equalsTo(pos1, pos2, eps, epsAlt)) {
1014
+ if (!super.equals(pos1, pos2, eps, epsAlt)) {
923
1015
  return false;
924
1016
  }
925
1017
 
@@ -928,8 +1020,8 @@ class UserPosition extends Coordinates {
928
1020
  && pos1.bearing === pos2.bearing;
929
1021
  }
930
1022
 
931
- equalsTo(other) {
932
- return UserPosition.equalsTo(this, other);
1023
+ equals(other) {
1024
+ return UserPosition.equals(this, other);
933
1025
  }
934
1026
 
935
1027
 
@@ -1044,14 +1136,14 @@ function trimRoute(route, startPosition = route[0], length = Number.MAX_VALUE) {
1044
1136
  const p1 = route[currentPointIndex - 1];
1045
1137
  const p2 = route[currentPointIndex];
1046
1138
 
1047
- if (Coordinates.equalsTo(startPosition, p1)) {
1139
+ if (Coordinates.equals(startPosition, p1)) {
1048
1140
  newRoute.push(p1);
1049
1141
  previousPoint = p1;
1050
1142
  break;
1051
1143
  }
1052
1144
 
1053
1145
  const proj = startPosition.getSegmentProjection(p1, p2);
1054
- if (proj && Coordinates.equalsTo(startPosition, proj) && !proj.equalsTo(p2)) {
1146
+ if (proj && Coordinates.equals(startPosition, proj) && !proj.equals(p2)) {
1055
1147
  newRoute.push(proj);
1056
1148
  previousPoint = proj;
1057
1149
  break;
@@ -1089,7 +1181,7 @@ function trimRoute(route, startPosition = route[0], length = Number.MAX_VALUE) {
1089
1181
  */
1090
1182
  function simplifyRoute(coords, precisionAngle = deg2rad(5)) {
1091
1183
 
1092
- const isClosed = (coords[0].equalsTo(coords[coords.length - 1]));
1184
+ const isClosed = (coords[0].equals(coords[coords.length - 1]));
1093
1185
 
1094
1186
  let newRoute = coords.slice(0, coords.length - (isClosed ? 1 : 0));
1095
1187
 
@@ -1336,13 +1428,13 @@ class BoundingBox {
1336
1428
  return this.northEast.lat;
1337
1429
  }
1338
1430
 
1339
- static equalsTo(bb1, bb2) {
1340
- return Coordinates.equalsTo(bb1.northEast, bb2.northEast)
1341
- && Coordinates.equalsTo(bb1.southWest, bb2.southWest);
1431
+ static equals(bb1, bb2) {
1432
+ return Coordinates.equals(bb1.northEast, bb2.northEast)
1433
+ && Coordinates.equals(bb1.southWest, bb2.southWest);
1342
1434
  }
1343
1435
 
1344
- equalsTo(other) {
1345
- return BoundingBox.equalsTo(this, other);
1436
+ equals(other) {
1437
+ return BoundingBox.equals(this, other);
1346
1438
  }
1347
1439
 
1348
1440
  /**
@@ -1478,7 +1570,7 @@ class RelativePosition {
1478
1570
  * @param {RelativePosition} pos2 position 2
1479
1571
  * @param {Number} eps x, y, z epsilon in meters (default: 1e-3 [= 1mm])
1480
1572
  */
1481
- static equalsTo(pos1, pos2, eps = Constants.EPS_MM) {
1573
+ static equals(pos1, pos2, eps = Constants.EPS_MM) {
1482
1574
 
1483
1575
  // Handle null comparison
1484
1576
  if (pos1 === null && pos1 === pos2) {
@@ -1497,8 +1589,8 @@ class RelativePosition {
1497
1589
  && pos1.bearing === pos2.bearing;
1498
1590
  }
1499
1591
 
1500
- equalsTo(other) {
1501
- return RelativePosition.equalsTo(this, other);
1592
+ equals(other) {
1593
+ return RelativePosition.equals(this, other);
1502
1594
  }
1503
1595
 
1504
1596
 
@@ -1715,7 +1807,7 @@ class Attitude {
1715
1807
  * @param {Attitude} att1 attitude 1
1716
1808
  * @param {Attitude} att2 attitude 2
1717
1809
  */
1718
- static equalsTo(att1, att2) {
1810
+ static equals(att1, att2) {
1719
1811
 
1720
1812
  // Handle null comparison
1721
1813
  if (att1 === null && att1 === att2) {
@@ -1731,15 +1823,15 @@ class Attitude {
1731
1823
  return true;
1732
1824
  }
1733
1825
 
1734
- return Quaternion.equalsTo(att1.quaternion, att2.quaternion);
1826
+ return Quaternion.equals(att1.quaternion, att2.quaternion);
1735
1827
  }
1736
1828
 
1737
1829
  /**
1738
1830
  * @param {Attitude} other
1739
1831
  * @returns {boolean}
1740
1832
  */
1741
- equalsTo(other) {
1742
- return Attitude.equalsTo(this, other);
1833
+ equals(other) {
1834
+ return Attitude.equals(this, other);
1743
1835
  }
1744
1836
 
1745
1837
  /**
@@ -1864,7 +1956,7 @@ class AbsoluteHeading {
1864
1956
  * @param {AbsoluteHeading} heading1 heading 1
1865
1957
  * @param {AbsoluteHeading} heading2 heading 2
1866
1958
  */
1867
- static equalsTo(heading1, heading2) {
1959
+ static equals(heading1, heading2) {
1868
1960
 
1869
1961
  // Handle null comparison
1870
1962
  if (heading1 === null && heading1 === heading2) {
@@ -1878,8 +1970,8 @@ class AbsoluteHeading {
1878
1970
  return Math.abs(heading1.heading - heading2.heading) < 1e-8;
1879
1971
  }
1880
1972
 
1881
- equalsTo(other) {
1882
- return AbsoluteHeading.equalsTo(this, other);
1973
+ equals(other) {
1974
+ return AbsoluteHeading.equals(this, other);
1883
1975
  }
1884
1976
 
1885
1977
  toJson() {
@@ -1964,8 +2056,8 @@ class GraphNode {
1964
2056
  * @param {GraphNode} other
1965
2057
  * @returns {boolean}
1966
2058
  */
1967
- equalsTo(other) {
1968
- return this.coords.equalsTo(other.coords)
2059
+ equals(other) {
2060
+ return this.coords.equals(other.coords)
1969
2061
  && this.builtFrom === other.builtFrom;
1970
2062
  }
1971
2063
 
@@ -1996,16 +2088,16 @@ class GraphNode {
1996
2088
  return new GraphNode(Coordinates.fromCompressedJson(json));
1997
2089
  }
1998
2090
 
1999
- generateLevelFromEdges() {
2091
+ _generateLevelFromEdges() {
2000
2092
  let tmpLevel = null;
2001
2093
  for (let i = 0; i < this.edges.length; i++) {
2002
2094
  const edge = this.edges[i];
2003
- if (edge.level) {
2004
- if (!tmpLevel) {
2005
- tmpLevel = edge.level.clone();
2095
+ if (edge.level !== null) {
2096
+ if (tmpLevel === null) {
2097
+ tmpLevel = Level.clone(edge.level);
2006
2098
  } else {
2007
- tmpLevel = tmpLevel.intersect(edge.level);
2008
- if (!tmpLevel) {
2099
+ tmpLevel = Level.intersection(tmpLevel, edge.level);
2100
+ if (tmpLevel === null) {
2009
2101
  Logger.error('Error: Something bad happend during parsing: We cannot retrieve node level from adjacent ways: ' + this.coords);
2010
2102
  return false;
2011
2103
  }
@@ -2020,9 +2112,9 @@ class GraphNode {
2020
2112
  /**
2021
2113
  * We suppose generateLevelFromEdges() was called before
2022
2114
  */
2023
- inferNodeLevelByRecursion() {
2115
+ _inferNodeLevelByRecursion() {
2024
2116
  const { level } = this.coords;
2025
- if (!level || !level.isRange) {
2117
+ if (level === null || !Level.isRange(level)) {
2026
2118
  return true;
2027
2119
  }
2028
2120
 
@@ -2038,16 +2130,17 @@ class GraphNode {
2038
2130
  * The result of this method is an union of all single level nodes found.
2039
2131
  * @param {GraphNode} node node to explore
2040
2132
  * @param {GraphNode[]} visitedNodes list of visited nodes
2133
+ * @returns {null|number|[number, number]}
2041
2134
  */
2042
2135
  const lookForLevel = (node, visitedNodes) => {
2043
2136
 
2044
2137
  visitedNodes.push(node);
2045
2138
 
2046
- if (!node.coords.level) {
2139
+ if (node.coords.level === null) {
2047
2140
  return null;
2048
2141
  }
2049
2142
 
2050
- if (!node.coords.level.isRange) {
2143
+ if (!Level.isRange(node.coords.level)) {
2051
2144
  return node.coords.level;
2052
2145
  }
2053
2146
 
@@ -2065,8 +2158,8 @@ class GraphNode {
2065
2158
  const othersLevels = lookForLevel(this, []);
2066
2159
 
2067
2160
  if (othersLevels !== null) {
2068
- if (!othersLevels.isRange) {
2069
- this.coords.level = new Level(othersLevels.val === level.low ? level.up : level.low);
2161
+ if (!Level.isRange(othersLevels)) {
2162
+ this.coords.level = othersLevels === level[0] ? level[1] : level[0];
2070
2163
  return true;
2071
2164
  }
2072
2165
  Logger.warn('Level of: ' + this.coords.toString() + ' cannot be decided');
@@ -2079,9 +2172,9 @@ class GraphNode {
2079
2172
  /**
2080
2173
  * We suppose generateLevelFromEdges() was called before
2081
2174
  */
2082
- inferNodeLevelByNeighboors() {
2175
+ _inferNodeLevelByNeighboors() {
2083
2176
  const { level } = this.coords;
2084
- if (!level || !level.isRange) {
2177
+ if (level === null || !Level.isRange(level)) {
2085
2178
  return true;
2086
2179
  }
2087
2180
 
@@ -2092,8 +2185,8 @@ class GraphNode {
2092
2185
  tmpLevel = Level.union(otherNode.coords.level, tmpLevel);
2093
2186
  }
2094
2187
 
2095
- if (tmpLevel === null || !tmpLevel.isRange) {
2096
- this.coords.level = new Level(tmpLevel.val === level.low ? level.up : level.low);
2188
+ if (tmpLevel === null || !Level.isRange(tmpLevel)) {
2189
+ this.coords.level = tmpLevel === level[0] ? level[1] : level[0];
2097
2190
  }
2098
2191
 
2099
2192
  return true;
@@ -2103,7 +2196,7 @@ class GraphNode {
2103
2196
  * Set node.io to true for nodes that make the link between
2104
2197
  * indoor and outdoor edges
2105
2198
  */
2106
- checkIO() {
2199
+ _checkIO() {
2107
2200
  this.io = this._coords.level !== null
2108
2201
  && this.edges.some(edge => edge.level === null);
2109
2202
  return true;
@@ -2113,7 +2206,7 @@ class GraphNode {
2113
2206
  * @param {GraphNode[]} nodes
2114
2207
  */
2115
2208
  static generateNodesLevels(nodes) {
2116
- const success = nodes.reduce((acc, node) => acc && node.generateLevelFromEdges(), true);
2209
+ const success = nodes.reduce((acc, node) => acc && node._generateLevelFromEdges(), true);
2117
2210
  if (!success) {
2118
2211
  return false;
2119
2212
  }
@@ -2122,12 +2215,12 @@ class GraphNode {
2122
2215
  // (e.g stairs without network at one of its bounds)
2123
2216
  // To infer this node level, we use inferNodeLevelByRecursion()
2124
2217
  const res = nodes.reduce((acc, node) => acc
2125
- && node.inferNodeLevelByNeighboors()
2126
- && node.inferNodeLevelByRecursion()
2218
+ && node._inferNodeLevelByNeighboors()
2219
+ && node._inferNodeLevelByRecursion()
2127
2220
  , true);
2128
2221
 
2129
2222
  // Finally define nodes that are links between indoor and outdoor
2130
- nodes.forEach(node => node.checkIO());
2223
+ nodes.forEach(node => node._checkIO());
2131
2224
 
2132
2225
  return res;
2133
2226
  }
@@ -2148,7 +2241,7 @@ class GraphEdge {
2148
2241
  /** @type {GraphNode<T>} */
2149
2242
  _node2 = null;
2150
2243
 
2151
- /** @type {?Level} */
2244
+ /** @type {null|number|[number, number]} */
2152
2245
  _level = null;
2153
2246
 
2154
2247
  /** @type {?number} */
@@ -2169,7 +2262,7 @@ class GraphEdge {
2169
2262
  /**
2170
2263
  * @param {!GraphNode} node1
2171
2264
  * @param {!GraphNode} node2
2172
- * @param {?Level} level
2265
+ * @param {?(null|number|[number, number])} level
2173
2266
  * @param {?T} builtFrom
2174
2267
  * @param {?string} name
2175
2268
  */
@@ -2224,21 +2317,15 @@ class GraphEdge {
2224
2317
  this._computedSizeAndBearing = false;
2225
2318
  }
2226
2319
 
2227
- /** @type {?Level} */
2320
+ /** @type {null|number|[number, number]} */
2228
2321
  get level() {
2229
2322
  return this._level;
2230
2323
  }
2231
2324
 
2232
- /** @type {?Level} */
2325
+ /** @type {null|number|[number, number]} */
2233
2326
  set level(level) {
2234
- if (level instanceof Level) {
2235
- this._level = level;
2236
- } else {
2237
- if (typeof level !== 'undefined' && level !== null) {
2238
- throw new Error('level argument is not a Level object');
2239
- }
2240
- this._level = null;
2241
- }
2327
+ Level.checkType(level);
2328
+ this._level = level;
2242
2329
  }
2243
2330
 
2244
2331
  /**
@@ -2273,7 +2360,7 @@ class GraphEdge {
2273
2360
  * @param {GraphEdge<T>} other
2274
2361
  * @returns {boolean}
2275
2362
  */
2276
- equalsTo(other) {
2363
+ equals(other) {
2277
2364
 
2278
2365
  if (this === other) {
2279
2366
  return true;
@@ -2283,9 +2370,9 @@ class GraphEdge {
2283
2370
  return false;
2284
2371
  }
2285
2372
 
2286
- return other.node1.equalsTo(this.node1)
2287
- && other.node2.equalsTo(this.node2)
2288
- && Level.equalsTo(other.level, this.level)
2373
+ return other.node1.equals(this.node1)
2374
+ && other.node2.equals(this.node2)
2375
+ && Level.equals(other.level, this.level)
2289
2376
  && other.isOneway === this.isOneway
2290
2377
  && other.builtFrom === this.builtFrom;
2291
2378
  }
@@ -2351,7 +2438,7 @@ class Network {
2351
2438
  * @returns {?GraphNode<T>}
2352
2439
  */
2353
2440
  getNodeByCoords(coords) {
2354
- return this.nodes.find(node => node.coords.equalsTo(coords));
2441
+ return this.nodes.find(node => node.coords.equals(coords));
2355
2442
  }
2356
2443
 
2357
2444
  /**
@@ -2427,7 +2514,7 @@ class Network {
2427
2514
  this.nodes.indexOf(edge.node2)
2428
2515
  ];
2429
2516
  if (edge.level !== null) {
2430
- output.push(edge.level.toString());
2517
+ output.push(edge.level);
2431
2518
  }
2432
2519
  if (edge.isOneway) {
2433
2520
  if (edge.level === null) {
@@ -2456,7 +2543,7 @@ class Network {
2456
2543
  network.nodes[jsonEdge[1]]
2457
2544
  );
2458
2545
  if (jsonEdge.length > 2 && jsonEdge[2] !== null) {
2459
- edge.level = Level.fromString(jsonEdge[2]);
2546
+ edge.level = jsonEdge[2];
2460
2547
  }
2461
2548
  if (jsonEdge.length > 3 && jsonEdge[3]) {
2462
2549
  edge.isOneway = true;
@@ -2478,7 +2565,7 @@ class Network {
2478
2565
  const network = new Network();
2479
2566
 
2480
2567
  const getOrCreateNode = coords =>
2481
- network.nodes.find(_coords => _coords.equalsTo(coords)) || new GraphNode(coords);
2568
+ network.nodes.find(_coords => _coords.equals(coords)) || new GraphNode(coords);
2482
2569
 
2483
2570
 
2484
2571
  const createEdgeFromNodes = (node1, node2) =>
@@ -2512,7 +2599,7 @@ class Network {
2512
2599
  getEdgesAtLevel(targetLevel, useMultiLevelEdges = true) {
2513
2600
  return this.edges.filter(
2514
2601
  ({ level }) => useMultiLevelEdges
2515
- ? Level.intersect(targetLevel, level) !== null
2602
+ ? Level.intersect(targetLevel, level)
2516
2603
  : Level.contains(targetLevel, level)
2517
2604
  );
2518
2605
  }
@@ -2607,37 +2694,29 @@ class MapMatching {
2607
2694
  */
2608
2695
  _shouldProjectOnEdgeAndNodes(edge, location, useBearing, useMultiLevelSegments, acceptEdgeFn) {
2609
2696
 
2697
+ // ignore projections if edge is not accepted
2610
2698
  if (!acceptEdgeFn(edge)) {
2611
- // if edge selection is not verified
2612
2699
  return [false, false, false];
2613
2700
  }
2614
2701
 
2615
- let checkNode1 = true;
2616
- let checkNode2 = true;
2617
- let checkEdge = true;
2702
+ // First, check if levels intersects
2703
+ let checkEdge = Level.intersect(location.level, edge.level);
2704
+ let checkNode1 = Level.intersect(location.level, edge.node1.coords.level);
2705
+ let checkNode2 = Level.intersect(location.level, edge.node2.coords.level);
2618
2706
 
2619
- if (
2620
- // Verify if edge level only if one of both is defined
2621
- (location.level || edge.level)
2622
- && (
2623
- // if edge level intersect location level
2624
- !Level.intersect(location.level, edge.level)
2625
- // ignore MultiLevelSegments if option used
2626
- || (!useMultiLevelSegments && edge.level && edge.level.isRange)
2627
- )) {
2628
- checkEdge = false;
2629
- }
2707
+ // Second, in case of IO nodes, accept matching if location's level is null
2708
+ checkNode1 ||= edge.node1.io && location.level === null;
2709
+ checkNode2 ||= edge.node2.io && location.level === null;
2630
2710
 
2631
- if (!Level.equalsTo(location.level, edge.node1.coords.level) && !(edge.node1.io && !location.level)) {
2632
- checkNode1 = false;
2633
- }
2634
-
2635
- if (!Level.equalsTo(location.level, edge.node2.coords.level) && !(edge.node2.io && !location.level)) {
2636
- checkNode2 = false;
2711
+ // Third, check if level is a range if useMultiLevelSegments is false
2712
+ if (!useMultiLevelSegments) {
2713
+ checkEdge &&= !Level.isRange(edge.level);
2714
+ checkNode1 &&= !Level.isRange(edge.node1.coords.level);
2715
+ checkNode2 &&= !Level.isRange(edge.node2.coords.level);
2637
2716
  }
2638
2717
 
2718
+ // Finally, check edges bearing if option is used
2639
2719
  if (useBearing) {
2640
- // if mapmatching bearing is enabled do not use nodes matching
2641
2720
  if (checkEdge) {
2642
2721
  // condition for optimisation
2643
2722
  const diffAngle = diffAngleLines(edge.bearing, location.bearing);
@@ -2646,6 +2725,7 @@ class MapMatching {
2646
2725
  checkEdge = false;
2647
2726
  }
2648
2727
  }
2728
+ // if mapmatching bearing is enabled do not use nodes matching
2649
2729
  checkNode1 = false;
2650
2730
  checkNode2 = false;
2651
2731
  }
@@ -2661,7 +2741,22 @@ class MapMatching {
2661
2741
  static _assignLatLngLevel(fromCoordinates, toCoordinates) {
2662
2742
  toCoordinates.lat = fromCoordinates.lat;
2663
2743
  toCoordinates.lng = fromCoordinates.lng;
2664
- toCoordinates.level = fromCoordinates.level;
2744
+ toCoordinates.level = Level.clone(fromCoordinates.level);
2745
+ }
2746
+
2747
+ /**
2748
+ * IO Nodes are typical because they have a non-null level but projection car works on them.
2749
+ * This function handles the case where the projection is on an IO node and a location with
2750
+ * a null level is required.
2751
+ *
2752
+ * @param {Coordinates} projection
2753
+ * @param {Coordinates} location
2754
+ * @param {GraphNode} projectionNode
2755
+ */
2756
+ static _handleLevelsWithIONodes(projection, location, projectionNode) {
2757
+ if (location.level === null && projectionNode.io) {
2758
+ projection.level = null;
2759
+ }
2665
2760
  }
2666
2761
 
2667
2762
  /**
@@ -2669,18 +2764,20 @@ class MapMatching {
2669
2764
  * @param {Coordinates} _projection
2670
2765
  */
2671
2766
  static _updateProjectionLevelFromEdge = (_edge, _projection) => {
2672
- if (_edge.level) {
2673
- _projection.level = _edge.level.clone();
2674
- }
2767
+ _projection.level = Level.clone(_edge.level);
2675
2768
  };
2676
2769
 
2677
2770
  /**
2771
+ * Main function for map-matching, the networks have to be set before calling this function
2772
+ * The function will returns a GraphProjection object given a coordinates object and a set
2773
+ * of options (useDistance, useBearing, useMultiLevelSegments, acceptEdgeFn).
2774
+ *
2678
2775
  * @param {!Coordinates} location
2679
2776
  * @param {boolean} useDistance
2680
2777
  * @param {boolean} useBearing
2681
2778
  * @param {boolean} useMultiLevelSegments
2682
2779
  * @param {function} acceptEdgeFn
2683
- * @returns {GraphProjection}
2780
+ * @returns {?GraphProjection}
2684
2781
  */
2685
2782
  getProjection(location, useDistance = false, useBearing = false,
2686
2783
  useMultiLevelSegments = true, acceptEdgeFn = () => true) {
@@ -2694,22 +2791,29 @@ class MapMatching {
2694
2791
  }
2695
2792
 
2696
2793
  if (useBearing && (!location.bearing || !this._maxAngleBearing)) {
2794
+ // If useBearing is true and bearing is not set in coordinates, return null
2697
2795
  return null;
2698
2796
  }
2699
2797
 
2798
+ // Build a new GraphProjection object from parameters
2700
2799
  const projection = new GraphProjection();
2701
2800
  projection.origin = location;
2702
2801
  projection.distanceFromNearestElement = Number.MAX_VALUE;
2703
2802
  projection.projection = location.clone();
2704
2803
 
2804
+ // Define a function to know if a projection is better than the current one
2705
2805
  const isProjectionBetter = (distanceOfNewProjection) => {
2706
2806
  return distanceOfNewProjection < projection.distanceFromNearestElement
2707
2807
  && (!useDistance || distanceOfNewProjection <= this._maxDistance);
2708
2808
  };
2709
2809
 
2710
- for (let i = 0; i < this.network.edges.length; i++) {
2711
- const edge = this.network.edges[i];
2810
+ // Loop on all the network edges
2811
+ // Each time a better projection is found (see isProjectionBetter()),
2812
+ // the current projection is replaced
2813
+ for (const edge of this.network.edges) {
2712
2814
 
2815
+ // Check if the specified edge and its nodes can be used for projection. See the
2816
+ // documentation of the corresponding function for more information.
2713
2817
  const [checkEdge, checkNode1, checkNode2] = this._shouldProjectOnEdgeAndNodes(
2714
2818
  edge, location, useBearing, useMultiLevelSegments, acceptEdgeFn);
2715
2819
 
@@ -2720,10 +2824,9 @@ class MapMatching {
2720
2824
  projection.distanceFromNearestElement = distNode1;
2721
2825
  projection.nearestElement = edge.node1;
2722
2826
  MapMatching._assignLatLngLevel(edge.node1.coords, projection.projection);
2723
- MapMatching._updateProjectionLevelFromEdge(edge, projection.projection);
2827
+ MapMatching._handleLevelsWithIONodes(projection.projection, location, edge.node1);
2724
2828
 
2725
- if (distNode1 <= Constants.EPS_MM
2726
- && location.level === edge.node1.coords.level) {
2829
+ if (distNode1 <= Constants.EPS_MM) {
2727
2830
  break;
2728
2831
  }
2729
2832
  }
@@ -2733,14 +2836,12 @@ class MapMatching {
2733
2836
 
2734
2837
  const distNode2 = location.distanceTo(edge.node2.coords);
2735
2838
  if (isProjectionBetter(distNode2) || distNode2 <= Constants.EPS_MM) {
2736
-
2737
2839
  projection.distanceFromNearestElement = distNode2;
2738
2840
  projection.nearestElement = edge.node2;
2739
2841
  MapMatching._assignLatLngLevel(edge.node2.coords, projection.projection);
2740
- MapMatching._updateProjectionLevelFromEdge(edge, projection.projection);
2842
+ MapMatching._handleLevelsWithIONodes(projection.projection, location, edge.node2);
2741
2843
 
2742
- if (distNode2 <= Constants.EPS_MM
2743
- && location.level === edge.node2.coords.level) {
2844
+ if (distNode2 <= Constants.EPS_MM) {
2744
2845
  break;
2745
2846
  }
2746
2847
  }
@@ -2903,11 +3004,11 @@ class GraphRouterOptions {
2903
3004
  */
2904
3005
  class GraphRouter {
2905
3006
 
2906
- /** @type {!Network<T>} */
3007
+ /** @type {!(Network<T>)} */
2907
3008
  _network;
2908
3009
 
2909
3010
  /**
2910
- * @param {!Network<T>} network
3011
+ * @param {!(Network<T>)} network
2911
3012
  */
2912
3013
  constructor(network) {
2913
3014
  this._network = network;
@@ -2954,9 +3055,9 @@ class GraphRouter {
2954
3055
  if (!proj) {
2955
3056
  let message = `Point ${point.toString()} is too far from the network `
2956
3057
  + `> ${this._mapMatching.maxDistance.toFixed(0)} meters.`;
2957
- if (point.level) {
3058
+ if (point.level !== null) {
2958
3059
  message += ' If it is a multi-level map, please verify if you have'
2959
- + ` a network at level ${point.level.toString()}.`;
3060
+ + ` a network at level ${Level.toString(point.level)}.`;
2960
3061
  }
2961
3062
  throw new NoRouteFoundError(start, end, message);
2962
3063
  }