CesiumJS Integration in GeoLibre: 3D Globe and MapLibre Camera Synchronization

GeoLibre integrates CesiumJS for 3D visualization by wrapping it in a React component that lazily loads the library, converts MapLibre camera states to Cesium parameters using pure math utilities, and maintains bidirectional synchronization through a Zustand store.

GeoLibre, an open-source geospatial framework maintained by opengeos, combines 2D MapLibre maps with a full 3D CesiumJS globe through a modular React architecture. This integration enables seamless switching between map views while keeping camera positions and layer visibility synchronized across both rendering engines.

Lazy Loading and Environment Preparation

To avoid impacting 2D MapLibre startup performance, GeoLibre loads CesiumJS only when the globe pane mounts. The CesiumCanvas component in packages/map/src/CesiumCanvas.tsx uses a dynamic import (import("cesium")) inside a useEffect hook to defer loading until runtime【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L44-L58】.

Before instantiating the viewer, the component prepares the runtime environment:

  • Sets window.CESIUM_BASE_URL to point to the public /cesium folder containing Cesium assets
  • Injects the Cesium widget stylesheet programmatically to ensure styles load correctly with the deferred bundle

This approach keeps the initial bundle size small while ensuring all Cesium Web Workers and assets resolve correctly when the 3D view activates.

Viewer Creation and Initialization

Once the environment is ready, CesiumCanvas creates a single Cesium.Viewer instance inside a useEffect with an empty dependency array. The viewer is stored in a React ref (viewerRef) for persistent access across renders【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L50-L78】.

The initialization disables most default UI widgets to maintain a clean interface consistent with GeoLibre's design:

const viewer = new Cesium.Viewer(containerRef.current, {
  animation: false,
  baseLayerPicker: false,
  fullscreenButton: false,
  vrButton: false,
  geocoder: false,
  homeButton: false,
  infoBox: false,
  sceneModePicker: false,
  selectionIndicator: false,
  timeline: false,
  navigationHelpButton: false,
});

The viewer receives an optional Ion access token via getCesiumIonToken() from packages/core/src/runtime-env.ts, falling back to keyless OpenStreetMap imagery when no token is provided【/cache/repos/github.com/opengeos/GeoLibre/main/packages/core/src/runtime-env.ts#L138】.

Layer Synchronization Architecture

GeoLibre reconciles its layer state onto the Cesium globe through the CesiumLayerSync class defined in packages/map/src/cesium-layer-sync.ts. This utility translates the store's GeoLibreLayer[] array into native Cesium primitives, handling:

  • GeoJSON sources as Cesium GeoJSON data sources
  • XYZ/WMS/WMTS raster tiles as Cesium imagery layers
  • 3D Tiles tilesets for photogrammetry and city models

The synchronization runs immediately after viewer creation and updates reactively whenever the layer configuration changes【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/cesium-layer-sync.ts#L4-L12】【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L98-L104】.

Camera Conversion Mathematics

All coordinate transformations between MapLibre's MapViewState (center, zoom, bearing, pitch) and Cesium's camera (range, heading, pitch) reside in packages/map/src/cesium-camera.ts. These pure functions perform calculations without importing Cesium at runtime, making them fully unit-testable【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/cesium-camera.ts#L4-L15】【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/cesium-camera.ts#L85-L104】.

Key conversion utilities include:

  • applyMapViewToCamera – Converts MapLibre center/zoom/pitch/bearing to Cesium camera position and orientation
  • readMapViewFromCamera – Extracts MapLibre-compatible view state from a Cesium camera instance
  • mapLibrePitchToCesiumDeg – Normalizes pitch angles between the two coordinate systems
  • zoomToRange – Translates MapLibre zoom levels to Cesium camera range in meters
import { applyMapViewToCamera, readMapViewFromCamera } from "@geolibre/map/cesium-camera";
import type { Viewer } from "cesium";

// Apply MapLibre view to Cesium
function syncToGlobe(Cesium: typeof import("cesium"), viewer: Viewer, view: MapViewState) {
  applyMapViewToCamera(Cesium, viewer, view);
}

// Read Cesium camera back to MapLibre format
function captureView(Cesium: typeof import("cesium"), viewer: Viewer): MapViewState {
  return readMapViewFromCamera(Cesium, viewer);
}

