@mapvx/web-js 3.4.0-dev.2 → 3.5.0-dev.1

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 (51) hide show
  1. package/dist/cjs/domain/models/campaign.js +73 -0
  2. package/dist/cjs/domain/models/campaign.js.map +1 -0
  3. package/dist/cjs/index.js +4 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/logger/sentry.js +1 -1
  6. package/dist/cjs/map/map.js +380 -14
  7. package/dist/cjs/map/map.js.map +1 -1
  8. package/dist/cjs/map/scene3DController.js +36 -0
  9. package/dist/cjs/map/scene3DController.js.map +1 -1
  10. package/dist/cjs/repository/repository.js +65 -4
  11. package/dist/cjs/repository/repository.js.map +1 -1
  12. package/dist/cjs/repository/requester.js +81 -9
  13. package/dist/cjs/repository/requester.js.map +1 -1
  14. package/dist/cjs/sdk.js +12 -0
  15. package/dist/cjs/sdk.js.map +1 -1
  16. package/dist/es/config/sdkConfig.d.ts +15 -0
  17. package/dist/es/config/sdkConfig.d.ts.map +1 -1
  18. package/dist/es/domain/models/campaign.d.ts +116 -0
  19. package/dist/es/domain/models/campaign.d.ts.map +1 -0
  20. package/dist/es/domain/models/campaign.js +69 -0
  21. package/dist/es/domain/models/campaign.js.map +1 -0
  22. package/dist/es/domain/models/mapConfig.d.ts +6 -0
  23. package/dist/es/domain/models/mapConfig.d.ts.map +1 -1
  24. package/dist/es/index.d.ts +2 -0
  25. package/dist/es/index.d.ts.map +1 -1
  26. package/dist/es/index.js +2 -0
  27. package/dist/es/index.js.map +1 -1
  28. package/dist/es/logger/sentry.js +1 -1
  29. package/dist/es/map/map.d.ts +145 -0
  30. package/dist/es/map/map.d.ts.map +1 -1
  31. package/dist/es/map/map.js +380 -14
  32. package/dist/es/map/map.js.map +1 -1
  33. package/dist/es/map/scene3DController.d.ts +20 -0
  34. package/dist/es/map/scene3DController.d.ts.map +1 -1
  35. package/dist/es/map/scene3DController.js +36 -0
  36. package/dist/es/map/scene3DController.js.map +1 -1
  37. package/dist/es/repository/repository.d.ts +16 -1
  38. package/dist/es/repository/repository.d.ts.map +1 -1
  39. package/dist/es/repository/repository.js +65 -4
  40. package/dist/es/repository/repository.js.map +1 -1
  41. package/dist/es/repository/requester.d.ts +30 -1
  42. package/dist/es/repository/requester.d.ts.map +1 -1
  43. package/dist/es/repository/requester.js +81 -9
  44. package/dist/es/repository/requester.js.map +1 -1
  45. package/dist/es/sdk.d.ts +11 -1
  46. package/dist/es/sdk.d.ts.map +1 -1
  47. package/dist/es/sdk.js +12 -0
  48. package/dist/es/sdk.js.map +1 -1
  49. package/dist/umd/index.js +1688 -838
  50. package/dist/umd/index.js.map +1 -1
  51. package/package.json +1 -1
@@ -63,6 +63,13 @@ const MAX_TILE_RETRIES = 5;
63
63
  * often instead of once per timer tick.
64
64
  */
65
65
  const TILE_RETRY_BASE_DELAY_MS = 1000;
66
+ /**
67
+ * Safety deadline for {@link InternalMapVXMap.setStyle}'s `onStyleReady` callback. The
68
+ * callback normally fires on the map's `idle` event, but `idle` may never arrive (the
69
+ * style request failed, the swap never settles). Callers hide the map behind a loading
70
+ * overlay until the callback arrives, so it must be released no matter what.
71
+ */
72
+ const STYLE_READY_TIMEOUT_MS = 4000;
66
73
  /** Fill/line layer ids for one circle render-order bucket. */
