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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # maplibre-gl-layer-control
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/maplibre-gl-layer-control.svg)](https://www.npmjs.com/package/maplibre-gl-layer-control)
4
+ [![npm downloads](https://img.shields.io/npm/dm/maplibre-gl-layer-control.svg)](https://www.npmjs.com/package/maplibre-gl-layer-control)
5
+ [![license](https://img.shields.io/npm/l/maplibre-gl-layer-control.svg)](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 || 1;
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
- row.appendChild(opacity);
635
- if (layerId === "Background") {
636
- const legendButton = this.createBackgroundLegendButton();
637
- row.appendChild(legendButton);
638
- } else {
639
- const styleButton = this.createStyleButton(layerId);
640
- if (styleButton) {
641
- row.appendChild(styleButton);
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 currentMapLayers = style.layers.map((layer) => layer.id).filter((id) => {
1392
- if (this.targetLayers.length > 0) {
1393
- return this.targetLayers.includes(id);
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
- currentMapLayers.forEach((layerId) => {
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: layerId.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
1570
+ name: this.generateFriendlyName(layerId)
1418
1571
  };
1419
1572
  this.addLayerItem(layerId, this.state.layerStates[layerId]);
1420
1573
  });