@mapvx/web-js 1.1.1 → 1.1.2-alpha.3

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 (110) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +12 -9
  3. package/dist/cjs/assets/icons.js +6 -8
  4. package/dist/cjs/assets/icons.js.map +1 -1
  5. package/dist/cjs/controllers/routeController.js +19 -19
  6. package/dist/cjs/controllers/routeController.js.map +1 -1
  7. package/dist/cjs/domain/models/animation.js +2 -2
  8. package/dist/cjs/domain/models/categories.js +23 -10
  9. package/dist/cjs/domain/models/categories.js.map +1 -1
  10. package/dist/cjs/domain/models/circle.js +253 -0
  11. package/dist/cjs/domain/models/circle.js.map +1 -0
  12. package/dist/cjs/domain/models/mapConfig.js +10 -1
  13. package/dist/cjs/domain/models/mapConfig.js.map +1 -1
  14. package/dist/cjs/domain/models/marker.js +86 -80
  15. package/dist/cjs/domain/models/marker.js.map +1 -1
  16. package/dist/cjs/domain/models/routeConfiguration.js +3 -1
  17. package/dist/cjs/domain/models/routeConfiguration.js.map +1 -1
  18. package/dist/cjs/index.js +21 -10
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/logger/logger.js +13 -8
  21. package/dist/cjs/logger/logger.js.map +1 -1
  22. package/dist/cjs/logger/rollbar.js +11 -6
  23. package/dist/cjs/logger/rollbar.js.map +1 -1
  24. package/dist/cjs/map/map.js +446 -28
  25. package/dist/cjs/map/map.js.map +1 -1
  26. package/dist/cjs/map/mapInteractionOptions.js +56 -0
  27. package/dist/cjs/map/mapInteractionOptions.js.map +1 -0
  28. package/dist/cjs/repository/repository.js +25 -26
  29. package/dist/cjs/repository/repository.js.map +1 -1
  30. package/dist/cjs/repository/requester.js +71 -91
  31. package/dist/cjs/repository/requester.js.map +1 -1
  32. package/dist/cjs/sdk.js +18 -1
  33. package/dist/cjs/sdk.js.map +1 -1
  34. package/dist/cjs/utils/semaphore.js +143 -0
  35. package/dist/cjs/utils/semaphore.js.map +1 -0
  36. package/dist/es/assets/icons.d.ts +4 -4
  37. package/dist/es/assets/icons.d.ts.map +1 -1
  38. package/dist/es/assets/icons.js +6 -8
  39. package/dist/es/assets/icons.js.map +1 -1
  40. package/dist/es/controllers/routeController.d.ts.map +1 -1
  41. package/dist/es/controllers/routeController.js +19 -19
  42. package/dist/es/controllers/routeController.js.map +1 -1
  43. package/dist/es/domain/models/animation.d.ts +3 -3
  44. package/dist/es/domain/models/animation.js +2 -2
  45. package/dist/es/domain/models/categories.d.ts +34 -10
  46. package/dist/es/domain/models/categories.d.ts.map +1 -1
  47. package/dist/es/domain/models/categories.js +21 -9
  48. package/dist/es/domain/models/categories.js.map +1 -1
  49. package/dist/es/domain/models/circle.d.ts +222 -0
  50. package/dist/es/domain/models/circle.d.ts.map +1 -0
  51. package/dist/es/domain/models/circle.js +246 -0
  52. package/dist/es/domain/models/circle.js.map +1 -0
  53. package/dist/es/domain/models/configuration.d.ts +8 -0
  54. package/dist/es/domain/models/configuration.d.ts.map +1 -1
  55. package/dist/es/domain/models/mapConfig.d.ts +118 -3
  56. package/dist/es/domain/models/mapConfig.d.ts.map +1 -1
  57. package/dist/es/domain/models/mapConfig.js +9 -0
  58. package/dist/es/domain/models/mapConfig.js.map +1 -1
  59. package/dist/es/domain/models/marker.d.ts +8 -0
  60. package/dist/es/domain/models/marker.d.ts.map +1 -1
  61. package/dist/es/domain/models/marker.js +86 -80
  62. package/dist/es/domain/models/marker.js.map +1 -1
  63. package/dist/es/domain/models/routeConfiguration.d.ts +47 -0
  64. package/dist/es/domain/models/routeConfiguration.d.ts.map +1 -1
  65. package/dist/es/domain/models/routeConfiguration.js +3 -1
  66. package/dist/es/domain/models/routeConfiguration.js.map +1 -1
  67. package/dist/es/index.d.ts +14 -11
  68. package/dist/es/index.d.ts.map +1 -1
  69. package/dist/es/index.js +9 -5
  70. package/dist/es/index.js.map +1 -1
  71. package/dist/es/interfaces/routeCacheResponse.d.ts.map +1 -1
  72. package/dist/es/logger/logger.d.ts.map +1 -1
  73. package/dist/es/logger/logger.js +13 -8
  74. package/dist/es/logger/logger.js.map +1 -1
  75. package/dist/es/logger/rollbar.d.ts.map +1 -1
  76. package/dist/es/logger/rollbar.js +11 -6
  77. package/dist/es/logger/rollbar.js.map +1 -1
  78. package/dist/es/map/map.d.ts +298 -0
  79. package/dist/es/map/map.d.ts.map +1 -1
  80. package/dist/es/map/map.js +447 -29
  81. package/dist/es/map/map.js.map +1 -1
  82. package/dist/es/map/mapInteractionOptions.d.ts +37 -0
  83. package/dist/es/map/mapInteractionOptions.d.ts.map +1 -0
  84. package/dist/es/map/mapInteractionOptions.js +51 -0
  85. package/dist/es/map/mapInteractionOptions.js.map +1 -0
  86. package/dist/es/repository/repository.d.ts +0 -1
  87. package/dist/es/repository/repository.d.ts.map +1 -1
  88. package/dist/es/repository/repository.js +25 -26
  89. package/dist/es/repository/repository.js.map +1 -1
  90. package/dist/es/repository/requester.d.ts +12 -2
  91. package/dist/es/repository/requester.d.ts.map +1 -1
  92. package/dist/es/repository/requester.js +71 -91
  93. package/dist/es/repository/requester.js.map +1 -1
  94. package/dist/es/sdk.d.ts +2 -0
  95. package/dist/es/sdk.d.ts.map +1 -1
  96. package/dist/es/sdk.js +18 -1
  97. package/dist/es/sdk.js.map +1 -1
  98. package/dist/es/utils/semaphore.d.ts +70 -0
  99. package/dist/es/utils/semaphore.d.ts.map +1 -0
  100. package/dist/es/utils/semaphore.js +139 -0
  101. package/dist/es/utils/semaphore.js.map +1 -0
  102. package/dist/umd/index.js +1792 -657
  103. package/dist/umd/index.js.map +1 -1
  104. package/dist/umd/styles.css +32 -14
  105. package/dist/umd/styles.css.map +1 -1
  106. package/package.json +63 -49
  107. package/dist/cjs/assets/route_animation_icon.svg +0 -15
  108. package/dist/cjs/assets/user-dot-icon.svg +0 -3
  109. package/dist/es/assets/route_animation_icon.svg +0 -15
  110. package/dist/es/assets/user-dot-icon.svg +0 -3
