maplibre-gl-layer-control-tidop 0.0.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/LICENSE +21 -0
- package/README.md +506 -0
- package/dist/index.cjs +3499 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +3499 -0
- package/dist/index.mjs.map +1 -0
- package/dist/maplibre-gl-layer-control-tidop.css +1088 -0
- package/dist/types/index.d.ts +796 -0
- package/package.json +93 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3499 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
class CustomLayerRegistry {
|
|
5
|
+
constructor() {
|
|
6
|
+
__publicField(this, "adapters", /* @__PURE__ */ new Map());
|
|
7
|
+
__publicField(this, "changeListeners", []);
|
|
8
|
+
__publicField(this, "unsubscribers", /* @__PURE__ */ new Map());
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Register a custom layer adapter.
|
|
12
|
+
* @param adapter The adapter to register
|
|
13
|
+
*/
|
|
14
|
+
register(adapter) {
|
|
15
|
+
this.adapters.set(adapter.type, adapter);
|
|
16
|
+
if (adapter.onLayerChange) {
|
|
17
|
+
const unsubscribe = adapter.onLayerChange((event, layerId) => {
|
|
18
|
+
this.notifyChange(event, layerId);
|
|
19
|
+
});
|
|
20
|
+
this.unsubscribers.set(adapter.type, unsubscribe);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Unregister an adapter by type.
|
|
25
|
+
* @param type The adapter type to unregister
|
|
26
|
+
*/
|
|
27
|
+
unregister(type) {
|
|
28
|
+
const unsubscribe = this.unsubscribers.get(type);
|
|
29
|
+
if (unsubscribe) {
|
|
30
|
+
unsubscribe();
|
|
31
|
+
this.unsubscribers.delete(type);
|
|
32
|
+
}
|
|
33
|
+
this.adapters.delete(type);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get all custom layer IDs across all adapters.
|
|
37
|
+
* @returns Array of layer IDs
|
|
38
|
+
*/
|
|
39
|
+
getAllLayerIds() {
|
|
40
|
+
const ids = [];
|
|
41
|
+
this.adapters.forEach((adapter) => {
|
|
42
|
+
ids.push(...adapter.getLayerIds());
|
|
43
|
+
});
|
|
44
|
+
return ids;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if a layer ID is managed by any adapter.
|
|
48
|
+
* @param layerId The layer ID to check
|
|
49
|
+
* @returns true if the layer is managed by an adapter
|
|
50
|
+
*/
|
|
51
|
+
hasLayer(layerId) {
|
|
52
|
+
for (const adapter of this.adapters.values()) {
|
|
53
|
+
if (adapter.getLayerIds().includes(layerId)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the adapter responsible for a specific layer.
|
|
61
|
+
* @param layerId The layer ID
|
|
62
|
+
* @returns The adapter or null if not found
|
|
63
|
+
*/
|
|
64
|
+
getAdapterForLayer(layerId) {
|
|
65
|
+
for (const adapter of this.adapters.values()) {
|
|
66
|
+
if (adapter.getLayerIds().includes(layerId)) {
|
|
67
|
+
return adapter;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get the state of a custom layer.
|
|
74
|
+
* @param layerId The layer ID
|
|
75
|
+
* @returns The layer state or null if not found
|
|
76
|
+
*/
|
|
77
|
+
getLayerState(layerId) {
|
|
78
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
79
|
+
return adapter ? adapter.getLayerState(layerId) : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Set visibility of a custom layer.
|
|
83
|
+
* @param layerId The layer ID
|
|
84
|
+
* @param visible Whether the layer should be visible
|
|
85
|
+
* @returns true if the operation was handled by an adapter
|
|
86
|
+
*/
|
|
87
|
+
setVisibility(layerId, visible) {
|
|
88
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
89
|
+
if (adapter) {
|
|
90
|
+
adapter.setVisibility(layerId, visible);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set opacity of a custom layer.
|
|
97
|
+
* @param layerId The layer ID
|
|
98
|
+
* @param opacity The opacity value (0-1)
|
|
99
|
+
* @returns true if the operation was handled by an adapter
|
|
100
|
+
*/
|
|
101
|
+
setOpacity(layerId, opacity) {
|
|
102
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
103
|
+
if (adapter) {
|
|
104
|
+
adapter.setOpacity(layerId, opacity);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the symbol type for a custom layer (for UI display).
|
|
111
|
+
* @param layerId The layer ID
|
|
112
|
+
* @returns The symbol type or null if not available
|
|
113
|
+
*/
|
|
114
|
+
getSymbolType(layerId) {
|
|
115
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
116
|
+
if (adapter && adapter.getSymbolType) {
|
|
117
|
+
return adapter.getSymbolType(layerId);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the bounds of a custom layer (for zoom-to-layer).
|
|
123
|
+
* @param layerId The layer ID
|
|
124
|
+
* @returns The bounds [west, south, east, north] or null if not available
|
|
125
|
+
*/
|
|
126
|
+
getBounds(layerId) {
|
|
127
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
128
|
+
if (adapter && adapter.getBounds) {
|
|
129
|
+
return adapter.getBounds(layerId);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get native MapLibre layer IDs for a custom layer.
|
|
135
|
+
* @param layerId The custom layer ID
|
|
136
|
+
* @returns Array of native layer IDs, or null if not available
|
|
137
|
+
*/
|
|
138
|
+
getNativeLayerIds(layerId) {
|
|
139
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
140
|
+
if (adapter && adapter.getNativeLayerIds) {
|
|
141
|
+
return adapter.getNativeLayerIds(layerId);
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Remove a custom layer through its adapter.
|
|
147
|
+
* @param layerId The layer ID to remove
|
|
148
|
+
* @returns true if the operation was handled by an adapter
|
|
149
|
+
*/
|
|
150
|
+
removeLayer(layerId) {
|
|
151
|
+
const adapter = this.getAdapterForLayer(layerId);
|
|
152
|
+
if (adapter && adapter.removeLayer) {
|
|
153
|
+
adapter.removeLayer(layerId);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Subscribe to layer changes across all adapters.
|
|
160
|
+
* @param callback Function called when layers are added or removed
|
|
161
|
+
* @returns Unsubscribe function
|
|
162
|
+
*/
|
|
163
|
+
onChange(callback) {
|
|
164
|
+
this.changeListeners.push(callback);
|
|
165
|
+
return () => {
|
|
166
|
+
const idx = this.changeListeners.indexOf(callback);
|
|
167
|
+
if (idx >= 0) {
|
|
168
|
+
this.changeListeners.splice(idx, 1);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
notifyChange(event, layerId) {
|
|
173
|
+
this.changeListeners.forEach((cb) => cb(event, layerId));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Clean up all subscriptions and adapters.
|
|
177
|
+
*/
|
|
178
|
+
destroy() {
|
|
179
|
+
this.unsubscribers.forEach((unsub) => unsub());
|
|
180
|
+
this.unsubscribers.clear();
|
|
181
|
+
this.adapters.clear();
|
|
182
|
+
this.changeListeners = [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function getOpacityProperty(layerType) {
|
|
186
|
+
switch (layerType) {
|
|
187
|
+
case "fill":
|
|
188
|
+
return "fill-opacity";
|
|
189
|
+
case "line":
|
|
190
|
+
return "line-opacity";
|
|
191
|
+
case "circle":
|
|
192
|
+
return "circle-opacity";
|
|
193
|
+
case "symbol":
|
|
194
|
+
return ["icon-opacity", "text-opacity"];
|
|
195
|
+
case "raster":
|
|
196
|
+
return "raster-opacity";
|
|
197
|
+
case "background":
|
|
198
|
+
return "background-opacity";
|
|
199
|
+
case "heatmap":
|
|
200
|
+
return "heatmap-opacity";
|
|
201
|
+
case "fill-extrusion":
|
|
202
|
+
return "fill-extrusion-opacity";
|
|
203
|
+
case "hillshade":
|
|
204
|
+
return "hillshade-exaggeration";
|
|
205
|
+
case "color-relief":
|
|
206
|
+
return null;
|
|
207
|
+
default:
|
|
208
|
+
return `${layerType}-opacity`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function getLayerOpacity(map, layerId, layerType) {
|
|
212
|
+
const opacityProp = getOpacityProperty(layerType);
|
|
213
|
+
if (opacityProp === null) {
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(opacityProp)) {
|
|
217
|
+
const opacity2 = map.getPaintProperty(layerId, opacityProp[0]);
|
|
218
|
+
return opacity2 !== void 0 && opacity2 !== null ? opacity2 : 1;
|
|
219
|
+
}
|
|
220
|
+
const opacity = map.getPaintProperty(layerId, opacityProp);
|
|
221
|
+
return opacity !== void 0 && opacity !== null ? opacity : 1;
|
|
222
|
+
}
|
|
223
|
+
function setLayerOpacity(map, layerId, layerType, opacity) {
|
|
224
|
+
const opacityProp = getOpacityProperty(layerType);
|
|
225
|
+
if (opacityProp === null) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(opacityProp)) {
|
|
229
|
+
opacityProp.forEach((prop) => {
|
|
230
|
+
map.setPaintProperty(layerId, prop, opacity);
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
map.setPaintProperty(layerId, opacityProp, opacity);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function isStyleableLayerType(layerType) {
|
|
237
|
+
return [
|
|
238
|
+
"fill",
|
|
239
|
+
"line",
|
|
240
|
+
"circle",
|
|
241
|
+
"symbol",
|
|
242
|
+
"raster",
|
|
243
|
+
"heatmap",
|
|
244
|
+
"fill-extrusion",
|
|
245
|
+
"hillshade"
|
|
246
|
+
].includes(layerType);
|
|
247
|
+
}
|
|
248
|
+
function getLayerType(map, layerId) {
|
|
249
|
+
try {
|
|
250
|
+
const layer = map.getLayer(layerId);
|
|
251
|
+
return layer ? layer.type : null;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.warn(`Failed to get layer type for ${layerId}:`, error);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function clonePaintValue(value) {
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return value.map((item) => clonePaintValue(item));
|
|
260
|
+
}
|
|
261
|
+
if (value && typeof value === "object") {
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(JSON.stringify(value));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
function cacheOriginalLayerStyle(map, layerId, originalStyles) {
|
|
271
|
+
var _a;
|
|
272
|
+
if (originalStyles.has(layerId)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const layer = map.getLayer(layerId);
|
|
277
|
+
if (!layer) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const paint = {};
|
|
281
|
+
const style = map.getStyle();
|
|
282
|
+
const layerDef = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
283
|
+
if (layer.type === "raster") {
|
|
284
|
+
const rasterDefaults = {
|
|
285
|
+
"raster-opacity": 1,
|
|
286
|
+
"raster-brightness-min": 0,
|
|
287
|
+
"raster-brightness-max": 1,
|
|
288
|
+
"raster-saturation": 0,
|
|
289
|
+
"raster-contrast": 0,
|
|
290
|
+
"raster-hue-rotate": 0
|
|
291
|
+
};
|
|
292
|
+
Object.assign(paint, rasterDefaults);
|
|
293
|
+
if (layerDef && "paint" in layerDef && layerDef.paint) {
|
|
294
|
+
Object.entries(layerDef.paint).forEach(([prop, value]) => {
|
|
295
|
+
if (prop.startsWith("raster-")) {
|
|
296
|
+
paint[prop] = clonePaintValue(value);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
if (layerDef && "paint" in layerDef && layerDef.paint) {
|
|
302
|
+
Object.entries(layerDef.paint).forEach(([prop, value]) => {
|
|
303
|
+
paint[prop] = clonePaintValue(value);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
originalStyles.set(layerId, { paint });
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.warn(`Failed to cache original style for ${layerId}:`, error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function restoreOriginalStyle(map, layerId, originalStyles) {
|
|
313
|
+
const original = originalStyles.get(layerId);
|
|
314
|
+
if (!original) {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
const applied = {};
|
|
318
|
+
Object.entries(original.paint).forEach(([property, value]) => {
|
|
319
|
+
try {
|
|
320
|
+
const restoredValue = clonePaintValue(value);
|
|
321
|
+
map.setPaintProperty(layerId, property, restoredValue);
|
|
322
|
+
applied[property] = restoredValue;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.warn(`Failed to restore ${property} for ${layerId}:`, error);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
return applied;
|
|
328
|
+
}
|
|
329
|
+
function rgbToHex(r, g, b) {
|
|
330
|
+
const clamp2 = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
331
|
+
const toHex = (v) => {
|
|
332
|
+
const hex = clamp2(v).toString(16);
|
|
333
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
|
334
|
+
};
|
|
335
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
336
|
+
}
|
|
337
|
+
function normalizeColor(value) {
|
|
338
|
+
if (typeof value === "string") {
|
|
339
|
+
if (value.startsWith("#")) {
|
|
340
|
+
if (value.length === 4) {
|
|
341
|
+
const r = value[1];
|
|
342
|
+
const g = value[2];
|
|
343
|
+
const b = value[3];
|
|
344
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
345
|
+
}
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
if (value.startsWith("rgb")) {
|
|
349
|
+
const match = value.match(/\d+/g);
|
|
350
|
+
if (match && match.length >= 3) {
|
|
351
|
+
const [r, g, b] = match.map((num) => parseInt(num, 10));
|
|
352
|
+
return rgbToHex(r, g, b);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (Array.isArray(value) && value.length >= 3) {
|
|
356
|
+
return rgbToHex(value[0], value[1], value[2]);
|
|
357
|
+
}
|
|
358
|
+
return "#3388ff";
|
|
359
|
+
}
|
|
360
|
+
function formatNumericValue(value, step) {
|
|
361
|
+
let decimals = 0;
|
|
362
|
+
if (step && Number(step) !== 1) {
|
|
363
|
+
const stepNumber = Number(step);
|
|
364
|
+
if (stepNumber > 0 && stepNumber < 1) {
|
|
365
|
+
decimals = Math.min(4, Math.ceil(Math.abs(Math.log10(stepNumber))));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return value.toFixed(decimals);
|
|
369
|
+
}
|
|
370
|
+
function clamp(value, min, max) {
|
|
371
|
+
return Math.max(min, Math.min(max, value));
|
|
372
|
+
}
|
|
373
|
+
const COLOR_PROPERTY_MAP = {
|
|
374
|
+
fill: ["fill-color", "fill-outline-color"],
|
|
375
|
+
line: ["line-color"],
|
|
376
|
+
circle: ["circle-color", "circle-stroke-color"],
|
|
377
|
+
symbol: ["icon-color", "text-color"],
|
|
378
|
+
background: ["background-color"],
|
|
379
|
+
heatmap: ["heatmap-color"],
|
|
380
|
+
"fill-extrusion": ["fill-extrusion-color"]
|
|
381
|
+
};
|
|
382
|
+
function extractColorFromExpression(expression) {
|
|
383
|
+
if (!Array.isArray(expression) || expression.length === 0) return null;
|
|
384
|
+
for (const item of expression) {
|
|
385
|
+
if (typeof item === "string") {
|
|
386
|
+
if (item.startsWith("#") || item.startsWith("rgb") || item.startsWith("hsl")) {
|
|
387
|
+
return normalizeColor(item);
|
|
388
|
+
}
|
|
389
|
+
} else if (Array.isArray(item)) {
|
|
390
|
+
const result = extractColorFromExpression(item);
|
|
391
|
+
if (result) return result;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function getLayerColor(map, layerId, layerType) {
|
|
397
|
+
var _a;
|
|
398
|
+
const propertyNames = COLOR_PROPERTY_MAP[layerType];
|
|
399
|
+
if (!propertyNames) return null;
|
|
400
|
+
for (const propertyName of propertyNames) {
|
|
401
|
+
try {
|
|
402
|
+
const runtimeColor = map.getPaintProperty(layerId, propertyName);
|
|
403
|
+
if (runtimeColor) {
|
|
404
|
+
if (typeof runtimeColor === "string") {
|
|
405
|
+
return normalizeColor(runtimeColor);
|
|
406
|
+
}
|
|
407
|
+
if (Array.isArray(runtimeColor)) {
|
|
408
|
+
const extracted = extractColorFromExpression(runtimeColor);
|
|
409
|
+
if (extracted) return extracted;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
const style = map.getStyle();
|
|
415
|
+
const layer = (_a = style == null ? void 0 : style.layers) == null ? void 0 : _a.find(
|
|
416
|
+
(l) => l.id === layerId
|
|
417
|
+
);
|
|
418
|
+
if (layer && "paint" in layer && layer.paint) {
|
|
419
|
+
const paintColor = layer.paint[propertyName];
|
|
420
|
+
if (paintColor) {
|
|
421
|
+
if (typeof paintColor === "string") {
|
|
422
|
+
return normalizeColor(paintColor);
|
|
423
|
+
}
|
|
424
|
+
if (Array.isArray(paintColor)) {
|
|
425
|
+
const extracted = extractColorFromExpression(paintColor);
|
|
426
|
+
if (extracted) return extracted;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
function getLayerColorFromSpec(layer) {
|
|
434
|
+
const propertyNames = COLOR_PROPERTY_MAP[layer.type];
|
|
435
|
+
if (!propertyNames) return null;
|
|
436
|
+
for (const propertyName of propertyNames) {
|
|
437
|
+
if ("paint" in layer && layer.paint) {
|
|
438
|
+
const paintColor = layer.paint[propertyName];
|
|
439
|
+
if (paintColor) {
|
|
440
|
+
if (typeof paintColor === "string") {
|
|
441
|
+
return normalizeColor(paintColor);
|
|
442
|
+
}
|
|
443
|
+
if (Array.isArray(paintColor)) {
|
|
444
|
+
const extracted = extractColorFromExpression(paintColor);
|
|
445
|
+
if (extracted) return extracted;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
function darkenColor(hexColor, amount) {
|
|
453
|
+
let hex = hexColor.replace("#", "");
|
|
454
|
+
if (hex.length === 3) {
|
|
455
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
456
|
+
}
|
|
457
|
+
const r = Math.max(
|
|
458
|
+
0,
|
|
459
|
+
parseInt(hex.slice(0, 2), 16) - Math.round(255 * amount)
|
|
460
|
+
);
|
|
461
|
+
const g = Math.max(
|
|
462
|
+
0,
|
|
463
|
+
parseInt(hex.slice(2, 4), 16) - Math.round(255 * amount)
|
|
464
|
+
);
|
|
465
|
+
const b = Math.max(
|
|
466
|
+
0,
|
|
467
|
+
parseInt(hex.slice(4, 6), 16) - Math.round(255 * amount)
|
|
468
|
+
);
|
|
469
|
+
return rgbToHex(r, g, b);
|
|
470
|
+
}
|
|
471
|
+
function createFillSymbol(size, color) {
|
|
472
|
+
const padding = 2;
|
|
473
|
+
const borderColor = darkenColor(color, 0.3);
|
|
474
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
475
|
+
<rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
|
|
476
|
+
fill="${color}" stroke="${borderColor}" stroke-width="1" rx="1"/>
|
|
477
|
+
</svg>`;
|
|
478
|
+
}
|
|
479
|
+
function createLineSymbol(size, color, strokeWidth = 2) {
|
|
480
|
+
const y = size / 2;
|
|
481
|
+
const padding = 2;
|
|
482
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
483
|
+
<line x1="${padding}" y1="${y}" x2="${size - padding}" y2="${y}"
|
|
484
|
+
stroke="${color}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
|
|
485
|
+
</svg>`;
|
|
486
|
+
}
|
|
487
|
+
function createCircleSymbol(size, color) {
|
|
488
|
+
const cx = size / 2;
|
|
489
|
+
const cy = size / 2;
|
|
490
|
+
const r = size / 2 - 3;
|
|
491
|
+
const borderColor = darkenColor(color, 0.3);
|
|
492
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
493
|
+
<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"
|
|
494
|
+
stroke="${borderColor}" stroke-width="1"/>
|
|
495
|
+
</svg>`;
|
|
496
|
+
}
|
|
497
|
+
function createMarkerSymbol(size, color) {
|
|
498
|
+
const borderColor = darkenColor(color, 0.3);
|
|
499
|
+
const cx = size / 2;
|
|
500
|
+
const pinWidth = size * 0.5;
|
|
501
|
+
const pinHeight = size * 0.7;
|
|
502
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
503
|
+
<path d="M${cx} ${size - 2}
|
|
504
|
+
L${cx - pinWidth / 2} ${size - pinHeight}
|
|
505
|
+
A${pinWidth / 2} ${pinWidth / 2} 0 1 1 ${cx + pinWidth / 2} ${size - pinHeight}
|
|
506
|
+
Z"
|
|
507
|
+
fill="${color}" stroke="${borderColor}" stroke-width="1"/>
|
|
508
|
+
<circle cx="${cx}" cy="${size - pinHeight - pinWidth / 4}" r="${pinWidth / 5}" fill="white"/>
|
|
509
|
+
</svg>`;
|
|
510
|
+
}
|
|
511
|
+
function createRasterSymbol(size) {
|
|
512
|
+
const padding = 2;
|
|
513
|
+
const id = `rasterGrad_${Math.random().toString(36).slice(2, 9)}`;
|
|
514
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
515
|
+
<defs>
|
|
516
|
+
<linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
517
|
+
<stop offset="0%" stop-color="#e0e0e0"/>
|
|
518
|
+
<stop offset="50%" stop-color="#808080"/>
|
|
519
|
+
<stop offset="100%" stop-color="#404040"/>
|
|
520
|
+
</linearGradient>
|
|
521
|
+
</defs>
|
|
522
|
+
<rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
|
|
523
|
+
fill="url(#${id})" rx="1"/>
|
|
524
|
+
</svg>`;
|
|
525
|
+
}
|
|
526
|
+
function createBackgroundSymbol(size, color) {
|
|
527
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
528
|
+
<rect x="1" y="1" width="${size - 2}" height="${size - 2}" fill="${color}" rx="2"/>
|
|
529
|
+
<rect x="3" y="3" width="${size - 6}" height="${size - 6}" fill="none"
|
|
530
|
+
stroke="white" stroke-width="1" stroke-opacity="0.5" rx="1"/>
|
|
531
|
+
</svg>`;
|
|
532
|
+
}
|
|
533
|
+
function createHeatmapSymbol(size) {
|
|
534
|
+
const padding = 2;
|
|
535
|
+
const id = `heatmapGrad_${Math.random().toString(36).slice(2, 9)}`;
|
|
536
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
537
|
+
<defs>
|
|
538
|
+
<radialGradient id="${id}" cx="50%" cy="50%" r="50%">
|
|
539
|
+
<stop offset="0%" stop-color="#ffff00"/>
|
|
540
|
+
<stop offset="50%" stop-color="#ff8800"/>
|
|
541
|
+
<stop offset="100%" stop-color="#ff0000"/>
|
|
542
|
+
</radialGradient>
|
|
543
|
+
</defs>
|
|
544
|
+
<rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
|
|
545
|
+
fill="url(#${id})" rx="1"/>
|
|
546
|
+
</svg>`;
|
|
547
|
+
}
|
|
548
|
+
function createHillshadeSymbol(size) {
|
|
549
|
+
const padding = 2;
|
|
550
|
+
const id = `hillshadeGrad_${Math.random().toString(36).slice(2, 9)}`;
|
|
551
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
552
|
+
<defs>
|
|
553
|
+
<linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
554
|
+
<stop offset="0%" stop-color="#ffffff"/>
|
|
555
|
+
<stop offset="100%" stop-color="#666666"/>
|
|
556
|
+
</linearGradient>
|
|
557
|
+
</defs>
|
|
558
|
+
<rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
|
|
559
|
+
fill="url(#${id})" rx="1"/>
|
|
560
|
+
</svg>`;
|
|
561
|
+
}
|
|
562
|
+
function createFillExtrusionSymbol(size, color) {
|
|
563
|
+
const borderColor = darkenColor(color, 0.3);
|
|
564
|
+
const topColor = color;
|
|
565
|
+
const sideColor = darkenColor(color, 0.2);
|
|
566
|
+
const depth = 3;
|
|
567
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
568
|
+
<polygon points="${2 + depth},2 ${size - 2},2 ${size - 2},${size - 2 - depth} ${size - 2 - depth},${size - 2} 2,${size - 2} 2,${2 + depth}"
|
|
569
|
+
fill="${topColor}" stroke="${borderColor}" stroke-width="1"/>
|
|
570
|
+
<polygon points="2,${2 + depth} ${2 + depth},2 ${2 + depth},${size - 2 - depth} 2,${size - 2}"
|
|
571
|
+
fill="${sideColor}" stroke="${borderColor}" stroke-width="0.5"/>
|
|
572
|
+
<polygon points="${2 + depth},${size - 2 - depth} ${size - 2},${size - 2 - depth} ${size - 2 - depth},${size - 2} 2,${size - 2}"
|
|
573
|
+
fill="${sideColor}" stroke="${borderColor}" stroke-width="0.5"/>
|
|
574
|
+
</svg>`;
|
|
575
|
+
}
|
|
576
|
+
function createDefaultSymbol(size, color) {
|
|
577
|
+
const padding = 2;
|
|
578
|
+
const borderColor = darkenColor(color, 0.3);
|
|
579
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
580
|
+
<rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
|
|
581
|
+
fill="${color}" stroke="${borderColor}" stroke-width="1"/>
|
|
582
|
+
</svg>`;
|
|
583
|
+
}
|
|
584
|
+
function createCOGSymbol(size, color) {
|
|
585
|
+
const padding = 2;
|
|
586
|
+
const borderColor = darkenColor(color, 0.3);
|
|
587
|
+
const cellSize = (size - padding * 2) / 2;
|
|
588
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
589
|
+
<rect x="${padding}" y="${padding}" width="${cellSize}" height="${cellSize}" fill="${color}" stroke="${borderColor}" stroke-width="0.5"/>
|
|
590
|
+
<rect x="${padding + cellSize}" y="${padding}" width="${cellSize}" height="${cellSize}" fill="${color}" stroke="${borderColor}" stroke-width="0.5" opacity="0.8"/>
|
|
591
|
+
<rect x="${padding}" y="${padding + cellSize}" width="${cellSize}" height="${cellSize}" fill="${color}" stroke="${borderColor}" stroke-width="0.5" opacity="0.6"/>
|
|
592
|
+
<rect x="${padding + cellSize}" y="${padding + cellSize}" width="${cellSize}" height="${cellSize}" fill="${color}" stroke="${borderColor}" stroke-width="0.5" opacity="0.4"/>
|
|
593
|
+
</svg>`;
|
|
594
|
+
}
|
|
595
|
+
function createZarrSymbol(size, color) {
|
|
596
|
+
const padding = 2;
|
|
597
|
+
const borderColor = darkenColor(color, 0.3);
|
|
598
|
+
const innerSize = size - padding * 2;
|
|
599
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
600
|
+
<rect x="${padding + 2}" y="${padding}" width="${innerSize - 2}" height="${innerSize - 2}" fill="${darkenColor(color, 0.2)}" stroke="${borderColor}" stroke-width="0.5" rx="1"/>
|
|
601
|
+
<rect x="${padding + 1}" y="${padding + 1}" width="${innerSize - 2}" height="${innerSize - 2}" fill="${darkenColor(color, 0.1)}" stroke="${borderColor}" stroke-width="0.5" rx="1"/>
|
|
602
|
+
<rect x="${padding}" y="${padding + 2}" width="${innerSize - 2}" height="${innerSize - 2}" fill="${color}" stroke="${borderColor}" stroke-width="0.5" rx="1"/>
|
|
603
|
+
<line x1="${padding + 3}" y1="${padding + 5}" x2="${padding + innerSize - 5}" y2="${padding + 5}" stroke="${borderColor}" stroke-width="0.5" opacity="0.5"/>
|
|
604
|
+
<line x1="${padding + 3}" y1="${padding + 8}" x2="${padding + innerSize - 5}" y2="${padding + 8}" stroke="${borderColor}" stroke-width="0.5" opacity="0.5"/>
|
|
605
|
+
</svg>`;
|
|
606
|
+
}
|
|
607
|
+
function createCustomRasterSymbol(size, color) {
|
|
608
|
+
const padding = 2;
|
|
609
|
+
const borderColor = darkenColor(color, 0.3);
|
|
610
|
+
const id = `customRasterGrad_${Math.random().toString(36).slice(2, 9)}`;
|
|
611
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
612
|
+
<defs>
|
|
613
|
+
<linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
614
|
+
<stop offset="0%" stop-color="${color}"/>
|
|
615
|
+
<stop offset="100%" stop-color="${darkenColor(color, 0.4)}"/>
|
|
616
|
+
</linearGradient>
|
|
617
|
+
</defs>
|
|
618
|
+
<rect x="${padding}" y="${padding}" width="${size - padding * 2}" height="${size - padding * 2}"
|
|
619
|
+
fill="url(#${id})" stroke="${borderColor}" stroke-width="1" rx="1"/>
|
|
620
|
+
<line x1="${size / 2}" y1="${padding}" x2="${size / 2}" y2="${size - padding}" stroke="${borderColor}" stroke-width="0.5" opacity="0.3"/>
|
|
621
|
+
<line x1="${padding}" y1="${size / 2}" x2="${size - padding}" y2="${size / 2}" stroke="${borderColor}" stroke-width="0.5" opacity="0.3"/>
|
|
622
|
+
</svg>`;
|
|
623
|
+
}
|
|
624
|
+
function createStackedLayersSymbol(size) {
|
|
625
|
+
const colors = ["#a8d4a8", "#8ec4e8", "#d4c4a8"];
|
|
626
|
+
const borderColor = "#666666";
|
|
627
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
628
|
+
<rect x="4" y="1" width="${size - 6}" height="${size - 6}" fill="${colors[2]}" stroke="${borderColor}" stroke-width="0.75" rx="1"/>
|
|
629
|
+
<rect x="2" y="3" width="${size - 6}" height="${size - 6}" fill="${colors[1]}" stroke="${borderColor}" stroke-width="0.75" rx="1"/>
|
|
630
|
+
<rect x="0" y="5" width="${size - 6}" height="${size - 6}" fill="${colors[0]}" stroke="${borderColor}" stroke-width="0.75" rx="1"/>
|
|
631
|
+
</svg>`;
|
|
632
|
+
}
|
|
633
|
+
function createLayerSymbolSVG(layerType, color, options = {}) {
|
|
634
|
+
const size = options.size || 16;
|
|
635
|
+
const strokeWidth = options.strokeWidth || 2;
|
|
636
|
+
const fillColor = color || "#888888";
|
|
637
|
+
switch (layerType) {
|
|
638
|
+
case "fill":
|
|
639
|
+
return createFillSymbol(size, fillColor);
|
|
640
|
+
case "line":
|
|
641
|
+
return createLineSymbol(size, fillColor, strokeWidth);
|
|
642
|
+
case "circle":
|
|
643
|
+
return createCircleSymbol(size, fillColor);
|
|
644
|
+
case "symbol":
|
|
645
|
+
return createMarkerSymbol(size, fillColor);
|
|
646
|
+
case "raster":
|
|
647
|
+
return createRasterSymbol(size);
|
|
648
|
+
case "background":
|
|
649
|
+
return createBackgroundSymbol(size, fillColor);
|
|
650
|
+
case "heatmap":
|
|
651
|
+
return createHeatmapSymbol(size);
|
|
652
|
+
case "hillshade":
|
|
653
|
+
return createHillshadeSymbol(size);
|
|
654
|
+
case "fill-extrusion":
|
|
655
|
+
return createFillExtrusionSymbol(size, fillColor);
|
|
656
|
+
case "background-group":
|
|
657
|
+
return createStackedLayersSymbol(size);
|
|
658
|
+
case "cog":
|
|
659
|
+
return createCOGSymbol(size, fillColor);
|
|
660
|
+
case "zarr":
|
|
661
|
+
return createZarrSymbol(size, fillColor);
|
|
662
|
+
case "custom-raster":
|
|
663
|
+
return createCustomRasterSymbol(size, fillColor);
|
|
664
|
+
default:
|
|
665
|
+
return createDefaultSymbol(size, fillColor);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function createBackgroundGroupSymbolSVG(size = 16) {
|
|
669
|
+
return createStackedLayersSymbol(size);
|
|
670
|
+
}
|
|
671
|
+
class LayerControl {
|
|
672
|
+
constructor(options = {}) {
|
|
673
|
+
__publicField(this, "map");
|
|
674
|
+
__publicField(this, "mapContainer");
|
|
675
|
+
__publicField(this, "container");
|
|
676
|
+
__publicField(this, "button");
|
|
677
|
+
__publicField(this, "panel");
|
|
678
|
+
// Panel positioning
|
|
679
|
+
__publicField(this, "resizeHandler", null);
|
|
680
|
+
__publicField(this, "mapResizeHandler", null);
|
|
681
|
+
// State management
|
|
682
|
+
__publicField(this, "state");
|
|
683
|
+
__publicField(this, "targetLayers");
|
|
684
|
+
__publicField(this, "styleEditors");
|
|
685
|
+
__publicField(this, "initialSourceIds", null);
|
|
686
|
+
__publicField(this, "initialLayerIds", null);
|
|
687
|
+
// Panel width management
|
|
688
|
+
__publicField(this, "minPanelWidth");
|
|
689
|
+
__publicField(this, "maxPanelWidth");
|
|
690
|
+
__publicField(this, "maxPanelHeight");
|
|
691
|
+
__publicField(this, "showStyleEditor");
|
|
692
|
+
__publicField(this, "showOpacitySlider");
|
|
693
|
+
__publicField(this, "showLayerSymbol");
|
|
694
|
+
__publicField(this, "excludeDrawnLayers");
|
|
695
|
+
__publicField(this, "excludeLayerPatterns");
|
|
696
|
+
__publicField(this, "customLayerRegistry", null);
|
|
697
|
+
__publicField(this, "customLayerUnsubscribe", null);
|
|
698
|
+
__publicField(this, "removedCustomLayerIds", /* @__PURE__ */ new Set());
|
|
699
|
+
__publicField(this, "nativeLayerGroups", /* @__PURE__ */ new Map());
|
|
700
|
+
__publicField(this, "basemapStyleUrl", null);
|
|
701
|
+
__publicField(this, "basemapLayerIds", null);
|
|
702
|
+
__publicField(this, "widthSliderEl", null);
|
|
703
|
+
__publicField(this, "widthThumbEl", null);
|
|
704
|
+
__publicField(this, "widthValueEl", null);
|
|
705
|
+
__publicField(this, "isWidthSliderActive", false);
|
|
706
|
+
__publicField(this, "widthDragRectWidth", null);
|
|
707
|
+
__publicField(this, "widthDragStartX", null);
|
|
708
|
+
__publicField(this, "widthDragStartWidth", null);
|
|
709
|
+
__publicField(this, "widthFrame", null);
|
|
710
|
+
// Context menu and drag-drop
|
|
711
|
+
__publicField(this, "contextMenuEl", null);
|
|
712
|
+
__publicField(this, "enableContextMenu");
|
|
713
|
+
__publicField(this, "enableDragAndDrop");
|
|
714
|
+
__publicField(this, "onLayerRename");
|
|
715
|
+
__publicField(this, "onLayerReorder");
|
|
716
|
+
__publicField(this, "onLayerRemove");
|
|
717
|
+
this.minPanelWidth = options.panelMinWidth || 240;
|
|
718
|
+
this.maxPanelWidth = options.panelMaxWidth || 420;
|
|
719
|
+
this.maxPanelHeight = options.panelMaxHeight || 600;
|
|
720
|
+
this.showStyleEditor = options.showStyleEditor !== false;
|
|
721
|
+
this.showOpacitySlider = options.showOpacitySlider !== false;
|
|
722
|
+
this.showLayerSymbol = options.showLayerSymbol !== false;
|
|
723
|
+
this.excludeDrawnLayers = options.excludeDrawnLayers !== false;
|
|
724
|
+
this.excludeLayerPatterns = this.wildcardPatternsToRegex(options.excludeLayers || []);
|
|
725
|
+
this.enableContextMenu = options.enableContextMenu !== false;
|
|
726
|
+
this.enableDragAndDrop = options.enableDragAndDrop !== false;
|
|
727
|
+
this.onLayerRename = options.onLayerRename;
|
|
728
|
+
this.onLayerReorder = options.onLayerReorder;
|
|
729
|
+
this.onLayerRemove = options.onLayerRemove;
|
|
730
|
+
this.state = {
|
|
731
|
+
collapsed: options.collapsed !== false,
|
|
732
|
+
panelWidth: options.panelWidth || 350,
|
|
733
|
+
activeStyleEditor: null,
|
|
734
|
+
layerStates: options.layerStates || {},
|
|
735
|
+
originalStyles: /* @__PURE__ */ new Map(),
|
|
736
|
+
userInteractingWithSlider: false,
|
|
737
|
+
backgroundLegendOpen: false,
|
|
738
|
+
backgroundLayerVisibility: /* @__PURE__ */ new Map(),
|
|
739
|
+
onlyRenderedFilter: false,
|
|
740
|
+
contextMenu: {
|
|
741
|
+
visible: false,
|
|
742
|
+
targetLayerId: null,
|
|
743
|
+
x: 0,
|
|
744
|
+
y: 0
|
|
745
|
+
},
|
|
746
|
+
renamingLayerId: null,
|
|
747
|
+
customLayerNames: /* @__PURE__ */ new Map(),
|
|
748
|
+
drag: {
|
|
749
|
+
active: false,
|
|
750
|
+
layerId: null,
|
|
751
|
+
startY: 0,
|
|
752
|
+
currentY: 0,
|
|
753
|
+
placeholder: null,
|
|
754
|
+
draggedElement: null
|
|
755
|
+
},
|
|
756
|
+
isStyleOperationInProgress: false
|
|
757
|
+
};
|
|
758
|
+
this.targetLayers = options.layers || Object.keys(this.state.layerStates);
|
|
759
|
+
this.styleEditors = /* @__PURE__ */ new Map();
|
|
760
|
+
if (options.customLayerAdapters && options.customLayerAdapters.length > 0) {
|
|
761
|
+
this.customLayerRegistry = new CustomLayerRegistry();
|
|
762
|
+
options.customLayerAdapters.forEach((adapter) => {
|
|
763
|
+
this.customLayerRegistry.register(adapter);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
this.basemapStyleUrl = options.basemapStyleUrl || null;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Called when the control is added to the map
|
|
770
|
+
*/
|
|
771
|
+
onAdd(map) {
|
|
772
|
+
this.map = map;
|
|
773
|
+
this.mapContainer = map.getContainer();
|
|
774
|
+
const style = this.map.getStyle();
|
|
775
|
+
if (style && style.sources) {
|
|
776
|
+
this.initialSourceIds = new Set(Object.keys(style.sources));
|
|
777
|
+
} else {
|
|
778
|
+
this.initialSourceIds = /* @__PURE__ */ new Set();
|
|
779
|
+
}
|
|
780
|
+
if (style && style.layers) {
|
|
781
|
+
this.initialLayerIds = new Set(style.layers.map((layer) => layer.id));
|
|
782
|
+
} else {
|
|
783
|
+
this.initialLayerIds = /* @__PURE__ */ new Set();
|
|
784
|
+
}
|
|
785
|
+
this.container = this.createContainer();
|
|
786
|
+
this.button = this.createToggleButton();
|
|
787
|
+
this.panel = this.createPanel();
|
|
788
|
+
this.container.appendChild(this.button);
|
|
789
|
+
this.mapContainer.appendChild(this.panel);
|
|
790
|
+
if (this.enableContextMenu) {
|
|
791
|
+
this.contextMenuEl = this.createContextMenu();
|
|
792
|
+
this.mapContainer.appendChild(this.contextMenuEl);
|
|
793
|
+
}
|
|
794
|
+
this.updateWidthDisplay();
|
|
795
|
+
this.setupEventListeners();
|
|
796
|
+
if (this.basemapStyleUrl && !this.basemapLayerIds) {
|
|
797
|
+
this.fetchBasemapStyle().then(() => {
|
|
798
|
+
if (Object.keys(this.state.layerStates).length === 0) {
|
|
799
|
+
this.autoDetectLayers();
|
|
800
|
+
}
|
|
801
|
+
this.buildLayerItems();
|
|
802
|
+
}).catch((error) => {
|
|
803
|
+
console.warn("Failed to fetch basemap style, falling back to heuristic detection:", error);
|
|
804
|
+
if (Object.keys(this.state.layerStates).length === 0) {
|
|
805
|
+
this.autoDetectLayers();
|
|
806
|
+
}
|
|
807
|
+
this.buildLayerItems();
|
|
808
|
+
});
|
|
809
|
+
} else {
|
|
810
|
+
if (Object.keys(this.state.layerStates).length === 0) {
|
|
811
|
+
this.autoDetectLayers();
|
|
812
|
+
}
|
|
813
|
+
this.buildLayerItems();
|
|
814
|
+
}
|
|
815
|
+
if (!this.state.collapsed) {
|
|
816
|
+
requestAnimationFrame(() => {
|
|
817
|
+
this.updatePanelPosition();
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return this.container;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Fetch the basemap style JSON and extract layer IDs.
|
|
824
|
+
* This provides reliable distinction between basemap and user-added layers.
|
|
825
|
+
*/
|
|
826
|
+
async fetchBasemapStyle() {
|
|
827
|
+
if (!this.basemapStyleUrl) return;
|
|
828
|
+
try {
|
|
829
|
+
const response = await fetch(this.basemapStyleUrl);
|
|
830
|
+
if (!response.ok) {
|
|
831
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
832
|
+
}
|
|
833
|
+
const styleJson = await response.json();
|
|
834
|
+
if (styleJson && Array.isArray(styleJson.layers)) {
|
|
835
|
+
this.basemapLayerIds = new Set(
|
|
836
|
+
styleJson.layers.map((layer) => layer.id)
|
|
837
|
+
);
|
|
838
|
+
} else {
|
|
839
|
+
this.basemapLayerIds = /* @__PURE__ */ new Set();
|
|
840
|
+
}
|
|
841
|
+
} catch (error) {
|
|
842
|
+
console.warn("Failed to fetch basemap style from URL:", this.basemapStyleUrl, error);
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Called when the control is removed from the map
|
|
848
|
+
*/
|
|
849
|
+
onRemove() {
|
|
850
|
+
var _a, _b, _c;
|
|
851
|
+
if (this.customLayerUnsubscribe) {
|
|
852
|
+
this.customLayerUnsubscribe();
|
|
853
|
+
this.customLayerUnsubscribe = null;
|
|
854
|
+
}
|
|
855
|
+
if (this.customLayerRegistry) {
|
|
856
|
+
this.customLayerRegistry.destroy();
|
|
857
|
+
this.customLayerRegistry = null;
|
|
858
|
+
}
|
|
859
|
+
if (this.resizeHandler) {
|
|
860
|
+
window.removeEventListener("resize", this.resizeHandler);
|
|
861
|
+
this.resizeHandler = null;
|
|
862
|
+
}
|
|
863
|
+
if (this.mapResizeHandler) {
|
|
864
|
+
this.map.off("resize", this.mapResizeHandler);
|
|
865
|
+
this.mapResizeHandler = null;
|
|
866
|
+
}
|
|
867
|
+
if (this.contextMenuEl) {
|
|
868
|
+
(_a = this.contextMenuEl.parentNode) == null ? void 0 : _a.removeChild(this.contextMenuEl);
|
|
869
|
+
this.contextMenuEl = null;
|
|
870
|
+
}
|
|
871
|
+
this.cleanupDragState();
|
|
872
|
+
(_b = this.panel.parentNode) == null ? void 0 : _b.removeChild(this.panel);
|
|
873
|
+
(_c = this.container.parentNode) == null ? void 0 : _c.removeChild(this.container);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Auto-detect layers from the map and populate layerStates
|
|
877
|
+
*/
|
|
878
|
+
autoDetectLayers() {
|
|
879
|
+
const style = this.map.getStyle();
|
|
880
|
+
if (!style || !style.layers) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const allLayerIds = style.layers.map((layer) => layer.id);
|
|
884
|
+
if (this.targetLayers.length === 0) {
|
|
885
|
+
const userAddedLayers = [];
|
|
886
|
+
const backgroundLayerIds = [];
|
|
887
|
+
const useBasemapStyleDetection = this.basemapLayerIds !== null && this.basemapLayerIds.size > 0;
|
|
888
|
+
const userAddedSourceIds = useBasemapStyleDetection ? /* @__PURE__ */ new Set() : this.detectUserAddedSources();
|
|
889
|
+
allLayerIds.forEach((layerId) => {
|
|
890
|
+
const layer = this.map.getLayer(layerId);
|
|
891
|
+
if (!layer) return;
|
|
892
|
+
if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
|
|
893
|
+
backgroundLayerIds.push(layerId);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (this.isExcludedByPattern(layerId)) {
|
|
897
|
+
backgroundLayerIds.push(layerId);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (useBasemapStyleDetection) {
|
|
901
|
+
if (this.basemapLayerIds.has(layerId)) {
|
|
902
|
+
backgroundLayerIds.push(layerId);
|
|
903
|
+
} else {
|
|
904
|
+
userAddedLayers.push(layerId);
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
const sourceId = layer.source;
|
|
908
|
+
if (sourceId && userAddedSourceIds.has(sourceId)) {
|
|
909
|
+
userAddedLayers.push(layerId);
|
|
910
|
+
} else {
|
|
911
|
+
backgroundLayerIds.push(layerId);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
if (backgroundLayerIds.length > 0) {
|
|
916
|
+
this.state.layerStates["Background"] = {
|
|
917
|
+
visible: true,
|
|
918
|
+
opacity: 1,
|
|
919
|
+
name: "Background"
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
userAddedLayers.forEach((layerId) => {
|
|
923
|
+
const layer = this.map.getLayer(layerId);
|
|
924
|
+
if (!layer) return;
|
|
925
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
926
|
+
const isVisible = visibility !== "none";
|
|
927
|
+
const layerType = layer.type;
|
|
928
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
929
|
+
const friendlyName = this.generateFriendlyName(layerId);
|
|
930
|
+
this.state.layerStates[layerId] = {
|
|
931
|
+
visible: isVisible,
|
|
932
|
+
opacity,
|
|
933
|
+
name: friendlyName
|
|
934
|
+
};
|
|
935
|
+
});
|
|
936
|
+
} else {
|
|
937
|
+
const userLayers = [];
|
|
938
|
+
const basemapLayers = [];
|
|
939
|
+
allLayerIds.forEach((layerId) => {
|
|
940
|
+
if (this.isExcludedByPattern(layerId)) {
|
|
941
|
+
basemapLayers.push(layerId);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (this.targetLayers.includes(layerId)) {
|
|
945
|
+
userLayers.push(layerId);
|
|
946
|
+
} else {
|
|
947
|
+
basemapLayers.push(layerId);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
if (basemapLayers.length > 0) {
|
|
951
|
+
this.state.layerStates["Background"] = {
|
|
952
|
+
visible: true,
|
|
953
|
+
opacity: 1,
|
|
954
|
+
name: "Background"
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
userLayers.forEach((layerId) => {
|
|
958
|
+
const layer = this.map.getLayer(layerId);
|
|
959
|
+
if (!layer) return;
|
|
960
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
961
|
+
const isVisible = visibility !== "none";
|
|
962
|
+
const layerType = layer.type;
|
|
963
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
964
|
+
const friendlyName = this.generateFriendlyName(layerId);
|
|
965
|
+
this.state.layerStates[layerId] = {
|
|
966
|
+
visible: isVisible,
|
|
967
|
+
opacity,
|
|
968
|
+
name: friendlyName
|
|
969
|
+
};
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if (this.customLayerRegistry) {
|
|
973
|
+
const customLayerIds = this.customLayerRegistry.getAllLayerIds();
|
|
974
|
+
customLayerIds.forEach((layerId) => {
|
|
975
|
+
if (this.state.layerStates[layerId]) return;
|
|
976
|
+
const customState = this.customLayerRegistry.getLayerState(layerId);
|
|
977
|
+
if (customState) {
|
|
978
|
+
this.state.layerStates[layerId] = {
|
|
979
|
+
visible: customState.visible,
|
|
980
|
+
opacity: customState.opacity,
|
|
981
|
+
name: customState.name,
|
|
982
|
+
isCustomLayer: true,
|
|
983
|
+
customLayerType: this.customLayerRegistry.getSymbolType(layerId) || void 0
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
this.targetLayers = Object.keys(this.state.layerStates);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Detect which sources are user-added (not from the basemap style)
|
|
992
|
+
* User-added sources are identified by:
|
|
993
|
+
* - Sources that were NOT present when the control was first added
|
|
994
|
+
* - Additionally for sources added later:
|
|
995
|
+
* - GeoJSON sources with inline data objects (not URL strings)
|
|
996
|
+
* - Image, video, canvas sources
|
|
997
|
+
* - Raster, raster-dem, vector sources from non-basemap tile providers
|
|
998
|
+
*/
|
|
999
|
+
detectUserAddedSources() {
|
|
1000
|
+
const userAddedSources = /* @__PURE__ */ new Set();
|
|
1001
|
+
const style = this.map.getStyle();
|
|
1002
|
+
if (!style || !style.sources) return userAddedSources;
|
|
1003
|
+
const basemapDomains = /* @__PURE__ */ new Set();
|
|
1004
|
+
const knownBasemapProviders = [
|
|
1005
|
+
"demotiles.maplibre.org",
|
|
1006
|
+
"api.maptiler.com",
|
|
1007
|
+
"tiles.stadiamaps.com",
|
|
1008
|
+
"api.mapbox.com",
|
|
1009
|
+
"basemaps.cartocdn.com",
|
|
1010
|
+
"tiles.mapbox.com",
|
|
1011
|
+
"a.basemaps.cartocdn.com",
|
|
1012
|
+
"b.basemaps.cartocdn.com",
|
|
1013
|
+
"c.basemaps.cartocdn.com",
|
|
1014
|
+
"d.basemaps.cartocdn.com",
|
|
1015
|
+
"tiles.arcgis.com",
|
|
1016
|
+
"server.arcgisonline.com",
|
|
1017
|
+
"services.arcgisonline.com"
|
|
1018
|
+
];
|
|
1019
|
+
const spriteUrl = style.sprite;
|
|
1020
|
+
if (spriteUrl && typeof spriteUrl === "string") {
|
|
1021
|
+
try {
|
|
1022
|
+
const url = new URL(spriteUrl);
|
|
1023
|
+
basemapDomains.add(url.hostname);
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (style.glyphs) {
|
|
1028
|
+
try {
|
|
1029
|
+
const url = new URL(style.glyphs.replace("{fontstack}", "x").replace("{range}", "x"));
|
|
1030
|
+
basemapDomains.add(url.hostname);
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
for (const [sourceId, source] of Object.entries(style.sources)) {
|
|
1035
|
+
if (this.initialSourceIds && this.initialSourceIds.has(sourceId)) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const src = source;
|
|
1039
|
+
const sourceType = src.type;
|
|
1040
|
+
if (sourceType === "image" || sourceType === "video" || sourceType === "canvas") {
|
|
1041
|
+
userAddedSources.add(sourceId);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (sourceType === "geojson") {
|
|
1045
|
+
if (src.data && typeof src.data === "object") {
|
|
1046
|
+
userAddedSources.add(sourceId);
|
|
1047
|
+
} else if (src.data && typeof src.data === "string") {
|
|
1048
|
+
try {
|
|
1049
|
+
const url = new URL(src.data);
|
|
1050
|
+
const isBasemap = knownBasemapProviders.some((p) => url.hostname.includes(p)) || basemapDomains.has(url.hostname);
|
|
1051
|
+
if (!isBasemap) {
|
|
1052
|
+
userAddedSources.add(sourceId);
|
|
1053
|
+
}
|
|
1054
|
+
} catch {
|
|
1055
|
+
userAddedSources.add(sourceId);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (sourceType === "raster" || sourceType === "raster-dem" || sourceType === "vector") {
|
|
1061
|
+
const tileUrl = src.url || src.tiles && src.tiles[0] || "";
|
|
1062
|
+
if (tileUrl) {
|
|
1063
|
+
try {
|
|
1064
|
+
const url = new URL(tileUrl.replace(/{[^}]+}/g, "0"));
|
|
1065
|
+
const hostname = url.hostname;
|
|
1066
|
+
const isKnownBasemap = knownBasemapProviders.some(
|
|
1067
|
+
(provider) => hostname === provider || hostname.endsWith("." + provider)
|
|
1068
|
+
);
|
|
1069
|
+
const matchesBasemapDomain = basemapDomains.has(hostname);
|
|
1070
|
+
if (!isKnownBasemap && !matchesBasemapDomain) {
|
|
1071
|
+
userAddedSources.add(sourceId);
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
userAddedSources.add(sourceId);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return userAddedSources;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Generate a friendly display name from a layer ID
|
|
1083
|
+
*/
|
|
1084
|
+
generateFriendlyName(layerId) {
|
|
1085
|
+
let name = layerId.replace(/^(layer[-_]?|gl[-_]?)/, "");
|
|
1086
|
+
name = name.replace(/[-_]/g, " ");
|
|
1087
|
+
name = name.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1088
|
+
return name || layerId;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Check if a layer ID belongs to a drawing library (Geoman, Mapbox GL Draw, etc.)
|
|
1092
|
+
* @param layerId The layer ID to check
|
|
1093
|
+
* @returns true if the layer is from a drawing library
|
|
1094
|
+
*/
|
|
1095
|
+
isDrawnLayer(layerId) {
|
|
1096
|
+
const drawnLayerPatterns = [
|
|
1097
|
+
// Drawing libraries
|
|
1098
|
+
/^gm[-_\s]/i,
|
|
1099
|
+
// Geoman (gm-main-*, gm_*, Gm Temporary...)
|
|
1100
|
+
/^gl-draw[-_]/i,
|
|
1101
|
+
// Mapbox GL Draw
|
|
1102
|
+
/^mapbox-gl-draw[-_]/i,
|
|
1103
|
+
// Mapbox GL Draw alternative
|
|
1104
|
+
/^terra-draw[-_]/i,
|
|
1105
|
+
// Terra Draw
|
|
1106
|
+
/^maplibre-gl-draw[-_]/i,
|
|
1107
|
+
// MapLibre GL Draw
|
|
1108
|
+
/^draw[-_]layer/i,
|
|
1109
|
+
// Generic draw layers
|
|
1110
|
+
// maplibre-gl-components internal layers
|
|
1111
|
+
/^measure-/i,
|
|
1112
|
+
// MeasureControl (measure-{id}-fill, measure-{id}-line)
|
|
1113
|
+
/^pmtiles-source-/i,
|
|
1114
|
+
// PMTilesLayerControl (managed via adapter)
|
|
1115
|
+
/^stac-search-footprints/i
|
|
1116
|
+
// StacSearchControl footprint layers
|
|
1117
|
+
];
|
|
1118
|
+
return drawnLayerPatterns.some((pattern) => pattern.test(layerId));
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Convert wildcard patterns (e.g., '*-temp-*', 'debug-*') to RegExp objects
|
|
1122
|
+
*/
|
|
1123
|
+
wildcardPatternsToRegex(patterns) {
|
|
1124
|
+
return patterns.map((pattern) => {
|
|
1125
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1126
|
+
const regexStr = escaped.replace(/\*/g, ".*");
|
|
1127
|
+
return new RegExp(`^${regexStr}$`, "i");
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Check if a layer matches any of the user-defined exclusion patterns
|
|
1132
|
+
*/
|
|
1133
|
+
isExcludedByPattern(layerId) {
|
|
1134
|
+
return this.excludeLayerPatterns.some((pattern) => pattern.test(layerId));
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Create the main container element
|
|
1138
|
+
*/
|
|
1139
|
+
createContainer() {
|
|
1140
|
+
const container = document.createElement("div");
|
|
1141
|
+
container.className = "maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-layer-control";
|
|
1142
|
+
return container;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Create the toggle button
|
|
1146
|
+
*/
|
|
1147
|
+
createToggleButton() {
|
|
1148
|
+
const button = document.createElement("button");
|
|
1149
|
+
button.type = "button";
|
|
1150
|
+
button.title = "Layer Control";
|
|
1151
|
+
button.setAttribute("aria-label", "Layer Control");
|
|
1152
|
+
const icon = document.createElement("span");
|
|
1153
|
+
icon.className = "layer-control-icon";
|
|
1154
|
+
icon.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 3 3 8.25 12 13.5 21 8.25 12 3"></polygon><polyline points="3 12.75 12 18 21 12.75"></polyline><polyline points="3 17.25 12 22 21 17.25"></polyline></svg>';
|
|
1155
|
+
button.appendChild(icon);
|
|
1156
|
+
return button;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Create the panel element
|
|
1160
|
+
*/
|
|
1161
|
+
createPanel() {
|
|
1162
|
+
const panel = document.createElement("div");
|
|
1163
|
+
panel.className = "layer-control-panel";
|
|
1164
|
+
panel.style.width = `${this.state.panelWidth}px`;
|
|
1165
|
+
panel.style.maxHeight = `${this.maxPanelHeight}px`;
|
|
1166
|
+
if (!this.state.collapsed) {
|
|
1167
|
+
panel.classList.add("expanded");
|
|
1168
|
+
}
|
|
1169
|
+
const header = this.createPanelHeader();
|
|
1170
|
+
panel.appendChild(header);
|
|
1171
|
+
const actionButtons = this.createActionButtons();
|
|
1172
|
+
panel.appendChild(actionButtons);
|
|
1173
|
+
return panel;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Detect which corner the control is positioned in
|
|
1177
|
+
* @returns The position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
1178
|
+
*/
|
|
1179
|
+
getControlPosition() {
|
|
1180
|
+
const parent = this.container.parentElement;
|
|
1181
|
+
if (!parent) return "top-right";
|
|
1182
|
+
if (parent.classList.contains("maplibregl-ctrl-top-left")) return "top-left";
|
|
1183
|
+
if (parent.classList.contains("maplibregl-ctrl-top-right")) return "top-right";
|
|
1184
|
+
if (parent.classList.contains("maplibregl-ctrl-bottom-left")) return "bottom-left";
|
|
1185
|
+
if (parent.classList.contains("maplibregl-ctrl-bottom-right")) return "bottom-right";
|
|
1186
|
+
return "top-right";
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Update the panel position based on button location and control corner
|
|
1190
|
+
* Positions the panel next to the button, expanding in the appropriate direction
|
|
1191
|
+
*/
|
|
1192
|
+
updatePanelPosition() {
|
|
1193
|
+
if (!this.button || !this.panel || !this.mapContainer) return;
|
|
1194
|
+
const buttonRect = this.button.getBoundingClientRect();
|
|
1195
|
+
const mapRect = this.mapContainer.getBoundingClientRect();
|
|
1196
|
+
const position = this.getControlPosition();
|
|
1197
|
+
const buttonTop = buttonRect.top - mapRect.top;
|
|
1198
|
+
const buttonBottom = mapRect.bottom - buttonRect.bottom;
|
|
1199
|
+
const buttonLeft = buttonRect.left - mapRect.left;
|
|
1200
|
+
const buttonRight = mapRect.right - buttonRect.right;
|
|
1201
|
+
const panelGap = 5;
|
|
1202
|
+
this.panel.style.top = "";
|
|
1203
|
+
this.panel.style.bottom = "";
|
|
1204
|
+
this.panel.style.left = "";
|
|
1205
|
+
this.panel.style.right = "";
|
|
1206
|
+
switch (position) {
|
|
1207
|
+
case "top-left":
|
|
1208
|
+
this.panel.style.top = `${buttonTop + buttonRect.height + panelGap}px`;
|
|
1209
|
+
this.panel.style.left = `${buttonLeft}px`;
|
|
1210
|
+
break;
|
|
1211
|
+
case "top-right":
|
|
1212
|
+
this.panel.style.top = `${buttonTop + buttonRect.height + panelGap}px`;
|
|
1213
|
+
this.panel.style.right = `${buttonRight}px`;
|
|
1214
|
+
break;
|
|
1215
|
+
case "bottom-left":
|
|
1216
|
+
this.panel.style.bottom = `${buttonBottom + buttonRect.height + panelGap}px`;
|
|
1217
|
+
this.panel.style.left = `${buttonLeft}px`;
|
|
1218
|
+
break;
|
|
1219
|
+
case "bottom-right":
|
|
1220
|
+
this.panel.style.bottom = `${buttonBottom + buttonRect.height + panelGap}px`;
|
|
1221
|
+
this.panel.style.right = `${buttonRight}px`;
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Create action buttons for Show All / Hide All
|
|
1227
|
+
*/
|
|
1228
|
+
createActionButtons() {
|
|
1229
|
+
const container = document.createElement("div");
|
|
1230
|
+
container.className = "layer-control-actions";
|
|
1231
|
+
const showAllBtn = document.createElement("button");
|
|
1232
|
+
showAllBtn.type = "button";
|
|
1233
|
+
showAllBtn.className = "layer-control-action-btn";
|
|
1234
|
+
showAllBtn.textContent = "Show All";
|
|
1235
|
+
showAllBtn.title = "Show all layers";
|
|
1236
|
+
showAllBtn.addEventListener("click", () => this.setAllLayersVisibility(true));
|
|
1237
|
+
const hideAllBtn = document.createElement("button");
|
|
1238
|
+
hideAllBtn.type = "button";
|
|
1239
|
+
hideAllBtn.className = "layer-control-action-btn";
|
|
1240
|
+
hideAllBtn.textContent = "Hide All";
|
|
1241
|
+
hideAllBtn.title = "Hide all layers";
|
|
1242
|
+
hideAllBtn.addEventListener("click", () => this.setAllLayersVisibility(false));
|
|
1243
|
+
container.appendChild(showAllBtn);
|
|
1244
|
+
container.appendChild(hideAllBtn);
|
|
1245
|
+
return container;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Set visibility of all layers
|
|
1249
|
+
*/
|
|
1250
|
+
setAllLayersVisibility(visible) {
|
|
1251
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
1252
|
+
this.toggleLayerVisibility(layerId, visible);
|
|
1253
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
1254
|
+
if (itemEl) {
|
|
1255
|
+
const checkbox = itemEl.querySelector(".layer-control-checkbox");
|
|
1256
|
+
if (checkbox) {
|
|
1257
|
+
checkbox.checked = visible;
|
|
1258
|
+
checkbox.indeterminate = false;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Create the panel header with title and width control
|
|
1265
|
+
*/
|
|
1266
|
+
createPanelHeader() {
|
|
1267
|
+
const header = document.createElement("div");
|
|
1268
|
+
header.className = "layer-control-panel-header";
|
|
1269
|
+
const title = document.createElement("span");
|
|
1270
|
+
title.className = "layer-control-panel-title";
|
|
1271
|
+
title.textContent = "Layers";
|
|
1272
|
+
header.appendChild(title);
|
|
1273
|
+
const widthControl = this.createWidthControl();
|
|
1274
|
+
header.appendChild(widthControl);
|
|
1275
|
+
return header;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Create the width control slider
|
|
1279
|
+
*/
|
|
1280
|
+
createWidthControl() {
|
|
1281
|
+
const widthControl = document.createElement("label");
|
|
1282
|
+
widthControl.className = "layer-control-width-control";
|
|
1283
|
+
widthControl.title = "Adjust layer panel width";
|
|
1284
|
+
const widthLabel = document.createElement("span");
|
|
1285
|
+
widthLabel.textContent = "Width";
|
|
1286
|
+
widthControl.appendChild(widthLabel);
|
|
1287
|
+
const widthSlider = document.createElement("div");
|
|
1288
|
+
widthSlider.className = "layer-control-width-slider";
|
|
1289
|
+
widthSlider.setAttribute("role", "slider");
|
|
1290
|
+
widthSlider.setAttribute("aria-valuemin", String(this.minPanelWidth));
|
|
1291
|
+
widthSlider.setAttribute("aria-valuemax", String(this.maxPanelWidth));
|
|
1292
|
+
widthSlider.setAttribute("aria-valuenow", String(this.state.panelWidth));
|
|
1293
|
+
widthSlider.setAttribute("aria-valuestep", "10");
|
|
1294
|
+
widthSlider.setAttribute("aria-label", "Layer panel width");
|
|
1295
|
+
widthSlider.tabIndex = 0;
|
|
1296
|
+
const widthTrack = document.createElement("div");
|
|
1297
|
+
widthTrack.className = "layer-control-width-track";
|
|
1298
|
+
const widthThumb = document.createElement("div");
|
|
1299
|
+
widthThumb.className = "layer-control-width-thumb";
|
|
1300
|
+
widthSlider.appendChild(widthTrack);
|
|
1301
|
+
widthSlider.appendChild(widthThumb);
|
|
1302
|
+
this.widthSliderEl = widthSlider;
|
|
1303
|
+
this.widthThumbEl = widthThumb;
|
|
1304
|
+
const widthValue = document.createElement("span");
|
|
1305
|
+
widthValue.className = "layer-control-width-value";
|
|
1306
|
+
this.widthValueEl = widthValue;
|
|
1307
|
+
widthControl.appendChild(widthSlider);
|
|
1308
|
+
widthControl.appendChild(widthValue);
|
|
1309
|
+
this.updateWidthDisplay();
|
|
1310
|
+
this.setupWidthSliderEvents(widthSlider);
|
|
1311
|
+
return widthControl;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Setup event listeners for width slider
|
|
1315
|
+
*/
|
|
1316
|
+
setupWidthSliderEvents(widthSlider) {
|
|
1317
|
+
widthSlider.addEventListener("pointerdown", (event) => {
|
|
1318
|
+
event.preventDefault();
|
|
1319
|
+
const rect = widthSlider.getBoundingClientRect();
|
|
1320
|
+
this.widthDragRectWidth = rect.width || 1;
|
|
1321
|
+
this.widthDragStartX = event.clientX;
|
|
1322
|
+
this.widthDragStartWidth = this.state.panelWidth;
|
|
1323
|
+
this.isWidthSliderActive = true;
|
|
1324
|
+
widthSlider.setPointerCapture(event.pointerId);
|
|
1325
|
+
this.updateWidthFromPointer(event, true);
|
|
1326
|
+
});
|
|
1327
|
+
widthSlider.addEventListener("pointermove", (event) => {
|
|
1328
|
+
if (!this.isWidthSliderActive) return;
|
|
1329
|
+
this.updateWidthFromPointer(event);
|
|
1330
|
+
});
|
|
1331
|
+
const endPointerDrag = (event) => {
|
|
1332
|
+
if (!this.isWidthSliderActive) return;
|
|
1333
|
+
if (event.pointerId !== void 0) {
|
|
1334
|
+
try {
|
|
1335
|
+
widthSlider.releasePointerCapture(event.pointerId);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
this.isWidthSliderActive = false;
|
|
1340
|
+
this.widthDragRectWidth = null;
|
|
1341
|
+
this.widthDragStartX = null;
|
|
1342
|
+
this.widthDragStartWidth = null;
|
|
1343
|
+
this.updateWidthDisplay();
|
|
1344
|
+
};
|
|
1345
|
+
widthSlider.addEventListener("pointerup", endPointerDrag);
|
|
1346
|
+
widthSlider.addEventListener("pointercancel", endPointerDrag);
|
|
1347
|
+
widthSlider.addEventListener("lostpointercapture", endPointerDrag);
|
|
1348
|
+
widthSlider.addEventListener("keydown", (event) => {
|
|
1349
|
+
let handled = true;
|
|
1350
|
+
const step = event.shiftKey ? 20 : 10;
|
|
1351
|
+
switch (event.key) {
|
|
1352
|
+
case "ArrowLeft":
|
|
1353
|
+
case "ArrowDown":
|
|
1354
|
+
this.applyPanelWidth(this.state.panelWidth - step, true);
|
|
1355
|
+
break;
|
|
1356
|
+
case "ArrowRight":
|
|
1357
|
+
case "ArrowUp":
|
|
1358
|
+
this.applyPanelWidth(this.state.panelWidth + step, true);
|
|
1359
|
+
break;
|
|
1360
|
+
case "Home":
|
|
1361
|
+
this.applyPanelWidth(this.minPanelWidth, true);
|
|
1362
|
+
break;
|
|
1363
|
+
case "End":
|
|
1364
|
+
this.applyPanelWidth(this.maxPanelWidth, true);
|
|
1365
|
+
break;
|
|
1366
|
+
case "PageUp":
|
|
1367
|
+
this.applyPanelWidth(this.state.panelWidth + 50, true);
|
|
1368
|
+
break;
|
|
1369
|
+
case "PageDown":
|
|
1370
|
+
this.applyPanelWidth(this.state.panelWidth - 50, true);
|
|
1371
|
+
break;
|
|
1372
|
+
default:
|
|
1373
|
+
handled = false;
|
|
1374
|
+
}
|
|
1375
|
+
if (handled) {
|
|
1376
|
+
event.preventDefault();
|
|
1377
|
+
this.updateWidthDisplay();
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Update panel width from pointer event
|
|
1383
|
+
*/
|
|
1384
|
+
updateWidthFromPointer(event, resetBaseline = false) {
|
|
1385
|
+
if (!this.widthSliderEl) return;
|
|
1386
|
+
const sliderWidth = this.widthDragRectWidth || this.widthSliderEl.getBoundingClientRect().width || 1;
|
|
1387
|
+
const widthRange = this.maxPanelWidth - this.minPanelWidth;
|
|
1388
|
+
let width;
|
|
1389
|
+
if (resetBaseline) {
|
|
1390
|
+
const rect = this.widthSliderEl.getBoundingClientRect();
|
|
1391
|
+
const relative = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0;
|
|
1392
|
+
const clampedRatio = Math.min(1, Math.max(0, relative));
|
|
1393
|
+
width = this.minPanelWidth + clampedRatio * widthRange;
|
|
1394
|
+
this.widthDragStartWidth = width;
|
|
1395
|
+
this.widthDragStartX = event.clientX;
|
|
1396
|
+
} else {
|
|
1397
|
+
const delta = event.clientX - (this.widthDragStartX || event.clientX);
|
|
1398
|
+
width = (this.widthDragStartWidth || this.state.panelWidth) + delta / sliderWidth * widthRange;
|
|
1399
|
+
}
|
|
1400
|
+
this.applyPanelWidth(width, this.isWidthSliderActive);
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Apply panel width (clamped to min/max)
|
|
1404
|
+
*/
|
|
1405
|
+
applyPanelWidth(width, immediate = false) {
|
|
1406
|
+
const clamped = Math.round(Math.min(this.maxPanelWidth, Math.max(this.minPanelWidth, width)));
|
|
1407
|
+
const applyWidth = () => {
|
|
1408
|
+
this.state.panelWidth = clamped;
|
|
1409
|
+
const px = `${clamped}px`;
|
|
1410
|
+
this.panel.style.width = px;
|
|
1411
|
+
this.updateWidthDisplay();
|
|
1412
|
+
};
|
|
1413
|
+
if (immediate) {
|
|
1414
|
+
applyWidth();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (this.widthFrame) {
|
|
1418
|
+
cancelAnimationFrame(this.widthFrame);
|
|
1419
|
+
}
|
|
1420
|
+
this.widthFrame = requestAnimationFrame(() => {
|
|
1421
|
+
applyWidth();
|
|
1422
|
+
this.widthFrame = null;
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Update width display (value label and thumb position)
|
|
1427
|
+
*/
|
|
1428
|
+
updateWidthDisplay() {
|
|
1429
|
+
if (this.widthValueEl) {
|
|
1430
|
+
this.widthValueEl.textContent = `${this.state.panelWidth}px`;
|
|
1431
|
+
}
|
|
1432
|
+
if (this.widthSliderEl) {
|
|
1433
|
+
this.widthSliderEl.setAttribute("aria-valuenow", String(this.state.panelWidth));
|
|
1434
|
+
const ratio = (this.state.panelWidth - this.minPanelWidth) / (this.maxPanelWidth - this.minPanelWidth || 1);
|
|
1435
|
+
if (this.widthThumbEl) {
|
|
1436
|
+
const sliderWidth = this.widthSliderEl.clientWidth;
|
|
1437
|
+
if (sliderWidth === 0) {
|
|
1438
|
+
requestAnimationFrame(() => this.updateWidthDisplay());
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const thumbWidth = this.widthThumbEl.offsetWidth || 14;
|
|
1442
|
+
const padding = 16;
|
|
1443
|
+
const available = Math.max(0, sliderWidth - padding - thumbWidth);
|
|
1444
|
+
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
|
1445
|
+
const leftPx = 8 + available * clampedRatio;
|
|
1446
|
+
this.widthThumbEl.style.left = `${leftPx}px`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Setup main event listeners
|
|
1452
|
+
*/
|
|
1453
|
+
setupEventListeners() {
|
|
1454
|
+
this.button.addEventListener("click", () => this.toggle());
|
|
1455
|
+
document.addEventListener("click", (e) => {
|
|
1456
|
+
const target = e.target;
|
|
1457
|
+
if (this.contextMenuEl && this.state.contextMenu.visible) {
|
|
1458
|
+
if (!this.contextMenuEl.contains(target)) {
|
|
1459
|
+
this.hideContextMenu();
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (!this.container.contains(target) && !this.panel.contains(target)) {
|
|
1463
|
+
this.collapse();
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
this.resizeHandler = () => {
|
|
1467
|
+
if (!this.state.collapsed) {
|
|
1468
|
+
this.updatePanelPosition();
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
1472
|
+
this.mapResizeHandler = () => {
|
|
1473
|
+
if (!this.state.collapsed) {
|
|
1474
|
+
this.updatePanelPosition();
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
this.map.on("resize", this.mapResizeHandler);
|
|
1478
|
+
this.setupLayerChangeListeners();
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Setup listeners for map layer changes
|
|
1482
|
+
*/
|
|
1483
|
+
setupLayerChangeListeners() {
|
|
1484
|
+
this.map.on("styledata", () => {
|
|
1485
|
+
setTimeout(() => {
|
|
1486
|
+
this.updateLayerStatesFromMap();
|
|
1487
|
+
this.checkForNewLayers();
|
|
1488
|
+
}, 100);
|
|
1489
|
+
});
|
|
1490
|
+
this.map.on("data", (e) => {
|
|
1491
|
+
if (e.sourceDataType === "content") {
|
|
1492
|
+
setTimeout(() => {
|
|
1493
|
+
this.updateLayerStatesFromMap();
|
|
1494
|
+
this.checkForNewLayers();
|
|
1495
|
+
}, 100);
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
this.map.on("sourcedata", (e) => {
|
|
1499
|
+
if (e.sourceDataType === "metadata") {
|
|
1500
|
+
setTimeout(() => {
|
|
1501
|
+
this.checkForNewLayers();
|
|
1502
|
+
}, 150);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
if (this.customLayerRegistry) {
|
|
1506
|
+
this.customLayerUnsubscribe = this.customLayerRegistry.onChange((event, layerId) => {
|
|
1507
|
+
if (event === "add" && layerId) {
|
|
1508
|
+
this.removedCustomLayerIds.delete(layerId);
|
|
1509
|
+
}
|
|
1510
|
+
setTimeout(() => this.checkForNewLayers(), 100);
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Toggle panel expanded/collapsed state
|
|
1516
|
+
*/
|
|
1517
|
+
toggle() {
|
|
1518
|
+
if (this.state.collapsed) {
|
|
1519
|
+
this.expand();
|
|
1520
|
+
} else {
|
|
1521
|
+
this.collapse();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Expand the panel
|
|
1526
|
+
*/
|
|
1527
|
+
expand() {
|
|
1528
|
+
this.state.collapsed = false;
|
|
1529
|
+
this.panel.classList.add("expanded");
|
|
1530
|
+
this.updatePanelPosition();
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Collapse the panel
|
|
1534
|
+
*/
|
|
1535
|
+
collapse() {
|
|
1536
|
+
this.state.collapsed = true;
|
|
1537
|
+
this.panel.classList.remove("expanded");
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Build layer items (called initially and when layers change)
|
|
1541
|
+
*/
|
|
1542
|
+
buildLayerItems() {
|
|
1543
|
+
const existingItems = this.panel.querySelectorAll(".layer-control-item");
|
|
1544
|
+
existingItems.forEach((item) => item.remove());
|
|
1545
|
+
this.styleEditors.clear();
|
|
1546
|
+
Object.entries(this.state.layerStates).forEach(([layerId, state]) => {
|
|
1547
|
+
if (this.targetLayers.length === 0 || this.targetLayers.includes(layerId)) {
|
|
1548
|
+
this.addLayerItem(layerId, state);
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Add a single layer item to the panel
|
|
1554
|
+
*/
|
|
1555
|
+
addLayerItem(layerId, state) {
|
|
1556
|
+
const item = document.createElement("div");
|
|
1557
|
+
item.className = "layer-control-item";
|
|
1558
|
+
item.setAttribute("data-layer-id", layerId);
|
|
1559
|
+
const row = document.createElement("div");
|
|
1560
|
+
row.className = "layer-control-row";
|
|
1561
|
+
if (this.enableDragAndDrop) {
|
|
1562
|
+
if (layerId === "Background") {
|
|
1563
|
+
const disabledHandle = this.createDisabledDragHandle();
|
|
1564
|
+
row.appendChild(disabledHandle);
|
|
1565
|
+
} else {
|
|
1566
|
+
const dragHandle = this.createDragHandle(layerId);
|
|
1567
|
+
row.appendChild(dragHandle);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const checkbox = document.createElement("input");
|
|
1571
|
+
checkbox.type = "checkbox";
|
|
1572
|
+
checkbox.className = "layer-control-checkbox";
|
|
1573
|
+
checkbox.checked = state.visible;
|
|
1574
|
+
checkbox.addEventListener("change", () => {
|
|
1575
|
+
this.toggleLayerVisibility(layerId, checkbox.checked);
|
|
1576
|
+
});
|
|
1577
|
+
const displayName = this.state.customLayerNames.get(layerId) || state.name || layerId;
|
|
1578
|
+
const name = document.createElement("span");
|
|
1579
|
+
name.className = "layer-control-name";
|
|
1580
|
+
name.textContent = displayName;
|
|
1581
|
+
name.title = displayName;
|
|
1582
|
+
row.appendChild(checkbox);
|
|
1583
|
+
if (this.showLayerSymbol) {
|
|
1584
|
+
if (layerId === "Background") {
|
|
1585
|
+
const symbol = this.createBackgroundGroupSymbol();
|
|
1586
|
+
row.appendChild(symbol);
|
|
1587
|
+
} else {
|
|
1588
|
+
const symbol = this.createLayerSymbol(layerId);
|
|
1589
|
+
if (symbol) {
|
|
1590
|
+
row.appendChild(symbol);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
row.appendChild(name);
|
|
1595
|
+
if (this.showOpacitySlider) {
|
|
1596
|
+
const opacity = document.createElement("input");
|
|
1597
|
+
opacity.type = "range";
|
|
1598
|
+
opacity.className = "layer-control-opacity";
|
|
1599
|
+
opacity.min = "0";
|
|
1600
|
+
opacity.max = "1";
|
|
1601
|
+
opacity.step = "0.01";
|
|
1602
|
+
opacity.value = String(state.opacity);
|
|
1603
|
+
opacity.title = `Opacity: ${Math.round(state.opacity * 100)}%`;
|
|
1604
|
+
opacity.addEventListener("mousedown", () => {
|
|
1605
|
+
this.state.userInteractingWithSlider = true;
|
|
1606
|
+
});
|
|
1607
|
+
opacity.addEventListener("mouseup", () => {
|
|
1608
|
+
this.state.userInteractingWithSlider = false;
|
|
1609
|
+
});
|
|
1610
|
+
opacity.addEventListener("input", () => {
|
|
1611
|
+
this.changeLayerOpacity(layerId, parseFloat(opacity.value));
|
|
1612
|
+
opacity.title = `Opacity: ${Math.round(parseFloat(opacity.value) * 100)}%`;
|
|
1613
|
+
});
|
|
1614
|
+
row.appendChild(opacity);
|
|
1615
|
+
}
|
|
1616
|
+
if (this.showStyleEditor) {
|
|
1617
|
+
if (layerId === "Background") {
|
|
1618
|
+
const legendButton = this.createBackgroundLegendButton();
|
|
1619
|
+
row.appendChild(legendButton);
|
|
1620
|
+
} else {
|
|
1621
|
+
const styleButton = this.createStyleButton(layerId);
|
|
1622
|
+
if (styleButton) {
|
|
1623
|
+
row.appendChild(styleButton);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
item.appendChild(row);
|
|
1628
|
+
if (this.enableContextMenu && layerId !== "Background") {
|
|
1629
|
+
row.addEventListener("contextmenu", (e) => {
|
|
1630
|
+
e.preventDefault();
|
|
1631
|
+
e.stopPropagation();
|
|
1632
|
+
this.showContextMenu(layerId, e.clientX, e.clientY);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
this.panel.appendChild(item);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Create a symbol element for a layer
|
|
1639
|
+
* @param layerId The layer ID
|
|
1640
|
+
* @returns The symbol HTML element, or null if layer not found
|
|
1641
|
+
*/
|
|
1642
|
+
createLayerSymbol(layerId) {
|
|
1643
|
+
const layerState = this.state.layerStates[layerId];
|
|
1644
|
+
if (layerState == null ? void 0 : layerState.isCustomLayer) {
|
|
1645
|
+
const symbolType = layerState.customLayerType || "custom-raster";
|
|
1646
|
+
const color2 = "#4a90d9";
|
|
1647
|
+
const svgMarkup2 = createLayerSymbolSVG(symbolType, color2);
|
|
1648
|
+
const symbolContainer2 = document.createElement("span");
|
|
1649
|
+
symbolContainer2.className = "layer-control-symbol";
|
|
1650
|
+
symbolContainer2.innerHTML = svgMarkup2;
|
|
1651
|
+
symbolContainer2.title = `Layer type: ${symbolType}`;
|
|
1652
|
+
return symbolContainer2;
|
|
1653
|
+
}
|
|
1654
|
+
const layer = this.map.getLayer(layerId);
|
|
1655
|
+
if (!layer) return null;
|
|
1656
|
+
const layerType = layer.type;
|
|
1657
|
+
const color = getLayerColor(this.map, layerId, layerType);
|
|
1658
|
+
const svgMarkup = createLayerSymbolSVG(layerType, color);
|
|
1659
|
+
const symbolContainer = document.createElement("span");
|
|
1660
|
+
symbolContainer.className = "layer-control-symbol";
|
|
1661
|
+
symbolContainer.innerHTML = svgMarkup;
|
|
1662
|
+
symbolContainer.title = `Layer type: ${layerType}`;
|
|
1663
|
+
return symbolContainer;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Create a symbol element for a background layer
|
|
1667
|
+
* @param layer The layer specification
|
|
1668
|
+
* @returns The symbol HTML element
|
|
1669
|
+
*/
|
|
1670
|
+
createBackgroundLayerSymbol(layer) {
|
|
1671
|
+
const color = getLayerColorFromSpec(layer);
|
|
1672
|
+
const svgMarkup = createLayerSymbolSVG(layer.type, color, { size: 14 });
|
|
1673
|
+
const symbolContainer = document.createElement("span");
|
|
1674
|
+
symbolContainer.className = "background-legend-layer-symbol";
|
|
1675
|
+
symbolContainer.innerHTML = svgMarkup;
|
|
1676
|
+
symbolContainer.title = `Layer type: ${layer.type}`;
|
|
1677
|
+
return symbolContainer;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Create a symbol element for the Background layer group
|
|
1681
|
+
* Shows a stacked layers icon to represent multiple background layers
|
|
1682
|
+
* @returns The symbol HTML element
|
|
1683
|
+
*/
|
|
1684
|
+
createBackgroundGroupSymbol() {
|
|
1685
|
+
const svgMarkup = createBackgroundGroupSymbolSVG(16);
|
|
1686
|
+
const symbolContainer = document.createElement("span");
|
|
1687
|
+
symbolContainer.className = "layer-control-symbol";
|
|
1688
|
+
symbolContainer.innerHTML = svgMarkup;
|
|
1689
|
+
symbolContainer.title = "Background layers";
|
|
1690
|
+
return symbolContainer;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Toggle layer visibility
|
|
1694
|
+
*/
|
|
1695
|
+
toggleLayerVisibility(layerId, visible) {
|
|
1696
|
+
var _a;
|
|
1697
|
+
if (layerId === "Background") {
|
|
1698
|
+
this.toggleBackgroundVisibility(visible);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
this.state.isStyleOperationInProgress = true;
|
|
1702
|
+
if (this.state.layerStates[layerId]) {
|
|
1703
|
+
this.state.layerStates[layerId].visible = visible;
|
|
1704
|
+
}
|
|
1705
|
+
if ((_a = this.customLayerRegistry) == null ? void 0 : _a.setVisibility(layerId, visible)) {
|
|
1706
|
+
setTimeout(() => {
|
|
1707
|
+
this.state.isStyleOperationInProgress = false;
|
|
1708
|
+
}, 200);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
|
1712
|
+
setTimeout(() => {
|
|
1713
|
+
this.state.isStyleOperationInProgress = false;
|
|
1714
|
+
}, 200);
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Change layer opacity
|
|
1718
|
+
*/
|
|
1719
|
+
changeLayerOpacity(layerId, opacity) {
|
|
1720
|
+
var _a;
|
|
1721
|
+
if (layerId === "Background") {
|
|
1722
|
+
this.changeBackgroundOpacity(opacity);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
this.state.isStyleOperationInProgress = true;
|
|
1726
|
+
if (this.state.layerStates[layerId]) {
|
|
1727
|
+
this.state.layerStates[layerId].opacity = opacity;
|
|
1728
|
+
}
|
|
1729
|
+
if ((_a = this.customLayerRegistry) == null ? void 0 : _a.setOpacity(layerId, opacity)) {
|
|
1730
|
+
setTimeout(() => {
|
|
1731
|
+
this.state.isStyleOperationInProgress = false;
|
|
1732
|
+
}, 200);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const layerType = getLayerType(this.map, layerId);
|
|
1736
|
+
if (layerType) {
|
|
1737
|
+
setLayerOpacity(this.map, layerId, layerType, opacity);
|
|
1738
|
+
}
|
|
1739
|
+
setTimeout(() => {
|
|
1740
|
+
this.state.isStyleOperationInProgress = false;
|
|
1741
|
+
}, 200);
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Check if a layer is a user-added layer (vs basemap layer)
|
|
1745
|
+
* Used primarily for the background legend to determine which layers are background
|
|
1746
|
+
*/
|
|
1747
|
+
isUserAddedLayer(layerId) {
|
|
1748
|
+
if (this.state.layerStates[layerId] !== void 0 && layerId !== "Background") {
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
if (this.basemapLayerIds !== null && this.basemapLayerIds.size > 0) {
|
|
1752
|
+
return !this.basemapLayerIds.has(layerId);
|
|
1753
|
+
}
|
|
1754
|
+
if (this.initialLayerIds !== null && !this.initialLayerIds.has(layerId)) {
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1757
|
+
const layer = this.map.getLayer(layerId);
|
|
1758
|
+
if (layer) {
|
|
1759
|
+
const sourceId = layer.source;
|
|
1760
|
+
if (sourceId) {
|
|
1761
|
+
const userAddedSourceIds = this.detectUserAddedSources();
|
|
1762
|
+
return userAddedSourceIds.has(sourceId);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Toggle visibility for all background layers (basemap layers)
|
|
1769
|
+
*/
|
|
1770
|
+
toggleBackgroundVisibility(visible) {
|
|
1771
|
+
if (this.state.layerStates["Background"]) {
|
|
1772
|
+
this.state.layerStates["Background"].visible = visible;
|
|
1773
|
+
}
|
|
1774
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
1775
|
+
styleLayers.forEach((layer) => {
|
|
1776
|
+
if (!this.isUserAddedLayer(layer.id)) {
|
|
1777
|
+
this.state.backgroundLayerVisibility.set(layer.id, visible);
|
|
1778
|
+
this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
if (this.state.backgroundLegendOpen) {
|
|
1782
|
+
const legendPanel = this.panel.querySelector(".layer-control-background-legend");
|
|
1783
|
+
if (legendPanel) {
|
|
1784
|
+
const checkboxes = legendPanel.querySelectorAll(".background-legend-checkbox");
|
|
1785
|
+
checkboxes.forEach((checkbox) => {
|
|
1786
|
+
checkbox.checked = visible;
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Change opacity for all background layers (basemap layers)
|
|
1793
|
+
*/
|
|
1794
|
+
changeBackgroundOpacity(opacity) {
|
|
1795
|
+
if (this.state.layerStates["Background"]) {
|
|
1796
|
+
this.state.layerStates["Background"].opacity = opacity;
|
|
1797
|
+
}
|
|
1798
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
1799
|
+
styleLayers.forEach((styleLayer) => {
|
|
1800
|
+
if (!this.isUserAddedLayer(styleLayer.id)) {
|
|
1801
|
+
const layerType = getLayerType(this.map, styleLayer.id);
|
|
1802
|
+
if (layerType) {
|
|
1803
|
+
setLayerOpacity(this.map, styleLayer.id, layerType, opacity);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
// ===== Background Legend Methods =====
|
|
1809
|
+
/**
|
|
1810
|
+
* Create legend button for Background layer
|
|
1811
|
+
*/
|
|
1812
|
+
createBackgroundLegendButton() {
|
|
1813
|
+
const button = document.createElement("button");
|
|
1814
|
+
button.className = "layer-control-style-button layer-control-background-legend-button";
|
|
1815
|
+
button.innerHTML = "⚙";
|
|
1816
|
+
button.title = "Show background layer details";
|
|
1817
|
+
button.setAttribute("aria-label", "Show background layer visibility controls");
|
|
1818
|
+
button.setAttribute("aria-expanded", String(this.state.backgroundLegendOpen));
|
|
1819
|
+
button.addEventListener("click", (e) => {
|
|
1820
|
+
e.stopPropagation();
|
|
1821
|
+
this.toggleBackgroundLegend();
|
|
1822
|
+
});
|
|
1823
|
+
return button;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Toggle background legend panel visibility
|
|
1827
|
+
*/
|
|
1828
|
+
toggleBackgroundLegend() {
|
|
1829
|
+
if (this.state.backgroundLegendOpen) {
|
|
1830
|
+
this.closeBackgroundLegend();
|
|
1831
|
+
} else {
|
|
1832
|
+
this.openBackgroundLegend();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Open background legend panel
|
|
1837
|
+
*/
|
|
1838
|
+
openBackgroundLegend() {
|
|
1839
|
+
if (this.state.activeStyleEditor) {
|
|
1840
|
+
this.closeStyleEditor(this.state.activeStyleEditor);
|
|
1841
|
+
}
|
|
1842
|
+
const itemEl = this.panel.querySelector('[data-layer-id="Background"]');
|
|
1843
|
+
if (!itemEl) return;
|
|
1844
|
+
let legendPanel = itemEl.querySelector(".layer-control-background-legend");
|
|
1845
|
+
if (legendPanel) {
|
|
1846
|
+
const layerList = legendPanel.querySelector(".background-legend-layer-list");
|
|
1847
|
+
if (layerList) {
|
|
1848
|
+
this.populateBackgroundLayerList(layerList);
|
|
1849
|
+
}
|
|
1850
|
+
} else {
|
|
1851
|
+
legendPanel = this.createBackgroundLegendPanel();
|
|
1852
|
+
itemEl.appendChild(legendPanel);
|
|
1853
|
+
}
|
|
1854
|
+
this.state.backgroundLegendOpen = true;
|
|
1855
|
+
const button = itemEl.querySelector(".layer-control-background-legend-button");
|
|
1856
|
+
if (button) {
|
|
1857
|
+
button.setAttribute("aria-expanded", "true");
|
|
1858
|
+
button.classList.add("active");
|
|
1859
|
+
}
|
|
1860
|
+
setTimeout(() => {
|
|
1861
|
+
legendPanel == null ? void 0 : legendPanel.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1862
|
+
}, 50);
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Close background legend panel
|
|
1866
|
+
*/
|
|
1867
|
+
closeBackgroundLegend() {
|
|
1868
|
+
const itemEl = this.panel.querySelector('[data-layer-id="Background"]');
|
|
1869
|
+
if (!itemEl) return;
|
|
1870
|
+
const legendPanel = itemEl.querySelector(".layer-control-background-legend");
|
|
1871
|
+
if (legendPanel) {
|
|
1872
|
+
legendPanel.remove();
|
|
1873
|
+
}
|
|
1874
|
+
this.state.backgroundLegendOpen = false;
|
|
1875
|
+
const button = itemEl.querySelector(".layer-control-background-legend-button");
|
|
1876
|
+
if (button) {
|
|
1877
|
+
button.setAttribute("aria-expanded", "false");
|
|
1878
|
+
button.classList.remove("active");
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Create the background legend panel with individual layer controls
|
|
1883
|
+
*/
|
|
1884
|
+
createBackgroundLegendPanel() {
|
|
1885
|
+
const panel = document.createElement("div");
|
|
1886
|
+
panel.className = "layer-control-background-legend";
|
|
1887
|
+
const header = document.createElement("div");
|
|
1888
|
+
header.className = "background-legend-header";
|
|
1889
|
+
const title = document.createElement("span");
|
|
1890
|
+
title.className = "background-legend-title";
|
|
1891
|
+
title.textContent = "Background Layers";
|
|
1892
|
+
const closeBtn = document.createElement("button");
|
|
1893
|
+
closeBtn.className = "background-legend-close";
|
|
1894
|
+
closeBtn.innerHTML = "×";
|
|
1895
|
+
closeBtn.title = "Close";
|
|
1896
|
+
closeBtn.addEventListener("click", (e) => {
|
|
1897
|
+
e.stopPropagation();
|
|
1898
|
+
this.closeBackgroundLegend();
|
|
1899
|
+
});
|
|
1900
|
+
header.appendChild(title);
|
|
1901
|
+
header.appendChild(closeBtn);
|
|
1902
|
+
const actionsRow = document.createElement("div");
|
|
1903
|
+
actionsRow.className = "background-legend-actions";
|
|
1904
|
+
const showAllBtn = document.createElement("button");
|
|
1905
|
+
showAllBtn.className = "background-legend-action-btn";
|
|
1906
|
+
showAllBtn.textContent = "Show All";
|
|
1907
|
+
showAllBtn.addEventListener("click", () => this.setAllBackgroundLayersVisibility(true));
|
|
1908
|
+
const hideAllBtn = document.createElement("button");
|
|
1909
|
+
hideAllBtn.className = "background-legend-action-btn";
|
|
1910
|
+
hideAllBtn.textContent = "Hide All";
|
|
1911
|
+
hideAllBtn.addEventListener("click", () => this.setAllBackgroundLayersVisibility(false));
|
|
1912
|
+
actionsRow.appendChild(showAllBtn);
|
|
1913
|
+
actionsRow.appendChild(hideAllBtn);
|
|
1914
|
+
const filterRow = document.createElement("div");
|
|
1915
|
+
filterRow.className = "background-legend-filter";
|
|
1916
|
+
const filterCheckbox = document.createElement("input");
|
|
1917
|
+
filterCheckbox.type = "checkbox";
|
|
1918
|
+
filterCheckbox.className = "background-legend-filter-checkbox";
|
|
1919
|
+
filterCheckbox.id = "background-legend-only-rendered";
|
|
1920
|
+
filterCheckbox.checked = this.state.onlyRenderedFilter;
|
|
1921
|
+
filterCheckbox.addEventListener("change", () => {
|
|
1922
|
+
this.state.onlyRenderedFilter = filterCheckbox.checked;
|
|
1923
|
+
const layerList2 = panel.querySelector(".background-legend-layer-list");
|
|
1924
|
+
if (layerList2) {
|
|
1925
|
+
this.populateBackgroundLayerList(layerList2);
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
const filterLabel = document.createElement("label");
|
|
1929
|
+
filterLabel.className = "background-legend-filter-label";
|
|
1930
|
+
filterLabel.htmlFor = "background-legend-only-rendered";
|
|
1931
|
+
filterLabel.textContent = "Only rendered";
|
|
1932
|
+
filterRow.appendChild(filterCheckbox);
|
|
1933
|
+
filterRow.appendChild(filterLabel);
|
|
1934
|
+
const layerList = document.createElement("div");
|
|
1935
|
+
layerList.className = "background-legend-layer-list";
|
|
1936
|
+
this.populateBackgroundLayerList(layerList);
|
|
1937
|
+
panel.appendChild(header);
|
|
1938
|
+
panel.appendChild(actionsRow);
|
|
1939
|
+
panel.appendChild(filterRow);
|
|
1940
|
+
panel.appendChild(layerList);
|
|
1941
|
+
return panel;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Check if a layer is currently rendered in the map viewport
|
|
1945
|
+
*/
|
|
1946
|
+
isLayerRendered(layerId) {
|
|
1947
|
+
try {
|
|
1948
|
+
const layer = this.map.getLayer(layerId);
|
|
1949
|
+
if (!layer) return false;
|
|
1950
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
1951
|
+
if (visibility === "none") return false;
|
|
1952
|
+
if (layer.type === "raster" || layer.type === "hillshade") {
|
|
1953
|
+
return true;
|
|
1954
|
+
}
|
|
1955
|
+
if (layer.type === "background") {
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
const features = this.map.queryRenderedFeatures({ layers: [layerId] });
|
|
1959
|
+
return features.length > 0;
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
return true;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Populate the background layer list with individual layers
|
|
1966
|
+
*/
|
|
1967
|
+
populateBackgroundLayerList(container) {
|
|
1968
|
+
container.innerHTML = "";
|
|
1969
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
1970
|
+
styleLayers.forEach((layer) => {
|
|
1971
|
+
if (!this.isUserAddedLayer(layer.id)) {
|
|
1972
|
+
if (this.excludeDrawnLayers && this.isDrawnLayer(layer.id)) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (this.isExcludedByPattern(layer.id)) {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (this.state.onlyRenderedFilter && !this.isLayerRendered(layer.id)) {
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const layerRow = document.createElement("div");
|
|
1982
|
+
layerRow.className = "background-legend-layer-row";
|
|
1983
|
+
layerRow.setAttribute("data-background-layer-id", layer.id);
|
|
1984
|
+
const checkbox = document.createElement("input");
|
|
1985
|
+
checkbox.type = "checkbox";
|
|
1986
|
+
checkbox.className = "background-legend-checkbox";
|
|
1987
|
+
const visibility = this.map.getLayoutProperty(layer.id, "visibility");
|
|
1988
|
+
const isVisible = visibility !== "none";
|
|
1989
|
+
checkbox.checked = isVisible;
|
|
1990
|
+
this.state.backgroundLayerVisibility.set(layer.id, isVisible);
|
|
1991
|
+
checkbox.addEventListener("change", () => {
|
|
1992
|
+
this.toggleIndividualBackgroundLayer(layer.id, checkbox.checked);
|
|
1993
|
+
});
|
|
1994
|
+
const name = document.createElement("span");
|
|
1995
|
+
name.className = "background-legend-layer-name";
|
|
1996
|
+
name.textContent = this.generateFriendlyName(layer.id);
|
|
1997
|
+
name.title = layer.id;
|
|
1998
|
+
const typeIndicator = document.createElement("span");
|
|
1999
|
+
typeIndicator.className = "background-legend-layer-type";
|
|
2000
|
+
typeIndicator.textContent = layer.type;
|
|
2001
|
+
layerRow.appendChild(checkbox);
|
|
2002
|
+
if (this.showLayerSymbol) {
|
|
2003
|
+
const symbol = this.createBackgroundLayerSymbol(layer);
|
|
2004
|
+
if (symbol) {
|
|
2005
|
+
layerRow.appendChild(symbol);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
layerRow.appendChild(name);
|
|
2009
|
+
layerRow.appendChild(typeIndicator);
|
|
2010
|
+
container.appendChild(layerRow);
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
if (container.children.length === 0) {
|
|
2014
|
+
const emptyMsg = document.createElement("p");
|
|
2015
|
+
emptyMsg.className = "background-legend-empty";
|
|
2016
|
+
emptyMsg.textContent = this.state.onlyRenderedFilter ? "No rendered layers in current view." : "No background layers found.";
|
|
2017
|
+
container.appendChild(emptyMsg);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Toggle visibility of an individual background layer
|
|
2022
|
+
*/
|
|
2023
|
+
toggleIndividualBackgroundLayer(layerId, visible) {
|
|
2024
|
+
this.state.backgroundLayerVisibility.set(layerId, visible);
|
|
2025
|
+
this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
|
2026
|
+
this.updateBackgroundCheckboxState();
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Set visibility for all background layers
|
|
2030
|
+
*/
|
|
2031
|
+
setAllBackgroundLayersVisibility(visible) {
|
|
2032
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
2033
|
+
styleLayers.forEach((layer) => {
|
|
2034
|
+
if (!this.isUserAddedLayer(layer.id)) {
|
|
2035
|
+
this.state.backgroundLayerVisibility.set(layer.id, visible);
|
|
2036
|
+
this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
const legendPanel = this.panel.querySelector(".layer-control-background-legend");
|
|
2040
|
+
if (legendPanel) {
|
|
2041
|
+
const checkboxes = legendPanel.querySelectorAll(".background-legend-checkbox");
|
|
2042
|
+
checkboxes.forEach((checkbox) => {
|
|
2043
|
+
checkbox.checked = visible;
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
this.updateBackgroundCheckboxState();
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Update the main Background checkbox based on individual layer states
|
|
2050
|
+
*/
|
|
2051
|
+
updateBackgroundCheckboxState() {
|
|
2052
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
2053
|
+
let anyVisible = false;
|
|
2054
|
+
let allVisible = true;
|
|
2055
|
+
styleLayers.forEach((layer) => {
|
|
2056
|
+
if (!this.isUserAddedLayer(layer.id)) {
|
|
2057
|
+
const visible = this.state.backgroundLayerVisibility.get(layer.id);
|
|
2058
|
+
if (visible === true) anyVisible = true;
|
|
2059
|
+
if (visible === false) allVisible = false;
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
const backgroundItem = this.panel.querySelector('[data-layer-id="Background"]');
|
|
2063
|
+
if (backgroundItem) {
|
|
2064
|
+
const checkbox = backgroundItem.querySelector(".layer-control-checkbox");
|
|
2065
|
+
if (checkbox) {
|
|
2066
|
+
checkbox.checked = anyVisible;
|
|
2067
|
+
checkbox.indeterminate = anyVisible && !allVisible;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
if (this.state.layerStates["Background"]) {
|
|
2071
|
+
this.state.layerStates["Background"].visible = anyVisible;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Create style button for a layer
|
|
2076
|
+
*/
|
|
2077
|
+
createStyleButton(layerId) {
|
|
2078
|
+
if (layerId === "Background") {
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
const layerState = this.state.layerStates[layerId];
|
|
2082
|
+
const isCustomLayer = (layerState == null ? void 0 : layerState.isCustomLayer) === true;
|
|
2083
|
+
const button = document.createElement("button");
|
|
2084
|
+
button.className = "layer-control-style-button";
|
|
2085
|
+
button.innerHTML = "⚙";
|
|
2086
|
+
if (isCustomLayer) {
|
|
2087
|
+
button.title = "Layer info (style editing not available)";
|
|
2088
|
+
button.setAttribute("aria-label", `Layer info for ${layerId}`);
|
|
2089
|
+
} else {
|
|
2090
|
+
button.title = "Edit layer style";
|
|
2091
|
+
button.setAttribute("aria-label", `Edit style for ${layerId}`);
|
|
2092
|
+
}
|
|
2093
|
+
button.addEventListener("click", (e) => {
|
|
2094
|
+
e.stopPropagation();
|
|
2095
|
+
this.toggleStyleEditor(layerId);
|
|
2096
|
+
});
|
|
2097
|
+
return button;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Toggle style editor for a layer
|
|
2101
|
+
*/
|
|
2102
|
+
toggleStyleEditor(layerId) {
|
|
2103
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
2104
|
+
this.closeStyleEditor(layerId);
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
if (this.state.activeStyleEditor) {
|
|
2108
|
+
this.closeStyleEditor(this.state.activeStyleEditor);
|
|
2109
|
+
}
|
|
2110
|
+
this.openStyleEditor(layerId);
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Open style editor for a layer
|
|
2114
|
+
*/
|
|
2115
|
+
openStyleEditor(layerId) {
|
|
2116
|
+
var _a;
|
|
2117
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
2118
|
+
if (!itemEl) return;
|
|
2119
|
+
const layerState = this.state.layerStates[layerId];
|
|
2120
|
+
if (layerState == null ? void 0 : layerState.isCustomLayer) {
|
|
2121
|
+
const nativeLayerIds = (_a = this.customLayerRegistry) == null ? void 0 : _a.getNativeLayerIds(layerId);
|
|
2122
|
+
if (nativeLayerIds && nativeLayerIds.length > 0) {
|
|
2123
|
+
for (const nativeId of nativeLayerIds) {
|
|
2124
|
+
if (!this.state.originalStyles.has(nativeId)) {
|
|
2125
|
+
const nativeLayer = this.map.getLayer(nativeId);
|
|
2126
|
+
if (nativeLayer) {
|
|
2127
|
+
cacheOriginalLayerStyle(this.map, nativeId, this.state.originalStyles);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const editor3 = this.createNativeSubLayerStyleEditor(layerId, nativeLayerIds);
|
|
2132
|
+
if (editor3) {
|
|
2133
|
+
itemEl.appendChild(editor3);
|
|
2134
|
+
this.styleEditors.set(layerId, editor3);
|
|
2135
|
+
this.state.activeStyleEditor = layerId;
|
|
2136
|
+
setTimeout(() => {
|
|
2137
|
+
editor3.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2138
|
+
}, 50);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const editor2 = this.createCustomLayerInfoPanel(layerId);
|
|
2143
|
+
itemEl.appendChild(editor2);
|
|
2144
|
+
this.styleEditors.set(layerId, editor2);
|
|
2145
|
+
this.state.activeStyleEditor = layerId;
|
|
2146
|
+
setTimeout(() => {
|
|
2147
|
+
editor2.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2148
|
+
}, 50);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (!this.state.originalStyles.has(layerId)) {
|
|
2152
|
+
const layer = this.map.getLayer(layerId);
|
|
2153
|
+
if (layer) {
|
|
2154
|
+
cacheOriginalLayerStyle(this.map, layerId, this.state.originalStyles);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const editor = this.createStyleEditor(layerId);
|
|
2158
|
+
if (!editor) return;
|
|
2159
|
+
itemEl.appendChild(editor);
|
|
2160
|
+
this.styleEditors.set(layerId, editor);
|
|
2161
|
+
this.state.activeStyleEditor = layerId;
|
|
2162
|
+
setTimeout(() => {
|
|
2163
|
+
editor.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2164
|
+
}, 50);
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Create info panel for custom layers (style editing not supported)
|
|
2168
|
+
*/
|
|
2169
|
+
createCustomLayerInfoPanel(layerId) {
|
|
2170
|
+
const editor = document.createElement("div");
|
|
2171
|
+
editor.className = "layer-control-style-editor layer-control-custom-info";
|
|
2172
|
+
const header = document.createElement("div");
|
|
2173
|
+
header.className = "style-editor-header";
|
|
2174
|
+
const title = document.createElement("span");
|
|
2175
|
+
title.className = "style-editor-title";
|
|
2176
|
+
title.textContent = "Layer Info";
|
|
2177
|
+
const closeBtn = document.createElement("button");
|
|
2178
|
+
closeBtn.className = "style-editor-close";
|
|
2179
|
+
closeBtn.innerHTML = "×";
|
|
2180
|
+
closeBtn.title = "Close";
|
|
2181
|
+
closeBtn.addEventListener("click", (e) => {
|
|
2182
|
+
e.stopPropagation();
|
|
2183
|
+
this.closeStyleEditor(layerId);
|
|
2184
|
+
});
|
|
2185
|
+
header.appendChild(title);
|
|
2186
|
+
header.appendChild(closeBtn);
|
|
2187
|
+
const content = document.createElement("div");
|
|
2188
|
+
content.className = "style-editor-controls";
|
|
2189
|
+
const infoText = document.createElement("p");
|
|
2190
|
+
infoText.className = "layer-control-custom-info-text";
|
|
2191
|
+
infoText.textContent = `This is a custom layer. Style editing is not available for this layer type. Use the visibility toggle and opacity slider to control the layer.`;
|
|
2192
|
+
content.appendChild(infoText);
|
|
2193
|
+
const actions = document.createElement("div");
|
|
2194
|
+
actions.className = "style-editor-actions";
|
|
2195
|
+
const removeBtn = document.createElement("button");
|
|
2196
|
+
removeBtn.className = "style-editor-button style-editor-button-remove";
|
|
2197
|
+
removeBtn.textContent = "Remove";
|
|
2198
|
+
removeBtn.title = "Remove layer from map";
|
|
2199
|
+
removeBtn.addEventListener("click", (e) => {
|
|
2200
|
+
e.stopPropagation();
|
|
2201
|
+
if (confirm("Are you sure you want to remove this layer?")) {
|
|
2202
|
+
this.closeStyleEditor(layerId);
|
|
2203
|
+
this.removeLayer(layerId);
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
const closeActionBtn = document.createElement("button");
|
|
2207
|
+
closeActionBtn.className = "style-editor-button style-editor-button-close";
|
|
2208
|
+
closeActionBtn.textContent = "Close";
|
|
2209
|
+
closeActionBtn.addEventListener("click", (e) => {
|
|
2210
|
+
e.stopPropagation();
|
|
2211
|
+
this.closeStyleEditor(layerId);
|
|
2212
|
+
});
|
|
2213
|
+
actions.appendChild(removeBtn);
|
|
2214
|
+
actions.appendChild(closeActionBtn);
|
|
2215
|
+
editor.appendChild(header);
|
|
2216
|
+
editor.appendChild(content);
|
|
2217
|
+
editor.appendChild(actions);
|
|
2218
|
+
return editor;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Create a combined style editor for custom layers with native MapLibre sublayers.
|
|
2222
|
+
* Groups controls by sublayer type (fill, line, circle, etc.).
|
|
2223
|
+
*/
|
|
2224
|
+
createNativeSubLayerStyleEditor(layerId, nativeLayerIds) {
|
|
2225
|
+
const layersByType = /* @__PURE__ */ new Map();
|
|
2226
|
+
for (const nativeId of nativeLayerIds) {
|
|
2227
|
+
const layer = this.map.getLayer(nativeId);
|
|
2228
|
+
if (layer) {
|
|
2229
|
+
const type = layer.type;
|
|
2230
|
+
if (!layersByType.has(type)) {
|
|
2231
|
+
layersByType.set(type, []);
|
|
2232
|
+
}
|
|
2233
|
+
layersByType.get(type).push(nativeId);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (layersByType.size === 0) return null;
|
|
2237
|
+
const editor = document.createElement("div");
|
|
2238
|
+
editor.className = "layer-control-style-editor";
|
|
2239
|
+
const header = document.createElement("div");
|
|
2240
|
+
header.className = "style-editor-header";
|
|
2241
|
+
const title = document.createElement("span");
|
|
2242
|
+
title.className = "style-editor-title";
|
|
2243
|
+
title.textContent = "Edit Style";
|
|
2244
|
+
const closeBtn = document.createElement("button");
|
|
2245
|
+
closeBtn.className = "style-editor-close";
|
|
2246
|
+
closeBtn.innerHTML = "×";
|
|
2247
|
+
closeBtn.title = "Close";
|
|
2248
|
+
closeBtn.addEventListener("click", (e) => {
|
|
2249
|
+
e.stopPropagation();
|
|
2250
|
+
this.closeStyleEditor(layerId);
|
|
2251
|
+
});
|
|
2252
|
+
header.appendChild(title);
|
|
2253
|
+
header.appendChild(closeBtn);
|
|
2254
|
+
editor.appendChild(header);
|
|
2255
|
+
const controls = document.createElement("div");
|
|
2256
|
+
controls.className = "style-editor-controls";
|
|
2257
|
+
const typeLabels = {
|
|
2258
|
+
fill: "Fill",
|
|
2259
|
+
line: "Line",
|
|
2260
|
+
circle: "Circle",
|
|
2261
|
+
symbol: "Symbol",
|
|
2262
|
+
raster: "Raster"
|
|
2263
|
+
};
|
|
2264
|
+
for (const [type, ids] of layersByType) {
|
|
2265
|
+
if (layersByType.size > 1) {
|
|
2266
|
+
const sectionHeader = document.createElement("div");
|
|
2267
|
+
sectionHeader.className = "style-editor-section-header";
|
|
2268
|
+
sectionHeader.textContent = typeLabels[type] || type;
|
|
2269
|
+
controls.appendChild(sectionHeader);
|
|
2270
|
+
}
|
|
2271
|
+
const primaryId = ids[0];
|
|
2272
|
+
this.addStyleControlsForNativeGroup(controls, ids, primaryId, type);
|
|
2273
|
+
}
|
|
2274
|
+
const actions = document.createElement("div");
|
|
2275
|
+
actions.className = "style-editor-actions";
|
|
2276
|
+
const resetBtn = document.createElement("button");
|
|
2277
|
+
resetBtn.className = "style-editor-button style-editor-button-reset";
|
|
2278
|
+
resetBtn.textContent = "Reset";
|
|
2279
|
+
resetBtn.addEventListener("click", (e) => {
|
|
2280
|
+
e.stopPropagation();
|
|
2281
|
+
for (const nativeId of nativeLayerIds) {
|
|
2282
|
+
restoreOriginalStyle(this.map, nativeId, this.state.originalStyles);
|
|
2283
|
+
}
|
|
2284
|
+
this.closeStyleEditor(layerId);
|
|
2285
|
+
this.openStyleEditor(layerId);
|
|
2286
|
+
});
|
|
2287
|
+
const removeBtn = document.createElement("button");
|
|
2288
|
+
removeBtn.className = "style-editor-button style-editor-button-remove";
|
|
2289
|
+
removeBtn.textContent = "Remove";
|
|
2290
|
+
removeBtn.title = "Remove layer from map";
|
|
2291
|
+
removeBtn.addEventListener("click", (e) => {
|
|
2292
|
+
e.stopPropagation();
|
|
2293
|
+
if (confirm("Are you sure you want to remove this layer?")) {
|
|
2294
|
+
this.closeStyleEditor(layerId);
|
|
2295
|
+
this.removeLayer(layerId);
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2298
|
+
const closeActionBtn = document.createElement("button");
|
|
2299
|
+
closeActionBtn.className = "style-editor-button style-editor-button-close";
|
|
2300
|
+
closeActionBtn.textContent = "Close";
|
|
2301
|
+
closeActionBtn.addEventListener("click", (e) => {
|
|
2302
|
+
e.stopPropagation();
|
|
2303
|
+
this.closeStyleEditor(layerId);
|
|
2304
|
+
});
|
|
2305
|
+
actions.appendChild(resetBtn);
|
|
2306
|
+
actions.appendChild(removeBtn);
|
|
2307
|
+
actions.appendChild(closeActionBtn);
|
|
2308
|
+
editor.appendChild(controls);
|
|
2309
|
+
editor.appendChild(actions);
|
|
2310
|
+
return editor;
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Add style controls for a group of native layers of the same type.
|
|
2314
|
+
* Changes to any control are applied to all layers in the group.
|
|
2315
|
+
*/
|
|
2316
|
+
addStyleControlsForNativeGroup(container, layerIds, primaryLayerId, layerType) {
|
|
2317
|
+
this.nativeLayerGroups.set(primaryLayerId, layerIds);
|
|
2318
|
+
this.addStyleControlsForLayerType(container, primaryLayerId, layerType);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Close style editor for a layer
|
|
2322
|
+
*/
|
|
2323
|
+
closeStyleEditor(layerId) {
|
|
2324
|
+
const editor = this.styleEditors.get(layerId);
|
|
2325
|
+
if (editor) {
|
|
2326
|
+
editor.remove();
|
|
2327
|
+
this.styleEditors.delete(layerId);
|
|
2328
|
+
}
|
|
2329
|
+
this.nativeLayerGroups.clear();
|
|
2330
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
2331
|
+
this.state.activeStyleEditor = null;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Create style editor UI
|
|
2336
|
+
*/
|
|
2337
|
+
createStyleEditor(layerId) {
|
|
2338
|
+
const layer = this.map.getLayer(layerId);
|
|
2339
|
+
if (!layer) return null;
|
|
2340
|
+
const editor = document.createElement("div");
|
|
2341
|
+
editor.className = "layer-control-style-editor";
|
|
2342
|
+
const header = document.createElement("div");
|
|
2343
|
+
header.className = "style-editor-header";
|
|
2344
|
+
const title = document.createElement("span");
|
|
2345
|
+
title.className = "style-editor-title";
|
|
2346
|
+
title.textContent = "Edit Style";
|
|
2347
|
+
const closeBtn = document.createElement("button");
|
|
2348
|
+
closeBtn.className = "style-editor-close";
|
|
2349
|
+
closeBtn.innerHTML = "×";
|
|
2350
|
+
closeBtn.title = "Close";
|
|
2351
|
+
closeBtn.addEventListener("click", (e) => {
|
|
2352
|
+
e.stopPropagation();
|
|
2353
|
+
this.closeStyleEditor(layerId);
|
|
2354
|
+
});
|
|
2355
|
+
header.appendChild(title);
|
|
2356
|
+
header.appendChild(closeBtn);
|
|
2357
|
+
const controls = document.createElement("div");
|
|
2358
|
+
controls.className = "style-editor-controls";
|
|
2359
|
+
const layerType = layer.type;
|
|
2360
|
+
this.addStyleControlsForLayerType(controls, layerId, layerType);
|
|
2361
|
+
const actions = document.createElement("div");
|
|
2362
|
+
actions.className = "style-editor-actions";
|
|
2363
|
+
const resetBtn = document.createElement("button");
|
|
2364
|
+
resetBtn.className = "style-editor-button style-editor-button-reset";
|
|
2365
|
+
resetBtn.textContent = "Reset";
|
|
2366
|
+
resetBtn.addEventListener("click", (e) => {
|
|
2367
|
+
e.stopPropagation();
|
|
2368
|
+
this.resetLayerStyle(layerId);
|
|
2369
|
+
});
|
|
2370
|
+
const removeBtn = document.createElement("button");
|
|
2371
|
+
removeBtn.className = "style-editor-button style-editor-button-remove";
|
|
2372
|
+
removeBtn.textContent = "Remove";
|
|
2373
|
+
removeBtn.title = "Remove layer from map";
|
|
2374
|
+
removeBtn.addEventListener("click", (e) => {
|
|
2375
|
+
e.stopPropagation();
|
|
2376
|
+
if (confirm("Are you sure you want to remove this layer?")) {
|
|
2377
|
+
this.closeStyleEditor(layerId);
|
|
2378
|
+
this.removeLayer(layerId);
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
const closeActionBtn = document.createElement("button");
|
|
2382
|
+
closeActionBtn.className = "style-editor-button style-editor-button-close";
|
|
2383
|
+
closeActionBtn.textContent = "Close";
|
|
2384
|
+
closeActionBtn.addEventListener("click", (e) => {
|
|
2385
|
+
e.stopPropagation();
|
|
2386
|
+
this.closeStyleEditor(layerId);
|
|
2387
|
+
});
|
|
2388
|
+
actions.appendChild(resetBtn);
|
|
2389
|
+
actions.appendChild(removeBtn);
|
|
2390
|
+
actions.appendChild(closeActionBtn);
|
|
2391
|
+
editor.appendChild(header);
|
|
2392
|
+
editor.appendChild(controls);
|
|
2393
|
+
editor.appendChild(actions);
|
|
2394
|
+
return editor;
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Add style controls based on layer type
|
|
2398
|
+
*/
|
|
2399
|
+
addStyleControlsForLayerType(container, layerId, layerType) {
|
|
2400
|
+
switch (layerType) {
|
|
2401
|
+
case "fill":
|
|
2402
|
+
this.addFillControls(container, layerId);
|
|
2403
|
+
break;
|
|
2404
|
+
case "line":
|
|
2405
|
+
this.addLineControls(container, layerId);
|
|
2406
|
+
break;
|
|
2407
|
+
case "circle":
|
|
2408
|
+
this.addCircleControls(container, layerId);
|
|
2409
|
+
break;
|
|
2410
|
+
case "raster":
|
|
2411
|
+
this.addRasterControls(container, layerId);
|
|
2412
|
+
break;
|
|
2413
|
+
case "symbol":
|
|
2414
|
+
this.addSymbolControls(container, layerId);
|
|
2415
|
+
break;
|
|
2416
|
+
default:
|
|
2417
|
+
container.textContent = `Style controls for ${layerType} layers not yet implemented.`;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Add controls for fill layers
|
|
2422
|
+
*/
|
|
2423
|
+
addFillControls(container, layerId) {
|
|
2424
|
+
var _a;
|
|
2425
|
+
const style = this.map.getStyle();
|
|
2426
|
+
const layer = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
2427
|
+
let fillColor = void 0;
|
|
2428
|
+
if (layer && "paint" in layer && layer.paint && "fill-color" in layer.paint) {
|
|
2429
|
+
fillColor = layer.paint["fill-color"];
|
|
2430
|
+
}
|
|
2431
|
+
if (!fillColor) {
|
|
2432
|
+
fillColor = this.map.getPaintProperty(layerId, "fill-color");
|
|
2433
|
+
}
|
|
2434
|
+
this.createColorControl(container, layerId, "fill-color", "Fill Color", normalizeColor(fillColor || "#088"));
|
|
2435
|
+
const fillOpacity = this.map.getPaintProperty(layerId, "fill-opacity");
|
|
2436
|
+
if (fillOpacity !== void 0 && typeof fillOpacity === "number") {
|
|
2437
|
+
this.createSliderControl(container, layerId, "fill-opacity", "Fill Opacity", fillOpacity, 0, 1, 0.05);
|
|
2438
|
+
}
|
|
2439
|
+
const outlineColor = this.map.getPaintProperty(layerId, "fill-outline-color");
|
|
2440
|
+
if (outlineColor !== void 0) {
|
|
2441
|
+
this.createColorControl(container, layerId, "fill-outline-color", "Outline Color", normalizeColor(outlineColor));
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Add controls for line layers
|
|
2446
|
+
*/
|
|
2447
|
+
addLineControls(container, layerId) {
|
|
2448
|
+
var _a;
|
|
2449
|
+
const style = this.map.getStyle();
|
|
2450
|
+
const layer = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
2451
|
+
let lineColor = void 0;
|
|
2452
|
+
if (layer && "paint" in layer && layer.paint && "line-color" in layer.paint) {
|
|
2453
|
+
lineColor = layer.paint["line-color"];
|
|
2454
|
+
}
|
|
2455
|
+
if (!lineColor) {
|
|
2456
|
+
lineColor = this.map.getPaintProperty(layerId, "line-color");
|
|
2457
|
+
}
|
|
2458
|
+
this.createColorControl(container, layerId, "line-color", "Line Color", normalizeColor(lineColor || "#000"));
|
|
2459
|
+
const lineWidth = this.map.getPaintProperty(layerId, "line-width");
|
|
2460
|
+
this.createSliderControl(container, layerId, "line-width", "Line Width", typeof lineWidth === "number" ? lineWidth : 1, 0, 20, 0.5);
|
|
2461
|
+
const lineOpacity = this.map.getPaintProperty(layerId, "line-opacity");
|
|
2462
|
+
if (lineOpacity !== void 0 && typeof lineOpacity === "number") {
|
|
2463
|
+
this.createSliderControl(container, layerId, "line-opacity", "Line Opacity", lineOpacity, 0, 1, 0.05);
|
|
2464
|
+
}
|
|
2465
|
+
const lineBlur = this.map.getPaintProperty(layerId, "line-blur");
|
|
2466
|
+
if (lineBlur !== void 0 && typeof lineBlur === "number") {
|
|
2467
|
+
this.createSliderControl(container, layerId, "line-blur", "Line Blur", lineBlur, 0, 5, 0.1);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Add controls for circle layers
|
|
2472
|
+
*/
|
|
2473
|
+
addCircleControls(container, layerId) {
|
|
2474
|
+
var _a;
|
|
2475
|
+
const style = this.map.getStyle();
|
|
2476
|
+
const layer = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
2477
|
+
let circleColor = void 0;
|
|
2478
|
+
if (layer && "paint" in layer && layer.paint && "circle-color" in layer.paint) {
|
|
2479
|
+
circleColor = layer.paint["circle-color"];
|
|
2480
|
+
}
|
|
2481
|
+
if (!circleColor) {
|
|
2482
|
+
circleColor = this.map.getPaintProperty(layerId, "circle-color");
|
|
2483
|
+
}
|
|
2484
|
+
this.createColorControl(container, layerId, "circle-color", "Circle Color", normalizeColor(circleColor || "#000"));
|
|
2485
|
+
const circleRadius = this.map.getPaintProperty(layerId, "circle-radius");
|
|
2486
|
+
this.createSliderControl(container, layerId, "circle-radius", "Radius", typeof circleRadius === "number" ? circleRadius : 5, 0, 40, 0.5);
|
|
2487
|
+
const circleOpacity = this.map.getPaintProperty(layerId, "circle-opacity");
|
|
2488
|
+
if (circleOpacity !== void 0 && typeof circleOpacity === "number") {
|
|
2489
|
+
this.createSliderControl(container, layerId, "circle-opacity", "Opacity", circleOpacity, 0, 1, 0.05);
|
|
2490
|
+
}
|
|
2491
|
+
const strokeColor = this.map.getPaintProperty(layerId, "circle-stroke-color");
|
|
2492
|
+
if (strokeColor !== void 0) {
|
|
2493
|
+
this.createColorControl(container, layerId, "circle-stroke-color", "Stroke Color", normalizeColor(strokeColor));
|
|
2494
|
+
}
|
|
2495
|
+
const strokeWidth = this.map.getPaintProperty(layerId, "circle-stroke-width");
|
|
2496
|
+
if (strokeWidth !== void 0 && typeof strokeWidth === "number") {
|
|
2497
|
+
this.createSliderControl(container, layerId, "circle-stroke-width", "Stroke Width", strokeWidth, 0, 10, 0.1);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Add controls for raster layers
|
|
2502
|
+
*/
|
|
2503
|
+
addRasterControls(container, layerId) {
|
|
2504
|
+
const rasterOpacity = this.map.getPaintProperty(layerId, "raster-opacity");
|
|
2505
|
+
this.createSliderControl(container, layerId, "raster-opacity", "Opacity", typeof rasterOpacity === "number" ? rasterOpacity : 1, 0, 1, 0.05);
|
|
2506
|
+
const brightnessMin = this.map.getPaintProperty(layerId, "raster-brightness-min");
|
|
2507
|
+
this.createSliderControl(container, layerId, "raster-brightness-min", "Brightness Min", typeof brightnessMin === "number" ? brightnessMin : 0, -1, 1, 0.05);
|
|
2508
|
+
const brightnessMax = this.map.getPaintProperty(layerId, "raster-brightness-max");
|
|
2509
|
+
this.createSliderControl(container, layerId, "raster-brightness-max", "Brightness Max", typeof brightnessMax === "number" ? brightnessMax : 1, -1, 1, 0.05);
|
|
2510
|
+
const saturation = this.map.getPaintProperty(layerId, "raster-saturation");
|
|
2511
|
+
this.createSliderControl(container, layerId, "raster-saturation", "Saturation", typeof saturation === "number" ? saturation : 0, -1, 1, 0.05);
|
|
2512
|
+
const contrast = this.map.getPaintProperty(layerId, "raster-contrast");
|
|
2513
|
+
this.createSliderControl(container, layerId, "raster-contrast", "Contrast", typeof contrast === "number" ? contrast : 0, -1, 1, 0.05);
|
|
2514
|
+
const hueRotate = this.map.getPaintProperty(layerId, "raster-hue-rotate");
|
|
2515
|
+
this.createSliderControl(container, layerId, "raster-hue-rotate", "Hue Rotate", typeof hueRotate === "number" ? hueRotate : 0, 0, 350, 5);
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Add controls for symbol layers
|
|
2519
|
+
*/
|
|
2520
|
+
addSymbolControls(container, layerId) {
|
|
2521
|
+
const textColor = this.map.getPaintProperty(layerId, "text-color");
|
|
2522
|
+
if (textColor !== void 0) {
|
|
2523
|
+
this.createColorControl(container, layerId, "text-color", "Text Color", normalizeColor(textColor));
|
|
2524
|
+
}
|
|
2525
|
+
const textOpacity = this.map.getPaintProperty(layerId, "text-opacity");
|
|
2526
|
+
if (textOpacity !== void 0 && typeof textOpacity === "number") {
|
|
2527
|
+
this.createSliderControl(container, layerId, "text-opacity", "Text Opacity", textOpacity, 0, 1, 0.05);
|
|
2528
|
+
}
|
|
2529
|
+
const iconOpacity = this.map.getPaintProperty(layerId, "icon-opacity");
|
|
2530
|
+
if (iconOpacity !== void 0 && typeof iconOpacity === "number") {
|
|
2531
|
+
this.createSliderControl(container, layerId, "icon-opacity", "Icon Opacity", iconOpacity, 0, 1, 0.05);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Create a color control
|
|
2536
|
+
*/
|
|
2537
|
+
createColorControl(container, layerId, property, label, initialValue) {
|
|
2538
|
+
const controlGroup = document.createElement("div");
|
|
2539
|
+
controlGroup.className = "style-control-group";
|
|
2540
|
+
const labelEl = document.createElement("label");
|
|
2541
|
+
labelEl.className = "style-control-label";
|
|
2542
|
+
labelEl.textContent = label;
|
|
2543
|
+
const inputWrapper = document.createElement("div");
|
|
2544
|
+
inputWrapper.className = "style-control-color-group";
|
|
2545
|
+
const colorInput = document.createElement("input");
|
|
2546
|
+
colorInput.type = "color";
|
|
2547
|
+
colorInput.className = "style-control-color-picker";
|
|
2548
|
+
colorInput.value = initialValue;
|
|
2549
|
+
colorInput.dataset.property = property;
|
|
2550
|
+
const hexDisplay = document.createElement("input");
|
|
2551
|
+
hexDisplay.type = "text";
|
|
2552
|
+
hexDisplay.className = "style-control-color-value";
|
|
2553
|
+
hexDisplay.value = initialValue;
|
|
2554
|
+
hexDisplay.readOnly = true;
|
|
2555
|
+
colorInput.addEventListener("input", () => {
|
|
2556
|
+
const color = colorInput.value;
|
|
2557
|
+
hexDisplay.value = color;
|
|
2558
|
+
const targetIds = this.nativeLayerGroups.get(layerId) || [layerId];
|
|
2559
|
+
for (const id of targetIds) {
|
|
2560
|
+
this.map.setPaintProperty(id, property, color);
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
inputWrapper.appendChild(colorInput);
|
|
2564
|
+
inputWrapper.appendChild(hexDisplay);
|
|
2565
|
+
controlGroup.appendChild(labelEl);
|
|
2566
|
+
controlGroup.appendChild(inputWrapper);
|
|
2567
|
+
container.appendChild(controlGroup);
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* Create a slider control
|
|
2571
|
+
*/
|
|
2572
|
+
createSliderControl(container, layerId, property, label, initialValue, min, max, step) {
|
|
2573
|
+
const controlGroup = document.createElement("div");
|
|
2574
|
+
controlGroup.className = "style-control-group";
|
|
2575
|
+
const labelEl = document.createElement("label");
|
|
2576
|
+
labelEl.className = "style-control-label";
|
|
2577
|
+
labelEl.textContent = label;
|
|
2578
|
+
const inputWrapper = document.createElement("div");
|
|
2579
|
+
inputWrapper.className = "style-control-input-wrapper";
|
|
2580
|
+
const slider = document.createElement("input");
|
|
2581
|
+
slider.type = "range";
|
|
2582
|
+
slider.className = "style-control-slider";
|
|
2583
|
+
slider.min = String(min);
|
|
2584
|
+
slider.max = String(max);
|
|
2585
|
+
slider.step = String(step);
|
|
2586
|
+
slider.value = String(initialValue);
|
|
2587
|
+
slider.dataset.property = property;
|
|
2588
|
+
const valueDisplay = document.createElement("span");
|
|
2589
|
+
valueDisplay.className = "style-control-value";
|
|
2590
|
+
valueDisplay.textContent = formatNumericValue(initialValue, step);
|
|
2591
|
+
slider.addEventListener("input", () => {
|
|
2592
|
+
const value = parseFloat(slider.value);
|
|
2593
|
+
valueDisplay.textContent = formatNumericValue(value, step);
|
|
2594
|
+
const targetIds = this.nativeLayerGroups.get(layerId) || [layerId];
|
|
2595
|
+
for (const id of targetIds) {
|
|
2596
|
+
this.map.setPaintProperty(id, property, value);
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
inputWrapper.appendChild(slider);
|
|
2600
|
+
inputWrapper.appendChild(valueDisplay);
|
|
2601
|
+
controlGroup.appendChild(labelEl);
|
|
2602
|
+
controlGroup.appendChild(inputWrapper);
|
|
2603
|
+
container.appendChild(controlGroup);
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Reset layer style to original
|
|
2607
|
+
*/
|
|
2608
|
+
resetLayerStyle(layerId) {
|
|
2609
|
+
const originalStyle = this.state.originalStyles.get(layerId);
|
|
2610
|
+
if (!originalStyle) return;
|
|
2611
|
+
restoreOriginalStyle(this.map, layerId, this.state.originalStyles);
|
|
2612
|
+
const editor = this.styleEditors.get(layerId);
|
|
2613
|
+
if (editor) {
|
|
2614
|
+
const sliders = editor.querySelectorAll(".style-control-slider");
|
|
2615
|
+
sliders.forEach((slider) => {
|
|
2616
|
+
var _a;
|
|
2617
|
+
const property = slider.dataset.property;
|
|
2618
|
+
if (property) {
|
|
2619
|
+
const value = this.map.getPaintProperty(layerId, property);
|
|
2620
|
+
if (value !== void 0 && typeof value === "number") {
|
|
2621
|
+
slider.value = String(value);
|
|
2622
|
+
const valueDisplay = (_a = slider.parentElement) == null ? void 0 : _a.querySelector(".style-control-value");
|
|
2623
|
+
if (valueDisplay) {
|
|
2624
|
+
const step = parseFloat(slider.step);
|
|
2625
|
+
valueDisplay.textContent = formatNumericValue(value, step);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
const colorPickers = editor.querySelectorAll(".style-control-color-picker");
|
|
2631
|
+
colorPickers.forEach((picker) => {
|
|
2632
|
+
var _a;
|
|
2633
|
+
const property = picker.dataset.property;
|
|
2634
|
+
if (property) {
|
|
2635
|
+
const value = this.map.getPaintProperty(layerId, property);
|
|
2636
|
+
if (value !== void 0) {
|
|
2637
|
+
const hexColor = normalizeColor(value);
|
|
2638
|
+
picker.value = hexColor;
|
|
2639
|
+
const hexDisplay = (_a = picker.parentElement) == null ? void 0 : _a.querySelector(".style-control-color-value");
|
|
2640
|
+
if (hexDisplay) {
|
|
2641
|
+
hexDisplay.textContent = hexColor;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Update layer states from map (sync UI with map)
|
|
2650
|
+
*/
|
|
2651
|
+
updateLayerStatesFromMap() {
|
|
2652
|
+
if (this.state.userInteractingWithSlider) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
2656
|
+
var _a;
|
|
2657
|
+
try {
|
|
2658
|
+
if ((_a = this.state.layerStates[layerId]) == null ? void 0 : _a.isCustomLayer) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (layerId === "Background") {
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
const layer = this.map.getLayer(layerId);
|
|
2665
|
+
if (!layer) return;
|
|
2666
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
2667
|
+
const isVisible = visibility !== "none";
|
|
2668
|
+
const layerType = layer.type;
|
|
2669
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
2670
|
+
if (this.state.layerStates[layerId]) {
|
|
2671
|
+
this.state.layerStates[layerId].visible = isVisible;
|
|
2672
|
+
this.state.layerStates[layerId].opacity = opacity;
|
|
2673
|
+
}
|
|
2674
|
+
this.updateUIForLayer(layerId, isVisible, opacity);
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
console.warn(`Failed to update state for layer ${layerId}:`, error);
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Update UI elements for a specific layer
|
|
2682
|
+
*/
|
|
2683
|
+
updateUIForLayer(layerId, visible, opacity) {
|
|
2684
|
+
const layerItems = this.panel.querySelectorAll(".layer-control-item");
|
|
2685
|
+
layerItems.forEach((item) => {
|
|
2686
|
+
if (item.dataset.layerId === layerId) {
|
|
2687
|
+
const checkbox = item.querySelector(".layer-control-checkbox");
|
|
2688
|
+
const opacitySlider = item.querySelector(".layer-control-opacity");
|
|
2689
|
+
if (checkbox) {
|
|
2690
|
+
checkbox.checked = visible;
|
|
2691
|
+
}
|
|
2692
|
+
if (opacitySlider) {
|
|
2693
|
+
opacitySlider.value = String(opacity);
|
|
2694
|
+
opacitySlider.title = `Opacity: ${Math.round(opacity * 100)}%`;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Check for new layers and add them to the control, remove deleted layers
|
|
2701
|
+
*/
|
|
2702
|
+
checkForNewLayers() {
|
|
2703
|
+
if (this.state.isStyleOperationInProgress) {
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
try {
|
|
2707
|
+
const style = this.map.getStyle();
|
|
2708
|
+
if (!style || !style.layers) {
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
const currentMapLayerIds = new Set(style.layers.map((layer) => layer.id));
|
|
2712
|
+
const isAutoDetectMode = this.targetLayers.length === 0 || this.targetLayers.length === 1 && this.targetLayers[0] === "Background" || this.targetLayers.every((id) => id === "Background" || this.state.layerStates[id]);
|
|
2713
|
+
const newLayers = [];
|
|
2714
|
+
const useBasemapStyleDetection = this.basemapLayerIds !== null && this.basemapLayerIds.size > 0;
|
|
2715
|
+
const useInitialLayerDetection = !useBasemapStyleDetection && this.initialLayerIds !== null && this.initialLayerIds.size > 0;
|
|
2716
|
+
currentMapLayerIds.forEach((layerId) => {
|
|
2717
|
+
if (layerId !== "Background" && !this.state.layerStates[layerId]) {
|
|
2718
|
+
const layer = this.map.getLayer(layerId);
|
|
2719
|
+
if (layer) {
|
|
2720
|
+
if (this.initialLayerIds !== null && this.initialLayerIds.has(layerId)) {
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (isAutoDetectMode) {
|
|
2724
|
+
if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (this.isExcludedByPattern(layerId)) {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if (!this.isUserAddedLayer(layerId)) {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (useBasemapStyleDetection) {
|
|
2734
|
+
if (this.basemapLayerIds.has(layerId)) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
} else if (useInitialLayerDetection) {
|
|
2738
|
+
if (this.initialLayerIds.has(layerId)) {
|
|
2739
|
+
const userAddedSourceIds = this.detectUserAddedSources();
|
|
2740
|
+
const sourceId = layer.source;
|
|
2741
|
+
if (!sourceId || !userAddedSourceIds.has(sourceId)) {
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
} else {
|
|
2746
|
+
const userAddedSourceIds = this.detectUserAddedSources();
|
|
2747
|
+
const sourceId = layer.source;
|
|
2748
|
+
if (!sourceId || !userAddedSourceIds.has(sourceId)) {
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
newLayers.push(layerId);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2757
|
+
const removedLayers = [];
|
|
2758
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
2759
|
+
const state = this.state.layerStates[layerId];
|
|
2760
|
+
if (layerId !== "Background" && !state.isCustomLayer && !currentMapLayerIds.has(layerId)) {
|
|
2761
|
+
removedLayers.push(layerId);
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
if (removedLayers.length > 0) {
|
|
2765
|
+
removedLayers.forEach((layerId) => {
|
|
2766
|
+
delete this.state.layerStates[layerId];
|
|
2767
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
2768
|
+
if (itemEl) {
|
|
2769
|
+
itemEl.remove();
|
|
2770
|
+
}
|
|
2771
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
2772
|
+
this.state.activeStyleEditor = null;
|
|
2773
|
+
}
|
|
2774
|
+
this.styleEditors.delete(layerId);
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
if (newLayers.length > 0) {
|
|
2778
|
+
newLayers.forEach((layerId) => {
|
|
2779
|
+
const layer = this.map.getLayer(layerId);
|
|
2780
|
+
if (!layer) return;
|
|
2781
|
+
const layerType = layer.type;
|
|
2782
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
2783
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
2784
|
+
const isVisible = visibility !== "none";
|
|
2785
|
+
this.state.layerStates[layerId] = {
|
|
2786
|
+
visible: isVisible,
|
|
2787
|
+
opacity,
|
|
2788
|
+
name: this.generateFriendlyName(layerId)
|
|
2789
|
+
};
|
|
2790
|
+
this.addLayerItem(layerId, this.state.layerStates[layerId]);
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
if (this.customLayerRegistry) {
|
|
2794
|
+
const customLayerIds = this.customLayerRegistry.getAllLayerIds();
|
|
2795
|
+
customLayerIds.forEach((layerId) => {
|
|
2796
|
+
if (!this.state.layerStates[layerId] && !this.removedCustomLayerIds.has(layerId)) {
|
|
2797
|
+
const customState = this.customLayerRegistry.getLayerState(layerId);
|
|
2798
|
+
if (customState) {
|
|
2799
|
+
this.state.layerStates[layerId] = {
|
|
2800
|
+
visible: customState.visible,
|
|
2801
|
+
opacity: customState.opacity,
|
|
2802
|
+
name: customState.name,
|
|
2803
|
+
isCustomLayer: true,
|
|
2804
|
+
customLayerType: this.customLayerRegistry.getSymbolType(layerId) || void 0
|
|
2805
|
+
};
|
|
2806
|
+
this.addLayerItem(layerId, this.state.layerStates[layerId]);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
2811
|
+
const state = this.state.layerStates[layerId];
|
|
2812
|
+
if (state.isCustomLayer && !customLayerIds.includes(layerId)) {
|
|
2813
|
+
delete this.state.layerStates[layerId];
|
|
2814
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
2815
|
+
if (itemEl) {
|
|
2816
|
+
itemEl.remove();
|
|
2817
|
+
}
|
|
2818
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
2819
|
+
this.state.activeStyleEditor = null;
|
|
2820
|
+
}
|
|
2821
|
+
this.styleEditors.delete(layerId);
|
|
2822
|
+
}
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
console.warn("Failed to check for new layers:", error);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Register a custom layer adapter dynamically.
|
|
2831
|
+
* This allows adding adapters after the LayerControl has been initialized.
|
|
2832
|
+
* @param adapter The custom layer adapter to register
|
|
2833
|
+
*/
|
|
2834
|
+
registerCustomAdapter(adapter) {
|
|
2835
|
+
if (!this.customLayerRegistry) {
|
|
2836
|
+
this.customLayerRegistry = new CustomLayerRegistry();
|
|
2837
|
+
this.customLayerUnsubscribe = this.customLayerRegistry.onChange((event, layerId) => {
|
|
2838
|
+
if (event === "add" && layerId) {
|
|
2839
|
+
this.removedCustomLayerIds.delete(layerId);
|
|
2840
|
+
}
|
|
2841
|
+
this.checkForNewLayers();
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
this.customLayerRegistry.register(adapter);
|
|
2845
|
+
if (this.panel) {
|
|
2846
|
+
this.checkForNewLayers();
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
// ===== Context Menu Methods =====
|
|
2850
|
+
/**
|
|
2851
|
+
* Create context menu element
|
|
2852
|
+
*/
|
|
2853
|
+
createContextMenu() {
|
|
2854
|
+
const menu = document.createElement("div");
|
|
2855
|
+
menu.className = "layer-control-context-menu";
|
|
2856
|
+
menu.style.display = "none";
|
|
2857
|
+
const renameItem = this.createContextMenuItem("Rename", "✏️", () => {
|
|
2858
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2859
|
+
this.startRenaming(this.state.contextMenu.targetLayerId);
|
|
2860
|
+
}
|
|
2861
|
+
this.hideContextMenu();
|
|
2862
|
+
});
|
|
2863
|
+
const zoomItem = this.createContextMenuItem("Zoom to Layer", "🔍", () => {
|
|
2864
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2865
|
+
this.zoomToLayer(this.state.contextMenu.targetLayerId);
|
|
2866
|
+
}
|
|
2867
|
+
this.hideContextMenu();
|
|
2868
|
+
});
|
|
2869
|
+
const sep1 = document.createElement("div");
|
|
2870
|
+
sep1.className = "context-menu-separator";
|
|
2871
|
+
const moveUpItem = this.createContextMenuItem("Move Up", "↑", () => {
|
|
2872
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2873
|
+
this.moveLayerUp(this.state.contextMenu.targetLayerId);
|
|
2874
|
+
}
|
|
2875
|
+
this.hideContextMenu();
|
|
2876
|
+
});
|
|
2877
|
+
const moveTopItem = this.createContextMenuItem("Move to Top", "⤒", () => {
|
|
2878
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2879
|
+
this.moveLayerToTop(this.state.contextMenu.targetLayerId);
|
|
2880
|
+
}
|
|
2881
|
+
this.hideContextMenu();
|
|
2882
|
+
});
|
|
2883
|
+
const moveDownItem = this.createContextMenuItem("Move Down", "↓", () => {
|
|
2884
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2885
|
+
this.moveLayerDown(this.state.contextMenu.targetLayerId);
|
|
2886
|
+
}
|
|
2887
|
+
this.hideContextMenu();
|
|
2888
|
+
});
|
|
2889
|
+
const moveBottomItem = this.createContextMenuItem("Move to Bottom", "⤓", () => {
|
|
2890
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2891
|
+
this.moveLayerToBottom(this.state.contextMenu.targetLayerId);
|
|
2892
|
+
}
|
|
2893
|
+
this.hideContextMenu();
|
|
2894
|
+
});
|
|
2895
|
+
const sep2 = document.createElement("div");
|
|
2896
|
+
sep2.className = "context-menu-separator";
|
|
2897
|
+
const removeItem = this.createContextMenuItem("Remove Layer", "🗑️", () => {
|
|
2898
|
+
if (this.state.contextMenu.targetLayerId) {
|
|
2899
|
+
this.removeLayer(this.state.contextMenu.targetLayerId);
|
|
2900
|
+
}
|
|
2901
|
+
this.hideContextMenu();
|
|
2902
|
+
}, true);
|
|
2903
|
+
menu.appendChild(renameItem);
|
|
2904
|
+
menu.appendChild(zoomItem);
|
|
2905
|
+
menu.appendChild(sep1);
|
|
2906
|
+
menu.appendChild(moveUpItem);
|
|
2907
|
+
menu.appendChild(moveTopItem);
|
|
2908
|
+
menu.appendChild(moveDownItem);
|
|
2909
|
+
menu.appendChild(moveBottomItem);
|
|
2910
|
+
menu.appendChild(sep2);
|
|
2911
|
+
menu.appendChild(removeItem);
|
|
2912
|
+
return menu;
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Create a context menu item
|
|
2916
|
+
*/
|
|
2917
|
+
createContextMenuItem(label, icon, onClick, isDanger = false) {
|
|
2918
|
+
const item = document.createElement("div");
|
|
2919
|
+
item.className = "context-menu-item" + (isDanger ? " context-menu-item-danger" : "");
|
|
2920
|
+
const iconEl = document.createElement("span");
|
|
2921
|
+
iconEl.className = "context-menu-item-icon";
|
|
2922
|
+
iconEl.textContent = icon;
|
|
2923
|
+
const labelEl = document.createElement("span");
|
|
2924
|
+
labelEl.className = "context-menu-item-label";
|
|
2925
|
+
labelEl.textContent = label;
|
|
2926
|
+
item.appendChild(iconEl);
|
|
2927
|
+
item.appendChild(labelEl);
|
|
2928
|
+
item.addEventListener("click", (e) => {
|
|
2929
|
+
e.stopPropagation();
|
|
2930
|
+
onClick();
|
|
2931
|
+
});
|
|
2932
|
+
return item;
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Show context menu at position
|
|
2936
|
+
*/
|
|
2937
|
+
showContextMenu(layerId, x, y) {
|
|
2938
|
+
if (!this.contextMenuEl) return;
|
|
2939
|
+
this.state.contextMenu = {
|
|
2940
|
+
visible: true,
|
|
2941
|
+
targetLayerId: layerId,
|
|
2942
|
+
x,
|
|
2943
|
+
y
|
|
2944
|
+
};
|
|
2945
|
+
const mapRect = this.mapContainer.getBoundingClientRect();
|
|
2946
|
+
let menuX = x - mapRect.left;
|
|
2947
|
+
let menuY = y - mapRect.top;
|
|
2948
|
+
this.contextMenuEl.style.display = "block";
|
|
2949
|
+
const menuRect = this.contextMenuEl.getBoundingClientRect();
|
|
2950
|
+
if (menuX + menuRect.width > mapRect.width) {
|
|
2951
|
+
menuX = mapRect.width - menuRect.width - 5;
|
|
2952
|
+
}
|
|
2953
|
+
if (menuY + menuRect.height > mapRect.height) {
|
|
2954
|
+
menuY = mapRect.height - menuRect.height - 5;
|
|
2955
|
+
}
|
|
2956
|
+
this.contextMenuEl.style.left = `${menuX}px`;
|
|
2957
|
+
this.contextMenuEl.style.top = `${menuY}px`;
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Hide context menu
|
|
2961
|
+
*/
|
|
2962
|
+
hideContextMenu() {
|
|
2963
|
+
if (!this.contextMenuEl) return;
|
|
2964
|
+
this.state.contextMenu = {
|
|
2965
|
+
visible: false,
|
|
2966
|
+
targetLayerId: null,
|
|
2967
|
+
x: 0,
|
|
2968
|
+
y: 0
|
|
2969
|
+
};
|
|
2970
|
+
this.contextMenuEl.style.display = "none";
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Start renaming a layer
|
|
2974
|
+
*/
|
|
2975
|
+
startRenaming(layerId) {
|
|
2976
|
+
var _a;
|
|
2977
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
2978
|
+
if (!itemEl) return;
|
|
2979
|
+
const nameEl = itemEl.querySelector(".layer-control-name");
|
|
2980
|
+
if (!nameEl) return;
|
|
2981
|
+
this.state.renamingLayerId = layerId;
|
|
2982
|
+
const currentName = this.state.customLayerNames.get(layerId) || ((_a = this.state.layerStates[layerId]) == null ? void 0 : _a.name) || layerId;
|
|
2983
|
+
const input = document.createElement("input");
|
|
2984
|
+
input.type = "text";
|
|
2985
|
+
input.className = "layer-control-name-input";
|
|
2986
|
+
input.value = currentName;
|
|
2987
|
+
const finishRename = () => {
|
|
2988
|
+
var _a2;
|
|
2989
|
+
const newName = input.value.trim() || currentName;
|
|
2990
|
+
const oldName = currentName;
|
|
2991
|
+
if (newName !== oldName) {
|
|
2992
|
+
this.state.customLayerNames.set(layerId, newName);
|
|
2993
|
+
if (this.state.layerStates[layerId]) {
|
|
2994
|
+
this.state.layerStates[layerId].name = newName;
|
|
2995
|
+
}
|
|
2996
|
+
(_a2 = this.onLayerRename) == null ? void 0 : _a2.call(this, layerId, oldName, newName);
|
|
2997
|
+
}
|
|
2998
|
+
const newNameEl = document.createElement("span");
|
|
2999
|
+
newNameEl.className = "layer-control-name";
|
|
3000
|
+
newNameEl.textContent = newName;
|
|
3001
|
+
newNameEl.title = newName;
|
|
3002
|
+
input.replaceWith(newNameEl);
|
|
3003
|
+
this.state.renamingLayerId = null;
|
|
3004
|
+
};
|
|
3005
|
+
input.addEventListener("blur", finishRename);
|
|
3006
|
+
input.addEventListener("keydown", (e) => {
|
|
3007
|
+
if (e.key === "Enter") {
|
|
3008
|
+
e.preventDefault();
|
|
3009
|
+
finishRename();
|
|
3010
|
+
} else if (e.key === "Escape") {
|
|
3011
|
+
e.preventDefault();
|
|
3012
|
+
const newNameEl = document.createElement("span");
|
|
3013
|
+
newNameEl.className = "layer-control-name";
|
|
3014
|
+
newNameEl.textContent = currentName;
|
|
3015
|
+
newNameEl.title = currentName;
|
|
3016
|
+
input.replaceWith(newNameEl);
|
|
3017
|
+
this.state.renamingLayerId = null;
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
nameEl.replaceWith(input);
|
|
3021
|
+
input.focus();
|
|
3022
|
+
input.select();
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Zoom to a layer's bounds
|
|
3026
|
+
*/
|
|
3027
|
+
zoomToLayer(layerId) {
|
|
3028
|
+
const layerState = this.state.layerStates[layerId];
|
|
3029
|
+
if ((layerState == null ? void 0 : layerState.isCustomLayer) && this.customLayerRegistry) {
|
|
3030
|
+
const bounds = this.customLayerRegistry.getBounds(layerId);
|
|
3031
|
+
if (bounds) {
|
|
3032
|
+
this.map.fitBounds(bounds, { padding: 50 });
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
const layer = this.map.getLayer(layerId);
|
|
3037
|
+
if (!layer) return;
|
|
3038
|
+
try {
|
|
3039
|
+
const sourceId = layer.source;
|
|
3040
|
+
let features = sourceId ? this.map.querySourceFeatures(sourceId) : [];
|
|
3041
|
+
if (features.length === 0) {
|
|
3042
|
+
features = this.map.queryRenderedFeatures({ layers: [layerId] });
|
|
3043
|
+
}
|
|
3044
|
+
if (features.length === 0) return;
|
|
3045
|
+
let minLng = Infinity, minLat = Infinity, maxLng = -Infinity, maxLat = -Infinity;
|
|
3046
|
+
features.forEach((feature) => {
|
|
3047
|
+
if (!feature.geometry) return;
|
|
3048
|
+
const processCoords = (coords) => {
|
|
3049
|
+
if (typeof coords[0] === "number") {
|
|
3050
|
+
const [lng, lat] = coords;
|
|
3051
|
+
minLng = Math.min(minLng, lng);
|
|
3052
|
+
minLat = Math.min(minLat, lat);
|
|
3053
|
+
maxLng = Math.max(maxLng, lng);
|
|
3054
|
+
maxLat = Math.max(maxLat, lat);
|
|
3055
|
+
} else {
|
|
3056
|
+
coords.forEach(processCoords);
|
|
3057
|
+
}
|
|
3058
|
+
};
|
|
3059
|
+
if (feature.geometry.type === "Point") {
|
|
3060
|
+
processCoords(feature.geometry.coordinates);
|
|
3061
|
+
} else if (feature.geometry.type === "LineString" || feature.geometry.type === "MultiPoint") {
|
|
3062
|
+
feature.geometry.coordinates.forEach(processCoords);
|
|
3063
|
+
} else if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiLineString") {
|
|
3064
|
+
feature.geometry.coordinates.forEach((ring) => ring.forEach(processCoords));
|
|
3065
|
+
} else if (feature.geometry.type === "MultiPolygon") {
|
|
3066
|
+
feature.geometry.coordinates.forEach(
|
|
3067
|
+
(polygon) => polygon.forEach((ring) => ring.forEach(processCoords))
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
});
|
|
3071
|
+
if (minLng !== Infinity && minLat !== Infinity && maxLng !== -Infinity && maxLat !== -Infinity) {
|
|
3072
|
+
this.map.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding: 50 });
|
|
3073
|
+
}
|
|
3074
|
+
} catch (error) {
|
|
3075
|
+
console.warn(`Failed to zoom to layer ${layerId}:`, error);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Get user layer IDs in current map order (top to bottom in UI = high z-index to low)
|
|
3080
|
+
* Includes both MapLibre layers and custom layers managed by adapters.
|
|
3081
|
+
*/
|
|
3082
|
+
getUserLayerIdsInMapOrder() {
|
|
3083
|
+
const style = this.map.getStyle();
|
|
3084
|
+
if (!(style == null ? void 0 : style.layers)) return [];
|
|
3085
|
+
const mapLayerIds = style.layers.map((l) => l.id);
|
|
3086
|
+
const userLayerIds = Object.keys(this.state.layerStates).filter((id) => id !== "Background");
|
|
3087
|
+
const mapLibreLayers = userLayerIds.filter((id) => mapLayerIds.includes(id));
|
|
3088
|
+
const customLayers = userLayerIds.filter(
|
|
3089
|
+
(id) => {
|
|
3090
|
+
var _a;
|
|
3091
|
+
return ((_a = this.state.layerStates[id]) == null ? void 0 : _a.isCustomLayer) && !mapLayerIds.includes(id);
|
|
3092
|
+
}
|
|
3093
|
+
);
|
|
3094
|
+
const sortedMapLibreLayers = mapLibreLayers.sort((a, b) => mapLayerIds.indexOf(b) - mapLayerIds.indexOf(a));
|
|
3095
|
+
return [...customLayers, ...sortedMapLibreLayers];
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Check if a layer is a MapLibre layer (not a custom layer)
|
|
3099
|
+
*/
|
|
3100
|
+
isMapLibreLayer(layerId) {
|
|
3101
|
+
return this.map.getLayer(layerId) !== void 0;
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Find the next MapLibre layer ID in a direction from a given index
|
|
3105
|
+
* @param layerIds Array of layer IDs
|
|
3106
|
+
* @param startIndex Index to start searching from
|
|
3107
|
+
* @param direction 1 for forward (toward bottom), -1 for backward (toward top)
|
|
3108
|
+
* @returns MapLibre layer ID or undefined if not found
|
|
3109
|
+
*/
|
|
3110
|
+
findNextMapLibreLayer(layerIds, startIndex, direction) {
|
|
3111
|
+
for (let i = startIndex; direction === 1 ? i < layerIds.length : i >= 0; i += direction) {
|
|
3112
|
+
if (this.isMapLibreLayer(layerIds[i])) {
|
|
3113
|
+
return layerIds[i];
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
return void 0;
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Move a layer up in UI (higher rendering order = move to higher z-index)
|
|
3120
|
+
*/
|
|
3121
|
+
moveLayerUp(layerId) {
|
|
3122
|
+
var _a, _b;
|
|
3123
|
+
const layerIds = this.getUserLayerIdsInMapOrder();
|
|
3124
|
+
const index = layerIds.indexOf(layerId);
|
|
3125
|
+
if (index <= 0) return;
|
|
3126
|
+
const isCustom = (_a = this.state.layerStates[layerId]) == null ? void 0 : _a.isCustomLayer;
|
|
3127
|
+
if (!isCustom && this.isMapLibreLayer(layerId)) {
|
|
3128
|
+
try {
|
|
3129
|
+
const targetBeforeId = index >= 2 ? this.findNextMapLibreLayer(layerIds, index - 2, -1) : void 0;
|
|
3130
|
+
this.map.moveLayer(layerId, targetBeforeId);
|
|
3131
|
+
} catch (e) {
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
const newLayerStates = {};
|
|
3135
|
+
const orderedIds = [...layerIds];
|
|
3136
|
+
[orderedIds[index], orderedIds[index - 1]] = [orderedIds[index - 1], orderedIds[index]];
|
|
3137
|
+
if (this.state.layerStates["Background"]) {
|
|
3138
|
+
newLayerStates["Background"] = this.state.layerStates["Background"];
|
|
3139
|
+
}
|
|
3140
|
+
orderedIds.forEach((id) => {
|
|
3141
|
+
if (this.state.layerStates[id]) {
|
|
3142
|
+
newLayerStates[id] = this.state.layerStates[id];
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
this.state.layerStates = newLayerStates;
|
|
3146
|
+
this.buildLayerItems();
|
|
3147
|
+
(_b = this.onLayerReorder) == null ? void 0 : _b.call(this, this.getUserLayerIdsInMapOrder());
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Move a layer to the top (highest rendering order)
|
|
3151
|
+
*/
|
|
3152
|
+
moveLayerToTop(layerId) {
|
|
3153
|
+
var _a, _b;
|
|
3154
|
+
const layerIds = this.getUserLayerIdsInMapOrder();
|
|
3155
|
+
const index = layerIds.indexOf(layerId);
|
|
3156
|
+
if (index <= 0) return;
|
|
3157
|
+
const isCustom = (_a = this.state.layerStates[layerId]) == null ? void 0 : _a.isCustomLayer;
|
|
3158
|
+
if (!isCustom && this.isMapLibreLayer(layerId)) {
|
|
3159
|
+
try {
|
|
3160
|
+
this.map.moveLayer(layerId);
|
|
3161
|
+
} catch (e) {
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
const newLayerStates = {};
|
|
3165
|
+
const orderedIds = [...layerIds];
|
|
3166
|
+
orderedIds.splice(index, 1);
|
|
3167
|
+
orderedIds.unshift(layerId);
|
|
3168
|
+
if (this.state.layerStates["Background"]) {
|
|
3169
|
+
newLayerStates["Background"] = this.state.layerStates["Background"];
|
|
3170
|
+
}
|
|
3171
|
+
orderedIds.forEach((id) => {
|
|
3172
|
+
if (this.state.layerStates[id]) {
|
|
3173
|
+
newLayerStates[id] = this.state.layerStates[id];
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
this.state.layerStates = newLayerStates;
|
|
3177
|
+
this.buildLayerItems();
|
|
3178
|
+
(_b = this.onLayerReorder) == null ? void 0 : _b.call(this, this.getUserLayerIdsInMapOrder());
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Move a layer down in UI (lower rendering order = move to lower z-index)
|
|
3182
|
+
*/
|
|
3183
|
+
moveLayerDown(layerId) {
|
|
3184
|
+
var _a, _b;
|
|
3185
|
+
const layerIds = this.getUserLayerIdsInMapOrder();
|
|
3186
|
+
const index = layerIds.indexOf(layerId);
|
|
3187
|
+
if (index < 0 || index >= layerIds.length - 1) return;
|
|
3188
|
+
const isCustom = (_a = this.state.layerStates[layerId]) == null ? void 0 : _a.isCustomLayer;
|
|
3189
|
+
if (!isCustom && this.isMapLibreLayer(layerId)) {
|
|
3190
|
+
const belowLayerId = this.findNextMapLibreLayer(layerIds, index + 1, 1);
|
|
3191
|
+
if (belowLayerId) {
|
|
3192
|
+
try {
|
|
3193
|
+
this.map.moveLayer(layerId, belowLayerId);
|
|
3194
|
+
} catch (e) {
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
const newLayerStates = {};
|
|
3199
|
+
const orderedIds = [...layerIds];
|
|
3200
|
+
[orderedIds[index], orderedIds[index + 1]] = [orderedIds[index + 1], orderedIds[index]];
|
|
3201
|
+
if (this.state.layerStates["Background"]) {
|
|
3202
|
+
newLayerStates["Background"] = this.state.layerStates["Background"];
|
|
3203
|
+
}
|
|
3204
|
+
orderedIds.forEach((id) => {
|
|
3205
|
+
if (this.state.layerStates[id]) {
|
|
3206
|
+
newLayerStates[id] = this.state.layerStates[id];
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
this.state.layerStates = newLayerStates;
|
|
3210
|
+
this.buildLayerItems();
|
|
3211
|
+
(_b = this.onLayerReorder) == null ? void 0 : _b.call(this, this.getUserLayerIdsInMapOrder());
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* Move a layer to the bottom (lowest rendering order among user layers)
|
|
3215
|
+
*/
|
|
3216
|
+
moveLayerToBottom(layerId) {
|
|
3217
|
+
var _a, _b;
|
|
3218
|
+
const layerIds = this.getUserLayerIdsInMapOrder();
|
|
3219
|
+
if (layerIds.length <= 1) return;
|
|
3220
|
+
const index = layerIds.indexOf(layerId);
|
|
3221
|
+
if (index < 0 || index === layerIds.length - 1) return;
|
|
3222
|
+
const isCustom = (_a = this.state.layerStates[layerId]) == null ? void 0 : _a.isCustomLayer;
|
|
3223
|
+
if (!isCustom && this.isMapLibreLayer(layerId)) {
|
|
3224
|
+
const bottomLayerId = this.findNextMapLibreLayer(layerIds, layerIds.length - 1, -1);
|
|
3225
|
+
if (bottomLayerId && bottomLayerId !== layerId) {
|
|
3226
|
+
try {
|
|
3227
|
+
this.map.moveLayer(layerId, bottomLayerId);
|
|
3228
|
+
} catch (e) {
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
const newLayerStates = {};
|
|
3233
|
+
const orderedIds = [...layerIds];
|
|
3234
|
+
orderedIds.splice(index, 1);
|
|
3235
|
+
orderedIds.push(layerId);
|
|
3236
|
+
if (this.state.layerStates["Background"]) {
|
|
3237
|
+
newLayerStates["Background"] = this.state.layerStates["Background"];
|
|
3238
|
+
}
|
|
3239
|
+
orderedIds.forEach((id) => {
|
|
3240
|
+
if (this.state.layerStates[id]) {
|
|
3241
|
+
newLayerStates[id] = this.state.layerStates[id];
|
|
3242
|
+
}
|
|
3243
|
+
});
|
|
3244
|
+
this.state.layerStates = newLayerStates;
|
|
3245
|
+
this.buildLayerItems();
|
|
3246
|
+
(_b = this.onLayerReorder) == null ? void 0 : _b.call(this, this.getUserLayerIdsInMapOrder());
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Remove a layer from the map
|
|
3250
|
+
*/
|
|
3251
|
+
removeLayer(layerId) {
|
|
3252
|
+
var _a, _b;
|
|
3253
|
+
this.state.isStyleOperationInProgress = true;
|
|
3254
|
+
const layerState = this.state.layerStates[layerId];
|
|
3255
|
+
if ((layerState == null ? void 0 : layerState.isCustomLayer) && this.customLayerRegistry) {
|
|
3256
|
+
this.removedCustomLayerIds.add(layerId);
|
|
3257
|
+
this.customLayerRegistry.setVisibility(layerId, false);
|
|
3258
|
+
this.customLayerRegistry.removeLayer(layerId);
|
|
3259
|
+
}
|
|
3260
|
+
try {
|
|
3261
|
+
const layer = this.map.getLayer(layerId);
|
|
3262
|
+
if (layer) {
|
|
3263
|
+
const sourceId = layer.source;
|
|
3264
|
+
this.map.removeLayer(layerId);
|
|
3265
|
+
if (sourceId) {
|
|
3266
|
+
const style = this.map.getStyle();
|
|
3267
|
+
const sourceStillUsed = (_a = style == null ? void 0 : style.layers) == null ? void 0 : _a.some((l) => l.source === sourceId);
|
|
3268
|
+
if (!sourceStillUsed) {
|
|
3269
|
+
try {
|
|
3270
|
+
this.map.removeSource(sourceId);
|
|
3271
|
+
} catch (e) {
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
} catch (error) {
|
|
3277
|
+
console.warn(`Failed to remove layer ${layerId}:`, error);
|
|
3278
|
+
}
|
|
3279
|
+
delete this.state.layerStates[layerId];
|
|
3280
|
+
this.state.customLayerNames.delete(layerId);
|
|
3281
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
3282
|
+
if (itemEl) {
|
|
3283
|
+
itemEl.remove();
|
|
3284
|
+
}
|
|
3285
|
+
(_b = this.onLayerRemove) == null ? void 0 : _b.call(this, layerId);
|
|
3286
|
+
setTimeout(() => {
|
|
3287
|
+
this.state.isStyleOperationInProgress = false;
|
|
3288
|
+
}, 200);
|
|
3289
|
+
}
|
|
3290
|
+
// ===== Drag and Drop Methods =====
|
|
3291
|
+
/**
|
|
3292
|
+
* Create drag handle element
|
|
3293
|
+
*/
|
|
3294
|
+
createDragHandle(layerId) {
|
|
3295
|
+
const handle = document.createElement("div");
|
|
3296
|
+
handle.className = "layer-control-drag-handle";
|
|
3297
|
+
handle.innerHTML = `<svg viewBox="0 0 16 16" fill="currentColor">
|
|
3298
|
+
<circle cx="5" cy="3" r="1.5"/>
|
|
3299
|
+
<circle cx="11" cy="3" r="1.5"/>
|
|
3300
|
+
<circle cx="5" cy="8" r="1.5"/>
|
|
3301
|
+
<circle cx="11" cy="8" r="1.5"/>
|
|
3302
|
+
<circle cx="5" cy="13" r="1.5"/>
|
|
3303
|
+
<circle cx="11" cy="13" r="1.5"/>
|
|
3304
|
+
</svg>`;
|
|
3305
|
+
handle.title = "Drag to reorder";
|
|
3306
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
3307
|
+
e.preventDefault();
|
|
3308
|
+
e.stopPropagation();
|
|
3309
|
+
this.startDrag(layerId, e);
|
|
3310
|
+
});
|
|
3311
|
+
return handle;
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Create a disabled drag handle for alignment (used for Background layer)
|
|
3315
|
+
*/
|
|
3316
|
+
createDisabledDragHandle() {
|
|
3317
|
+
const handle = document.createElement("div");
|
|
3318
|
+
handle.className = "layer-control-drag-handle layer-control-drag-handle-disabled";
|
|
3319
|
+
handle.innerHTML = `<svg viewBox="0 0 16 16" fill="currentColor">
|
|
3320
|
+
<circle cx="5" cy="3" r="1.5"/>
|
|
3321
|
+
<circle cx="11" cy="3" r="1.5"/>
|
|
3322
|
+
<circle cx="5" cy="8" r="1.5"/>
|
|
3323
|
+
<circle cx="11" cy="8" r="1.5"/>
|
|
3324
|
+
<circle cx="5" cy="13" r="1.5"/>
|
|
3325
|
+
<circle cx="11" cy="13" r="1.5"/>
|
|
3326
|
+
</svg>`;
|
|
3327
|
+
return handle;
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Start dragging a layer
|
|
3331
|
+
*/
|
|
3332
|
+
startDrag(layerId, e) {
|
|
3333
|
+
var _a;
|
|
3334
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
3335
|
+
if (!itemEl) return;
|
|
3336
|
+
const rect = itemEl.getBoundingClientRect();
|
|
3337
|
+
this.state.drag = {
|
|
3338
|
+
active: true,
|
|
3339
|
+
layerId,
|
|
3340
|
+
startY: e.clientY,
|
|
3341
|
+
currentY: e.clientY,
|
|
3342
|
+
placeholder: null,
|
|
3343
|
+
draggedElement: null
|
|
3344
|
+
};
|
|
3345
|
+
this.panel.classList.add("dragging-active");
|
|
3346
|
+
itemEl.classList.add("dragging");
|
|
3347
|
+
const placeholder = document.createElement("div");
|
|
3348
|
+
placeholder.className = "layer-control-drop-placeholder";
|
|
3349
|
+
placeholder.style.height = `${rect.height}px`;
|
|
3350
|
+
(_a = itemEl.parentNode) == null ? void 0 : _a.insertBefore(placeholder, itemEl);
|
|
3351
|
+
this.state.drag.placeholder = placeholder;
|
|
3352
|
+
const clone = itemEl.cloneNode(true);
|
|
3353
|
+
clone.classList.remove("dragging");
|
|
3354
|
+
clone.className = "layer-control-item layer-control-item-dragging";
|
|
3355
|
+
clone.style.width = `${rect.width}px`;
|
|
3356
|
+
clone.style.left = `${rect.left}px`;
|
|
3357
|
+
clone.style.top = `${rect.top}px`;
|
|
3358
|
+
document.body.appendChild(clone);
|
|
3359
|
+
this.state.drag.draggedElement = clone;
|
|
3360
|
+
const onMove = (moveE) => this.onDragMove(moveE);
|
|
3361
|
+
const onEnd = (endE) => {
|
|
3362
|
+
document.removeEventListener("pointermove", onMove);
|
|
3363
|
+
document.removeEventListener("pointerup", onEnd);
|
|
3364
|
+
document.removeEventListener("pointercancel", onEnd);
|
|
3365
|
+
this.endDrag(endE);
|
|
3366
|
+
};
|
|
3367
|
+
document.addEventListener("pointermove", onMove);
|
|
3368
|
+
document.addEventListener("pointerup", onEnd);
|
|
3369
|
+
document.addEventListener("pointercancel", onEnd);
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Handle drag move
|
|
3373
|
+
*/
|
|
3374
|
+
onDragMove(e) {
|
|
3375
|
+
var _a, _b;
|
|
3376
|
+
if (!this.state.drag.active || !this.state.drag.draggedElement) return;
|
|
3377
|
+
const placeholder = this.state.drag.placeholder;
|
|
3378
|
+
if (!placeholder) return;
|
|
3379
|
+
const deltaY = e.clientY - this.state.drag.startY;
|
|
3380
|
+
const clone = this.state.drag.draggedElement;
|
|
3381
|
+
const currentTop = parseFloat(clone.style.top) || 0;
|
|
3382
|
+
clone.style.top = `${currentTop + deltaY}px`;
|
|
3383
|
+
this.state.drag.startY = e.clientY;
|
|
3384
|
+
this.state.drag.currentY = e.clientY;
|
|
3385
|
+
const items = Array.from(this.panel.querySelectorAll(".layer-control-item:not(.dragging)")).filter((item) => item.dataset.layerId !== "Background");
|
|
3386
|
+
for (const item of items) {
|
|
3387
|
+
const itemRect = item.getBoundingClientRect();
|
|
3388
|
+
if (e.clientY >= itemRect.top && e.clientY <= itemRect.bottom) {
|
|
3389
|
+
const itemMiddle = itemRect.top + itemRect.height / 2;
|
|
3390
|
+
if (e.clientY < itemMiddle) {
|
|
3391
|
+
if (placeholder.nextSibling !== item) {
|
|
3392
|
+
(_a = item.parentNode) == null ? void 0 : _a.insertBefore(placeholder, item);
|
|
3393
|
+
}
|
|
3394
|
+
} else {
|
|
3395
|
+
if (placeholder.previousSibling !== item) {
|
|
3396
|
+
(_b = item.parentNode) == null ? void 0 : _b.insertBefore(placeholder, item.nextSibling);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
break;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* End dragging
|
|
3405
|
+
*/
|
|
3406
|
+
endDrag(_e) {
|
|
3407
|
+
var _a;
|
|
3408
|
+
if (!this.state.drag.active) return;
|
|
3409
|
+
const layerId = this.state.drag.layerId;
|
|
3410
|
+
const placeholder = this.state.drag.placeholder;
|
|
3411
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
3412
|
+
if (itemEl && placeholder) {
|
|
3413
|
+
(_a = placeholder.parentNode) == null ? void 0 : _a.insertBefore(itemEl, placeholder);
|
|
3414
|
+
itemEl.classList.remove("dragging");
|
|
3415
|
+
}
|
|
3416
|
+
this.cleanupDragState();
|
|
3417
|
+
this.applyUIOrderToMap();
|
|
3418
|
+
}
|
|
3419
|
+
/**
|
|
3420
|
+
* Clean up drag state
|
|
3421
|
+
*/
|
|
3422
|
+
cleanupDragState() {
|
|
3423
|
+
if (this.state.drag.draggedElement) {
|
|
3424
|
+
this.state.drag.draggedElement.remove();
|
|
3425
|
+
}
|
|
3426
|
+
if (this.state.drag.placeholder) {
|
|
3427
|
+
this.state.drag.placeholder.remove();
|
|
3428
|
+
}
|
|
3429
|
+
this.panel.classList.remove("dragging-active");
|
|
3430
|
+
this.panel.querySelectorAll(".layer-control-item.dragging").forEach((el) => {
|
|
3431
|
+
el.classList.remove("dragging");
|
|
3432
|
+
});
|
|
3433
|
+
this.state.drag = {
|
|
3434
|
+
active: false,
|
|
3435
|
+
layerId: null,
|
|
3436
|
+
startY: 0,
|
|
3437
|
+
currentY: 0,
|
|
3438
|
+
placeholder: null,
|
|
3439
|
+
draggedElement: null
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Apply UI order to map layers
|
|
3444
|
+
*/
|
|
3445
|
+
applyUIOrderToMap() {
|
|
3446
|
+
var _a, _b;
|
|
3447
|
+
this.state.isStyleOperationInProgress = true;
|
|
3448
|
+
const items = this.panel.querySelectorAll(".layer-control-item");
|
|
3449
|
+
const uiLayerIds = [];
|
|
3450
|
+
items.forEach((item) => {
|
|
3451
|
+
const layerId = item.dataset.layerId;
|
|
3452
|
+
if (layerId && layerId !== "Background") {
|
|
3453
|
+
uiLayerIds.push(layerId);
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
const reversedIds = [...uiLayerIds].reverse();
|
|
3457
|
+
const style = this.map.getStyle();
|
|
3458
|
+
const mapLibreLayerIds = new Set(((_a = style == null ? void 0 : style.layers) == null ? void 0 : _a.map((l) => l.id)) || []);
|
|
3459
|
+
for (let i = 0; i < reversedIds.length; i++) {
|
|
3460
|
+
const layerId = reversedIds[i];
|
|
3461
|
+
if (!mapLibreLayerIds.has(layerId)) {
|
|
3462
|
+
continue;
|
|
3463
|
+
}
|
|
3464
|
+
let beforeId = void 0;
|
|
3465
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
3466
|
+
if (mapLibreLayerIds.has(reversedIds[j])) {
|
|
3467
|
+
beforeId = reversedIds[j];
|
|
3468
|
+
break;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
try {
|
|
3472
|
+
this.map.moveLayer(layerId, beforeId);
|
|
3473
|
+
} catch (e) {
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
(_b = this.onLayerReorder) == null ? void 0 : _b.call(this, uiLayerIds);
|
|
3477
|
+
setTimeout(() => {
|
|
3478
|
+
this.state.isStyleOperationInProgress = false;
|
|
3479
|
+
}, 200);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
export {
|
|
3483
|
+
CustomLayerRegistry,
|
|
3484
|
+
LayerControl,
|
|
3485
|
+
clamp,
|
|
3486
|
+
createBackgroundGroupSymbolSVG,
|
|
3487
|
+
createLayerSymbolSVG,
|
|
3488
|
+
darkenColor,
|
|
3489
|
+
formatNumericValue,
|
|
3490
|
+
getLayerColor,
|
|
3491
|
+
getLayerColorFromSpec,
|
|
3492
|
+
getLayerOpacity,
|
|
3493
|
+
getLayerType,
|
|
3494
|
+
isStyleableLayerType,
|
|
3495
|
+
normalizeColor,
|
|
3496
|
+
rgbToHex,
|
|
3497
|
+
setLayerOpacity
|
|
3498
|
+
};
|
|
3499
|
+
//# sourceMappingURL=index.mjs.map
|