maplibre-gl-layer-control 0.15.1 → 0.17.0
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.
- package/README.md +31 -5
- package/dist/index.cjs +289 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +289 -174
- package/dist/index.mjs.map +1 -1
- package/dist/maplibre-gl-layer-control.css +55 -57
- package/dist/types/index.d.ts +104 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A comprehensive layer control for MapLibre GL with advanced styling capabilities
|
|
|
14
14
|
- ✅ **Layer visibility toggle** - Checkbox control for each layer
|
|
15
15
|
- ✅ **Layer opacity control** - Smooth opacity slider with type-aware property mapping; **double-click the slider to type an exact percentage (0-100%)**
|
|
16
16
|
- ✅ **Layer symbols** - Visual type indicators (colored shapes) next to layer names, auto-detected from layer paint properties
|
|
17
|
-
- ✅ **Resizable panel** - **Drag either edge of the panel to resize it
|
|
17
|
+
- ✅ **Resizable panel** - **Drag either edge of the panel to resize it**; double-click an edge to reset to the default width. The panel also grows to fill the available height so long layer lists only scroll once they exceed the map.
|
|
18
18
|
- ✅ **Advanced style editor** - Per-layer-type styling controls:
|
|
19
19
|
- **Fill layers**: color, opacity, outline-color
|
|
20
20
|
- **Line layers**: color, width, opacity, blur
|
|
@@ -24,6 +24,7 @@ A comprehensive layer control for MapLibre GL with advanced styling capabilities
|
|
|
24
24
|
- ✅ **Dynamic layer detection** - Automatically detect and manage new layers
|
|
25
25
|
- ✅ **Background layer grouping** - Control all basemap layers as one group
|
|
26
26
|
- ✅ **Background layer legend** - Gear icon to toggle individual background layer visibility
|
|
27
|
+
- ✅ **Saved configurations** - Save the current basemap element visibility as a named preset and re-apply it with one click; presets persist across sessions and projects via `localStorage`
|
|
27
28
|
- ✅ **Accessibility** - Full ARIA support and keyboard navigation
|
|
28
29
|
- ✅ **TypeScript** - Full type safety and IntelliSense support
|
|
29
30
|
- ✅ **React integration** - Optional React components and hooks
|
|
@@ -72,7 +73,7 @@ map.on('load', () => {
|
|
|
72
73
|
layers: ['my-layer'], // LayerControl auto-detects opacity, visibility, and generates friendly names
|
|
73
74
|
panelWidth: 340,
|
|
74
75
|
panelMinWidth: 240,
|
|
75
|
-
panelMaxWidth:
|
|
76
|
+
panelMaxWidth: 960
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
// Option 2: Auto-detect with basemapStyleUrl (recommended for reliable basemap detection)
|
|
@@ -92,7 +93,7 @@ map.on('load', () => {
|
|
|
92
93
|
// collapsed: false,
|
|
93
94
|
// panelWidth: 340,
|
|
94
95
|
// panelMinWidth: 240,
|
|
95
|
-
// panelMaxWidth:
|
|
96
|
+
// panelMaxWidth: 960
|
|
96
97
|
// });
|
|
97
98
|
|
|
98
99
|
// Option 4: Manually specify layer states (for full control over names)
|
|
@@ -166,8 +167,8 @@ function MapComponent() {
|
|
|
166
167
|
| `layerStates` | `Record<string, LayerState>` | `undefined` | Manual layer state configuration |
|
|
167
168
|
| `panelWidth` | `number` | `320` | Initial panel width in pixels |
|
|
168
169
|
| `panelMinWidth` | `number` | `240` | Minimum panel width |
|
|
169
|
-
| `panelMaxWidth` | `number` | `
|
|
170
|
-
| `panelMaxHeight` | `number` | `
|
|
170
|
+
| `panelMaxWidth` | `number` | `960` | Maximum panel width |
|
|
171
|
+
| `panelMaxHeight` | `number` | `undefined` | Maximum panel height in pixels. Omit to fill the available vertical space (scrollable only when the layer list is taller than the map) |
|
|
171
172
|
| `showStyleEditor` | `boolean` | `true` | Show gear icon for style editor |
|
|
172
173
|
| `showOpacitySlider` | `boolean` | `true` | Show opacity slider for layers |
|
|
173
174
|
| `showLayerSymbol` | `boolean` | `true` | Show layer type symbols (colored icons) next to layer names |
|
|
@@ -175,6 +176,9 @@ function MapComponent() {
|
|
|
175
176
|
| `excludeLayers` | `string[]` | `undefined` | Array of wildcard patterns to exclude layers by name (e.g., `['*-temp-*', 'debug-*']`) |
|
|
176
177
|
| `customLayerAdapters` | `CustomLayerAdapter[]` | `undefined` | Adapters for non-MapLibre layers (deck.gl, Zarr, etc.) |
|
|
177
178
|
| `basemapStyleUrl` | `string` | `undefined` | URL of basemap style JSON for reliable layer detection (see below) |
|
|
179
|
+
| `enableBackgroundPresets` | `boolean` | `true` | Show the "Saved configurations" controls in the Background Layers panel |
|
|
180
|
+
| `backgroundPresetStorageKey` | `string` | `'maplibre-layer-control:background-presets'` | `localStorage` key under which background visibility presets are stored |
|
|
181
|
+
| `onBackgroundPresetsChange` | `(presets: BackgroundPresets) => void` | `undefined` | Called whenever the saved preset set changes (created or deleted); applying a preset does not change the set, so it does not fire |
|
|
178
182
|
|
|
179
183
|
### LayerState
|
|
180
184
|
|
|
@@ -293,9 +297,31 @@ When using the `layers` option to specify specific layers, all other layers are
|
|
|
293
297
|
- Quick "Show All" / "Hide All" buttons
|
|
294
298
|
- **"Only rendered" filter** - Shows only layers that are currently rendered in the map viewport
|
|
295
299
|
- Indeterminate checkbox state when some layers are hidden
|
|
300
|
+
- **Saved configurations** - Save the current set of visibility toggles as a named preset and re-apply it later with one click
|
|
296
301
|
|
|
297
302
|
This allows fine-grained control over which basemap layers are visible while maintaining a simplified layer control interface.
|
|
298
303
|
|
|
304
|
+
#### Saved configurations (presets)
|
|
305
|
+
|
|
306
|
+
The Background Layers panel includes a **Saved configurations** section that lets users save the current basemap element visibility as a named preset and re-apply it with a single click. Presets are stored in `localStorage`, so they persist across page reloads and apply to any project that uses the same basemap layers. Toggle the UI off with `enableBackgroundPresets: false`, or change the storage location with `backgroundPresetStorageKey`.
|
|
307
|
+
|
|
308
|
+
The same functionality is available programmatically:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
// Capture the current basemap element visibility
|
|
312
|
+
const visibility = layerControl.getBackgroundLayerVisibility();
|
|
313
|
+
// → { 'water': true, 'road-primary': false, ... }
|
|
314
|
+
|
|
315
|
+
// Apply a configuration (only layers present in the style are affected)
|
|
316
|
+
layerControl.applyBackgroundLayerVisibility(visibility);
|
|
317
|
+
|
|
318
|
+
// Named presets (persisted to localStorage)
|
|
319
|
+
layerControl.saveBackgroundPreset('Minimal'); // save current visibility
|
|
320
|
+
layerControl.getBackgroundPresets(); // → { Minimal: { ... } }
|
|
321
|
+
layerControl.applyBackgroundPreset('Minimal'); // → true if it existed
|
|
322
|
+
layerControl.deleteBackgroundPreset('Minimal');
|
|
323
|
+
```
|
|
324
|
+
|
|
299
325
|
### Custom Layer Adapters
|
|
300
326
|
|
|
301
327
|
The layer control supports non-MapLibre layers (such as deck.gl or Zarr layers) through the Custom Layer Adapter interface. This allows you to integrate any custom layer type with the layer control's visibility toggle, opacity slider, and layer list.
|
package/dist/index.cjs
CHANGED
|
@@ -691,6 +691,7 @@ class LayerControl {
|
|
|
691
691
|
__publicField(this, "minPanelWidth");
|
|
692
692
|
__publicField(this, "maxPanelWidth");
|
|
693
693
|
__publicField(this, "initialPanelWidth");
|
|
694
|
+
/** Explicit max panel height; null means fill available vertical space */
|
|
694
695
|
__publicField(this, "maxPanelHeight");
|
|
695
696
|
__publicField(this, "showStyleEditor");
|
|
696
697
|
__publicField(this, "showOpacitySlider");
|
|
@@ -703,13 +704,6 @@ class LayerControl {
|
|
|
703
704
|
__publicField(this, "nativeLayerGroups", /* @__PURE__ */ new Map());
|
|
704
705
|
__publicField(this, "basemapStyleUrl", null);
|
|
705
706
|
__publicField(this, "basemapLayerIds", null);
|
|
706
|
-
__publicField(this, "widthSliderEl", null);
|
|
707
|
-
__publicField(this, "widthThumbEl", null);
|
|
708
|
-
__publicField(this, "widthValueEl", null);
|
|
709
|
-
__publicField(this, "isWidthSliderActive", false);
|
|
710
|
-
__publicField(this, "widthDragRectWidth", null);
|
|
711
|
-
__publicField(this, "widthDragStartX", null);
|
|
712
|
-
__publicField(this, "widthDragStartWidth", null);
|
|
713
707
|
__publicField(this, "widthFrame", null);
|
|
714
708
|
// Exact-opacity input popup
|
|
715
709
|
__publicField(this, "opacityInputEl", null);
|
|
@@ -730,10 +724,14 @@ class LayerControl {
|
|
|
730
724
|
__publicField(this, "onLayerRename");
|
|
731
725
|
__publicField(this, "onLayerReorder");
|
|
732
726
|
__publicField(this, "onLayerRemove");
|
|
727
|
+
// Background-layer visibility presets
|
|
728
|
+
__publicField(this, "enableBackgroundPresets");
|
|
729
|
+
__publicField(this, "backgroundPresetStorageKey");
|
|
730
|
+
__publicField(this, "onBackgroundPresetsChange");
|
|
733
731
|
this.minPanelWidth = options.panelMinWidth || 240;
|
|
734
|
-
this.maxPanelWidth = options.panelMaxWidth ||
|
|
732
|
+
this.maxPanelWidth = options.panelMaxWidth || 960;
|
|
735
733
|
this.initialPanelWidth = options.panelWidth || 350;
|
|
736
|
-
this.maxPanelHeight = options.panelMaxHeight
|
|
734
|
+
this.maxPanelHeight = options.panelMaxHeight ?? null;
|
|
737
735
|
this.showStyleEditor = options.showStyleEditor !== false;
|
|
738
736
|
this.showOpacitySlider = options.showOpacitySlider !== false;
|
|
739
737
|
this.showLayerSymbol = options.showLayerSymbol !== false;
|
|
@@ -746,6 +744,9 @@ class LayerControl {
|
|
|
746
744
|
this.onLayerRename = options.onLayerRename;
|
|
747
745
|
this.onLayerReorder = options.onLayerReorder;
|
|
748
746
|
this.onLayerRemove = options.onLayerRemove;
|
|
747
|
+
this.enableBackgroundPresets = options.enableBackgroundPresets !== false;
|
|
748
|
+
this.backgroundPresetStorageKey = options.backgroundPresetStorageKey || "maplibre-layer-control:background-presets";
|
|
749
|
+
this.onBackgroundPresetsChange = options.onBackgroundPresetsChange;
|
|
749
750
|
this.initialLayerStates = options.layerStates || {};
|
|
750
751
|
this.state = {
|
|
751
752
|
collapsed: options.collapsed !== false,
|
|
@@ -811,7 +812,6 @@ class LayerControl {
|
|
|
811
812
|
this.contextMenuEl = this.createContextMenu();
|
|
812
813
|
this.mapContainer.appendChild(this.contextMenuEl);
|
|
813
814
|
}
|
|
814
|
-
this.updateWidthDisplay();
|
|
815
815
|
this.setupEventListeners();
|
|
816
816
|
if (this.basemapStyleUrl && !this.basemapLayerIds) {
|
|
817
817
|
this.fetchBasemapStyle().then(() => {
|
|
@@ -1211,7 +1211,9 @@ class LayerControl {
|
|
|
1211
1211
|
const panel = document.createElement("div");
|
|
1212
1212
|
panel.className = "layer-control-panel";
|
|
1213
1213
|
panel.style.width = `${this.state.panelWidth}px`;
|
|
1214
|
-
|
|
1214
|
+
if (this.maxPanelHeight != null) {
|
|
1215
|
+
panel.style.maxHeight = `${this.maxPanelHeight}px`;
|
|
1216
|
+
}
|
|
1215
1217
|
if (!this.state.collapsed) {
|
|
1216
1218
|
panel.classList.add("expanded");
|
|
1217
1219
|
}
|
|
@@ -1378,6 +1380,15 @@ class LayerControl {
|
|
|
1378
1380
|
this.panel.style.right = `${buttonRight}px`;
|
|
1379
1381
|
break;
|
|
1380
1382
|
}
|
|
1383
|
+
const edgeMargin = 8;
|
|
1384
|
+
const isTopAnchored = position === "top-left" || position === "top-right";
|
|
1385
|
+
const occupiedFromEdge = isTopAnchored ? buttonTop + buttonRect.height + panelGap : buttonBottom + buttonRect.height + panelGap;
|
|
1386
|
+
const availableHeight = Math.max(
|
|
1387
|
+
120,
|
|
1388
|
+
Math.round(mapRect.height - occupiedFromEdge - edgeMargin)
|
|
1389
|
+
);
|
|
1390
|
+
const maxHeight = this.maxPanelHeight != null ? Math.min(this.maxPanelHeight, availableHeight) : availableHeight;
|
|
1391
|
+
this.panel.style.maxHeight = `${maxHeight}px`;
|
|
1381
1392
|
}
|
|
1382
1393
|
/**
|
|
1383
1394
|
* Create action buttons for Show All / Hide All
|
|
@@ -1426,7 +1437,8 @@ class LayerControl {
|
|
|
1426
1437
|
});
|
|
1427
1438
|
}
|
|
1428
1439
|
/**
|
|
1429
|
-
* Create the panel header with title
|
|
1440
|
+
* Create the panel header with the title. Panel width is adjusted by
|
|
1441
|
+
* dragging either edge of the panel (see createResizeHandle).
|
|
1430
1442
|
*/
|
|
1431
1443
|
createPanelHeader() {
|
|
1432
1444
|
const header = document.createElement("div");
|
|
@@ -1435,135 +1447,8 @@ class LayerControl {
|
|
|
1435
1447
|
title.className = "layer-control-panel-title";
|
|
1436
1448
|
title.textContent = "Layers";
|
|
1437
1449
|
header.appendChild(title);
|
|
1438
|
-
const widthControl = this.createWidthControl();
|
|
1439
|
-
header.appendChild(widthControl);
|
|
1440
1450
|
return header;
|
|
1441
1451
|
}
|
|
1442
|
-
/**
|
|
1443
|
-
* Create the width control slider
|
|
1444
|
-
*/
|
|
1445
|
-
createWidthControl() {
|
|
1446
|
-
const widthControl = document.createElement("label");
|
|
1447
|
-
widthControl.className = "layer-control-width-control";
|
|
1448
|
-
widthControl.title = "Adjust layer panel width";
|
|
1449
|
-
const widthLabel = document.createElement("span");
|
|
1450
|
-
widthLabel.textContent = "Width";
|
|
1451
|
-
widthControl.appendChild(widthLabel);
|
|
1452
|
-
const widthSlider = document.createElement("div");
|
|
1453
|
-
widthSlider.className = "layer-control-width-slider";
|
|
1454
|
-
widthSlider.setAttribute("role", "slider");
|
|
1455
|
-
widthSlider.setAttribute("aria-valuemin", String(this.minPanelWidth));
|
|
1456
|
-
widthSlider.setAttribute("aria-valuemax", String(this.maxPanelWidth));
|
|
1457
|
-
widthSlider.setAttribute("aria-valuenow", String(this.state.panelWidth));
|
|
1458
|
-
widthSlider.setAttribute("aria-valuestep", "10");
|
|
1459
|
-
widthSlider.setAttribute("aria-label", "Layer panel width");
|
|
1460
|
-
widthSlider.tabIndex = 0;
|
|
1461
|
-
const widthTrack = document.createElement("div");
|
|
1462
|
-
widthTrack.className = "layer-control-width-track";
|
|
1463
|
-
const widthThumb = document.createElement("div");
|
|
1464
|
-
widthThumb.className = "layer-control-width-thumb";
|
|
1465
|
-
widthSlider.appendChild(widthTrack);
|
|
1466
|
-
widthSlider.appendChild(widthThumb);
|
|
1467
|
-
this.widthSliderEl = widthSlider;
|
|
1468
|
-
this.widthThumbEl = widthThumb;
|
|
1469
|
-
const widthValue = document.createElement("span");
|
|
1470
|
-
widthValue.className = "layer-control-width-value";
|
|
1471
|
-
this.widthValueEl = widthValue;
|
|
1472
|
-
widthControl.appendChild(widthSlider);
|
|
1473
|
-
widthControl.appendChild(widthValue);
|
|
1474
|
-
this.updateWidthDisplay();
|
|
1475
|
-
this.setupWidthSliderEvents(widthSlider);
|
|
1476
|
-
return widthControl;
|
|
1477
|
-
}
|
|
1478
|
-
/**
|
|
1479
|
-
* Setup event listeners for width slider
|
|
1480
|
-
*/
|
|
1481
|
-
setupWidthSliderEvents(widthSlider) {
|
|
1482
|
-
widthSlider.addEventListener("pointerdown", (event) => {
|
|
1483
|
-
event.preventDefault();
|
|
1484
|
-
const rect = widthSlider.getBoundingClientRect();
|
|
1485
|
-
this.widthDragRectWidth = rect.width || 1;
|
|
1486
|
-
this.widthDragStartX = event.clientX;
|
|
1487
|
-
this.widthDragStartWidth = this.state.panelWidth;
|
|
1488
|
-
this.isWidthSliderActive = true;
|
|
1489
|
-
widthSlider.setPointerCapture(event.pointerId);
|
|
1490
|
-
this.updateWidthFromPointer(event, true);
|
|
1491
|
-
});
|
|
1492
|
-
widthSlider.addEventListener("pointermove", (event) => {
|
|
1493
|
-
if (!this.isWidthSliderActive) return;
|
|
1494
|
-
this.updateWidthFromPointer(event);
|
|
1495
|
-
});
|
|
1496
|
-
const endPointerDrag = (event) => {
|
|
1497
|
-
if (!this.isWidthSliderActive) return;
|
|
1498
|
-
if (event.pointerId !== void 0) {
|
|
1499
|
-
try {
|
|
1500
|
-
widthSlider.releasePointerCapture(event.pointerId);
|
|
1501
|
-
} catch (error) {
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
this.isWidthSliderActive = false;
|
|
1505
|
-
this.widthDragRectWidth = null;
|
|
1506
|
-
this.widthDragStartX = null;
|
|
1507
|
-
this.widthDragStartWidth = null;
|
|
1508
|
-
this.updateWidthDisplay();
|
|
1509
|
-
};
|
|
1510
|
-
widthSlider.addEventListener("pointerup", endPointerDrag);
|
|
1511
|
-
widthSlider.addEventListener("pointercancel", endPointerDrag);
|
|
1512
|
-
widthSlider.addEventListener("lostpointercapture", endPointerDrag);
|
|
1513
|
-
widthSlider.addEventListener("keydown", (event) => {
|
|
1514
|
-
let handled = true;
|
|
1515
|
-
const step = event.shiftKey ? 20 : 10;
|
|
1516
|
-
switch (event.key) {
|
|
1517
|
-
case "ArrowLeft":
|
|
1518
|
-
case "ArrowDown":
|
|
1519
|
-
this.applyPanelWidth(this.state.panelWidth - step, true);
|
|
1520
|
-
break;
|
|
1521
|
-
case "ArrowRight":
|
|
1522
|
-
case "ArrowUp":
|
|
1523
|
-
this.applyPanelWidth(this.state.panelWidth + step, true);
|
|
1524
|
-
break;
|
|
1525
|
-
case "Home":
|
|
1526
|
-
this.applyPanelWidth(this.minPanelWidth, true);
|
|
1527
|
-
break;
|
|
1528
|
-
case "End":
|
|
1529
|
-
this.applyPanelWidth(this.maxPanelWidth, true);
|
|
1530
|
-
break;
|
|
1531
|
-
case "PageUp":
|
|
1532
|
-
this.applyPanelWidth(this.state.panelWidth + 50, true);
|
|
1533
|
-
break;
|
|
1534
|
-
case "PageDown":
|
|
1535
|
-
this.applyPanelWidth(this.state.panelWidth - 50, true);
|
|
1536
|
-
break;
|
|
1537
|
-
default:
|
|
1538
|
-
handled = false;
|
|
1539
|
-
}
|
|
1540
|
-
if (handled) {
|
|
1541
|
-
event.preventDefault();
|
|
1542
|
-
this.updateWidthDisplay();
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Update panel width from pointer event
|
|
1548
|
-
*/
|
|
1549
|
-
updateWidthFromPointer(event, resetBaseline = false) {
|
|
1550
|
-
if (!this.widthSliderEl) return;
|
|
1551
|
-
const sliderWidth = this.widthDragRectWidth || this.widthSliderEl.getBoundingClientRect().width || 1;
|
|
1552
|
-
const widthRange = this.maxPanelWidth - this.minPanelWidth;
|
|
1553
|
-
let width;
|
|
1554
|
-
if (resetBaseline) {
|
|
1555
|
-
const rect = this.widthSliderEl.getBoundingClientRect();
|
|
1556
|
-
const relative = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0;
|
|
1557
|
-
const clampedRatio = Math.min(1, Math.max(0, relative));
|
|
1558
|
-
width = this.minPanelWidth + clampedRatio * widthRange;
|
|
1559
|
-
this.widthDragStartWidth = width;
|
|
1560
|
-
this.widthDragStartX = event.clientX;
|
|
1561
|
-
} else {
|
|
1562
|
-
const delta = event.clientX - (this.widthDragStartX || event.clientX);
|
|
1563
|
-
width = (this.widthDragStartWidth || this.state.panelWidth) + delta / sliderWidth * widthRange;
|
|
1564
|
-
}
|
|
1565
|
-
this.applyPanelWidth(width, this.isWidthSliderActive);
|
|
1566
|
-
}
|
|
1567
1452
|
/**
|
|
1568
1453
|
* Apply panel width (clamped to min/max)
|
|
1569
1454
|
*/
|
|
@@ -1575,7 +1460,6 @@ class LayerControl {
|
|
|
1575
1460
|
this.state.panelWidth = clamped;
|
|
1576
1461
|
const px = `${clamped}px`;
|
|
1577
1462
|
this.panel.style.width = px;
|
|
1578
|
-
this.updateWidthDisplay();
|
|
1579
1463
|
};
|
|
1580
1464
|
if (immediate) {
|
|
1581
1465
|
applyWidth();
|
|
@@ -1589,34 +1473,6 @@ class LayerControl {
|
|
|
1589
1473
|
this.widthFrame = null;
|
|
1590
1474
|
});
|
|
1591
1475
|
}
|
|
1592
|
-
/**
|
|
1593
|
-
* Update width display (value label and thumb position)
|
|
1594
|
-
*/
|
|
1595
|
-
updateWidthDisplay() {
|
|
1596
|
-
if (this.widthValueEl) {
|
|
1597
|
-
this.widthValueEl.textContent = `${this.state.panelWidth}px`;
|
|
1598
|
-
}
|
|
1599
|
-
if (this.widthSliderEl) {
|
|
1600
|
-
this.widthSliderEl.setAttribute(
|
|
1601
|
-
"aria-valuenow",
|
|
1602
|
-
String(this.state.panelWidth)
|
|
1603
|
-
);
|
|
1604
|
-
const ratio = (this.state.panelWidth - this.minPanelWidth) / (this.maxPanelWidth - this.minPanelWidth || 1);
|
|
1605
|
-
if (this.widthThumbEl) {
|
|
1606
|
-
const sliderWidth = this.widthSliderEl.clientWidth;
|
|
1607
|
-
if (sliderWidth === 0) {
|
|
1608
|
-
requestAnimationFrame(() => this.updateWidthDisplay());
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
const thumbWidth = this.widthThumbEl.offsetWidth || 14;
|
|
1612
|
-
const padding = 16;
|
|
1613
|
-
const available = Math.max(0, sliderWidth - padding - thumbWidth);
|
|
1614
|
-
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
|
1615
|
-
const leftPx = 8 + available * clampedRatio;
|
|
1616
|
-
this.widthThumbEl.style.left = `${leftPx}px`;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
1476
|
/**
|
|
1621
1477
|
* Setup main event listeners
|
|
1622
1478
|
*/
|
|
@@ -1715,7 +1571,21 @@ class LayerControl {
|
|
|
1715
1571
|
const existingItems = this.panel.querySelectorAll(".layer-control-item");
|
|
1716
1572
|
existingItems.forEach((item) => item.remove());
|
|
1717
1573
|
this.styleEditors.clear();
|
|
1718
|
-
|
|
1574
|
+
const orderedLayerIds = this.getUserLayerIdsInMapOrder();
|
|
1575
|
+
const captured = new Set(orderedLayerIds);
|
|
1576
|
+
for (const layerId of Object.keys(this.state.layerStates)) {
|
|
1577
|
+
if (layerId !== "Background" && !captured.has(layerId)) {
|
|
1578
|
+
orderedLayerIds.push(layerId);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (this.state.layerStates["Background"]) {
|
|
1582
|
+
orderedLayerIds.push("Background");
|
|
1583
|
+
}
|
|
1584
|
+
orderedLayerIds.forEach((layerId) => {
|
|
1585
|
+
const state = this.state.layerStates[layerId];
|
|
1586
|
+
if (!state) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1719
1589
|
if (this.targetLayers.length === 0 || this.targetLayers.includes(layerId)) {
|
|
1720
1590
|
this.addLayerItem(layerId, state);
|
|
1721
1591
|
}
|
|
@@ -2234,8 +2104,112 @@ class LayerControl {
|
|
|
2234
2104
|
panel.appendChild(actionsRow);
|
|
2235
2105
|
panel.appendChild(filterRow);
|
|
2236
2106
|
panel.appendChild(layerList);
|
|
2107
|
+
if (this.enableBackgroundPresets) {
|
|
2108
|
+
panel.appendChild(this.createBackgroundPresetsRow());
|
|
2109
|
+
}
|
|
2237
2110
|
return panel;
|
|
2238
2111
|
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Build the "Saved configurations" controls: a dropdown of saved presets with
|
|
2114
|
+
* Apply/Delete actions, plus a name field and Save button to capture the
|
|
2115
|
+
* current basemap element visibility as a reusable, persisted preset.
|
|
2116
|
+
*/
|
|
2117
|
+
createBackgroundPresetsRow() {
|
|
2118
|
+
const section = document.createElement("div");
|
|
2119
|
+
section.className = "background-legend-presets";
|
|
2120
|
+
const label = document.createElement("div");
|
|
2121
|
+
label.className = "background-legend-presets-label";
|
|
2122
|
+
label.textContent = "Saved configurations";
|
|
2123
|
+
section.appendChild(label);
|
|
2124
|
+
const selectRow = document.createElement("div");
|
|
2125
|
+
selectRow.className = "background-legend-presets-row";
|
|
2126
|
+
const select = document.createElement("select");
|
|
2127
|
+
select.className = "background-legend-presets-select";
|
|
2128
|
+
select.title = "Saved configurations";
|
|
2129
|
+
const applyBtn = document.createElement("button");
|
|
2130
|
+
applyBtn.className = "background-legend-action-btn";
|
|
2131
|
+
applyBtn.textContent = "Apply";
|
|
2132
|
+
applyBtn.title = "Apply the selected configuration";
|
|
2133
|
+
const deleteBtn = document.createElement("button");
|
|
2134
|
+
deleteBtn.className = "background-legend-action-btn";
|
|
2135
|
+
deleteBtn.textContent = "Delete";
|
|
2136
|
+
deleteBtn.title = "Delete the selected configuration";
|
|
2137
|
+
const refreshSelect = (selected) => {
|
|
2138
|
+
const presets = this.getBackgroundPresets();
|
|
2139
|
+
const names = Object.keys(presets).sort(
|
|
2140
|
+
(a, b) => a.localeCompare(b, void 0, { sensitivity: "base" })
|
|
2141
|
+
);
|
|
2142
|
+
select.innerHTML = "";
|
|
2143
|
+
const placeholder = document.createElement("option");
|
|
2144
|
+
placeholder.value = "";
|
|
2145
|
+
placeholder.textContent = names.length ? "Select a configuration…" : "No saved configurations";
|
|
2146
|
+
placeholder.disabled = names.length > 0;
|
|
2147
|
+
select.appendChild(placeholder);
|
|
2148
|
+
names.forEach((name) => {
|
|
2149
|
+
const option = document.createElement("option");
|
|
2150
|
+
option.value = name;
|
|
2151
|
+
option.textContent = name;
|
|
2152
|
+
select.appendChild(option);
|
|
2153
|
+
});
|
|
2154
|
+
select.value = selected && names.includes(selected) ? selected : "";
|
|
2155
|
+
const hasSelection = select.value !== "";
|
|
2156
|
+
applyBtn.disabled = !hasSelection;
|
|
2157
|
+
deleteBtn.disabled = !hasSelection;
|
|
2158
|
+
};
|
|
2159
|
+
select.addEventListener("change", () => {
|
|
2160
|
+
const hasSelection = select.value !== "";
|
|
2161
|
+
applyBtn.disabled = !hasSelection;
|
|
2162
|
+
deleteBtn.disabled = !hasSelection;
|
|
2163
|
+
});
|
|
2164
|
+
applyBtn.addEventListener("click", (e) => {
|
|
2165
|
+
e.stopPropagation();
|
|
2166
|
+
if (select.value) this.applyBackgroundPreset(select.value);
|
|
2167
|
+
});
|
|
2168
|
+
deleteBtn.addEventListener("click", (e) => {
|
|
2169
|
+
e.stopPropagation();
|
|
2170
|
+
if (select.value) {
|
|
2171
|
+
this.deleteBackgroundPreset(select.value);
|
|
2172
|
+
refreshSelect();
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
selectRow.appendChild(select);
|
|
2176
|
+
selectRow.appendChild(applyBtn);
|
|
2177
|
+
selectRow.appendChild(deleteBtn);
|
|
2178
|
+
const saveRow = document.createElement("div");
|
|
2179
|
+
saveRow.className = "background-legend-presets-row";
|
|
2180
|
+
const nameInput = document.createElement("input");
|
|
2181
|
+
nameInput.type = "text";
|
|
2182
|
+
nameInput.className = "background-legend-presets-input";
|
|
2183
|
+
nameInput.placeholder = "Configuration name…";
|
|
2184
|
+
nameInput.maxLength = 60;
|
|
2185
|
+
const saveBtn = document.createElement("button");
|
|
2186
|
+
saveBtn.className = "background-legend-action-btn";
|
|
2187
|
+
saveBtn.textContent = "Save";
|
|
2188
|
+
saveBtn.title = "Save the current visibility as a configuration";
|
|
2189
|
+
const commitSave = () => {
|
|
2190
|
+
const name = nameInput.value.trim();
|
|
2191
|
+
if (!name) return;
|
|
2192
|
+
this.saveBackgroundPreset(name);
|
|
2193
|
+
nameInput.value = "";
|
|
2194
|
+
refreshSelect(name);
|
|
2195
|
+
};
|
|
2196
|
+
saveBtn.addEventListener("click", (e) => {
|
|
2197
|
+
e.stopPropagation();
|
|
2198
|
+
commitSave();
|
|
2199
|
+
});
|
|
2200
|
+
nameInput.addEventListener("keydown", (e) => {
|
|
2201
|
+
if (e.key === "Enter") {
|
|
2202
|
+
e.preventDefault();
|
|
2203
|
+
commitSave();
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
saveRow.appendChild(nameInput);
|
|
2207
|
+
saveRow.appendChild(saveBtn);
|
|
2208
|
+
section.appendChild(selectRow);
|
|
2209
|
+
section.appendChild(saveRow);
|
|
2210
|
+
refreshSelect();
|
|
2211
|
+
return section;
|
|
2212
|
+
}
|
|
2239
2213
|
/**
|
|
2240
2214
|
* Check if a layer is currently rendered in the map viewport
|
|
2241
2215
|
*/
|
|
@@ -2383,6 +2357,148 @@ class LayerControl {
|
|
|
2383
2357
|
this.state.layerStates["Background"].visible = anyVisible;
|
|
2384
2358
|
}
|
|
2385
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Return the IDs of the background (basemap) style layers the control can
|
|
2362
|
+
* toggle, honoring the drawn-layer and user-defined exclusion filters but not
|
|
2363
|
+
* the transient "only rendered" view filter.
|
|
2364
|
+
*/
|
|
2365
|
+
getControllableBackgroundLayerIds() {
|
|
2366
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
2367
|
+
return styleLayers.filter((layer) => {
|
|
2368
|
+
if (this.isUserAddedLayer(layer.id)) return false;
|
|
2369
|
+
if (this.excludeDrawnLayers && this.isDrawnLayer(layer.id))
|
|
2370
|
+
return false;
|
|
2371
|
+
if (this.isExcludedByPattern(layer.id)) return false;
|
|
2372
|
+
return true;
|
|
2373
|
+
}).map((layer) => layer.id);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Get the current visibility of every controllable background (basemap) layer.
|
|
2377
|
+
*
|
|
2378
|
+
* @returns A map of style-layer ID to whether it is currently visible.
|
|
2379
|
+
*/
|
|
2380
|
+
getBackgroundLayerVisibility() {
|
|
2381
|
+
const visibility = {};
|
|
2382
|
+
for (const layerId of this.getControllableBackgroundLayerIds()) {
|
|
2383
|
+
const value = this.map.getLayoutProperty(layerId, "visibility");
|
|
2384
|
+
visibility[layerId] = value !== "none";
|
|
2385
|
+
}
|
|
2386
|
+
return visibility;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Apply a saved background-layer visibility configuration to the map. Only
|
|
2390
|
+
* layers that currently exist in the style are affected, so a configuration
|
|
2391
|
+
* captured on one basemap degrades gracefully when applied to another.
|
|
2392
|
+
*
|
|
2393
|
+
* @param visibility - Map of style-layer ID to desired visibility.
|
|
2394
|
+
*/
|
|
2395
|
+
applyBackgroundLayerVisibility(visibility) {
|
|
2396
|
+
for (const [layerId, visible] of Object.entries(visibility)) {
|
|
2397
|
+
if (this.isUserAddedLayer(layerId)) continue;
|
|
2398
|
+
if (!this.map.getLayer(layerId)) continue;
|
|
2399
|
+
this.state.backgroundLayerVisibility.set(layerId, visible);
|
|
2400
|
+
this.map.setLayoutProperty(
|
|
2401
|
+
layerId,
|
|
2402
|
+
"visibility",
|
|
2403
|
+
visible ? "visible" : "none"
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
const legendPanel = this.panel.querySelector(
|
|
2407
|
+
".background-legend-layer-list"
|
|
2408
|
+
);
|
|
2409
|
+
if (legendPanel) {
|
|
2410
|
+
this.populateBackgroundLayerList(legendPanel);
|
|
2411
|
+
}
|
|
2412
|
+
this.updateBackgroundCheckboxState();
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Read all saved background-layer presets from localStorage. Returns an empty
|
|
2416
|
+
* object when storage is unavailable or the stored value is malformed.
|
|
2417
|
+
*/
|
|
2418
|
+
getBackgroundPresets() {
|
|
2419
|
+
try {
|
|
2420
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(this.backgroundPresetStorageKey) : null;
|
|
2421
|
+
if (!raw) return {};
|
|
2422
|
+
const parsed = JSON.parse(raw);
|
|
2423
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2424
|
+
return {};
|
|
2425
|
+
}
|
|
2426
|
+
const presets = {};
|
|
2427
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
2428
|
+
if (this.isUnsafePresetKey(name)) continue;
|
|
2429
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2430
|
+
presets[name] = value;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
return presets;
|
|
2434
|
+
} catch {
|
|
2435
|
+
return {};
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Reject preset names that, used as object keys, could pollute the prototype
|
|
2440
|
+
* chain or otherwise alias built-in object members.
|
|
2441
|
+
*/
|
|
2442
|
+
isUnsafePresetKey(name) {
|
|
2443
|
+
return name === "__proto__" || name === "constructor" || name === "prototype";
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Persist the full preset map to localStorage and notify listeners.
|
|
2447
|
+
*/
|
|
2448
|
+
writeBackgroundPresets(presets) {
|
|
2449
|
+
var _a;
|
|
2450
|
+
try {
|
|
2451
|
+
if (typeof localStorage !== "undefined") {
|
|
2452
|
+
localStorage.setItem(
|
|
2453
|
+
this.backgroundPresetStorageKey,
|
|
2454
|
+
JSON.stringify(presets)
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
(_a = this.onBackgroundPresetsChange) == null ? void 0 : _a.call(this, presets);
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Save the current background-layer visibility as a named preset. Reusing an
|
|
2463
|
+
* existing name overwrites that preset.
|
|
2464
|
+
*
|
|
2465
|
+
* @param name - Preset name; whitespace is trimmed. Empty names are ignored.
|
|
2466
|
+
* @returns The updated preset map.
|
|
2467
|
+
*/
|
|
2468
|
+
saveBackgroundPreset(name) {
|
|
2469
|
+
const trimmed = name.trim();
|
|
2470
|
+
const presets = this.getBackgroundPresets();
|
|
2471
|
+
if (!trimmed || this.isUnsafePresetKey(trimmed)) return presets;
|
|
2472
|
+
presets[trimmed] = this.getBackgroundLayerVisibility();
|
|
2473
|
+
this.writeBackgroundPresets(presets);
|
|
2474
|
+
return presets;
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Apply a previously saved preset to the map.
|
|
2478
|
+
*
|
|
2479
|
+
* @param name - The preset name to apply.
|
|
2480
|
+
* @returns `true` if a preset with that name existed and was applied.
|
|
2481
|
+
*/
|
|
2482
|
+
applyBackgroundPreset(name) {
|
|
2483
|
+
const presets = this.getBackgroundPresets();
|
|
2484
|
+
if (!Object.prototype.hasOwnProperty.call(presets, name)) return false;
|
|
2485
|
+
this.applyBackgroundLayerVisibility(presets[name]);
|
|
2486
|
+
return true;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Delete a saved preset.
|
|
2490
|
+
*
|
|
2491
|
+
* @param name - The preset name to remove.
|
|
2492
|
+
* @returns The updated preset map.
|
|
2493
|
+
*/
|
|
2494
|
+
deleteBackgroundPreset(name) {
|
|
2495
|
+
const presets = this.getBackgroundPresets();
|
|
2496
|
+
if (Object.prototype.hasOwnProperty.call(presets, name)) {
|
|
2497
|
+
delete presets[name];
|
|
2498
|
+
this.writeBackgroundPresets(presets);
|
|
2499
|
+
}
|
|
2500
|
+
return presets;
|
|
2501
|
+
}
|
|
2386
2502
|
/**
|
|
2387
2503
|
* Create style button for a layer
|
|
2388
2504
|
*/
|
|
@@ -4068,18 +4184,17 @@ class LayerControl {
|
|
|
4068
4184
|
uiLayerIds.push(layerId);
|
|
4069
4185
|
}
|
|
4070
4186
|
});
|
|
4071
|
-
const reversedIds = [...uiLayerIds].reverse();
|
|
4072
4187
|
const style = this.map.getStyle();
|
|
4073
4188
|
const mapLibreLayerIds = new Set(((_a = style == null ? void 0 : style.layers) == null ? void 0 : _a.map((l) => l.id)) || []);
|
|
4074
|
-
for (let i = 0; i <
|
|
4075
|
-
const layerId =
|
|
4189
|
+
for (let i = 0; i < uiLayerIds.length; i++) {
|
|
4190
|
+
const layerId = uiLayerIds[i];
|
|
4076
4191
|
if (!mapLibreLayerIds.has(layerId)) {
|
|
4077
4192
|
continue;
|
|
4078
4193
|
}
|
|
4079
4194
|
let beforeId = void 0;
|
|
4080
4195
|
for (let j = i - 1; j >= 0; j--) {
|
|
4081
|
-
if (mapLibreLayerIds.has(
|
|
4082
|
-
beforeId =
|
|
4196
|
+
if (mapLibreLayerIds.has(uiLayerIds[j])) {
|
|
4197
|
+
beforeId = uiLayerIds[j];
|
|
4083
4198
|
break;
|
|
4084
4199
|
}
|
|
4085
4200
|
}
|