67
74
  function circleLayerIdsFor(order) {
68
75
  const suffix = CIRCLE_LAYER_SUFFIXES[order];
@@ -207,7 +214,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
207
214
  * @returns A new instance of MapVXMap.
208
215
  */
209
216
  constructor(mapConfig, container, token) {
210
- var _a, _b, _c;
217
+ var _a, _b, _c, _d;
211
218
  super();
212
219
  /**
213
220
  * Monotonic counter for {@link addLiveRoute} calls. Refreshes overlap (each one
@@ -221,6 +228,34 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
221
228
  this.subPlaces = [];
222
229
  this.currentFloor = "";
223
230
  this.baseFilters = {};
231
+ /**
232
+ * Layer ids whose declared (base) filter has already been resolved — either captured
233
+ * into {@link baseFilters} or confirmed absent.
234
+ *
235
+ * `baseFilters[id] === undefined` cannot carry that information on its own: it means
236
+ * both "not looked at yet" and "looked at, the layer declares no filter". Without the
237
+ * distinction, a later pass re-reads the layer's LIVE filter — which by then is the
238
+ * one this class wrote — and stores our own floor/public-place filter as if it were
239
+ * the style's, so the layer keeps filtering by a stale floor forever.
240
+ *
241
+ * An id lands here the moment its declared filter is read from the style spec, or the
242
+ * moment this class writes a filter to that layer, whichever happens first. Layers
243
+ * added asynchronously after a `setStyle` swap are in neither situation, so their
244
+ * genuinely pristine live filter is still captured on first touch.
245
+ */
246
+ this.capturedBaseFilters = new Set();
247
+ /**
248
+ * Monotonic id of the most recently STARTED style swap. Every {@link setStyle} call
249
+ * captures its own value and checks it again in each async continuation, so a
250
+ * superseded swap can neither touch the shared bookkeeping (`styleReadyTimer`,
251
+ * `pendingStyleSwapListener`) nor apply its obsolete style to the live map.
252
+ *
253
+ * A per-call `readied` flag is not enough: the timer and the listener slot are
254
+ * single-valued instance state, so two overlapping swaps otherwise clobber each
255
+ * other's — one caller's overlay is released by the other's failure, or an
256
+ * abandoned listener re-seeds filters from layer ids the current style lacks.
257
+ */
258
+ this.styleSwapGeneration = 0;
224
259
  this.markers = [];
225
260
  this.circles = [];
226
261
  this.enableHover = false;
@@ -242,15 +277,18 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
242
277
  routeApiUrl: mapConfig.routeApiUrl,
243
278
  healthApiUrl: mapConfig.healthApiUrl,
244
279
  legacyApiUrl: mapConfig.legacyApiUrl,
280
+ portalApiUrl: mapConfig.portalApiUrl,
245
281
  });
246
282
  this.token = token;
283
+ this.authToken = mapConfig.authToken;
284
+ this.currentLang = (_c = mapConfig.lang) !== null && _c !== void 0 ? _c : "en";
247
285
  this.parentPlaceId = mapConfig.parentPlaceId;
248
286
  this.routeController = new routeController_1.RouteController(this);
249
287
  this.userLocationMarker = undefined;
250
288
  this.watchPositionID = undefined;
251
289
  this.onFloorChange = mapConfig.onFloorChange;
252
290
  this.onParentPlaceChange = mapConfig.onParentPlaceChange;
253
- this.mode = (_c = mapConfig.mode) !== null && _c !== void 0 ? _c : "2D";
291
+ this.mode = (_d = mapConfig.mode) !== null && _d !== void 0 ? _d : "2D";
254
292
  this.scene3DConfig = mapConfig.scene3D;
255
293
  this.initialCenter = mapConfig.center;
256
294
  this.tileCacheConfig = (() => {
@@ -433,6 +471,17 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
433
471
  style.layers
434
472
  .filter((layer) => layer.id.startsWith("indoor-"))
435
473
  .forEach((layer) => {
474
+ // Seed from the STYLE SPEC — the only pristine source — and do it BEFORE the
475
+ // setFilter below overwrites the live filter. Reading it back afterwards would
476
+ // return our own `publicPlaceFilter` and store it as the layer's declared one,
477
+ // turning `public_place` from an OR into an AND and hiding every private
478
+ // feature on the selected floor. Every indoor layer is covered, not just
479
+ // fill/line/symbol: a `circle` layer is discriminated by its filter just the
480
+ // same, and dropping it makes the layer repaint every feature.
481
+ // `filter` is absent from the union member for `background` layers, hence the
482
+ // structural read rather than `layer.filter`.
483
+ const declared = layer.filter;
484
+ this.captureDeclaredBaseFilter(layer.id, declared);
436
485
  const baseFilter = this.baseFilters[layer.id];
437
486
  if (baseFilter != null) {
438
487
  const finalFilter = ["all", baseFilter, publicPlaceFilter];
@@ -441,20 +490,29 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
441
490
  else {
442
491
  this.map.setFilter(layer.id, publicPlaceFilter);
443
492
  }
444
- switch (layer.type) {
445
- case "line":
446
- case "symbol":
447
- case "fill-extrusion":
448
- case "fill":
449
- if (layer.filter != null) {
450
- this.baseFilters[layer.id] = layer.filter;
451
- }
452
- break;
453
- default:
454
- break;
455
- }
456
493
  });
457
494
  }
495
+ /**
496
+ * Records a layer's declared (base) filter the first time the layer is seen, from an
497
+ * explicit value (a style-spec `filter`) when one is available, otherwise by reading
498
+ * the live filter — which is only pristine for layers this class has never written to.
499
+ *
500
+ * Marking happens even when no filter exists, so the layer is never revisited: on a
501
+ * second visit its live filter is one we produced, and storing that as the "declared"
502
+ * filter is what freezes a layer to a stale floor.
503
+ *
504
+ * @param layerId Id of the layer whose base filter should be resolved.
505
+ * @param declared Declared filter straight from the style spec, when the caller has it.
506
+ */
507
+ captureDeclaredBaseFilter(layerId, declared) {
508
+ if (this.capturedBaseFilters.has(layerId))
509
+ return;
510
+ this.capturedBaseFilters.add(layerId);
511
+ const baseFilter = declared !== null && declared !== void 0 ? declared : this.safeGetFilter(layerId);
512
+ if (baseFilter != null) {
513
+ this.baseFilters[layerId] = baseFilter;
514
+ }
515
+ }
458
516
  setLayersForLanguage(lang) {
459
517
  var _a, _b;
460
518
  // outdoor layers
@@ -536,6 +594,14 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
536
594
  var _a;
537
595
  this.circles = [];
538
596
  this.markerLayoutController.destroy();
597
+ // Drop any in-flight `setStyle` bookkeeping while the map still exists: the
598
+ // listener has to be detached before `map.remove()`, and the safety timer must not
599
+ // outlive the map and call `onStyleReady` for a map that is already gone.
600
+ this.clearPendingStyleSwapListener();
601
+ if (this.styleReadyTimer) {
602
+ clearTimeout(this.styleReadyTimer);
603
+ this.styleReadyTimer = undefined;
604
+ }
539
605
  // Dispose the 3D scene's GPU resources before the map (and its shared WebGL context) go away —
540
606
  // MapLibre's own teardown never calls the custom GLB layer's `onRemove`.
541
607
  (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.dispose();
@@ -610,6 +676,13 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
610
676
  const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
611
677
  const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
612
678
  this.map.setStyle(finalStyle);
679
+ // The new place's style declares its own filters for the same `indoor-*` layer
680
+ // ids. Without this reset every id is still marked captured from the previous
681
+ // place, so `captureDeclaredBaseFilter` early-returns for all of them and the
682
+ // previous place's expressions keep filtering the new style forever. (The
683
+ // `switch (layer.type)` block that used to re-read them on every style load was
684
+ // removed when capture became once-per-id, which is what made this necessary.)
685
+ this.resetBaseFilters();
613
686
  this.whenStyleUpdates(finalStyle);
614
687
  onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady();
615
688
  })
@@ -649,6 +722,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
649
722
  }
650
723
  setLang(lang) {
651
724
  this.repository.setLang(lang);
725
+ this.currentLang = lang;
652
726
  if (_rtl_1.rtlLanguages.includes(lang)) {
653
727
  this.setRTLSupport();
654
728
  }
@@ -666,6 +740,201 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
666
740
  setParentPlace(place, updateStyle, onStyleReady) {
667
741
  this.changeParentPlaceTo(place, updateStyle, onStyleReady);
668
742
  }
743
+ setStyle(styleRef, styleTheme, onStyleReady) {
744
+ // Every swap gets a generation. All shared bookkeeping below is touched only while
745
+ // this swap is still the current one, so two overlapping calls cannot release each
746
+ // other's overlay or apply an obsolete style. `readied` stays per-call because each
747
+ // caller's callback must still fire exactly once.
748
+ const generation = ++this.styleSwapGeneration;
749
+ const isCurrent = () => this.styleSwapGeneration === generation;
750
+ let readied = false;
751
+ const reportReady = (error) => {
752
+ if (readied)
753
+ return;
754
+ readied = true;
755
+ // Only the current swap owns the shared timer; a superseded swap clearing it here
756
+ // would leave the live swap with no safety net (and its caller hidden forever).
757
+ if (isCurrent() && this.styleReadyTimer) {
758
+ clearTimeout(this.styleReadyTimer);
759
+ this.styleReadyTimer = undefined;
760
+ }
761
+ onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady(error);
762
+ };
763
+ // `onStyleReady` must fire on EVERY path — success, a rejected style request, or a
764
+ // swap that never settles — because callers keep the map hidden behind a loading
765
+ // overlay until it arrives. The safety timer is therefore armed before the request
766
+ // instead of inside `.then()`.
767
+ if (this.styleReadyTimer)
768
+ clearTimeout(this.styleReadyTimer);
769
+ this.styleReadyTimer = setTimeout(reportReady, STYLE_READY_TIMEOUT_MS);
770
+ // A previous swap may still hold a `styledata` listener whose style never reached the
771
+ // loaded state. Drop it before starting a new one: it captured an obsolete style and
772
+ // would otherwise fire on this swap, wiping `baseFilters` and calling `setFilter`
773
+ // with layer ids the new style does not have.
774
+ this.clearPendingStyleSwapListener();
775
+ // `this.map` is created asynchronously (see `onMapStyleLoaded`), so a `setStyle` call
776
+ // made right after `createMap` can land before it exists. Fail loudly instead of
777
+ // letting `this.map.setStyle` throw into `.catch`, which reported READY and left the
778
+ // caller revealing the previous style as if the swap had succeeded.
779
+ if (!this.map) {
780
+ const error = new Error("setStyle called before the map finished initializing; wait for the map's load event");
781
+ this.logError(error, { functionName: "setStyle" });
782
+ reportReady(error);
783
+ return;
784
+ }
785
+ // When a user bearer token is present, the style is resolved through the
786
+ // bearer-authenticated endpoint, which authorizes by the user's client access
787
+ // rather than the api key's scopes — so a client's branded style can be
788
+ // previewed/applied without granting that place to a shared api key.
789
+ // Api-key consumers keep the `/api/sdk/v2/mapStyleJson` path.
790
+ const stylePromise = this.authToken
791
+ ? this.repository.fetchAndParseMapStyleByBearer(this.parentPlaceId, this.authToken, styleRef, styleTheme)
792
+ : this.repository.fetchAndParseMapStyle(this.parentPlaceId, styleRef, styleTheme);
793
+ stylePromise
794
+ .then((style) => {
795
+ // A newer swap started while this style was in flight. Applying this one now
796
+ // would fight the newer style layer by layer, so drop it and let the newer swap
797
+ // own the map and its caller's callback.
798
+ if (!isCurrent())
799
+ return;
800
+ const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
801
+ const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
802
+ // Full (non-diff) swap so the previewed style completely replaces the base —
803
+ // a diff would keep old base layers and reappear on the next floor change.
804
+ this.map.setStyle(finalStyle, { diff: false });
805
+ // Drop the previous style's base filters SYNCHRONOUSLY, right where the layers
806
+ // they describe stop existing. They are keyed by layer id and persist across
807
+ // swaps, so any code reading them before the apply pass below (the `idle`
808
+ // handler's `filterByFloorKey`, a floor change racing the swap) would AND the
809
+ // previous style's discriminator into the new style's layers — the stale-filter
810
+ // washout this reset exists to prevent. Gating it on `isStyleLoaded` left exactly
811
+ // that window open. Fresh declared filters are re-captured lazily in
812
+ // `applyLayerFilters` as each indoor layer is first touched.
813
+ this.resetBaseFilters();
814
+ // Re-apply floor filters, route/circle layers and markers ONLY once the new
815
+ // style's sources/layers have finished loading; running them mid-reload throws
816
+ // (`getSource` on an undefined style) and the floor filter would not stick.
817
+ const applyOnceLoaded = () => {
818
+ // Superseded: detach and leave the map to the newer swap. Without this the
819
+ // listener stays attached (the slot it was tracked in has been overwritten)
820
+ // and re-seeds filters from layer ids the current style does not have, which
821
+ // makes maplibre throw and aborts the newer swap's own listener.
822
+ if (!isCurrent()) {
823
+ this.map.off("styledata", applyOnceLoaded);
824
+ return;
825
+ }
826
+ if (!this.hasLoadedStyle())
827
+ return;
828
+ this.map.off("styledata", applyOnceLoaded);
829
+ if (this.pendingStyleSwapListener === applyOnceLoaded) {
830
+ this.pendingStyleSwapListener = undefined;
831
+ }
832
+ this.whenStyleUpdates(finalStyle);
833
+ // A non-diff swap replaces every layer, so anything applied ON TOP of the
834
+ // style — label language, caller-requested place highlights, the 3D scene's
835
+ // GLB layer and hidden-layer overrides — is gone and has to be replayed.
836
+ // `whenStyleUpdates` covers only floor filters, routes and circles.
837
+ this.restoreStyleOverlays();
838
+ this.logEvent("setStyle");
839
+ };
840
+ this.pendingStyleSwapListener = applyOnceLoaded;
841
+ this.map.on("styledata", applyOnceLoaded);
842
+ applyOnceLoaded();
843
+ // Re-apply the floor filter and report ready once the map is idle. This is
844
+ // intentionally OUTSIDE applyOnceLoaded's `isStyleLoaded` gate: the indoor
845
+ // polygon layers settle a beat after the swap (so the floor filter in
846
+ // whenStyleUpdates misses them and the venue paints every floor at once), and
847
+ // `idle` only fires once the style is loaded and its first tiles rendered — so
848
+ // filterByFloorKey is safe here and does not depend on the sometimes-flaky
849
+ // isStyleLoaded check. onStyleReady lets callers reveal the map only once it is
850
+ // settled; the timer armed above just clears a hanging overlay if idle never fires.
851
+ this.map.once("idle", () => {
852
+ // `once` cannot be cancelled, so a superseded swap's handler still fires —
853
+ // filtering here keeps it from re-filtering the newer style's floors and from
854
+ // reporting the newer swap's caller ready ahead of time.
855
+ if (!isCurrent())
856
+ return;
857
+ this.filterByFloorKey(this.currentFloor);
858
+ reportReady();
859
+ });
860
+ })
861
+ .catch((error) => {
862
+ // A superseded swap must not tear down the listener the CURRENT swap installed.
863
+ if (isCurrent()) {
864
+ this.clearPendingStyleSwapListener();
865
+ }
866
+ this.logError(error, { functionName: "setStyle" });
867
+ // Release the caller immediately instead of making it wait out the safety timer:
868
+ // the style will never arrive, so the overlay has nothing to wait for. The error
869
+ // is handed to the callback so a caller can tell a failed swap from an applied
870
+ // one — the previous style is still on screen either way.
871
+ reportReady(error instanceof Error ? error : new Error(String(error)));
872
+ });
873
+ }
874
+ /**
875
+ * Forgets every captured base filter so the next style's declared filters are read
876
+ * fresh from that style instead of inherited from the previous one.
877
+ *
878
+ * Both halves must move together: `baseFilters` holds the expressions and
879
+ * `capturedBaseFilters` records which layer ids have already been resolved, so
880
+ * clearing only the first would leave every id marked "already captured" and the new
881
+ * style's filters would never be read at all.
882
+ */
883
+ resetBaseFilters() {
884
+ this.baseFilters = {};
885
+ this.capturedBaseFilters.clear();
886
+ }
887
+ /**
888
+ * Replays everything that lives ON TOP of the map style after a full (non-diff) swap
889
+ * has deleted it: label language expressions, caller-requested place highlights and
890
+ * the 3D scene's GLB layer / hidden-layer overrides.
891
+ *
892
+ * These are deliberately not part of `whenStyleUpdates`, which restores only
893
+ * style-derived state. These are decisions the SDK's callers made through `setLang`,
894
+ * `setPlacesAsSelected`, `addBorderToPlaces` and the 3D config, and nothing else would
895
+ * ever restore them.
896
+ */
897
+ restoreStyleOverlays() {
898
+ // Each step is isolated: the style itself has already been applied by the time we get
899
+ // here, so one failing restore must neither skip the others nor bubble out into
900
+ // `setStyle`'s `.catch`, which would report the successful swap to `onStyleReady` as
901
+ // a failure. `setPlacesAsSelected` / `addBorderToPlaces` throw by contract.
902
+ const restore = (what, step) => {
903
+ try {
904
+ step();
905
+ }
906
+ catch (error) {
907
+ this.logError(error, { functionName: "restoreStyleOverlays", step: what });
908
+ }
909
+ };
910
+ restore("language", () => this.setLayersForLanguage(this.currentLang));
911
+ if (this.coloredPlacesRequest) {
912
+ const { placeIds, color } = this.coloredPlacesRequest;
913
+ restore("coloredPlaces", () => this.setPlacesAsSelected(placeIds, color));
914
+ }
915
+ if (this.borderedPlacesRequest) {
916
+ const { placeIds, color, width } = this.borderedPlacesRequest;
917
+ restore("borderedPlaces", () => this.addBorderToPlaces(placeIds, color, width));
918
+ }
919
+ // The swap removed the GLB custom layer, and MapLibre called its `onRemove`, which
920
+ // disposes the whole three.js scene. Nothing else re-adds it, so without this every
921
+ // 3D model disappears permanently and `hideLayers` stops being applied.
922
+ restore("scene3D", () => { var _a; return (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.reattachToStyle(); });
923
+ }
924
+ /**
925
+ * Removes the `styledata` listener left by an in-flight {@link setStyle} swap, if any.
926
+ * Called before starting a new swap and when one fails, so a listener holding an
927
+ * obsolete style can never fire against a different style's layers.
928
+ */
929
+ clearPendingStyleSwapListener() {
930
+ var _a;
931
+ if (!this.pendingStyleSwapListener)
932
+ return;
933
+ // `this.map?` — a listener can only have been installed while the map existed, but
934
+ // this also runs from `destroyMap` and from `setStyle` before the map is created.
935
+ (_a = this.map) === null || _a === void 0 ? void 0 : _a.off("styledata", this.pendingStyleSwapListener);
936
+ this.pendingStyleSwapListener = undefined;
937
+ }
669
938
  /**
670
939
  * Removes current parent place and updates style to default
671
940
  */
@@ -1182,6 +1451,15 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1182
1451
  });
1183
1452
  const baseFloorId = (_c = this.innerFloors.find((floor) => floor.key === floorId)) === null || _c === void 0 ? void 0 : _c.baseFloor;
1184
1453
  const indoorLayers = layers.filter((layer) => layer.id.startsWith("indoor-"));
1454
+ // Resolve each indoor layer's declared (base) filter before we start folding floor
1455
+ // filters into it — and before cloning it into a `base-indoor-*` layer, which copies
1456
+ // the base filter across. Layers added asynchronously after a `setStyle` swap are
1457
+ // missing when `setBaseFilters` runs, so this is where their still-pristine filter
1458
+ // is captured; without it the layer loses its own discrimination and repaints every
1459
+ // feature (the washed-out / blue-outline look after changing floors). Layers already
1460
+ // resolved are skipped, so a filter this class wrote is never mistaken for a
1461
+ // declared one.
1462
+ indoorLayers.forEach((layer) => this.captureDeclaredBaseFilter(layer.id));
1185
1463
  if (baseFloorId != undefined) {
1186
1464
  const firstLayerId = (_d = indoorLayers[0]) === null || _d === void 0 ? void 0 : _d.id;
1187
1465
  indoorLayers
@@ -1198,8 +1476,33 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1198
1476
  this.applyLayerFilters(layer, floorId);
1199
1477
  });
