@ridewolf/maplibre-zone-editor
v0.2.2
Published
Headless polygon zone editor for MapLibre GL on top of mapbox-gl-draw: draw/edit modes, undo stack, double-click vertex deletion with a triangle guard, and clean geometry events. Framework-agnostic.
Readme
Building a geofence editor on MapLibre means gluing mapbox-gl-draw onto a library it
wasn't written for, styling its internals, inventing undo, and teaching mobile users to
edit vertices. Every mobility dashboard does this from scratch. This package is that
glue, extracted from the Ridewolf zone editor — headless, so it renders zero UI and
works with any framework's buttons.
Quickstart
bun add @ridewolf/maplibre-zone-editor maplibre-gl @mapbox/mapbox-gl-drawimport MapboxDraw from '@mapbox/mapbox-gl-draw';
import { Map as MapLibreMap } from 'maplibre-gl';
import { createZoneEditor } from '@ridewolf/maplibre-zone-editor';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
const map = new MapLibreMap({ container: 'map', style: STYLE_URL });
const editor = createZoneEditor(map, MapboxDraw, {
color: '#3B82F6',
onChange: (polygon) => save(polygon), // GeoJSON Polygon | null
onInvalidAction: (reason) => toast(reason), // 'no-polygon' | 'min-vertices'
});
editor.attach();
// Wire your own toolbar:
drawButton.onclick = () => editor.startDrawing();
editButton.onclick = () => editor.startEditing();
undoButton.onclick = () => editor.undo();
deleteButton.onclick = () => editor.deletePolygon();Or run the bundled demo — the toolbar above, on a map that needs no tile server or API key (the basemap is generated as GeoJSON):
bun install && bun run build && bunx serve . # → examples/demo.htmlWhat it handles for you
- The MapLibre compatibility patch —
mapbox-gl-drawtargets Mapbox GL's DOM class names and silently misbehaves on MapLibre;patchMapboxDrawForMapLibre()rewrites them (applied automatically by the editor). - Undo — every create/update/delete snapshots the draw state (bounded depth,
default 20);
undo()walks back through it down to an empty canvas. - Double-click / double-tap vertex deletion — with a finger-friendly hit box, the
default dblclick-zoom swallowed, and a triangle guard: a deletion that would
leave fewer than three corners is refused (reported via
onInvalidAction) instead of producing invalid geometry. - Clean geometry events —
onChangefires with a plain GeoJSONPolygon(ornull), not draw's internal feature collection. - Touch-tuned draw styles — translucent fill, 6.5 px vertex handles, subtle
midpoints, all in your accent colour (
buildDrawStyles(color)is exported for customization). - Injected dependencies — the
MapboxDrawconstructor is a parameter, so forks work and the whole editor is testable without a browser (see the fake-driven test suite).
API
| Member | Purpose |
| --- | --- |
| createZoneEditor(map, MapboxDraw, options) | Construct; attach() / detach() manage map bindings. |
| startDrawing() | Enter polygon-drawing mode. |
| startEditing() | Enter vertex-editing mode on the drawn polygon. |
| deletePolygon() | Remove the polygon (undoable). |
| undo() | Step back through the snapshot stack. |
| getGeometry() / setGeometry(polygon \| null) | GeoJSON in, GeoJSON out. setGeometry resets the undo stack and does not fire onChange. |
| setColor(color) | Restyle the polygon, keeping the operator's place (see below). |
Options: color, onChange, onInvalidAction, maxUndoStates, vertexHitTolerance.
Surviving a rebuild
MapboxDraw bakes its styles in at construction, and a basemap swap tears custom
layers off the map — so both a colour change and a style change mean rebuilding
or re-adding the control. Done naively, that drops the operator out of whatever
they were doing into an empty simple_select.
detach() banks the polygon, the current mode and the selection; the next
attach() restores them. A basemap swap is then:
editor.detach();
map.setStyle(nextStyle);
onceStyleIsReady(() => editor.attach()); // same polygon, same mode, same selectionand a colour change is just editor.setColor('#22c55e') — it rebuilds the
control through that same pair, so the operator keeps their place and the undo
stack survives, recolouring not being an edit.
Documentation
- Design notes — why headless, the vertex-deletion mechanics, the single-polygon model, and integration recipes (Vue/React).
Why we built this
At Ridewolf operators draw geofence zones — parking, no-go, speed-limited — directly on the dashboard map. The editor behind that went through the usual pain: draw misbehaving on MapLibre, accidental vertex loss producing degenerate polygons the backend rejected, and mobile operators unable to delete vertices at all. This package is the survivor, minus our UI.
Contributing
Contributions welcome — see the contributing guide.
Run bun test, bun run lint, bun run typecheck before a PR. Security issues:
SECURITY.md — never in a public issue.
