# How to Implement a CesiumJS 3D Globe as a Secondary Map Pane with Camera Sync

> Learn how to sync a CesiumJS 3D globe with 2D map panes using a shared Zustand store for bidirectional camera control. Mirror views in real-time across rendering engines.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-02

---

**GeoLibre synchronizes a CesiumJS 3D globe with 2D map panes through a shared Zustand store that coordinates camera state bi-directionally, allowing real-time view mirroring across mixed rendering engines.**

The opengeos/GeoLibre architecture treats every visualization as a thin view component that reads from a centralized state store in `@geolibre/core`. When implementing a **CesiumJS 3D globe as a secondary map pane**, you leverage the same reactive patterns used for 2D MapLibre canvases, ensuring camera movements and layer visibility stay synchronized across all panes.

## Defining the Secondary Map View Schema

Before rendering a 3D globe, you must declare the pane configuration in the shared store. In [`packages/core/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/types.ts), the `SecondaryMapView` interface supports both 2D and 3D rendering engines through a discriminated union on the `viewKind` property.

```typescript
// packages/core/src/types.ts
export interface SecondaryMapView {
  id: string;
  view?: MapViewState;  // Saved camera position
  layerVisibility?: Record<string, boolean>;  // Per-pane overrides
  viewKind?: "cesium" | "maplibre";  // Determines component selection
}

```

The `secondaryMapViews` array in your project JSON (or Zustand store) holds these definitions. When `viewKind` is `"cesium"`, the layout grid automatically mounts the `CesiumCanvas` component instead of the standard `SecondaryMapCanvas`.

## Preparing the Cesium Environment

CesiumJS requires specific asset loading configuration before the viewer initializes. The `prepareCesiumEnvironment()` function in [`packages/map/src/CesiumCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/CesiumCanvas.tsx) sets `window.CESIUM_BASE_URL` and injects the widget stylesheet so the engine can locate its Web Workers and assets.

```typescript
// packages/map/src/CesiumCanvas.tsx
function prepareCesiumEnvironment() {
  window.CESIUM_BASE_URL = "/cesium";
  // Stylesheet injection logic ensures proper widget rendering
}

```

Call this setup routine before the dynamic import to prevent 404 errors on Cesium's static resources.

## Initializing the Cesium Canvas Component

The `CesiumCanvas` component uses a lazy dynamic import to avoid bundling Cesium until needed. A `useEffect` hook with an empty dependency list guarantees a single `Viewer` instance per pane, preventing memory leaks and duplicate globe initializations.

```tsx
// packages/map/src/CesiumCanvas.tsx
export const CesiumCanvas = memo(function CesiumCanvas({ 
  viewId, 
  ionToken 
}: { 
  viewId: string; 
  ionToken?: string; 
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewerRef = useRef<any>(null);
  
  useEffect(() => {
    prepareCesiumEnvironment();
    
    const init = async () => {
      const Cesium = await import("cesium");
      if (!containerRef.current) return;
      
      viewerRef.current = new Cesium.Viewer(containerRef.current, {
        terrainProvider: Cesium.createWorldTerrain(),
        shouldAnimate: false
      });
      
      // Layer sync and camera setup follows...
    };
    
    init();
    
    // Cleanup on unmount
    return () => {
      viewerRef.current?.destroy();
    };
  }, []); // Empty array ensures single instance
  
  return <div ref={containerRef} className="cesium-container" />;
});

```

## Synchronizing Camera State Globally

Camera synchronization operates in two modes controlled by `mapLayout.syncView`. When **global sync** is enabled, the pane reads the shared `mapView` (global view) from the store and applies it to the Cesium camera using `applyView`. When disabled, it uses its own saved `entryView`.

The global sync effect in [`CesiumCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/CesiumCanvas.tsx) (lines 106-112) watches the store and updates the camera programmatically:

```typescript
// Global sync: react to shared store changes
useEffect(() => {
  if (!viewerRef.current || !syncView) return;
  
  const view = globalView; // From Zustand store
  applyMapViewToCamera(viewerRef.current.camera, view);
}, [globalView, syncView]);

```

The local sync effect (lines 123-130) handles independent pane state when synchronization is toggled off:

```typescript
// Local sync: use pane-specific saved view
useEffect(() => {
  if (!viewerRef.current || syncView || !entryView) return;
  
  applyMapViewToCamera(viewerRef.current.camera, entryView);
}, [entryView, syncView]);

```

Both effects rely on conversion utilities in [`packages/map/src/cesium-camera.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/cesium-camera.ts) to translate between GeoLibre's `MapViewState` and Cesium's `Camera` orientation.

## Mirroring User Navigation Back to the Store

