maplibre-gl-layer-control 0.7.2 → 0.8.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 CHANGED
@@ -73,7 +73,17 @@ map.on('load', () => {
73
73
  panelMaxWidth: 450
74
74
  });
75
75
 
76
- // Option 2: Show ALL layers individually (no layers parameter)
76
+ // Option 2: Auto-detect with basemapStyleUrl (recommended for reliable basemap detection)
77
+ // - Fetches the basemap style to identify basemap layers
78
+ // - All basemap layers are grouped under "Background"
79
+ // - User-added layers are shown individually
80
+ // const BASEMAP_STYLE = 'https://demotiles.maplibre.org/style.json';
81
+ // const layerControl = new LayerControl({
82
+ // collapsed: false,
83
+ // basemapStyleUrl: BASEMAP_STYLE
84
+ // });
85
+
86
+ // Option 3: Show ALL layers individually (no layers parameter)
77
87
  // - Auto-detects ALL layers from the map
78
88
  // - Generates friendly names from layer IDs (e.g., 'countries-layer' → 'Countries Layer')
79
89
  // const layerControl = new LayerControl({
@@ -83,7 +93,7 @@ map.on('load', () => {
83
93
  // panelMaxWidth: 450
84
94
  // });
85
95
 
86
- // Option 3: Manually specify layer states (for full control over names)
96
+ // Option 4: Manually specify layer states (for full control over names)
87
97
  // const layerControl = new LayerControl({
88
98
  // collapsed: false,
89
99
  // layerStates: {
@@ -161,6 +171,7 @@ function MapComponent() {
161
171
  | `showLayerSymbol` | `boolean` | `true` | Show layer type symbols (colored icons) next to layer names |
162
172
  | `excludeDrawnLayers` | `boolean` | `true` | Exclude layers from drawing libraries (Geoman, Mapbox GL Draw, etc.) |
163
173
  | `customLayerAdapters` | `CustomLayerAdapter[]` | `undefined` | Adapters for non-MapLibre layers (deck.gl, Zarr, etc.) |
174
+ | `basemapStyleUrl` | `string` | `undefined` | URL of basemap style JSON for reliable layer detection (see below) |
164
175
 
165
176
  ### LayerState
166
177
 
@@ -172,14 +183,76 @@ interface LayerState {
172
183
  }
173
184
  ```
174
185
 
186
+ ### Basemap Style URL Detection
187
+
188
+ 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.
189
+
190
+ For **reliable detection**, provide the `basemapStyleUrl` option with the same URL used for the map's style:
191
+
192
+ ```typescript
193
+ const BASEMAP_STYLE_URL = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
194
+
195
+ const map = new maplibregl.Map({
196
+ container: 'map',
197
+ style: BASEMAP_STYLE_URL,
198
+ center: [0, 0],
199
+ zoom: 2
200
+ });
201
+
202
+ map.on('load', () => {
203
+ // Add your custom layers
204
+ map.addLayer({
205
+ id: 'my-custom-layer',
206
+ type: 'fill',
207
+ source: 'my-source',
208
+ paint: { 'fill-color': '#088' }
209
+ });
210
+
211
+ // Create layer control with basemapStyleUrl for reliable detection
212
+ const layerControl = new LayerControl({
213
+ collapsed: false,
214
+ basemapStyleUrl: BASEMAP_STYLE_URL // All layers from this URL go to "Background"
215
+ });
216
+
217
+ map.addControl(layerControl, 'top-right');
218
+ });
219
+ ```
220
+
221
+ When `basemapStyleUrl` is provided:
222
+ - The control fetches the style JSON and extracts all layer IDs
223
+ - Layers that exist in the basemap style are grouped under "Background"
224
+ - All other layers (user-added) are shown individually in the control
225
+ - New layers added later are automatically detected as user layers
226
+
227
+ ### Automatic Detection Without basemapStyleUrl
228
+
229
+ 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!
230
+
231
+ ```typescript
232
+ map.on('load', () => {
233
+ // Add custom layers BEFORE the control - they will be detected
234
+ map.addSource('my-source', { type: 'geojson', data: myGeoJson });
235
+ map.addLayer({ id: 'my-layer', type: 'fill', source: 'my-source', ... });
236
+
237
+ // Add the control - it detects existing custom layers
238
+ const layerControl = new LayerControl({ collapsed: false });
239
+ map.addControl(layerControl, 'top-right');
240
+
241
+ // Add more layers AFTER the control - they will also be detected automatically
242
+ map.addLayer({ id: 'another-layer', type: 'circle', source: 'another-source', ... });
243
+ });
244
+ ```
245
+
175
246
  ## Examples
176
247
 
177
248
  See the [examples](./examples) folder for complete working examples:
178
249
 
179
250
  - **[basic](./examples/basic)** - Simple vanilla JavaScript example
180
- - **[full-demo](./examples/full-demo)** - Full demo with multiple layer types
251
+ - **[full-demo](./examples/full-demo)** - Full demo with multiple layer types and `basemapStyleUrl` for reliable basemap detection
252
+ - **[dynamic-layers](./examples/dynamic-layers)** - Auto-detect layers added before or after control
181
253
  - **[background-legend](./examples/background-legend)** - Background layer visibility control
182
254
  - **[react](./examples/react)** - React integration example
255
+ - **[cdn](./examples/cdn)** - Browser-only example using CDN (no build step required)
183
256
 
184
257
  ### Layer Symbols
185
258
 
@@ -386,6 +459,41 @@ npm test
386
459
  npm run build
387
460
  ```
388
461
 
462
+ ## Docker
463
+
464
+ The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
465
+
466
+ ### Pull and Run
467
+
468
+ ```bash
469
+ # Pull the latest image
470
+ docker pull ghcr.io/opengeos/maplibre-gl-layer-control:latest
471
+
472
+ # Run the container
473
+ docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-layer-control:latest
474
+ ```
475
+
476
+ Then open http://localhost:8080/maplibre-gl-layer-control/ in your browser to view the examples.
477
+
478
+ ### Build Locally
479
+
480
+ ```bash
481
+ # Build the image
482
+ docker build -t maplibre-gl-layer-control .
483
+
484
+ # Run the container
485
+ docker run -p 8080:80 maplibre-gl-layer-control
486
+ ```
487
+
488
+ ### Available Tags
489
+
490
+ | Tag | Description |
491
+ |-----|-------------|
492
+ | `latest` | Latest release |
493
+ | `x.y.z` | Specific version (e.g., `1.0.0`) |
494
+ | `x.y` | Minor version (e.g., `1.0`) |
495
+
496
+
389
497
  ## License
390
498
 
391
499
  MIT © Qiusheng Wu
package/dist/index.cjs CHANGED
@@ -643,6 +643,7 @@ class LayerControl {
643
643
  __publicField(this, "targetLayers");
644
644
  __publicField(this, "styleEditors");
645
645
  __publicField(this, "initialSourceIds", null);
646
+ __publicField(this, "initialLayerIds", null);
646
647
  // Panel width management
647
648
  __publicField(this, "minPanelWidth");
648
649
  __publicField(this, "maxPanelWidth");
@@ -653,6 +654,8 @@ class LayerControl {
653
654
  __publicField(this, "excludeDrawnLayers");
654
655
  __publicField(this, "customLayerRegistry", null);
655
656
  __publicField(this, "customLayerUnsubscribe", null);
657
+ __publicField(this, "basemapStyleUrl", null);
658
+ __publicField(this, "basemapLayerIds", null);
656
659
  __publicField(this, "widthSliderEl", null);
657
660
  __publicField(this, "widthThumbEl", null);
658
661
  __publicField(this, "widthValueEl", null);
@@ -687,6 +690,7 @@ class LayerControl {
687
690
  this.customLayerRegistry.register(adapter);
688
691
  });
689
692
  }
693
+ this.basemapStyleUrl = options.basemapStyleUrl || null;
690
694
  }
691
695
  /**
692
696
  * Called when the control is added to the map
@@ -700,8 +704,10 @@ class LayerControl {
700
704
  } else {
701
705
  this.initialSourceIds = /* @__PURE__ */ new Set();
702
706
  }
703
- if (Object.keys(this.state.layerStates).length === 0) {
704
- this.autoDetectLayers();
707
+ if (style && style.layers) {
708
+ this.initialLayerIds = new Set(style.layers.map((layer) => layer.id));
709
+ } else {
710
+ this.initialLayerIds = /* @__PURE__ */ new Set();
705
711
  }
706
712
  this.container = this.createContainer();
707
713
  this.button = this.createToggleButton();
@@ -710,7 +716,25 @@ class LayerControl {
710
716
  this.mapContainer.appendChild(this.panel);
711
717
  this.updateWidthDisplay();
712
718
  this.setupEventListeners();
713
- this.buildLayerItems();
719
+ if (this.basemapStyleUrl && !this.basemapLayerIds) {
720
+ this.fetchBasemapStyle().then(() => {
721
+ if (Object.keys(this.state.layerStates).length === 0) {
722
+ this.autoDetectLayers();
723
+ }
724
+ this.buildLayerItems();
725
+ }).catch((error) => {
726
+ console.warn("Failed to fetch basemap style, falling back to heuristic detection:", error);
727
+ if (Object.keys(this.state.layerStates).length === 0) {
728
+ this.autoDetectLayers();
729
+ }
730
+ this.buildLayerItems();
731
+ });
732
+ } else {
733
+ if (Object.keys(this.state.layerStates).length === 0) {
734
+ this.autoDetectLayers();
735
+ }
736
+ this.buildLayerItems();
737
+ }
714
738
  if (!this.state.collapsed) {
715
739
  requestAnimationFrame(() => {
716
740
  this.updatePanelPosition();
@@ -718,6 +742,30 @@ class LayerControl {
718
742
  }
719
743
  return this.container;
720
744
  }
745
+ /**
746
+ * Fetch the basemap style JSON and extract layer IDs.
747
+ * This provides reliable distinction between basemap and user-added layers.
748
+ */
749
+ async fetchBasemapStyle() {
750
+ if (!this.basemapStyleUrl) return;
751
+ try {
752
+ const response = await fetch(this.basemapStyleUrl);
753
+ if (!response.ok) {
754
+ throw new Error(`HTTP error! status: ${response.status}`);
755
+ }
756
+ const styleJson = await response.json();
757
+ if (styleJson && Array.isArray(styleJson.layers)) {
758
+ this.basemapLayerIds = new Set(
759
+ styleJson.layers.map((layer) => layer.id)
760
+ );
761
+ } else {
762
+ this.basemapLayerIds = /* @__PURE__ */ new Set();
763
+ }
764
+ } catch (error) {
765
+ console.warn("Failed to fetch basemap style from URL:", this.basemapStyleUrl, error);
766
+ throw error;
767
+ }
768
+ }
721
769
  /**
722
770
  * Called when the control is removed from the map
723
771
  */
@@ -754,7 +802,8 @@ class LayerControl {
754
802
  if (this.targetLayers.length === 0) {
755
803
  const userAddedLayers = [];
756
804
  const backgroundLayerIds = [];
757
- const userAddedSourceIds = this.detectUserAddedSources();
805
+ const useBasemapStyleDetection = this.basemapLayerIds !== null && this.basemapLayerIds.size > 0;
806
+ const userAddedSourceIds = useBasemapStyleDetection ? /* @__PURE__ */ new Set() : this.detectUserAddedSources();
758
807
  allLayerIds.forEach((layerId) => {
759
808
  const layer = this.map.getLayer(layerId);
760
809
  if (!layer) return;
@@ -762,11 +811,19 @@ class LayerControl {
762
811
  backgroundLayerIds.push(layerId);
763
812
  return;
764
813
  }
765
- const sourceId = layer.source;
766
- if (sourceId && userAddedSourceIds.has(sourceId)) {
767
- userAddedLayers.push(layerId);
814
+ if (useBasemapStyleDetection) {
815
+ if (this.basemapLayerIds.has(layerId)) {
816
+ backgroundLayerIds.push(layerId);
817
+ } else {
818
+ userAddedLayers.push(layerId);
819
+ }
768
820
  } else {
769
- backgroundLayerIds.push(layerId);
821
+ const sourceId = layer.source;
822
+ if (sourceId && userAddedSourceIds.has(sourceId)) {
823
+ userAddedLayers.push(layerId);
824
+ } else {
825
+ backgroundLayerIds.push(layerId);
826
+ }
770
827
  }
771
828
  });
772
829
  if (backgroundLayerIds.length > 0) {
@@ -1532,9 +1589,27 @@ class LayerControl {
1532
1589
  }
1533
1590
  /**
1534
1591
  * Check if a layer is a user-added layer (vs basemap layer)
1592
+ * Used primarily for the background legend to determine which layers are background
1535
1593
  */
1536
1594
  isUserAddedLayer(layerId) {
1537
- return this.state.layerStates[layerId] !== void 0 && layerId !== "Background";
1595
+ if (this.state.layerStates[layerId] !== void 0 && layerId !== "Background") {
1596
+ return true;
1597
+ }
1598
+ if (this.basemapLayerIds !== null && this.basemapLayerIds.size > 0) {
1599
+ return !this.basemapLayerIds.has(layerId);
1600
+ }
1601
+ if (this.initialLayerIds !== null && !this.initialLayerIds.has(layerId)) {
1602
+ return true;
1603
+ }
1604
+ const layer = this.map.getLayer(layerId);
1605
+ if (layer) {
1606
+ const sourceId = layer.source;
1607
+ if (sourceId) {
1608
+ const userAddedSourceIds = this.detectUserAddedSources();
1609
+ return userAddedSourceIds.has(sourceId);
1610
+ }
1611
+ }
1612
+ return false;
1538
1613
  }
1539
1614
  /**
1540
1615
  * Toggle visibility for all background layers (basemap layers)
@@ -2324,7 +2399,8 @@ class LayerControl {
2324
2399
  const currentMapLayerIds = new Set(style.layers.map((layer) => layer.id));
2325
2400
  const isAutoDetectMode = this.targetLayers.length === 0 || this.targetLayers.length === 1 && this.targetLayers[0] === "Background" || this.targetLayers.every((id) => id === "Background" || this.state.layerStates[id]);
2326
2401
  const newLayers = [];
2327
- const userAddedSourceIds = isAutoDetectMode ? this.detectUserAddedSources() : /* @__PURE__ */ new Set();
2402
+ const useBasemapStyleDetection = this.basemapLayerIds !== null && this.basemapLayerIds.size > 0;
2403
+ const useInitialLayerDetection = !useBasemapStyleDetection && this.initialLayerIds !== null && this.initialLayerIds.size > 0;
2328
2404
  currentMapLayerIds.forEach((layerId) => {
2329
2405
  if (layerId !== "Background" && !this.state.layerStates[layerId]) {
2330
2406
  const layer = this.map.getLayer(layerId);
@@ -2333,9 +2409,24 @@ class LayerControl {
2333
2409
  if (this.excludeDrawnLayers && this.isDrawnLayer(layerId)) {
2334
2410
  return;
2335
2411
  }
2336
- const sourceId = layer.source;
2337
- if (!sourceId || !userAddedSourceIds.has(sourceId)) {
2338
- return;
2412
+ if (useBasemapStyleDetection) {
2413
+ if (this.basemapLayerIds.has(layerId)) {
2414
+ return;
2415
+ }
2416
+ } else if (useInitialLayerDetection) {
2417
+ if (this.initialLayerIds.has(layerId)) {
2418
+ const userAddedSourceIds = this.detectUserAddedSources();
2419
+ const sourceId = layer.source;
2420
+ if (!sourceId || !userAddedSourceIds.has(sourceId)) {
2421
+ return;
2422
+ }
2423
+ }
2424
+ } else {
2425
+ const userAddedSourceIds = this.detectUserAddedSources();
2426
+ const sourceId = layer.source;
2427
+ if (!sourceId || !userAddedSourceIds.has(sourceId)) {
2428
+ return;
2429
+ }
2339
2430
  }
2340
2431
  }
2341
2432
  newLayers.push(layerId);