@sindicum/libre-draw
v0.16.0
Published
MapLibre GL JS polygon drawing and editing library
Downloads
2,178
Maintainers
Readme
LibreDraw
A point, line, and polygon drawing and editing library for MapLibre GL JS.
Features
- Zero-config —
new LibreDraw(map)gives you a full toolbar and drawing capabilities out of the box - Draw points — Click/tap to place point features
- Draw lines — Click/tap to add vertices, click/tap the last vertex to finalize
- Draw polygons — Click/tap to place vertices, click/tap the first or last vertex to close
- Draw rectangles — Click/tap two opposite corners to create an axis-aligned rectangle
- Draw angled rectangles — Click/tap a base edge at any angle, then a point that sets the width
- Select & edit — Click a feature to select it, drag vertices to reshape, drag midpoints to add vertices; a polygon's holes are edited the same way
- Feature drag — Drag an entire selected point, line, or polygon to reposition it
- Split — Cut a polygon or line into two with a two-point split line
- Cut — Remove an area from a polygon by drawing its outline: a hole, a notch, or separate pieces
- Reshape — Redraw part of a polygon's boundary with a line that crosses it twice, adding or removing area
- Union — Merge two or more touching or overlapping polygons into one: click them to select, then press Enter or the execute button
- Setback edge — Offset a selected edge inward and remove the setback band
- Rotate — Turn a polygon or line by dragging it or by entering a relative angle
- Snap — Vertices snap to nearby existing vertices and edges during drawing and editing
- Undo / Redo — Full history support for all operations
- GeoJSON in/out — Import and export standard GeoJSON FeatureCollections (Point, LineString, Polygon)
- Touch-first — Designed for mobile with proper touch targets (44px+), long-press support, and gesture handling
- Center reticle input — Optionally place points with a crosshair fixed at the map center and "Add point" / "Undo point" / "Finish" buttons, instead of tapping where a finger hides the spot. Switch from the toolbar or the API
- Self-intersection prevention — Invalid polygon geometries are rejected during editing, including holes that would cross or leave the outer ring
- Framework-agnostic — Works with vanilla JS, React, Vue, or any framework
- TypeScript — Full type definitions included
- Headless mode — Disable the toolbar and drive everything via API
Quick Start
npm install @sindicum/libre-draw maplibre-glimport * as maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
import { LibreDraw } from '@sindicum/libre-draw';
// MapLibre GL JS v6 loads its worker from a URL you give it once (this is the Vite form;
// see https://maplibre.org/maplibre-gl-js/docs/#installation for other bundlers).
// On MapLibre v5 the worker is inside the bundle: drop this call and the import above.
maplibregl.setWorkerUrl(workerUrl);
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
const draw = new LibreDraw(map);
draw.on('create', (e) => {
console.log('Feature created:', e.feature.geometry.type, e.feature);
});
draw.on('update', (e) => {
console.log('Feature updated:', e.feature.geometry.type, e.feature);
});API
Constructor
new LibreDraw(map: maplibregl.Map, options?: LibreDrawOptions)Methods
| Method | Description |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| setMode(mode) | Set active mode: 'idle', 'draw-point', 'draw-line', 'draw-polygon', 'draw-rectangle', 'draw-angled-rectangle', 'select', 'split', 'union', 'setback', 'rotate', 'cut', or 'reshape' |
| getMode() | Get the current mode |
| setInputMethod(method) | How the drawing modes take a point: 'tap' (click / tap the map) or 'reticle' (center crosshair and an "Add point" button) |
| getInputMethod() | Get the current input method |
| finishDrawing() | Finish the in-progress line or polygon |
| cancelDrawing() | Discard the in-progress draft |
| undoLastVertex() | Take back the last placed point of the draft |
| getDraftVertexCount() | Get the number of points in the current draft |
| getFeatures() | Get all features as an array |
| toGeoJSON() | Export all features as a GeoJSON FeatureCollection |
| getFeatureById(id) | Get a single feature by ID |
| setFeatures(geojson) | Replace all features with a GeoJSON FeatureCollection (all or nothing, returns { ok, ... }) |
| addFeatures(features) | Add an array of GeoJSON Feature objects (undoable as one step). Returns one { valid, id, reason? } per feature; invalid entries are reported there, never thrown |
| validateFeature(feature) | Check an object against the same rules as addFeatures without adding it and without throwing |
| deleteFeature(id) | Delete a feature by ID (undoable) |
| updateFeature(id, patch) | Replace a feature's geometry and/or properties (undoable, returns { ok, ... }) |
| rotate(id, angleDeg) | Rotate a polygon or line around its centroid (undoable, returns { ok, ... }) |
| split(id, line) | Split a polygon or line along the line through two points (undoable, returns { ok, ... }) |
| setback(id, edge, distanceMeters) | Move one edge of a polygon inward by a distance in meters (undoable, returns { ok, ... }) |
| union(ids) | Merge two or more polygons into one (undoable, returns { ok, ... }) |
| cut(id, cutter) | Cut the area of a ring out of a polygon (undoable, returns { ok, ... }) |
| reshape(id, line) | Replace part of a polygon's outer ring with a line (undoable, returns { ok, ... }) |
| selectFeature(id) | Programmatically select a feature (returns false for an unknown id) |
| selectFeatures(ids) | Select several features at once (returns false for an empty list or any unknown id) |
| clearSelection() | Clear the current selection |
| getSelectedFeatureIds() | Get IDs of selected features |
| undo() | Undo the last action |
| redo() | Redo the last undone action |
| setStyle(style) | Change the map style of the features (merged onto the current style) |
| getStyle() | Get the current style |
| on(event, callback) | Register an event listener |
| off(event, callback) | Remove an event listener |
| destroy() | Clean up all resources |
Events
| Event | Payload | Description |
| ----------------- | ----------------------------------------------------- | -------------------------------------------------------------------------- |
| create | { feature } | A feature was created (point, line, or polygon) |
| update | { feature, oldFeature } | A feature was updated |
| delete | { feature } | A feature was deleted |
| split | { originalFeature, features: [featureA, featureB] } | A polygon or line was split into two |
| splitfailed | { reason, featureId } | Split operation failed |
| setback | { originalFeature, feature, edgeIndex, distance } | Setback operation succeeded |
| setbackfailed | { reason, featureId } | Setback operation failed |
| union | { originalFeatures: [...features], feature } | Two or more polygons were merged into one |
| unionfailed | { reason, featureIds } | Union operation failed |
| cut | { originalFeature, features: [...pieces] } | An area was cut out of a polygon |
| cutfailed | { reason, featureId } | Cut operation failed |
| reshape | { originalFeature, feature } | Part of a polygon's boundary was redrawn |
| reshapefailed | { reason, featureId } | Reshape operation failed |
| rotate | { originalFeature, feature, angle } | A polygon or line was rotated |
| selectionchange | { selectedIds } | Selection changed |
| modechange | { mode, previousMode } | Active mode changed |
| draftchange | { vertexCount } | The in-progress draft gained or lost a point, or was finished or discarded |
Every payload also carries origin: 'api' | 'user', so a listener can tell changes made through the API (its own addFeatures() / deleteFeature() / undo() …) from the user's pointer, toolbar, and keyboard input.
Options
interface LibreDrawOptions {
toolbar?:
| boolean
| {
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
controls?: {
drawPoint?: boolean;
drawLine?: boolean;
drawPolygon?: boolean;
drawRectangle?: boolean;
drawAngledRectangle?: boolean;
inputMethod?: boolean; // tap / center reticle toggle
select?: boolean;
split?: boolean;
cut?: boolean;
reshape?: boolean;
union?: boolean;
setback?: boolean;
rotate?: boolean;
settings?: boolean; // style settings panel
delete?: boolean;
undo?: boolean;
redo?: boolean;
};
};
keyboard?: boolean | { undoRedo?: boolean }; // Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y. Default: true
historyLimit?: number; // Default: 100
style?: PartialStyleConfig; // Overrides for the map style of the features
snap?: boolean | { enabled?: boolean; threshold?: number }; // Default: true
locale?: 'en' | 'ja'; // UI language. Default: 'en'
messages?: Partial<Messages>; // Override individual UI strings
inputMethod?: 'tap' | 'reticle'; // How drawing modes take a point. Default: 'tap'
}Set toolbar: false for headless mode (API-only, no UI). Undo / redo keyboard shortcuts stay active in headless mode and only fire while the map has focus; set keyboard: false to turn them off.
Documentation
Full documentation with interactive demos is available at:
https://sindicum.github.io/libre-draw/
Every method answers with a return value ({ ok, ... }, per-feature results, or a boolean) and throws only for misuse of the instance, so the same API serves your own code and an AI agent. See Programmatic API for the return value / exception table and the origin rules. A reference MCP server for local use is available in examples/mcp.
Development
# Install dependencies
npm install
# Run dev server with example
npm run dev
# Run tests
npm test
# Lint
npm run lint
# Type check
npm run typecheck
# Build
npm run build
# Documentation site
npm run docs:dev
# Format (Prettier)
npm run format.git-blame-ignore-revs lists commits that only reformat code. Run this once
per clone so git blame skips them and shows who actually wrote each line
(GitHub's blame view honours the file without any setup):
git config blame.ignoreRevsFile .git-blame-ignore-revsRequirements
- MapLibre GL JS v5 or v6 (peer dependency,
>=5.0.0 <7.0.0) - Modern browser with WebGL support (WebGL2 with MapLibre v6)
