@drecchia/maplibre-layerlibre 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -0
- package/dist/css/all.css +1 -0
- package/dist/js/all.min.js +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# maplibre-layerlibre
|
|
2
|
+
|
|
3
|
+
A compact layer-switcher control for [MapLibre GL JS](https://maplibre.org/) with [deck.gl](https://deck.gl/) overlay support.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Base map switching** — radio-button selector, `setStyle` strategy
|
|
13
|
+
- **deck.gl overlays** — static `deckLayers` or lazy-loaded via `onChecked` callback
|
|
14
|
+
- **Overlay groups** — group-level visibility toggle and opacity control
|
|
15
|
+
- **Per-overlay opacity sliders** and status indicators (loading / error / zoom-filtered)
|
|
16
|
+
- **Viewport targeting** — `fitBounds`, `center+zoom`, `bearing`, `pitch` applied on activation
|
|
17
|
+
- **Forced base layer** — overlay can require a specific base style before activating
|
|
18
|
+
- **State persistence** — base, overlays, opacity, viewport saved to `localStorage`
|
|
19
|
+
- **Zoom filtering** — overlays hidden automatically outside `minZoomLevel`/`maxZoomLevel`
|
|
20
|
+
- **Event-driven API** — all state changes emit typed events
|
|
21
|
+
- **Dark theme + responsive** — CSS media queries included
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Load MapLibre GL JS, deck.gl, and the LayersControl bundle from CDN, then build with `npm run build` to produce `dist/js/all.min.js` and `dist/css/all.css`.
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<link href="https://unpkg.com/maplibre-gl@4.1.1/dist/maplibre-gl.css" rel="stylesheet">
|
|
31
|
+
<link href="dist/css/all.css" rel="stylesheet">
|
|
32
|
+
|
|
33
|
+
<script src="https://unpkg.com/maplibre-gl@4.1.1/dist/maplibre-gl.js"></script>
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/deck.gl@9.1.14/dist.min.js"></script>
|
|
35
|
+
<script src="dist/js/all.min.js"></script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Build the bundle:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install
|
|
42
|
+
npm run build # → dist/js/all.min.js + dist/css/all.css
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
All classes (`EventEmitter`, `StateService`, `MapService`, `UIService`, `BusinessLogicService`, `LayersControl`, `BoundsHelper`) are globals exposed by the bundle.
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<div id="map"></div>
|
|
53
|
+
<script>
|
|
54
|
+
const baseStyles = [
|
|
55
|
+
{
|
|
56
|
+
id: 'osm',
|
|
57
|
+
label: 'OpenStreetMap',
|
|
58
|
+
style: 'https://demotiles.maplibre.org/style.json',
|
|
59
|
+
strategy: 'setStyle'
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const overlays = [
|
|
64
|
+
{
|
|
65
|
+
id: 'cities',
|
|
66
|
+
label: 'Major Cities',
|
|
67
|
+
deckLayers: [
|
|
68
|
+
{
|
|
69
|
+
id: 'cities-layer',
|
|
70
|
+
type: 'ScatterplotLayer',
|
|
71
|
+
props: {
|
|
72
|
+
data: [
|
|
73
|
+
{ position: [-74.0, 40.7], name: 'New York' },
|
|
74
|
+
{ position: [-87.6, 41.9], name: 'Chicago' },
|
|
75
|
+
{ position: [-118.2, 34.0], name: 'Los Angeles' }
|
|
76
|
+
],
|
|
77
|
+
getPosition: d => d.position,
|
|
78
|
+
getRadius: 20000,
|
|
79
|
+
getFillColor: [255, 100, 0],
|
|
80
|
+
pickable: true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
tooltip: 'name',
|
|
85
|
+
defaultVisible: true,
|
|
86
|
+
opacityControls: true
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// ── Instantiate services ───────────────────────────────────────────────────
|
|
91
|
+
const eventEmitter = new EventEmitter();
|
|
92
|
+
const stateService = new StateService(eventEmitter, 'my-app-layers'); // localStorage key
|
|
93
|
+
const mapService = new MapService(eventEmitter);
|
|
94
|
+
const uiService = new UIService(stateService, mapService, eventEmitter);
|
|
95
|
+
const businessLogicService = new BusinessLogicService(stateService, eventEmitter);
|
|
96
|
+
|
|
97
|
+
const layersControl = new LayersControl(
|
|
98
|
+
{ baseStyles, overlays, defaultBaseId: 'osm' },
|
|
99
|
+
{ stateService, uiService, mapService, businessLogicService, eventEmitter }
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// ── Create map and add control ─────────────────────────────────────────────
|
|
103
|
+
const map = new maplibregl.Map({
|
|
104
|
+
container: 'map',
|
|
105
|
+
style: baseStyles[0].style,
|
|
106
|
+
center: [-95, 40],
|
|
107
|
+
zoom: 3
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
map.addControl(layersControl, 'top-left');
|
|
111
|
+
|
|
112
|
+
// ── Subscribe to events ────────────────────────────────────────────────────
|
|
113
|
+
layersControl
|
|
114
|
+
.on('basechange', e => console.log('base →', e.id))
|
|
115
|
+
.on('overlaychange', e => console.log('overlay →', e.id, e.visible))
|
|
116
|
+
.on('error', e => console.error('error →', e.id, e.error));
|
|
117
|
+
</script>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Dynamic Overlays (`onChecked`)
|
|
123
|
+
|
|
124
|
+
Use `onChecked` to load data lazily — only when the user first activates the overlay:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
{
|
|
128
|
+
id: 'live-data',
|
|
129
|
+
label: 'Live Data',
|
|
130
|
+
onChecked: async (context) => {
|
|
131
|
+
if (context.getCache()) return; // skip if already loaded
|
|
132
|
+
|
|
133
|
+
const data = await fetch('/api/data').then(r => r.json());
|
|
134
|
+
|
|
135
|
+
context.setOverlayConfig({
|
|
136
|
+
deckLayers: [{
|
|
137
|
+
id: 'live-layer',
|
|
138
|
+
type: 'ScatterplotLayer',
|
|
139
|
+
props: { data, getPosition: d => d.position, getRadius: 5000, getFillColor: [0, 180, 255], pickable: true }
|
|
140
|
+
}]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
context.setCache({ loaded: true }); // prevent re-fetch
|
|
144
|
+
},
|
|
145
|
+
tooltip: 'name',
|
|
146
|
+
defaultVisible: false
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The `loading`, `success`, and `error` events fire automatically. An in-UI retry button appears on failure.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Viewport Targeting
|
|
155
|
+
|
|
156
|
+
Fit the map to specific bounds when an overlay is activated:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
{
|
|
160
|
+
id: 'usa-cities',
|
|
161
|
+
label: 'USA Cities',
|
|
162
|
+
viewport: {
|
|
163
|
+
fitBounds: BoundsHelper.calculateBounds(usaData.map(d => d.position))
|
|
164
|
+
},
|
|
165
|
+
deckLayers: [...]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Or pan to a specific location:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
viewport: { center: [-74.0, 40.7], zoom: 10, bearing: 45, pitch: 30 }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Overlay Groups
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const groups = [{ id: 'transport', label: 'Transport' }];
|
|
181
|
+
const overlays = [
|
|
182
|
+
{ id: 'roads', label: 'Roads', group: 'transport', deckLayers: [...] },
|
|
183
|
+
{ id: 'transit', label: 'Transit', group: 'transport', deckLayers: [...] }
|
|
184
|
+
];
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The group header shows an all-at-once visibility toggle and (optionally) a shared opacity slider.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Runtime API
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
// Base layers
|
|
195
|
+
layersControl.setBaseLayer('satellite');
|
|
196
|
+
layersControl.addBaseStyle({ id: 'terrain', label: 'Terrain', style: '...', strategy: 'setStyle' });
|
|
197
|
+
|
|
198
|
+
// Overlays
|
|
199
|
+
layersControl.addOverlay({ id: 'new', label: 'New Layer', deckLayers: [...] });
|
|
200
|
+
layersControl.showOverlay('my-overlay');
|
|
201
|
+
layersControl.hideOverlay('my-overlay');
|
|
202
|
+
layersControl.setOverlayOpacity('my-overlay', 0.5);
|
|
203
|
+
layersControl.removeOverlay('my-overlay');
|
|
204
|
+
|
|
205
|
+
// Groups
|
|
206
|
+
layersControl.showGroup('transport');
|
|
207
|
+
layersControl.setGroupOpacity('transport', 0.7);
|
|
208
|
+
|
|
209
|
+
// Persistence
|
|
210
|
+
layersControl.clearPersistedData();
|
|
211
|
+
|
|
212
|
+
// State
|
|
213
|
+
const state = layersControl.getCurrentState();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Configuration Options
|
|
219
|
+
|
|
220
|
+
| Option | Type | Default | Description |
|
|
221
|
+
|--------|------|---------|-------------|
|
|
222
|
+
| `baseStyles` | `Array` | required | Base map styles |
|
|
223
|
+
| `overlays` | `Array` | required | Overlay definitions |
|
|
224
|
+
| `groups` | `Array` | `[]` | Group metadata |
|
|
225
|
+
| `defaultBaseId` | `string` | `null` | Initial active base style |
|
|
226
|
+
| `showOpacity` | `boolean` | `true` | Show opacity sliders |
|
|
227
|
+
| `autoClose` | `boolean` | `false` | Close panel after base selection |
|
|
228
|
+
| `icon` | `string` | `'☰'` | Toggle button icon |
|
|
229
|
+
| `i18n` | `object` | see docs | UI string overrides `{ baseHeader, overlaysHeader }` |
|
|
230
|
+
|
|
231
|
+
**Persistence** is configured via `new StateService(eventEmitter, 'your-key')` — the second argument is the `localStorage` key.
|
|
232
|
+
|
|
233
|
+
**Control position** is set via `map.addControl(layersControl, 'top-left')` (standard MapLibre API).
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Events
|
|
238
|
+
|
|
239
|
+
| Event | Payload | When |
|
|
240
|
+
|-------|---------|------|
|
|
241
|
+
| `basechange` | `{ id }` | Active base style changed |
|
|
242
|
+
| `overlaychange` | `{ id, visible, opacity }` | Overlay visibility or opacity changed |
|
|
243
|
+
| `overlaygroupchange` | `{ id, visible }` | Group visibility changed |
|
|
244
|
+
| `loading` | `{ id }` | `onChecked` callback started |
|
|
245
|
+
| `success` | `{ id }` | `onChecked` completed |
|
|
246
|
+
| `error` | `{ id, error }` | Activation failed |
|
|
247
|
+
| `styleload` | `{ baseId }` | Base style finished loading |
|
|
248
|
+
| `zoomfilter` | `{ id, filtered }` | Overlay shown/hidden by zoom |
|
|
249
|
+
| `viewportchange` | `{ center, zoom, ... }` | Viewport saved |
|
|
250
|
+
| `memorycleared` | `{}` | localStorage cleared |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Documentation
|
|
255
|
+
|
|
256
|
+
| Doc | Contents |
|
|
257
|
+
|-----|----------|
|
|
258
|
+
| [docs/QUICKSTART.md](docs/QUICKSTART.md) | Setup guide and minimal examples |
|
|
259
|
+
| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Full options reference |
|
|
260
|
+
| [docs/API_REFERENCE.md](docs/API_REFERENCE.md) | All public methods and return values |
|
|
261
|
+
| [docs/ONECHECKED.md](docs/ONECHECKED.md) | `onChecked` dynamic overlay contract |
|
|
262
|
+
| [docs/EVENTS.md](docs/EVENTS.md) | Event payloads and subscription patterns |
|
|
263
|
+
| [docs/CSS.md](docs/CSS.md) | BEM class reference and customization |
|
|
264
|
+
| [docs/WORKFLOWS.md](docs/WORKFLOWS.md) | Runtime flows and lifecycle |
|
|
265
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Internal service design |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Browser Support
|
|
270
|
+
|
|
271
|
+
Any modern browser supporting ES2020+. No bundler required — the library is a concatenated, minified global script.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
[CC-BY-NC-4.0](https://creativecommons.org/licenses/by-nc/4.0/) — non-commercial use only.
|
package/dist/css/all.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.layers-control-container{background:#fff;border-radius:4px;box-shadow:0 0 0 2px rgba(0,0,0,.1);position:relative}.layers-control__toggle{background:0 0;border:none;cursor:pointer;padding:8px;font-size:16px;color:#333;display:flex;align-items:center;justify-content:center;width:29px;height:29px;border-radius:4px;transition:background-color .2s ease}.layers-control__toggle:hover{background-color:rgba(0,0,0,.05)}.layers-control__toggle:focus{outline:2px solid #007cba;outline-offset:-2px}.layers-control__panel{position:absolute;background:#fff;border-radius:4px;box-shadow:0 2px 10px rgba(0,0,0,.15);min-width:250px;max-width:300px;max-height:min(70dvh,400px);overflow-y:auto;overflow-x:hidden;z-index:1000;opacity:0;visibility:hidden;transform:translateY(-10px);transition:all .3s ease}.layers-control__panel--open{opacity:1;visibility:visible;transform:translateY(0)}.maplibregl-ctrl-top-left .layers-control__panel,.maplibregl-ctrl-top-right .layers-control__panel{top:100%;margin-top:4px}.maplibregl-ctrl-bottom-left .layers-control__panel,.maplibregl-ctrl-bottom-right .layers-control__panel{bottom:100%;margin-bottom:4px;transform:translateY(10px)}.maplibregl-ctrl-bottom-left .layers-control__panel--open,.maplibregl-ctrl-bottom-right .layers-control__panel--open{transform:translateY(0)}.maplibregl-ctrl-bottom-right .layers-control__panel,.maplibregl-ctrl-top-right .layers-control__panel{right:0}.maplibregl-ctrl-bottom-left .layers-control__panel,.maplibregl-ctrl-top-left .layers-control__panel{left:0}.layers-control__base-section,.layers-control__overlays-section{border-bottom:1px solid #f0f0f0}.layers-control__base-section:last-child,.layers-control__overlays-section:last-child{border-bottom:none}.layers-control__section-title{font-weight:600;font-size:12px;color:#9d9d9d;text-transform:uppercase;letter-spacing:.5px;padding:8px 12px 4px;margin:0;background:#f8f9fa;border-bottom:1px solid #e9ecef}.layers-control__base-list,.layers-control__overlays-list{padding:4px 0}.layers-control__base-item{display:flex;align-items:center;padding:6px 12px;cursor:pointer;font-size:13px;transition:background-color .2s ease;border:none;background:0 0;width:100%}.layers-control__base-item:hover{background-color:rgba(0,0,0,.05)}.layers-control__base-item--active{background-color:rgba(0,124,186,.1);color:#007cba;font-weight:500}.layers-control__base-item input[type=radio]{margin-right:8px;cursor:pointer;accent-color:#007cba}.layers-control__group{margin:4px 0}.layers-control__group-header{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background-color:rgba(0,0,0,.02);border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0}.layers-control__group-toggle{display:flex;align-items:center;cursor:pointer;font-size:13px;font-weight:500;flex:1}.layers-control__group-toggle input[type=checkbox]{margin-right:8px;cursor:pointer;accent-color:#007cba}.layers-control__group-overlays{background-color:rgba(0,0,0,.01)}.layers-control__overlay-item{padding:6px 12px;transition:background-color .2s ease;border-left:3px solid transparent}.layers-control__overlay-item:hover{background-color:rgba(0,0,0,.03)}.layers-control__overlay-item--loading{border-left-color:#007cba;background-color:rgba(0,124,186,.05)}.layers-control__overlay-toggle{display:flex;align-items:center;cursor:pointer;font-size:13px;width:100%;margin-bottom:4px}.layers-control__overlay-toggle input[type=checkbox]{margin-right:8px;cursor:pointer;accent-color:#007cba}.layers-control__overlay-toggle input[type=checkbox]:disabled{opacity:.5;cursor:not-allowed}.layers-control__label{flex:1;-webkit-user-select:none;-moz-user-select:none;user-select:none}.layers-control__loading{margin-left:8px;font-size:12px;color:#007cba}.layers-control__loading.loadingRotate{animation:layers-control-spin 1s linear infinite}@keyframes layers-control-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.layers-control__opacity-control{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-left:20px;padding-top:5px}.layers-control__opacity-label{font-size:11px;color:#666;font-weight:500;min-width:35px;text-align:right}.layers-control__opacity-slider{flex:1;max-width:100px;height:4px;background:#ddd;border-radius:2px;outline:0;cursor:pointer;margin-left:20px;transition:background .2s ease;-webkit-appearance:none;-moz-appearance:none;appearance:none}.layers-control__opacity-slider:hover{background:#bbb}.layers-control__opacity-slider:disabled{opacity:.5;cursor:not-allowed}.layers-control__opacity-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#007cba;cursor:pointer;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.3);-webkit-transition:all .2s ease;transition:all .2s ease}.layers-control__opacity-slider::-webkit-slider-thumb:hover{background:#005a8a;transform:scale(1.1)}.layers-control__opacity-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#007cba;cursor:pointer;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.3);-moz-transition:all .2s ease;transition:all .2s ease}.layers-control__opacity-slider::-moz-range-thumb:hover{background:#005a8a;transform:scale(1.1)}.layers-control__error{margin-top:4px;padding:6px 8px;background-color:#fff5f5;border:1px solid #fed7d7;border-radius:3px;font-size:11px}.layers-control__error-message{color:#c53030;display:block;margin-bottom:4px;line-height:1.3}.layers-control__retry-button{background:#e53e3e;color:#fff;border:none;border-radius:2px;padding:2px 6px;font-size:10px;cursor:pointer;transition:background-color .2s ease}.layers-control__retry-button:hover{background:#c53030}@media (max-width:480px){.layers-control__panel{min-width:180px;max-width:250px;max-height:60dvh}.layers-control__section-title{font-size:11px;padding:6px 10px 3px}.layers-control__base-item,.layers-control__group-toggle,.layers-control__overlay-toggle{font-size:12px;padding:5px 10px}.layers-control__opacity-slider{max-width:100px;margin-left:16px}}.layers-control__base-item:focus,.layers-control__group-toggle:focus,.layers-control__overlay-toggle:focus,.layers-control__toggle:focus{outline:2px solid #007cba;outline-offset:-2px}.layers-control__opacity-slider:focus{outline:2px solid #007cba;outline-offset:2px}@media (prefers-contrast:high){.layers-control__panel{border:2px solid #000}.layers-control__section-title{border-bottom:2px solid #000}.layers-control__base-item--active{border:2px solid #007cba}}@media (prefers-reduced-motion:reduce){.layers-control__base-item,.layers-control__opacity-slider::-moz-range-thumb,.layers-control__opacity-slider::-webkit-slider-thumb,.layers-control__overlay-item,.layers-control__panel,.layers-control__retry-button,.layers-control__toggle{-webkit-transition:none;-moz-transition:none;transition:none}.layers-control__loading{animation:none}}@media (prefers-color-scheme:dark){.layers-control-container,.layers-control__panel{background:#2d3748;color:#e2e8f0;box-shadow:0 0 0 2px rgba(255,255,255,.1)}.layers-control__panel{box-shadow:0 2px 10px rgba(0,0,0,.5)}.layers-control__toggle{color:#e2e8f0}.layers-control__toggle:hover{background-color:rgba(255,255,255,.1)}.layers-control__section-title{background:#4a5568;color:#a0aec0;border-bottom-color:#2d3748}.layers-control__base-item:hover,.layers-control__overlay-item:hover{background-color:rgba(255,255,255,.1)}.layers-control__base-item--active{background-color:rgba(0,124,186,.3);color:#63b3ed}.layers-control__group-header{background-color:rgba(255,255,255,.05);border-color:#4a5568}.layers-control__group-overlays{background-color:rgba(255,255,255,.02)}.layers-control__overlay-item--loading{background-color:rgba(0,124,186,.2)}.layers-control__opacity-slider{background:#4a5568}.layers-control__opacity-slider:hover{background:#718096}.layers-control__error{background-color:rgba(254,178,178,.1);border-color:rgba(254,215,215,.3)}}@media print{.layers-control-container,.layers-control__panel{display:none}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class BoundsHelper{static calculateBounds(e,t=0){if(!e||!Array.isArray(e)||0===e.length)throw new Error("Points array is required and must not be empty");let i=1/0,s=1/0,a=-1/0,r=-1/0;for(var[o,n]of e)i=Math.min(i,o),a=Math.max(a,o),s=Math.min(s,n),r=Math.max(r,n);let l,h,c,v;return"number"==typeof t?l=h=c=v=t:"object"==typeof t&&null!==t?(l=t.top||0,h=t.bottom||0,c=t.left||0,v=t.right||0):l=h=c=v=0,[[i-c,s-h],[a+v,r+l]]}static calculateBoundsCenter(e){var t,i;if(e&&Array.isArray(e)&&2===e.length)return t=e[0][0],i=e[0][1],[(t+e[1][0])/2,(i+e[1][1])/2];throw new Error("Bounds must be an array with two coordinate pairs")}static calculateBoundsZoom(e,t,i=.5){if(!e||!Array.isArray(e)||2!==e.length)throw new Error("Bounds must be an array with two coordinate pairs");var s,a,r;if(t&&"number"==typeof t.width&&"number"==typeof t.height)return r=e[0][0],s=e[0][1],a=e[1][0],e=e[1][1]-s,s=Math.log2(360*t.width/(512*(a-r))),a=Math.log2(180*t.height/(512*e)),r=Math.min(s,a)-i,Math.max(1,Math.min(20,r));throw new Error("Container must have numeric width and height properties")}static calculatePanCenter(t){try{if(t.deckLayers&&0<t.deckLayers.length){var e=t.deckLayers[0];if(e.props&&e.props.data&&0<e.props.data.length){var i=e.props.data[0];if(i.position)return i.position}}}catch(e){console.error(`Failed to calculate pan center for overlay ${t.id}:`,e)}return null}}class EventEmitter{constructor(){this._events=new Map}on(e,t){if("function"!=typeof t)throw new TypeError("Handler must be a function");return this._events.has(e)||this._events.set(e,new Set),this._events.get(e).add(t),this}off(e,t){var i=this._events.get(e);return i&&(i.delete(t),0===i.size)&&this._events.delete(e),this}emit(t,e){var i=this._events.get(t);if(i)for(var s of Array.from(i))try{s(e)}catch(e){console.error(`EventEmitter: error in "${t}" handler:`,e)}}once(t,i){let s=e=>{this.off(t,s),i(e)};return this.on(t,s)}removeAllListeners(e){return e?this._events.delete(e):this._events.clear(),this}}class StateService{constructor(e,t){this.eventEmitter=e,this.persistenceKey=t||null,this._debounceTimer=null,this._state={base:null,overlays:{},groups:{},layerOrder:[],viewport:{center:null,zoom:null,bearing:0,pitch:0}},this._loadPersisted()}get(e){return this._state[e]}getCurrentBase(){return this._state.base}getOverlayStates(){return this._state.overlays}getGroupStates(){return this._state.groups}getViewport(){return this._state.viewport||{}}getAll(){return JSON.parse(JSON.stringify(this._state))}initOverlay(e,t){this._state.overlays[e]||(this._state.overlays[e]={visible:!0===t.defaultVisible,opacity:void 0!==t.defaultOpacity?t.defaultOpacity:1})}setBase(e){var t=this._state.base;this._state.base=e,this._schedulePersist(),t!==e&&(this.eventEmitter.emit("basechange",{id:e}),this.eventEmitter.emit("change",{type:"basechange",id:e}))}setOverlayVisibility(e,t){this._state.overlays[e]||(this._state.overlays[e]={visible:!1,opacity:1}),this._state.overlays[e].visible=t,this._schedulePersist()}setOverlayOpacity(e,t){this._state.overlays[e]||(this._state.overlays[e]={visible:!1,opacity:1}),this._state.overlays[e].opacity=Math.max(0,Math.min(1,parseFloat(t)||0)),this._schedulePersist()}setGroupVisibility(e,t){this._state.groups[e]||(this._state.groups[e]={visible:!1,opacity:1}),this._state.groups[e].visible=t,this._schedulePersist()}setGroupOpacity(e,t){this._state.groups[e]||(this._state.groups[e]={visible:!1,opacity:1}),this._state.groups[e].opacity=Math.max(0,Math.min(1,parseFloat(t)||0)),this._schedulePersist()}setViewport(e){this._state.viewport={...this._state.viewport||{},...e},this._schedulePersist(),this.eventEmitter.emit("viewportchange",{...this._state.viewport}),this.eventEmitter.emit("change",{type:"viewportchange",...this._state.viewport})}reorderLayers(e){this._state.layerOrder=[...e],this._schedulePersist()}removeOverlay(e){delete this._state.overlays[e];e=this._state.layerOrder.indexOf(e);-1<e&&this._state.layerOrder.splice(e,1),this._schedulePersist()}clearPersisted(){if(!this.persistenceKey)return!1;try{return localStorage.removeItem(this.persistenceKey),this.eventEmitter.emit("memorycleared",{}),this.eventEmitter.emit("change",{type:"memorycleared"}),!0}catch(e){return console.error("StateService: failed to clear persisted state:",e),!1}}destroy(){this._debounceTimer&&(clearTimeout(this._debounceTimer),this._debounceTimer=null)}_schedulePersist(){this.persistenceKey&&(clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(()=>this._persist(),300))}_persist(){if(this.persistenceKey)try{localStorage.setItem(this.persistenceKey,JSON.stringify(this._state))}catch(e){console.warn("StateService: failed to persist state:",e)}}_loadPersisted(){if(this.persistenceKey)try{var e,t=localStorage.getItem(this.persistenceKey);t&&(void 0!==(e=JSON.parse(t)).base&&(this._state.base=e.base),e.overlays&&(this._state.overlays={...e.overlays}),e.groups&&(this._state.groups={...e.groups}),Array.isArray(e.layerOrder)&&(this._state.layerOrder=e.layerOrder),e.viewport)&&(this._state.viewport={...this._state.viewport,...e.viewport})}catch(e){console.warn("StateService: failed to load persisted state:",e)}}}class MapService{constructor(e){this.eventEmitter=e,this.map=null}setMap(e){this.map=e}getMap(){return this.map}getCurrentViewport(){var e;return this.map?{center:{lng:(e=this.map.getCenter()).lng,lat:e.lat},zoom:this.map.getZoom(),bearing:this.map.getBearing(),pitch:this.map.getPitch()}:null}destroy(){this.map=null}}class UIManager{constructor(e,t,i){if(!e)throw new Error("UIManager requires stateService");if(!i)throw new Error("UIManager requires eventEmitter");this.stateService=e,this.mapService=t||null,this.eventEmitter=i,this.businessLogicService=null,this.options={},this.map=null,this.container=null,this.deckOverlay=null,this.toggle=null,this.panel=null,this.isOpen=!1,this.deckLayers=new Map,this.overlayToLayerIds=new Map,this.loadingStates=new Map,this.errorStates=new Map,this.zoomFilteredOverlays=new Set,this.overlayCache=new Map,this._handleToggleClick=this._handleToggleClick.bind(this),this._handleDocumentClick=this._handleDocumentClick.bind(this),this._onZoomEnd=this._onZoomEnd.bind(this)}setOptions(e){this.options=e||{}}setBusinessLogicService(e){this.businessLogicService=e}setMap(e){this.map=e,this.mapService&&this.mapService.setMap(e),e&&(this._initializeDeckOverlay(),e.on("zoomend",this._onZoomEnd))}setContainer(e){this.container=e}render(){this.container&&(this.container.innerHTML="",this.toggle=document.createElement("button"),this.toggle.className="layers-control__toggle",this.toggle.setAttribute("aria-label","Toggle layers"),this.toggle.textContent=this.options.icon||"☰",this.toggle.addEventListener("click",this._handleToggleClick),this.panel=document.createElement("div"),this.panel.className="layers-control__panel",this.panel.style.display="none",this.container.appendChild(this.toggle),this.container.appendChild(this.panel),this._renderPanelContent(),this._setupEventDelegation())}destroy(){if(this.map&&this.map.off("zoomend",this._onZoomEnd),this.options&&this.options.autoClose&&document.removeEventListener("click",this._handleDocumentClick),this.deckOverlay&&this.map)try{this.map.removeControl(this.deckOverlay)}catch(e){}this.deckLayers.clear(),this.overlayToLayerIds.clear(),this.loadingStates.clear(),this.errorStates.clear(),this.zoomFilteredOverlays.clear(),this.overlayCache.clear(),this.map=null,this.container=null,this.deckOverlay=null,this.panel=null,this.toggle=null}updateOverlays(){this._renderPanelContent()}updateBaseStyles(){this._renderPanelContent()}updateBaseUI(){let s=this.stateService.getCurrentBase();this.panel&&this.panel.querySelectorAll(".layers-control__base-item").forEach(e=>{var t,i=e.querySelector('input[type="radio"]');i&&(t=i.value===s,e.classList.toggle("layers-control__base-item--active",i.checked=t))})}updateOverlayUI(e){this._updateOverlayUI(e)}updateGroupUI(e){this._updateGroupUI(e)}setLoadingState(e,t){this._setLoadingState(e,t)}setErrorState(e,t){t?this.errorStates.set(e,t):this.errorStates.delete(e),this._updateOverlayUI(e)}async activateOverlay(e,t=!1){return this._activateOverlay(e,t)}deactivateOverlay(e){this._deactivateOverlay(e),this._updateOverlayUI(e)}updateOverlayOpacity(e,t){var i;this._updateOverlayOpacity(e,t),this.panel&&(e=this.panel.querySelector(`[data-overlay-id="${e}"]`))&&(i=e.querySelector(".layers-control__opacity-slider"),e=e.querySelector(".layers-control__opacity-label"),i&&(i.value=t),e)&&(e.textContent=Math.round(100*t)+"%")}clearAll(){this.deckLayers.clear(),this.overlayToLayerIds.clear(),this._updateDeckOverlay(),this.loadingStates.clear(),this.errorStates.clear(),this.zoomFilteredOverlays.clear(),this.overlayCache.clear()}applyBaseStyle(e){this._applyBaseToMap(e)}_renderPanelContent(){this.panel&&(this.panel.innerHTML="",this.options.baseStyles&&0<this.options.baseStyles.length&&this.panel.appendChild(this._createBaseSection()),this.options.overlays)&&0<this.options.overlays.length&&this.panel.appendChild(this._createOverlaysSection())}_createBaseSection(){var e=document.createElement("div"),t=(e.className="layers-control__base-section",document.createElement("h3"));t.className="layers-control__section-title",t.textContent=this.options.i18n&&this.options.i18n.baseHeader?this.options.i18n.baseHeader:"Base Layers";let a=document.createElement("div"),r=(a.className="layers-control__base-list",this.stateService.getCurrentBase());return(this.options.baseStyles||[]).forEach(e=>{var t=document.createElement("label");t.className="layers-control__base-item",e.id===r&&t.classList.add("layers-control__base-item--active");let i=document.createElement("input");i.type="radio",i.name="base-layer",i.value=e.id,i.checked=e.id===r,i.addEventListener("change",()=>{i.checked&&this._handleBaseChange(e.id)});var s=document.createElement("span");s.textContent=e.label||e.id,t.appendChild(i),t.appendChild(s),a.appendChild(t)}),e.appendChild(t),e.appendChild(a),e}_createOverlaysSection(){var e=document.createElement("div"),t=(e.className="layers-control__overlays-section",document.createElement("h3"));t.className="layers-control__section-title",t.textContent=this.options.i18n&&this.options.i18n.overlaysHeader?this.options.i18n.overlaysHeader:"Overlays";let i=document.createElement("div"),s=(i.className="layers-control__overlays-list",new Map),a=[];return(this.options.overlays||[]).forEach(e=>{(e.group?(s.has(e.group)||s.set(e.group,[]),s.get(e.group)):a).push(e)}),s.forEach((e,t)=>{i.appendChild(this._createGroupElement(t,e))}),a.forEach(e=>{i.appendChild(this._createOverlayElement(e))}),e.appendChild(t),e.appendChild(i),e}_createGroupElement(t,e){var i=document.createElement("div"),s=(i.className="layers-control__group",document.createElement("div")),a=(s.className="layers-control__group-header",document.createElement("label")),r=(a.className="layers-control__group-toggle",document.createElement("input")),o=(r.type="checkbox",r.value=t,this.stateService.getGroupStates()[t]),o=(r.checked=!!o&&o.visible,r.addEventListener("change",()=>this._handleToggleGroup(t)),(this.options.groups||[]).find(e=>e.id===t)),n=document.createElement("span");n.textContent=o&&o.label?o.label:t,a.appendChild(r),a.appendChild(n),s.appendChild(a);let l=document.createElement("div");return l.className="layers-control__group-overlays",e.forEach(e=>{l.appendChild(this._createOverlayElement(e))}),i.appendChild(s),i.appendChild(l),i}_createOverlayElement(e){var t=document.createElement("div"),i=(t.className="layers-control__overlay-item",t.dataset.overlayId=e.id,document.createElement("label")),s=(i.className="layers-control__overlay-toggle",document.createElement("input")),a=(s.type="checkbox",s.value=e.id,this.stateService.getOverlayStates()[e.id]),r=(s.checked=!!a&&a.visible,s.addEventListener("change",()=>this._handleToggleOverlay(e.id)),document.createElement("span")),o=(r.className="layers-control__label",r.textContent=e.label||e.id,document.createElement("span"));return o.className="layers-control__loading",o.style.display="none",i.appendChild(s),i.appendChild(r),i.appendChild(o),t.appendChild(i),!1!==this.options.showOpacity&&e.opacityControls&&(s=a?a.opacity:1,t.appendChild(this._createOpacitySlider(e.id,s))),t}_createOpacitySlider(t,e){var i=document.createElement("div"),s=(i.className="layers-control__opacity-control",document.createElement("input"));s.type="range",s.className="layers-control__opacity-slider",s.min="0",s.max="1",s.step="0.01",s.value=e;let a=document.createElement("span");return a.className="layers-control__opacity-label",a.textContent=Math.round(100*e)+"%",s.addEventListener("input",e=>{e=parseFloat(e.target.value);a.textContent=Math.round(100*e)+"%",this._handleOpacitySlider(t,e,!1)}),i.appendChild(s),i.appendChild(a),i}_setupEventDelegation(){this.options.autoClose&&document.addEventListener("click",this._handleDocumentClick)}_handleToggleClick(e){e.stopPropagation(),this.isOpen=!this.isOpen,this.panel&&(this.panel.style.display=this.isOpen?"block":"none",this.panel.classList.toggle("layers-control__panel--open",this.isOpen))}_handleDocumentClick(e){this.container&&!this.container.contains(e.target)&&(this.isOpen=!1,this.panel)&&(this.panel.style.display="none",this.panel.classList.remove("layers-control__panel--open"))}_handleBaseChange(e){this.businessLogicService&&this.businessLogicService.setBaseLayer(e)}_handleToggleOverlay(e){var t;this.businessLogicService&&(!((t=this.stateService.getOverlayStates()[e])&&t.visible)?this.businessLogicService.showOverlay(e,!0):this.businessLogicService.hideOverlay(e,!0))}_handleToggleGroup(e){var t;this.businessLogicService&&(t=!((t=this.stateService.getGroupStates()[e])&&t.visible),this.businessLogicService.setGroupVisibility(e,t))}_handleOpacitySlider(e,t,i){this.businessLogicService&&(i?this.businessLogicService.setGroupOpacity(e,t):this.businessLogicService.setOverlayOpacity(e,t))}_onZoomEnd(){this.updateAllZoomFiltering()}updateAllZoomFiltering(){if(this.map){let a=this.stateService.getOverlayStates();Object.keys(a).forEach(t=>{var e,i,s=a[t];s&&s.visible&&(s=(this.options.overlays||[]).find(e=>e.id===t))&&(e=this._checkZoomConstraints(s),i=this.zoomFilteredOverlays.has(t),e&&i?(this.zoomFilteredOverlays.delete(t),this._showOverlayLayers(s),this._updateOverlayUI(t),this.eventEmitter.emit("zoomfilter",{id:t,filtered:!1})):e||i||(this.zoomFilteredOverlays.add(t),this._hideOverlayLayers(s),this._updateOverlayUI(t),this.eventEmitter.emit("zoomfilter",{id:t,filtered:!0})))})}}_checkZoomConstraints(e){var t;return!(this.map&&(t=this.map.getZoom(),void 0!==(e=this._getFilterConfig(e)).minZoom&&t<e.minZoom||void 0!==e.maxZoom&&t>=e.maxZoom))}_getFilterConfig(e){var t=e.filter||{};return{minZoom:void 0!==t.minZoom?t.minZoom:e.minZoomLevel,maxZoom:void 0!==t.maxZoom?t.maxZoom:e.maxZoomLevel}}_updateZoomFiltering(t){var e=(this.options.overlays||[]).find(e=>e.id===t);return!e||((e=this._checkZoomConstraints(e))?this.zoomFilteredOverlays.delete(t):this.zoomFilteredOverlays.add(t),e)}_getViewportConfig(e){var t=e.viewport||{};return e.viewport||e.fitBounds||e.forcedCenter||void 0!==e.panZoom||void 0!==e.forcedBearing||void 0!==e.forcedPitch?{fitBounds:t.fitBounds||e.fitBounds,center:t.center||e.forcedCenter,zoom:void 0!==t.zoom?t.zoom:e.panZoom,bearing:void 0!==t.bearing?t.bearing:e.forcedBearing,pitch:void 0!==t.pitch?t.pitch:e.forcedPitch}:null}_applyViewportConfig(e){if(this.map&&e){var t={};if(e.fitBounds)try{var i=BoundsHelper.calculateBoundsCenter(e.fitBounds),s=this.map.getContainer(),a=BoundsHelper.calculateBoundsZoom(e.fitBounds,{width:s.offsetWidth,height:s.offsetHeight});t.center=i,t.zoom=a}catch(e){console.warn("UIManager: fitBounds calculation failed:",e)}else void 0!==e.center&&(t.center=e.center),void 0!==e.zoom&&(t.zoom=e.zoom);void 0!==e.bearing&&(t.bearing=e.bearing),void 0!==e.pitch&&(t.pitch=e.pitch),0<Object.keys(t).length&&(t.duration=1e3,this.map.flyTo(t))}}_applyBaseToMap(t){if(this.map){var e=(this.options.baseStyles||[]).find(e=>e.id===t);if(e&&e.style){if(this.deckOverlay){try{this.map.removeControl(this.deckOverlay)}catch(e){}this.deckOverlay=null}this.deckLayers.clear(),this.overlayToLayerIds.clear(),this.map.setStyle(e.style),this.map.once("styledata",()=>{setTimeout(()=>{this._initializeDeckOverlay();let i=this.stateService.getOverlayStates();Object.keys(i).forEach(e=>{var t=i[e];t&&t.visible&&this._activateOverlay(e,!1)}),this.eventEmitter.emit("styleload",{baseId:t}),this.eventEmitter.emit("change",{type:"styleload",baseId:t})},50)})}}}_initializeDeckOverlay(){this.map&&!this.deckOverlay&&("undefined"==typeof deck?console.warn("UIManager: deck.gl not found on window.deck"):(this.deckOverlay=new deck.MapboxOverlay({interleaved:!0,pickingRadius:10,controller:!1,getTooltip:this._getTooltip.bind(this)}),this.map.addControl(this.deckOverlay)))}async _activateOverlay(t,i=!1){if(this.map&&(this.deckOverlay||this._initializeDeckOverlay(),this.deckOverlay)){let e=(this.options.overlays||[]).find(e=>e.id===t);if(e){this._setLoadingState(t,!0);try{var s,a;if(i&&e.forcedBaseLayerId)if(this.stateService.getCurrentBase()!==e.forcedBaseLayerId)return(s=this._getViewportConfig(e))&&this._applyViewportConfig(s),this.businessLogicService&&this.businessLogicService.setBaseLayer(e.forcedBaseLayerId),void this._setLoadingState(t,!1);i&&(a=this._getViewportConfig(e))&&this._applyViewportConfig(a),this._updateZoomFiltering(t)?(e.onChecked&&(await this._executeOnChecked(t,e,i),e=(this.options.overlays||[]).find(e=>e.id===t)),e&&e.deckLayers&&this._createAndStoreDeckLayers(t,e),this._setLoadingState(t,!1),this.errorStates.delete(t),this._updateOverlayUI(t)):(this._setLoadingState(t,!1),this._updateOverlayUI(t),this.eventEmitter.emit("zoomfilter",{id:t,filtered:!0}))}catch(e){console.error(`UIManager: error activating overlay "${t}":`,e),this._setLoadingState(t,!1),this.errorStates.set(t,e.message||String(e)),this._updateOverlayUI(t),this.eventEmitter.emit("error",{id:t,error:e.message||String(e)})}}}}async _executeOnChecked(a,e,t){this.eventEmitter.emit("loading",{id:a});t={map:this.map,overlayManager:this,stateManager:this.stateService,stateService:this.stateService,overlayId:a,overlay:e,isUserInteraction:t,deckOverlay:this.deckOverlay,getCurrentViewport:()=>{var e=this.map.getCenter();return{center:[e.lng,e.lat],zoom:this.map.getZoom(),bearing:this.map.getBearing(),pitch:this.map.getPitch()}},getOverlayState:e=>this.stateService.getOverlayStates()[e],getAllOverlayStates:()=>this.stateService.getOverlayStates(),getOverlayConfig:()=>(this.options.overlays||[]).find(e=>e.id===a),setOverlayConfig:(e,t={})=>{var i,s=(this.options.overlays||[]).findIndex(e=>e.id===a);-1!==s&&(Object.assign(this.options.overlays[s],e),t.applyViewport&&(t=this.options.overlays[s],s=this._getViewportConfig(t))&&this._applyViewportConfig(s),e.label)&&this.panel&&(i=this.panel.querySelector(`[data-overlay-id="${a}"] .layers-control__label`))&&(i.textContent=e.label)},getCache:()=>this.overlayCache.get(a),setCache:e=>this.overlayCache.set(a,e),clearCache:()=>this.overlayCache.delete(a)};try{await Promise.resolve(e.onChecked(t)),this.eventEmitter.emit("success",{id:a})}catch(e){throw this.eventEmitter.emit("error",{id:a,error:e.message||String(e)}),e}}_deactivateOverlay(e){var t=this.overlayToLayerIds.get(e);t&&(t.forEach(e=>this.deckLayers.delete(e)),this.overlayToLayerIds.delete(e)),this.zoomFilteredOverlays.delete(e),this._setLoadingState(e,!1),this._updateDeckOverlay()}_createAndStoreDeckLayers(e,t){(this.overlayToLayerIds.get(e)||[]).forEach(e=>this.deckLayers.delete(e));var i=this.stateService.getOverlayStates()[e];let s=i?i.opacity:1,a=[];t.deckLayers.forEach(e=>{var t=this._createDeckLayer(e,s);t&&(this.deckLayers.set(e.id,t),a.push(e.id))}),this.overlayToLayerIds.set(e,a),this._updateDeckOverlay()}_createDeckLayer(t,e){if("undefined"==typeof deck)return null;try{var i=deck[t.type];return i?new i({id:t.id,opacity:void 0!==e?e:1,...t.props}):(console.error(`UIManager: unknown deck.gl layer type "${t.type}"`),null)}catch(e){return console.error(`UIManager: failed to create deck.gl layer "${t.id}":`,e),null}}_showOverlayLayers(e){if(e&&e.deckLayers){var t=this.stateService.getOverlayStates()[e.id];let i=t?t.opacity:1;e.deckLayers.forEach(e=>{var t=this._createDeckLayer(e,i);t&&this.deckLayers.set(e.id,t)}),this._updateDeckOverlay()}}_hideOverlayLayers(e){e&&e.deckLayers&&(e.deckLayers.forEach(e=>this.deckLayers.delete(e.id)),this._updateDeckOverlay())}_updateOverlayOpacity(e,i){e=this.overlayToLayerIds.get(e);e&&(e.forEach(e=>{var t=this.deckLayers.get(e);t&&(t=t.clone({opacity:i}),this.deckLayers.set(e,t))}),this._updateDeckOverlay())}_updateDeckOverlay(){this.deckOverlay&&this.deckOverlay.setProps({layers:Array.from(this.deckLayers.values())})}_setLoadingState(e,t){this.loadingStates.set(e,t),this._updateOverlayUI(e)}_updateOverlayUI(t){if(this.panel){var i=this.panel.querySelector(`[data-overlay-id="${t}"]`);if(i){var s=this.stateService.getOverlayStates()[t],a=i.querySelector('input[type="checkbox"]'),a=(a&&(a.checked=!!s&&s.visible),this.loadingStates.get(t)),s=this.errorStates.has(t),r=this.zoomFilteredOverlays.has(t);let e="ok";a?e="loading":s?e="error":r&&(e="zoomfiltered");var o=i.querySelector(".layers-control__loading");if(o)switch(e){case"loading":o.textContent="↻",o.style.display="inline",o.classList.add("loadingRotate"),o.title="Loading...";break;case"error":o.textContent="🚨",o.style.display="inline",o.classList.remove("loadingRotate"),o.title=this.errorStates.get(t)||"Error loading overlay";break;case"zoomfiltered":o.textContent="🔍",o.style.display="inline",o.classList.remove("loadingRotate"),o.title="Hidden due to zoom level";break;default:o.style.display="none",o.classList.remove("loadingRotate"),o.title=""}i.classList.toggle("layers-control__overlay-item--loading","loading"===e),i.classList.toggle("layers-control__overlay-item--error","error"===e),i.classList.toggle("layers-control__overlay-item--filtered","zoomfiltered"===e)}}}_updateGroupUI(e){var t;this.panel&&(t=this.panel.querySelector(`.layers-control__group-toggle input[value="${e}"]`))&&(e=this.stateService.getGroupStates()[e],t.checked=!!e&&e.visible)}_getTooltip(e){if(!e||!e.object||!e.layer)return null;var t=e.layer.id,i=e.object,t=this._findOverlayByLayerId(t);if(!t)return null;if(t.getTooltip&&"function"==typeof t.getTooltip){try{var s=t.getTooltip(i,e);if(s)return{html:s.html||this._formatDefaultTooltip(s),style:s.style||this._defaultTooltipStyle()}}catch(e){console.error("UIManager: error in getTooltip:",e)}return null}return t.tooltip?{html:this._formatTooltipFromConfig(t.tooltip,i),style:this._defaultTooltipStyle()}:null}_findOverlayByLayerId(e){for(var t of this.options.overlays||[])if(t.deckLayers)for(var i of t.deckLayers)if(i.id===e)return t;return null}_formatTooltipFromConfig(e,i){var t;if("string"==typeof e)return`<div class="tooltip-content">${""!==(t=this._getNestedValue(i,e))?t:"No data"}</div>`;if("object"!=typeof e||null===e)return'<div class="tooltip-content">No data</div>';{let t='<div class="tooltip-content">';return e.title&&(t+=`<div class="tooltip-title">${this._getNestedValue(i,e.title)}</div>`),e.fields&&Array.isArray(e.fields)&&(t+='<div class="tooltip-body"><div class="tooltip-fields">',e.fields.forEach(e=>{"string"==typeof e?t+=`<div class="tooltip-field"><strong>${e}:</strong> ${this._getNestedValue(i,e)}</div>`:e.label&&e.property&&(t+=`<div class="tooltip-field"><strong>${e.label}:</strong> ${this._getNestedValue(i,e.property)}</div>`)}),t+="</div></div>"),t+="</div>"}}_formatDefaultTooltip(t){if("string"==typeof t)return`<div class="tooltip-content">${t}</div>`;if(t&&(t.title||t.content)){let e='<div class="tooltip-content">';return t.title&&(e+=`<div class="tooltip-title">${t.title}</div>`),t.content&&(e+=`<div class="tooltip-body">${t.content}</div>`),e+="</div>"}return`<div class="tooltip-content">${JSON.stringify(t)}</div>`}_getNestedValue(e,t){if(!t||null==e)return"";var i;let s=e;for(i of t.split(".")){if(null==s||"object"!=typeof s)return"";s=s[i]}return null!=s?String(s):""}_defaultTooltipStyle(){return{backgroundColor:"rgba(0,0,0,0.8)",color:"white",padding:"8px 12px",borderRadius:"4px",fontSize:"12px",fontFamily:"Arial, sans-serif",maxWidth:"300px",boxShadow:"0 2px 8px rgba(0,0,0,0.3)",pointerEvents:"none",zIndex:1e3}}_updateOverlayLayers(t){var e=(this.options.overlays||[]).find(e=>e.id===t);e&&(e.deckLayers?this._createAndStoreDeckLayers(t,e):((this.overlayToLayerIds.get(t)||[]).forEach(e=>this.deckLayers.delete(e)),this.overlayToLayerIds.delete(t),this._updateDeckOverlay()))}}let UIService=UIManager;class BusinessLogicService{constructor(e,t){if(!e)throw new Error("BusinessLogicService requires stateService");if(!t)throw new Error("BusinessLogicService requires eventEmitter");this.stateService=e,this.eventEmitter=t,this.map=null,this.uiManager=null,this.mapService=null,this.options=null}initialize(e){this.map=e.map,this.uiManager=e.uiService,this.mapService=e.mapService,e.options&&(this.options=e.options)}setBaseLayer(t){return(this.options.baseStyles||[]).find(e=>e.id===t)?(this.stateService.setBase(t),this.uiManager&&(this.uiManager.applyBaseStyle(t),this.uiManager.updateBaseUI()),!0):(console.warn(`BusinessLogicService: base style "${t}" not found`),!1)}updateBaseStyles(e){this.options&&(this.options.baseStyles=e),this.uiManager&&this.uiManager.updateBaseStyles()}async showOverlay(e,t=!1){var i=this.stateService.getOverlayStates();return i[e]&&i[e].visible||(this.stateService.setOverlayVisibility(e,!0),this.uiManager&&await this.uiManager.activateOverlay(e,t),t&&(t=(i=this.stateService.getOverlayStates()[e])?i.opacity:1,this.eventEmitter.emit("overlaychange",{id:e,visible:!0,opacity:t}),this.eventEmitter.emit("change",{type:"overlaychange",id:e,visible:!0,opacity:t}))),!0}hideOverlay(e,t=!1){var i=this.stateService.getOverlayStates();return i[e]&&i[e].visible&&(this.stateService.setOverlayVisibility(e,!1),this.uiManager&&this.uiManager.deactivateOverlay(e),t)&&(t=(i=this.stateService.getOverlayStates()[e])?i.opacity:1,this.eventEmitter.emit("overlaychange",{id:e,visible:!1,opacity:t}),this.eventEmitter.emit("change",{type:"overlaychange",id:e,visible:!1,opacity:t})),!0}async activateOverlay(e,t=!1){this.uiManager&&await this.uiManager.activateOverlay(e,t)}setOverlayOpacity(e,t){var t=Math.max(0,Math.min(1,parseFloat(t)||0)),i=(this.stateService.setOverlayOpacity(e,t),this.stateService.getOverlayStates()[e]);return this.uiManager&&i&&i.visible&&this.uiManager.updateOverlayOpacity(e,t),this.eventEmitter.emit("overlaychange",{id:e,visible:!!i&&i.visible,opacity:t}),this.eventEmitter.emit("change",{type:"overlaychange",id:e,visible:!!i&&i.visible,opacity:t}),!0}addOverlay(e,t=!1){this.stateService.initOverlay(e.id,e);var i=this.stateService.getOverlayStates()[e.id];return this.uiManager&&i&&i.visible&&this.uiManager.activateOverlay(e.id,!1),t&&this.eventEmitter.emit("overlaychange",{id:e.id,visible:!!i&&i.visible,opacity:i?i.opacity:1}),!0}removeOverlay(e,t=!1){return this.uiManager&&(this.uiManager.deactivateOverlay(e),this.uiManager.setLoadingState(e,!1),this.uiManager.setErrorState(e,null)),this.stateService.removeOverlay(e),t&&this.eventEmitter.emit("overlaychange",{id:e,visible:!1,opacity:1}),!0}removeAllOverlays(){return this.options&&this.options.overlays&&this.options.overlays.forEach(e=>{this.uiManager&&this.uiManager.deactivateOverlay(e.id),this.stateService.removeOverlay(e.id)}),this.uiManager&&this.uiManager.clearAll(),!0}setGroupVisibility(t,e){this.stateService.setGroupVisibility(t,e);var s=(this.options&&this.options.overlays||[]).filter(e=>e.group===t);if(e){let i=s.some(e=>{e=this.stateService.getOverlayStates()[e.id];return e&&e.visible});s.forEach(e=>{i||this.stateService.setOverlayVisibility(e.id,!0);var t=this.stateService.getOverlayStates()[e.id];this.uiManager&&t&&t.visible&&this.uiManager.activateOverlay(e.id,!1),this.uiManager&&this.uiManager.updateOverlayUI(e.id)})}else s.forEach(e=>{this.stateService.setOverlayVisibility(e.id,!1),this.uiManager&&this.uiManager.deactivateOverlay(e.id)});return this.uiManager&&this.uiManager.updateGroupUI(t),this.eventEmitter.emit("overlaygroupchange",{id:t,visible:e}),this.eventEmitter.emit("change",{type:"overlaygroupchange",id:t,visible:e}),!0}setGroupOpacity(t,e){let i=Math.max(0,Math.min(1,parseFloat(e)||0));return this.stateService.setGroupOpacity(t,i),(this.options&&this.options.overlays||[]).filter(e=>e.group===t).forEach(e=>{this.stateService.setOverlayOpacity(e.id,i);var t=this.stateService.getOverlayStates()[e.id];this.uiManager&&t&&t.visible&&this.uiManager.updateOverlayOpacity(e.id,i)}),!0}destroy(){this.map=null,this.uiManager=null,this.mapService=null,this.options=null}}class LayersControl{constructor(e={},t={}){if(!t.stateService)throw new Error("LayersControl requires stateService");if(!t.uiService)throw new Error("LayersControl requires uiService");if(!t.mapService)throw new Error("LayersControl requires mapService");if(!t.businessLogicService)throw new Error("LayersControl requires businessLogicService");if(!t.eventEmitter)throw new Error("LayersControl requires eventEmitter");if(!e.baseStyles||!Array.isArray(e.baseStyles))throw new Error("LayersControl requires a baseStyles array");if(!e.overlays||!Array.isArray(e.overlays))throw new Error("LayersControl requires an overlays array");this.options={showOpacity:!0,autoClose:!1,icon:"☰",i18n:{baseHeader:"Base Layers",overlaysHeader:"Overlays"},...e,i18n:{baseHeader:"Base Layers",overlaysHeader:"Overlays",...e.i18n||{}}},this.stateService=t.stateService,this.uiService=t.uiService,this.mapService=t.mapService,this.businessLogicService=t.businessLogicService,this.eventEmitter=t.eventEmitter,this.map=null,this.container=null,this._viewportSaveTimeout=null,this._mapEventHandlers=new Map,this._setupPublicEventForwarding()}onAdd(e){var t;return this.map=e,this.container=document.createElement("div"),this.container.className="maplibregl-ctrl maplibregl-ctrl-group layers-control-container",this.mapService.setMap(e),this.uiService.setOptions(this.options),this.uiService.setMap(e),this.uiService.setContainer(this.container),this.options.overlays.forEach(e=>{this.stateService.initOverlay(e.id,e)}),this.stateService.getCurrentBase()||(t=this.options.defaultBaseId||(0<this.options.baseStyles.length?this.options.baseStyles[0].id:null))&&this.stateService.setBase(t),this.businessLogicService.initialize({map:e,stateService:this.stateService,uiService:this.uiService,mapService:this.mapService,eventEmitter:this.eventEmitter,options:this.options}),this.uiService.setBusinessLogicService(this.businessLogicService),this.uiService.render(),this._setupMapEventListeners(),this._restoreMapState(),this.container}onRemove(){this._cleanupMapEventListeners(),this.mapService.setMap(null),this.uiService.setMap(null),this.uiService.setContainer(null),this.container&&this.container.parentNode&&this.container.parentNode.removeChild(this.container),this.map=null,this.container=null}destroy(){if(this.removeAllOverlays(),this._cleanupMapEventListeners(),this.map&&this.container)try{this.map.removeControl(this)}catch(e){}return this.businessLogicService.destroy(),this.uiService.destroy(),this.mapService.destroy(),this.stateService.destroy(),this.container&&this.container.parentNode&&this.container.parentNode.removeChild(this.container),this.map=null,this.container=null,this.stateService=null,this.uiService=null,this.mapService=null,this.businessLogicService=null,this.eventEmitter=null,this.options=null,this._mapEventHandlers.clear(),this._viewportSaveTimeout&&(clearTimeout(this._viewportSaveTimeout),this._viewportSaveTimeout=null),!0}setBaseLayer(t){return this.options.baseStyles.find(e=>e.id===t)?(this.businessLogicService.setBaseLayer(t),!0):(console.warn(`LayersControl.setBaseLayer: base style "${t}" not found`),!1)}setBase(e){return this.setBaseLayer(e)}addBaseStyle(t){var e;if(t&&t.id)return-1<(e=this.options.baseStyles.findIndex(e=>e.id===t.id))?this.options.baseStyles[e]={...this.options.baseStyles[e],...t}:this.options.baseStyles.push(t),this.businessLogicService.updateBaseStyles(this.options.baseStyles),!0;throw new Error("Base style must have an id")}removeBaseStyle(t){var e,i;return!!t&&(-1===(i=this.options.baseStyles.findIndex(e=>e.id===t))?(console.warn(`LayersControl.removeBaseStyle: "${t}" not found`),!1):(e=this.stateService.getCurrentBase()===t,this.options.baseStyles.splice(i,1),e&&0<this.options.baseStyles.length&&(i=this.options.baseStyles.find(e=>e.id===this.options.defaultBaseId)||this.options.baseStyles[0],this.businessLogicService.setBaseLayer(i.id)),this.businessLogicService.updateBaseStyles(this.options.baseStyles),!0))}getBaseLayers(){let t=this.stateService.getCurrentBase();return this.options.baseStyles.map(e=>({...e,active:e.id===t}))}addOverlay(t,e=!1){var i;if(t&&t.id)return-1<(i=this.options.overlays.findIndex(e=>e.id===t.id))?this.options.overlays[i]={...this.options.overlays[i],...t}:this.options.overlays.push(t),this.businessLogicService.addOverlay(t,e),this.uiService.updateOverlays(),!0;throw new Error("Overlay config must have an id")}removeOverlay(t,e=!1){var i=this.options.overlays.findIndex(e=>e.id===t);return-1===i?(console.warn(`LayersControl.removeOverlay: "${t}" not found`),!1):(this.businessLogicService.removeOverlay(t,e),this.options.overlays.splice(i,1),this.uiService.updateOverlays(),!0)}removeAllOverlays(){return this.businessLogicService.removeAllOverlays(),this.options.overlays=[],this.uiService.updateOverlays(),!0}showOverlay(t,e=!1){return this.options.overlays.find(e=>e.id===t)?(this.businessLogicService.showOverlay(t,e),!0):(console.warn(`LayersControl.showOverlay: "${t}" not found`),!1)}hideOverlay(t,e=!1){return this.options.overlays.find(e=>e.id===t)?(this.businessLogicService.hideOverlay(t,e),!0):(console.warn(`LayersControl.hideOverlay: "${t}" not found`),!1)}setOverlayOpacity(t,e){return this.options.overlays.find(e=>e.id===t)?(this.businessLogicService.setOverlayOpacity(t,e),!0):(console.warn(`LayersControl.setOverlayOpacity: "${t}" not found`),!1)}getOverlays(){let t=this.stateService.getOverlayStates();return this.options.overlays.map(e=>({...e,visible:!!t[e.id]&&t[e.id].visible,opacity:t[e.id]?t[e.id].opacity:1}))}showGroup(t){return this.options.overlays.some(e=>e.group===t)?(this.businessLogicService.setGroupVisibility(t,!0),!0):(console.warn(`LayersControl.showGroup: group "${t}" not found`),!1)}hideGroup(t){return this.options.overlays.some(e=>e.group===t)?(this.businessLogicService.setGroupVisibility(t,!1),!0):(console.warn(`LayersControl.hideGroup: group "${t}" not found`),!1)}setGroupOpacity(t,e){return this.options.overlays.some(e=>e.group===t)?(this.businessLogicService.setGroupOpacity(t,e),!0):(console.warn(`LayersControl.setGroupOpacity: group "${t}" not found`),!1)}getGroups(){let i=this.stateService.getGroupStates(),s=new Map;return this.options.overlays.forEach(t=>{var e;t.group&&!s.has(t.group)&&(e=(this.options.groups||[]).find(e=>e.id===t.group),s.set(t.group,{id:t.group,label:e?e.label:t.group,visible:!!i[t.group]&&i[t.group].visible,opacity:i[t.group]?i[t.group].opacity:1,overlays:[]}))}),this.options.overlays.forEach(e=>{e.group&&s.has(e.group)&&s.get(e.group).overlays.push(e.id)}),Array.from(s.values())}saveCurrentViewport(){var e;return!!this.map&&(e=this.map.getCenter(),this.stateService.setViewport({center:{lng:e.lng,lat:e.lat},zoom:this.map.getZoom(),bearing:this.map.getBearing(),pitch:this.map.getPitch()}),!0)}applySavedViewport(){var e;return!!this.map&&!(!(e=this.stateService.getViewport())||!e.center||(this.map.jumpTo({center:e.center,zoom:e.zoom||0,bearing:e.bearing||0,pitch:e.pitch||0}),0))}clearPersistedData(){return this.stateService.clearPersisted()}on(e,t){return this.eventEmitter.on(e,t),this}off(e,t){return this.eventEmitter.off(e,t),this}getCurrentState(){return this.stateService.getAll()}_setupPublicEventForwarding(){}_setupMapEventListeners(){var e;this.map&&(this._mapEventHandlers.set("moveend",e=()=>{clearTimeout(this._viewportSaveTimeout),this._viewportSaveTimeout=setTimeout(()=>{this.saveCurrentViewport()},500)}),this.map.on("moveend",e))}_cleanupMapEventListeners(){this.map&&(this._mapEventHandlers.forEach((e,t)=>{this.map.off(t,e)}),this._mapEventHandlers.clear(),this._viewportSaveTimeout)&&(clearTimeout(this._viewportSaveTimeout),this._viewportSaveTimeout=null)}_restoreMapState(){if(this.map){let t=this.stateService.getCurrentBase(),e=!1;t&&(this.options.baseStyles.find(e=>e.id===t)?(this.businessLogicService.setBaseLayer(t),e=!0):(i=this.options.baseStyles.find(e=>e.id===this.options.defaultBaseId)||this.options.baseStyles[0])&&(this.stateService.setBase(i.id),this.uiService.updateBaseUI()));var i=this.stateService.getViewport();i&&i.center&&setTimeout(()=>this.applySavedViewport(),e?400:100),e||(i=this.stateService.getOverlayStates(),Object.entries(i).forEach(([e,t])=>{t&&t.visible&&setTimeout(()=>{this.businessLogicService.activateOverlay(e,!1)},200)}))}}}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drecchia/maplibre-layerlibre",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"author": "Danilo T Recchia",
|
|
5
|
+
"main": "dist/js/all.min.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/js/all.min.js",
|
|
8
|
+
"dist/css/all.css"
|
|
9
|
+
],
|
|
10
|
+
"license": "CC-BY-NC-4.0",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/drecchia/maplibre-layerlibre.git"
|
|
14
|
+
},
|
|
15
|
+
"description": "MapLibre LayerLibre — a compact layer-switcher control for MapLibre GL JS with deck.gl overlay support.",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"layer",
|
|
18
|
+
"control",
|
|
19
|
+
"maplibre",
|
|
20
|
+
"vanillajs"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "gulp default"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"gulp": "^4.0.2",
|
|
27
|
+
"gulp-autoprefixer": "^8.0.0",
|
|
28
|
+
"gulp-clean-css": "^4.3.0",
|
|
29
|
+
"gulp-concat": "^2.6.1",
|
|
30
|
+
"gulp-postcss": "^10.0.0",
|
|
31
|
+
"gulp-uglify": "^3.0.2",
|
|
32
|
+
"postcss-nested": "^7.0.2"
|
|
33
|
+
}
|
|
34
|
+
}
|