Bidirectional Camera Synchronization

GeoLibre maintains a single source of truth for camera state in its Zustand store while keeping both MapLibre and Cesium views in sync through a bidirectional flow.

Store to Cesium

The CesiumCanvas component subscribes to the global mapView or pane-specific secondaryMapViews from the Zustand store via useAppStore. When the view changes and the globe is ready, the applyView function calls applyMapViewToCamera to update the Cesium camera position programmatically【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L94-L112】.

Cesium to Store

To capture user interactions like pan, zoom, or rotate on the globe, the component listens to the viewer's camera.moveEnd event. The handler reads the current camera back into a MapViewState using readMapViewFromCamera, then compares it against the last programmatic view stored in lastAppliedRef. If the view differs and passes the isSameView validation, the store updates via setMapView or setSecondaryMapView with a flag indicating whether the move was user-driven (userMovedRef)【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L49-L78】.

This pattern prevents infinite update loops while ensuring both 2D and 3D views remain synchronized regardless of which canvas the user interacts with.

Ion Token Configuration

The Cesium Ion token can be injected at build time through the VITE_CESIUM_TOKEN environment variable or overridden at runtime through the getCesiumIonToken helper. This flexibility allows deployments to switch between Ion imagery/terrain and fallback OpenStreetMap layers without rebuilding the application【/cache/repos/github.com/opengeos/GeoLibre/main/packages/core/src/runtime-env.ts#L138】.

import { CesiumCanvas } from "@geolibre/map";

function GlobePane() {
  // Runtime token injection
  const cesiumToken = process.env.CESIUM_TOKEN;
  return <CesiumCanvas viewId="global-globe" ionToken={cesiumToken} />;
}

Summary

  • Lazy loading via dynamic import("cesium") ensures the 3D globe does not impact initial 2D map load times
  • Environment setup in CesiumCanvas.tsx configures window.CESIUM_BASE_URL and injects required stylesheets
  • Layer synchronization through CesiumLayerSync reconciles GeoJSON, raster tiles, and 3D Tiles between MapLibre and Cesium
  • Pure math utilities in cesium-camera.ts convert camera states without Cesium runtime dependencies, enabling comprehensive unit testing
  • Bidirectional sync uses Zustand store subscriptions and camera.moveEnd events to maintain consistent views across both engines
  • Runtime configuration via getCesiumIonToken supports both Ion and open imagery sources without rebuilds

Frequently Asked Questions

How does GeoLibre prevent CesiumJS from slowing down the initial page load?

GeoLibre uses dynamic imports to load CesiumJS only when the CesiumCanvas component mounts. By calling import("cesium") inside a useEffect hook rather than at the top of the file, the library code splits into a separate chunk that browsers fetch only when users open the 3D globe view【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L44-L58】.

What layer types synchronize between MapLibre and the Cesium globe?

The CesiumLayerSync utility handles GeoJSON vector data, XYZ/WMS/WMTS raster tile layers, and Cesium 3D Tiles. It reads the same GeoLibreLayer[] configuration used by MapLibre and translates each layer type into its Cesium equivalent, ensuring visibility and styling remain consistent when switching between 2D and 3D modes【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/cesium-layer-sync.ts#L4-L12】.

How does the camera synchronization avoid infinite loops between MapLibre and Cesium?

The implementation uses a reference (lastAppliedRef) to track the last programmatically applied view state. When Cesium fires the camera.moveEnd event, the handler compares the new camera position against this reference. Only views that differ from the last applied state and pass the isSameView validation trigger a store update, preventing circular updates when programmatically setting the camera from MapLibre state changes【/cache/repos/github.com/opengeos/GeoLibre/main/packages/map/src/CesiumCanvas.tsx#L49-L78】.

Can GeoLibre work without a Cesium Ion token?

Yes. While GeoLibre supports Cesium Ion tokens via the VITE_CESIUM_TOKEN environment variable or runtime getCesiumIonToken configuration, it falls back to keyless OpenStreetMap imagery when no token is provided. This ensures the 3D globe remains functional in open-source deployments without requiring Ion subscriptions【/cache/repos/github.com/opengeos/GeoLibre/main/packages/core/src/runtime-env.ts#L138】.

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 →