How to Create a Custom Map Control in GeoLibre: The Complete Plugin Guide
Create a custom map control in GeoLibre by implementing the GeoLibrePlugin interface, instantiating any MapLibre-GL IControl object in the activate callback, and attaching it with app.addMapControl(control, position), then clean up in deactivate with app.removeMapControl(control).
Creating custom map controls in GeoLibre leverages a plugin-based architecture where controls are first-class plugins that hook into the core application. This approach ensures your custom controls integrate cleanly with GeoLibre's state management, respect runtime environment changes, and follow predictable lifecycle patterns. Whether you're building a simple toolbar button or a complex interactive panel, the same fundamental pattern applies.
Understanding the GeoLibre Plugin Architecture
GeoLibre's control system is built on a minimal but powerful abstraction. The core exposes the GeoLibreAppAPI interface, which provides methods for adding, removing, and repositioning map controls.
The GeoLibrePlugin Interface (from packages/plugins/src/types.ts)
Every control plugin must implement this contract:
id: Unique string identifier for the pluginname: Human-readable display nameversion: Semver string for compatibility trackingactivate(app): Called when the plugin loads; create and attach your control heredeactivate(app): Called when the plugin unloads; remove your control and clean upgetMapControlPosition(optional): Return the current position stringsetMapControlPosition(optional): Handle runtime position changes from the UI
Control Position Options
GeoLibre uses MapLibre-GL's standard corner positions:
"top-left"— Above the zoom controls"top-right"— Opposite side, common for custom toolbars"bottom-left"— Below attribution"bottom-right"— Common for legends and time sliders
Step-by-Step: Building Your First Custom Control
Follow this sequence to create a production-ready control plugin.
Step 1: Create the MapLibre Control Class
Your control must implement MapLibre's IControl interface with onAdd(map) and onRemove() methods.
// packages/plugins/src/plugins/alert-center-control.ts
import type { IControl, Map as MapLibreMap } from "maplibre-gl";
class AlertCenterControl implements IControl {
private _container: HTMLElement | null = null;
onAdd(map: MapLibreMap): HTMLElement {
this._container = document.createElement("div");
this._container.className = "alert-center-control maplibregl-ctrl-group";
const button = document.createElement("button");
button.title = "Show Map Center";
button.innerText = "📍";
button.onclick = () => {
const center = map.getCenter();
alert(`Center: ${center.lng.toFixed(4)}, ${center.lat.toFixed(4)}`);
};
this._container.appendChild(button);
return this._container;
}
onRemove(): void {
// Clean up any event listeners or external refs
this._container?.remove();
this._container = null;
}
}
Step 2: Wrap It in a GeoLibre Plugin
The plugin connects your control to GeoLibre's lifecycle.
// packages/plugins/src/plugins/alert-center-control.ts (continued)
import type { GeoLibreAppAPI, GeoLibreMapControlPosition, GeoLibrePlugin } from "../types";
export const alertCenterControlPlugin: GeoLibrePlugin = {
id: "alert-center-control",
name: "Center Alert Button",
version: "1.0.0",
activate(app: GeoLibreAppAPI): void {
const control = new AlertCenterControl();
// Attempt to add immediately; handle deferred load if map not ready
const added = app.addMapControl(control, "top-right");
if (!added) {
console.warn("[alert-center-control] Map not ready, control queued");
}
// Store reference for cleanup and position changes
(app as any)._alertCenterControl = control;
},
deactivate(app: GeoLibreAppAPI): void {
const control = (app as any)._alertCenterControl;
if (control) {
app.removeMapControl(control);
delete (app as any)._alertCenterControl;
}
},
// Optional: enable runtime repositioning via settings UI
getMapControlPosition(): GeoLibreMapControlPosition {
return "top-right";
},
setMapControlPosition(app: GeoLibreAppAPI, position: GeoLibreMapControlPosition): boolean {
const control = (app as any)._alertCenterControl;
if (!control) return false;
app.removeMapControl(control);
return app.addMapControl(control, position);
}
};
Step 3: Register the Plugin
Export from the plugins barrel file so the plugin manager discovers it:
// packages/plugins/src/index.ts
export { alertCenterControlPlugin } from "./plugins/alert-center-control";
The plugin manager at packages/plugins/src/plugin-manager.ts handles the rest—loading, version checking, and calling activate/deactivate at appropriate times.
Production Patterns from the Source Code
Study these reference implementations in the GeoLibre repository for advanced patterns.
Pattern 1: State Persistence and Restoration
The Basemap Control in packages/plugins/src/plugins/maplibre-basemap-control.ts (lines 41-56) demonstrates how to restore previously saved layers when re-instantiating a control:
// Simplified excerpt showing state restoration pattern
function relinkRestoredRasterBasemaps(app: GeoLibreAppAPI, control: BasemapControl): void {
const savedState = app.getPersistentState("basemap-layers");
if (savedState) {
control.restoreLayers(savedState);
}
}
Pattern 2: Reacting to Environment Changes
Lines 21-32 of the same file show runtime environment handling via addRuntimeEnvListener, letting controls adapt without reload:
// From maplibre-basemap-control.ts
app.addRuntimeEnvListener((env) => {
control.updateAvailableBasemaps(env.allowedBasemapProviders);
});
Pattern 3: Deferred Control Attachment
For controls that depend on async initialization, check the return value of addMapControl and retry on the "mapready" event:
activate(app: GeoLibreAppAPI): void {
const tryAdd = () => {
const control = new TimeSliderControl();
const success = app.addMapControl(control, "bottom-right");
if (success) {
(app as any)._timeSlider = control;
} else {
// Retry once map becomes available
app.once("mapready", tryAdd);
}
};
tryAdd();
}
Common Control Types and Implementation Tips
| Control Type | Key Technique | Reference File |
|---|---|---|
| Toolbar button | Simple IControl with event listener |
Pattern shown above |
| Layer switcher | Track active layers in plugin state, sync with app store | maplibre-basemap-control.ts |
| Time slider | Range input with throttled map updates | maplibre-time-slider.ts |
| Draw/measure tool | Integrate existing MapLibre plugins (e.g., maplibre-gl-draw) | Wrap with plugin lifecycle |
| Fullscreen panel | Render React/Vue component via portal, position off-map | Use top-left with custom CSS |
Summary
- Custom map controls in GeoLibre are plugins that implement the
GeoLibrePlugininterface defined inpackages/plugins/src/types.ts. - Create standard MapLibre-GL controls by implementing the
IControlinterface withonAddandonRemovemethods. - Attach controls via
app.addMapControl(control, position)in theactivatecallback; positions are"top-left","top-right","bottom-left", or"bottom-right". - Clean up with
app.removeMapControl(control)indeactivateto prevent memory leaks and DOM residue. - Enable position changes by implementing
getMapControlPositionandsetMapControlPositionfor UI-driven repositioning. - Study built-in plugins in
packages/plugins/src/plugins/for state management, environment reactivity, and deferred loading patterns.
Frequently Asked Questions
What happens if I call addMapControl before the map is ready?
The method returns false, and you should retry on the "mapready" event. Most built-in plugins follow this pattern to handle race conditions during startup.
Can I use existing MapLibre-GL plugins as GeoLibre controls?
Yes. Any object implementing IControl works—wrap it in a plugin following the lifecycle pattern above. Popular choices include maplibre-gl-draw, maplibre-gl-geocoder, and maplibre-gl-inspect.
How do I style my custom control?
Add CSS classes following MapLibre conventions: maplibregl-ctrl for the outer container, maplibregl-ctrl-group for button groups, and maplibregl-ctrl-icon for icon buttons. GeoLibre's theme variables are available for consistent styling.
Where does the control position appear in the UI?
The Controls Menu at apps/geolibre-desktop/src/components/layout/toolbar/ControlsMenu.tsx lists loaded controls and can trigger position changes if your plugin implements the optional position methods.
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 →