@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
@@ -1,4 +1,29 @@
1
1
  import maplibregl, { LngLatBounds, Map, NavigationControl, setRTLTextPlugin, } from "maplibre-gl";
2
+ import { Semaphore } from "../utils/semaphore";
3
+ /** Shared GeoJSON source holding every circle drawn through the circle API. */
4
+ const CIRCLE_SOURCE_ID = "mapvx-circles";
5
+ /** Fill layer rendering the translucent interior of the circles. */
6
+ const CIRCLE_FILL_LAYER_ID = "mapvx-circles-fill";
7
+ /** Line layer rendering the circle outlines. */
8
+ const CIRCLE_LINE_LAYER_ID = "mapvx-circles-line";
9
+ /**
10
+ * Layer-id suffix per circle render order. The default placement keeps the
11
+ * unsuffixed ids so existing consumers referencing them keep working.
12
+ */
13
+ const CIRCLE_LAYER_SUFFIXES = {
14
+ aboveBasemap: "",
15
+ belowLabels: "-below-labels",
16
+ top: "-top",
17
+ };
18
+ /** Fill/line layer ids for one circle render-order bucket. */
19
+ function circleLayerIdsFor(order) {
20
+ const suffix = CIRCLE_LAYER_SUFFIXES[order];
21
+ return { fill: CIRCLE_FILL_LAYER_ID + suffix, line: CIRCLE_LINE_LAYER_ID + suffix };
22
+ }
23
+ /** True for the layer ids owned by the circle API, regardless of bucket. */
24
+ function isCircleLayerId(id) {
25
+ return id.startsWith(CIRCLE_FILL_LAYER_ID) || id.startsWith(CIRCLE_LINE_LAYER_ID);
26
+ }
2
27
  // Flag to track if cached-tile protocol has been registered
3
28
  let cachedTileProtocolRegistered = false;
4
29
  /**
@@ -19,21 +44,36 @@ function deepClone(obj) {
19
44
  /**
20
45
  * Register a custom protocol for cached tiles that routes requests through the main thread.
21
46
  * This allows the service worker to intercept and cache tile requests.
47
+ *
48
+ * Wraps fetches in a {@link Semaphore} so tile CDN / WAFs are not hit with
49
+ * unbounded parallel requests (same host as the service worker cache). The
50
+ * semaphore is created once with the limit from the first map's config; later
51
+ * calls are no-ops because the protocol can only be registered once globally
52
+ * on MapLibre.
53
+ *
54
+ * @param maxConcurrentFetches - Maximum number of in-flight tile fetches.
22
55
  */
