@mapvx/web-js 1.2.1 → 1.3.0-dev.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.
Files changed (95) hide show
  1. package/README.md +2 -0
  2. package/dist/cjs/controllers/routeController.js +19 -19
  3. package/dist/cjs/controllers/routeController.js.map +1 -1
  4. package/dist/cjs/domain/models/categories.js +23 -10
  5. package/dist/cjs/domain/models/categories.js.map +1 -1
  6. package/dist/cjs/domain/models/mapConfig.js +10 -1
  7. package/dist/cjs/domain/models/mapConfig.js.map +1 -1
  8. package/dist/cjs/domain/models/marker.js +86 -80
  9. package/dist/cjs/domain/models/marker.js.map +1 -1
  10. package/dist/cjs/domain/models/routeConfiguration.js +3 -1
  11. package/dist/cjs/domain/models/routeConfiguration.js.map +1 -1
  12. package/dist/cjs/index.js +17 -12
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/logger/logger.js +13 -8
  15. package/dist/cjs/logger/logger.js.map +1 -1
  16. package/dist/cjs/logger/rollbar.js +11 -6
  17. package/dist/cjs/logger/rollbar.js.map +1 -1
  18. package/dist/cjs/map/map.js +549 -28
  19. package/dist/cjs/map/map.js.map +1 -1
  20. package/dist/cjs/map/mapInteractionOptions.js +56 -0
  21. package/dist/cjs/map/mapInteractionOptions.js.map +1 -0
  22. package/dist/cjs/repository/repository.js +25 -26
  23. package/dist/cjs/repository/repository.js.map +1 -1
  24. package/dist/cjs/repository/requester.js +71 -91
  25. package/dist/cjs/repository/requester.js.map +1 -1
  26. package/dist/cjs/sdk.js +18 -0
  27. package/dist/cjs/sdk.js.map +1 -1
  28. package/dist/cjs/utils/3d.js +12 -0
  29. package/dist/cjs/utils/3d.js.map +1 -0
  30. package/dist/cjs/utils/semaphore.js +143 -0
  31. package/dist/cjs/utils/semaphore.js.map +1 -0
  32. package/dist/es/controllers/routeController.d.ts.map +1 -1
  33. package/dist/es/controllers/routeController.js +19 -19
  34. package/dist/es/controllers/routeController.js.map +1 -1
  35. package/dist/es/domain/models/categories.d.ts +34 -10
  36. package/dist/es/domain/models/categories.d.ts.map +1 -1
  37. package/dist/es/domain/models/categories.js +21 -9
  38. package/dist/es/domain/models/categories.js.map +1 -1
  39. package/dist/es/domain/models/configuration.d.ts +8 -0
  40. package/dist/es/domain/models/configuration.d.ts.map +1 -1
  41. package/dist/es/domain/models/mapConfig.d.ts +129 -3
  42. package/dist/es/domain/models/mapConfig.d.ts.map +1 -1
  43. package/dist/es/domain/models/mapConfig.js +9 -0
  44. package/dist/es/domain/models/mapConfig.js.map +1 -1
  45. package/dist/es/domain/models/marker.d.ts +8 -0
  46. package/dist/es/domain/models/marker.d.ts.map +1 -1
  47. package/dist/es/domain/models/marker.js +86 -80
  48. package/dist/es/domain/models/marker.js.map +1 -1
  49. package/dist/es/domain/models/routeConfiguration.d.ts +47 -0
  50. package/dist/es/domain/models/routeConfiguration.d.ts.map +1 -1
  51. package/dist/es/domain/models/routeConfiguration.js +3 -1
  52. package/dist/es/domain/models/routeConfiguration.js.map +1 -1
  53. package/dist/es/index.d.ts +13 -12
  54. package/dist/es/index.d.ts.map +1 -1
  55. package/dist/es/index.js +8 -6
  56. package/dist/es/index.js.map +1 -1
  57. package/dist/es/logger/logger.d.ts.map +1 -1
  58. package/dist/es/logger/logger.js +13 -8
  59. package/dist/es/logger/logger.js.map +1 -1
  60. package/dist/es/logger/rollbar.d.ts.map +1 -1
  61. package/dist/es/logger/rollbar.js +11 -6
  62. package/dist/es/logger/rollbar.js.map +1 -1
  63. package/dist/es/map/map.d.ts +106 -1
  64. package/dist/es/map/map.d.ts.map +1 -1
  65. package/dist/es/map/map.js +551 -30
  66. package/dist/es/map/map.js.map +1 -1
  67. package/dist/es/map/mapInteractionOptions.d.ts +37 -0
  68. package/dist/es/map/mapInteractionOptions.d.ts.map +1 -0
  69. package/dist/es/map/mapInteractionOptions.js +51 -0
  70. package/dist/es/map/mapInteractionOptions.js.map +1 -0
  71. package/dist/es/repository/repository.d.ts +0 -1
  72. package/dist/es/repository/repository.d.ts.map +1 -1
  73. package/dist/es/repository/repository.js +25 -26
  74. package/dist/es/repository/repository.js.map +1 -1
  75. package/dist/es/repository/requester.d.ts +12 -2
  76. package/dist/es/repository/requester.d.ts.map +1 -1
  77. package/dist/es/repository/requester.js +71 -91
  78. package/dist/es/repository/requester.js.map +1 -1
  79. package/dist/es/sdk.d.ts +2 -0
  80. package/dist/es/sdk.d.ts.map +1 -1
  81. package/dist/es/sdk.js +18 -0
  82. package/dist/es/sdk.js.map +1 -1
  83. package/dist/es/utils/3d.d.ts +2 -0
  84. package/dist/es/utils/3d.d.ts.map +1 -0
  85. package/dist/es/utils/3d.js +8 -0
  86. package/dist/es/utils/3d.js.map +1 -0
  87. package/dist/es/utils/semaphore.d.ts +70 -0
  88. package/dist/es/utils/semaphore.d.ts.map +1 -0
  89. package/dist/es/utils/semaphore.js +139 -0
  90. package/dist/es/utils/semaphore.js.map +1 -0
  91. package/dist/umd/index.js +154429 -2148
  92. package/dist/umd/index.js.map +1 -1
  93. package/dist/umd/styles.css +32 -14
  94. package/dist/umd/styles.css.map +1 -1
  95. package/package.json +19 -5