@@ -25,6 +25,31 @@ var __importStar = (this && this.__importStar) || function (mod) {
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.InternalMapVXMap = void 0;
27
27
  const maplibre_gl_1 = __importStar(require("maplibre-gl"));
28
+ const semaphore_1 = require("../utils/semaphore");
29
+ /** Shared GeoJSON source holding every circle drawn through the circle API. */
30
+ const CIRCLE_SOURCE_ID = "mapvx-circles";
31
+ /** Fill layer rendering the translucent interior of the circles. */
32
+ const CIRCLE_FILL_LAYER_ID = "mapvx-circles-fill";
33
+ /** Line layer rendering the circle outlines. */
34
+ const CIRCLE_LINE_LAYER_ID = "mapvx-circles-line";
35
+ /**
36
+ * Layer-id suffix per circle render order. The default placement keeps the
37
+ * unsuffixed ids so existing consumers referencing them keep working.
38
+ */
39
+ const CIRCLE_LAYER_SUFFIXES = {
40
+ aboveBasemap: "",
41
+ belowLabels: "-below-labels",
42
+ top: "-top",
43
+ };
44
+ /** Fill/line layer ids for one circle render-order bucket. */
45
+ function circleLayerIdsFor(order) {
46
+ const suffix = CIRCLE_LAYER_SUFFIXES[order];
47
+ return { fill: CIRCLE_FILL_LAYER_ID + suffix, line: CIRCLE_LINE_LAYER_ID + suffix };
48
+ }
49
+ /** True for the layer ids owned by the circle API, regardless of bucket. */
50
+ function isCircleLayerId(id) {
51
+ return id.startsWith(CIRCLE_FILL_LAYER_ID) || id.startsWith(CIRCLE_LINE_LAYER_ID);
52
+ }
28
53
  // Flag to track if cached-tile protocol has been registered
29
54
  let cachedTileProtocolRegistered = false;
30
55
  /**
@@ -45,21 +70,36 @@ function deepClone(obj) {
45
70
  /**
46
71
  * Register a custom protocol for cached tiles that routes requests through the main thread.
47
72
  * This allows the service worker to intercept and cache tile requests.
73
+ *
74
+ * Wraps fetches in a {@link Semaphore} so tile CDN / WAFs are not hit with
75
+ * unbounded parallel requests (same host as the service worker cache). The
76
+ * semaphore is created once with the limit from the first map's config; later
77
+ * calls are no-ops because the protocol can only be registered once globally
78
+ * on MapLibre.
79
+ *
80
+ * @param maxConcurrentFetches - Maximum number of in-flight tile fetches.
48
81
  */
49
- function registerCachedTileProtocol() {
82
+ function registerCachedTileProtocol(maxConcurrentFetches) {
50
83
  if (cachedTileProtocolRegistered)
51
84
  return;
85
+ const semaphore = new semaphore_1.Semaphore(maxConcurrentFetches);
52
86
  maplibre_gl_1.default.addProtocol("cached-tile", (params, abortController) => {
53
- // Convert cached-tile:// URL back to https://
54
87
  const url = params.url.replace("cached-tile://", "https://");
55
- return fetch(url, { signal: abortController.signal })
88
+ // Pass the abort signal to acquire() so a request cancelled while still
89
+ // queued (e.g. during rapid zoom) drops out of the FIFO queue instead of
90
+ // waiting for a slot just to bail. release() runs only inside this chain,
91
+ // i.e. only after a slot was actually granted.
92
+ return semaphore.acquire(abortController.signal).then(() => fetch(url, { signal: abortController.signal })
56
93
  .then((response) => {
57
94
  if (!response.ok) {
58
95
  throw new Error(`HTTP error! status: ${response.status}`);
59
96
  }
60
97
  return response.arrayBuffer();
61
98
  })
62
- .then((data) => ({ data }));
99
+ .then((data) => ({ data }))
100
+ .finally(() => {
101
+ semaphore.release();
102
+ }));
63
103
  });
64
104
  cachedTileProtocolRegistered = true;
65
105
  }
@@ -119,12 +159,14 @@ const _rtl_1 = require("../domain/models/_rtl");
119
159
  const animation_1 = require("../domain/models/animation");
120
160
  const loggeable_1 = require("../domain/models/loggeable");
121
161
  const mapConfig_1 = require("../domain/models/mapConfig");
162
+ const circle_1 = require("../domain/models/circle");
122
163
  const marker_1 = require("../domain/models/marker");
123
164
  const route_1 = require("../domain/models/route");
124
165
  const routeConfiguration_1 = require("../domain/models/routeConfiguration");
125
166
  const repository_1 = require("../repository/repository");
126
167
  const utils_1 = require("../utils/utils");
127
168
  const route_utils_1 = require("../utils/route-utils");
169
+ const mapInteractionOptions_1 = require("./mapInteractionOptions");
128
170
  /**
129
171
  * Class to interact with the map.
130
172
  * @category Map
@@ -148,6 +190,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
148
190
  this.currentFloor = "";
149
191
  this.baseFilters = {};
150
192
  this.markers = [];
193
+ this.circles = [];
151
194
  this.enableHover = false;
152
195
  this.hoveredId = "unselected";
153
196
  this.failedTiles = new Set();
@@ -163,8 +206,11 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
163
206
  this.watchPositionID = undefined;
164
207
  this.onFloorChange = mapConfig.onFloorChange;
165
208
  this.onParentPlaceChange = mapConfig.onParentPlaceChange;
166
- // Merge tile cache config with defaults
167
- this.tileCacheConfig = Object.assign(Object.assign({}, mapConfig_1.DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
209
+ this.tileCacheConfig = (() => {
210
+ const merged = Object.assign(Object.assign({}, mapConfig_1.DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
211
+ merged.maxTiles = Math.min(merged.maxTiles, mapConfig_1.MAPLIBRE_MAX_TILE_CACHE_HARD_CAP);
212
+ return merged;
213
+ })();
168
214
  if (mapConfig.parentPlaceId != null) {
169
215
  this.initialPlaceDetailSetUp(mapConfig.parentPlaceId, mapConfig.authToken);
170
216
  }
@@ -261,28 +307,17 @@ class InternalMapVXMap extends loggeable_1.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 }, (0, mapInteractionOptions_1.buildInteractionOptions)(mapConfig));
286
321
  if (mapConfig.maxZoom)
287
322
  mapOptions.maxZoom = mapConfig.maxZoom;
288
323
  if (mapConfig.minZoom)
@@ -292,10 +327,15 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
292
327
  mapOptions.maxBounds = new maplibre_gl_1.LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
293
328
  }
294
329
  this.map = new maplibre_gl_1.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 ((0, mapInteractionOptions_1.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 maplibre_gl_1.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;
301
341
  this.whenStyleUpdates(style);
@@ -305,6 +345,12 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
305
345
  this.onHover();
306
346
  this.subscribeToFailedTiles();
307
347
  });
348
+ // Self-healing for circles: a full style reload wipes custom sources and
349
+ // layers, and not every restyle path goes through whenStyleUpdates.
350
+ this.map.on("styledata", () => {
351
+ if (this.circles.length > 0)
352
+ this.ensureCircleLayers();
353
+ });
308
354
  this.map.on("zoomend", () => {
309
355
  var _a;
310
356
  (_a = mapConfig.onZoomEnd) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.getZoomLevel());
@@ -423,6 +469,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
423
469
  return Object.assign(Object.assign({}, segmentation), { token: this.token, parentPlaceId: (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "None" });
424
470
  }
425
471
  destroyMap() {
472
+ this.circles = [];
426
473
  this.map.remove();
427
474
  this.unsubscribeFromFailedTiles();
428
475
  }
@@ -506,6 +553,19 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
506
553
  }
507
554
  setLang(lang) {
508
555
  this.repository.setLang(lang);
556
+ if (_rtl_1.rtlLanguages.includes(lang)) {
557
+ this.setRTLSupport();
558
+ }
559
+ // setLayersForLanguage reads this.map.getStyle()?.layers, which is empty
560
+ // until the style finishes loading. If setLang is called right after
561
+ // createMap (before the "load" event), apply it once the style is ready
562
+ // so the language change is not silently dropped.
563
+ if (this.map.isStyleLoaded()) {
564
+ this.setLayersForLanguage(lang);
565
+ }
566
+ else {
567
+ this.map.once("load", () => this.setLayersForLanguage(lang));
568
+ }
509
569
  }
510
570
  setParentPlace(place, updateStyle, onStyleReady) {
511
571
  this.changeParentPlaceTo(place, updateStyle, onStyleReady);
@@ -526,6 +586,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
526
586
  this.setBaseFilters(newStyle);
527
587
  }
528
588
  this.routeController.addSourcesAndLayers();
589
+ this.refreshCircles();
529
590
  this.filterByFloorKey(this.currentFloor);
530
591
  }
531
592
  addMarker(marker) {
@@ -631,6 +692,320 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
631
692
  throw new Error("Failed to remove all markers");
632
693
  }
633
694
  }
695
+ addCircle(circle) {
696
+ try {
697
+ // Check if a circle with the same ID already exists and replace it
698
+ if (circle.id) {
699
+ this.circles = this.circles.filter((c) => c.id !== circle.id);
700
+ }
701
+ // resolveCircleConfig validates radiusMeters and coordinates
702
+ const record = (0, circle_1.resolveCircleConfig)(circle);
703
+ this.circles.push(record);
704
+ this.refreshCircles();
705
+ this.logEvent("addCircle");
706
+ return record.id;
707
+ }
708
+ catch (error) {
709
+ throw new Error(`Failed to add circle: ${error instanceof Error ? error.message : String(error)}`);
710
+ }
711
+ }
712
+ updateCircle(circleConfig) {
713
+ try {
714
+ const index = this.circles.findIndex((c) => c.id === circleConfig.id);
715
+ if (index === -1)
716
+ return null;
717
+ // resolveCircleConfig validates radiusMeters and coordinates
718
+ this.circles[index] = (0, circle_1.resolveCircleConfig)(circleConfig, this.circles[index].hidden);
719
+ this.refreshCircles();
720
+ this.logEvent("updateCircle");
721
+ return this.circles[index].id;
722
+ }
723
+ catch (error) {
724
+ throw new Error(`Failed to update circle: ${error instanceof Error ? error.message : String(error)}`);
725
+ }
726
+ }
727
+ getCircle(circleId) {
728
+ const circle = this.circles.find((c) => c.id === circleId);
729
+ return circle ? (0, circle_1.cloneCircleRecord)(circle) : undefined;
730
+ }
731
+ getCircles() {
732
+ return this.circles.map(circle_1.cloneCircleRecord);
733
+ }
734
+ hasCircle(circleId) {
735
+ return this.circles.some((c) => c.id === circleId);
736
+ }
737
+ updateCirclePosition(circleId, center, radiusMeters) {
738
+ try {
739
+ const circle = this.circles.find((c) => c.id === circleId);
740
+ if (circle === undefined)
741
+ return false;
742
+ // Validate inputs before modifying
743
+ if (!center) {
744
+ throw new Error("Circle center is required");
745
+ }
746
+ const { lat, lng } = center;
747
+ if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
748
+ throw new Error(`Invalid latitude: ${lat}. Must be between -90 and 90.`);
749
+ }
750
+ if (!Number.isFinite(lng) || lng < -180 || lng > 180) {
751
+ throw new Error(`Invalid longitude: ${lng}. Must be between -180 and 180.`);
752
+ }
753
+ if (radiusMeters !== undefined) {
754
+ if (!Number.isFinite(radiusMeters) || radiusMeters <= 0) {
755
+ throw new Error(`Invalid radiusMeters: ${radiusMeters}. Must be a positive finite number.`);
756
+ }
757
+ }
758
+ circle.coordinate = { lat: center.lat, lng: center.lng };
759
+ if (radiusMeters !== undefined) {
760
+ circle.radiusMeters = radiusMeters;
761
+ }
762
+ this.refreshCircles();
763
+ this.logEvent("updateCirclePosition");
764
+ return true;
765
+ }
766
+ catch (error) {
767
+ throw new Error(`Failed to update circle position: ${error instanceof Error ? error.message : String(error)}`);
768
+ }
769
+ }
770
+ updateCircleStyle(circleId, style) {
771
+ try {
772
+ const circle = this.circles.find((c) => c.id === circleId);
773
+ if (circle === undefined)
774
+ return false;
775
+ // Update only style properties, clamp opacities
776
+ if (style.fillColor !== undefined)
777
+ circle.fillColor = style.fillColor;
778
+ if (style.fillOpacity !== undefined)
779
+ circle.fillOpacity = Math.max(0, Math.min(1, style.fillOpacity));
780
+ if (style.strokeColor !== undefined)
781
+ circle.strokeColor = style.strokeColor;
782
+ if (style.strokeWidth !== undefined)
783
+ circle.strokeWidth = style.strokeWidth;
784
+ if (style.strokeOpacity !== undefined)
785
+ circle.strokeOpacity = Math.max(0, Math.min(1, style.strokeOpacity));
786
+ this.refreshCircles();
787
+ this.logEvent("updateCircleStyle");
788
+ return true;
789
+ }
790
+ catch (error) {
791
+ throw new Error(`Failed to update circle style: ${error instanceof Error ? error.message : String(error)}`);
792
+ }
793
+ }
794
+ removeCircle(circleId) {
795
+ try {
796
+ this.circles = this.circles.filter((c) => c.id !== circleId);
797
+ this.refreshCircles();
798
+ this.logEvent("removeCircle");
799
+ }
800
+ catch (error) {
801
+ throw new Error("Failed to remove circle");
802
+ }
803
+ }
804
+ removeAllCircles() {
805
+ try {
806
+ this.circles = [];
807
+ this.refreshCircles();
808
+ this.logEvent("removeAllCircles");
809
+ }
810
+ catch (error) {
811
+ throw new Error("Failed to remove all circles");
812
+ }
813
+ }
814
+ showCircle(circleId) {
815
+ try {
816
+ const circle = this.circles.find((c) => c.id === circleId);
817
+ if (circle === undefined) {
818
+ return false;
819
+ }
820
+ circle.hidden = false;
821
+ this.refreshCircles();
822
+ this.logEvent("showCircle");
823
+ return true;
824
+ }
825
+ catch (error) {
826
+ return false;
827
+ }
828
+ }
829
+ hideCircle(circleId) {
830
+ try {
831
+ const circle = this.circles.find((c) => c.id === circleId);
832
+ if (circle === undefined) {
833
+ return false;
834
+ }
835
+ circle.hidden = true;
836
+ this.refreshCircles();
837
+ this.logEvent("hideCircle");
838
+ return true;
839
+ }
840
+ catch (error) {
841
+ return false;
842
+ }
843
+ }
844
+ /**
845
+ * Builds the GeoJSON FeatureCollection for every currently visible circle.
846
+ * Visibility mirrors marker semantics: a circle with a floor is shown only
847
+ * while that floor is displayed, and a circle without a floor is shown only
848
+ * in outdoor contexts. Hidden circles are always omitted.
849
+ */
850
+ circleFeatureCollection() {
851
+ var _a, _b, _c;
852
+ const floorId = (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
853
+ const isOutdoor = !this.parentPlace ||
854
+ ((_c = (_b = this.innerFloors.find((floor) => floor.key === floorId)) === null || _b === void 0 ? void 0 : _b.reachableFromGPS) !== null && _c !== void 0 ? _c : false);
855
+ const features = this.circles
856
+ .filter((circle) => {
857
+ var _a;
858
+ if (circle.hidden)
859
+ return false;
860
+ const circleFloor = (_a = circle.floorId) !== null && _a !== void 0 ? _a : "";
861
+ return circleFloor === floorId || (isOutdoor && circleFloor === "");
862
+ })
863
+ .map((circle) => ({
864
+ type: "Feature",
865
+ properties: {
866
+ id: circle.id,
867
+ fillColor: circle.fillColor,
868
+ fillOpacity: circle.fillOpacity,
869
+ strokeColor: circle.strokeColor,
870
+ strokeWidth: circle.strokeWidth,
871
+ strokeOpacity: circle.strokeOpacity,
872
+ renderOrder: circle.renderOrder,
873
+ },
874
+ geometry: {
875
+ type: "Polygon",
876
+ coordinates: [
877
+ (0, circle_1.circleRing)(circle.coordinate.lng, circle.coordinate.lat, circle.radiusMeters),
878
+ ],
879
+ },
880
+ }));
881
+ return { type: "FeatureCollection", features };
882
+ }
883
+ /**
884
+ * Idempotently adds the shared circle source and its fill/line layers.
885
+ * Layers are inserted below the first symbol layer so place labels and
886
+ * markers stay readable above the translucent fill. Safe to call at any
887
+ * time: a style that is still loading simply rejects the calls, and the
888
+ * styledata listener retries once the style is ready.
889
+ */
890
+ /**
891
+ * Resolves the layer id to insert circle layers before, for one render
892
+ * order. `undefined` means "append on top".
893
+ *
894
+ * `aboveBasemap` finds the topmost non-symbol layer of the style and
895
+ * inserts before the first symbol layer that follows it. On indoor styles
896
+ * the floor-plate and building polygons are ordered after the first symbol
897
+ * layer, so anchoring on the topmost geometry layer — instead of the first
898
+ * symbol layer — guarantees circles are never occluded by basemap fills
899
+ * while still rendering below the labels that follow them.
900
+ */
901
+ circleBeforeIdFor(order) {
902
+ var _a, _b, _c, _d;
903
+ if (order === "top")
904
+ return undefined;
905
+ const layers = ((_b = (_a = this.map.getStyle()) === null || _a === void 0 ? void 0 : _a.layers) !== null && _b !== void 0 ? _b : []).filter((layer) => !isCircleLayerId(layer.id));
906
+ if (order === "belowLabels") {
907
+ return (_c = layers.find((layer) => layer.type === "symbol")) === null || _c === void 0 ? void 0 : _c.id;
908
+ }
909
+ // aboveBasemap
910
+ let lastNonSymbolIndex = -1;
911
+ layers.forEach((layer, index) => {
912
+ if (layer.type !== "symbol")
913
+ lastNonSymbolIndex = index;
914
+ });
915
+ return (_d = layers.slice(lastNonSymbolIndex + 1).find((layer) => layer.type === "symbol")) === null || _d === void 0 ? void 0 : _d.id;
916
+ }
917
+ /**
918
+ * Idempotently ensures the shared circle source and one fill/line layer
919
+ * pair per render order in use, at the placement that order requires.
920
+ * Placement is recomputed and re-asserted (via moveLayer) on every call,
921
+ * so circles regain their correct z-order after any style reload or
922
+ * floor/parent-place change. Safe to call at any time: a style that is
923
+ * still loading simply rejects the calls, and the styledata listener
924
+ * retries once the style is ready.
925
+ */
926
+ ensureCircleLayers() {
927
+ if (!this.map)
928
+ return;
929
+ try {
930
+ if (!this.map.getSource(CIRCLE_SOURCE_ID)) {
931
+ this.map.addSource(CIRCLE_SOURCE_ID, {
932
+ type: "geojson",
933
+ data: this.circleFeatureCollection(),
934
+ });
935
+ }
936
+ const ordersInUse = new Set(this.circles.map((c) => c.renderOrder));
937
+ // Always keep the default bucket alive so an empty map still renders
938
+ // newly added circles without a layer rebuild
939
+ ordersInUse.add("aboveBasemap");
940
+ // Process buckets lowest-first (CIRCLE_RENDER_ORDERS is canonical):
941
+ // when two buckets resolve to the same anchor, each addLayer/moveLayer
942
+ // lands immediately before it, so a bucket processed later paints above
943
+ // the ones processed earlier. Canonical order keeps
944
+ // belowLabels < aboveBasemap < top regardless of circle insertion order.
945
+ for (const order of circle_1.CIRCLE_RENDER_ORDERS) {
946
+ if (!ordersInUse.has(order))
947
+ continue;
948
+ const ids = circleLayerIdsFor(order);
949
+ const beforeId = this.circleBeforeIdFor(order);
950
+ const orderFilter = ["==", ["get", "renderOrder"], order];
951
+ if (!this.map.getLayer(ids.fill)) {
952
+ const fillLayer = {
953
+ id: ids.fill,
954
+ type: "fill",
955
+ source: CIRCLE_SOURCE_ID,
956
+ filter: orderFilter,
957
+ paint: {
958
+ "fill-color": ["get", "fillColor"],
959
+ "fill-opacity": ["get", "fillOpacity"],
960
+ },
961
+ };
962
+ this.map.addLayer(fillLayer, beforeId);
963
+ }
964
+ else {
965
+ this.map.moveLayer(ids.fill, beforeId);
966
+ }
967
+ if (!this.map.getLayer(ids.line)) {
968
+ const lineLayer = {
969
+ id: ids.line,
970
+ type: "line",
971
+ source: CIRCLE_SOURCE_ID,
972
+ filter: orderFilter,
973
+ paint: {
974
+ "line-color": ["get", "strokeColor"],
975
+ "line-width": ["get", "strokeWidth"],
976
+ "line-opacity": ["get", "strokeOpacity"],
977
+ },
978
+ };
979
+ this.map.addLayer(lineLayer, beforeId);
980
+ }
981
+ else {
982
+ this.map.moveLayer(ids.line, beforeId);
983
+ }
984
+ }
985
+ }
986
+ catch (error) {
987
+ // Style may not be loaded yet; the styledata listener re-adds the layers
988
+ }
989
+ }
990
+ /**
991
+ * Re-renders all circles: ensures the source and layers exist, then pushes
992
+ * the current FeatureCollection. Called after every mutation of the circle
993
+ * list, on floor changes, and when the map style reloads.
994
+ */
995
+ refreshCircles() {
996
+ if (!this.map)
997
+ return;
998
+ if (this.circles.length === 0 && !this.map.getSource(CIRCLE_SOURCE_ID))
999
+ return;
1000
+ this.ensureCircleLayers();
1001
+ try {
1002
+ const source = this.map.getSource(CIRCLE_SOURCE_ID);
1003
+ source === null || source === void 0 ? void 0 : source.setData(this.circleFeatureCollection());
1004
+ }
1005
+ catch (error) {
1006
+ // Source may not exist while a new style is loading
1007
+ }
1008
+ }
634
1009
  /**
635
1010
  * Use it to change the current layer
636
1011
  * @param floorKey floor id
@@ -643,6 +1018,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
643
1018
  }
644
1019
  this.updateFiltersTo(floorKeyString);
645
1020
  this.updateMarkersTo(floorKeyString);
1021
+ this.refreshCircles();
646
1022
  this.routeController.updateRouteLayers(floorKeyString);
647
1023
  this.routeController.updateRouteMarkerVisibility(floorKeyString);
648
1024
  }
@@ -748,6 +1124,49 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
748
1124
  this.map.setMinZoom(zoomLvl);
749
1125
  (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
750
1126
  }
1127
+ setBearing(degrees, options) {
1128
+ var _a;
1129
+ if (!Number.isFinite(degrees))
1130
+ return;
1131
+ if (options === null || options === void 0 ? void 0 : options.animate) {
1132
+ if (options.onComplete)
1133
+ void this.map.once("moveend", options.onComplete);
1134
+ this.map.rotateTo(degrees, { duration: 600 });
1135
+ return;
1136
+ }
1137
+ this.map.setBearing(degrees);
1138
+ (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1139
+ }
1140
+ setRotationEnabled(enabled) {
1141
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1142
+ if (enabled) {
1143
+ (_b = (_a = this.map.dragRotate) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1144
+ (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.enableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
1145
+ (_f = (_e = this.map.keyboard) === null || _e === void 0 ? void 0 : _e.enable) === null || _f === void 0 ? void 0 : _f.call(_e);
1146
+ }
1147
+ else {
1148
+ (_h = (_g = this.map.dragRotate) === null || _g === void 0 ? void 0 : _g.disable) === null || _h === void 0 ? void 0 : _h.call(_g);
1149
+ (_k = (_j = this.map.touchZoomRotate) === null || _j === void 0 ? void 0 : _j.disableRotation) === null || _k === void 0 ? void 0 : _k.call(_j);
1150
+ }
1151
+ }
1152
+ setPanEnabled(enabled) {
1153
+ var _a, _b, _c, _d;
1154
+ if (enabled) {
1155
+ (_b = (_a = this.map.dragPan) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1156
+ }
1157
+ else {
1158
+ (_d = (_c = this.map.dragPan) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1159
+ }
1160
+ }
1161
+ setScrollZoomEnabled(enabled) {
1162
+ var _a, _b, _c, _d;
1163
+ if (enabled) {
1164
+ (_b = (_a = this.map.scrollZoom) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1165
+ }
1166
+ else {
1167
+ (_d = (_c = this.map.scrollZoom) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1168
+ }
1169
+ }
751
1170
  isInsideBounds(point) {
752
1171
  const mapBounds = this.map.getMaxBounds();
753
1172
  if (mapBounds != null) {
@@ -866,7 +1285,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
866
1285
  throw new Error("Error: Failed to add route");
867
1286
  }
868
1287
  }
869
- updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#757575" }) {
1288
+ updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#276EF1" }) {
870
1289
  try {
871
1290
  const behindConfig = new routeConfiguration_1.InternalDrawRouteConfiguration({
872
1291
  routeStyle: behindStyle,
@@ -916,13 +1335,12 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
916
1335
  if (pointedPlace !== undefined) {
917
1336
  new maplibre_gl_1.default.Popup()
918
1337
  .setLngLat(pointedPlace.position)
919
- .setHTML(pointedPlace.title)
1338
+ .setText(pointedPlace.title)
920
1339
  .addTo(this.map);
921
1340
  }
922
1341
  }
923
1342
  return "";
924
1343
  }
925
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
926
1344
  removePopOver(id) {
927
1345
  throw Error("Not implemented");
928
1346
  }
@@ -1045,7 +1463,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1045
1463
  filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
1046
1464
  paint: {
1047
1465
  "fill-extrusion-height": 2.5,
1048
- "fill-extrusion-color": "#276EF1",
1466
+ "fill-extrusion-color": circle_1.MAPVX_BRAND_COLOR,
1049
1467
  "fill-extrusion-opacity": 0.8,
1050
1468
  },
1051
1469
  };
@@ -1059,7 +1477,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1059
1477
  "source-layer": "area",
1060
1478
  filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
1061
1479
  paint: {
1062
- "fill-color": "#276EF1",
1480
+ "fill-color": circle_1.MAPVX_BRAND_COLOR,
1063
1481
  },
1064
1482
  };
1065
1483
  this.map.addLayer(layer);
@@ -1317,7 +1735,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1317
1735
  "source-layer": "area",
1318
1736
  filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
1319
1737
  paint: {
1320
- "line-color": "#276EF1",
1738
+ "line-color": circle_1.MAPVX_BRAND_COLOR,
1321
1739
  "line-width": 4,
1322
1740
  },
1323
1741
  };