Bi-directional sync requires capturing manual camera movements and writing them back to the store. The `camera.moveEnd` event listener (lines 250-275 in [`CesiumCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/CesiumCanvas.tsx)) compares the current camera state against the last programmatic view to filter out echo updates from terrain streaming or window resizes.

```typescript
// Inside the mount effect
viewerRef.current.camera.moveEnd.addEventListener(() => {
  if (userMovedRef.current === false) return; // Skip programmatic moves
  
  const currentView = readMapViewFromCamera(viewerRef.current.camera);
  
  if (syncView) {
    setMapView(currentView); // Update global view
  } else {
    setSecondaryMapView(viewId, currentView); // Update local pane view
  }
});

```

The `userMovedRef` boolean distinguishes genuine user interaction from autonomous camera settles, preventing sync loops that could jerk the view.

## Syncing Layers to the 3D Globe

Layer visibility propagates to the Cesium globe through the `CesiumLayerSync` class imported from [`packages/map/src/cesium-layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/cesium-layer-sync.ts). This helper translates `GeoLibreLayer` objects into Cesium-native sources including GeoJSON, XYZ imagery, and 3D Tiles.

```typescript
// Inside CesiumCanvas.tsx mount effect (lines 46-48)
const layerSyncRef = useRef(new CesiumLayerSync(viewerRef.current));

// Sync whenever layer list changes (lines 98-104)
useEffect(() => {
  if (!layerSyncRef.current) return;
  layerSyncRef.current.sync(paneLayers); // Respects visibility overrides
}, [paneLayers]);

```

The `paneLayers` variable combines the global layer list with any per-pane visibility overrides stored in `secondaryMapViews[id].layerVisibility`.

## Complete Project Configuration Example

To add a Cesium pane to your GeoLibre project, define the view in your project configuration and ensure the grid layout can access the component:

```json
/* .geolibre.json project configuration */
{
  "mapLayout": { 
    "rows": 2, 
    "cols": 2, 
    "syncView": true 
  },
  "secondaryMapViews": [
    {
      "id": "cesium-globe",
      "viewKind": "cesium",
      "view": { 
        "center": [0, 0], 
        "zoom": 2, 
        "bearing": 0, 
        "pitch": 45 
      },
      "layerVisibility": {
        "satellite-layer": true,
        "terrain-layer": true
      }
    }
  ]
}

```

Toggle camera synchronization at runtime using the store's `setMapLayoutSyncView` action:

```tsx
import { useAppStore } from "@geolibre/core";

function SyncToggle() {
  const sync = useAppStore(s => s.mapLayout.syncView);
  const setSync = useAppStore.getState().setMapLayoutSyncView;
  
  return (
    <button onClick={() => setSync(!sync)}>
      {sync ? "Unsync Panes" : "Sync Panes"}
    </button>
  );
}

```

## Summary

Implementing a **CesiumJS 3D globe as a secondary map pane** in GeoLibre requires coordinating state across three architectural layers:

- **Store Definition**: Use `SecondaryMapView` with `viewKind: "cesium"` to declare 3D panes in [`packages/core/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/types.ts)
- **Environment Setup**: Call `prepareCesiumEnvironment()` before dynamically importing Cesium to resolve asset paths
- **Camera Synchronization**: Apply views using `applyMapViewToCamera` from [`packages/map/src/cesium-camera.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/cesium-camera.ts), with separate effects handling global and local sync modes
- **Bi-Directional Updates**: Write user navigation back to the store via `camera.moveEnd` listeners that respect the `userMovedRef` guard
- **Layer Management**: Instantiate `CesiumLayerSync` to translate GeoLibre layers into Cesium primitives while honoring per-pane visibility overrides

## Frequently Asked Questions

### How does GeoLibre prevent infinite camera sync loops between panes?

The system uses a `userMovedRef` boolean flag to distinguish between programmatic camera updates (which should not trigger store writes) and genuine user interaction. When `applyMapViewToCamera` updates the camera programmatically, `userMovedRef` is set to `false`, causing the `camera.moveEnd` listener to skip the reciprocal store update. Only physical mouse or touch navigation sets `userMovedRef` to `true`, ensuring one-way propagation during synchronized moves.

### Can I display multiple Cesium 3D globes simultaneously?

Yes. Each `CesiumCanvas` component mounts an independent Cesium `Viewer` instance within its own DOM container. Because each pane maintains its own `viewId` and references the shared store individually, you can configure multiple entries in `secondaryMapViews` with `viewKind: "cesium"`. Each globe syncs to the global camera state or maintains independent views based on the `syncView` layout setting.

### What geospatial data formats does the Cesium secondary pane support?

According to the `CesiumLayerSync` implementation in [`packages/map/src/cesium-layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/cesium-layer-sync.ts), the 3D pane supports GeoJSON vectors, XYZ raster tiles, and 3D Tiles for mesh and point cloud data. The sync helper automatically converts GeoLibre layer definitions into Cesium `GeoJsonDataSource`, `UrlTemplateImageryProvider`, or `Cesium3DTileset` instances, applying per-pane visibility filters from the store.

### How do I disable camera synchronization for only the Cesium pane while keeping 2D panes synced?

Set `mapLayout.syncView` to `true` for the 2D panes, but store an independent `view` in the specific `secondaryMapViews` entry for the Cesium pane. When the global sync is enabled, the Cesium component checks for a local `entryView` override. Alternatively, modify the sync logic in [`CesiumCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/CesiumCanvas.tsx) to ignore `globalView` when a specific flag is set, though the default architecture requires toggling sync globally or managing the pane's view state independently when sync is disabled.