maplibre-gl-layer-control 0.3.0 → 0.5.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.cjs CHANGED
@@ -168,6 +168,258 @@ function formatNumericValue(value, step) {
168
168
  function clamp(value, min, max) {
169
169
  return Math.max(min, Math.min(max, value));
170
170
  }
171
+ const COLOR_PROPERTY_MAP = {
172
+ fill: ["fill-color", "fill-outline-color"],
173
+ line: ["line-color"],
174
+ circle: ["circle-color", "circle-stroke-color"],
175
+ symbol: ["icon-color", "text-color"],
176
+ background: ["background-color"],
177
+ heatmap: ["heatmap-color"],
178
+ "fill-extrusion": ["fill-extrusion-color"]
179
+ };
180
+ function extractColorFromExpression(expression) {
181
+ if (!Array.isArray(expression) || expression.length === 0) return null;
182
+ for (const item of expression) {
183
+ if (typeof item === "string") {
184
+ if (item.startsWith("#") || item.startsWith("rgb") || item.startsWith("hsl")) {
185
+ return normalizeColor(item);
186
+ }
187
+ } else if (Array.isArray(item)) {
188
+ const result = extractColorFromExpression(item);
189
+ if (result) return result;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+ function getLayerColor(map, layerId, layerType) {
195
+ var _a;
196
+ const propertyNames = COLOR_PROPERTY_MAP[layerType];
197
+ if (!propertyNames) return null;
198
+ for (const propertyName of propertyNames) {
199
+ try {
200
+ const runtimeColor = map.getPaintProperty(layerId, propertyName);
201
+ if (runtimeColor) {
202
+ if (typeof runtimeColor === "string") {
203
+ return normalizeColor(runtimeColor);
204
+ }
205
+ if (Array.isArray(runtimeColor)) {
206
+ const extracted = extractColorFromExpression(runtimeColor);
207
+ if (extracted) return extracted;
208
+ }
209
+ }
210
+ } catch {
211
+ }
212
+ const style = map.getStyle();
213
+ const layer = (_a = style == null ? void 0 : style.layers) == null ? void 0 : _a.find(
214
+ (l) => l.id === layerId
215
+ );
216
+ if (layer && "paint" in layer && layer.paint) {
217
+ const paintColor = layer.paint[propertyName];
218
+ if (paintColor) {
219
+ if (typeof paintColor === "string") {
220
+ return normalizeColor(paintColor);
221
+ }
222
+ if (Array.isArray(paintColor)) {
223
+ const extracted = extractColorFromExpression(paintColor);
224
+ if (extracted) return extracted;
225
+ }
226
+ }
227
+ }
228
+ }
229
+ return null;
230
+ }
231
+ function getLayerColorFromSpec(layer) {
232
+ const propertyNames = COLOR_PROPERTY_MAP[layer.type];
233
+ if (!propertyNames) return null;
234
+ for (const propertyName of propertyNames) {
235
+ if ("paint" in layer && layer.paint) {
236
+ const paintColor = layer.paint[propertyName];
237
+ if (paintColor) {
238
+ if (typeof paintColor === "string") {
239
+ return normalizeColor(paintColor);
240
+ }
241
+ if (Array.isArray(paintColor)) {
242
+ const extracted = extractColorFromExpression(paintColor);
243
+ if (extracted) return extracted;
244
+ }
245
+ }
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+ function darkenColor(hexColor, amount) {
251
+ let hex = hexColor.replace("#", "");
252
+ if (hex.length === 3) {
253
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
254
+ }
255
+ const r = Math.max(
256
+ 0,
257
+ parseInt(hex.slice(0, 2), 16) - Math.round(255 * amount)
258
+ );
259
+ const g = Math.max(
260
+ 0,
261
+ parseInt(hex.slice(2, 4), 16) - Math.round(255 * amount)
262
+ );
263
+ const b = Math.max(
264
+ 0,
265
+ parseInt(hex.slice(4, 6), 16) - Math.round(255 * amount)
266
+ );
267
+ return rgbToHex(r, g, b);
268
+ }
269
+ function createFillSymbol(size, color) {
270
+ const padding = 2;
271
+ const borderColor = darkenColor(color, 0.3);
272
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
273
+ <rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
274
+ fill="${color}" stroke="${borderColor}" stroke-width="1" rx="1"/>
275
+ </svg>`;
276
+ }
277
+ function createLineSymbol(size, color, strokeWidth = 2) {
278
+ const y = size / 2;
279
+ const padding = 2;
280
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
281
+ <line x1="${padding}" y1="${y}" x2="${size - padding}" y2="${y}"
282
+ stroke="${color}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
283
+ </svg>`;
284
+ }
285
+ function createCircleSymbol(size, color) {
286
+ const cx = size / 2;
287
+ const cy = size / 2;
288
+ const r = size / 2 - 3;
289
+ const borderColor = darkenColor(color, 0.3);
290
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
291
+ <circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"
292
+ stroke="${borderColor}" stroke-width="1"/>
293
+ </svg>`;
294
+ }
295
+ function createMarkerSymbol(size, color) {
296
+ const borderColor = darkenColor(color, 0.3);
297
+ const cx = size / 2;
298
+ const pinWidth = size * 0.5;
299
+ const pinHeight = size * 0.7;
300
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
301
+ <path d="M${cx} ${size - 2}
302
+ L${cx - pinWidth / 2} ${size - pinHeight}
303
+ A${pinWidth / 2} ${pinWidth / 2} 0 1 1 ${cx + pinWidth / 2} ${size - pinHeight}
304
+ Z"
305
+ fill="${color}" stroke="${borderColor}" stroke-width="1"/>
306
+ <circle cx="${cx}" cy="${size - pinHeight - pinWidth / 4}" r="${pinWidth / 5}" fill="white"/>
307
+ </svg>`;
308
+ }
309
+ function createRasterSymbol(size) {
310
+ const padding = 2;
311
+ const id = `rasterGrad_${Math.random().toString(36).slice(2, 9)}`;
312
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
313
+ <defs>
314
+ <linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="100%">
315
+ <stop offset="0%" stop-color="#e0e0e0"/>
316
+ <stop offset="50%" stop-color="#808080"/>
317
+ <stop offset="100%" stop-color="#404040"/>
318
+ </linearGradient>
319
+ </defs>
320
+ <rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
321
+ fill="url(#${id})" rx="1"/>
322
+ </svg>`;
323
+ }
324
+ function createBackgroundSymbol(size, color) {
325
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
326
+ <rect x="1" y="1" width="${size - 2}" height="${size - 2}" fill="${color}" rx="2"/>
327
+ <rect x="3" y="3" width="${size - 6}" height="${size - 6}" fill="none"
328
+ stroke="white" stroke-width="1" stroke-opacity="0.5" rx="1"/>
329
+ </svg>`;
330
+ }
331
+ function createHeatmapSymbol(size) {
332
+ const padding = 2;
333
+ const id = `heatmapGrad_${Math.random().toString(36).slice(2, 9)}`;
334
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
335
+ <defs>
336
+ <radialGradient id="${id}" cx="50%" cy="50%" r="50%">
337
+ <stop offset="0%" stop-color="#ffff00"/>
338
+ <stop offset="50%" stop-color="#ff8800"/>
339
+ <stop offset="100%" stop-color="#ff0000"/>
340
+ </radialGradient>
341
+ </defs>
342
+ <rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
343
+ fill="url(#${id})" rx="1"/>
344
+ </svg>`;
345
+ }
346
+ function createHillshadeSymbol(size) {
347
+ const padding = 2;
348
+ const id = `hillshadeGrad_${Math.random().toString(36).slice(2, 9)}`;
349
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
350
+ <defs>
351
+ <linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="100%">
352
+ <stop offset="0%" stop-color="#ffffff"/>
353
+ <stop offset="100%" stop-color="#666666"/>
354
+ </linearGradient>
355
+ </defs>
356
+ <rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
357
+ fill="url(#${id})" rx="1"/>
358
+ </svg>`;
359
+ }
360
+ function createFillExtrusionSymbol(size, color) {
361
+ const borderColor = darkenColor(color, 0.3);
362
+ const topColor = color;
363
+ const sideColor = darkenColor(color, 0.2);
364
+ const depth = 3;
365
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
366
+ <polygon points="${2 + depth},2 ${size - 2},2 ${size - 2},${size - 2 - depth} ${size - 2 - depth},${size - 2} 2,${size - 2} 2,${2 + depth}"
367
+ fill="${topColor}" stroke="${borderColor}" stroke-width="1"/>
368
+ <polygon points="2,${2 + depth} ${2 + depth},2 ${2 + depth},${size - 2 - depth} 2,${size - 2}"
369
+ fill="${sideColor}" stroke="${borderColor}" stroke-width="0.5"/>
370
+ <polygon points="${2 + depth},${size - 2 - depth} ${size - 2},${size - 2 - depth} ${size - 2 - depth},${size - 2} 2,${size - 2}"
371
+ fill="${sideColor}" stroke="${borderColor}" stroke-width="0.5"/>
372
+ </svg>`;
373
+ }
374
+ function createDefaultSymbol(size, color) {
375
+ const padding = 2;
376
+ const borderColor = darkenColor(color, 0.3);
377
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
378
+ <rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
379
+ fill="${color}" stroke="${borderColor}" stroke-width="1"/>
380
+ </svg>`;
381
+ }
382
+ function createStackedLayersSymbol(size) {
383
+ const colors = ["#a8d4a8", "#8ec4e8", "#d4c4a8"];
384
+ const borderColor = "#666666";
385
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
386
+ <rect x="4" y="1" width="${size - 6}" height="${size - 6}" fill="${colors[2]}" stroke="${borderColor}" stroke-width="0.75" rx="1"/>
387
+ <rect x="2" y="3" width="${size - 6}" height="${size - 6}" fill="${colors[1]}" stroke="${borderColor}" stroke-width="0.75" rx="1"/>
388
+ <rect x="0" y="5" width="${size - 6}" height="${size - 6}" fill="${colors[0]}" stroke="${borderColor}" stroke-width="0.75" rx="1"/>
389
+ </svg>`;
390
+ }
391
+ function createLayerSymbolSVG(layerType, color, options = {}) {
392
+ const size = options.size || 16;
393
+ const strokeWidth = options.strokeWidth || 2;
394
+ const fillColor = color || "#888888";
395
+ switch (layerType) {
396
+ case "fill":
397
+ return createFillSymbol(size, fillColor);
398
+ case "line":
399
+ return createLineSymbol(size, fillColor, strokeWidth);
400
+ case "circle":
401
+ return createCircleSymbol(size, fillColor);
402
+ case "symbol":
403
+ return createMarkerSymbol(size, fillColor);
404
+ case "raster":
405
+ return createRasterSymbol(size);
406
+ case "background":
407
+ return createBackgroundSymbol(size, fillColor);
408
+ case "heatmap":
409
+ return createHeatmapSymbol(size);
410
+ case "hillshade":
411
+ return createHillshadeSymbol(size);
412
+ case "fill-extrusion":
413
+ return createFillExtrusionSymbol(size, fillColor);
414
+ case "background-group":
415
+ return createStackedLayersSymbol(size);
416
+ default:
417
+ return createDefaultSymbol(size, fillColor);
418
+ }
419
+ }
420
+ function createBackgroundGroupSymbolSVG(size = 16) {
421
+ return createStackedLayersSymbol(size);
422
+ }
171
423
  class LayerControl {
172
424
  constructor(options = {}) {
173
425
  __publicField(this, "map");
@@ -181,6 +433,11 @@ class LayerControl {
181
433
  // Panel width management
182
434
  __publicField(this, "minPanelWidth");
183
435
  __publicField(this, "maxPanelWidth");
436
+ __publicField(this, "maxPanelHeight");
437
+ __publicField(this, "showStyleEditor");
438
+ __publicField(this, "showOpacitySlider");
439
+ __publicField(this, "showLayerSymbol");
440
+ __publicField(this, "excludeDrawnLayers");
184
441
  __publicField(this, "widthSliderEl", null);
185
442
  __publicField(this, "widthThumbEl", null);
186
443
  __publicField(this, "widthValueEl", null);
@@ -191,6 +448,11 @@ class LayerControl {
191
448
  __publicField(this, "widthFrame", null);
192
449
  this.minPanelWidth = options.panelMinWidth || 240;
193
450
  this.maxPanelWidth = options.panelMaxWidth || 420;
451
+ this.maxPanelHeight = options.panelMaxHeight || 600;
452
+ this.showStyleEditor = options.showStyleEditor !== false;
453
+ this.showOpacitySlider = options.showOpacitySlider !== false;
454
+ this.showLayerSymbol = options.showLayerSymbol !== false;
455
+ this.excludeDrawnLayers = options.excludeDrawnLayers !== false;
194
456
  this.state = {
195
457
  collapsed: options.collapsed !== false,
196
458
  panelWidth: options.panelWidth || 320,
@@ -240,7 +502,31 @@ class LayerControl {
240
502
  }
241
503
  const allLayerIds = style.layers.map((layer) => layer.id);
242
504
  if (this.targetLayers.length === 0) {
505
+ const originalSourceIds = this.getOriginalStyleSourceIds();
506
+ const userAddedLayers = [];
507
+ const backgroundLayerIds = [];
243
508
  allLayerIds.forEach((layerId) => {
509
+ const layer = this.map.getLayer(layerId);
510
+ if (!layer) return;
511
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
512
+ backgroundLayerIds.push(layerId);
513
+ return;
514
+ }
515
+ const sourceId = layer.source;
516
+ if (!sourceId || originalSourceIds.has(sourceId)) {
517
+ backgroundLayerIds.push(layerId);
518
+ } else {
519
+ userAddedLayers.push(layerId);
520
+ }
521
+ });
522
+ if (backgroundLayerIds.length > 0) {
523
+ this.state.layerStates["Background"] = {
524
+ visible: true,
525
+ opacity: 1,
526
+ name: "Background"
527
+ };
528
+ }
529
+ userAddedLayers.forEach((layerId) => {
244
530
  const layer = this.map.getLayer(layerId);
245
531
  if (!layer) return;
246
532
  const visibility = this.map.getLayoutProperty(layerId, "visibility");
@@ -288,6 +574,60 @@ class LayerControl {
288
574
  }
289
575
  this.targetLayers = Object.keys(this.state.layerStates);
290
576
  }
577
+ /**
578
+ * Get the source IDs that were part of the original style (from the style URL)
579
+ * Sources added via map.addSource() are considered user-added
580
+ */
581
+ getOriginalStyleSourceIds() {
582
+ const originalSourceIds = /* @__PURE__ */ new Set();
583
+ const style = this.map.getStyle();
584
+ if (!style || !style.sources) return originalSourceIds;
585
+ const spriteUrl = style.sprite;
586
+ const glyphsUrl = style.glyphs;
587
+ let styleBaseDomain = "";
588
+ if (spriteUrl) {
589
+ try {
590
+ const url = new URL(typeof spriteUrl === "string" ? spriteUrl : "");
591
+ styleBaseDomain = url.hostname;
592
+ } catch {
593
+ }
594
+ } else if (glyphsUrl) {
595
+ try {
596
+ const url = new URL(glyphsUrl.replace("{fontstack}", "test").replace("{range}", "test"));
597
+ styleBaseDomain = url.hostname;
598
+ } catch {
599
+ }
600
+ }
601
+ Object.entries(style.sources).forEach(([sourceId, source]) => {
602
+ const src = source;
603
+ let sourceUrl = src.url || src.tiles && src.tiles[0] || "";
604
+ if (sourceUrl) {
605
+ try {
606
+ const url = new URL(sourceUrl);
607
+ if (styleBaseDomain && url.hostname === styleBaseDomain) {
608
+ originalSourceIds.add(sourceId);
609
+ return;
610
+ }
611
+ const basemapDomains = [
612
+ "demotiles.maplibre.org",
613
+ "api.maptiler.com",
614
+ "tiles.stadiamaps.com",
615
+ "api.mapbox.com",
616
+ "basemaps.cartocdn.com"
617
+ ];
618
+ if (basemapDomains.some((domain) => url.hostname.includes(domain))) {
619
+ originalSourceIds.add(sourceId);
620
+ return;
621
+ }
622
+ } catch {
623
+ }
624
+ }
625
+ if (!src.data && !sourceUrl && src.type !== "geojson") {
626
+ originalSourceIds.add(sourceId);
627
+ }
628
+ });
629
+ return originalSourceIds;
630
+ }
291
631
  /**
292
632
  * Generate a friendly display name from a layer ID
293
633
  */
@@ -297,6 +637,28 @@ class LayerControl {
297
637
  name = name.replace(/\b\w/g, (char) => char.toUpperCase());
298
638
  return name || layerId;
299
639
  }
640
+ /**
641
+ * Check if a layer ID belongs to a drawing library (Geoman, Mapbox GL Draw, etc.)
642
+ * @param layerId The layer ID to check
643
+ * @returns true if the layer is from a drawing library
644
+ */
645
+ isDrawnLayer(layerId) {
646
+ const drawnLayerPatterns = [
647
+ /^gm[-_]/i,
648
+ // Geoman (gm-main-*, gm_*)
649
+ /^gl-draw[-_]/i,
650
+ // Mapbox GL Draw
651
+ /^mapbox-gl-draw[-_]/i,
652
+ // Mapbox GL Draw alternative
653
+ /^terra-draw[-_]/i,
654
+ // Terra Draw
655
+ /^maplibre-gl-draw[-_]/i,
656
+ // MapLibre GL Draw
657
+ /^draw[-_]layer/i
658
+ // Generic draw layers
659
+ ];
660
+ return drawnLayerPatterns.some((pattern) => pattern.test(layerId));
661
+ }
300
662
  /**
301
663
  * Create the main container element
302
664
  */
@@ -326,13 +688,59 @@ class LayerControl {
326
688
  const panel = document.createElement("div");
327
689
  panel.className = "layer-control-panel";
328
690
  panel.style.width = `${this.state.panelWidth}px`;
691
+ panel.style.maxHeight = `${this.maxPanelHeight}px`;
329
692
  if (!this.state.collapsed) {
330
693
  panel.classList.add("expanded");
331
694
  }
332
695
  const header = this.createPanelHeader();
333
696
  panel.appendChild(header);
697
+ const actionButtons = this.createActionButtons();
698
+ panel.appendChild(actionButtons);
334
699
  return panel;
335
700
  }
701
+ /**
702
+ * Create action buttons for Show All / Hide All
703
+ */
704
+ createActionButtons() {
705
+ const container = document.createElement("div");
706
+ container.className = "layer-control-actions";
707
+ const showAllBtn = document.createElement("button");
708
+ showAllBtn.type = "button";
709
+ showAllBtn.className = "layer-control-action-btn";
710
+ showAllBtn.textContent = "Show All";
711
+ showAllBtn.title = "Show all layers";
712
+ showAllBtn.addEventListener("click", () => this.setAllLayersVisibility(true));
713
+ const hideAllBtn = document.createElement("button");
714
+ hideAllBtn.type = "button";
715
+ hideAllBtn.className = "layer-control-action-btn";
716
+ hideAllBtn.textContent = "Hide All";
717
+ hideAllBtn.title = "Hide all layers";
718
+ hideAllBtn.addEventListener("click", () => this.setAllLayersVisibility(false));
719
+ container.appendChild(showAllBtn);
720
+ container.appendChild(hideAllBtn);
721
+ return container;
722
+ }
723
+ /**
724
+ * Set visibility of all layers
725
+ */
726
+ setAllLayersVisibility(visible) {
727
+ Object.keys(this.state.layerStates).forEach((layerId) => {
728
+ if (layerId === "Background") {
729
+ this.toggleBackgroundVisibility(visible);
730
+ } else {
731
+ this.state.layerStates[layerId].visible = visible;
732
+ this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
733
+ }
734
+ const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
735
+ if (itemEl) {
736
+ const checkbox = itemEl.querySelector(".layer-control-checkbox");
737
+ if (checkbox) {
738
+ checkbox.checked = visible;
739
+ checkbox.indeterminate = false;
740
+ }
741
+ }
742
+ });
743
+ }
336
744
  /**
337
745
  * Create the panel header with title and width control
338
746
  */
@@ -506,7 +914,11 @@ class LayerControl {
506
914
  this.widthSliderEl.setAttribute("aria-valuenow", String(this.state.panelWidth));
507
915
  const ratio = (this.state.panelWidth - this.minPanelWidth) / (this.maxPanelWidth - this.minPanelWidth || 1);
508
916
  if (this.widthThumbEl) {
509
- const sliderWidth = this.widthSliderEl.clientWidth || 1;
917
+ const sliderWidth = this.widthSliderEl.clientWidth;
918
+ if (sliderWidth === 0) {
919
+ requestAnimationFrame(() => this.updateWidthDisplay());
920
+ return;
921
+ }
510
922
  const thumbWidth = this.widthThumbEl.offsetWidth || 14;
511
923
  const padding = 16;
512
924
  const available = Math.max(0, sliderWidth - padding - thumbWidth);
@@ -611,39 +1023,98 @@ class LayerControl {
611
1023
  name.className = "layer-control-name";
612
1024
  name.textContent = state.name || layerId;
613
1025
  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
1026
  row.appendChild(checkbox);
1027
+ if (this.showLayerSymbol) {
1028
+ if (layerId === "Background") {
1029
+ const symbol = this.createBackgroundGroupSymbol();
1030
+ row.appendChild(symbol);
1031
+ } else {
1032
+ const symbol = this.createLayerSymbol(layerId);
1033
+ if (symbol) {
1034
+ row.appendChild(symbol);
1035
+ }
1036
+ }
1037
+ }
633
1038
  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);
1039
+ if (this.showOpacitySlider) {
1040
+ const opacity = document.createElement("input");
1041
+ opacity.type = "range";
1042
+ opacity.className = "layer-control-opacity";
1043
+ opacity.min = "0";
1044
+ opacity.max = "1";
1045
+ opacity.step = "0.01";
1046
+ opacity.value = String(state.opacity);
1047
+ opacity.title = `Opacity: ${Math.round(state.opacity * 100)}%`;
1048
+ opacity.addEventListener("mousedown", () => {
1049
+ this.state.userInteractingWithSlider = true;
1050
+ });
1051
+ opacity.addEventListener("mouseup", () => {
1052
+ this.state.userInteractingWithSlider = false;
1053
+ });
1054
+ opacity.addEventListener("input", () => {
1055
+ this.changeLayerOpacity(layerId, parseFloat(opacity.value));
1056
+ opacity.title = `Opacity: ${Math.round(parseFloat(opacity.value) * 100)}%`;
1057
+ });
1058
+ row.appendChild(opacity);
1059
+ }
1060
+ if (this.showStyleEditor) {
1061
+ if (layerId === "Background") {
1062
+ const legendButton = this.createBackgroundLegendButton();
1063
+ row.appendChild(legendButton);
1064
+ } else {
1065
+ const styleButton = this.createStyleButton(layerId);
1066
+ if (styleButton) {
1067
+ row.appendChild(styleButton);
1068
+ }
642
1069
  }
643
1070
  }
644
1071
  item.appendChild(row);
645
1072
  this.panel.appendChild(item);
646
1073
  }
1074
+ /**
1075
+ * Create a symbol element for a layer
1076
+ * @param layerId The layer ID
1077
+ * @returns The symbol HTML element, or null if layer not found
1078
+ */
1079
+ createLayerSymbol(layerId) {
1080
+ const layer = this.map.getLayer(layerId);
1081
+ if (!layer) return null;
1082
+ const layerType = layer.type;
1083
+ const color = getLayerColor(this.map, layerId, layerType);
1084
+ const svgMarkup = createLayerSymbolSVG(layerType, color);
1085
+ const symbolContainer = document.createElement("span");
1086
+ symbolContainer.className = "layer-control-symbol";
1087
+ symbolContainer.innerHTML = svgMarkup;
1088
+ symbolContainer.title = `Layer type: ${layerType}`;
1089
+ return symbolContainer;
1090
+ }
1091
+ /**
1092
+ * Create a symbol element for a background layer
1093
+ * @param layer The layer specification
1094
+ * @returns The symbol HTML element
1095
+ */
1096
+ createBackgroundLayerSymbol(layer) {
1097
+ const color = getLayerColorFromSpec(layer);
1098
+ const svgMarkup = createLayerSymbolSVG(layer.type, color, { size: 14 });
1099
+ const symbolContainer = document.createElement("span");
1100
+ symbolContainer.className = "background-legend-layer-symbol";
1101
+ symbolContainer.innerHTML = svgMarkup;
1102
+ symbolContainer.title = `Layer type: ${layer.type}`;
1103
+ return symbolContainer;
1104
+ }
1105
+ /**
1106
+ * Create a symbol element for the Background layer group
1107
+ * Shows a stacked layers icon to represent multiple background layers
1108
+ * @returns The symbol HTML element
1109
+ */
1110
+ createBackgroundGroupSymbol() {
1111
+ const svgMarkup = createBackgroundGroupSymbolSVG(16);
1112
+ const symbolContainer = document.createElement("span");
1113
+ symbolContainer.className = "layer-control-symbol";
1114
+ symbolContainer.innerHTML = svgMarkup;
1115
+ symbolContainer.title = "Background layers";
1116
+ return symbolContainer;
1117
+ }
647
1118
  /**
648
1119
  * Toggle layer visibility
649
1120
  */
@@ -908,6 +1379,12 @@ class LayerControl {
908
1379
  typeIndicator.className = "background-legend-layer-type";
909
1380
  typeIndicator.textContent = layer.type;
910
1381
  layerRow.appendChild(checkbox);
1382
+ if (this.showLayerSymbol) {
1383
+ const symbol = this.createBackgroundLayerSymbol(layer);
1384
+ if (symbol) {
1385
+ layerRow.appendChild(symbol);
1386
+ }
1387
+ }
911
1388
  layerRow.appendChild(name);
912
1389
  layerRow.appendChild(typeIndicator);
913
1390
  container.appendChild(layerRow);
@@ -1380,7 +1857,7 @@ class LayerControl {
1380
1857
  });
1381
1858
  }
1382
1859
  /**
1383
- * Check for new layers and add them to the control
1860
+ * Check for new layers and add them to the control, remove deleted layers
1384
1861
  */
1385
1862
  checkForNewLayers() {
1386
1863
  try {
@@ -1388,21 +1865,46 @@ class LayerControl {
1388
1865
  if (!style || !style.layers) {
1389
1866
  return;
1390
1867
  }
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
- });
1868
+ const currentMapLayerIds = new Set(style.layers.map((layer) => layer.id));
1869
+ const originalSourceIds = this.getOriginalStyleSourceIds();
1870
+ 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
1871
  const newLayers = [];
1398
- currentMapLayers.forEach((layerId) => {
1872
+ currentMapLayerIds.forEach((layerId) => {
1399
1873
  if (layerId !== "Background" && !this.state.layerStates[layerId]) {
1400
1874
  const layer = this.map.getLayer(layerId);
1401
1875
  if (layer) {
1876
+ if (isAutoDetectMode) {
1877
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
1878
+ return;
1879
+ }
1880
+ const sourceId = layer.source;
1881
+ if (!sourceId || originalSourceIds.has(sourceId)) {
1882
+ return;
1883
+ }
1884
+ }
1402
1885
  newLayers.push(layerId);
1403
1886
  }
1404
1887
  }
1405
1888
  });
1889
+ const removedLayers = [];
1890
+ Object.keys(this.state.layerStates).forEach((layerId) => {
1891
+ if (layerId !== "Background" && !currentMapLayerIds.has(layerId)) {
1892
+ removedLayers.push(layerId);
1893
+ }
1894
+ });
1895
+ if (removedLayers.length > 0) {
1896
+ removedLayers.forEach((layerId) => {
1897
+ delete this.state.layerStates[layerId];
1898
+ const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
1899
+ if (itemEl) {
1900
+ itemEl.remove();
1901
+ }
1902
+ if (this.state.activeStyleEditor === layerId) {
1903
+ this.state.activeStyleEditor = null;
1904
+ }
1905
+ this.styleEditors.delete(layerId);
1906
+ });
1907
+ }
1406
1908
  if (newLayers.length > 0) {
1407
1909
  newLayers.forEach((layerId) => {
1408
1910
  const layer = this.map.getLayer(layerId);
@@ -1414,7 +1916,7 @@ class LayerControl {
1414
1916
  this.state.layerStates[layerId] = {
1415
1917
  visible: isVisible,
1416
1918
  opacity,
1417
- name: layerId.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
1919
+ name: this.generateFriendlyName(layerId)
1418
1920
  };
1419
1921
  this.addLayerItem(layerId, this.state.layerStates[layerId]);
1420
1922
  });
@@ -1426,7 +1928,12 @@ class LayerControl {
1426
1928
  }
1427
1929
  exports.LayerControl = LayerControl;
1428
1930
  exports.clamp = clamp;
1931
+ exports.createBackgroundGroupSymbolSVG = createBackgroundGroupSymbolSVG;
1932
+ exports.createLayerSymbolSVG = createLayerSymbolSVG;
1933
+ exports.darkenColor = darkenColor;
1429
1934
  exports.formatNumericValue = formatNumericValue;
1935
+ exports.getLayerColor = getLayerColor;
1936
+ exports.getLayerColorFromSpec = getLayerColorFromSpec;
1430
1937
  exports.getLayerOpacity = getLayerOpacity;
1431
1938
  exports.getLayerType = getLayerType;
1432
1939
  exports.isStyleableLayerType = isStyleableLayerType;