How to Implement Custom Layer Types in the GeoLibre Plugin System
Create a MapLibre custom layer inside your plugin's activate method, register it with app.registerExternalNativeLayer(), and clean up on deactivate to add fully functional custom layers that appear in GeoLibre's Layers panel.
GeoLibre's plugin architecture lets you extend the core map engine with bespoke layer types that behave exactly like native layers. Whether you need a custom WebGL renderer, a novel data format, or specialized visualization, you can implement custom layer types in the GeoLibre plugin system while maintaining full UI integration—including opacity sliders, visibility toggles, and project persistence. This guide walks through the complete implementation based on the opengeos/GeoLibre source code.
Prerequisites and Core Types
Before building a custom layer, understand the foundational types defined in packages/plugins/src/types.ts. These interfaces govern how plugins interact with GeoLibre's application API.
The two critical types for custom layers are:
GeoLibrePlugin: The interface your plugin must implement, withactivate()anddeactivate()lifecycle methodsGeoLibreExternalNativeLayerRegistration: The configuration object that bridges your MapLibre layer into GeoLibre's state management
Step-by-Step Implementation
1. Define Your Custom Layer Class
Create a class that implements MapLibre's CustomLayerInterface. This is your actual rendering layer—typically WebGL-based.
// src/plugins/my-custom-layer.ts
import type {
GeoLibreAppAPI,
GeoLibrePlugin,
GeoLibreExternalNativeLayerRegistration,
} from "@geolibre/plugins";
class MyWebGLLayer implements maplibregl.CustomLayerInterface {
id = "my-webgl-layer";
constructor(private gl: WebGLRenderingContext) {}
onAdd(map: maplibregl.Map) {
// Allocate WebGL buffers, compile shaders, initialize state
}
render(gl: WebGLRenderingContext, matrix: Float32Array) {
// Execute draw calls for your custom visualization
}
// Custom methods for UI integration
setOpacity(opacity: number) {
// Update shader uniform for alpha blending
}
setActive(active: boolean) {
// Toggle visibility in render loop
}
}
The id property must be unique across all layers in the map instance.
2. Implement the Plugin Interface
Structure your plugin according to GeoLibrePlugin from packages/plugins/src/types.ts. The activate method is where you wire everything together.
export const myCustomLayerPlugin: GeoLibrePlugin = {
id: "my-custom-layer",
name: "My Custom Layer",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
// Obtain WebGL context from MapLibre's canvas
const gl = (app.getMap?.()?.getCanvas()?.getContext("webgl") ??
undefined) as WebGLRenderingContext;
const layer = new MyWebGLLayer(gl);
// Add to MapLibre (does NOT automatically appear in GeoLibre UI)
app.getMap?.()?.addLayer({
id: layer.id,
type: "custom",
renderingMode: "3d",
customLayer: layer,
});
// Register with GeoLibre to enable UI integration
const registration: GeoLibreExternalNativeLayerRegistration = {
id: layer.id,
name: "My WebGL Layer",
type: "raster", // Closest built-in type for icon/styling
nativeLayerIds: [layer.id],
paintMode: "plugin", // Critical: tells GeoLibre you handle paint
paintBridge: {
setOpacity: (opacity) => layer.setOpacity(opacity),
setVisibility: (visible) => layer.setActive(visible),
},
};
app.registerExternalNativeLayer?.(registration);
},
deactivate(app: GeoLibreAppAPI) {
// Clean up MapLibre layer
app.getMap?.()?.removeLayer("my-webgl-layer");
// Remove from GeoLibre's store
app.unregisterExternalNativeLayer?.("my-webgl-layer");
},
};
The paintMode: "plugin" setting is essential when implementing custom layer types in the GeoLibre plugin system with their own rendering logic. Without this, GeoLibre would expose generic raster controls that have no effect on your layer.
3. Expose and Register the Plugin
Export your plugin from the package entry point at packages/plugins/src/index.ts:
export { myCustomLayerPlugin } from "./plugins/my-custom-layer";
Then register it in the application bootstrap at apps/geolibre-desktop/src/hooks/usePlugins.ts:
import { myCustomLayerPlugin } from "@geolibre/plugins";
manager.registerAll([
// ...other built-in plugins
myCustomLayerPlugin,
]);
Understanding the Registration Configuration
The GeoLibreExternalNativeLayerRegistration object controls how GeoLibre represents your layer in the UI and state system:
| Field | Purpose | Required |
|---|---|---|
id |
Unique identifier matching the MapLibre layer ID | Yes |
name |
Display name in the Layers panel | Yes |
type |
Native type hint ("raster", "vector", "fill", etc.) for icons |
Yes |
nativeLayerIds |
Array of MapLibre layer IDs belonging to this logical layer | Yes |
paintMode |
"plugin" or "native"—whether paint properties are handled externally |
Yes |
paintBridge |
Callbacks for setOpacity and setVisibility when paintMode: "plugin" |
Recommended |
paintBridge Callbacks Explained
When paintMode is "plugin", GeoLibre cannot directly manipulate your layer's visual properties because it doesn't know your internal rendering state. The paintBridge provides the translation layer:
setOpacity(opacity: number): Called when the user adjusts the opacity slider in the Layers panel. Receives a value from 0 to 1.setVisibility(visible: boolean): Called when the user toggles layer visibility via checkbox or eye icon.
Without these callbacks, your layer renders but the UI controls remain disconnected.
Alternative: Native Paint Mode
If your custom layer uses standard MapLibre paint properties, you can set paintMode: "native" and omit paintBridge. GeoLibre will manipulate raster-opacity or equivalent properties directly. This only works for layers that don't implement CustomLayerInterface.
Lifecycle and Cleanup
Proper cleanup in deactivate prevents memory leaks and dangling references:
- Remove the MapLibre layer with
app.getMap?.()?.removeLayer(id) - Unregister from GeoLibre's store with
app.unregisterExternalNativeLayer?.(id)
The order matters: unregister from GeoLibre first if you need to access the map instance during cleanup, though typically map removal comes first.
Key Source Files Reference
| File | Location | Purpose |
|---|---|---|
| Plugin type definitions | packages/plugins/src/types.ts |
GeoLibrePlugin, GeoLibreAppAPI, GeoLibreExternalNativeLayerRegistration |
| Plugin manager | packages/plugins/src/plugin-manager.ts |
Lifecycle orchestration and activation |
| Package exports | packages/plugins/src/index.ts |
Public API surface for built-in plugins |
| Application integration | apps/geolibre-desktop/src/hooks/usePlugins.ts |
Runtime plugin registration |
| API documentation | docs/plugin-api.md |
Complete reference including Custom (WebGL) layers section |
| User guide | docs/user-guide/plugins.md |
End-user documentation for plugin management |
Summary
- Create a
CustomLayerInterfaceimplementation for WebGL rendering or use standard MapLibre layer types - Register with
app.registerExternalNativeLayer()usingpaintMode: "plugin"andpaintBridgecallbacks for full UI control - Clean up both MapLibre and GeoLibre registrations in
deactivateto prevent leaks - Export from
packages/plugins/src/index.tsand register inusePlugins.tsfor activation
Frequently Asked Questions
What is the difference between addLayer() and registerExternalNativeLayer() in GeoLibre?
app.getMap?.()?.addLayer() adds your layer to the underlying MapLibre map instance, making it render on the canvas. However, GeoLibre's UI and state system remain unaware of it. app.registerExternalNativeLayer() mirrors the layer into GeoLibre's store, causing it to appear in the Layers panel, participate in opacity/visibility controls, and persist in project files. Both calls are required for full integration.
Can I implement custom layer types without WebGL?
Yes. Any valid MapLibre layer type—including fill, line, circle, or raster with custom sources—can be registered as an external native layer. Only layers that implement CustomLayerInterface require paintMode: "plugin". Standard layers can use paintMode: "native" and let GeoLibre manipulate paint properties directly.
How do multiple nativeLayerIds work in a single registration?
Some complex visualizations require multiple MapLibre layers working together—perhaps a base fill layer with an outline stroke. Pass all related layer IDs in nativeLayerIds: ["fill-layer", "outline-layer"]. GeoLibre treats them as a single logical layer in the UI, so one visibility toggle controls both, and opacity changes apply to the primary layer while the secondary follows.
What happens if I forget to call unregisterExternalNativeLayer() in deactivate?
The layer remains in GeoLibre's internal store as a stale reference. This causes several problems: the Layers panel displays a non-functional entry, project serialization may include invalid layer state, and re-activating the plugin can trigger ID collision errors. Always pair registration with unregistration in the plugin lifecycle.
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 →