How `MapController.syncLayers` Synchronizes the Store with MapLibre in GeoLibre
MapController.syncLayers performs a deterministic reconciliation between the Zustand store's layer list and the MapLibre GL map by removing stale layers, then iteratively adding or updating remaining layers bottom-up while preserving order.
GeoLibre manages application state through a Zustand store, where the layers array serves as the single source of truth. The MapController in packages/map/src/map-controller.ts bridges this reactive state to the imperative MapLibre API. When syncLayers executes, it transforms the store's declarative layer configuration into concrete MapLibre style layers with full preservation of ordering, visibility, and type-specific properties.
The Six-Stage Synchronization Pipeline
Guard Clauses Prevent Premature Execution
syncLayers begins with defensive checks to ensure the map is ready to accept modifications.
// Line 73 in packages/map/src/map-controller.ts
if (!this.map || !this.map.isStyleLoaded()) {
return;
}
These guards protect against race conditions during map initialization or style transitions. If the style isn't loaded or the map instance is missing, the method exits early without side effects.
Identify and Remove Stale Layers
The controller maintains an internal layerIds array tracking which MapLibre layers currently exist. It computes the difference between this set and the store's incoming layer list.
// Lines 78-85 in map-controller.ts
const currentIds = this.layerIds;
const newIds = layers.map(l => l.id);
const removedIds = currentIds.filter(id => !newIds.includes(id));
removedIds.forEach(id => this.removeLayerFromMap(id));
Each removed layer is detached via removeLayerFromMap, which cleans up associated resources, sources, and event handlers.
Bottom-Up Layer Processing with Ordering
The method walks the store's layers from last to first (bottom to top), ensuring the visual stacking order matches the array order.
// Lines 99-101 in map-controller.ts
for (let i = layers.length - 1; i >= 0; i--) {
const layer = layers[i];
const beforeId = i < layers.length - 1 ? layers[i + 1].id : undefined;
this.syncLayer(layer, beforeId);
}
The beforeId parameter passed to syncLayer guarantees that each layer renders beneath the one that follows it in the store array.
Type-Specific Synchronization via syncLayer
The core dispatch logic lives in packages/map/src/layer-sync.ts starting at line 56. The syncLayer function branches by layer.type:
| Layer Type | Handler Function | Key Operations |
|---|---|---|
| GeoJSON | syncGeoJSONLayer |
Add/update source, apply clustering, set data |
| Raster | syncRasterLayer |
Configure tiles, bounds, scheme, tile size |
| Vector (MVT) | syncVectorLayer |
Attach PMTiles protocol, style layers |
| MBTiles | syncMBTilesLayer |
Register custom protocol, extract metadata |
| Video | syncVideoLayer |
Coordinates, autoplay, loop settings |
| Image | syncImageLayer |
Bounds-based image overlay |
| Native | syncNativeLayer |
Pass-through to MapLibre with minimal wrapping |
Each handler manages:
- Source creation or updates
- Style layer generation with filters and paint properties
- Visibility and opacity application
- Min/max zoom range constraints
- Custom layer bridge registration for advanced use cases
Post-Sync State Bookkeeping
After the layer loop completes, syncLayers updates the controller's internal tracking and triggers UI refreshes.
// Lines 102-109 in map-controller.ts
this.layerIds = layers.map(l => l.id);
this.syncedLayers = new Map(layers.map(l => [l.id, l]));
this.applyBasemapSettings();
this.refreshLayerControl();
this.publishDisplayNames();
This ensures subsequent diffs compare against the correct baseline and that derived UI state stays consistent.
Initialization: waitAndSyncLayers and Style Loading
The waitAndSyncLayers method (lines 13-33) solves a critical timing problem: MapLibre cannot accept layers until its style has loaded.
// From map-controller.ts
public waitAndSyncLayers(layers: Layer[]): void {
if (this.map.isStyleLoaded()) {
this.syncLayers(layers);
} else {
const handler = () => {
this.syncLayers(layers);
this.map.off('style.load', handler);
this.map.off('load', handler);
};
this.map.once('style.load', handler);
this.map.once('load', handler); // Fallback for older MapLibre versions
}
}
This one-time listener registers both style.load and load events as fallbacks, guaranteeing synchronization occurs exactly once when the map becomes ready.
Practical Usage Examples
Automatic Store Subscription
import { useMapStore } from '@geolibre/core';
const mapController = new MapController(map);
// Subscribe to layer changes and auto-sync
useMapStore.subscribe(
(state) => state.layers,
(layers) => mapController.syncLayers(layers),
);
Manual Sync After Project Load
// After restoring a saved project
const layers = useMapStore.getState().layers;
mapController.waitAndSyncLayers(layers); // Handles style-not-ready case
Forcing a Complete Refresh
// Useful after style changes that might invalidate layer sources
const freshLayers = store.getState().layers;
mapController.syncLayers(freshLayers);
Source Files and Architecture
| File | Purpose | Key Exports |
|---|---|---|
packages/map/src/map-controller.ts |
Orchestration | MapController, syncLayers, waitAndSyncLayers |
packages/map/src/layer-sync.ts |
Per-type implementations | syncLayer, syncGeoJSONLayer, syncRasterLayer, etc. |
packages/core/src/store.ts |
State container | useMapStore, layers array definition |
tests/map-controller.test.ts |
Verification suite | Removal, ordering, visibility, time-filter tests |
Summary
- Guarded execution:
syncLayersvalidates map readiness before any MapLibre calls. - Deterministic diffing: Removed layers are identified by set difference and purged first.
- Bottom-up ordering: The layer loop runs in reverse array order with
beforeIdto preserve visual stacking. - Type-dispatched sync:
syncLayerdelegates to specialized handlers for each layer type inlayer-sync.ts. - Defensive initialization:
waitAndSyncLayersensures synchronization only after the style loads. - Complete state tracking: Internal
layerIdsandsyncedLayersenable efficient future diffs.
Frequently Asked Questions
What triggers syncLayers to run?
The MapController typically subscribes to the Zustand store's layers array. Any mutation—adding, removing, reordering, or updating layer properties—causes the subscription callback to invoke syncLayers with the new array.
Why does syncLayers process layers bottom-up?
Processing from the last array index to the first allows each layer to specify the next layer as its beforeId. This approach leverages MapLibre's beforeId parameter to achieve deterministic z-order without removing and re-adding all layers.
What happens if the map style isn't loaded yet?
The guard clause at line 73 returns early. For initialization scenarios, waitAndSyncLayers defers execution until the style.load or load event fires, preventing errors from premature layer operations.
How are custom protocols like PMTiles handled?
The type-specific sync functions in layer-sync.ts register protocols during layer creation. For example, syncVectorLayer attaches the PMTiles protocol handler before adding the source, enabling on-the-fly tile decoding without external preprocessing.
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 →