# How `MapController.syncLayers` Synchronizes the Store with MapLibre in GeoLibre

> Discover how MapController.syncLayers synchronizes your GeoLibre store with MapLibre GL. Learn about deterministic reconciliation for efficient layer management.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-08-01

---

**`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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
// 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.

```typescript
// 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

```typescript
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

```typescript
// After restoring a saved project
const layers = useMapStore.getState().layers;
mapController.waitAndSyncLayers(layers); // Handles style-not-ready case

```

### Forcing a Complete Refresh

```typescript
// 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`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/map-controller.ts) | Orchestration | `MapController`, `syncLayers`, `waitAndSyncLayers` |
| [`packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/layer-sync.ts) | Per-type implementations | `syncLayer`, `syncGeoJSONLayer`, `syncRasterLayer`, etc. |
| [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) | State container | `useMapStore`, `layers` array definition |
| [`tests/map-controller.test.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.