1200
1478
  }
1479
+ /**
1480
+ * Reads a layer's current filter, tolerating the layer having been removed or the
1481
+ * style being mid-swap (maplibre `getFilter` throws in those cases). Returns
1482
+ * `undefined` instead of throwing so base-filter capture never aborts floor filtering.
1483
+ */
1484
+ safeGetFilter(layerId) {
1485
+ try {
1486
+ return this.map.getFilter(layerId);
1487
+ }
1488
+ catch (_a) {
1489
+ return undefined;
1490
+ }
1491
+ }
1201
1492
  applyLayerFilters(layer, floorId) {
1202
1493
  var _a;
1494
+ // Resolve the layer's declared (base) filter the first time we touch it. Indoor
1495
+ // layers such as `indoor-polygon-room` share one source and are told apart only by
1496
+ // their own filter (e.g. `["in","room",["get","tags"]]`); we must fold that into the
1497
+ // floor filter or the layer repaints every feature. Some of these layers are added
1498
+ // asynchronously after a `setStyle` swap, so they are absent when `setBaseFilters`
1499
+ // runs — capturing here, while their live filter is still pristine, is what keeps a
1500
+ // floor change from wiping the style (the washed-out, blue-outline look).
1501
+ // `base-indoor-*` clones inherit their base filter from the layer they were cloned
1502
+ // from, so they are never read back.
1503
+ if (!layer.id.startsWith("base-indoor-")) {
1504
+ this.captureDeclaredBaseFilter(layer.id);
1505
+ }
1203
1506
  let baseFilter = this.baseFilters[layer.id];
1204
1507
  const floorIdString = floorId !== null && floorId !== void 0 ? floorId : "";
1205
1508
  const floorFilter = [
@@ -1232,6 +1535,10 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1232
1535
  (layer.type == "fill" || layer.type === "fill-extrusion")) {
1233
1536
  customFilters.push(["!", ["in", this.hoveredId, ["get", "ref"]]]);
1234
1537
  }
1538
+ // From here on this layer's live filter is ours, never the style's declared one.
1539
+ // Marking it keeps any later pass from reading it back as a base filter — that is
1540
+ // what used to AND the previous floor into the next one and blank the layer out.
1541
+ this.capturedBaseFilters.add(layer.id);
1235
1542
  if (baseFilter != null) {
1236
1543
  const finalFilter = ["all", baseFilter, customFilters];
1237
1544
  this.map.setFilter(layer.id, finalFilter);
@@ -1658,6 +1965,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1658
1965
  }
1659
1966
  setPlacesAsSelected(placeIds, color) {
1660
1967
  try {
1968
+ this.coloredPlacesRequest = { placeIds: [...placeIds], color };
1661
1969
  if (!this.hasLayer("indoor-colored-places")) {
1662
1970
  this.createColoredPlacesLayer();
1663
1971
  }
@@ -1669,6 +1977,9 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1669
1977
  const placeFilter = ["any", ...coloredPlacesFilters];
1670
1978
  const condition = ["all", placeFilter, basicFilter];
1671
1979
  this.map.setFilter("indoor-colored-places", condition);
1980
+ // Caller-defined base filter: mark it resolved so no later capture can
1981
+ // overwrite this customization with the layer's style-declared filter.
1982
+ this.capturedBaseFilters.add("indoor-colored-places");
1672
1983
  this.baseFilters["indoor-colored-places"] = placeFilter;
1673
1984
  if (color !== undefined) {
1674
1985
  const layer = this.map.getLayer("indoor-colored-places");
@@ -1752,8 +2063,12 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1752
2063
  return layer !== undefined;
1753
2064
  }
1754
2065
  clearColoredPlaces() {
2066
+ this.coloredPlacesRequest = undefined;
1755
2067
  if (this.hasLayer("indoor-colored-places")) {
1756
2068
  const condition = ["==", ["get", "ref"], "unselected"];
2069
+ // Caller-defined base filter: mark it resolved so no later capture can
2070
+ // overwrite this customization with the layer's style-declared filter.
2071
+ this.capturedBaseFilters.add("indoor-colored-places");
1757
2072
  this.baseFilters["indoor-colored-places"] = condition;
1758
2073
  this.map.setFilter("indoor-colored-places", condition);
1759
2074
  }
@@ -1948,6 +2263,7 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1948
2263
  }
1949
2264
  addBorderToPlaces(placeIds, color, width) {
1950
2265
  try {
2266
+ this.borderedPlacesRequest = { placeIds: [...placeIds], color, width };
1951
2267
  if (!this.hasLayer("indoor-bordered-places")) {
1952
2268
  this.createBorderedPlacesLayer();
1953
2269
  }
@@ -1959,6 +2275,9 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1959
2275
  const placeFilter = ["any", ...coloredPlacesFilters];
1960
2276
  const condition = ["all", placeFilter, basicFilter];
1961
2277
  this.map.setFilter("indoor-bordered-places", condition);
2278
+ // Caller-defined base filter: mark it resolved so no later capture can
2279
+ // overwrite this customization with the layer's style-declared filter.
2280
+ this.capturedBaseFilters.add("indoor-bordered-places");
1962
2281
  this.baseFilters["indoor-bordered-places"] = placeFilter;
1963
2282
  if (color !== undefined && width !== undefined) {
1964
2283
  this.map.setPaintProperty("indoor-bordered-places", "line-color", color);
@@ -1984,8 +2303,12 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
1984
2303
  this.map.addLayer(layer);
1985
2304
  }
1986
2305
  clearBorderedPlaces() {
2306
+ this.borderedPlacesRequest = undefined;
1987
2307
  if (this.hasLayer("indoor-bordered-places")) {
1988
2308
  const condition = ["==", ["get", "ref"], "unselected"];
2309
+ // Caller-defined base filter: mark it resolved so no later capture can
2310
+ // overwrite this customization with the layer's style-declared filter.
2311
+ this.capturedBaseFilters.add("indoor-bordered-places");
1989
2312
  this.baseFilters["indoor-bordered-places"] = condition;
1990
2313
  this.map.setFilter("indoor-bordered-places", condition);
1991
2314
  }
@@ -2030,6 +2353,38 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
2030
2353
  this.failedTiles.delete(e.tile);
2031
2354
  }
2032
2355
  }
2356
+ /**
2357
+ * True when a style object is attached to the map. During a full `setStyle` swap the
2358
+ * map is momentarily style-less and any `getSource`/`getFilter` call throws
2359
+ * ("Cannot read properties of undefined"), so call sites that only need the style to
2360
+ * exist should gate on this rather than on `isStyleLoaded()`.
2361
+ */
2362
+ hasStyle() {
2363
+ var _a;
2364
+ // `map.getStyle()` is the PUBLIC probe: it returns the serialized style, or throws
2365
+ // while the map is momentarily style-less mid-swap. Reading maplibre's internal
2366
+ // `style` field instead (it is absent from the public typings, so it needed a double
2367
+ // cast) would fail closed and silently forever the moment a maplibre upgrade renames
2368
+ // or privatizes it — disabling both the tile-retry loop and `setStyle`'s apply pass
2369
+ // with no throw and no log. `safeGetFilter` already uses this try/catch shape.
2370
+ try {
2371
+ return ((_a = this.map) === null || _a === void 0 ? void 0 : _a.getStyle()) != null;
2372
+ }
2373
+ catch (_b) {
2374
+ return false;
2375
+ }
2376
+ }
2377
+ /**
2378
+ * True when the map has a style object attached and fully loaded. Unlike
2379
+ * `map.isStyleLoaded()`, this does not emit maplibre's "There is no style
2380
+ * added to the map" warning when the style is momentarily absent (e.g. during
2381
+ * a `setStyle` swap), because it checks for the style object first.
2382
+ */
2383
+ hasLoadedStyle() {
2384
+ if (!this.hasStyle())
2385
+ return false;
2386
+ return this.map.isStyleLoaded() === true;
2387
+ }
2033
2388
  subscribeToFailedTiles() {
2034
2389
  if (this.retryTimer)
2035
2390
  return;
@@ -2039,6 +2394,17 @@ class InternalMapVXMap extends loggeable_1.Loggeable {
2039
2394
  this.boundOnWebGLContextLost = this.onWebGLContextLost.bind(this);
2040
2395
  this.boundOnWebGLContextRestored = this.onWebGLContextRestored.bind(this);
2041
2396
  this.retryTimer = setInterval(() => {
2397
+ // During a full style swap (`setStyle` with `diff: false`) the map can
2398
+ // momentarily have no style object attached. Calling `getSource` then
2399
+ // throws ("Cannot read properties of undefined (reading 'getSource')").
2400
+ // Only the style's PRESENCE is checked here, never `isStyleLoaded()`:
2401
+ // that is false while any source still has tiles in flight, which is
2402
+ // exactly when tiles fail — gating on it would skip every tick during a
2403
+ // bad-network burst. The queue is left untouched so the tiles queued
2404
+ // mid-swap are still retried on the next tick.
2405
+ if (!this.hasStyle()) {
2406
+ return;
2407
+ }
2042
2408
  if (!this.map.getSource("indoorequal")) {
2043
2409
  this.failedTiles.clear();
2044
2410
  return;