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

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:

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, the useCesiumIonToken hook manages token reactivity:

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:

// 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 renders a toolbar button per pane:

// 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:

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 patterns:

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 Cesium viewer implementation, camera/layer sync
packages/map/src/SecondaryMapCanvas.tsx 2D MapLibre pane reference implementation
apps/geolibre-desktop/src/components/layout/MapGrid.tsx Pane layout, 2D/3D switching logic
packages/core/src/store.ts Store definitions for secondaryMapViews, setSecondaryViewKind
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 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.

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 →