How GeoLibre Implements Deck.gl Integration for Advanced WebGL Map Visualizations
GeoLibre implements deck.gl integration through a singleton MapboxOverlay that interleaves WebGL-accelerated visualizations with MapLibre GL JS, enabling high-performance scatter plots, 3D tiles, and heatmaps while maintaining synchronized view states and deterministic draw ordering.
The open-source GeoLibre project (opengeos/GeoLibre) combines MapLibre GL JS for raster and vector basemaps with deck.gl for advanced data visualization. This deck.gl integration architecture deliberately uses a single shared instance to render all specialized layers—from hexagon heatmaps to 3D scatter plots—ensuring minimal resource overhead and correct z-ordering relative to base map features.
Architecture: The Singleton Shared Overlay
The MapboxOverlay Instance
At the core of GeoLibre's deck.gl integration is the shared overlay pattern implemented in packages/plugins/src/plugins/shared-deck-overlay.ts. Rather than creating multiple deck.gl instances that compete for WebGL resources, the system maintains one MapboxOverlay attached to map.__deck.
When the first deckgl-viz layer activates, the overlay lazily resolves deck.gl modules via app.getDeckGL() and constructs a single MapboxOverlay instance. All subsequent producers—including the deck-viz plugin, Google 3D-Tiles integration, and COG raster overlays—contribute layers through setSharedDeckLayers(producerId, layers).
Layer Ordering Strategy
The overlay merges layer contributions according to the SOURCE_DRAW_ORDER constant defined at line 35 of shared-deck-overlay.ts:
const SOURCE_DRAW_ORDER = [
"raster",
"google-3d-tiles",
"deckviz",
"route-anim"
];
This ordering ensures raster basemaps render first, followed by 3D tilesets, then deck.gl visualizations, maintaining correct depth buffering and visual hierarchy between WebGL contexts.
Layer Routing and Store Integration
The Layer Sync Router
The layer synchronization system in packages/map/src/layer-sync.ts inspects each layer's type and metadata properties. When encountering the deckgl-viz source kind, it bypasses native MapLibre layer creation and instead forwards the deck.gl layer object to the shared overlay.
Store Schema and Type Safety
Layer definitions reside in the Zustand store under the deckgl-viz source kind, defined in packages/plugins/src/plugins/deckgl-viz/store-layer.ts. This separation isolates UI state from rendering logic, allowing the store to hold complex deck.gl layer configurations while the overlay handles WebGL execution.
Activating Deck.gl Visualizations
Plugin Activation Lifecycle
The deck-viz plugin entry point in packages/plugins/src/plugins/maplibre-deckgl-viz.ts exports activateDeckViz and deactivateDeckViz functions. Activation registers a UI dialog that collects layer parameters and dispatches deck.gl layer objects to the store via store.layers.setSharedDeckLayers.
The overlay wrapper in packages/plugins/src/plugins/deckgl-viz/overlay.ts manages the lifecycle of the MapboxOverlay, handling error boundaries and cleanup when producers detach. This ensures that removing one deck.gl layer from the UI doesn't destroy the shared context needed by other visualizations.
Third-Party Plugin Extensibility
API Surface and Module Resolution
GeoLibre exposes deck.gl functionality to external plugins through the GeoLibreAppAPI interface defined in packages/plugins/src/types.ts (lines 288-304). The method getDeckGL() returns resolved deck.gl modules (@deck.gl/core, @deck.gl/layers, etc.), allowing plugins to instantiate layers without bundling their own deck.gl copy.
This design prevents the "duplicate luma.gl" runtime error (see lines 587-591 in types.ts) that occurs when multiple deck.gl instances compete for the same WebGL context. Plugins contribute layers using setSharedDeckLayers with a unique producer identifier:
// Example plugin registration
export async function register(api: GeoLibreAppAPI) {
const deckGL = await api.getDeckGL();
const layer = new deckGL.layers.ScatterplotLayer({
id: 'plugin-scatter',
data: [{ position: [-122.45, 37.78] }],
getPosition: d => d.position,
getRadius: 100,
getFillColor: [255, 0, 0]
});
await api.setSharedDeckLayers('my-plugin-id', [layer]);
}
View State Synchronization and Constraints
The @deck.gl/mapbox MapboxOverlay automatically synchronizes with MapLibre's view state, listening to move, zoom, bearing, and pitch events. This ensures that deck.gl visualizations remain geographically aligned with the basemap during user interactions.
However, the integration enforces Mercator projection only. As implemented in packages/plugins/src/types.ts (lines 586-608), the shared overlay configures deck.gl with "mercator" view state because deck.gl's tiled rendering does not support MapLibre's Globe projection mode. Attempting to use deck.gl layers while the map is in Globe view will result in rendering artifacts.
Implementation Examples
Adding a ScatterplotLayer
To add a deck.gl visualization from within the GeoLibre ecosystem, resolve the shared deck instance and contribute layers to the overlay:
import { useGeoLibreApp } from '@geolibre/core';
import { ScatterplotLayer } from '@deck.gl/layers';
export function AddScatterplot() {
const app = useGeoLibreApp();
const addLayer = async () => {
const deck = await app.getDeckGL();
const layer = new deck.layers.ScatterplotLayer({
id: 'my-scatter',
data: [{ position: [-122.45, 37.78], weight: 10 }],
getPosition: d => d.position,
getRadius: d => 100,
getFillColor: [255, 0, 0, 180],
});
await setSharedDeckLayers('my-plugin', [layer as never]);
};
return <button onClick={addLayer}>Add Scatterplot</button>;
}
Creating Hexagon Aggregation Layers
For aggregation layers like hexagon heatmaps, access the specific submodule through the resolved API:
import { HexagonLayer } from '@deck.gl/aggregation-layers';
const deckGL = await api.getDeckGL();
const hexLayer = new deckGL.aggregationLayers.HexagonLayer({
id: 'hex-heat',
data: 'https://example.com/points.geojson',
getPosition: d => d.geometry.coordinates,
radius: 2000,
elevationScale: 50,
});
await api.setSharedDeckLayers('hex-plugin', [hexLayer as never]);
Summary
-
Singleton Pattern: GeoLibre uses one shared
MapboxOverlayinstance inshared-deck-overlay.tsto prevent WebGL context duplication and manage resource allocation across all deck.gl visualizations. -
Producer Architecture: Multiple features contribute layers via
setSharedDeckLayerswith unique producer IDs, whileSOURCE_DRAW_ORDERmaintains correct z-indexing between raster, 3D tiles, and deck.gl layers. -
Store Integration: The
layer-sync.tsrouter directsdeckgl-vizlayers to the shared overlay rather than creating native MapLibre layers, enabling complex WebGL rendering while preserving the layer management UI. -
API Exposure:
GeoLibreAppAPI.getDeckGL()provides third-party plugins access to the host's deck.gl modules, eliminating bundle duplication and version conflicts. -
Projection Constraints: deck.gl integration requires Mercator projection and automatically synchronizes view state with MapLibre, but does not support Globe view mode.
Frequently Asked Questions
How does GeoLibre prevent duplicate deck.gl instances when multiple plugins request visualization capabilities?
GeoLibre implements a singleton MapboxOverlay pattern where the first activation of a deck.gl feature lazily initializes map.__deck via shared-deck-overlay.ts. All subsequent layer requests—whether from the core deck-viz plugin, Google 3D-Tiles, or external plugins—contribute to this single instance through setSharedDeckLayers. This design prevents the "duplicate luma.gl" error that occurs when separate deck.gl bundles compete for WebGL contexts.
What map projections are supported when using deck.gl layers in GeoLibre?
The deck.gl integration in GeoLibre enforces Mercator projection only. As implemented in packages/plugins/src/types.ts (lines 586-608), the shared overlay configures deck.gl with a "mercator" view state because deck.gl's tiled rendering architecture does not support MapLibre's Globe projection. Users must switch to Mercator view before activating deck.gl visualizations to ensure proper geographic alignment.
How can third-party plugins add custom deck.gl layers without bundling the library?
External plugins access the host's deck.gl modules through GeoLibreAppAPI.getDeckGL(), which returns resolved references to @deck.gl/core, @deck.gl/layers, and other submodules. Plugins can then instantiate any deck.gl layer class and contribute it via api.setSharedDeckLayers('unique-producer-id', [layer]). This approach eliminates the need for plugins to include deck.gl in their own bundle, reducing download size and ensuring version compatibility with the host application.
Why are deck.gl layers rendered through a shared overlay instead of as native MapLibre layers?
Native MapLibre layers are optimized for vector tiles and raster sources but lack the WebGL compute capabilities required for massive dataset visualization (millions of points, 3D extrusions, GPU aggregation). The shared MapboxOverlay interleaves deck.gl's rendering context with MapLibre's canvas, enabling high-performance scatter plots, heatmaps, and 3D tiles while maintaining synchronized camera state and correct draw order via the SOURCE_DRAW_ORDER constant in shared-deck-overlay.ts.
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 →