maplibre-gl-layer-control 0.4.0 → 0.5.1

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,8 +431,11 @@ class LayerControl {
179
431
  // Panel width management
180
432
  __publicField(this, "minPanelWidth");
181
433
  __publicField(this, "maxPanelWidth");
434
+ __publicField(this, "maxPanelHeight");
182
435
  __publicField(this, "showStyleEditor");
183
436
  __publicField(this, "showOpacitySlider");
437
+ __publicField(this, "showLayerSymbol");
438
+ __publicField(this, "excludeDrawnLayers");
184
439
  __publicField(this, "widthSliderEl", null);
185
440
  __publicField(this, "widthThumbEl", null);
186
441
  __publicField(this, "widthValueEl", null);
@@ -191,8 +446,11 @@ class LayerControl {
191
446
  __publicField(this, "widthFrame", null);
192
447
  this.minPanelWidth = options.panelMinWidth || 240;
193
448
  this.maxPanelWidth = options.panelMaxWidth || 420;
449
+ this.maxPanelHeight = options.panelMaxHeight || 600;
194
450
  this.showStyleEditor = options.showStyleEditor !== false;
195
451
  this.showOpacitySlider = options.showOpacitySlider !== false;
452
+ this.showLayerSymbol = options.showLayerSymbol !== false;
453
+ this.excludeDrawnLayers = options.excludeDrawnLayers !== false;
196
454
  this.state = {
197
455
  collapsed: options.collapsed !== false,
198
456
  panelWidth: options.panelWidth || 320,
@@ -248,6 +506,10 @@ class LayerControl {
248
506
  allLayerIds.forEach((layerId) => {
249
507
  const layer = this.map.getLayer(layerId);
250
508
  if (!layer) return;
509
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
510
+ backgroundLayerIds.push(layerId);
511
+ return;
512
+ }
251
513
  const sourceId = layer.source;
252
514
  if (!sourceId || originalSourceIds.has(sourceId)) {
253
515
  backgroundLayerIds.push(layerId);
@@ -373,6 +635,28 @@ class LayerControl {
373
635
  name = name.replace(/\b\w/g, (char) => char.toUpperCase());
374
636
  return name || layerId;
375
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[-_\s]/i,
646
+ // Geoman (gm-main-*, gm_*, Gm Temporary...)
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
+ }
376
660
  /**
377
661
  * Create the main container element
378
662
  */
@@ -402,6 +686,7 @@ class LayerControl {
402
686
  const panel = document.createElement("div");
403
687
  panel.className = "layer-control-panel";
404
688
  panel.style.width = `${this.state.panelWidth}px`;
689
+ panel.style.maxHeight = `${this.maxPanelHeight}px`;
405
690
  if (!this.state.collapsed) {
406
691
  panel.classList.add("expanded");
407
692
  }
@@ -737,6 +1022,17 @@ class LayerControl {
737
1022
  name.textContent = state.name || layerId;
738
1023
  name.title = state.name || layerId;
739
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
+ }
740
1036
  row.appendChild(name);
741
1037
  if (this.showOpacitySlider) {
742
1038
  const opacity = document.createElement("input");
@@ -773,6 +1069,50 @@ class LayerControl {
773
1069
  item.appendChild(row);
774
1070
  this.panel.appendChild(item);
775
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
+ }
776
1116
  /**
777
1117
  * Toggle layer visibility
778
1118
  */
@@ -1013,6 +1353,9 @@ class LayerControl {
1013
1353
  const styleLayers = this.map.getStyle().layers || [];
1014
1354
  styleLayers.forEach((layer) => {
1015
1355
  if (!this.isUserAddedLayer(layer.id)) {
1356
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layer.id)) {
1357
+ return;
1358
+ }
1016
1359
  if (this.state.onlyRenderedFilter && !this.isLayerRendered(layer.id)) {
1017
1360
  return;
1018
1361
  }
@@ -1037,6 +1380,12 @@ class LayerControl {
1037
1380
  typeIndicator.className = "background-legend-layer-type";
1038
1381
  typeIndicator.textContent = layer.type;
1039
1382
  layerRow.appendChild(checkbox);
1383
+ if (this.showLayerSymbol) {
1384
+ const symbol = this.createBackgroundLayerSymbol(layer);
1385
+ if (symbol) {
1386
+ layerRow.appendChild(symbol);
1387
+ }
1388
+ }
1040
1389
  layerRow.appendChild(name);
1041
1390
  layerRow.appendChild(typeIndicator);
1042
1391
  container.appendChild(layerRow);
@@ -1526,6 +1875,9 @@ class LayerControl {
1526
1875
  const layer = this.map.getLayer(layerId);
1527
1876
  if (layer) {
1528
1877
  if (isAutoDetectMode) {
1878
+ if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
1879
+ return;
1880
+ }
1529
1881
  const sourceId = layer.source;
1530
1882
  if (!sourceId || originalSourceIds.has(sourceId)) {
1531
1883
  return;
@@ -1578,7 +1930,12 @@ class LayerControl {
1578
1930
  export {
1579
1931
  LayerControl,
1580
1932
  clamp,
1933
+ createBackgroundGroupSymbolSVG,
1934
+ createLayerSymbolSVG,
1935
+ darkenColor,
1581
1936
  formatNumericValue,
1937
+ getLayerColor,
1938
+ getLayerColorFromSpec,
1582
1939
  getLayerOpacity,
1583
1940
  getLayerType,
1584
1941
  isStyleableLayerType,