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: syncLayers validates 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 beforeId to preserve visual stacking.
  • Type-dispatched sync: syncLayer delegates to specialized handlers for each layer type in layer-sync.ts.
  • Defensive initialization: waitAndSyncLayers ensures synchronization only after the style loads.
  • Complete state tracking: Internal layerIds and syncedLayers enable 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →