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.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
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|