maplibre-gl-layer-control 0.2.0 → 0.4.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 +6 -0
- package/dist/index.cjs +189 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +189 -36
- package/dist/index.mjs.map +1 -1
- package/dist/maplibre-gl-layer-control.css +43 -6
- package/dist/types/index.d.ts +394 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# maplibre-gl-layer-control
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/maplibre-gl-layer-control)
|
|
4
|
+
[](https://www.npmjs.com/package/maplibre-gl-layer-control)
|
|
5
|
+
[](https://github.com/opengeos/maplibre-gl-layer-control/blob/main/LICENSE)
|
|
6
|
+
|
|
3
7
|
A comprehensive layer control for MapLibre GL with advanced styling capabilities. Built with TypeScript and React, providing both vanilla JavaScript and React integration options.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
@@ -149,6 +153,8 @@ function MapComponent() {
|
|
|
149
153
|
| `panelWidth` | `number` | `320` | Initial panel width in pixels |
|
|
150
154
|
| `panelMinWidth` | `number` | `240` | Minimum panel width |
|
|
151
155
|
| `panelMaxWidth` | `number` | `420` | Maximum panel width |
|
|
156
|
+
| `showStyleEditor` | `boolean` | `true` | Show gear icon for style editor |
|
|
157
|
+
| `showOpacitySlider` | `boolean` | `true` | Show opacity slider for layers |
|
|
152
158
|
|
|
153
159
|
### LayerState
|
|
154
160
|
|
package/dist/index.cjs
CHANGED
|
@@ -181,6 +181,8 @@ class LayerControl {
|
|
|
181
181
|
// Panel width management
|
|
182
182
|
__publicField(this, "minPanelWidth");
|
|
183
183
|
__publicField(this, "maxPanelWidth");
|
|
184
|
+
__publicField(this, "showStyleEditor");
|
|
185
|
+
__publicField(this, "showOpacitySlider");
|
|
184
186
|
__publicField(this, "widthSliderEl", null);
|
|
185
187
|
__publicField(this, "widthThumbEl", null);
|
|
186
188
|
__publicField(this, "widthValueEl", null);
|
|
@@ -191,6 +193,8 @@ class LayerControl {
|
|
|
191
193
|
__publicField(this, "widthFrame", null);
|
|
192
194
|
this.minPanelWidth = options.panelMinWidth || 240;
|
|
193
195
|
this.maxPanelWidth = options.panelMaxWidth || 420;
|
|
196
|
+
this.showStyleEditor = options.showStyleEditor !== false;
|
|
197
|
+
this.showOpacitySlider = options.showOpacitySlider !== false;
|
|
194
198
|
this.state = {
|
|
195
199
|
collapsed: options.collapsed !== false,
|
|
196
200
|
panelWidth: options.panelWidth || 320,
|
|
@@ -240,7 +244,27 @@ class LayerControl {
|
|
|
240
244
|
}
|
|
241
245
|
const allLayerIds = style.layers.map((layer) => layer.id);
|
|
242
246
|
if (this.targetLayers.length === 0) {
|
|
247
|
+
const originalSourceIds = this.getOriginalStyleSourceIds();
|
|
248
|
+
const userAddedLayers = [];
|
|
249
|
+
const backgroundLayerIds = [];
|
|
243
250
|
allLayerIds.forEach((layerId) => {
|
|
251
|
+
const layer = this.map.getLayer(layerId);
|
|
252
|
+
if (!layer) return;
|
|
253
|
+
const sourceId = layer.source;
|
|
254
|
+
if (!sourceId || originalSourceIds.has(sourceId)) {
|
|
255
|
+
backgroundLayerIds.push(layerId);
|
|
256
|
+
} else {
|
|
257
|
+
userAddedLayers.push(layerId);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
if (backgroundLayerIds.length > 0) {
|
|
261
|
+
this.state.layerStates["Background"] = {
|
|
262
|
+
visible: true,
|
|
263
|
+
opacity: 1,
|
|
264
|
+
name: "Background"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
userAddedLayers.forEach((layerId) => {
|
|
244
268
|
const layer = this.map.getLayer(layerId);
|
|
245
269
|
if (!layer) return;
|
|
246
270
|
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
@@ -288,6 +312,60 @@ class LayerControl {
|
|
|
288
312
|
}
|
|
289
313
|
this.targetLayers = Object.keys(this.state.layerStates);
|
|
290
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Get the source IDs that were part of the original style (from the style URL)
|
|
317
|
+
* Sources added via map.addSource() are considered user-added
|
|
318
|
+
*/
|
|
319
|
+
getOriginalStyleSourceIds() {
|
|
320
|
+
const originalSourceIds = /* @__PURE__ */ new Set();
|
|
321
|
+
const style = this.map.getStyle();
|
|
322
|
+
if (!style || !style.sources) return originalSourceIds;
|
|
323
|
+
const spriteUrl = style.sprite;
|
|
324
|
+
const glyphsUrl = style.glyphs;
|
|
325
|
+
let styleBaseDomain = "";
|
|
326
|
+
if (spriteUrl) {
|
|
327
|
+
try {
|
|
328
|
+
const url = new URL(typeof spriteUrl === "string" ? spriteUrl : "");
|
|
329
|
+
styleBaseDomain = url.hostname;
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
} else if (glyphsUrl) {
|
|
333
|
+
try {
|
|
334
|
+
const url = new URL(glyphsUrl.replace("{fontstack}", "test").replace("{range}", "test"));
|
|
335
|
+
styleBaseDomain = url.hostname;
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
Object.entries(style.sources).forEach(([sourceId, source]) => {
|
|
340
|
+
const src = source;
|
|
341
|
+
let sourceUrl = src.url || src.tiles && src.tiles[0] || "";
|
|
342
|
+
if (sourceUrl) {
|
|
343
|
+
try {
|
|
344
|
+
const url = new URL(sourceUrl);
|
|
345
|
+
if (styleBaseDomain && url.hostname === styleBaseDomain) {
|
|
346
|
+
originalSourceIds.add(sourceId);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const basemapDomains = [
|
|
350
|
+
"demotiles.maplibre.org",
|
|
351
|
+
"api.maptiler.com",
|
|
352
|
+
"tiles.stadiamaps.com",
|
|
353
|
+
"api.mapbox.com",
|
|
354
|
+
"basemaps.cartocdn.com"
|
|
355
|
+
];
|
|
356
|
+
if (basemapDomains.some((domain) => url.hostname.includes(domain))) {
|
|
357
|
+
originalSourceIds.add(sourceId);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!src.data && !sourceUrl && src.type !== "geojson") {
|
|
364
|
+
originalSourceIds.add(sourceId);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return originalSourceIds;
|
|
368
|
+
}
|
|
291
369
|
/**
|
|
292
370
|
* Generate a friendly display name from a layer ID
|
|
293
371
|
*/
|
|
@@ -331,8 +409,53 @@ class LayerControl {
|
|
|
331
409
|
}
|
|
332
410
|
const header = this.createPanelHeader();
|
|
333
411
|
panel.appendChild(header);
|
|
412
|
+
const actionButtons = this.createActionButtons();
|
|
413
|
+
panel.appendChild(actionButtons);
|
|
334
414
|
return panel;
|
|
335
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Create action buttons for Show All / Hide All
|
|
418
|
+
*/
|
|
419
|
+
createActionButtons() {
|
|
420
|
+
const container = document.createElement("div");
|
|
421
|
+
container.className = "layer-control-actions";
|
|
422
|
+
const showAllBtn = document.createElement("button");
|
|
423
|
+
showAllBtn.type = "button";
|
|
424
|
+
showAllBtn.className = "layer-control-action-btn";
|
|
425
|
+
showAllBtn.textContent = "Show All";
|
|
426
|
+
showAllBtn.title = "Show all layers";
|
|
427
|
+
showAllBtn.addEventListener("click", () => this.setAllLayersVisibility(true));
|
|
428
|
+
const hideAllBtn = document.createElement("button");
|
|
429
|
+
hideAllBtn.type = "button";
|
|
430
|
+
hideAllBtn.className = "layer-control-action-btn";
|
|
431
|
+
hideAllBtn.textContent = "Hide All";
|
|
432
|
+
hideAllBtn.title = "Hide all layers";
|
|
433
|
+
hideAllBtn.addEventListener("click", () => this.setAllLayersVisibility(false));
|
|
434
|
+
container.appendChild(showAllBtn);
|
|
435
|
+
container.appendChild(hideAllBtn);
|
|
436
|
+
return container;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Set visibility of all layers
|
|
440
|
+
*/
|
|
441
|
+
setAllLayersVisibility(visible) {
|
|
442
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
443
|
+
if (layerId === "Background") {
|
|
444
|
+
this.toggleBackgroundVisibility(visible);
|
|
445
|
+
} else {
|
|
446
|
+
this.state.layerStates[layerId].visible = visible;
|
|
447
|
+
this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
|
448
|
+
}
|
|
449
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
450
|
+
if (itemEl) {
|
|
451
|
+
const checkbox = itemEl.querySelector(".layer-control-checkbox");
|
|
452
|
+
if (checkbox) {
|
|
453
|
+
checkbox.checked = visible;
|
|
454
|
+
checkbox.indeterminate = false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
336
459
|
/**
|
|
337
460
|
* Create the panel header with title and width control
|
|
338
461
|
*/
|
|
@@ -506,7 +629,11 @@ class LayerControl {
|
|
|
506
629
|
this.widthSliderEl.setAttribute("aria-valuenow", String(this.state.panelWidth));
|
|
507
630
|
const ratio = (this.state.panelWidth - this.minPanelWidth) / (this.maxPanelWidth - this.minPanelWidth || 1);
|
|
508
631
|
if (this.widthThumbEl) {
|
|
509
|
-
const sliderWidth = this.widthSliderEl.clientWidth
|
|
632
|
+
const sliderWidth = this.widthSliderEl.clientWidth;
|
|
633
|
+
if (sliderWidth === 0) {
|
|
634
|
+
requestAnimationFrame(() => this.updateWidthDisplay());
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
510
637
|
const thumbWidth = this.widthThumbEl.offsetWidth || 14;
|
|
511
638
|
const padding = 16;
|
|
512
639
|
const available = Math.max(0, sliderWidth - padding - thumbWidth);
|
|
@@ -611,34 +738,38 @@ class LayerControl {
|
|
|
611
738
|
name.className = "layer-control-name";
|
|
612
739
|
name.textContent = state.name || layerId;
|
|
613
740
|
name.title = state.name || layerId;
|
|
614
|
-
const opacity = document.createElement("input");
|
|
615
|
-
opacity.type = "range";
|
|
616
|
-
opacity.className = "layer-control-opacity";
|
|
617
|
-
opacity.min = "0";
|
|
618
|
-
opacity.max = "1";
|
|
619
|
-
opacity.step = "0.01";
|
|
620
|
-
opacity.value = String(state.opacity);
|
|
621
|
-
opacity.title = `Opacity: ${Math.round(state.opacity * 100)}%`;
|
|
622
|
-
opacity.addEventListener("mousedown", () => {
|
|
623
|
-
this.state.userInteractingWithSlider = true;
|
|
624
|
-
});
|
|
625
|
-
opacity.addEventListener("mouseup", () => {
|
|
626
|
-
this.state.userInteractingWithSlider = false;
|
|
627
|
-
});
|
|
628
|
-
opacity.addEventListener("input", () => {
|
|
629
|
-
this.changeLayerOpacity(layerId, parseFloat(opacity.value));
|
|
630
|
-
opacity.title = `Opacity: ${Math.round(parseFloat(opacity.value) * 100)}%`;
|
|
631
|
-
});
|
|
632
741
|
row.appendChild(checkbox);
|
|
633
742
|
row.appendChild(name);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
743
|
+
if (this.showOpacitySlider) {
|
|
744
|
+
const opacity = document.createElement("input");
|
|
745
|
+
opacity.type = "range";
|
|
746
|
+
opacity.className = "layer-control-opacity";
|
|
747
|
+
opacity.min = "0";
|
|
748
|
+
opacity.max = "1";
|
|
749
|
+
opacity.step = "0.01";
|
|
750
|
+
opacity.value = String(state.opacity);
|
|
751
|
+
opacity.title = `Opacity: ${Math.round(state.opacity * 100)}%`;
|
|
752
|
+
opacity.addEventListener("mousedown", () => {
|
|
753
|
+
this.state.userInteractingWithSlider = true;
|
|
754
|
+
});
|
|
755
|
+
opacity.addEventListener("mouseup", () => {
|
|
756
|
+
this.state.userInteractingWithSlider = false;
|
|
757
|
+
});
|
|
758
|
+
opacity.addEventListener("input", () => {
|
|
759
|
+
this.changeLayerOpacity(layerId, parseFloat(opacity.value));
|
|
760
|
+
opacity.title = `Opacity: ${Math.round(parseFloat(opacity.value) * 100)}%`;
|
|
761
|
+
});
|
|
762
|
+
row.appendChild(opacity);
|
|
763
|
+
}
|
|
764
|
+
if (this.showStyleEditor) {
|
|
765
|
+
if (layerId === "Background") {
|
|
766
|
+
const legendButton = this.createBackgroundLegendButton();
|
|
767
|
+
row.appendChild(legendButton);
|
|
768
|
+
} else {
|
|
769
|
+
const styleButton = this.createStyleButton(layerId);
|
|
770
|
+
if (styleButton) {
|
|
771
|
+
row.appendChild(styleButton);
|
|
772
|
+
}
|
|
642
773
|
}
|
|
643
774
|
}
|
|
644
775
|
item.appendChild(row);
|
|
@@ -1380,7 +1511,7 @@ class LayerControl {
|
|
|
1380
1511
|
});
|
|
1381
1512
|
}
|
|
1382
1513
|
/**
|
|
1383
|
-
* Check for new layers and add them to the control
|
|
1514
|
+
* Check for new layers and add them to the control, remove deleted layers
|
|
1384
1515
|
*/
|
|
1385
1516
|
checkForNewLayers() {
|
|
1386
1517
|
try {
|
|
@@ -1388,21 +1519,43 @@ class LayerControl {
|
|
|
1388
1519
|
if (!style || !style.layers) {
|
|
1389
1520
|
return;
|
|
1390
1521
|
}
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1395
|
-
return true;
|
|
1396
|
-
});
|
|
1522
|
+
const currentMapLayerIds = new Set(style.layers.map((layer) => layer.id));
|
|
1523
|
+
const originalSourceIds = this.getOriginalStyleSourceIds();
|
|
1524
|
+
const isAutoDetectMode = this.targetLayers.length === 0 || this.targetLayers.length === 1 && this.targetLayers[0] === "Background" || this.targetLayers.every((id) => id === "Background" || this.state.layerStates[id]);
|
|
1397
1525
|
const newLayers = [];
|
|
1398
|
-
|
|
1526
|
+
currentMapLayerIds.forEach((layerId) => {
|
|
1399
1527
|
if (layerId !== "Background" && !this.state.layerStates[layerId]) {
|
|
1400
1528
|
const layer = this.map.getLayer(layerId);
|
|
1401
1529
|
if (layer) {
|
|
1530
|
+
if (isAutoDetectMode) {
|
|
1531
|
+
const sourceId = layer.source;
|
|
1532
|
+
if (!sourceId || originalSourceIds.has(sourceId)) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1402
1536
|
newLayers.push(layerId);
|
|
1403
1537
|
}
|
|
1404
1538
|
}
|
|
1405
1539
|
});
|
|
1540
|
+
const removedLayers = [];
|
|
1541
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
1542
|
+
if (layerId !== "Background" && !currentMapLayerIds.has(layerId)) {
|
|
1543
|
+
removedLayers.push(layerId);
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
if (removedLayers.length > 0) {
|
|
1547
|
+
removedLayers.forEach((layerId) => {
|
|
1548
|
+
delete this.state.layerStates[layerId];
|
|
1549
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
1550
|
+
if (itemEl) {
|
|
1551
|
+
itemEl.remove();
|
|
1552
|
+
}
|
|
1553
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
1554
|
+
this.state.activeStyleEditor = null;
|
|
1555
|
+
}
|
|
1556
|
+
this.styleEditors.delete(layerId);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1406
1559
|
if (newLayers.length > 0) {
|
|
1407
1560
|
newLayers.forEach((layerId) => {
|
|
1408
1561
|
const layer = this.map.getLayer(layerId);
|
|
@@ -1414,7 +1567,7 @@ class LayerControl {
|
|
|
1414
1567
|
this.state.layerStates[layerId] = {
|
|
1415
1568
|
visible: isVisible,
|
|
1416
1569
|
opacity,
|
|
1417
|
-
name:
|
|
1570
|
+
name: this.generateFriendlyName(layerId)
|
|
1418
1571
|
};
|
|
1419
1572
|
this.addLayerItem(layerId, this.state.layerStates[layerId]);
|
|
1420
1573
|
});
|