How Layer Groups and Visibility Controls Are Managed in GeoLibre
GeoLibre manages layer groups and visibility controls through a flat data model where layers reference groups via groupId, while utility functions transform this structure into a hierarchical tree and compute effective visibility by combining layer and group states.
GeoLibre is an open-source geospatial visualization library built on top of MapLibre GL JS. Understanding how layer groups and visibility controls are managed in this framework requires examining the interaction between the Zustand-based state management, the core layer-groups utilities, and the rendering logic that synchronizes the UI with the map.
The Flat Data Model and Group References
GeoLibre stores all layers in a flat array inside the Zustand store rather than a nested structure. Each layer can optionally belong to a group through a groupId property, while groups themselves are maintained as separate folder-like containers. This design choice preserves reference equality for memoization and simplifies drag-and-drop operations.
When a layer lacks a groupId or references a dangling group, it renders independently. Otherwise, it inherits behavioral traits from its parent group through computed properties rather than direct mutation.
Building the Hierarchical Layer Tree
Constructing UI Views with buildLayerTree
The transformation from flat storage to hierarchical UI representation occurs in packages/core/src/layer-groups.ts. The buildLayerTree(layers, groups) function walks the layer list in reverse order (to match the UI's top-first display convention) and buckets layers by their groupId.
The function emits a LayerTreeItem for each top-level layer or for each group header containing its children. Empty groups are intentionally added at the very top of the output to ensure they can receive drag-and-drop operations. This separation between storage and presentation allows the data layer to remain simple while supporting complex UI interactions.
Cascading Group Effects to Child Layers
Computing Effective Visibility and Opacity
Before syncing to MapLibre-GL, the applyGroupEffects(layers, groups) utility (also in packages/core/src/layer-groups.ts) computes each child layer's effective state:
- Visibility: Determined as
layer.visible && group.visible - Opacity: Calculated as
layer.opacity * group.opacity
If a layer has no group or references a non-existent group, the function returns the layer unchanged to preserve reference equality for React memoization. This approach ensures that toggling a group automatically affects all members without modifying individual layer records in the store.
Maintaining Group Contiguity
Normalizing Array Order with normalizeGroupContiguity
When users drag layers into groups, the normalizeGroupContiguity(layers) function reorders the flat array so that all members of each group occupy a single contiguous block. This invariant is strictly required by the rest of the codebase for rendering optimizations and proper z-index management.
The function mutates the array order while preserving the layer objects themselves, ensuring that group membership remains logically consistent with the visual stacking order.
UI Controls and User Interaction
The Layers Panel Implementation
The Layers panel UI consumes the LayerTreeItem objects produced by buildLayerTree. Each row displays:
- An eye-icon for toggling visibility
- An opacity slider for transparency control
- A collapse/expand toggle for groups
The "Hide all layers" button iterates over the store and sets visible: false on every layer object, while group visibility is stored on the LayerGroup object itself and cascaded through applyGroupEffects.
Built-in Map Control Plugin
Separate from the core visibility logic, the built-in MapLibre layer control is implemented in packages/plugins/src/plugins/layer-control.ts. This plugin exposes setBuiltInMapControlVisible() and setBuiltInMapControlPosition() for showing, hiding, and repositioning the UI widget.
Crucially, this plugin's state does not affect the per-layer visibility logic described above—it solely controls the toggleable UI element that lets users interact with the layer list directly on the map canvas.
Practical Implementation Examples
// Build the UI tree from store data
import { buildLayerTree } from "@geolibre/core/src/layer-groups";
import type { GeoLibreLayer, LayerGroup } from "@geolibre/core/src/types";
const tree = buildLayerTree(appStore.layers, appStore.layerGroups);
// Returns: [{kind:"layer", ...}, {kind:"group", children: [...]}, ...]
// Apply group visibility/opacity before syncing to MapLibre
import { applyGroupEffects } from "@geolibre/core/src/layer-groups";
const syncedLayers = applyGroupEffects(appStore.layers, appStore.layerGroups);
// Each layer now has effective `visible` and `opacity` baked in
// Reorder layers after moving a layer into a group
import { normalizeGroupContiguity } from "@geolibre/core/src/layer-groups";
const reordered = normalizeGroupContiguity(appStore.layers);
appStore.setLayers(reordered);
Summary
- GeoLibre uses a flat array data model where layers reference groups via
groupId, stored in a Zustand state container. - The
buildLayerTreefunction inpackages/core/src/layer-groups.tstransforms flat data into hierarchical UI representations, placing empty groups at the top to support drag-and-drop. applyGroupEffectscomputes effective visibility and opacity by combining layer values with parent group states, preserving reference equality for memoization.normalizeGroupContiguitymaintains the invariant that group members occupy contiguous blocks in the array after reordering operations.- The Layers panel consumes tree structures to render controls, while the layer-control plugin provides an optional MapLibre widget for external UI access.
Frequently Asked Questions
How does GeoLibre store layer groups internally?
GeoLibre stores layers and groups as separate entities in a Zustand store. Layers exist in a flat array and optionally reference a group through a groupId string property. Groups themselves are distinct objects containing metadata like name, visibility, and opacity, but the hierarchical relationship is computed on-demand rather than stored as nested data.
What happens to child layer visibility when a group is toggled off?
When a group is toggled off, the applyGroupEffects function computes each child layer's effective visibility as the logical AND of layer.visible && group.visible. The child layers retain their individual visibility values in the store, but the computed result passed to MapLibre shows them as hidden. This preserves user preferences for when the group is toggled back on.
Why does GeoLibre require contiguous group members in the array?
The normalizeGroupContiguity function ensures all members of a group occupy consecutive positions in the layers array. This architectural requirement supports rendering optimizations and correct z-index management, as MapLibre renders layers in array order. Without contiguity, visual stacking would break when group-level opacity or visibility effects are applied.
How does the built-in layer control differ from the main Layers panel?
The built-in layer control in packages/plugins/src/plugins/layer-control.ts is a MapLibre plugin that provides a collapsible UI widget on the map canvas. Unlike the main Layers panel, it does not participate in the core visibility state management—it only offers an alternative interface for toggling layers. The core visibility logic remains independent, handled by the store and applyGroupEffects regardless of which UI element the user interacts with.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →