Skip to content

Commit 485e80a

Browse files
authored
feat(map): exclude plugin-internal style layers from the layer control (#1012)
* feat(map): exclude plugin-internal style layers from the layer control Plugins can add native style layers as internal chrome (selection footprints, draw/highlight helpers) that should not appear as individual entries in the on-map Layer Control. Exclude style layers whose metadata carries `geolibre:internal`, and refresh the layer control on styledata (debounced) so the exclusion applies reactively when a plugin adds or removes such layers. * Address review feedback - styledata listener: reset the timer on each event so it is a true trailing-edge debounce instead of a leading-edge throttle, avoiding a control rebuild against a half-built style mid-burst (Claude, CodeRabbit). - destroy(): clear the pending layerControlStyleRefreshTimer so it cannot fire after teardown or race a later init() (Claude). - createLayerControlConfig: sort internalStyleLayerIds and dedupe the merged excludeLayers via a Set so reordering an already-hidden internal layer does not change the signature and force a rebuild (CodeRabbit).
1 parent 68b04cf commit 485e80a

1 file changed

Lines changed: 47 additions & 4 deletions

File tree

packages/map/src/map-controller.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ export class MapController {
260260
private logoControl: maplibregl.LogoControl | null = null;
261261
private layerControl: LayerControl | null = null;
262262
private layerControlSignature = "";
263+
// Debounce timer for refreshing the layer control on style changes, so a
264+
// plugin adding/removing native style layers (e.g. ones flagged
265+
// `metadata["geolibre:internal"]`) updates the control's exclusion list.
266+
private layerControlStyleRefreshTimer: ReturnType<typeof setTimeout> | null =
267+
null;
263268
// True while pushing store paint back into the layer control's open style
264269
// editor, so onLayerStyleChange callbacks during that refresh are ignored
265270
// (reentrancy guard against a sync loop). See syncLayerControlState.
@@ -347,6 +352,22 @@ export class MapController {
347352
this.map.on("style.load", handleStyleReady);
348353
this.map.once("load", handleStyleReady);
349354
this.map.once("idle", () => this.enforceProjection());
355+
// Plugins can add native style layers directly (outside the layer store);
356+
// refresh the layer control on style changes so internal-flagged layers are
357+
// excluded reactively. Debounced (trailing edge) because styledata fires
358+
// frequently, and refreshLayerControl no-ops when the computed signature is
359+
// unchanged. Resetting the timer on each event waits until the burst of
360+
// style updates quiets so the control never rebuilds against a half-built
361+
// style.
362+
this.map.on("styledata", () => {
363+
if (this.layerControlStyleRefreshTimer !== null) {
364+
clearTimeout(this.layerControlStyleRefreshTimer);
365+
}
366+
this.layerControlStyleRefreshTimer = setTimeout(() => {
367+
this.layerControlStyleRefreshTimer = null;
368+
this.refreshLayerControl(this.syncedLayers);
369+
}, 200);
370+
});
350371
// Add the fullscreen toggle first so it anchors the top of the top-right
351372
// control cluster, matching the universal placement users expect (issue
352373
// #512). MapLibre stacks controls in insertion order within a corner.
@@ -664,6 +685,10 @@ export class MapController {
664685
this.removeAttributionControl();
665686
this.removeLogoControl();
666687
this.removeLayerControl();
688+
if (this.layerControlStyleRefreshTimer !== null) {
689+
clearTimeout(this.layerControlStyleRefreshTimer);
690+
this.layerControlStyleRefreshTimer = null;
691+
}
667692
this.map?.remove();
668693
this.map = null;
669694
this.styleReady = false;
@@ -1474,10 +1499,28 @@ export class MapController {
14741499
const nativeStyleLayerIds = layers.flatMap((layer) =>
14751500
this.getCandidateStyleLayers(layer).map(({ id }) => id),
14761501
);
1477-
const excludeLayers = [
1478-
...LAYER_CONTROL_EXCLUDED_LAYERS,
1479-
...nativeStyleLayerIds,
1480-
];
1502+
// Hide style layers a plugin marks as internal chrome (e.g. selection
1503+
// footprints, draw/highlight helpers) so they don't clutter the control.
1504+
const internalStyleLayerIds = (this.map?.getStyle()?.layers ?? [])
1505+
.filter((styleLayer) =>
1506+
Boolean(
1507+
(styleLayer.metadata as Record<string, unknown> | undefined)?.[
1508+
"geolibre:internal"
1509+
],
1510+
),
1511+
)
1512+
.map((styleLayer) => styleLayer.id)
1513+
// Sort so a plugin reordering an already-hidden internal layer (which
1514+
// shuffles live style order) doesn't change the exclusion signature and
1515+
// force an unnecessary control rebuild.
1516+
.sort();
1517+
const excludeLayers = Array.from(
1518+
new Set([
1519+
...LAYER_CONTROL_EXCLUDED_LAYERS,
1520+
...nativeStyleLayerIds,
1521+
...internalStyleLayerIds,
1522+
]),
1523+
);
14811524
const controllableLayers = layers.filter(
14821525
(layer) =>
14831526
this.getNativeLayerIds(layer).length > 0 ||

0 commit comments

Comments
 (0)