How the CesiumJS 3D Globe Integrates with the 2D MapLibre Map

GeoLibre synchronizes its 2D MapLibre map and 3D CesiumJS globe through a centralized Zustand store, using CesiumLayerSync to reconcile layers and pure-function camera converters to maintain bidirectional view consistency.

GeoLibre is an open-source geospatial framework maintained by opengeos that renders both a 2D MapLibre map and an optional 3D CesiumJS globe within the same application. The CesiumJS 3D globe integrates with the 2D MapLibre map through a store-driven architecture that treats both views as parallel renderers of shared state. This design ensures that layer additions, style changes, and camera movements remain perfectly synchronized across dimensions.

Store-Driven Architecture for Dual-View Synchronization

The integration relies on a single source of truth defined in @geolibre/core. The Zustand store holds the canonical list of layers (GeoLibreLayer[]) and the current view state (MapViewState), including center coordinates, zoom level, bearing, and pitch.

Both the 2D and 3D views subscribe to this store but render it differently:

When a user adds, edits, or removes a layer, the store updates trigger both sync handlers simultaneously, keeping the 2D and 3D views consistent.

Layer Synchronization with CesiumLayerSync

The CesiumLayerSync class handles the creation, rebuilding, styling, and ordering of Cesium objects that correspond to supported layer types: raster/XYZ/WMS/WMTS, GeoJSON, and 3D Tiles.

During each sync cycle, the class performs three checks via helper functions:

  • isCesiumSupportedLayerType – Determines if the layer type can be rendered on the globe.
  • needsRebuild – Detects if the source or style has changed, requiring destruction and recreation of the Cesium object.
  • applyAppearance / applyGeoJsonStyle – Updates visibility, opacity, and vector styling without rebuilding the entire layer.

This granular approach minimizes performance overhead when only visual properties change.

Camera Coordination Between MapLibre and Cesium

View synchronization requires translating between MapLibre's Web-Mercator zoom model and Cesium's camera model (range, heading, pitch). The packages/map/src/cesium-camera.ts module exports pure functions that perform these conversions using ground resolution math, enabling unit testing without importing Cesium into the test build graph.

applyMapViewToCamera converts MapLibre's MapViewState (zoom + nadir-referenced pitch) into Cesium camera parameters (range + horizon-referenced pitch). This ensures the ground resolution matches across both canvases, even if their pixel heights differ.

readMapViewFromCamera performs the inverse operation, reading the Cesium camera's current position and orientation to produce a MapViewState object that can be written back to the store.

isSameView prevents feedback loops by checking if the new camera-derived view differs significantly from the current store state before triggering an update.

Implementation Examples

Initializing the Cesium Synchronization

import { Cesium } from "cesium";
import { CesiumLayerSync } from "@geolibre/map";
import { Viewer } from "cesium";

// Assume a Cesium Viewer has been created elsewhere
const viewer: Viewer = new Viewer("cesiumContainer");

// Create the sync helper – inject the Cesium namespace and the viewer
const cesiumSync = new CesiumLayerSync(Cesium, viewer);

Syncing Layers on Store Updates

import { useStore } from "@geolibre/core";

// Subscribe to the store's layer list
useStore.subscribe(
  (state) => state.layers,
  (layers) => {
    // Reconcile the globe with the current layer array
    cesiumSync.sync(layers);
  },
);

Applying MapLibre View to Cesium Camera

import { applyMapViewToCamera } from "@geolibre/map/cesium-camera";
import { useStore } from "@geolibre/core";

// When the view changes (e.g., user zooms), update the globe camera
useStore.subscribe(
  (state) => state.mapView,
  (view) => {
    applyMapViewToCamera(Cesium, viewer, view);
  },
);

Reading Cesium Camera Back to Store

import { readMapViewFromCamera, isSameView } from "@geolibre/map/cesium-camera";
import { setMapView } from "@geolibre/core";

// Listen to Cesium's moveEnd event
viewer.camera.moveEnd.addEventListener(() => {
  const newView = readMapViewFromCamera(Cesium, viewer);
  const currentView = useStore.getState().mapView;

  // Avoid feedback loops by checking for near-identical views
  if (!isSameView(currentView, newView)) {
    setMapView(newView);
  }
});

Summary

  • Shared State: Both views read from a single Zustand store in @geolibre/core containing layers and MapViewState.
  • Layer Sync: CesiumLayerSync in packages/map/src/cesium-layer-sync.ts mirrors store layers onto Cesium objects, handling raster, GeoJSON, and 3D Tiles.
  • Camera Math: Pure functions in packages/map/src/cesium-camera.ts convert between MapLibre zoom/pitch and Cesium range/pitch to maintain identical ground resolution.
  • Bidirectional Flow: Map interactions update the store, which updates Cesium; Cesium camera moves read back to the store via readMapViewFromCamera, updating MapLibre.
  • Performance: Granular checks like needsRebuild and isSameView prevent unnecessary recreation of layers and camera feedback loops.

Frequently Asked Questions

Which layer types does the CesiumJS globe support?

The integration supports raster/XYZ tiles, WMS/WMTS services, GeoJSON vectors, and 3D Tiles tilesets. The isCesiumSupportedLayerType function in packages/map/src/cesium-layer-sync.ts filters the store's layer list to only these compatible types before creating corresponding Cesium ImageryLayer, GeoJsonDataSource, or Cesium3DTileset instances.

How does GeoLibre prevent infinite loops between the 2D and 3D views?

The system uses the isSameView utility from packages/map/src/cesium-camera.ts to compare the current store state against newly derived camera positions. When the Cesium camera stops moving, readMapViewFromCamera generates a candidate view, but the store only updates if isSameView returns false, breaking potential feedback cycles before they begin.

Why are the camera conversion functions implemented as pure functions?

The camera conversion logic in packages/map/src/cesium-camera.ts uses pure functions (without side effects or Cesium dependencies) to calculate ground resolution, range-to-zoom ratios, and pitch conversions. This design allows the core synchronization math to be unit-tested in isolation without pulling the heavy Cesium library into the test build graph, improving CI performance and reliability.

Can the 3D globe be used without the 2D MapLibre map?

Yes. Because the architecture is store-driven, the CesiumJS globe operates as an independent consumer of the centralized state. If the MapLibre map component is not mounted, CesiumLayerSync continues to function by reading directly from the Zustand store, making the 3D view viable as a standalone renderer for applications that require only a globe visualization.

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 →