maplibre-gl-layer-control 0.1.0 → 0.3.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
@@ -1,5 +1,9 @@
1
1
  # maplibre-gl-layer-control
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/maplibre-gl-layer-control.svg)](https://www.npmjs.com/package/maplibre-gl-layer-control)
4
+ [![npm downloads](https://img.shields.io/npm/dm/maplibre-gl-layer-control.svg)](https://www.npmjs.com/package/maplibre-gl-layer-control)
5
+ [![license](https://img.shields.io/npm/l/maplibre-gl-layer-control.svg)](https://github.com/opengeos/maplibre-gl-layer-control/blob/main/LICENSE)
6
+
3
7
  A comprehensive layer control for MapLibre GL with advanced styling capabilities. Built with TypeScript and React, providing both vanilla JavaScript and React integration options.
4
8
 
5
9
  ## Features
@@ -16,6 +20,7 @@ A comprehensive layer control for MapLibre GL with advanced styling capabilities
16
20
  - **Raster layers**: opacity, brightness, saturation, contrast, hue-rotate
17
21
  - ✅ **Dynamic layer detection** - Automatically detect and manage new layers
18
22
  - ✅ **Background layer grouping** - Control all basemap layers as one group
23
+ - ✅ **Background layer legend** - Gear icon to toggle individual background layer visibility
19
24
  - ✅ **Accessibility** - Full ARIA support and keyboard navigation
20
25
  - ✅ **TypeScript** - Full type safety and IntelliSense support
21
26
  - ✅ **React integration** - Optional React components and hooks
@@ -165,8 +170,21 @@ See the [examples](./examples) folder for complete working examples:
165
170
 
166
171
  - **[basic](./examples/basic)** - Simple vanilla JavaScript example
167
172
  - **[full-demo](./examples/full-demo)** - Full demo with multiple layer types
173
+ - **[background-legend](./examples/background-legend)** - Background layer visibility control
168
174
  - **[react](./examples/react)** - React integration example
169
175
 
176
+ ### Background Layer Legend
177
+
178
+ 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:
179
+
180
+ - Individual visibility toggles for each background layer
181
+ - Layer type indicators (fill, line, symbol, etc.)
182
+ - Quick "Show All" / "Hide All" buttons
183
+ - **"Only rendered" filter** - Shows only layers that are currently rendered in the map viewport
184
+ - Indeterminate checkbox state when some layers are hidden
185
+
186
+ This allows fine-grained control over which basemap layers are visible while maintaining a simplified layer control interface.
187
+
170
188
  ## Development
171
189
 
172
190
  ```bash
package/dist/index.cjs CHANGED
@@ -197,7 +197,10 @@ class LayerControl {
197
197
  activeStyleEditor: null,
198
198
  layerStates: options.layerStates || {},
199
199
  originalStyles: /* @__PURE__ */ new Map(),
200
- userInteractingWithSlider: false
200
+ userInteractingWithSlider: false,
201
+ backgroundLegendOpen: false,
202
+ backgroundLayerVisibility: /* @__PURE__ */ new Map(),
203
+ onlyRenderedFilter: false
201
204
  };
202
205
  this.targetLayers = options.layers || Object.keys(this.state.layerStates);
203
206
  this.styleEditors = /* @__PURE__ */ new Map();
@@ -629,7 +632,10 @@ class LayerControl {
629
632
  row.appendChild(checkbox);
630
633
  row.appendChild(name);
631
634
  row.appendChild(opacity);
632
- if (layerId !== "Background") {
635
+ if (layerId === "Background") {
636
+ const legendButton = this.createBackgroundLegendButton();
637
+ row.appendChild(legendButton);
638
+ } else {
633
639
  const styleButton = this.createStyleButton(layerId);
634
640
  if (styleButton) {
635
641
  row.appendChild(styleButton);
@@ -683,9 +689,19 @@ class LayerControl {
683
689
  const styleLayers = this.map.getStyle().layers || [];
684
690
  styleLayers.forEach((layer) => {
685
691
  if (!this.isUserAddedLayer(layer.id)) {
692
+ this.state.backgroundLayerVisibility.set(layer.id, visible);
686
693
  this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
687
694
  }
688
695
  });
696
+ if (this.state.backgroundLegendOpen) {
697
+ const legendPanel = this.panel.querySelector(".layer-control-background-legend");
698
+ if (legendPanel) {
699
+ const checkboxes = legendPanel.querySelectorAll(".background-legend-checkbox");
700
+ checkboxes.forEach((checkbox) => {
701
+ checkbox.checked = visible;
702
+ });
703
+ }
704
+ }
689
705
  }
690
706
  /**
691
707
  * Change opacity for all background layers (basemap layers)
@@ -704,6 +720,260 @@ class LayerControl {
704
720
  }
705
721
  });
706
722
  }
723
+ // ===== Background Legend Methods =====
724
+ /**
725
+ * Create legend button for Background layer
726
+ */
727
+ createBackgroundLegendButton() {
728
+ const button = document.createElement("button");
729
+ button.className = "layer-control-style-button layer-control-background-legend-button";
730
+ button.innerHTML = "⚙";
731
+ button.title = "Show background layer details";
732
+ button.setAttribute("aria-label", "Show background layer visibility controls");
733
+ button.setAttribute("aria-expanded", String(this.state.backgroundLegendOpen));
734
+ button.addEventListener("click", (e) => {
735
+ e.stopPropagation();
736
+ this.toggleBackgroundLegend();
737
+ });
738
+ return button;
739
+ }
740
+ /**
741
+ * Toggle background legend panel visibility
742
+ */
743
+ toggleBackgroundLegend() {
744
+ if (this.state.backgroundLegendOpen) {
745
+ this.closeBackgroundLegend();
746
+ } else {
747
+ this.openBackgroundLegend();
748
+ }
749
+ }
750
+ /**
751
+ * Open background legend panel
752
+ */
753
+ openBackgroundLegend() {
754
+ if (this.state.activeStyleEditor) {
755
+ this.closeStyleEditor(this.state.activeStyleEditor);
756
+ }
757
+ const itemEl = this.panel.querySelector('[data-layer-id="Background"]');
758
+ if (!itemEl) return;
759
+ let legendPanel = itemEl.querySelector(".layer-control-background-legend");
760
+ if (legendPanel) {
761
+ const layerList = legendPanel.querySelector(".background-legend-layer-list");
762
+ if (layerList) {
763
+ this.populateBackgroundLayerList(layerList);
764
+ }
765
+ } else {
766
+ legendPanel = this.createBackgroundLegendPanel();
767
+ itemEl.appendChild(legendPanel);
768
+ }
769
+ this.state.backgroundLegendOpen = true;
770
+ const button = itemEl.querySelector(".layer-control-background-legend-button");
771
+ if (button) {
772
+ button.setAttribute("aria-expanded", "true");
773
+ button.classList.add("active");
774
+ }
775
+ setTimeout(() => {
776
+ legendPanel == null ? void 0 : legendPanel.scrollIntoView({ behavior: "smooth", block: "nearest" });
777
+ }, 50);
778
+ }
779
+ /**
780
+ * Close background legend panel
781
+ */
782
+ closeBackgroundLegend() {
783
+ const itemEl = this.panel.querySelector('[data-layer-id="Background"]');
784
+ if (!itemEl) return;
785
+ const legendPanel = itemEl.querySelector(".layer-control-background-legend");
786
+ if (legendPanel) {
787
+ legendPanel.remove();
788
+ }
789
+ this.state.backgroundLegendOpen = false;
790
+ const button = itemEl.querySelector(".layer-control-background-legend-button");
791
+ if (button) {
792
+ button.setAttribute("aria-expanded", "false");
793
+ button.classList.remove("active");
794
+ }
795
+ }
796
+ /**
797
+ * Create the background legend panel with individual layer controls
798
+ */
799
+ createBackgroundLegendPanel() {
800
+ const panel = document.createElement("div");
801
+ panel.className = "layer-control-background-legend";
802
+ const header = document.createElement("div");
803
+ header.className = "background-legend-header";
804
+ const title = document.createElement("span");
805
+ title.className = "background-legend-title";
806
+ title.textContent = "Background Layers";
807
+ const closeBtn = document.createElement("button");
808
+ closeBtn.className = "background-legend-close";
809
+ closeBtn.innerHTML = "×";
810
+ closeBtn.title = "Close";
811
+ closeBtn.addEventListener("click", (e) => {
812
+ e.stopPropagation();
813
+ this.closeBackgroundLegend();
814
+ });
815
+ header.appendChild(title);
816
+ header.appendChild(closeBtn);
817
+ const actionsRow = document.createElement("div");
818
+ actionsRow.className = "background-legend-actions";
819
+ const showAllBtn = document.createElement("button");
820
+ showAllBtn.className = "background-legend-action-btn";
821
+ showAllBtn.textContent = "Show All";
822
+ showAllBtn.addEventListener("click", () => this.setAllBackgroundLayersVisibility(true));
823
+ const hideAllBtn = document.createElement("button");
824
+ hideAllBtn.className = "background-legend-action-btn";
825
+ hideAllBtn.textContent = "Hide All";
826
+ hideAllBtn.addEventListener("click", () => this.setAllBackgroundLayersVisibility(false));
827
+ actionsRow.appendChild(showAllBtn);
828
+ actionsRow.appendChild(hideAllBtn);
829
+ const filterRow = document.createElement("div");
830
+ filterRow.className = "background-legend-filter";
831
+ const filterCheckbox = document.createElement("input");
832
+ filterCheckbox.type = "checkbox";
833
+ filterCheckbox.className = "background-legend-filter-checkbox";
834
+ filterCheckbox.id = "background-legend-only-rendered";
835
+ filterCheckbox.checked = this.state.onlyRenderedFilter;
836
+ filterCheckbox.addEventListener("change", () => {
837
+ this.state.onlyRenderedFilter = filterCheckbox.checked;
838
+ const layerList2 = panel.querySelector(".background-legend-layer-list");
839
+ if (layerList2) {
840
+ this.populateBackgroundLayerList(layerList2);
841
+ }
842
+ });
843
+ const filterLabel = document.createElement("label");
844
+ filterLabel.className = "background-legend-filter-label";
845
+ filterLabel.htmlFor = "background-legend-only-rendered";
846
+ filterLabel.textContent = "Only rendered";
847
+ filterRow.appendChild(filterCheckbox);
848
+ filterRow.appendChild(filterLabel);
849
+ const layerList = document.createElement("div");
850
+ layerList.className = "background-legend-layer-list";
851
+ this.populateBackgroundLayerList(layerList);
852
+ panel.appendChild(header);
853
+ panel.appendChild(actionsRow);
854
+ panel.appendChild(filterRow);
855
+ panel.appendChild(layerList);
856
+ return panel;
857
+ }
858
+ /**
859
+ * Check if a layer is currently rendered in the map viewport
860
+ */
861
+ isLayerRendered(layerId) {
862
+ try {
863
+ const layer = this.map.getLayer(layerId);
864
+ if (!layer) return false;
865
+ const visibility = this.map.getLayoutProperty(layerId, "visibility");
866
+ if (visibility === "none") return false;
867
+ if (layer.type === "raster" || layer.type === "hillshade") {
868
+ return true;
869
+ }
870
+ if (layer.type === "background") {
871
+ return true;
872
+ }
873
+ const features = this.map.queryRenderedFeatures({ layers: [layerId] });
874
+ return features.length > 0;
875
+ } catch (error) {
876
+ return true;
877
+ }
878
+ }
879
+ /**
880
+ * Populate the background layer list with individual layers
881
+ */
882
+ populateBackgroundLayerList(container) {
883
+ container.innerHTML = "";
884
+ const styleLayers = this.map.getStyle().layers || [];
885
+ styleLayers.forEach((layer) => {
886
+ if (!this.isUserAddedLayer(layer.id)) {
887
+ if (this.state.onlyRenderedFilter && !this.isLayerRendered(layer.id)) {
888
+ return;
889
+ }
890
+ const layerRow = document.createElement("div");
891
+ layerRow.className = "background-legend-layer-row";
892
+ layerRow.setAttribute("data-background-layer-id", layer.id);
893
+ const checkbox = document.createElement("input");
894
+ checkbox.type = "checkbox";
895
+ checkbox.className = "background-legend-checkbox";
896
+ const visibility = this.map.getLayoutProperty(layer.id, "visibility");
897
+ const isVisible = visibility !== "none";
898
+ checkbox.checked = isVisible;
899
+ this.state.backgroundLayerVisibility.set(layer.id, isVisible);
900
+ checkbox.addEventListener("change", () => {
901
+ this.toggleIndividualBackgroundLayer(layer.id, checkbox.checked);
902
+ });
903
+ const name = document.createElement("span");
904
+ name.className = "background-legend-layer-name";
905
+ name.textContent = this.generateFriendlyName(layer.id);
906
+ name.title = layer.id;
907
+ const typeIndicator = document.createElement("span");
908
+ typeIndicator.className = "background-legend-layer-type";
909
+ typeIndicator.textContent = layer.type;
910
+ layerRow.appendChild(checkbox);
911
+ layerRow.appendChild(name);
912
+ layerRow.appendChild(typeIndicator);
913
+ container.appendChild(layerRow);
914
+ }
915
+ });
916
+ if (container.children.length === 0) {
917
+ const emptyMsg = document.createElement("p");
918
+ emptyMsg.className = "background-legend-empty";
919
+ emptyMsg.textContent = this.state.onlyRenderedFilter ? "No rendered layers in current view." : "No background layers found.";
920
+ container.appendChild(emptyMsg);
921
+ }
922
+ }
923
+ /**
924
+ * Toggle visibility of an individual background layer
925
+ */
926
+ toggleIndividualBackgroundLayer(layerId, visible) {
927
+ this.state.backgroundLayerVisibility.set(layerId, visible);
928
+ this.map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
929
+ this.updateBackgroundCheckboxState();
930
+ }
931
+ /**
932
+ * Set visibility for all background layers
933
+ */
934
+ setAllBackgroundLayersVisibility(visible) {
935
+ const styleLayers = this.map.getStyle().layers || [];
936
+ styleLayers.forEach((layer) => {
937
+ if (!this.isUserAddedLayer(layer.id)) {
938
+ this.state.backgroundLayerVisibility.set(layer.id, visible);
939
+ this.map.setLayoutProperty(layer.id, "visibility", visible ? "visible" : "none");
940
+ }
941
+ });
942
+ const legendPanel = this.panel.querySelector(".layer-control-background-legend");
943
+ if (legendPanel) {
944
+ const checkboxes = legendPanel.querySelectorAll(".background-legend-checkbox");
945
+ checkboxes.forEach((checkbox) => {
946
+ checkbox.checked = visible;
947
+ });
948
+ }
949
+ this.updateBackgroundCheckboxState();
950
+ }
951
+ /**
952
+ * Update the main Background checkbox based on individual layer states
953
+ */
954
+ updateBackgroundCheckboxState() {
955
+ const styleLayers = this.map.getStyle().layers || [];
956
+ let anyVisible = false;
957
+ let allVisible = true;
958
+ styleLayers.forEach((layer) => {
959
+ if (!this.isUserAddedLayer(layer.id)) {
960
+ const visible = this.state.backgroundLayerVisibility.get(layer.id);
961
+ if (visible === true) anyVisible = true;
962
+ if (visible === false) allVisible = false;
963
+ }
964
+ });
965
+ const backgroundItem = this.panel.querySelector('[data-layer-id="Background"]');
966
+ if (backgroundItem) {
967
+ const checkbox = backgroundItem.querySelector(".layer-control-checkbox");
968
+ if (checkbox) {
969
+ checkbox.checked = anyVisible;
970
+ checkbox.indeterminate = anyVisible && !allVisible;
971
+ }
972
+ }
973
+ if (this.state.layerStates["Background"]) {
974
+ this.state.layerStates["Background"].visible = anyVisible;
975
+ }
976
+ }
707
977
  /**
708
978
  * Create style button for a layer
709
979
  */