23
- function registerCachedTileProtocol() {
56
+ function registerCachedTileProtocol(maxConcurrentFetches) {
24
57
  if (cachedTileProtocolRegistered)
25
58
  return;
59
+ const semaphore = new Semaphore(maxConcurrentFetches);
26
60
  maplibregl.addProtocol("cached-tile", (params, abortController) => {
27
- // Convert cached-tile:// URL back to https://
28
61
  const url = params.url.replace("cached-tile://", "https://");
29
- return fetch(url, { signal: abortController.signal })
62
+ // Pass the abort signal to acquire() so a request cancelled while still
63
+ // queued (e.g. during rapid zoom) drops out of the FIFO queue instead of
64
+ // waiting for a slot just to bail. release() runs only inside this chain,
65
+ // i.e. only after a slot was actually granted.
66
+ return semaphore.acquire(abortController.signal).then(() => fetch(url, { signal: abortController.signal })
30
67
  .then((response) => {
31
68
  if (!response.ok) {
32
69
  throw new Error(`HTTP error! status: ${response.status}`);
33
70
  }
34
71
  return response.arrayBuffer();
35
72
  })
36
- .then((data) => ({ data }));
73
+ .then((data) => ({ data }))
74
+ .finally(() => {
75
+ semaphore.release();
76
+ }));
37
77
  });
38
78
  cachedTileProtocolRegistered = true;
39
79
  }
@@ -92,13 +132,15 @@ import { RouteController } from "../controllers/routeController";
92
132
  import { rtlLanguages } from "../domain/models/_rtl";
93
133
  import { InternalAnimationConfig, InternalAnimationDrawingConfig, } from "../domain/models/animation";
94
134
  import { Loggeable } from "../domain/models/loggeable";
95
- import { DEFAULT_TILE_CACHE_CONFIG, } from "../domain/models/mapConfig";
135
+ import { DEFAULT_TILE_CACHE_CONFIG, MAPLIBRE_MAX_TILE_CACHE_HARD_CAP, } from "../domain/models/mapConfig";
136
+ import { CIRCLE_RENDER_ORDERS, circleRing, cloneCircleRecord, MAPVX_BRAND_COLOR, resolveCircleConfig, } from "../domain/models/circle";
96
137
  import { MarkerAttribute } from "../domain/models/marker";
97
138
  import { MVXRoute } from "../domain/models/route";
98
139
  import { InternalDrawRouteConfiguration, InternalGetRouteConfiguration, } from "../domain/models/routeConfiguration";
99
140
  import { Repository } from "../repository/repository";
100
141
  import { getBoundingBox } from "../utils/utils";
101
142
  import { extractStepCoordinates } from "../utils/route-utils";
143
+ import { buildInteractionOptions, shouldDisableTouchRotation } from "./mapInteractionOptions";
102
144
  /**
103
145
  * Class to interact with the map.
104
146
  * @category Map
@@ -122,6 +164,7 @@ export class InternalMapVXMap extends Loggeable {
122
164
  this.currentFloor = "";
123
165
  this.baseFilters = {};
124
166
  this.markers = [];
167
+ this.circles = [];
125
168
  this.enableHover = false;
126
169
  this.hoveredId = "unselected";
127
170
  this.failedTiles = new Set();
@@ -137,8 +180,11 @@ export class InternalMapVXMap extends Loggeable {
137
180
  this.watchPositionID = undefined;
138
181
  this.onFloorChange = mapConfig.onFloorChange;
139
182
  this.onParentPlaceChange = mapConfig.onParentPlaceChange;
140
- // Merge tile cache config with defaults
141
- this.tileCacheConfig = Object.assign(Object.assign({}, DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
183
+ this.tileCacheConfig = (() => {
184
+ const merged = Object.assign(Object.assign({}, DEFAULT_TILE_CACHE_CONFIG), mapConfig.tileCache);
185
+ merged.maxTiles = Math.min(merged.maxTiles, MAPLIBRE_MAX_TILE_CACHE_HARD_CAP);
186
+ return merged;
187
+ })();
142
188
  if (mapConfig.parentPlaceId != null) {
143
189
  this.initialPlaceDetailSetUp(mapConfig.parentPlaceId, mapConfig.authToken);
144
190
  }
@@ -235,28 +281,17 @@ export class InternalMapVXMap extends Loggeable {
235
281
  .catch(console.error);
236
282
  }
237
283
  onMapStyleLoaded(mapConfig, container, style) {
238
- var _a, _b, _c;
284
+ var _a, _b, _c, _d, _e;
239
285
  // Determine if service worker caching should be enabled
240
286
  const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
241
- // Register cached-tile protocol only if service worker caching is enabled
242
287
  if (useServiceWorkerCaching) {
243
- registerCachedTileProtocol();
288
+ registerCachedTileProtocol(this.tileCacheConfig.maxConcurrentTileFetches);
244
289
  }
245
290
  // Transform tile URLs only if service worker caching is enabled
246
291
  const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
247
- const mapOptions = {
248
- container,
249
- style: finalStyle,
250
- center: mapConfig.center,
251
- zoom: mapConfig.zoom,
252
- pitch: (_a = mapConfig.pitch) !== null && _a !== void 0 ? _a : 0,
253
- attributionControl: false,
254
- maplibreLogo: false,
255
- bearingSnap: (_b = mapConfig.bearingSnap) !== null && _b !== void 0 ? _b : 0,
256
- cancelPendingTileRequestsWhileZooming: false,
292
+ 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,
257
293
  // Use configured maxTiles for MapLibre's memory cache
258
- maxTileCacheSize: this.tileCacheConfig.maxTiles,
259
- };
294
+ maxTileCacheSize: this.tileCacheConfig.maxTiles }, buildInteractionOptions(mapConfig));
260
295
  if (mapConfig.maxZoom)
261
296
  mapOptions.maxZoom = mapConfig.maxZoom;
262
297
  if (mapConfig.minZoom)
@@ -266,10 +301,15 @@ export class InternalMapVXMap extends Loggeable {
266
301
  mapOptions.maxBounds = new LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
267
302
  }
268
303
  this.map = new Map(mapOptions);
304
+ // When rotation is disabled we still want pinch-to-zoom to work, so the
305
+ // two-finger rotation is turned off here instead of via the constructor.
306
+ if (shouldDisableTouchRotation(mapConfig)) {
307
+ (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.disableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
308
+ }
269
309
  this.map.addControl(new NavigationControl({
270
310
  showCompass: mapConfig.showCompass !== undefined ? mapConfig.showCompass : true,
271
311
  showZoom: mapConfig.showZoom !== undefined ? mapConfig.showZoom : true,
272
- }), (_c = mapConfig.navigationPosition) !== null && _c !== void 0 ? _c : "top-right");
312
+ }), (_e = mapConfig.navigationPosition) !== null && _e !== void 0 ? _e : "top-right");
273
313
  this.map.on("load", () => {
274
314
  var _a;
275
315
  this.whenStyleUpdates(style);
@@ -279,6 +319,12 @@ export class InternalMapVXMap extends Loggeable {
279
319
  this.onHover();
280
320
  this.subscribeToFailedTiles();
281
321
  });
322
+ // Self-healing for circles: a full style reload wipes custom sources and
323
+ // layers, and not every restyle path goes through whenStyleUpdates.
324
+ this.map.on("styledata", () => {
325
+ if (this.circles.length > 0)
326
+ this.ensureCircleLayers();
327
+ });
282
328
  this.map.on("zoomend", () => {
283
329
  var _a;
284
330
  (_a = mapConfig.onZoomEnd) === null || _a === void 0 ? void 0 : _a.call(mapConfig, this.getZoomLevel());
@@ -397,6 +443,7 @@ export class InternalMapVXMap extends Loggeable {
397
443
  return Object.assign(Object.assign({}, segmentation), { token: this.token, parentPlaceId: (_a = this.parentPlaceId) !== null && _a !== void 0 ? _a : "None" });
398
444
  }
399
445
  destroyMap() {
446
+ this.circles = [];
400
447
  this.map.remove();
401
448
  this.unsubscribeFromFailedTiles();
402
449
  }
@@ -480,6 +527,19 @@ export class InternalMapVXMap extends Loggeable {
480
527
  }
481
528
  setLang(lang) {
482
529
  this.repository.setLang(lang);
530
+ if (rtlLanguages.includes(lang)) {
531
+ this.setRTLSupport();
532
+ }
533
+ // setLayersForLanguage reads this.map.getStyle()?.layers, which is empty
534
+ // until the style finishes loading. If setLang is called right after
535
+ // createMap (before the "load" event), apply it once the style is ready
536
+ // so the language change is not silently dropped.
537
+ if (this.map.isStyleLoaded()) {
538
+ this.setLayersForLanguage(lang);
539
+ }
540
+ else {
541
+ this.map.once("load", () => this.setLayersForLanguage(lang));
542
+ }
483
543
  }
484
544
  setParentPlace(place, updateStyle, onStyleReady) {
485
545
  this.changeParentPlaceTo(place, updateStyle, onStyleReady);
@@ -500,6 +560,7 @@ export class InternalMapVXMap extends Loggeable {
500
560
  this.setBaseFilters(newStyle);
501
561
  }
502
562
  this.routeController.addSourcesAndLayers();
563
+ this.refreshCircles();
503
564
  this.filterByFloorKey(this.currentFloor);
504
565
  }
505
566
  addMarker(marker) {
@@ -605,6 +666,320 @@ export class InternalMapVXMap extends Loggeable {
605
666
  throw new Error("Failed to remove all markers");
606
667
  }
607
668
  }
669
+ addCircle(circle) {
670
+ try {
671
+ // Check if a circle with the same ID already exists and replace it
672
+ if (circle.id) {
673
+ this.circles = this.circles.filter((c) => c.id !== circle.id);
674
+ }
675
+ // resolveCircleConfig validates radiusMeters and coordinates
676
+ const record = resolveCircleConfig(circle);
677
+ this.circles.push(record);
678
+ this.refreshCircles();
679
+ this.logEvent("addCircle");
680
+ return record.id;
681
+ }
682
+ catch (error) {
683
+ throw new Error(`Failed to add circle: ${error instanceof Error ? error.message : String(error)}`);
684
+ }
685
+ }
686
+ updateCircle(circleConfig) {
687
+ try {
688
+ const index = this.circles.findIndex((c) => c.id === circleConfig.id);
689
+ if (index === -1)
690
+ return null;
691
+ // resolveCircleConfig validates radiusMeters and coordinates
692
+ this.circles[index] = resolveCircleConfig(circleConfig, this.circles[index].hidden);
693
+ this.refreshCircles();
694
+ this.logEvent("updateCircle");
695
+ return this.circles[index].id;
696
+ }
697
+ catch (error) {
698
+ throw new Error(`Failed to update circle: ${error instanceof Error ? error.message : String(error)}`);
699
+ }
700
+ }
701
+ getCircle(circleId) {
702
+ const circle = this.circles.find((c) => c.id === circleId);
703
+ return circle ? cloneCircleRecord(circle) : undefined;
704
+ }
705
+ getCircles() {
706
+ return this.circles.map(cloneCircleRecord);
707
+ }
708
+ hasCircle(circleId) {
709
+ return this.circles.some((c) => c.id === circleId);
710
+ }
711
+ updateCirclePosition(circleId, center, radiusMeters) {
712
+ try {
713
+ const circle = this.circles.find((c) => c.id === circleId);
714
+ if (circle === undefined)
715
+ return false;
716
+ // Validate inputs before modifying
717
+ if (!center) {
718
+ throw new Error("Circle center is required");
719
+ }
720
+ const { lat, lng } = center;
721
+ if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
722
+ throw new Error(`Invalid latitude: ${lat}. Must be between -90 and 90.`);
723
+ }
724
+ if (!Number.isFinite(lng) || lng < -180 || lng > 180) {
725
+ throw new Error(`Invalid longitude: ${lng}. Must be between -180 and 180.`);
726
+ }
727
+ if (radiusMeters !== undefined) {
728
+ if (!Number.isFinite(radiusMeters) || radiusMeters <= 0) {
729
+ throw new Error(`Invalid radiusMeters: ${radiusMeters}. Must be a positive finite number.`);
730
+ }
731
+ }
732
+ circle.coordinate = { lat: center.lat, lng: center.lng };
733
+ if (radiusMeters !== undefined) {
734
+ circle.radiusMeters = radiusMeters;
735
+ }
736
+ this.refreshCircles();
737
+ this.logEvent("updateCirclePosition");
738
+ return true;
739
+ }
740
+ catch (error) {
741
+ throw new Error(`Failed to update circle position: ${error instanceof Error ? error.message : String(error)}`);
742
+ }
743
+ }
744
+ updateCircleStyle(circleId, style) {
745
+ try {
746
+ const circle = this.circles.find((c) => c.id === circleId);
747
+ if (circle === undefined)
748
+ return false;
749
+ // Update only style properties, clamp opacities
750
+ if (style.fillColor !== undefined)
751
+ circle.fillColor = style.fillColor;
752
+ if (style.fillOpacity !== undefined)
753
+ circle.fillOpacity = Math.max(0, Math.min(1, style.fillOpacity));
754
+ if (style.strokeColor !== undefined)
755
+ circle.strokeColor = style.strokeColor;
756
+ if (style.strokeWidth !== undefined)
757
+ circle.strokeWidth = style.strokeWidth;
758
+ if (style.strokeOpacity !== undefined)
759
+ circle.strokeOpacity = Math.max(0, Math.min(1, style.strokeOpacity));
760
+ this.refreshCircles();
761
+ this.logEvent("updateCircleStyle");
762
+ return true;
763
+ }
764
+ catch (error) {
765
+ throw new Error(`Failed to update circle style: ${error instanceof Error ? error.message : String(error)}`);
766
+ }
767
+ }
768
+ removeCircle(circleId) {
769
+ try {
770
+ this.circles = this.circles.filter((c) => c.id !== circleId);
771
+ this.refreshCircles();
772
+ this.logEvent("removeCircle");
773
+ }
774
+ catch (error) {
775
+ throw new Error("Failed to remove circle");
776
+ }
777
+ }
778
+ removeAllCircles() {
779
+ try {
780
+ this.circles = [];
781
+ this.refreshCircles();
782
+ this.logEvent("removeAllCircles");
783
+ }
784
+ catch (error) {
785
+ throw new Error("Failed to remove all circles");
786
+ }
787
+ }
788
+ showCircle(circleId) {
789
+ try {
790
+ const circle = this.circles.find((c) => c.id === circleId);
791
+ if (circle === undefined) {
792
+ return false;
793
+ }
794
+ circle.hidden = false;
795
+ this.refreshCircles();
796
+ this.logEvent("showCircle");
797
+ return true;
798
+ }
799
+ catch (error) {
800
+ return false;
801
+ }
802
+ }
803
+ hideCircle(circleId) {
804
+ try {
805
+ const circle = this.circles.find((c) => c.id === circleId);
806
+ if (circle === undefined) {
807
+ return false;
808
+ }
809
+ circle.hidden = true;
810
+ this.refreshCircles();
811
+ this.logEvent("hideCircle");
812
+ return true;
813
+ }
814
+ catch (error) {
815
+ return false;
816
+ }
817
+ }
818
+ /**
819
+ * Builds the GeoJSON FeatureCollection for every currently visible circle.
820
+ * Visibility mirrors marker semantics: a circle with a floor is shown only
821
+ * while that floor is displayed, and a circle without a floor is shown only
822
+ * in outdoor contexts. Hidden circles are always omitted.
823
+ */
824
+ circleFeatureCollection() {
825
+ var _a, _b, _c;
826
+ const floorId = (_a = this.currentFloor) !== null && _a !== void 0 ? _a : "";
827
+ const isOutdoor = !this.parentPlace ||
828
+ ((_c = (_b = this.innerFloors.find((floor) => floor.key === floorId)) === null || _b === void 0 ? void 0 : _b.reachableFromGPS) !== null && _c !== void 0 ? _c : false);
829
+ const features = this.circles
830
+ .filter((circle) => {
831
+ var _a;
832
+ if (circle.hidden)
833
+ return false;
834
+ const circleFloor = (_a = circle.floorId) !== null && _a !== void 0 ? _a : "";
835
+ return circleFloor === floorId || (isOutdoor && circleFloor === "");
836
+ })
837
+ .map((circle) => ({
838
+ type: "Feature",
839
+ properties: {
840
+ id: circle.id,
841
+ fillColor: circle.fillColor,
842
+ fillOpacity: circle.fillOpacity,
843
+ strokeColor: circle.strokeColor,
844
+ strokeWidth: circle.strokeWidth,
845
+ strokeOpacity: circle.strokeOpacity,
846
+ renderOrder: circle.renderOrder,
847
+ },
848
+ geometry: {
849
+ type: "Polygon",
850
+ coordinates: [
851
+ circleRing(circle.coordinate.lng, circle.coordinate.lat, circle.radiusMeters),
852
+ ],
853
+ },
854
+ }));
855
+ return { type: "FeatureCollection", features };
856
+ }
857
+ /**
858
+ * Idempotently adds the shared circle source and its fill/line layers.
859
+ * Layers are inserted below the first symbol layer so place labels and
860
+ * markers stay readable above the translucent fill. Safe to call at any
861
+ * time: a style that is still loading simply rejects the calls, and the
862
+ * styledata listener retries once the style is ready.
863
+ */
864
+ /**
865
+ * Resolves the layer id to insert circle layers before, for one render
866
+ * order. `undefined` means "append on top".
867
+ *
868
+ * `aboveBasemap` finds the topmost non-symbol layer of the style and
869
+ * inserts before the first symbol layer that follows it. On indoor styles
870
+ * the floor-plate and building polygons are ordered after the first symbol
871
+ * layer, so anchoring on the topmost geometry layer — instead of the first
872
+ * symbol layer — guarantees circles are never occluded by basemap fills
873
+ * while still rendering below the labels that follow them.
874
+ */
875
+ circleBeforeIdFor(order) {
876
+ var _a, _b, _c, _d;
877
+ if (order === "top")
878
+ return undefined;
879
+ 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));
880
+ if (order === "belowLabels") {
881
+ return (_c = layers.find((layer) => layer.type === "symbol")) === null || _c === void 0 ? void 0 : _c.id;
882
+ }
883
+ // aboveBasemap
884
+ let lastNonSymbolIndex = -1;
885
+ layers.forEach((layer, index) => {
886
+ if (layer.type !== "symbol")
887
+ lastNonSymbolIndex = index;
888
+ });
889
+ return (_d = layers.slice(lastNonSymbolIndex + 1).find((layer) => layer.type === "symbol")) === null || _d === void 0 ? void 0 : _d.id;
890
+ }
891
+ /**
892
+ * Idempotently ensures the shared circle source and one fill/line layer
893
+ * pair per render order in use, at the placement that order requires.
894
+ * Placement is recomputed and re-asserted (via moveLayer) on every call,
895
+ * so circles regain their correct z-order after any style reload or
896
+ * floor/parent-place change. Safe to call at any time: a style that is
897
+ * still loading simply rejects the calls, and the styledata listener
898
+ * retries once the style is ready.
899
+ */
900
+ ensureCircleLayers() {
901
+ if (!this.map)
902
+ return;
903
+ try {
904
+ if (!this.map.getSource(CIRCLE_SOURCE_ID)) {
905
+ this.map.addSource(CIRCLE_SOURCE_ID, {
906
+ type: "geojson",
907
+ data: this.circleFeatureCollection(),
908
+ });
909
+ }
910
+ const ordersInUse = new Set(this.circles.map((c) => c.renderOrder));
911
+ // Always keep the default bucket alive so an empty map still renders
912
+ // newly added circles without a layer rebuild
913
+ ordersInUse.add("aboveBasemap");
914
+ // Process buckets lowest-first (CIRCLE_RENDER_ORDERS is canonical):
915
+ // when two buckets resolve to the same anchor, each addLayer/moveLayer
916
+ // lands immediately before it, so a bucket processed later paints above
917
+ // the ones processed earlier. Canonical order keeps
918
+ // belowLabels < aboveBasemap < top regardless of circle insertion order.
919
+ for (const order of CIRCLE_RENDER_ORDERS) {
920
+ if (!ordersInUse.has(order))
921
+ continue;
922
+ const ids = circleLayerIdsFor(order);
923
+ const beforeId = this.circleBeforeIdFor(order);
924
+ const orderFilter = ["==", ["get", "renderOrder"], order];
925
+ if (!this.map.getLayer(ids.fill)) {
926
+ const fillLayer = {
927
+ id: ids.fill,
928
+ type: "fill",
929
+ source: CIRCLE_SOURCE_ID,
930
+ filter: orderFilter,
931
+ paint: {
932
+ "fill-color": ["get", "fillColor"],
933
+ "fill-opacity": ["get", "fillOpacity"],
934
+ },
935
+ };
936
+ this.map.addLayer(fillLayer, beforeId);
937
+ }
938
+ else {
939
+ this.map.moveLayer(ids.fill, beforeId);
940
+ }
941
+ if (!this.map.getLayer(ids.line)) {
942
+ const lineLayer = {
943
+ id: ids.line,
944
+ type: "line",
945
+ source: CIRCLE_SOURCE_ID,
946
+ filter: orderFilter,
947
+ paint: {
948
+ "line-color": ["get", "strokeColor"],
949
+ "line-width": ["get", "strokeWidth"],
950
+ "line-opacity": ["get", "strokeOpacity"],
951
+ },
952
+ };
953
+ this.map.addLayer(lineLayer, beforeId);
954
+ }
955
+ else {
956
+ this.map.moveLayer(ids.line, beforeId);
957
+ }
958
+ }
959
+ }
960
+ catch (error) {
961
+ // Style may not be loaded yet; the styledata listener re-adds the layers
962
+ }
963
+ }
964
+ /**
965
+ * Re-renders all circles: ensures the source and layers exist, then pushes
966
+ * the current FeatureCollection. Called after every mutation of the circle
967
+ * list, on floor changes, and when the map style reloads.
968
+ */
969
+ refreshCircles() {
970
+ if (!this.map)
971
+ return;
972
+ if (this.circles.length === 0 && !this.map.getSource(CIRCLE_SOURCE_ID))
973
+ return;
974
+ this.ensureCircleLayers();
975
+ try {
976
+ const source = this.map.getSource(CIRCLE_SOURCE_ID);
977
+ source === null || source === void 0 ? void 0 : source.setData(this.circleFeatureCollection());
978
+ }
979
+ catch (error) {
980
+ // Source may not exist while a new style is loading
981
+ }
982
+ }
608
983
  /**
609
984
  * Use it to change the current layer
610
985
  * @param floorKey floor id
@@ -617,6 +992,7 @@ export class InternalMapVXMap extends Loggeable {
617
992
  }
618
993
  this.updateFiltersTo(floorKeyString);
619
994
  this.updateMarkersTo(floorKeyString);
995
+ this.refreshCircles();
620
996
  this.routeController.updateRouteLayers(floorKeyString);
621
997
  this.routeController.updateRouteMarkerVisibility(floorKeyString);
622
998
  }
@@ -722,6 +1098,49 @@ export class InternalMapVXMap extends Loggeable {
722
1098
  this.map.setMinZoom(zoomLvl);
723
1099
  (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
724
1100
  }
1101
+ setBearing(degrees, options) {
1102
+ var _a;
1103
+ if (!Number.isFinite(degrees))
1104
+ return;
1105
+ if (options === null || options === void 0 ? void 0 : options.animate) {
1106
+ if (options.onComplete)
1107
+ void this.map.once("moveend", options.onComplete);
1108
+ this.map.rotateTo(degrees, { duration: 600 });
1109
+ return;
1110
+ }
1111
+ this.map.setBearing(degrees);
1112
+ (_a = options === null || options === void 0 ? void 0 : options.onComplete) === null || _a === void 0 ? void 0 : _a.call(options);
1113
+ }
1114
+ setRotationEnabled(enabled) {
1115
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1116
+ if (enabled) {
1117
+ (_b = (_a = this.map.dragRotate) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1118
+ (_d = (_c = this.map.touchZoomRotate) === null || _c === void 0 ? void 0 : _c.enableRotation) === null || _d === void 0 ? void 0 : _d.call(_c);
1119
+ (_f = (_e = this.map.keyboard) === null || _e === void 0 ? void 0 : _e.enable) === null || _f === void 0 ? void 0 : _f.call(_e);
1120
+ }
1121
+ else {
1122
+ (_h = (_g = this.map.dragRotate) === null || _g === void 0 ? void 0 : _g.disable) === null || _h === void 0 ? void 0 : _h.call(_g);
1123
+ (_k = (_j = this.map.touchZoomRotate) === null || _j === void 0 ? void 0 : _j.disableRotation) === null || _k === void 0 ? void 0 : _k.call(_j);
1124
+ }
1125
+ }
1126
+ setPanEnabled(enabled) {
1127
+ var _a, _b, _c, _d;
1128
+ if (enabled) {
1129
+ (_b = (_a = this.map.dragPan) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1130
+ }
1131
+ else {
1132
+ (_d = (_c = this.map.dragPan) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1133
+ }
1134
+ }
1135
+ setScrollZoomEnabled(enabled) {
1136
+ var _a, _b, _c, _d;
1137
+ if (enabled) {
1138
+ (_b = (_a = this.map.scrollZoom) === null || _a === void 0 ? void 0 : _a.enable) === null || _b === void 0 ? void 0 : _b.call(_a);
1139
+ }
1140
+ else {
1141
+ (_d = (_c = this.map.scrollZoom) === null || _c === void 0 ? void 0 : _c.disable) === null || _d === void 0 ? void 0 : _d.call(_c);
1142
+ }
1143
+ }
725
1144
  isInsideBounds(point) {
726
1145
  const mapBounds = this.map.getMaxBounds();
727
1146
  if (mapBounds != null) {
@@ -840,7 +1259,7 @@ export class InternalMapVXMap extends Loggeable {
840
1259
  throw new Error("Error: Failed to add route");
841
1260
  }
842
1261
  }
843
- updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#757575" }) {
1262
+ updateRouteProgress(routeId, position, behindStyle = { type: "Solid", color: "#276EF1" }) {
844
1263
  try {
845
1264
  const behindConfig = new InternalDrawRouteConfiguration({
846
1265
  routeStyle: behindStyle,
@@ -890,13 +1309,12 @@ export class InternalMapVXMap extends Loggeable {
890
1309
  if (pointedPlace !== undefined) {
891
1310
  new maplibregl.Popup()
892
1311
  .setLngLat(pointedPlace.position)
893
- .setHTML(pointedPlace.title)
1312
+ .setText(pointedPlace.title)
894
1313
  .addTo(this.map);
895
1314
  }
896
1315
  }
897
1316
  return "";
898
1317
  }
899
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
900
1318
  removePopOver(id) {
901
1319
  throw Error("Not implemented");
902
1320
  }
@@ -1019,7 +1437,7 @@ export class InternalMapVXMap extends Loggeable {
1019
1437
  filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
1020
1438
  paint: {
1021
1439
  "fill-extrusion-height": 2.5,
1022
- "fill-extrusion-color": "#276EF1",
1440
+ "fill-extrusion-color": MAPVX_BRAND_COLOR,
1023
1441
  "fill-extrusion-opacity": 0.8,
1024
1442
  },
1025
1443
  };
@@ -1033,7 +1451,7 @@ export class InternalMapVXMap extends Loggeable {
1033
1451
  "source-layer": "area",
1034
1452
  filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
1035
1453
  paint: {
1036
- "fill-color": "#276EF1",
1454
+ "fill-color": MAPVX_BRAND_COLOR,
1037
1455
  },
1038
1456
  };
1039
1457
  this.map.addLayer(layer);
@@ -1291,7 +1709,7 @@ export class InternalMapVXMap extends Loggeable {
1291
1709
  "source-layer": "area",
1292
1710
  filter: ["all", ["has", "ref"], ["==", ["get", "ref"], "unselected"]],
1293
1711
  paint: {
1294
- "line-color": "#276EF1",
1712
+ "line-color": MAPVX_BRAND_COLOR,
1295
1713
  "line-width": 4,
1296
1714
  },
1297
1715
  };