@@ -1,4 +1,9 @@
1
- import maplibregl, { LngLatBounds, Map, NavigationControl, setRTLTextPlugin, } from "maplibre-gl";
1
+ import { MVTLayer } from "@deck.gl/geo-layers";
2
+ import { IconLayer, PathLayer, TextLayer } from "@deck.gl/layers";
3
+ import maplibregl, { LngLatBounds, Map, MercatorCoordinate, NavigationControl, setRTLTextPlugin, } from "maplibre-gl";
4
+ import * as THREE from "three";
5
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
6
+ import { Semaphore } from "../utils/semaphore";
2
7
  /** Shared GeoJSON source holding every circle drawn through the circle API. */
3
8
  const CIRCLE_SOURCE_ID = "mapvx-circles";
4
9
  /** Fill layer rendering the translucent interior of the circles. */
@@ -43,21 +48,36 @@ function deepClone(obj) {
43
48
  /**
44
49
  * Register a custom protocol for cached tiles that routes requests through the main thread.
45
50
  * This allows the service worker to intercept and cache tile requests.
51
+ *
52
+ * Wraps fetches in a {@link Semaphore} so tile CDN / WAFs are not hit with
53
+ * unbounded parallel requests (same host as the service worker cache). The
54
+ * semaphore is created once with the limit from the first map's config; later
55
+ * calls are no-ops because the protocol can only be registered once globally
56
+ * on MapLibre.
57
+ *
58
+ * @param maxConcurrentFetches - Maximum number of in-flight tile fetches.
46
59
  */
47
- function registerCachedTileProtocol() {
60
+ function registerCachedTileProtocol(maxConcurrentFetches) {
48
61
  if (cachedTileProtocolRegistered)
49
62
  return;
63
+ const semaphore = new Semaphore(maxConcurrentFetches);
50
64
  maplibregl.addProtocol("cached-tile", (params, abortController) => {
51
- // Convert cached-tile:// URL back to https://
52
65
  const url = params.url.replace("cached-tile://", "https://");
53
- return fetch(url, { signal: abortController.signal })
66
+ // Pass the abort signal to acquire() so a request cancelled while still
67
+ // queued (e.g. during rapid zoom) drops out of the FIFO queue instead of
68
+ // waiting for a slot just to bail. release() runs only inside this chain,
69
+ // i.e. only after a slot was actually granted.
70
+ return semaphore.acquire(abortController.signal).then(() => fetch(url, { signal: abortController.signal })
54
71
  .then((response) => {
55
72
  if (!response.ok) {
56
73
  throw new Error(`HTTP error! status: ${response.status}`);
57
74
  }
58
75
  return response.arrayBuffer();
59
76
  })
60
- .then((data) => ({ data }));
77
+ .then((data) => ({ data }))
78
+ .finally(() => {
79
+ semaphore.release();
80
+ }));
61
81
  });
62
82
  cachedTileProtocolRegistered = true;
63
83
  }
@@ -111,19 +131,21 @@ function convertPaddingToPixels(padding, containerWidth, containerHeight) {
111
131
  right: parsePaddingValue(padding.right, containerWidth),
112
132
  };
113
133
  }
134
+ import { MapboxOverlay } from "@deck.gl/mapbox";
114
135
  import { userLocationDataUrl } from "../assets/icons";
115
136
  import { RouteController } from "../controllers/routeController";
116
137
  import { rtlLanguages } from "../domain/models/_rtl";
117
138
  import { InternalAnimationConfig, InternalAnimationDrawingConfig, } from "../domain/models/animation";
139
+ import { CIRCLE_RENDER_ORDERS, circleRing, cloneCircleRecord, MAPVX_BRAND_COLOR, resolveCircleConfig, } from "../domain/models/circle";
118
140
  import { Loggeable } from "../domain/models/loggeable";
119
- import { DEFAULT_TILE_CACHE_CONFIG, } from "../domain/models/mapConfig";
120
- import { circleRing, cloneCircleRecord, MAPVX_BRAND_COLOR, resolveCircleConfig, } from "../domain/models/circle";
141
+ import { DEFAULT_TILE_CACHE_CONFIG, MAPLIBRE_MAX_TILE_CACHE_HARD_CAP, } from "../domain/models/mapConfig";
121
142
  import { MarkerAttribute } from "../domain/models/marker";
122
143
  import { MVXRoute } from "../domain/models/route";
123
144
  import { InternalDrawRouteConfiguration, InternalGetRouteConfiguration, } from "../domain/models/routeConfiguration";
124
145
  import { Repository } from "../repository/repository";
125
- import { getBoundingBox } from "../utils/utils";
126
146
  import { extractStepCoordinates } from "../utils/route-utils";
147
+ import { getBoundingBox } from "../utils/utils";
148
+ import { buildInteractionOptions, shouldDisableTouchRotation } from "./mapInteractionOptions";
127
149
  /**
128
150
  * Class to interact with the map.
129
151
  * @category Map
@@ -139,7 +161,7 @@ export class InternalMapVXMap extends Loggeable {
139
161
  * @returns A new instance of MapVXMap.
140
162
  */
141
163
  constructor(mapConfig, container, token) {
142
- var _a, _b;
164
+ var _a, _b, _c;
143
165
  super();
144
166
  this.potentialParentPlaces = [];
145
167
  this.innerFloors = [];
@@ -152,6 +174,22 @@ export class InternalMapVXMap extends Loggeable {
152
174
  this.hoveredId = "unselected";
153
175
  this.failedTiles = new Set();
154
176
  this.geoLocation = navigator.geolocation;
177
+ // 3d related variables
178
+ this.mode = "2D";
179
+ this.deckOverlay = undefined;
180
+ this.escalatorScene = undefined;
181
+ this.escalatorModelTemplate = undefined;
182
+ this.escalatorCenter = undefined;
183
+ this.escalatorCache = {};
184
+ this.ESCALATOR_MODEL_URL = "https://mapvx-glb-assets.s3.us-east-1.amazonaws.com/shared/escalator.glb";
185
+ /**
186
+ * Number of lights to use for the escalators
187
+ *
188
+ */
189
+ this.ESCALATOR_LIGHTS = 3;
190
+ this.SPRITE_URL = "https://lazarillo.app/internal/maps/vector-style/cenco-cl-pe-co-ar-3D/sprites_cencosud";
191
+ this.spriteIconMapping = {};
192
+ this.spriteAtlasImage = new Image();
155
193
  if (rtlLanguages.includes((_a = mapConfig.lang) !== null && _a !== void 0 ? _a : "")) {
156
194
  this.setRTLSupport();
157
195
  }
@@ -163,8 +201,16 @@ export class InternalMapVXMap extends Loggeable {
163
201
  this.watchPositionID = undefined;
164
202
  this.onFloorChange = mapConfig.onFloorChange;
165
203
  this.onParentPlaceChange = mapConfig.onParentPlaceChange;
166
- // Merge tile cache config with defaults
167
- this.tileCacheConfig = Object.assign(Object.assign({}, DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
204
+ this.mode = (_c = mapConfig.mode) !== null && _c !== void 0 ? _c : "2D";
205
+ this.initialCenter = mapConfig.center;
206
+ this.tileCacheConfig = (() => {
207
+ const merged = Object.assign(Object.assign({}, DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
208
+ merged.maxTiles = Math.min(merged.maxTiles, MAPLIBRE_MAX_TILE_CACHE_HARD_CAP);
209
+ return merged;
210
+ })();
211
+ if (this.mode === "3D") {
212
+ this.loadSpriteAtlas().catch(console.error);
213
+ }
168
214
  if (mapConfig.parentPlaceId != null) {
169
215
  this.initialPlaceDetailSetUp(mapConfig.parentPlaceId, mapConfig.authToken);
170
216
  }
@@ -261,28 +307,17 @@ export class InternalMapVXMap extends Loggeable {
261
307
  .catch(console.error);
262
308
  }
263
309
  onMapStyleLoaded(mapConfig, container, style) {
264
- var _a, _b, _c;
310
+ var _a, _b, _c, _d, _e;
265
311
  // Determine if service worker caching should be enabled
266
312
  const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
267
- // Register cached-tile protocol only if service worker caching is enabled
268
313
  if (useServiceWorkerCaching) {
269
- registerCachedTileProtocol();
314
+ registerCachedTileProtocol(this.tileCacheConfig.maxConcurrentTileFetches);
270
315
  }
271
316
  // Transform tile URLs only if service worker caching is enabled
272
317
  const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
273
- const mapOptions = {
274
- container,
275
- style: finalStyle,
276
- center: mapConfig.center,
277
- zoom: mapConfig.zoom,
278
- pitch: (_a = mapConfig.pitch) !== null && _a !== void 0 ? _a : 0,
279
- attributionControl: false,
280
- maplibreLogo: false,
281
- bearingSnap: (_b = mapConfig.bearingSnap) !== null && _b !== void 0 ? _b : 0,
282
- cancelPendingTileRequestsWhileZooming: false,
318
+ const mapOptions = Object.assign({ container, style: finalStyle, center: mapConfig.center, zoom: mapConfig.zoom, pitch: (_a = mapConfig.pitch) !== null && _a !== void 0 ? _a : 0, attributionControl: false, maplibreLogo: false, bearingSnap: (_b = mapConfig.bearingSnap) !== null && _b !== void 0 ? _b : 0, cancelPendingTileRequestsWhileZooming: true,
283
319
  // Use configured maxTiles for MapLibre's memory cache
284
- maxTileCacheSize: this.tileCacheConfig.maxTiles,
285
- };
320
+ maxTileCacheSize: this.tileCacheConfig.maxTiles }, buildInteractionOptions(mapConfig));
286
321
  if (mapConfig.maxZoom)
287
322
  mapOptions.maxZoom = mapConfig.maxZoom;
288
323
  if (mapConfig.minZoom)
@@ -292,12 +327,41 @@ export class InternalMapVXMap extends Loggeable {
292
327
  mapOptions.maxBounds = new LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
293
328
  }
294
329
  this.map = new Map(mapOptions);
330
+ // When rotation is disabled we still want pinch-to-zoom to work, so the
331
+ // two-finger rotation is turned off here instead of via the constructor.
332
+ if (shouldDisableTouchRotation(mapConfig)) {
333
+ (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.disableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
334
+ }
295
335
  this.map.addControl(new NavigationControl({
296
336
  showCompass: mapConfig.showCompass !== undefined ? mapConfig.showCompass : true,
297
337
  showZoom: mapConfig.showZoom !== undefined ? mapConfig.showZoom : true,
298
- }), (_c = mapConfig.navigationPosition) !== null && _c !== void 0 ? _c : "top-right");
338
+ }), (_e = mapConfig.navigationPosition) !== null && _e !== void 0 ? _e : "top-right");
299
339
  this.map.on("load", () => {
300
340
  var _a;
341
+ if (this.mode === "3D") {
342
+ this.deckOverlay = new MapboxOverlay({ layers: [] });
343
+ this.map.addControl(this.deckOverlay);
344
+ this.hideIndoorSymbolLayers();
345
+ // Hide MapLibre icon layers — replaced by deck.gl IconLayer
346
+ this.map.setLayoutProperty("indoor-poi-logo", "visibility", "none");
347
+ const style = this.map.getStyle();
348
+ for (const layer of style.layers) {
349
+ if (layer.id.startsWith("indoor-logo-tiendas-ancla")) {
350
+ this.map.setLayoutProperty(layer.id, "visibility", "none");
351
+ }
352
+ }
353
+ // Add 3D escalator layer and hide 2D transportation line
354
+ this.map.addLayer(this.createEscalatorLayer());
355
+ this.map.setPaintProperty("indoor-transportation", "line-opacity", 0);
356
+ // Capture escalator positions when new tiles arrive (stops once cached)
357
+ this.map.on("sourcedata", (e) => {
358
+ if (e.sourceId === "indoorequal" &&
359
+ this.currentFloor &&
360
+ !this.escalatorCache[this.currentFloor]) {
361
+ this.placeEscalators();
362
+ }
363
+ });
364
+ }
301
365
  this.whenStyleUpdates(style);
302
366
  if (mapConfig.lang)
303
367
  this.setLayersForLanguage(mapConfig.lang);
@@ -320,7 +384,6 @@ export class InternalMapVXMap extends Loggeable {
320
384
  (_a = mapConfig.onRotate) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.map.getBearing());
321
385
  });
322
386
  this.defaultClickListener();
323
- return this.map;
324
387
  }
325
388
  setBaseFilters(style) {
326
389
  if (!style)
@@ -449,6 +512,386 @@ export class InternalMapVXMap extends Loggeable {
449
512
  // Tile cache clear may fail if cache doesn't exist
450
513
  }
451
514
  }
515
+ // Start 3D related methods ------------------------------------------------------------
516
+ hideIndoorSymbolLayers() {
517
+ var _a, _b;
518
+ const style = this.map.getStyle();
519
+ for (const layer of style.layers) {
520
+ if (layer.type === "symbol" && layer.id.startsWith("indoor-")) {
521
+ const hasText = !!((_a = layer.layout) === null || _a === void 0 ? void 0 : _a["text-field"]);
522
+ const hasIcon = !!((_b = layer.layout) === null || _b === void 0 ? void 0 : _b["icon-image"]);
523
+ // Hide text-only layers (replaced by deck.gl)
524
+ if (hasText && !hasIcon) {
525
+ this.map.setLayoutProperty(layer.id, "visibility", "none");
526
+ }
527
+ }
528
+ }
529
+ }
530
+ createEscalatorLayer() {
531
+ const refMercator = MercatorCoordinate.fromLngLat(this.initialCenter, 0);
532
+ const meterScale = refMercator.meterInMercatorCoordinateUnits();
533
+ const modelTransform = new THREE.Matrix4()
534
+ .makeTranslation(refMercator.x, refMercator.y, refMercator.z)
535
+ .scale(new THREE.Vector3(meterScale, -meterScale, meterScale))
536
+ .multiply(new THREE.Matrix4().makeRotationX(Math.PI / 2));
537
+ this.escalatorScene = new THREE.Scene();
538
+ const dirLight = new THREE.DirectionalLight(0xffffff, 2.0);
539
+ dirLight.position.set(50, 100, 80);
540
+ this.escalatorScene.add(dirLight);
541
+ const dirLight2 = new THREE.DirectionalLight(0xffffff, 1.5);
542
+ dirLight2.position.set(-50, 80, -60);
543
+ this.escalatorScene.add(dirLight2);
544
+ this.escalatorScene.add(new THREE.AmbientLight(0xffffff, 1.5));
545
+ new GLTFLoader().load(this.ESCALATOR_MODEL_URL, (gltf) => {
546
+ this.escalatorModelTemplate = gltf.scene;
547
+ const box = new THREE.Box3().setFromObject(this.escalatorModelTemplate);
548
+ this.escalatorCenter = box.getCenter(new THREE.Vector3());
549
+ this.escalatorCenter.y = box.min.y; // align bottom to ground
550
+ this.placeEscalators();
551
+ });
552
+ let renderer = undefined;
553
+ let camera = undefined;
554
+ const tmpMatrix = new THREE.Matrix4();
555
+ return {
556
+ id: "3d-escalators",
557
+ type: "custom",
558
+ renderingMode: "3d",
559
+ onAdd(_map, gl) {
560
+ camera = new THREE.Camera();
561
+ renderer = new THREE.WebGLRenderer({
562
+ canvas: _map.getCanvas(),
563
+ context: gl,
564
+ antialias: true,
565
+ });
566
+ renderer.autoClear = false;
567
+ },
568
+ render: (gl, args) => {
569
+ var _a, _b;
570
+ if (!camera ||
571
+ !renderer ||
572
+ !this.escalatorModelTemplate ||
573
+ !this.escalatorScene ||
574
+ this.escalatorScene.children.length <= this.ESCALATOR_LIGHTS)
575
+ return;
576
+ const matrixData = (_b = (_a = args === null || args === void 0 ? void 0 : args.defaultProjectionData) === null || _a === void 0 ? void 0 : _a.mainMatrix) !== null && _b !== void 0 ? _b : args;
577
+ tmpMatrix.fromArray(matrixData).multiply(modelTransform);
578
+ camera.projectionMatrix.copy(tmpMatrix);
579
+ renderer.resetState();
580
+ renderer.render(this.escalatorScene, camera);
581
+ },
582
+ };
583
+ }
584
+ placeEscalators() {
585
+ if (!this.escalatorModelTemplate ||
586
+ !this.escalatorScene ||
587
+ !this.currentFloor ||
588
+ !this.escalatorCenter)
589
+ return;
590
+ // Try to capture positions if not cached yet
591
+ this.captureEscalatorPositions();
592
+ const positions = this.escalatorCache[this.currentFloor];
593
+ if (!positions)
594
+ return;
595
+ // Clear scene models (keep lights)
596
+ while (this.escalatorScene.children.length > this.ESCALATOR_LIGHTS) {
597
+ const lastChild = this.escalatorScene.children.at(-1);
598
+ if (lastChild) {
599
+ this.escalatorScene.remove(lastChild);
600
+ }
601
+ }
602
+ const refMercator = MercatorCoordinate.fromLngLat(this.initialCenter, 0);
603
+ const meterScale = refMercator.meterInMercatorCoordinateUnits();
604
+ for (const pos of positions) {
605
+ const target = maplibregl.MercatorCoordinate.fromLngLat([pos.lng, pos.lat], 0);
606
+ const sx = (target.x - refMercator.x) / meterScale;
607
+ const sz = (target.y - refMercator.y) / meterScale;
608
+ const pivot = new THREE.Group();
609
+ pivot.position.set(sx, 0, sz);
610
+ pivot.rotation.y = Math.PI / 2 - pos.bearing;
611
+ pivot.scale.set(0.3, 0.3, 0.3);
612
+ const model = this.escalatorModelTemplate.clone();
613
+ model.position.set(-this.escalatorCenter.x, -this.escalatorCenter.y - 3, -this.escalatorCenter.z);
614
+ pivot.add(model);
615
+ this.escalatorScene.add(pivot);
616
+ }
617
+ this.map.triggerRepaint();
618
+ }
619
+ captureEscalatorPositions() {
620
+ if (this.currentFloor && this.escalatorCache[this.currentFloor])
621
+ return;
622
+ // queryRenderedFeatures uses exact rendered coordinates (not tile-approximated)
623
+ const features = this.map
624
+ .queryRenderedFeatures(undefined, {
625
+ layers: ["indoor-transportation"],
626
+ })
627
+ .filter((f) => { var _a; return ((_a = f.properties) === null || _a === void 0 ? void 0 : _a.floor_key) === this.currentFloor; });
628
+ if (!features.length)
629
+ return;
630
+ // Collect all midpoints first
631
+ const allMids = [];
632
+ for (const f of features) {
633
+ const coords = f.geometry.coordinates;
634
+ if (!coords || coords.length < 2)
635
+ continue;
636
+ const start = coords[0];
637
+ const end = coords[coords.length - 1];
638
+ const midLng = (start[0] + end[0]) / 2;
639
+ const midLat = (start[1] + end[1]) / 2;
640
+ const bearing = Math.atan2(end[0] - start[0], end[1] - start[1]);
641
+ allMids.push({ lng: midLng, lat: midLat, bearing });
642
+ }
643
+ // Merge points within ~15m of each other into one
644
+ const MERGE_DEG = 0.00015; // ~15m
645
+ const positions = [];
646
+ const used = new Set();
647
+ for (let i = 0; i < allMids.length; i++) {
648
+ if (used.has(i))
649
+ continue;
650
+ used.add(i);
651
+ const group = [allMids[i]];
652
+ for (let j = i + 1; j < allMids.length; j++) {
653
+ if (used.has(j))
654
+ continue;
655
+ const dLng = allMids[i].lng - allMids[j].lng;
656
+ const dLat = allMids[i].lat - allMids[j].lat;
657
+ if (Math.abs(dLng) < MERGE_DEG && Math.abs(dLat) < MERGE_DEG) {
658
+ used.add(j);
659
+ group.push(allMids[j]);
660
+ }
661
+ }
662
+ // Use average position and first bearing
663
+ const avgLng = group.reduce((s, p) => s + p.lng, 0) / group.length;
664
+ const avgLat = group.reduce((s, p) => s + p.lat, 0) / group.length;
665
+ positions.push({ lng: avgLng, lat: avgLat, bearing: group[0].bearing });
666
+ }
667
+ if (positions.length && this.currentFloor) {
668
+ this.escalatorCache[this.currentFloor] = positions;
669
+ }
670
+ }
671
+ async loadSpriteAtlas() {
672
+ const [json, img] = await Promise.all([
673
+ fetch(`${this.SPRITE_URL}.json`).then((r) => r.json()),
674
+ new Promise((resolve, reject) => {
675
+ const image = new Image();
676
+ image.crossOrigin = "anonymous";
677
+ image.onload = () => resolve(image);
678
+ image.onerror = reject;
679
+ image.src = `${this.SPRITE_URL}.png`;
680
+ }),
681
+ ]);
682
+ this.spriteIconMapping = json;
683
+ this.spriteAtlasImage = img;
684
+ }
685
+ makeMVTLayer(floorKey) {
686
+ return new MVTLayer({
687
+ id: "indoor-lines-elevated",
688
+ data: "https://tiles.mapvx.com/",
689
+ binary: false,
690
+ renderSubLayers: (props) => {
691
+ var _a;
692
+ const roomData = (props.data || []);
693
+ const roomFeatures = roomData.filter((f) => {
694
+ var _a, _b, _c;
695
+ return ((_a = f.properties) === null || _a === void 0 ? void 0 : _a.class) === "room" &&
696
+ ((_b = f.properties) === null || _b === void 0 ? void 0 : _b.subclass) !== "empty" &&
697
+ ((_c = f.properties) === null || _c === void 0 ? void 0 : _c.floor_key) === floorKey;
698
+ });
699
+ if (!roomFeatures.length)
700
+ return null;
701
+ const paths = [];
702
+ for (const f of roomFeatures) {
703
+ const z = ((_a = f.properties) === null || _a === void 0 ? void 0 : _a.height) ? Number(f.properties.height) : 5;
704
+ const geom = f.geometry;
705
+ const polygons = geom.type === "Polygon"
706
+ ? [geom.coordinates]
707
+ : geom.type === "MultiPolygon"
708
+ ? geom.coordinates
709
+ : [];
710
+ for (const rings of polygons) {
711
+ for (const ring of rings) {
712
+ paths.push({ path: ring.map(([lon, lat]) => [lon, lat, z]) });
713
+ }
714
+ }
715
+ }
716
+ return new PathLayer(Object.assign(Object.assign({}, props), { id: `${props.id}-paths`, data: paths, getPath: (d) => d.path, getColor: [203, 203, 203], getWidth: 1.5, widthUnits: "pixels", pickable: false }));
717
+ },
718
+ updateTriggers: {
719
+ renderSubLayers: [floorKey],
720
+ },
721
+ });
722
+ }
723
+ makeLabelLayer(floorKey, useSdf = true) {
724
+ return new MVTLayer({
725
+ id: "indoor-labels",
726
+ data: "https://tiles.mapvx.com/",
727
+ binary: false,
728
+ minZoom: 19.5,
729
+ maxZoom: 24,
730
+ renderSubLayers: (props) => {
731
+ // Use area_name and poi Point features — unique per tile, no duplicates
732
+ // If logo-{name} image exists in sprite, icon has priority over text label
733
+ const labelData = (props.data || []);
734
+ const labelFeatures = labelData.filter((f) => {
735
+ var _a, _b, _c, _d, _e, _f;
736
+ if (((_a = f.properties) === null || _a === void 0 ? void 0 : _a.layerName) !== "area_name" && ((_b = f.properties) === null || _b === void 0 ? void 0 : _b.layerName) !== "poi")
737
+ return false;
738
+ if (((_c = f.geometry) === null || _c === void 0 ? void 0 : _c.type) !== "Point")
739
+ return false;
740
+ const name = ((_d = f.properties) === null || _d === void 0 ? void 0 : _d["name:latin"]) || ((_e = f.properties) === null || _e === void 0 ? void 0 : _e.name);
741
+ if (!name)
742
+ return false;
743
+ if (((_f = f.properties) === null || _f === void 0 ? void 0 : _f.floor_key) && f.properties.floor_key !== floorKey)
744
+ return false;
745
+ if (this.map.hasImage("logo-" + name))
746
+ return false; // icon has priority
747
+ return true;
748
+ });
749
+ if (!labelFeatures.length)
750
+ return null;
751
+ const textData = labelFeatures.map((f) => {
752
+ var _a, _b, _c;
753
+ return ({
754
+ position: [
755
+ ...f.geometry.coordinates.slice(0, 2),
756
+ ((_a = f.properties) === null || _a === void 0 ? void 0 : _a.height) ? Number(f.properties.height) : 5,
757
+ ],
758
+ text: ((_b = f.properties) === null || _b === void 0 ? void 0 : _b["name:latin"]) || ((_c = f.properties) === null || _c === void 0 ? void 0 : _c.name) || "",
759
+ });
760
+ });
761
+ return new TextLayer(Object.assign(Object.assign({}, props), { id: `${props.id}-labels`, data: textData, getPosition: (d) => d.position, getText: (d) => d.text, getSize: 16, getColor: [145, 150, 155, 255], outlineColor: useSdf ? [255, 255, 255, 255] : [0, 0, 0, 0], outlineWidth: useSdf ? 4 : 0, fontSettings: { sdf: useSdf }, fontFamily: "Open Sans, sans-serif", fontWeight: 600, billboard: true, pickable: false, characterSet: "auto", wordBreak: "break-word", maxWidth: 150 }));
762
+ },
763
+ updateTriggers: {
764
+ renderSubLayers: [floorKey],
765
+ },
766
+ });
767
+ }
768
+ makePoiIconLayer(floorKey) {
769
+ if (!this.spriteIconMapping || !this.spriteAtlasImage)
770
+ return null;
771
+ return new MVTLayer({
772
+ id: "indoor-poi-icons",
773
+ data: "https://tiles.mapvx.com/",
774
+ binary: false,
775
+ minZoom: 17,
776
+ maxZoom: 24,
777
+ renderSubLayers: (props) => {
778
+ const poiData = (props.data || []);
779
+ const poiFeatures = poiData.filter((f) => {
780
+ var _a, _b, _c;
781
+ if (((_a = f.geometry) === null || _a === void 0 ? void 0 : _a.type) !== "Point")
782
+ return false;
783
+ if (((_b = f.properties) === null || _b === void 0 ? void 0 : _b.floor_key) && f.properties.floor_key !== floorKey)
784
+ return false;
785
+ const sub = (_c = f.properties) === null || _c === void 0 ? void 0 : _c.subclass;
786
+ return sub === "elevator" || sub === "toilets";
787
+ });
788
+ if (!poiFeatures.length)
789
+ return null;
790
+ const iconData = poiFeatures.map((f) => {
791
+ var _a, _b, _c;
792
+ const tags = ((_a = f.properties) === null || _a === void 0 ? void 0 : _a.tags) || "";
793
+ let icon;
794
+ if (tags.includes("wheelchair"))
795
+ icon = "accessible_toilets";
796
+ else if (tags.includes("changing_table"))
797
+ icon = "changing_table";
798
+ else
799
+ icon = ((_b = f.properties) === null || _b === void 0 ? void 0 : _b.subclass) || "";
800
+ return {
801
+ position: [
802
+ ...f.geometry.coordinates.slice(0, 2),
803
+ ((_c = f.properties) === null || _c === void 0 ? void 0 : _c.height) ? Number(f.properties.height) : 5,
804
+ ],
805
+ icon,
806
+ };
807
+ });
808
+ return new IconLayer(Object.assign(Object.assign({}, props), { id: `${props.id}-poi-icons`, data: iconData, iconAtlas: this.spriteAtlasImage, iconMapping: this.spriteIconMapping, getIcon: (d) => d.icon, getPosition: (d) => d.position, getSize: 2, sizeUnits: "meters", sizeScale: 1, billboard: true, pickable: false }));
809
+ },
810
+ updateTriggers: {
811
+ renderSubLayers: [floorKey],
812
+ },
813
+ });
814
+ }
815
+ makeStoreLogoLayer(floorKey) {
816
+ if (!this.spriteIconMapping || !this.spriteAtlasImage)
817
+ return null;
818
+ return new MVTLayer({
819
+ id: "indoor-store-logos",
820
+ data: "https://tiles.mapvx.com/",
821
+ binary: false,
822
+ minZoom: 13,
823
+ maxZoom: 24,
824
+ renderSubLayers: (props) => {
825
+ const propsData = (props.data || []);
826
+ const logoFeatures = propsData.filter((f) => {
827
+ var _a, _b, _c;
828
+ if (((_a = f.geometry) === null || _a === void 0 ? void 0 : _a.type) !== "Point")
829
+ return false;
830
+ if (((_b = f.properties) === null || _b === void 0 ? void 0 : _b.floor_key) && f.properties.floor_key !== floorKey)
831
+ return false;
832
+ const name = (_c = f.properties) === null || _c === void 0 ? void 0 : _c.name;
833
+ if (!name)
834
+ return false;
835
+ return !!this.spriteIconMapping["logo-" + name];
836
+ });
837
+ if (!logoFeatures.length)
838
+ return null;
839
+ const logoData = logoFeatures.map((f) => {
840
+ var _a, _b, _c;
841
+ const name = (_a = f.properties) === null || _a === void 0 ? void 0 : _a.name;
842
+ const icon = "logo-" + name;
843
+ const rotation = ((_b = f.properties) === null || _b === void 0 ? void 0 : _b.rotation_icon) ? Number(f.properties.rotation_icon) : 0;
844
+ return {
845
+ position: [
846
+ ...f.geometry.coordinates.slice(0, 2),
847
+ ((_c = f.properties) === null || _c === void 0 ? void 0 : _c.height) ? Number(f.properties.height) : 5,
848
+ ],
849
+ icon,
850
+ rotation,
851
+ };
852
+ });
853
+ return new IconLayer(Object.assign(Object.assign({}, props), { id: `${props.id}-store-logos`, data: logoData, iconAtlas: this.spriteAtlasImage, iconMapping: this.spriteIconMapping, getIcon: (d) => d.icon, getPosition: (d) => d.position, getAngle: (d) => -d.rotation, getSize: (d) => {
854
+ const LOGO_SCALE = {
855
+ "logo-Maxi-K": 0.4,
856
+ "logo-Ripley": 2,
857
+ "logo-Falabella": 2,
858
+ "logo-CasaIdeas": 2,
859
+ "logo-Sky Costanera": 0.6,
860
+ "logo-Easy": 1.4,
861
+ "logo-Jumbo": 2,
862
+ "logo-Decathlon": 1.4,
863
+ };
864
+ const m = this.spriteIconMapping[d.icon];
865
+ if (!m)
866
+ return 10;
867
+ const pr = m.pixelRatio;
868
+ const w = m.width / pr;
869
+ const h = m.height / pr;
870
+ const base = 10 * (h / Math.max(w, h));
871
+ return base * (LOGO_SCALE[d.icon] || 1);
872
+ }, sizeUnits: "meters", sizeScale: 1, billboard: false, pickable: false }));
873
+ },
874
+ updateTriggers: {
875
+ renderSubLayers: [floorKey],
876
+ },
877
+ });
878
+ }
879
+ updateDeckOverlay() {
880
+ if (!this.deckOverlay || !this.currentFloor)
881
+ return;
882
+ const layers = [
883
+ this.makeMVTLayer(this.currentFloor),
884
+ this.makeLabelLayer(this.currentFloor, false),
885
+ ];
886
+ const poiLayer = this.makePoiIconLayer(this.currentFloor);
887
+ if (poiLayer)
888
+ layers.push(poiLayer);
889
+ const storeLayer = this.makeStoreLogoLayer(this.currentFloor);
890
+ if (storeLayer)
891
+ layers.push(storeLayer);
892
+ this.deckOverlay.setProps({ layers });
893
+ }
894
+ // End 3D related methods ------------------------------------------------------------
452
895
  getCurrentFloor() {
453
896
  var _a;
454
897
  return (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
@@ -468,6 +911,10 @@ export class InternalMapVXMap extends Loggeable {
468
911
  if (place) {
469
912
  this.subPlacesLoad(place.mapvxId);
470
913
  }
914
+ if (this.mode === "3D") {
915
+ this.updateDeckOverlay();
916
+ this.placeEscalators();
917
+ }
471
918
  if (updateStyle) {
472
919
  this.repository
473
920
  .fetchAndParseMapStyle(place === null || place === void 0 ? void 0 : place.mapvxId)
@@ -496,6 +943,10 @@ export class InternalMapVXMap extends Loggeable {
496
943
  this.innerFloors = (_a = place === null || place === void 0 ? void 0 : place.innerFloors.sort((a, b) => a.index - b.index)) !== null && _a !== void 0 ? _a : [];
497
944
  this.currentFloor =
498
945
  (_e = (_c = floorId !== null && floorId !== void 0 ? floorId : (_b = this.innerFloors.find((floor) => floor.defaultFloor)) === null || _b === void 0 ? void 0 : _b.key) !== null && _c !== void 0 ? _c : (_d = this.innerFloors[0]) === null || _d === void 0 ? void 0 : _d.key) !== null && _e !== void 0 ? _e : "";
946
+ if (this.mode === "3D") {
947
+ this.updateDeckOverlay();
948
+ this.placeEscalators();
949
+ }
499
950
  }
500
951
  updateParentPlaceAndFloor(parentPlaceId, floorId, options) {
501
952
  var _a, _b, _c, _d;
@@ -513,6 +964,19 @@ export class InternalMapVXMap extends Loggeable {
513
964
  }
514
965
  setLang(lang) {
515
966
  this.repository.setLang(lang);
967
+ if (rtlLanguages.includes(lang)) {
968
+ this.setRTLSupport();
969
+ }
970
+ // setLayersForLanguage reads this.map.getStyle()?.layers, which is empty
971
+ // until the style finishes loading. If setLang is called right after
972
+ // createMap (before the "load" event), apply it once the style is ready
973
+ // so the language change is not silently dropped.
974
+ if (this.map.isStyleLoaded()) {
975
+ this.setLayersForLanguage(lang);
976
+ }
977
+ else {
978
+ this.map.once("load", () => this.setLayersForLanguage(lang));
979
+ }
516
980
  }
517
981
  setParentPlace(place, updateStyle, onStyleReady) {
518
982
  this.changeParentPlaceTo(place, updateStyle, onStyleReady);
@@ -535,6 +999,9 @@ export class InternalMapVXMap extends Loggeable {
535
999
  this.routeController.addSourcesAndLayers();
536
1000
  this.refreshCircles();
537
1001
  this.filterByFloorKey(this.currentFloor);
1002
+ if (this.mode === "3D") {
1003
+ this.updateDeckOverlay();
1004
+ }
538
1005
  }
539
1006
  addMarker(marker) {
540
1007
  var _a, _b;
@@ -884,7 +1351,14 @@ export class InternalMapVXMap extends Loggeable {
884
1351
  // Always keep the default bucket alive so an empty map still renders
885
1352
  // newly added circles without a layer rebuild
886
1353
  ordersInUse.add("aboveBasemap");
887
- for (const order of ordersInUse) {
1354
+ // Process buckets lowest-first (CIRCLE_RENDER_ORDERS is canonical):
1355
+ // when two buckets resolve to the same anchor, each addLayer/moveLayer
1356
+ // lands immediately before it, so a bucket processed later paints above
1357
+ // the ones processed earlier. Canonical order keeps
1358
+ // belowLabels < aboveBasemap < top regardless of circle insertion order.
1359
+ for (const order of CIRCLE_RENDER_ORDERS) {
1360
+ if (!ordersInUse.has(order))
1361
+ continue;
888
1362
  const ids = circleLayerIdsFor(order);
889
1363
  const beforeId = this.circleBeforeIdFor(order);
890
1364
  const orderFilter = ["==", ["get", "renderOrder"], order];
@@ -956,6 +1430,10 @@ export class InternalMapVXMap extends Loggeable {
956
1430
  if (this.innerFloors.every((floor) => floor.key !== floorKey) && floorKey !== "") {
957
1431
  this.logEvent("invalidFloorKey", { floorKey: floorKeyString });
958
1432
  }
1433
+ if (this.mode === "3D") {
1434
+ this.updateDeckOverlay();
1435
+ this.placeEscalators();
1436
+ }
959
1437
  this.updateFiltersTo(floorKeyString);
960
1438
  this.updateMarkersTo(floorKeyString);
961
1439
  this.refreshCircles();
@@ -1064,6 +1542,49 @@ export class InternalMapVXMap extends Loggeable {
1064
1542
  this.map.setMinZoom(zoomLvl);
1065
1543
  (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1066
1544
  }
1545
+ setBearing(degrees, options) {
1546
+ var _a;
1547
+ if (!Number.isFinite(degrees))
1548
+ return;
1549
+ if (options === null || options === void 0 ? void 0 : options.animate) {
1550
+ if (options.onComplete)
1551
+ void this.map.once("moveend", options.onComplete);
1552
+ this.map.rotateTo(degrees, { duration: 600 });
1553
+ return;
1554
+ }
1555
+ this.map.setBearing(degrees);
1556
+ (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1557
+ }
1558
+ setRotationEnabled(enabled) {
1559
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1560
+ if (enabled) {
1561
+ (_b = (_a = this.map.dragRotate) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1562
+ (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.enableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
1563
+ (_f = (_e = this.map.keyboard) === null || _e === void 0 ? void 0 : _e.enable) === null || _f === void 0 ? void 0 : _f.call(_e);
1564
+ }
1565
+ else {
1566
+ (_h = (_g = this.map.dragRotate) === null || _g === void 0 ? void 0 : _g.disable) === null || _h === void 0 ? void 0 : _h.call(_g);
1567
+ (_k = (_j = this.map.touchZoomRotate) === null || _j === void 0 ? void 0 : _j.disableRotation) === null || _k === void 0 ? void 0 : _k.call(_j);
1568
+ }
1569
+ }
1570
+ setPanEnabled(enabled) {
1571
+ var _a, _b, _c, _d;
1572
+ if (enabled) {
1573
+ (_b = (_a = this.map.dragPan) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1574
+ }
1575
+ else {
1576
+ (_d = (_c = this.map.dragPan) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1577
+ }
1578
+ }
1579
+ setScrollZoomEnabled(enabled) {
1580
+ var _a, _b, _c, _d;
1581
+ if (enabled) {
1582
+ (_b = (_a = this.map.scrollZoom) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1583
+ }
1584
+ else {
1585
+ (_d = (_c = this.map.scrollZoom) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1586
+ }
1587
+ }
1067
1588
  isInsideBounds(point) {
1068
1589
  const mapBounds = this.map.getMaxBounds();
1069
1590
  if (mapBounds != null) {
@@ -1232,7 +1753,7 @@ export class InternalMapVXMap extends Loggeable {
1232
1753
  if (pointedPlace !== undefined) {
1233
1754
  new maplibregl.Popup()
1234
1755
  .setLngLat(pointedPlace.position)
1235
- .setHTML(pointedPlace.title)
1756
+ .setText(pointedPlace.title)
1236
1757
  .addTo(this.map);
1237
1758
  }
1238
1759
  }