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/README.md +33 -0
- package/dist/index.cjs +543 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +543 -36
- package/dist/index.mjs.map +1 -1
- package/dist/maplibre-gl-layer-control.css +75 -6
- package/dist/types/index.d.ts +104 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|