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