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:
CesiumCanvas(packages/map/src/CesiumCanvas.tsx) — Lazily loads CesiumJS, creates aViewerinstance, and syncs layers viaCesiumLayerSyncSecondaryMapCanvas(packages/map/src/SecondaryMapCanvas.tsx) — The existing 2D MapLibre GL implementationMapGrid(apps/geolibre-desktop/src/components/layout/MapGrid.tsx) — Layout container that renders the appropriate component based onviewKind
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_TOKENenvironment variable- Runtime setting
VITE_CESIUM_TOKEN - Emits
geolibre:runtime-env-changeevent 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:
- Global sync enabled — Pane applies
mapViewstore state on every change viaapplyView - Global sync disabled — Pane uses its own saved
entryView - User interaction — Camera moves pushed back to store via
setMapVieworsetSecondaryMapView, 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
CesiumCanvascomponent 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →