@mapvx/web-js 3.5.0-dev.3 → 3.5.0-dev.5

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 (35) hide show
  1. package/dist/cjs/controllers/routeController.js +396 -106
  2. package/dist/cjs/controllers/routeController.js.map +1 -1
  3. package/dist/cjs/domain/models/routeInstance.js +1 -0
  4. package/dist/cjs/domain/models/routeInstance.js.map +1 -1
  5. package/dist/cjs/logger/sentry.js +1 -1
  6. package/dist/cjs/map/map.js +250 -24
  7. package/dist/cjs/map/map.js.map +1 -1
  8. package/dist/cjs/map/mapInteractionOptions.js +18 -0
  9. package/dist/cjs/map/mapInteractionOptions.js.map +1 -1
  10. package/dist/es/controllers/routeController.d.ts +91 -13
  11. package/dist/es/controllers/routeController.d.ts.map +1 -1
  12. package/dist/es/controllers/routeController.js +396 -106
  13. package/dist/es/controllers/routeController.js.map +1 -1
  14. package/dist/es/domain/models/mapConfig.d.ts +76 -0
  15. package/dist/es/domain/models/mapConfig.d.ts.map +1 -1
  16. package/dist/es/domain/models/routeInstance.d.ts +70 -1
  17. package/dist/es/domain/models/routeInstance.d.ts.map +1 -1
  18. package/dist/es/domain/models/routeInstance.js +1 -0
  19. package/dist/es/domain/models/routeInstance.js.map +1 -1
  20. package/dist/es/logger/sentry.js +1 -1
  21. package/dist/es/map/map.d.ts +76 -0
  22. package/dist/es/map/map.d.ts.map +1 -1
  23. package/dist/es/map/map.js +250 -24
  24. package/dist/es/map/map.js.map +1 -1
  25. package/dist/es/map/mapInteractionOptions.d.ts +5 -1
  26. package/dist/es/map/mapInteractionOptions.d.ts.map +1 -1
  27. package/dist/es/map/mapInteractionOptions.js +18 -0
  28. package/dist/es/map/mapInteractionOptions.js.map +1 -1
  29. package/dist/umd/index.js +1070 -502
  30. package/dist/umd/index.js.map +1 -1
  31. package/dist/umd/styles.css +1 -1
  32. package/dist/umd/styles.css.map +1 -1
  33. package/dist/umd/styles.js +5 -9
  34. package/dist/umd/styles.js.map +1 -1
  35. package/package.json +27 -27
@@ -34,6 +34,22 @@ const TILE_RETRY_BASE_DELAY_MS = 1000;
34
34
  * overlay until the callback arrives, so it must be released no matter what.
35
35
  */
36
36
  const STYLE_READY_TIMEOUT_MS = 4000;
37
+ /**
38
+ * Frames a floor change waits before it will believe the indoor tiles are
39
+ * already rebuilt (see `updateMarkersWhenFloorSettles`). Genuinely a frame
40
+ * count: the reload is dispatched on the map's next render, so this is how
41
+ * many renders it gets to be observed in.
42
+ */
43
+ const FLOOR_SETTLE_MIN_FRAMES = 3;
44
+ /**
45
+ * Longest a floor change waits for the new floor's tiles before switching the
46
+ * HTML markers anyway, so a stalled source never hides markers for good.
47
+ *
48
+ * A deadline rather than a frame count. The panels this matters on run at
49
+ * 20-30 fps, where a 60-frame cap is two to three seconds — and the wait also
50
+ * gates `addMarker`, so overshooting it is not merely a late marker switch.
51
+ */
52
+ const FLOOR_SETTLE_MAX_MS = 1000;
37
53
  /** Fill/line layer ids for one circle render-order bucket. */
38
54
  function circleLayerIdsFor(order) {
39
55
  const suffix = CIRCLE_LAYER_SUFFIXES[order];
@@ -169,6 +185,14 @@ import { buildInteractionOptions, shouldDisableTouchRotation } from "./mapIntera
169
185
  * @hidden
170
186
  */
171
187
  export class InternalMapVXMap extends Loggeable {
188
+ /** Whether a floor change is still rebuilding, so markers wait for it. */
189
+ get floorSettlePending() {
190
+ return this.floorSettleDeadline != null && this.now() < this.floorSettleDeadline;
191
+ }
192
+ /** Wall clock for the settle deadline; `Date.now` where `performance` is absent. */
193
+ now() {
194
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
195
+ }
172
196
  /**
173
197
  * Creates a new map instance with the parameters specified in mapConfig and assigns it to the provided HTML container.
174
198
  * @param mapConfig Configuration parameters for the map.
@@ -232,6 +256,20 @@ export class InternalMapVXMap extends Loggeable {
232
256
  */
233
257
  this.tileRetryState = new WeakMap();
234
258
  this.geoLocation = navigator.geolocation;
259
+ /** Floor-change settle bookkeeping (see `updateMarkersWhenFloorSettles`). */
260
+ this.floorSettleToken = 0;
261
+ /**
262
+ * When the floor-change settle stops holding markers back, or `null` when no
263
+ * change is in flight.
264
+ *
265
+ * A deadline rather than a flag, because `addMarker` reads it: the poll that
266
+ * would clear a flag runs on `requestAnimationFrame`, which does not fire in
267
+ * a backgrounded tab and does not run at all for a map torn down through
268
+ * MapLibre's own `remove()` rather than `destroyMap`. A flag left set there
269
+ * makes every later `addMarker` skip the call that attaches the marker, for
270
+ * good; a deadline expires on its own.
271
+ */
272
+ this.floorSettleDeadline = null;
235
273
  /** Owns the deck.gl overlay + GLB scene; created on style load only when 3D is active. */
236
274
  this.scene3D = undefined;
237
275
  if (rtlLanguages.includes((_a = mapConfig.lang) !== null && _a !== void 0 ? _a : "")) {
@@ -253,6 +291,8 @@ export class InternalMapVXMap extends Loggeable {
253
291
  this.onFloorChange = mapConfig.onFloorChange;
254
292
  this.onParentPlaceChange = mapConfig.onParentPlaceChange;
255
293
  this.mode = (_d = mapConfig.mode) !== null && _d !== void 0 ? _d : "2D";
294
+ this.styleRef = mapConfig.styleRef;
295
+ this.styleTheme = mapConfig.styleTheme;
256
296
  this.scene3DConfig = mapConfig.scene3D;
257
297
  this.initialCenter = mapConfig.center;
258
298
  this.tileCacheConfig = (() => {
@@ -354,12 +394,18 @@ export class InternalMapVXMap extends Loggeable {
354
394
  .catch(console.error);
355
395
  return;
356
396
  }
357
- this.repository
358
- .fetchAndParseMapStyle(mapConfig.parentPlaceId)
397
+ // Through the same helper as `setStyle` and `changeParentPlaceTo`. This PR
398
+ // forwards `styleRef`/`styleTheme` to the initial load too, and a ref scoped
399
+ // to the user's client rather than to the api key resolves only over bearer
400
+ // — fetched over the api-key path it 4xx'd, and since this is the call that
401
+ // creates the map, the map was never created at all.
402
+ this.fetchVenueStyle(mapConfig.parentPlaceId, mapConfig.styleRef, mapConfig.styleTheme)
359
403
  .then((style) => {
360
404
  return this.onMapStyleLoaded(mapConfig, container, style);
361
405
  })
362
- .catch(console.error);
406
+ .catch((error) => {
407
+ this.logError(error, { functionName: "initialMapStyleSetUp" });
408
+ });
363
409
  }
364
410
  onMapStyleLoaded(mapConfig, container, style) {
365
411
  var _a, _b, _c, _d, _e, _f, _g;
@@ -377,6 +423,13 @@ export class InternalMapVXMap extends Loggeable {
377
423
  mapOptions.maxZoom = mapConfig.maxZoom;
378
424
  if (mapConfig.minZoom)
379
425
  mapOptions.minZoom = mapConfig.minZoom;
426
+ // Reduced internal resolution on request (fill-rate relief for low-end GPUs);
427
+ // MapLibre keeps devicePixelRatio when the option is left out.
428
+ if (mapConfig.pixelRatio !== undefined)
429
+ mapOptions.pixelRatio = mapConfig.pixelRatio;
430
+ // `0` disables the post-gesture fade tail entirely; keep MapLibre's default otherwise.
431
+ if (mapConfig.fadeDuration !== undefined)
432
+ mapOptions.fadeDuration = mapConfig.fadeDuration;
380
433
  if (mapConfig.maxBounds && mapConfig.maxBounds.length > 1) {
381
434
  const boundingBox = getBoundingBox(mapConfig.maxBounds);
382
435
  mapOptions.maxBounds = new LngLatBounds([boundingBox[0].lng, boundingBox[0].lat], [boundingBox[1].lng, boundingBox[1].lat]);
@@ -566,6 +619,11 @@ export class InternalMapVXMap extends Loggeable {
566
619
  clearTimeout(this.styleReadyTimer);
567
620
  this.styleReadyTimer = undefined;
568
621
  }
622
+ // Drop any floor-settle poll in flight: bumping the token makes the next
623
+ // `requestAnimationFrame` callback return instead of re-queueing itself and
624
+ // re-attaching markers to a map that has already been removed.
625
+ this.floorSettleToken++;
626
+ this.floorSettleDeadline = null;
569
627
  // Dispose the 3D scene's GPU resources before the map (and its shared WebGL context) go away —
570
628
  // MapLibre's own teardown never calls the custom GLB layer's `onRemove`.
571
629
  (_a = this.scene3D) === null || _a === void 0 ? void 0 : _a.dispose();
@@ -634,8 +692,7 @@ export class InternalMapVXMap extends Loggeable {
634
692
  }
635
693
  (_g = this.scene3D) === null || _g === void 0 ? void 0 : _g.onFloorChange(this.currentFloor);
636
694
  if (updateStyle) {
637
- this.repository
638
- .fetchAndParseMapStyle(place === null || place === void 0 ? void 0 : place.mapvxId)
695
+ this.fetchVenueStyle(place === null || place === void 0 ? void 0 : place.mapvxId, this.styleRef, this.styleTheme)
639
696
  .then((style) => {
640
697
  const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
641
698
  const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
@@ -650,9 +707,35 @@ export class InternalMapVXMap extends Loggeable {
650
707
  this.whenStyleUpdates(finalStyle);
651
708
  onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady();
652
709
  })
653
- .catch(console.error);
710
+ .catch((error) => {
711
+ this.logError(error, { functionName: "changeParentPlaceTo" });
712
+ // Release the caller either way. `onStyleReady` is what a consumer
713
+ // hides its loading overlay on, so swallowing the failure here left
714
+ // the overlay up for good rather than merely unstyled.
715
+ onStyleReady === null || onStyleReady === void 0 ? void 0 : onStyleReady();
716
+ });
654
717
  }
655
718
  }
719
+ /**
720
+ * Fetch the venue style over whichever endpoint this map is authorized on.
721
+ *
722
+ * A bearer token authorizes by the user's client access rather than the api
723
+ * key's scopes, so a client's branded style resolves without granting that
724
+ * place to a shared api key; api-key consumers keep the
725
+ * `/api/sdk/v2/mapStyleJson` path.
726
+ *
727
+ * Both callers go through here so they cannot disagree. They used to: once
728
+ * `setStyle` had stored a `styleRef` it resolved over bearer, the next
729
+ * parent-place change refetched that same ref over the api-key path, whose
730
+ * key does not scope it — a non-ok response, a rejected promise, and a style
731
+ * that was never applied, with `onStyleReady` left unfired under a caller's
732
+ * loading overlay.
733
+ */
734
+ fetchVenueStyle(placeId, styleRef, styleTheme) {
735
+ return this.authToken
736
+ ? this.repository.fetchAndParseMapStyleByBearer(placeId, this.authToken, styleRef, styleTheme)
737
+ : this.repository.fetchAndParseMapStyle(placeId, styleRef, styleTheme);
738
+ }
656
739
  /**
657
740
  * Update the parent place and the current floor.
658
741
  * Does not update layers, markers, etc.
@@ -746,14 +829,7 @@ export class InternalMapVXMap extends Loggeable {
746
829
  reportReady(error);
747
830
  return;
748
831
  }
749
- // When a user bearer token is present, the style is resolved through the
750
- // bearer-authenticated endpoint, which authorizes by the user's client access
751
- // rather than the api key's scopes — so a client's branded style can be
752
- // previewed/applied without granting that place to a shared api key.
753
- // Api-key consumers keep the `/api/sdk/v2/mapStyleJson` path.
754
- const stylePromise = this.authToken
755
- ? this.repository.fetchAndParseMapStyleByBearer(this.parentPlaceId, this.authToken, styleRef, styleTheme)
756
- : this.repository.fetchAndParseMapStyle(this.parentPlaceId, styleRef, styleTheme);
832
+ const stylePromise = this.fetchVenueStyle(this.parentPlaceId, styleRef, styleTheme);
757
833
  stylePromise
758
834
  .then((style) => {
759
835
  // A newer swap started while this style was in flight. Applying this one now
@@ -761,6 +837,13 @@ export class InternalMapVXMap extends Loggeable {
761
837
  // own the map and its caller's callback.
762
838
  if (!isCurrent())
763
839
  return;
840
+ // Remember what is actually on screen: `changeParentPlaceTo` refetches the
841
+ // style with these, so without this an explicitly applied style would be
842
+ // silently reverted to the constructor's on the next parent-place change.
843
+ // After the generation check: a superseded swap never reached the map, so it
844
+ // must not claim to be what is on it.
845
+ this.styleRef = styleRef;
846
+ this.styleTheme = styleTheme;
764
847
  const useServiceWorkerCaching = this.tileCacheConfig.enabled && this.tileCacheConfig.persistToServiceWorker;
765
848
  const finalStyle = useServiceWorkerCaching ? this.transformStyleForCaching(style) : style;
766
849
  // Full (non-diff) swap so the previewed style completely replaces the base —
@@ -954,7 +1037,11 @@ export class InternalMapVXMap extends Loggeable {
954
1037
  }
955
1038
  }
956
1039
  const markerCreated = new MarkerAttribute(Object.assign(Object.assign({}, marker), { maxLabelWidthPx: (_a = marker.maxLabelWidthPx) !== null && _a !== void 0 ? _a : this.markerLayoutConfig.maxLabelWidthPx }));
957
- markerCreated.changeFloor(this.currentFloor, this.map, this.isOutdoorFloor(this.currentFloor));
1040
+ // While a floor change is still being rebuilt, the marker is shown by that
1041
+ // change's settle step, together with the new floor.
1042
+ if (!this.floorSettlePending) {
1043
+ markerCreated.changeFloor(this.currentFloor, this.map, this.isOutdoorFloor(this.currentFloor));
1044
+ }
958
1045
  this.markers.push(markerCreated);
959
1046
  this.markerLayoutController.scheduleRelayout();
960
1047
  this.logEvent("addMarker");
@@ -1371,12 +1458,110 @@ export class InternalMapVXMap extends Loggeable {
1371
1458
  if (this.innerFloors.every((floor) => floor.key !== floorKey) && floorKey !== "") {
1372
1459
  this.logEvent("invalidFloorKey", { floorKey: floorKeyString });
1373
1460
  }
1374
- this.updateFiltersTo(floorKeyString);
1375
- this.updateMarkersTo(floorKeyString);
1461
+ // One style read for the whole floor change. `getStyle()` serializes every
1462
+ // source and layer of the venue style, and the second read is the expensive
1463
+ // one: `updateFiltersTo` writes filters and layout properties in between,
1464
+ // and each write nulls maplibre's `_serializedLayers` cache, so anything
1465
+ // that reads the style afterwards rebuilds it from scratch.
1466
+ const layers = this.safeStyleLayers();
1467
+ this.updateFiltersTo(floorKeyString, layers);
1468
+ this.updateMarkersWhenFloorSettles(floorKeyString, layers);
1376
1469
  this.refreshCircles();
1377
1470
  this.routeController.updateRouteLayers(floorKeyString);
1378
1471
  this.routeController.updateRouteMarkerVisibility(floorKeyString);
1379
1472
  }
1473
+ /**
1474
+ * Show/hide the HTML markers for `floorId` once the indoor tiles have been
1475
+ * rebuilt with the new floor filters.
1476
+ *
1477
+ * A floor change is a `setFilter` on every indoor layer: MapLibre rebuilds the
1478
+ * tile buckets in its worker, so the new floor appears a few frames later
1479
+ * (hundreds of ms on a low-end panel) while a marker is a DOM element that
1480
+ * would move instantly. Waiting for the source to report loaded makes floor
1481
+ * polygons and markers switch in the same frame. The wait is bounded, is
1482
+ * dropped when a newer floor change arrives, and is skipped outside a
1483
+ * browser (no `requestAnimationFrame`).
1484
+ */
1485
+ updateMarkersWhenFloorSettles(floorId, styleLayers) {
1486
+ const token = ++this.floorSettleToken;
1487
+ // Resolved rather than probed with `hasStyle()`, which is another full
1488
+ // `getStyle()` — and by this point the most expensive kind, because
1489
+ // `updateFiltersTo` has just invalidated the serialized-layer cache.
1490
+ const layers = styleLayers === undefined ? this.safeStyleLayers() : styleLayers;
1491
+ if (typeof requestAnimationFrame !== "function" || layers === null) {
1492
+ // No poll will run for this floor change, so nothing would ever clear the
1493
+ // flag later: leaving it set makes every subsequent `addMarker` skip
1494
+ // `changeFloor` and the marker is never attached to the map.
1495
+ this.floorSettleDeadline = null;
1496
+ this.updateMarkersTo(floorId);
1497
+ return;
1498
+ }
1499
+ const sourceIds = this.floorSettleSourceIds(floorId, layers);
1500
+ const deadline = this.now() + FLOOR_SETTLE_MAX_MS;
1501
+ this.floorSettleDeadline = deadline;
1502
+ let sawReloading = false;
1503
+ let frames = 0;
1504
+ const poll = () => {
1505
+ if (token !== this.floorSettleToken)
1506
+ return;
1507
+ frames++;
1508
+ const loaded = sourceIds.every((id) => this.isSourceSettled(id));
1509
+ if (!loaded)
1510
+ sawReloading = true;
1511
+ // The reload is dispatched on the map's next render, which may run after
1512
+ // this callback within the same frame: give it a couple of frames to be
1513
+ // observed before concluding there was nothing to reload.
1514
+ const settled = loaded && (sawReloading || frames >= FLOOR_SETTLE_MIN_FRAMES);
1515
+ if (settled || this.now() >= deadline) {
1516
+ this.floorSettleDeadline = null;
1517
+ this.updateMarkersTo(floorId);
1518
+ return;
1519
+ }
1520
+ requestAnimationFrame(poll);
1521
+ };
1522
+ requestAnimationFrame(poll);
1523
+ }
1524
+ /**
1525
+ * Sources whose tiles a switch to `floorId` actually reloads, and which the
1526
+ * marker settle therefore has to wait on.
1527
+ *
1528
+ * With a vector basemap that is the source the `indoor-*` layers draw from,
1529
+ * because the floor change is a `setFilter` over those layers. An offline
1530
+ * bundle's raster basemap switches floors by making `raster-floor-<key>`
1531
+ * visible instead, and those tiles only begin loading at that moment — while
1532
+ * its `indoor-*` layers carry labels and hover, are already loaded, and would
1533
+ * report settled at once, letting the markers switch a floor before its image
1534
+ * had painted. That is the configuration this wait exists for, so both are
1535
+ * waited on.
1536
+ */
1537
+ floorSettleSourceIds(floorId, layers) {
1538
+ const ids = new Set();
1539
+ const rasterFloor = layers.find((layer) => layer.id === `raster-floor-${floorId}`);
1540
+ if (rasterFloor && "source" in rasterFloor && typeof rasterFloor.source === "string") {
1541
+ ids.add(rasterFloor.source);
1542
+ }
1543
+ ids.add(this.indoorSourceId(layers));
1544
+ return [...ids];
1545
+ }
1546
+ /** The vector source the `indoor-*` layers draw from (`indoorequal` by default). */
1547
+ indoorSourceId(layers) {
1548
+ for (const layer of layers) {
1549
+ if (layer.id.startsWith("indoor-") && "source" in layer && typeof layer.source === "string") {
1550
+ return layer.source;
1551
+ }
1552
+ }
1553
+ return "indoorequal";
1554
+ }
1555
+ isSourceSettled(sourceId) {
1556
+ try {
1557
+ if (!this.map.getSource(sourceId))
1558
+ return true;
1559
+ return this.map.isSourceLoaded(sourceId) !== false;
1560
+ }
1561
+ catch (_a) {
1562
+ return true;
1563
+ }
1564
+ }
1380
1565
  getContainer() {
1381
1566
  return this.map.getContainer();
1382
1567
  }
@@ -1398,17 +1583,40 @@ export class InternalMapVXMap extends Loggeable {
1398
1583
  });
1399
1584
  this.markerLayoutController.scheduleRelayout();
1400
1585
  }
1401
- updateFiltersTo(floorId) {
1402
- var _a, _b, _c, _d;
1586
+ updateFiltersTo(floorId, styleLayers) {
1587
+ var _a, _b, _c;
1403
1588
  if (!this.map)
1404
1589
  return;
1405
- const layers = (_b = (_a = this.map.getStyle()) === null || _a === void 0 ? void 0 : _a.layers) !== null && _b !== void 0 ? _b : [];
1590
+ // `null` means the caller looked and there was no style; only `undefined`
1591
+ // means it did not look.
1592
+ const layers = (_a = (styleLayers === undefined ? this.safeStyleLayers() : styleLayers)) !== null && _a !== void 0 ? _a : [];
1593
+ // Raster basemap (offline bundles baked with one raster archive per floor):
1594
+ // a floor is switched by showing its `raster-floor-<key>` layer and hiding
1595
+ // the others. The remaining `indoor-*` layers (labels, hover, highlights)
1596
+ // are still vector and keep going through the filter logic below.
1597
+ const rasterFloorPrefix = "raster-floor-";
1598
+ // Only switch the raster basemap when there is a baked layer to switch to.
1599
+ // Hiding every raster floor leaves the basemap blank, and no id can match in
1600
+ // two cases: no floor at all (an outdoor place, or a venue with no floors,
1601
+ // where `currentFloor` is `""`), and a floor the bundle has no raster
1602
+ // archive for — a venue where only some floors are baked, or a floor added
1603
+ // after the bundle was built. Keeping the current one visible is the safe
1604
+ // fallback for both.
1605
+ const rasterFloorLayerIds = layers
1606
+ .filter((l) => l.id.startsWith(rasterFloorPrefix))
1607
+ .map((l) => l.id);
1608
+ const targetRasterFloor = rasterFloorPrefix + floorId;
1609
+ if (floorId && rasterFloorLayerIds.includes(targetRasterFloor)) {
1610
+ rasterFloorLayerIds.forEach((id) => {
1611
+ this.map.setLayoutProperty(id, "visibility", id === targetRasterFloor ? "visible" : "none");
1612
+ });
1613
+ }
1406
1614
  layers
1407
1615
  .filter((l) => l.id.startsWith("base-indoor-"))
1408
1616
  .forEach((l) => {
1409
1617
  this.map.removeLayer(l.id);
1410
1618
  });
1411
- const baseFloorId = (_c = this.innerFloors.find((floor) => floor.key === floorId)) === null || _c === void 0 ? void 0 : _c.baseFloor;
1619
+ const baseFloorId = (_b = this.innerFloors.find((floor) => floor.key === floorId)) === null || _b === void 0 ? void 0 : _b.baseFloor;
1412
1620
  const indoorLayers = layers.filter((layer) => layer.id.startsWith("indoor-"));
1413
1621
  // Resolve each indoor layer's declared (base) filter before we start folding floor
1414
1622
  // filters into it — and before cloning it into a `base-indoor-*` layer, which copies
@@ -1420,7 +1628,7 @@ export class InternalMapVXMap extends Loggeable {
1420
1628
  // declared one.
1421
1629
  indoorLayers.forEach((layer) => this.captureDeclaredBaseFilter(layer.id));
1422
1630
  if (baseFloorId != undefined) {
1423
- const firstLayerId = (_d = indoorLayers[0]) === null || _d === void 0 ? void 0 : _d.id;
1631
+ const firstLayerId = (_c = indoorLayers[0]) === null || _c === void 0 ? void 0 : _c.id;
1424
1632
  indoorLayers
1425
1633
  .filter((layer) => layer.type !== "fill-extrusion")
1426
1634
  .forEach((layer) => {
@@ -1727,8 +1935,9 @@ export class InternalMapVXMap extends Loggeable {
1727
1935
  var _a;
1728
1936
  try {
1729
1937
  this.updatePlaceAndFloor(placeId, floorId);
1730
- this.updateFiltersTo(floorId);
1731
- this.updateMarkersTo(floorId);
1938
+ const layers = this.safeStyleLayers();
1939
+ this.updateFiltersTo(floorId, layers);
1940
+ this.updateMarkersWhenFloorSettles(floorId, layers);
1732
1941
  (_a = this.onFloorChange) === null || _a === void 0 ? void 0 : _a.call(this, floorId);
1733
1942
  this.logEvent("updatePlaceAndFloorForRouting", { floorId });
1734
1943
  }
@@ -2331,6 +2540,23 @@ export class InternalMapVXMap extends Loggeable {
2331
2540
  * ("Cannot read properties of undefined"), so call sites that only need the style to
2332
2541
  * exist should gate on this rather than on `isStyleLoaded()`.
2333
2542
  */
2543
+ /**
2544
+ * This style's layers, or `null` when the map has no style right now.
2545
+ *
2546
+ * The one place the style is read for a floor change, so callers can pass the
2547
+ * result on instead of each probing again — `getStyle()` serializes every
2548
+ * source and layer, and `Style._updateLayer` drops the cache behind it on
2549
+ * every filter or layout write.
2550
+ */
2551
+ safeStyleLayers() {
2552
+ var _a, _b, _c;
2553
+ try {
2554
+ return (_c = (_b = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getStyle()) === null || _b === void 0 ? void 0 : _b.layers) !== null && _c !== void 0 ? _c : null;
2555
+ }
2556
+ catch (_d) {
2557
+ return null;
2558
+ }
2559
+ }
2334
2560
  hasStyle() {
2335
2561
  var _a;
2336
2562
  // `map.getStyle()` is the PUBLIC probe: it returns the serialized style, or throws