maplibre-gl-layer-control-tidop 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Qiusheng Wu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,506 @@
1
+ # maplibre-gl-layer-control-tidop
2
+
3
+ [![npm version](https://img.shields.io/npm/v/maplibre-gl-layer-control-tidop.svg)](https://www.npmjs.com/package/maplibre-gl-layer-control-tidop)
4
+ [![npm downloads](https://img.shields.io/npm/dm/maplibre-gl-layer-control-tidop.svg)](https://www.npmjs.com/package/maplibre-gl-layer-control-tidop)
5
+ [![license](https://img.shields.io/npm/l/maplibre-gl-layer-control-tidop.svg)](https://github.com/opengeos/maplibre-gl-layer-control-tidop/blob/main/LICENSE)
6
+ [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?logo=codesandbox)](https://codesandbox.io/p/github/opengeos/maplibre-gl-layer-control-tidop)
7
+ [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?logo=stackblitz)](https://stackblitz.com/github/opengeos/maplibre-gl-layer-control-tidop)
8
+
9
+ A comprehensive layer control for MapLibre GL with advanced styling capabilities. Built with TypeScript and React, providing both vanilla JavaScript and React integration options.
10
+
11
+ ## Features
12
+
13
+ - ✅ **Auto-detection** - Automatically detects layer properties (opacity, visibility) and generates friendly names
14
+ - ✅ **Layer visibility toggle** - Checkbox control for each layer
15
+ - ✅ **Layer opacity control** - Smooth opacity slider with type-aware property mapping
16
+ - ✅ **Layer symbols** - Visual type indicators (colored shapes) next to layer names, auto-detected from layer paint properties
17
+ - ✅ **Resizable panel** - Adjustable panel width (240-420px) with keyboard support
18
+ - ✅ **Advanced style editor** - Per-layer-type styling controls:
19
+ - **Fill layers**: color, opacity, outline-color
20
+ - **Line layers**: color, width, opacity, blur
21
+ - **Circle layers**: color, radius, opacity, blur, stroke properties
22
+ - **Symbol layers**: text-color, text-halo-color, halo-width, text/icon-opacity
23
+ - **Raster layers**: opacity, brightness, saturation, contrast, hue-rotate
24
+ - ✅ **Dynamic layer detection** - Automatically detect and manage new layers
25
+ - ✅ **Background layer grouping** - Control all basemap layers as one group
26
+ - ✅ **Background layer legend** - Gear icon to toggle individual background layer visibility
27
+ - ✅ **Accessibility** - Full ARIA support and keyboard navigation
28
+ - ✅ **TypeScript** - Full type safety and IntelliSense support
29
+ - ✅ **React integration** - Optional React components and hooks
30
+ - ✅ **Custom layer adapters** - Integrate non-MapLibre layers (deck.gl, Zarr, etc.)
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install maplibre-gl-layer-control-tidop
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ### Vanilla JavaScript
41
+
42
+ ```typescript
43
+ import maplibregl from 'maplibre-gl';
44
+ import { LayerControl } from 'maplibre-gl-layer-control-tidop';
45
+ import 'maplibre-gl-layer-control-tidop/style.css';
46
+
47
+ const map = new maplibregl.Map({
48
+ container: 'map',
49
+ style: 'https://demotiles.maplibre.org/style.json',
50
+ center: [0, 0],
51
+ zoom: 2
52
+ });
53
+
54
+ map.on('load', () => {
55
+ // Add your custom layers
56
+ map.addLayer({
57
+ id: 'my-layer',
58
+ type: 'fill',
59
+ source: 'my-source',
60
+ paint: {
61
+ 'fill-color': '#088',
62
+ 'fill-opacity': 0.5
63
+ }
64
+ });
65
+
66
+ // Create layer control with auto-detection
67
+ // Option 1: Specify which layers to control (recommended for most use cases)
68
+ // - Shows specified layers with auto-detected opacity, visibility, and friendly names
69
+ // - Groups all other layers as "Background"
70
+ const layerControl = new LayerControl({
71
+ collapsed: false,
72
+ layers: ['my-layer'], // LayerControl auto-detects opacity, visibility, and generates friendly names
73
+ panelWidth: 340,
74
+ panelMinWidth: 240,
75
+ panelMaxWidth: 450
76
+ });
77
+
78
+ // Option 2: Auto-detect with basemapStyleUrl (recommended for reliable basemap detection)
79
+ // - Fetches the basemap style to identify basemap layers
80
+ // - All basemap layers are grouped under "Background"
81
+ // - User-added layers are shown individually
82
+ // const BASEMAP_STYLE = 'https://demotiles.maplibre.org/style.json';
83
+ // const layerControl = new LayerControl({
84
+ // collapsed: false,
85
+ // basemapStyleUrl: BASEMAP_STYLE
86
+ // });
87
+
88
+ // Option 3: Show ALL layers individually (no layers parameter)
89
+ // - Auto-detects ALL layers from the map
90
+ // - Generates friendly names from layer IDs (e.g., 'countries-layer' → 'Countries Layer')
91
+ // const layerControl = new LayerControl({
92
+ // collapsed: false,
93
+ // panelWidth: 340,
94
+ // panelMinWidth: 240,
95
+ // panelMaxWidth: 450
96
+ // });
97
+
98
+ // Option 4: Manually specify layer states (for full control over names)
99
+ // const layerControl = new LayerControl({
100
+ // collapsed: false,
101
+ // layerStates: {
102
+ // 'my-layer': {
103
+ // visible: true,
104
+ // opacity: 0.5,
105
+ // name: 'My Custom Layer Name'
106
+ // }
107
+ // }
108
+ // });
109
+
110
+ map.addControl(layerControl, 'top-right');
111
+ });
112
+ ```
113
+
114
+ ### React
115
+
116
+ ```typescript
117
+ import { useState, useEffect } from 'react';
118
+ import maplibregl, { Map as MapLibreMap } from 'maplibre-gl';
119
+ import { LayerControlReact } from 'maplibre-gl-layer-control-tidop/react';
120
+ import 'maplibre-gl/dist/maplibre-gl.css';
121
+ import 'maplibre-gl-layer-control-tidop/style.css';
122
+
123
+ function MapComponent() {
124
+ const [map, setMap] = useState<MapLibreMap | null>(null);
125
+
126
+ useEffect(() => {
127
+ const newMap = new maplibregl.Map({
128
+ container: 'map',
129
+ style: 'https://demotiles.maplibre.org/style.json',
130
+ center: [0, 0],
131
+ zoom: 2
132
+ });
133
+
134
+ newMap.on('load', () => {
135
+ // Add your custom layers here
136
+ setMap(newMap);
137
+ });
138
+
139
+ return () => newMap.remove();
140
+ }, []);
141
+
142
+ return (
143
+ <div>
144
+ <div id="map" style={{ width: '100%', height: '600px' }} />
145
+ {map && (
146
+ <LayerControlReact
147
+ map={map}
148
+ position="top-right"
149
+ layers={['my-layer']}
150
+ collapsed={false}
151
+ />
152
+ )}
153
+ </div>
154
+ );
155
+ }
156
+ ```
157
+
158
+ ## API
159
+
160
+ ### LayerControl Options
161
+
162
+ | Option | Type | Default | Description |
163
+ |--------|------|---------|-------------|
164
+ | `collapsed` | `boolean` | `true` | Start with panel collapsed |
165
+ | `layers` | `string[]` | `undefined` | Layer IDs to control (auto-detects all if omitted) |
166
+ | `layerStates` | `Record<string, LayerState>` | `undefined` | Manual layer state configuration |
167
+ | `panelWidth` | `number` | `320` | Initial panel width in pixels |
168
+ | `panelMinWidth` | `number` | `240` | Minimum panel width |
169
+ | `panelMaxWidth` | `number` | `420` | Maximum panel width |
170
+ | `panelMaxHeight` | `number` | `600` | Maximum panel height (scrollable when exceeded) |
171
+ | `showStyleEditor` | `boolean` | `true` | Show gear icon for style editor |
172
+ | `showOpacitySlider` | `boolean` | `true` | Show opacity slider for layers |
173
+ | `showLayerSymbol` | `boolean` | `true` | Show layer type symbols (colored icons) next to layer names |
174
+ | `excludeDrawnLayers` | `boolean` | `true` | Exclude layers from drawing libraries (Geoman, Mapbox GL Draw, etc.) |
175
+ | `excludeLayers` | `string[]` | `undefined` | Array of wildcard patterns to exclude layers by name (e.g., `['*-temp-*', 'debug-*']`) |
176
+ | `customLayerAdapters` | `CustomLayerAdapter[]` | `undefined` | Adapters for non-MapLibre layers (deck.gl, Zarr, etc.) |
177
+ | `basemapStyleUrl` | `string` | `undefined` | URL of basemap style JSON for reliable layer detection (see below) |
178
+
179
+ ### LayerState
180
+
181
+ ```typescript
182
+ interface LayerState {
183
+ visible: boolean; // Layer visibility
184
+ opacity: number; // Opacity (0-1)
185
+ name?: string; // Display name (auto-generated if omitted)
186
+ }
187
+ ```
188
+
189
+ ### Basemap Style URL Detection
190
+
191
+ When using auto-detection (without specifying `layers`), the control needs to distinguish between basemap layers and user-added layers. By default, it uses heuristics based on source detection, which may not always be reliable.
192
+
193
+ For **reliable detection**, provide the `basemapStyleUrl` option with the same URL used for the map's style:
194
+
195
+ ```typescript
196
+ const BASEMAP_STYLE_URL = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
197
+
198
+ const map = new maplibregl.Map({
199
+ container: 'map',
200
+ style: BASEMAP_STYLE_URL,
201
+ center: [0, 0],
202
+ zoom: 2
203
+ });
204
+
205
+ map.on('load', () => {
206
+ // Add your custom layers
207
+ map.addLayer({
208
+ id: 'my-custom-layer',
209
+ type: 'fill',
210
+ source: 'my-source',
211
+ paint: { 'fill-color': '#088' }
212
+ });
213
+
214
+ // Create layer control with basemapStyleUrl for reliable detection
215
+ const layerControl = new LayerControl({
216
+ collapsed: false,
217
+ basemapStyleUrl: BASEMAP_STYLE_URL // All layers from this URL go to "Background"
218
+ });
219
+
220
+ map.addControl(layerControl, 'top-right');
221
+ });
222
+ ```
223
+
224
+ When `basemapStyleUrl` is provided:
225
+ - The control fetches the style JSON and extracts all layer IDs
226
+ - Layers that exist in the basemap style are grouped under "Background"
227
+ - All other layers (user-added) are shown individually in the control
228
+ - New layers added later are automatically detected as user layers
229
+
230
+ ### Automatic Detection Without basemapStyleUrl
231
+
232
+ Even without `basemapStyleUrl`, the control uses source-based heuristics to detect user-added layers. Custom MapLibre layers (using `map.addLayer()`) are automatically detected whether they are added **before** or **after** the layer control - no custom adapter is needed for standard MapLibre layer types!
233
+
234
+ ```typescript
235
+ map.on('load', () => {
236
+ // Add custom layers BEFORE the control - they will be detected
237
+ map.addSource('my-source', { type: 'geojson', data: myGeoJson });
238
+ map.addLayer({ id: 'my-layer', type: 'fill', source: 'my-source', ... });
239
+
240
+ // Add the control - it detects existing custom layers
241
+ const layerControl = new LayerControl({ collapsed: false });
242
+ map.addControl(layerControl, 'top-right');
243
+
244
+ // Add more layers AFTER the control - they will also be detected automatically
245
+ map.addLayer({ id: 'another-layer', type: 'circle', source: 'another-source', ... });
246
+ });
247
+ ```
248
+
249
+ ## Examples
250
+
251
+ See the [examples](./examples) folder for complete working examples:
252
+
253
+ - **[basic](./examples/basic)** - Simple vanilla JavaScript example
254
+ - **[full-demo](./examples/full-demo)** - Full demo with multiple layer types and `basemapStyleUrl` for reliable basemap detection
255
+ - **[dynamic-layers](./examples/dynamic-layers)** - Auto-detect layers added before or after control
256
+ - **[background-legend](./examples/background-legend)** - Background layer visibility control
257
+ - **[react](./examples/react)** - React integration example
258
+ - **[cdn](./examples/cdn)** - Browser-only example using CDN (no build step required)
259
+
260
+ ### Layer Symbols
261
+
262
+ The layer control displays visual symbols (colored icons) next to each layer name to indicate the layer type. Symbols are automatically generated based on the layer's type and paint properties:
263
+
264
+ | Layer Type | Symbol |
265
+ |------------|--------|
266
+ | `fill` | Colored rectangle with border |
267
+ | `line` | Horizontal line |
268
+ | `circle` | Colored circle |
269
+ | `symbol` | Marker/pin icon |
270
+ | `raster` | Gradient rectangle |
271
+ | `heatmap` | Orange-red gradient |
272
+ | `hillshade` | Gray gradient |
273
+ | `fill-extrusion` | 3D rectangle |
274
+ | `background` | Rectangle with inner border |
275
+ | Background group | Stacked layers icon |
276
+
277
+ The symbol color is automatically extracted from the layer's paint properties (e.g., `fill-color`, `line-color`, `circle-color`). If a color cannot be determined, a neutral gray is used.
278
+
279
+ To disable layer symbols:
280
+
281
+ ```typescript
282
+ const layerControl = new LayerControl({
283
+ showLayerSymbol: false
284
+ });
285
+ ```
286
+
287
+ ### Background Layer Legend
288
+
289
+ When using the `layers` option to specify specific layers, all other layers are grouped under a "Background" entry. The Background layer includes a **gear icon** that opens a detailed legend panel showing:
290
+
291
+ - Individual visibility toggles for each background layer
292
+ - Layer type indicators (fill, line, symbol, etc.)
293
+ - Quick "Show All" / "Hide All" buttons
294
+ - **"Only rendered" filter** - Shows only layers that are currently rendered in the map viewport
295
+ - Indeterminate checkbox state when some layers are hidden
296
+
297
+ This allows fine-grained control over which basemap layers are visible while maintaining a simplified layer control interface.
298
+
299
+ ### Custom Layer Adapters
300
+
301
+ The layer control supports non-MapLibre layers (such as deck.gl or Zarr layers) through the Custom Layer Adapter interface. This allows you to integrate any custom layer type with the layer control's visibility toggle, opacity slider, and layer list.
302
+
303
+ #### CustomLayerAdapter Interface
304
+
305
+ ```typescript
306
+ interface CustomLayerAdapter {
307
+ /** Unique type identifier for this adapter (e.g., 'cog', 'zarr', 'deck') */
308
+ type: string;
309
+
310
+ /** Get all layer IDs managed by this adapter */
311
+ getLayerIds(): string[];
312
+
313
+ /** Get the current state of a layer */
314
+ getLayerState(layerId: string): LayerState | null;
315
+
316
+ /** Set layer visibility */
317
+ setVisibility(layerId: string, visible: boolean): void;
318
+
319
+ /** Set layer opacity (0-1) */
320
+ setOpacity(layerId: string, opacity: number): void;
321
+
322
+ /** Get display name for a layer */
323
+ getName(layerId: string): string;
324
+
325
+ /** Get layer symbol type for UI display (optional) */
326
+ getSymbolType?(layerId: string): string;
327
+
328
+ /**
329
+ * Subscribe to layer changes (add/remove).
330
+ * Returns an unsubscribe function.
331
+ */
332
+ onLayerChange?(callback: (event: 'add' | 'remove', layerId: string) => void): () => void;
333
+ }
334
+ ```
335
+
336
+ #### Implementing a Custom Adapter
337
+
338
+ Here's an example of implementing an adapter for deck.gl layers:
339
+
340
+ ```typescript
341
+ import type { CustomLayerAdapter, LayerState } from 'maplibre-gl-layer-control-tidop';
342
+ import type { MapboxOverlay } from '@deck.gl/mapbox';
343
+
344
+ class DeckLayerAdapter implements CustomLayerAdapter {
345
+ readonly type = 'deck';
346
+
347
+ private deckOverlay: MapboxOverlay;
348
+ private deckLayers: Map<string, any>;
349
+ private changeCallbacks: Array<(event: 'add' | 'remove', layerId: string) => void> = [];
350
+
351
+ constructor(deckOverlay: MapboxOverlay, deckLayers: Map<string, any>) {
352
+ this.deckOverlay = deckOverlay;
353
+ this.deckLayers = deckLayers;
354
+ }
355
+
356
+ getLayerIds(): string[] {
357
+ return Array.from(this.deckLayers.keys());
358
+ }
359
+
360
+ getLayerState(layerId: string): LayerState | null {
361
+ const layer = this.deckLayers.get(layerId);
362
+ if (!layer?.props) return null;
363
+
364
+ return {
365
+ visible: layer.props.visible !== false,
366
+ opacity: layer.props.opacity ?? 1,
367
+ name: this.getName(layerId),
368
+ };
369
+ }
370
+
371
+ setVisibility(layerId: string, visible: boolean): void {
372
+ const layer = this.deckLayers.get(layerId);
373
+ if (!layer?.clone) return;
374
+
375
+ // deck.gl layers are immutable; clone with new props
376
+ const updatedLayer = layer.clone({ visible });
377
+ this.deckLayers.set(layerId, updatedLayer);
378
+ this.updateOverlay();
379
+ }
380
+
381
+ setOpacity(layerId: string, opacity: number): void {
382
+ const layer = this.deckLayers.get(layerId);
383
+ if (!layer?.clone) return;
384
+
385
+ const updatedLayer = layer.clone({ opacity });
386
+ this.deckLayers.set(layerId, updatedLayer);
387
+ this.updateOverlay();
388
+ }
389
+
390
+ getName(layerId: string): string {
391
+ return layerId.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
392
+ }
393
+
394
+ getSymbolType(): string {
395
+ return 'raster'; // Use raster symbol for deck.gl layers
396
+ }
397
+
398
+ onLayerChange(callback: (event: 'add' | 'remove', layerId: string) => void): () => void {
399
+ this.changeCallbacks.push(callback);
400
+ return () => {
401
+ const idx = this.changeCallbacks.indexOf(callback);
402
+ if (idx >= 0) this.changeCallbacks.splice(idx, 1);
403
+ };
404
+ }
405
+
406
+ // Call this when layers are added/removed
407
+ notifyLayerAdded(layerId: string): void {
408
+ this.changeCallbacks.forEach(cb => cb('add', layerId));
409
+ }
410
+
411
+ notifyLayerRemoved(layerId: string): void {
412
+ this.changeCallbacks.forEach(cb => cb('remove', layerId));
413
+ }
414
+
415
+ private updateOverlay(): void {
416
+ this.deckOverlay.setProps({ layers: Array.from(this.deckLayers.values()) });
417
+ }
418
+ }
419
+ ```
420
+
421
+ #### Using Custom Adapters
422
+
423
+ Pass your custom adapters to the `customLayerAdapters` option:
424
+
425
+ ```typescript
426
+ import { LayerControl } from 'maplibre-gl-layer-control-tidop';
427
+
428
+ // Create your custom adapter
429
+ const deckAdapter = new DeckLayerAdapter(deckOverlay, deckLayers);
430
+
431
+ // Create the layer control with the adapter
432
+ const layerControl = new LayerControl({
433
+ collapsed: false,
434
+ customLayerAdapters: [deckAdapter]
435
+ });
436
+
437
+ map.addControl(layerControl, 'top-right');
438
+
439
+ // When you add a new deck.gl layer, notify the adapter
440
+ deckLayers.set('my-deck-layer', myDeckLayer);
441
+ deckAdapter.notifyLayerAdded('my-deck-layer');
442
+ ```
443
+
444
+ #### Limitations
445
+
446
+ - **Style Editor**: The style editor (gear icon) is not available for custom layers since they don't use MapLibre's paint properties. Clicking the gear icon will show an info panel explaining this.
447
+ - **Opacity Support**: Some layer types (like deck.gl's COGLayer) may not support dynamic opacity changes due to underlying library limitations. In these cases, the opacity slider will have no effect.
448
+
449
+ ## Development
450
+
451
+ ```bash
452
+ # Install dependencies
453
+ npm install
454
+
455
+ # Run development server
456
+ npm run dev
457
+
458
+ # Run tests
459
+ npm test
460
+
461
+ # Build for production
462
+ npm run build
463
+ ```
464
+
465
+ ## Docker
466
+
467
+ The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
468
+
469
+ ### Pull and Run
470
+
471
+ ```bash
472
+ # Pull the latest image
473
+ docker pull ghcr.io/opengeos/maplibre-gl-layer-control-tidop:latest
474
+
475
+ # Run the container
476
+ docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-layer-control-tidop:latest
477
+ ```
478
+
479
+ Then open http://localhost:8080/maplibre-gl-layer-control-tidop/ in your browser to view the examples.
480
+
481
+ ### Build Locally
482
+
483
+ ```bash
484
+ # Build the image
485
+ docker build -t maplibre-gl-layer-control-tidop .
486
+
487
+ # Run the container
488
+ docker run -p 8080:80 maplibre-gl-layer-control-tidop
489
+ ```
490
+
491
+ ### Available Tags
492
+
493
+ | Tag | Description |
494
+ |-----|-------------|
495
+ | `latest` | Latest release |
496
+ | `x.y.z` | Specific version (e.g., `1.0.0`) |
497
+ | `x.y` | Minor version (e.g., `1.0`) |
498
+
499
+
500
+ ## License
501
+
502
+ MIT © Qiusheng Wu
503
+
504
+ ## Contributing
505
+
506
+ Contributions are welcome! Please feel free to submit a Pull Request.