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