# How to Implement a CesiumJS 3D Globe as a Secondary Map Pane in GeoLibre

> Learn to implement a CesiumJS 3D globe as a secondary map pane in GeoLibre. The CesiumCanvas component syncs layers and camera for a seamless 3D experience.

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

---

**GeoLibre supports CesiumJS 3D globes as secondary map panes through the `CesiumCanvas` component, which mirrors the same store-driven layer synchronization and camera management used by 2D panes.**

GeoLibre's multi-pane architecture allows developers to render **CesiumJS 3D globes** alongside the primary 2D MapLibre map. This is implemented through a pluggable secondary pane system where each pane's rendering engine is determined by its `viewKind` property in the shared Zustand store. According to the opengeos/GeoLibre source code, adding a 3D globe requires only a Cesium Ion token and a single store action to switch the pane type.

## Architecture Overview: How Secondary Panes Work

GeoLibre's secondary map system is built on three core components that share state through `@geolibre/core`:

- **`CesiumCanvas`** ([`packages/map/src/CesiumCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/CesiumCanvas.tsx)) — Lazily loads CesiumJS, creates a `Viewer` instance, and syncs layers via `CesiumLayerSync`
- **`SecondaryMapCanvas`** ([`packages/map/src/SecondaryMapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/SecondaryMapCanvas.tsx)) — The existing 2D MapLibre GL implementation
- **`MapGrid`** ([`apps/geolibre-desktop/src/components/layout/MapGrid.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/layout/MapGrid.tsx)) — Layout container that renders the appropriate component based on `viewKind`

Each secondary pane entry in the store contains:

| Property | Purpose |
|----------|---------|
| `id` | Unique pane identifier |
| `label` | Optional user-defined name |
| `view` | Saved camera state (`MapViewState`) |
| `layerVisibility` | Per-layer visibility overrides |
| `viewKind` | `"maplibre"` (default) or `"cesium"` |

## Cesium Token Configuration

The globe feature is gated by `getCesiumIonToken()` in `@geolibre/core`, which reads:

- `CESIUM_TOKEN` environment variable
- Runtime setting `VITE_CESIUM_TOKEN`
- Emits `geolibre:runtime-env-change` event when updated

If no token is present, the 3D toggle is hidden and panes default to 2D. In [`MapGrid.tsx`](https://github.com/opengeos/GeoLibre/blob/main/MapGrid.tsx), the `useCesiumIonToken` hook manages token reactivity:

```tsx
function useCesiumIonToken(): string | undefined {
  const [token, setToken] = useState<string | undefined>(() => getCesiumIonToken());
  useEffect(() => {
    const refresh = () => setToken(getCesiumIonToken());
    window.addEventListener("geolibre:runtime-env-change", refresh);
    return () => window.removeEventListener("geolibre:runtime-env-change", refresh);
  }, []);
  return token;
}

```

## Implementing the Cesium Canvas

### Component Lifecycle and Lazy Loading

`CesiumCanvas` creates its Cesium `Viewer` exactly once using an effect with an empty dependency array. The `viewId` is accessed through a mutable ref to preserve camera state across re-renders:

```tsx
// From packages/map/src/CesiumCanvas.tsx
const viewIdRef = useRef(viewId);
viewIdRef.current = viewId;

useEffect(() => {
  // Viewer created once; reads viewIdRef.current for store operations
  const viewer = new Cesium.Viewer(containerRef.current, {
    imageryProvider: false, // Custom imagery via layer sync
    baseLayerPicker: false,
    // ... other Cesium options
  });
  
  // Camera sync, layer sync initialization...
  
  return () => {
    viewer.destroy();
  };
}, []); // Empty deps = single initialization

```

### Camera Synchronization

The component implements bidirectional camera sync controlled by `mapLayout.syncView`:

1. **Global sync enabled** — Pane applies `mapView` store state on every change via `applyView`
2. **Global sync disabled** — Pane uses its own saved `entryView`
3. **User interaction** — Camera moves pushed back to store via `setMapView` or `setSecondaryMapView`, with echo filtering to prevent loops

### Layer Synchronization via CesiumLayerSync

`CesiumLayerSync` (from `@geolibre/map`) renders:

- GeoJSON vector data
- Raster tile layers
- 3D Tiles

All layers respect the pane's `layerVisibility` overrides, matching the 2D canvas behavior.

## Adding a 3D Globe Pane: Two Methods

### Method 1: UI Toggle in MapGrid

When a Cesium token is present, [`MapGrid.tsx`](https://github.com/opengeos/GeoLibre/blob/main/MapGrid.tsx) renders a toolbar button per pane:

```tsx
// Logic from MapGrid.tsx
const is3d = cesiumAvailable && wantsCesium;

{is3d ? (
  <CesiumCanvas key={cesiumToken} viewId={viewId} ionToken={cesiumToken} />
) : (
  <SecondaryMapCanvas viewId={viewId} />
)}

```

The `key={cesiumToken}` forces remount when tokens change. The toggle button calls `setSecondaryViewKind(viewId, "cesium")` to switch modes.

### Method 2: Programmatic Creation

Dispatch store actions directly for custom workflows:

```ts
import { useAppStore, generateUuid } from "@geolibre/core";

function addGlobePane() {
  const id = generateUuid();
  const store = useAppStore.getState();

  store.addSecondaryMapView(id);          // Creates entry with defaults
  store.setSecondaryViewKind(id, "cesium"); // Switches to 3D globe
}

```

The pane initializes immediately with the current global camera state and layer configuration.

## Custom Toggle Button Implementation

For custom UIs, implement a conditional toggle following [`MapGrid.tsx`](https://github.com/opengeos/GeoLibre/blob/main/MapGrid.tsx) patterns:

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

function GlobeToggle({ viewId }: { viewId: string }) {
  const cesiumToken = getCesiumIonToken();
  const wantsCesium = useAppStore(
    s => s.secondaryMapViews.find(p => p.id === viewId)?.viewKind === "cesium"
  );
  const setKind = useAppStore(s => s.setSecondaryViewKind);

  if (!cesiumToken) return null;

  return (
    <button
      onClick={() => setKind(viewId, wantsCesium ? "maplibre" : "cesium")}
      aria-pressed={wantsCesium}
    >
      {wantsCesium ? "Show 2D" : "Show 3D"}
    </button>
  );
}

```

## Store Persistence and Fallback Behavior

The `viewKind` field is serialized with project files. When loading a saved project:

- If `viewKind: "cesium"` and token is present → Renders 3D globe
- If `viewKind: "cesium"` and token is missing → Gracefully falls back to 2D MapLibre

This ensures projects remain functional across environments with different token configurations.

## Key Source Files Reference

| File | Role |
|------|------|
| [`packages/map/src/CesiumCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/CesiumCanvas.tsx) | Cesium viewer implementation, camera/layer sync |
| [`packages/map/src/SecondaryMapCanvas.tsx`](https://github.com/opengeos/GeoLibre/blob/main/packages/map/src/SecondaryMapCanvas.tsx) | 2D MapLibre pane reference implementation |
| [`apps/geolibre-desktop/src/components/layout/MapGrid.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/layout/MapGrid.tsx) | Pane layout, 2D/3D switching logic |
| [`packages/core/src/store.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/store.ts) | Store definitions for `secondaryMapViews`, `setSecondaryViewKind` |
| [`packages/core/src/utils/cesium.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/core/src/utils/cesium.ts) | `getCesiumIonToken()` helper |

## Summary

- **CesiumJS 3D globes** in GeoLibre are implemented via the `CesiumCanvas` component that integrates with the existing secondary pane store system
- **Token-gated access** — The `getCesiumIonToken()` helper controls feature availability; missing tokens hide 3D options automatically
- **Single store action** — `setSecondaryViewKind(id, "cesium")` switches any pane between 2D and 3D rendering
- **Synchronized state** — Camera position, layer visibility, and basemap selection propagate across all panes regardless of renderer
- **Graceful degradation** — Saved projects with 3D panes fall back to 2D when Cesium tokens are unavailable

## Frequently Asked Questions

### What Cesium Ion token is required and where do I set it?

GeoLibre requires a Cesium Ion access token for terrain, imagery, and 3D Tiles. Set it via the `CESIUM_TOKEN` environment variable at build time or `VITE_CESIUM_TOKEN` for runtime configuration. The `getCesiumIonToken()` helper in `@geolibre/core` reads both sources and emits `geolibre:runtime-env-change` when updated.

### Can multiple 3D globe panes run simultaneously?

Yes. Each `CesiumCanvas` instance is isolated by its `viewId` and maintains independent camera state when `mapLayout.syncView` is disabled. Multiple globes share the same Cesium runtime assets but create separate `Viewer` instances. Performance depends on GPU capabilities and layer complexity.

### How do I sync layers between 2D and 3D panes?

Layer synchronization is automatic through `CesiumLayerSync`, which subscribes to the same `layerVisibility` store as `SecondaryMapCanvas`. Both components apply per-pane overrides identically—visibility toggles, opacity changes, and layer reordering propagate immediately across renderers without additional code.

### What happens to my 3D panes if I share the project without a Cesium token?

The `viewKind: "cesium"` setting is preserved in the saved project file. When loaded in an environment without a token, [`MapGrid.tsx`](https://github.com/opengeos/GeoLibre/blob/main/MapGrid.tsx) evaluates `is3d` as false and renders `SecondaryMapCanvas` instead. The pane remains functional in 2D mode; switching back to 3D requires only adding a valid Cesium Ion token and toggling the view kind.