maplibre-gl-layer-control 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/index.cjs +1166 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +1166 -0
- package/dist/index.mjs.map +1 -0
- package/dist/maplibre-gl-layer-control.css +644 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +91 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1166 @@
|
|
|
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
|
+
function getOpacityProperty(layerType) {
|
|
5
|
+
switch (layerType) {
|
|
6
|
+
case "fill":
|
|
7
|
+
return "fill-opacity";
|
|
8
|
+
case "line":
|
|
9
|
+
return "line-opacity";
|
|
10
|
+
case "circle":
|
|
11
|
+
return "circle-opacity";
|
|
12
|
+
case "symbol":
|
|
13
|
+
return ["icon-opacity", "text-opacity"];
|
|
14
|
+
case "raster":
|
|
15
|
+
return "raster-opacity";
|
|
16
|
+
case "background":
|
|
17
|
+
return "background-opacity";
|
|
18
|
+
default:
|
|
19
|
+
return `${layerType}-opacity`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function getLayerOpacity(map, layerId, layerType) {
|
|
23
|
+
const opacityProp = getOpacityProperty(layerType);
|
|
24
|
+
if (Array.isArray(opacityProp)) {
|
|
25
|
+
const opacity2 = map.getPaintProperty(layerId, opacityProp[0]);
|
|
26
|
+
return opacity2 !== void 0 && opacity2 !== null ? opacity2 : 1;
|
|
27
|
+
}
|
|
28
|
+
const opacity = map.getPaintProperty(layerId, opacityProp);
|
|
29
|
+
return opacity !== void 0 && opacity !== null ? opacity : 1;
|
|
30
|
+
}
|
|
31
|
+
function setLayerOpacity(map, layerId, layerType, opacity) {
|
|
32
|
+
const opacityProp = getOpacityProperty(layerType);
|
|
33
|
+
if (Array.isArray(opacityProp)) {
|
|
34
|
+
opacityProp.forEach((prop) => {
|
|
35
|
+
map.setPaintProperty(layerId, prop, opacity);
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
map.setPaintProperty(layerId, opacityProp, opacity);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isStyleableLayerType(layerType) {
|
|
42
|
+
return ["fill", "line", "circle", "symbol", "raster"].includes(layerType);
|
|
43
|
+
}
|
|
44
|
+
function getLayerType(map, layerId) {
|
|
45
|
+
try {
|
|
46
|
+
const layer = map.getLayer(layerId);
|
|
47
|
+
return layer ? layer.type : null;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn(`Failed to get layer type for ${layerId}:`, error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function clonePaintValue(value) {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return value.map((item) => clonePaintValue(item));
|
|
56
|
+
}
|
|
57
|
+
if (value && typeof value === "object") {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(JSON.stringify(value));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function cacheOriginalLayerStyle(map, layerId, originalStyles) {
|
|
67
|
+
var _a;
|
|
68
|
+
if (originalStyles.has(layerId)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const layer = map.getLayer(layerId);
|
|
73
|
+
if (!layer) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const paint = {};
|
|
77
|
+
const style = map.getStyle();
|
|
78
|
+
const layerDef = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
79
|
+
if (layer.type === "raster") {
|
|
80
|
+
const rasterDefaults = {
|
|
81
|
+
"raster-opacity": 1,
|
|
82
|
+
"raster-brightness-min": 0,
|
|
83
|
+
"raster-brightness-max": 1,
|
|
84
|
+
"raster-saturation": 0,
|
|
85
|
+
"raster-contrast": 0,
|
|
86
|
+
"raster-hue-rotate": 0
|
|
87
|
+
};
|
|
88
|
+
Object.assign(paint, rasterDefaults);
|
|
89
|
+
if (layerDef && "paint" in layerDef && layerDef.paint) {
|
|
90
|
+
Object.entries(layerDef.paint).forEach(([prop, value]) => {
|
|
91
|
+
if (prop.startsWith("raster-")) {
|
|
92
|
+
paint[prop] = clonePaintValue(value);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (layerDef && "paint" in layerDef && layerDef.paint) {
|
|
98
|
+
Object.entries(layerDef.paint).forEach(([prop, value]) => {
|
|
99
|
+
paint[prop] = clonePaintValue(value);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
originalStyles.set(layerId, { paint });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn(`Failed to cache original style for ${layerId}:`, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function restoreOriginalStyle(map, layerId, originalStyles) {
|
|
109
|
+
const original = originalStyles.get(layerId);
|
|
110
|
+
if (!original) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
const applied = {};
|
|
114
|
+
Object.entries(original.paint).forEach(([property, value]) => {
|
|
115
|
+
try {
|
|
116
|
+
const restoredValue = clonePaintValue(value);
|
|
117
|
+
map.setPaintProperty(layerId, property, restoredValue);
|
|
118
|
+
applied[property] = restoredValue;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(`Failed to restore ${property} for ${layerId}:`, error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return applied;
|
|
124
|
+
}
|
|
125
|
+
function rgbToHex(r, g, b) {
|
|
126
|
+
const clamp2 = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
127
|
+
const toHex = (v) => {
|
|
128
|
+
const hex = clamp2(v).toString(16);
|
|
129
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
|
130
|
+
};
|
|
131
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
132
|
+
}
|
|
133
|
+
function normalizeColor(value) {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
if (value.startsWith("#")) {
|
|
136
|
+
if (value.length === 4) {
|
|
137
|
+
const r = value[1];
|
|
138
|
+
const g = value[2];
|
|
139
|
+
const b = value[3];
|
|
140
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
if (value.startsWith("rgb")) {
|
|
145
|
+
const match = value.match(/\d+/g);
|
|
146
|
+
if (match && match.length >= 3) {
|
|
147
|
+
const [r, g, b] = match.map((num) => parseInt(num, 10));
|
|
148
|
+
return rgbToHex(r, g, b);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else if (Array.isArray(value) && value.length >= 3) {
|
|
152
|
+
return rgbToHex(value[0], value[1], value[2]);
|
|
153
|
+
}
|
|
154
|
+
return "#3388ff";
|
|
155
|
+
}
|
|
156
|
+
function formatNumericValue(value, step) {
|
|
157
|
+
let decimals = 0;
|
|
158
|
+
if (step && Number(step) !== 1) {
|
|
159
|
+
const stepNumber = Number(step);
|
|
160
|
+
if (stepNumber > 0 && stepNumber < 1) {
|
|
161
|
+
decimals = Math.min(4, Math.ceil(Math.abs(Math.log10(stepNumber))));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return value.toFixed(decimals);
|
|
165
|
+
}
|
|
166
|
+
function clamp(value, min, max) {
|
|
167
|
+
return Math.max(min, Math.min(max, value));
|
|
168
|
+
}
|
|
169
|
+
class LayerControl {
|
|
170
|
+
constructor(options = {}) {
|
|
171
|
+
__publicField(this, "map");
|
|
172
|
+
__publicField(this, "container");
|
|
173
|
+
__publicField(this, "button");
|
|
174
|
+
__publicField(this, "panel");
|
|
175
|
+
// State management
|
|
176
|
+
__publicField(this, "state");
|
|
177
|
+
__publicField(this, "targetLayers");
|
|
178
|
+
__publicField(this, "styleEditors");
|
|
179
|
+
// Panel width management
|
|
180
|
+
__publicField(this, "minPanelWidth");
|
|
181
|
+
__publicField(this, "maxPanelWidth");
|
|
182
|
+
__publicField(this, "widthSliderEl", null);
|
|
183
|
+
__publicField(this, "widthThumbEl", null);
|
|
184
|
+
__publicField(this, "widthValueEl", null);
|
|
185
|
+
__publicField(this, "isWidthSliderActive", false);
|
|
186
|
+
__publicField(this, "widthDragRectWidth", null);
|
|
187
|
+
__publicField(this, "widthDragStartX", null);
|
|
188
|
+
__publicField(this, "widthDragStartWidth", null);
|
|
189
|
+
__publicField(this, "widthFrame", null);
|
|
190
|
+
this.minPanelWidth = options.panelMinWidth || 240;
|
|
191
|
+
this.maxPanelWidth = options.panelMaxWidth || 420;
|
|
192
|
+
this.state = {
|
|
193
|
+
collapsed: options.collapsed !== false,
|
|
194
|
+
panelWidth: options.panelWidth || 320,
|
|
195
|
+
activeStyleEditor: null,
|
|
196
|
+
layerStates: options.layerStates || {},
|
|
197
|
+
originalStyles: /* @__PURE__ */ new Map(),
|
|
198
|
+
userInteractingWithSlider: false
|
|
199
|
+
};
|
|
200
|
+
this.targetLayers = options.layers || Object.keys(this.state.layerStates);
|
|
201
|
+
this.styleEditors = /* @__PURE__ */ new Map();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Called when the control is added to the map
|
|
205
|
+
*/
|
|
206
|
+
onAdd(map) {
|
|
207
|
+
this.map = map;
|
|
208
|
+
if (Object.keys(this.state.layerStates).length === 0) {
|
|
209
|
+
this.autoDetectLayers();
|
|
210
|
+
}
|
|
211
|
+
this.container = this.createContainer();
|
|
212
|
+
this.button = this.createToggleButton();
|
|
213
|
+
this.panel = this.createPanel();
|
|
214
|
+
this.container.appendChild(this.button);
|
|
215
|
+
this.container.appendChild(this.panel);
|
|
216
|
+
this.updateWidthDisplay();
|
|
217
|
+
this.setupEventListeners();
|
|
218
|
+
this.buildLayerItems();
|
|
219
|
+
return this.container;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Called when the control is removed from the map
|
|
223
|
+
*/
|
|
224
|
+
onRemove() {
|
|
225
|
+
var _a;
|
|
226
|
+
(_a = this.container.parentNode) == null ? void 0 : _a.removeChild(this.container);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Auto-detect layers from the map and populate layerStates
|
|
230
|
+
*/
|
|
231
|
+
autoDetectLayers() {
|
|
232
|
+
const style = this.map.getStyle();
|
|
233
|
+
if (!style || !style.layers) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const allLayerIds = style.layers.map((layer) => layer.id);
|
|
237
|
+
if (this.targetLayers.length === 0) {
|
|
238
|
+
allLayerIds.forEach((layerId) => {
|
|
239
|
+
const layer = this.map.getLayer(layerId);
|
|
240
|
+
if (!layer) return;
|
|
241
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
242
|
+
const isVisible = visibility !== "none";
|
|
243
|
+
const layerType = layer.type;
|
|
244
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
245
|
+
const friendlyName = this.generateFriendlyName(layerId);
|
|
246
|
+
this.state.layerStates[layerId] = {
|
|
247
|
+
visible: isVisible,
|
|
248
|
+
opacity,
|
|
249
|
+
name: friendlyName
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
const userLayers = [];
|
|
254
|
+
const basemapLayers = [];
|
|
255
|
+
allLayerIds.forEach((layerId) => {
|
|
256
|
+
if (this.targetLayers.includes(layerId)) {
|
|
257
|
+
userLayers.push(layerId);
|
|
258
|
+
} else {
|
|
259
|
+
basemapLayers.push(layerId);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (basemapLayers.length > 0) {
|
|
263
|
+
this.state.layerStates["Background"] = {
|
|
264
|
+
visible: true,
|
|
265
|
+
opacity: 1,
|
|
266
|
+
name: "Background"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
userLayers.forEach((layerId) => {
|
|
270
|
+
const layer = this.map.getLayer(layerId);
|
|
271
|
+
if (!layer) return;
|
|
272
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
273
|
+
const isVisible = visibility !== "none";
|
|
274
|
+
const layerType = layer.type;
|
|
275
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
276
|
+
const friendlyName = this.generateFriendlyName(layerId);
|
|
277
|
+
this.state.layerStates[layerId] = {
|
|
278
|
+
visible: isVisible,
|
|
279
|
+
opacity,
|
|
280
|
+
name: friendlyName
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
this.targetLayers = Object.keys(this.state.layerStates);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generate a friendly display name from a layer ID
|
|
288
|
+
*/
|
|
289
|
+
generateFriendlyName(layerId) {
|
|
290
|
+
let name = layerId.replace(/^(layer[-_]?|gl[-_]?)/, "");
|
|
291
|
+
name = name.replace(/[-_]/g, " ");
|
|
292
|
+
name = name.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
293
|
+
return name || layerId;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Create the main container element
|
|
297
|
+
*/
|
|
298
|
+
createContainer() {
|
|
299
|
+
const container = document.createElement("div");
|
|
300
|
+
container.className = "maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-layer-control";
|
|
301
|
+
return container;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Create the toggle button
|
|
305
|
+
*/
|
|
306
|
+
createToggleButton() {
|
|
307
|
+
const button = document.createElement("button");
|
|
308
|
+
button.type = "button";
|
|
309
|
+
button.title = "Layer Control";
|
|
310
|
+
button.setAttribute("aria-label", "Layer Control");
|
|
311
|
+
const icon = document.createElement("span");
|
|
312
|
+
icon.className = "layer-control-icon";
|
|
313
|
+
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>';
|
|
314
|
+
button.appendChild(icon);
|
|
315
|
+
return button;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Create the panel element
|
|
319
|
+
*/
|
|
320
|
+
createPanel() {
|
|
321
|
+
const panel = document.createElement("div");
|
|
322
|
+
panel.className = "layer-control-panel";
|
|
323
|
+
panel.style.width = `${this.state.panelWidth}px`;
|
|
324
|
+
if (!this.state.collapsed) {
|
|
325
|
+
panel.classList.add("expanded");
|
|
326
|
+
}
|
|
327
|
+
const header = this.createPanelHeader();
|
|
328
|
+
panel.appendChild(header);
|
|
329
|
+
return panel;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Create the panel header with title and width control
|
|
333
|
+
*/
|
|
334
|
+
createPanelHeader() {
|
|
335
|
+
const header = document.createElement("div");
|
|
336
|
+
header.className = "layer-control-panel-header";
|
|
337
|
+
const title = document.createElement("span");
|
|
338
|
+
title.className = "layer-control-panel-title";
|
|
339
|
+
title.textContent = "Layers";
|
|
340
|
+
header.appendChild(title);
|
|
341
|
+
const widthControl = this.createWidthControl();
|
|
342
|
+
header.appendChild(widthControl);
|
|
343
|
+
return header;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Create the width control slider
|
|
347
|
+
*/
|
|
348
|
+
createWidthControl() {
|
|
349
|
+
const widthControl = document.createElement("label");
|
|
350
|
+
widthControl.className = "layer-control-width-control";
|
|
351
|
+
widthControl.title = "Adjust layer panel width";
|
|
352
|
+
const widthLabel = document.createElement("span");
|
|
353
|
+
widthLabel.textContent = "Width";
|
|
354
|
+
widthControl.appendChild(widthLabel);
|
|
355
|
+
const widthSlider = document.createElement("div");
|
|
356
|
+
widthSlider.className = "layer-control-width-slider";
|
|
357
|
+
widthSlider.setAttribute("role", "slider");
|
|
358
|
+
widthSlider.setAttribute("aria-valuemin", String(this.minPanelWidth));
|
|
359
|
+
widthSlider.setAttribute("aria-valuemax", String(this.maxPanelWidth));
|
|
360
|
+
widthSlider.setAttribute("aria-valuenow", String(this.state.panelWidth));
|
|
361
|
+
widthSlider.setAttribute("aria-valuestep", "10");
|
|
362
|
+
widthSlider.setAttribute("aria-label", "Layer panel width");
|
|
363
|
+
widthSlider.tabIndex = 0;
|
|
364
|
+
const widthTrack = document.createElement("div");
|
|
365
|
+
widthTrack.className = "layer-control-width-track";
|
|
366
|
+
const widthThumb = document.createElement("div");
|
|
367
|
+
widthThumb.className = "layer-control-width-thumb";
|
|
368
|
+
widthSlider.appendChild(widthTrack);
|
|
369
|
+
widthSlider.appendChild(widthThumb);
|
|
370
|
+
this.widthSliderEl = widthSlider;
|
|
371
|
+
this.widthThumbEl = widthThumb;
|
|
372
|
+
const widthValue = document.createElement("span");
|
|
373
|
+
widthValue.className = "layer-control-width-value";
|
|
374
|
+
this.widthValueEl = widthValue;
|
|
375
|
+
widthControl.appendChild(widthSlider);
|
|
376
|
+
widthControl.appendChild(widthValue);
|
|
377
|
+
this.updateWidthDisplay();
|
|
378
|
+
this.setupWidthSliderEvents(widthSlider);
|
|
379
|
+
return widthControl;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Setup event listeners for width slider
|
|
383
|
+
*/
|
|
384
|
+
setupWidthSliderEvents(widthSlider) {
|
|
385
|
+
widthSlider.addEventListener("pointerdown", (event) => {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
const rect = widthSlider.getBoundingClientRect();
|
|
388
|
+
this.widthDragRectWidth = rect.width || 1;
|
|
389
|
+
this.widthDragStartX = event.clientX;
|
|
390
|
+
this.widthDragStartWidth = this.state.panelWidth;
|
|
391
|
+
this.isWidthSliderActive = true;
|
|
392
|
+
widthSlider.setPointerCapture(event.pointerId);
|
|
393
|
+
this.updateWidthFromPointer(event, true);
|
|
394
|
+
});
|
|
395
|
+
widthSlider.addEventListener("pointermove", (event) => {
|
|
396
|
+
if (!this.isWidthSliderActive) return;
|
|
397
|
+
this.updateWidthFromPointer(event);
|
|
398
|
+
});
|
|
399
|
+
const endPointerDrag = (event) => {
|
|
400
|
+
if (!this.isWidthSliderActive) return;
|
|
401
|
+
if (event.pointerId !== void 0) {
|
|
402
|
+
try {
|
|
403
|
+
widthSlider.releasePointerCapture(event.pointerId);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
this.isWidthSliderActive = false;
|
|
408
|
+
this.widthDragRectWidth = null;
|
|
409
|
+
this.widthDragStartX = null;
|
|
410
|
+
this.widthDragStartWidth = null;
|
|
411
|
+
this.updateWidthDisplay();
|
|
412
|
+
};
|
|
413
|
+
widthSlider.addEventListener("pointerup", endPointerDrag);
|
|
414
|
+
widthSlider.addEventListener("pointercancel", endPointerDrag);
|
|
415
|
+
widthSlider.addEventListener("lostpointercapture", endPointerDrag);
|
|
416
|
+
widthSlider.addEventListener("keydown", (event) => {
|
|
417
|
+
let handled = true;
|
|
418
|
+
const step = event.shiftKey ? 20 : 10;
|
|
419
|
+
switch (event.key) {
|
|
420
|
+
case "ArrowLeft":
|
|
421
|
+
case "ArrowDown":
|
|
422
|
+
this.applyPanelWidth(this.state.panelWidth - step, true);
|
|
423
|
+
break;
|
|
424
|
+
case "ArrowRight":
|
|
425
|
+
case "ArrowUp":
|
|
426
|
+
this.applyPanelWidth(this.state.panelWidth + step, true);
|
|
427
|
+
break;
|
|
428
|
+
case "Home":
|
|
429
|
+
this.applyPanelWidth(this.minPanelWidth, true);
|
|
430
|
+
break;
|
|
431
|
+
case "End":
|
|
432
|
+
this.applyPanelWidth(this.maxPanelWidth, true);
|
|
433
|
+
break;
|
|
434
|
+
case "PageUp":
|
|
435
|
+
this.applyPanelWidth(this.state.panelWidth + 50, true);
|
|
436
|
+
break;
|
|
437
|
+
case "PageDown":
|
|
438
|
+
this.applyPanelWidth(this.state.panelWidth - 50, true);
|
|
439
|
+
break;
|
|
440
|
+
default:
|
|
441
|
+
handled = false;
|
|
442
|
+
}
|
|
443
|
+
if (handled) {
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
this.updateWidthDisplay();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Update panel width from pointer event
|
|
451
|
+
*/
|
|
452
|
+
updateWidthFromPointer(event, resetBaseline = false) {
|
|
453
|
+
if (!this.widthSliderEl) return;
|
|
454
|
+
const sliderWidth = this.widthDragRectWidth || this.widthSliderEl.getBoundingClientRect().width || 1;
|
|
455
|
+
const widthRange = this.maxPanelWidth - this.minPanelWidth;
|
|
456
|
+
let width;
|
|
457
|
+
if (resetBaseline) {
|
|
458
|
+
const rect = this.widthSliderEl.getBoundingClientRect();
|
|
459
|
+
const relative = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0;
|
|
460
|
+
const clampedRatio = Math.min(1, Math.max(0, relative));
|
|
461
|
+
width = this.minPanelWidth + clampedRatio * widthRange;
|
|
462
|
+
this.widthDragStartWidth = width;
|
|
463
|
+
this.widthDragStartX = event.clientX;
|
|
464
|
+
} else {
|
|
465
|
+
const delta = event.clientX - (this.widthDragStartX || event.clientX);
|
|
466
|
+
width = (this.widthDragStartWidth || this.state.panelWidth) + delta / sliderWidth * widthRange;
|
|
467
|
+
}
|
|
468
|
+
this.applyPanelWidth(width, this.isWidthSliderActive);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Apply panel width (clamped to min/max)
|
|
472
|
+
*/
|
|
473
|
+
applyPanelWidth(width, immediate = false) {
|
|
474
|
+
const clamped = Math.round(Math.min(this.maxPanelWidth, Math.max(this.minPanelWidth, width)));
|
|
475
|
+
const applyWidth = () => {
|
|
476
|
+
this.state.panelWidth = clamped;
|
|
477
|
+
const px = `${clamped}px`;
|
|
478
|
+
this.panel.style.width = px;
|
|
479
|
+
this.updateWidthDisplay();
|
|
480
|
+
};
|
|
481
|
+
if (immediate) {
|
|
482
|
+
applyWidth();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (this.widthFrame) {
|
|
486
|
+
cancelAnimationFrame(this.widthFrame);
|
|
487
|
+
}
|
|
488
|
+
this.widthFrame = requestAnimationFrame(() => {
|
|
489
|
+
applyWidth();
|
|
490
|
+
this.widthFrame = null;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Update width display (value label and thumb position)
|
|
495
|
+
*/
|
|
496
|
+
updateWidthDisplay() {
|
|
497
|
+
if (this.widthValueEl) {
|
|
498
|
+
this.widthValueEl.textContent = `${this.state.panelWidth}px`;
|
|
499
|
+
}
|
|
500
|
+
if (this.widthSliderEl) {
|
|
501
|
+
this.widthSliderEl.setAttribute("aria-valuenow", String(this.state.panelWidth));
|
|
502
|
+
const ratio = (this.state.panelWidth - this.minPanelWidth) / (this.maxPanelWidth - this.minPanelWidth || 1);
|
|
503
|
+
if (this.widthThumbEl) {
|
|
504
|
+
const sliderWidth = this.widthSliderEl.clientWidth || 1;
|
|
505
|
+
const thumbWidth = this.widthThumbEl.offsetWidth || 14;
|
|
506
|
+
const padding = 16;
|
|
507
|
+
const available = Math.max(0, sliderWidth - padding - thumbWidth);
|
|
508
|
+
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
|
509
|
+
const leftPx = 8 + available * clampedRatio;
|
|
510
|
+
this.widthThumbEl.style.left = `${leftPx}px`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Setup main event listeners
|
|
516
|
+
*/
|
|
517
|
+
setupEventListeners() {
|
|
518
|
+
this.button.addEventListener("click", () => this.toggle());
|
|
519
|
+
document.addEventListener("click", (e) => {
|
|
520
|
+
if (!this.container.contains(e.target)) {
|
|
521
|
+
this.collapse();
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
this.setupLayerChangeListeners();
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Setup listeners for map layer changes
|
|
528
|
+
*/
|
|
529
|
+
setupLayerChangeListeners() {
|
|
530
|
+
this.map.on("styledata", () => {
|
|
531
|
+
setTimeout(() => {
|
|
532
|
+
this.updateLayerStatesFromMap();
|
|
533
|
+
this.checkForNewLayers();
|
|
534
|
+
}, 100);
|
|
535
|
+
});
|
|
536
|
+
this.map.on("data", (e) => {
|
|
537
|
+
if (e.sourceDataType === "content") {
|
|
538
|
+
setTimeout(() => {
|
|
539
|
+
this.updateLayerStatesFromMap();
|
|
540
|
+
this.checkForNewLayers();
|
|
541
|
+
}, 100);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
this.map.on("sourcedata", (e) => {
|
|
545
|
+
if (e.sourceDataType === "metadata") {
|
|
546
|
+
setTimeout(() => {
|
|
547
|
+
this.checkForNewLayers();
|
|
548
|
+
}, 150);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Toggle panel expanded/collapsed state
|
|
554
|
+
*/
|
|
555
|
+
toggle() {
|
|
556
|
+
if (this.state.collapsed) {
|
|
557
|
+
this.expand();
|
|
558
|
+
} else {
|
|
559
|
+
this.collapse();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Expand the panel
|
|
564
|
+
*/
|
|
565
|
+
expand() {
|
|
566
|
+
this.state.collapsed = false;
|
|
567
|
+
this.panel.classList.add("expanded");
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Collapse the panel
|
|
571
|
+
*/
|
|
572
|
+
collapse() {
|
|
573
|
+
this.state.collapsed = true;
|
|
574
|
+
this.panel.classList.remove("expanded");
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Build layer items (called initially and when layers change)
|
|
578
|
+
*/
|
|
579
|
+
buildLayerItems() {
|
|
580
|
+
const existingItems = this.panel.querySelectorAll(".layer-control-item");
|
|
581
|
+
existingItems.forEach((item) => item.remove());
|
|
582
|
+
this.styleEditors.clear();
|
|
583
|
+
Object.entries(this.state.layerStates).forEach(([layerId, state]) => {
|
|
584
|
+
if (this.targetLayers.length === 0 || this.targetLayers.includes(layerId)) {
|
|
585
|
+
this.addLayerItem(layerId, state);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Add a single layer item to the panel
|
|
591
|
+
*/
|
|
592
|
+
addLayerItem(layerId, state) {
|
|
593
|
+
const item = document.createElement("div");
|
|
594
|
+
item.className = "layer-control-item";
|
|
595
|
+
item.setAttribute("data-layer-id", layerId);
|
|
596
|
+
const row = document.createElement("div");
|
|
597
|
+
row.className = "layer-control-row";
|
|
598
|
+
const checkbox = document.createElement("input");
|
|
599
|
+
checkbox.type = "checkbox";
|
|
600
|
+
checkbox.className = "layer-control-checkbox";
|
|
601
|
+
checkbox.checked = state.visible;
|
|
602
|
+
checkbox.addEventListener("change", () => {
|
|
603
|
+
this.toggleLayerVisibility(layerId, checkbox.checked);
|
|
604
|
+
});
|
|
605
|
+
const name = document.createElement("span");
|
|
606
|
+
name.className = "layer-control-name";
|
|
607
|
+
name.textContent = state.name || layerId;
|
|
608
|
+
name.title = state.name || layerId;
|
|
609
|
+
const opacity = document.createElement("input");
|
|
610
|
+
opacity.type = "range";
|
|
611
|
+
opacity.className = "layer-control-opacity";
|
|
612
|
+
opacity.min = "0";
|
|
613
|
+
opacity.max = "1";
|
|
614
|
+
opacity.step = "0.01";
|
|
615
|
+
opacity.value = String(state.opacity);
|
|
616
|
+
opacity.title = `Opacity: ${Math.round(state.opacity * 100)}%`;
|
|
617
|
+
opacity.addEventListener("mousedown", () => {
|
|
618
|
+
this.state.userInteractingWithSlider = true;
|
|
619
|
+
});
|
|
620
|
+
opacity.addEventListener("mouseup", () => {
|
|
621
|
+
this.state.userInteractingWithSlider = false;
|
|
622
|
+
});
|
|
623
|
+
opacity.addEventListener("input", () => {
|
|
624
|
+
this.changeLayerOpacity(layerId, parseFloat(opacity.value));
|
|
625
|
+
opacity.title = `Opacity: ${Math.round(parseFloat(opacity.value) * 100)}%`;
|
|
626
|
+
});
|
|
627
|
+
row.appendChild(checkbox);
|
|
628
|
+
row.appendChild(name);
|
|
629
|
+
row.appendChild(opacity);
|
|
630
|
+
if (layerId !== "Background") {
|
|
631
|
+
const styleButton = this.createStyleButton(layerId);
|
|
632
|
+
if (styleButton) {
|
|
633
|
+
row.appendChild(styleButton);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
item.appendChild(row);
|
|
637
|
+
this.panel.appendChild(item);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Toggle layer visibility
|
|
641
|
+
*/
|
|
642
|
+
toggleLayerVisibility(layerId, visible) {
|
|
643
|
+
if (layerId === "Background") {
|
|
644
|
+
this.toggleBackgroundVisibility(visible);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (this.state.layerStates[layerId]) {
|
|
648
|
+
this.state.layerStates[layerId].visible = visible;
|
|
649
|
+
}
|
|
650
|
+
this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Change layer opacity
|
|
654
|
+
*/
|
|
655
|
+
changeLayerOpacity(layerId, opacity) {
|
|
656
|
+
if (layerId === "Background") {
|
|
657
|
+
this.changeBackgroundOpacity(opacity);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (this.state.layerStates[layerId]) {
|
|
661
|
+
this.state.layerStates[layerId].opacity = opacity;
|
|
662
|
+
}
|
|
663
|
+
const layerType = getLayerType(this.map, layerId);
|
|
664
|
+
if (layerType) {
|
|
665
|
+
setLayerOpacity(this.map, layerId, layerType, opacity);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Check if a layer is a user-added layer (vs basemap layer)
|
|
670
|
+
*/
|
|
671
|
+
isUserAddedLayer(layerId) {
|
|
672
|
+
return this.state.layerStates[layerId] !== void 0 && layerId !== "Background";
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Toggle visibility for all background layers (basemap layers)
|
|
676
|
+
*/
|
|
677
|
+
toggleBackgroundVisibility(visible) {
|
|
678
|
+
if (this.state.layerStates["Background"]) {
|
|
679
|
+
this.state.layerStates["Background"].visible = visible;
|
|
680
|
+
}
|
|
681
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
682
|
+
styleLayers.forEach((layer) => {
|
|
683
|
+
if (!this.isUserAddedLayer(layer.id)) {
|
|
684
|
+
this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Change opacity for all background layers (basemap layers)
|
|
690
|
+
*/
|
|
691
|
+
changeBackgroundOpacity(opacity) {
|
|
692
|
+
if (this.state.layerStates["Background"]) {
|
|
693
|
+
this.state.layerStates["Background"].opacity = opacity;
|
|
694
|
+
}
|
|
695
|
+
const styleLayers = this.map.getStyle().layers || [];
|
|
696
|
+
styleLayers.forEach((styleLayer) => {
|
|
697
|
+
if (!this.isUserAddedLayer(styleLayer.id)) {
|
|
698
|
+
const layerType = getLayerType(this.map, styleLayer.id);
|
|
699
|
+
if (layerType) {
|
|
700
|
+
setLayerOpacity(this.map, styleLayer.id, layerType, opacity);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Create style button for a layer
|
|
707
|
+
*/
|
|
708
|
+
createStyleButton(layerId) {
|
|
709
|
+
if (layerId === "Background") {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const button = document.createElement("button");
|
|
713
|
+
button.className = "layer-control-style-button";
|
|
714
|
+
button.innerHTML = "⚙";
|
|
715
|
+
button.title = "Edit layer style";
|
|
716
|
+
button.setAttribute("aria-label", `Edit style for ${layerId}`);
|
|
717
|
+
button.addEventListener("click", (e) => {
|
|
718
|
+
e.stopPropagation();
|
|
719
|
+
this.toggleStyleEditor(layerId);
|
|
720
|
+
});
|
|
721
|
+
return button;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Toggle style editor for a layer
|
|
725
|
+
*/
|
|
726
|
+
toggleStyleEditor(layerId) {
|
|
727
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
728
|
+
this.closeStyleEditor(layerId);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (this.state.activeStyleEditor) {
|
|
732
|
+
this.closeStyleEditor(this.state.activeStyleEditor);
|
|
733
|
+
}
|
|
734
|
+
this.openStyleEditor(layerId);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Open style editor for a layer
|
|
738
|
+
*/
|
|
739
|
+
openStyleEditor(layerId) {
|
|
740
|
+
const itemEl = this.panel.querySelector(`[data-layer-id="${layerId}"]`);
|
|
741
|
+
if (!itemEl) return;
|
|
742
|
+
if (!this.state.originalStyles.has(layerId)) {
|
|
743
|
+
const layer = this.map.getLayer(layerId);
|
|
744
|
+
if (layer) {
|
|
745
|
+
cacheOriginalLayerStyle(this.map, layerId, this.state.originalStyles);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const editor = this.createStyleEditor(layerId);
|
|
749
|
+
if (!editor) return;
|
|
750
|
+
itemEl.appendChild(editor);
|
|
751
|
+
this.styleEditors.set(layerId, editor);
|
|
752
|
+
this.state.activeStyleEditor = layerId;
|
|
753
|
+
setTimeout(() => {
|
|
754
|
+
editor.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
755
|
+
}, 50);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Close style editor for a layer
|
|
759
|
+
*/
|
|
760
|
+
closeStyleEditor(layerId) {
|
|
761
|
+
const editor = this.styleEditors.get(layerId);
|
|
762
|
+
if (editor) {
|
|
763
|
+
editor.remove();
|
|
764
|
+
this.styleEditors.delete(layerId);
|
|
765
|
+
}
|
|
766
|
+
if (this.state.activeStyleEditor === layerId) {
|
|
767
|
+
this.state.activeStyleEditor = null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Create style editor UI
|
|
772
|
+
*/
|
|
773
|
+
createStyleEditor(layerId) {
|
|
774
|
+
const layer = this.map.getLayer(layerId);
|
|
775
|
+
if (!layer) return null;
|
|
776
|
+
const editor = document.createElement("div");
|
|
777
|
+
editor.className = "layer-control-style-editor";
|
|
778
|
+
const header = document.createElement("div");
|
|
779
|
+
header.className = "style-editor-header";
|
|
780
|
+
const title = document.createElement("span");
|
|
781
|
+
title.className = "style-editor-title";
|
|
782
|
+
title.textContent = "Edit Style";
|
|
783
|
+
const closeBtn = document.createElement("button");
|
|
784
|
+
closeBtn.className = "style-editor-close";
|
|
785
|
+
closeBtn.innerHTML = "×";
|
|
786
|
+
closeBtn.title = "Close";
|
|
787
|
+
closeBtn.addEventListener("click", (e) => {
|
|
788
|
+
e.stopPropagation();
|
|
789
|
+
this.closeStyleEditor(layerId);
|
|
790
|
+
});
|
|
791
|
+
header.appendChild(title);
|
|
792
|
+
header.appendChild(closeBtn);
|
|
793
|
+
const controls = document.createElement("div");
|
|
794
|
+
controls.className = "style-editor-controls";
|
|
795
|
+
const layerType = layer.type;
|
|
796
|
+
this.addStyleControlsForLayerType(controls, layerId, layerType);
|
|
797
|
+
const actions = document.createElement("div");
|
|
798
|
+
actions.className = "style-editor-actions";
|
|
799
|
+
const resetBtn = document.createElement("button");
|
|
800
|
+
resetBtn.className = "style-editor-button style-editor-button-reset";
|
|
801
|
+
resetBtn.textContent = "Reset";
|
|
802
|
+
resetBtn.addEventListener("click", (e) => {
|
|
803
|
+
e.stopPropagation();
|
|
804
|
+
this.resetLayerStyle(layerId);
|
|
805
|
+
});
|
|
806
|
+
const closeActionBtn = document.createElement("button");
|
|
807
|
+
closeActionBtn.className = "style-editor-button style-editor-button-close";
|
|
808
|
+
closeActionBtn.textContent = "Close";
|
|
809
|
+
closeActionBtn.addEventListener("click", (e) => {
|
|
810
|
+
e.stopPropagation();
|
|
811
|
+
this.closeStyleEditor(layerId);
|
|
812
|
+
});
|
|
813
|
+
actions.appendChild(resetBtn);
|
|
814
|
+
actions.appendChild(closeActionBtn);
|
|
815
|
+
editor.appendChild(header);
|
|
816
|
+
editor.appendChild(controls);
|
|
817
|
+
editor.appendChild(actions);
|
|
818
|
+
return editor;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Add style controls based on layer type
|
|
822
|
+
*/
|
|
823
|
+
addStyleControlsForLayerType(container, layerId, layerType) {
|
|
824
|
+
switch (layerType) {
|
|
825
|
+
case "fill":
|
|
826
|
+
this.addFillControls(container, layerId);
|
|
827
|
+
break;
|
|
828
|
+
case "line":
|
|
829
|
+
this.addLineControls(container, layerId);
|
|
830
|
+
break;
|
|
831
|
+
case "circle":
|
|
832
|
+
this.addCircleControls(container, layerId);
|
|
833
|
+
break;
|
|
834
|
+
case "raster":
|
|
835
|
+
this.addRasterControls(container, layerId);
|
|
836
|
+
break;
|
|
837
|
+
case "symbol":
|
|
838
|
+
this.addSymbolControls(container, layerId);
|
|
839
|
+
break;
|
|
840
|
+
default:
|
|
841
|
+
container.textContent = `Style controls for ${layerType} layers not yet implemented.`;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Add controls for fill layers
|
|
846
|
+
*/
|
|
847
|
+
addFillControls(container, layerId) {
|
|
848
|
+
var _a;
|
|
849
|
+
const style = this.map.getStyle();
|
|
850
|
+
const layer = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
851
|
+
let fillColor = void 0;
|
|
852
|
+
if (layer && "paint" in layer && layer.paint && "fill-color" in layer.paint) {
|
|
853
|
+
fillColor = layer.paint["fill-color"];
|
|
854
|
+
}
|
|
855
|
+
if (!fillColor) {
|
|
856
|
+
fillColor = this.map.getPaintProperty(layerId, "fill-color");
|
|
857
|
+
}
|
|
858
|
+
this.createColorControl(container, layerId, "fill-color", "Fill Color", normalizeColor(fillColor || "#088"));
|
|
859
|
+
const fillOpacity = this.map.getPaintProperty(layerId, "fill-opacity");
|
|
860
|
+
if (fillOpacity !== void 0 && typeof fillOpacity === "number") {
|
|
861
|
+
this.createSliderControl(container, layerId, "fill-opacity", "Fill Opacity", fillOpacity, 0, 1, 0.05);
|
|
862
|
+
}
|
|
863
|
+
const outlineColor = this.map.getPaintProperty(layerId, "fill-outline-color");
|
|
864
|
+
if (outlineColor !== void 0) {
|
|
865
|
+
this.createColorControl(container, layerId, "fill-outline-color", "Outline Color", normalizeColor(outlineColor));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Add controls for line layers
|
|
870
|
+
*/
|
|
871
|
+
addLineControls(container, layerId) {
|
|
872
|
+
var _a;
|
|
873
|
+
const style = this.map.getStyle();
|
|
874
|
+
const layer = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
875
|
+
let lineColor = void 0;
|
|
876
|
+
if (layer && "paint" in layer && layer.paint && "line-color" in layer.paint) {
|
|
877
|
+
lineColor = layer.paint["line-color"];
|
|
878
|
+
}
|
|
879
|
+
if (!lineColor) {
|
|
880
|
+
lineColor = this.map.getPaintProperty(layerId, "line-color");
|
|
881
|
+
}
|
|
882
|
+
this.createColorControl(container, layerId, "line-color", "Line Color", normalizeColor(lineColor || "#000"));
|
|
883
|
+
const lineWidth = this.map.getPaintProperty(layerId, "line-width");
|
|
884
|
+
this.createSliderControl(container, layerId, "line-width", "Line Width", typeof lineWidth === "number" ? lineWidth : 1, 0, 20, 0.5);
|
|
885
|
+
const lineOpacity = this.map.getPaintProperty(layerId, "line-opacity");
|
|
886
|
+
if (lineOpacity !== void 0 && typeof lineOpacity === "number") {
|
|
887
|
+
this.createSliderControl(container, layerId, "line-opacity", "Line Opacity", lineOpacity, 0, 1, 0.05);
|
|
888
|
+
}
|
|
889
|
+
const lineBlur = this.map.getPaintProperty(layerId, "line-blur");
|
|
890
|
+
if (lineBlur !== void 0 && typeof lineBlur === "number") {
|
|
891
|
+
this.createSliderControl(container, layerId, "line-blur", "Line Blur", lineBlur, 0, 5, 0.1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Add controls for circle layers
|
|
896
|
+
*/
|
|
897
|
+
addCircleControls(container, layerId) {
|
|
898
|
+
var _a;
|
|
899
|
+
const style = this.map.getStyle();
|
|
900
|
+
const layer = (_a = style.layers) == null ? void 0 : _a.find((l) => l.id === layerId);
|
|
901
|
+
let circleColor = void 0;
|
|
902
|
+
if (layer && "paint" in layer && layer.paint && "circle-color" in layer.paint) {
|
|
903
|
+
circleColor = layer.paint["circle-color"];
|
|
904
|
+
}
|
|
905
|
+
if (!circleColor) {
|
|
906
|
+
circleColor = this.map.getPaintProperty(layerId, "circle-color");
|
|
907
|
+
}
|
|
908
|
+
this.createColorControl(container, layerId, "circle-color", "Circle Color", normalizeColor(circleColor || "#000"));
|
|
909
|
+
const circleRadius = this.map.getPaintProperty(layerId, "circle-radius");
|
|
910
|
+
this.createSliderControl(container, layerId, "circle-radius", "Radius", typeof circleRadius === "number" ? circleRadius : 5, 0, 40, 0.5);
|
|
911
|
+
const circleOpacity = this.map.getPaintProperty(layerId, "circle-opacity");
|
|
912
|
+
if (circleOpacity !== void 0 && typeof circleOpacity === "number") {
|
|
913
|
+
this.createSliderControl(container, layerId, "circle-opacity", "Opacity", circleOpacity, 0, 1, 0.05);
|
|
914
|
+
}
|
|
915
|
+
const strokeColor = this.map.getPaintProperty(layerId, "circle-stroke-color");
|
|
916
|
+
if (strokeColor !== void 0) {
|
|
917
|
+
this.createColorControl(container, layerId, "circle-stroke-color", "Stroke Color", normalizeColor(strokeColor));
|
|
918
|
+
}
|
|
919
|
+
const strokeWidth = this.map.getPaintProperty(layerId, "circle-stroke-width");
|
|
920
|
+
if (strokeWidth !== void 0 && typeof strokeWidth === "number") {
|
|
921
|
+
this.createSliderControl(container, layerId, "circle-stroke-width", "Stroke Width", strokeWidth, 0, 10, 0.1);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Add controls for raster layers
|
|
926
|
+
*/
|
|
927
|
+
addRasterControls(container, layerId) {
|
|
928
|
+
const rasterOpacity = this.map.getPaintProperty(layerId, "raster-opacity");
|
|
929
|
+
this.createSliderControl(container, layerId, "raster-opacity", "Opacity", typeof rasterOpacity === "number" ? rasterOpacity : 1, 0, 1, 0.05);
|
|
930
|
+
const brightnessMin = this.map.getPaintProperty(layerId, "raster-brightness-min");
|
|
931
|
+
this.createSliderControl(container, layerId, "raster-brightness-min", "Brightness Min", typeof brightnessMin === "number" ? brightnessMin : 0, -1, 1, 0.05);
|
|
932
|
+
const brightnessMax = this.map.getPaintProperty(layerId, "raster-brightness-max");
|
|
933
|
+
this.createSliderControl(container, layerId, "raster-brightness-max", "Brightness Max", typeof brightnessMax === "number" ? brightnessMax : 1, -1, 1, 0.05);
|
|
934
|
+
const saturation = this.map.getPaintProperty(layerId, "raster-saturation");
|
|
935
|
+
this.createSliderControl(container, layerId, "raster-saturation", "Saturation", typeof saturation === "number" ? saturation : 0, -1, 1, 0.05);
|
|
936
|
+
const contrast = this.map.getPaintProperty(layerId, "raster-contrast");
|
|
937
|
+
this.createSliderControl(container, layerId, "raster-contrast", "Contrast", typeof contrast === "number" ? contrast : 0, -1, 1, 0.05);
|
|
938
|
+
const hueRotate = this.map.getPaintProperty(layerId, "raster-hue-rotate");
|
|
939
|
+
this.createSliderControl(container, layerId, "raster-hue-rotate", "Hue Rotate", typeof hueRotate === "number" ? hueRotate : 0, 0, 360, 5);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Add controls for symbol layers
|
|
943
|
+
*/
|
|
944
|
+
addSymbolControls(container, layerId) {
|
|
945
|
+
const textColor = this.map.getPaintProperty(layerId, "text-color");
|
|
946
|
+
if (textColor !== void 0) {
|
|
947
|
+
this.createColorControl(container, layerId, "text-color", "Text Color", normalizeColor(textColor));
|
|
948
|
+
}
|
|
949
|
+
const textOpacity = this.map.getPaintProperty(layerId, "text-opacity");
|
|
950
|
+
if (textOpacity !== void 0 && typeof textOpacity === "number") {
|
|
951
|
+
this.createSliderControl(container, layerId, "text-opacity", "Text Opacity", textOpacity, 0, 1, 0.05);
|
|
952
|
+
}
|
|
953
|
+
const iconOpacity = this.map.getPaintProperty(layerId, "icon-opacity");
|
|
954
|
+
if (iconOpacity !== void 0 && typeof iconOpacity === "number") {
|
|
955
|
+
this.createSliderControl(container, layerId, "icon-opacity", "Icon Opacity", iconOpacity, 0, 1, 0.05);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Create a color control
|
|
960
|
+
*/
|
|
961
|
+
createColorControl(container, layerId, property, label, initialValue) {
|
|
962
|
+
const controlGroup = document.createElement("div");
|
|
963
|
+
controlGroup.className = "style-control-group";
|
|
964
|
+
const labelEl = document.createElement("label");
|
|
965
|
+
labelEl.className = "style-control-label";
|
|
966
|
+
labelEl.textContent = label;
|
|
967
|
+
const inputWrapper = document.createElement("div");
|
|
968
|
+
inputWrapper.className = "style-control-color-group";
|
|
969
|
+
const colorInput = document.createElement("input");
|
|
970
|
+
colorInput.type = "color";
|
|
971
|
+
colorInput.className = "style-control-color-picker";
|
|
972
|
+
colorInput.value = initialValue;
|
|
973
|
+
colorInput.dataset.property = property;
|
|
974
|
+
const hexDisplay = document.createElement("input");
|
|
975
|
+
hexDisplay.type = "text";
|
|
976
|
+
hexDisplay.className = "style-control-color-value";
|
|
977
|
+
hexDisplay.value = initialValue;
|
|
978
|
+
hexDisplay.readOnly = true;
|
|
979
|
+
colorInput.addEventListener("input", () => {
|
|
980
|
+
const color = colorInput.value;
|
|
981
|
+
hexDisplay.value = color;
|
|
982
|
+
this.map.setPaintProperty(layerId, property, color);
|
|
983
|
+
});
|
|
984
|
+
inputWrapper.appendChild(colorInput);
|
|
985
|
+
inputWrapper.appendChild(hexDisplay);
|
|
986
|
+
controlGroup.appendChild(labelEl);
|
|
987
|
+
controlGroup.appendChild(inputWrapper);
|
|
988
|
+
container.appendChild(controlGroup);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Create a slider control
|
|
992
|
+
*/
|
|
993
|
+
createSliderControl(container, layerId, property, label, initialValue, min, max, step) {
|
|
994
|
+
const controlGroup = document.createElement("div");
|
|
995
|
+
controlGroup.className = "style-control-group";
|
|
996
|
+
const labelEl = document.createElement("label");
|
|
997
|
+
labelEl.className = "style-control-label";
|
|
998
|
+
labelEl.textContent = label;
|
|
999
|
+
const inputWrapper = document.createElement("div");
|
|
1000
|
+
inputWrapper.className = "style-control-input-wrapper";
|
|
1001
|
+
const slider = document.createElement("input");
|
|
1002
|
+
slider.type = "range";
|
|
1003
|
+
slider.className = "style-control-slider";
|
|
1004
|
+
slider.min = String(min);
|
|
1005
|
+
slider.max = String(max);
|
|
1006
|
+
slider.step = String(step);
|
|
1007
|
+
slider.value = String(initialValue);
|
|
1008
|
+
slider.dataset.property = property;
|
|
1009
|
+
const valueDisplay = document.createElement("span");
|
|
1010
|
+
valueDisplay.className = "style-control-value";
|
|
1011
|
+
valueDisplay.textContent = formatNumericValue(initialValue, step);
|
|
1012
|
+
slider.addEventListener("input", () => {
|
|
1013
|
+
const value = parseFloat(slider.value);
|
|
1014
|
+
valueDisplay.textContent = formatNumericValue(value, step);
|
|
1015
|
+
this.map.setPaintProperty(layerId, property, value);
|
|
1016
|
+
});
|
|
1017
|
+
inputWrapper.appendChild(slider);
|
|
1018
|
+
inputWrapper.appendChild(valueDisplay);
|
|
1019
|
+
controlGroup.appendChild(labelEl);
|
|
1020
|
+
controlGroup.appendChild(inputWrapper);
|
|
1021
|
+
container.appendChild(controlGroup);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Reset layer style to original
|
|
1025
|
+
*/
|
|
1026
|
+
resetLayerStyle(layerId) {
|
|
1027
|
+
const originalStyle = this.state.originalStyles.get(layerId);
|
|
1028
|
+
if (!originalStyle) return;
|
|
1029
|
+
restoreOriginalStyle(this.map, layerId, this.state.originalStyles);
|
|
1030
|
+
const editor = this.styleEditors.get(layerId);
|
|
1031
|
+
if (editor) {
|
|
1032
|
+
const sliders = editor.querySelectorAll(".style-control-slider");
|
|
1033
|
+
sliders.forEach((slider) => {
|
|
1034
|
+
var _a;
|
|
1035
|
+
const property = slider.dataset.property;
|
|
1036
|
+
if (property) {
|
|
1037
|
+
const value = this.map.getPaintProperty(layerId, property);
|
|
1038
|
+
if (value !== void 0 && typeof value === "number") {
|
|
1039
|
+
slider.value = String(value);
|
|
1040
|
+
const valueDisplay = (_a = slider.parentElement) == null ? void 0 : _a.querySelector(".style-control-value");
|
|
1041
|
+
if (valueDisplay) {
|
|
1042
|
+
const step = parseFloat(slider.step);
|
|
1043
|
+
valueDisplay.textContent = formatNumericValue(value, step);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
const colorPickers = editor.querySelectorAll(".style-control-color-picker");
|
|
1049
|
+
colorPickers.forEach((picker) => {
|
|
1050
|
+
var _a;
|
|
1051
|
+
const property = picker.dataset.property;
|
|
1052
|
+
if (property) {
|
|
1053
|
+
const value = this.map.getPaintProperty(layerId, property);
|
|
1054
|
+
if (value !== void 0) {
|
|
1055
|
+
const hexColor = normalizeColor(value);
|
|
1056
|
+
picker.value = hexColor;
|
|
1057
|
+
const hexDisplay = (_a = picker.parentElement) == null ? void 0 : _a.querySelector(".style-control-color-value");
|
|
1058
|
+
if (hexDisplay) {
|
|
1059
|
+
hexDisplay.textContent = hexColor;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Update layer states from map (sync UI with map)
|
|
1068
|
+
*/
|
|
1069
|
+
updateLayerStatesFromMap() {
|
|
1070
|
+
if (this.state.userInteractingWithSlider) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
Object.keys(this.state.layerStates).forEach((layerId) => {
|
|
1074
|
+
try {
|
|
1075
|
+
const layer = this.map.getLayer(layerId);
|
|
1076
|
+
if (!layer) return;
|
|
1077
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
1078
|
+
const isVisible = visibility !== "none";
|
|
1079
|
+
const layerType = layer.type;
|
|
1080
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
1081
|
+
if (this.state.layerStates[layerId]) {
|
|
1082
|
+
this.state.layerStates[layerId].visible = isVisible;
|
|
1083
|
+
this.state.layerStates[layerId].opacity = opacity;
|
|
1084
|
+
}
|
|
1085
|
+
this.updateUIForLayer(layerId, isVisible, opacity);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
console.warn(`Failed to update state for layer ${layerId}:`, error);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Update UI elements for a specific layer
|
|
1093
|
+
*/
|
|
1094
|
+
updateUIForLayer(layerId, visible, opacity) {
|
|
1095
|
+
const layerItems = this.panel.querySelectorAll(".layer-control-item");
|
|
1096
|
+
layerItems.forEach((item) => {
|
|
1097
|
+
if (item.dataset.layerId === layerId) {
|
|
1098
|
+
const checkbox = item.querySelector(".layer-control-checkbox");
|
|
1099
|
+
const opacitySlider = item.querySelector(".layer-control-opacity");
|
|
1100
|
+
if (checkbox) {
|
|
1101
|
+
checkbox.checked = visible;
|
|
1102
|
+
}
|
|
1103
|
+
if (opacitySlider) {
|
|
1104
|
+
opacitySlider.value = String(opacity);
|
|
1105
|
+
opacitySlider.title = `Opacity: ${Math.round(opacity * 100)}%`;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Check for new layers and add them to the control
|
|
1112
|
+
*/
|
|
1113
|
+
checkForNewLayers() {
|
|
1114
|
+
try {
|
|
1115
|
+
const style = this.map.getStyle();
|
|
1116
|
+
if (!style || !style.layers) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const currentMapLayers = style.layers.map((layer) => layer.id).filter((id) => {
|
|
1120
|
+
if (this.targetLayers.length > 0) {
|
|
1121
|
+
return this.targetLayers.includes(id);
|
|
1122
|
+
}
|
|
1123
|
+
return true;
|
|
1124
|
+
});
|
|
1125
|
+
const newLayers = [];
|
|
1126
|
+
currentMapLayers.forEach((layerId) => {
|
|
1127
|
+
if (layerId !== "Background" && !this.state.layerStates[layerId]) {
|
|
1128
|
+
const layer = this.map.getLayer(layerId);
|
|
1129
|
+
if (layer) {
|
|
1130
|
+
newLayers.push(layerId);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
if (newLayers.length > 0) {
|
|
1135
|
+
newLayers.forEach((layerId) => {
|
|
1136
|
+
const layer = this.map.getLayer(layerId);
|
|
1137
|
+
if (!layer) return;
|
|
1138
|
+
const layerType = layer.type;
|
|
1139
|
+
const opacity = getLayerOpacity(this.map, layerId, layerType);
|
|
1140
|
+
const visibility = this.map.getLayoutProperty(layerId, "visibility");
|
|
1141
|
+
const isVisible = visibility !== "none";
|
|
1142
|
+
this.state.layerStates[layerId] = {
|
|
1143
|
+
visible: isVisible,
|
|
1144
|
+
opacity,
|
|
1145
|
+
name: layerId.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())
|
|
1146
|
+
};
|
|
1147
|
+
this.addLayerItem(layerId, this.state.layerStates[layerId]);
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
console.warn("Failed to check for new layers:", error);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
export {
|
|
1156
|
+
LayerControl,
|
|
1157
|
+
clamp,
|
|
1158
|
+
formatNumericValue,
|
|
1159
|
+
getLayerOpacity,
|
|
1160
|
+
getLayerType,
|
|
1161
|
+
isStyleableLayerType,
|
|
1162
|
+
normalizeColor,
|
|
1163
|
+
rgbToHex,
|
|
1164
|
+
setLayerOpacity
|
|
1165
|
+
};
|
|
1166
|
+
//# sourceMappingURL=index.mjs.map
|