Advanced geometry editing plugin for MapLibre GL with Geoman integration
npm install maplibre-gl-geo-editorA powerful MapLibre GL plugin for creating and editing geometries. Extends the free Geoman control with advanced editing features including Union, Split, Scale, Difference, Simplify, Copy, and Lasso selection.





``bash`
npm install maplibre-gl-geo-editor @geoman-io/maplibre-geoman-free maplibre-gl
`typescript
import 'maplibre-gl/dist/maplibre-gl.css';
import '@geoman-io/maplibre-geoman-free/dist/maplibre-geoman.css';
import 'maplibre-gl-geo-editor/style.css';
import maplibregl from 'maplibre-gl';
import { Geoman } from '@geoman-io/maplibre-geoman-free';
import { GeoEditor } from 'maplibre-gl-geo-editor';
// Create the map
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
map.on('load', () => {
// Initialize Geoman
const geoman = new Geoman(map, {});
map.on('gm:loaded', () => {
// Create GeoEditor
const geoEditor = new GeoEditor({
position: 'top-left',
toolbarOrientation: 'vertical',
columns: 2, // Display buttons in 2 columns (reduces toolbar height)
drawModes: ['polygon', 'line', 'rectangle', 'circle', 'marker', 'freehand'],
editModes: [
'select', 'drag', 'change', 'rotate', 'cut', 'delete',
'scale', 'copy', 'split', 'union', 'difference', 'simplify', 'lasso'
],
showFeatureProperties: true, // Show popup with properties on selection
fitBoundsOnLoad: true, // Auto-zoom to extent when loading GeoJSON
onFeatureCreate: (feature) => console.log('Created:', feature),
onSelectionChange: (features) => console.log('Selected:', features.length),
});
// Connect with Geoman
geoEditor.setGeoman(geoman);
// Add to map
map.addControl(geoEditor, 'top-left');
});
});
`
Enable the attribute editing panel to edit feature properties with a schema-based form:
`typescript`
const geoEditor = new GeoEditor({
position: 'top-left',
enableAttributeEditing: true,
attributePanelPosition: 'right', // 'left' or 'right'
attributePanelWidth: 300,
attributePanelTitle: 'Feature Properties',
attributeSchema: {
// Fields for polygon features
polygon: [
{ name: 'name', label: 'Name', type: 'string', required: true },
{
name: 'land_use',
label: 'Land Use',
type: 'select',
options: [
{ value: 'residential', label: 'Residential' },
{ value: 'commercial', label: 'Commercial' },
{ value: 'industrial', label: 'Industrial' },
],
defaultValue: 'residential'
},
{ name: 'description', label: 'Description', type: 'textarea' }
],
// Fields for line features
line: [
{ name: 'name', label: 'Name', type: 'string', required: true },
{ name: 'road_type', label: 'Road Type', type: 'string' },
{ name: 'lanes', label: 'Lanes', type: 'number', min: 1, max: 8 }
],
// Fields for point features
point: [
{ name: 'name', label: 'Name', type: 'string', required: true },
{ name: 'category', label: 'Category', type: 'string' },
{ name: 'active', label: 'Active', type: 'boolean', defaultValue: true }
],
// Common fields for all geometry types
common: [
{ name: 'notes', label: 'Notes', type: 'textarea' },
{ name: 'color', label: 'Color', type: 'color', defaultValue: '#3388ff' }
]
},
onAttributeChange: (event) => {
console.log('Feature:', event.feature);
console.log('Previous:', event.previousProperties);
console.log('New:', event.newProperties);
console.log('Is new feature:', event.isNewFeature);
}
});
The panel auto-appears when:
- A new geometry is drawn (after drawing completes)
- A single feature is selected in select mode
#### Field Types
| Type | Description | Additional Options |
|------|-------------|-------------------|
| string | Single-line text input | placeholder |number
| | Numeric input | min, max, step |boolean
| | Checkbox | - |select
| | Dropdown select | options: [{value, label}] |date
| | Date picker | - |color
| | Color picker | - |textarea
| | Multi-line text | placeholder |
#### Programmatic Control
`typescript
// Open editor for a specific feature
geoEditor.openAttributeEditor(feature);
// Close the editor
geoEditor.closeAttributeEditor();
// Toggle visibility
geoEditor.toggleAttributePanel();
// Update schema dynamically
geoEditor.setAttributeSchema(newSchema);
// Get current schema
const schema = geoEditor.getAttributeSchema();
`
`tsx
import { useEffect, useRef, useState } from 'react';
import maplibregl from 'maplibre-gl';
import { Geoman } from '@geoman-io/maplibre-geoman-free';
import { GeoEditorReact } from 'maplibre-gl-geo-editor/react';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@geoman-io/maplibre-geoman-free/dist/maplibre-geoman.css';
import 'maplibre-gl-geo-editor/style.css';
function App() {
const mapContainer = useRef(null);
const [map, setMap] = useState(null);
const [geoman, setGeoman] = useState(null);
useEffect(() => {
const newMap = new maplibregl.Map({
container: mapContainer.current,
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
newMap.on('load', () => {
const gm = new Geoman(newMap, {});
newMap.on('gm:loaded', () => {
setMap(newMap);
setGeoman(gm);
});
});
return () => newMap.remove();
}, []);
return (
API Reference
$3
| Option | Type | Default | Description |
|--------|------|---------|-------------|
|
position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'top-left' | Position of the control |
| collapsed | boolean | false | Start with toolbar collapsed |
| drawModes | DrawMode[] | All modes | Draw modes to enable |
| editModes | EditMode[] | All modes | Edit modes to enable |
| fileModes | FileMode[] | ['open', 'save'] | File operations to enable |
| toolbarOrientation | 'vertical' \| 'horizontal' | 'vertical' | Toolbar layout |
| columns | number | 1 | Number of button columns (vertical orientation only) |
| showLabels | boolean | false | Show text labels on buttons |
| simplifyTolerance | number | 0.001 | Default simplification tolerance |
| saveFilename | string | 'features.geojson' | Default filename for saving |
| showFeatureProperties | boolean | false | Show popup with feature properties when selected |
| fitBoundsOnLoad | boolean | true | Auto-zoom to extent when loading GeoJSON |
| onFeatureCreate | (feature) => void | - | Callback when feature is created |
| onFeatureEdit | (feature, oldFeature) => void | - | Callback when feature is edited |
| onFeatureDelete | (featureId) => void | - | Callback when feature is deleted |
| onSelectionChange | (features) => void | - | Callback when selection changes |
| onModeChange | (mode) => void | - | Callback when mode changes |
| onGeoJsonLoad | (result) => void | - | Callback when GeoJSON is loaded |
| onGeoJsonSave | (result) => void | - | Callback when GeoJSON is saved |
| enableAttributeEditing | boolean | false | Enable attribute editing panel |
| attributeSchema | AttributeSchema | - | Schema defining fields per geometry type |
| attributePanelPosition | 'left' \| 'right' | 'right' | Position of the attribute panel |
| attributePanelWidth | number | 300 | Width of the attribute panel in pixels |
| attributePanelMaxHeight | number \| string | '80vh' | Maximum height of the attribute panel (px or CSS value) |
| attributePanelTop | number | 10 | Offset from top of map container in pixels |
| attributePanelSideOffset | number | 10 | Offset from left/right side of map container in pixels |
| attributePanelTitle | string | 'Feature Properties' | Title of the attribute panel |
| onAttributeChange | (event) => void | - | Callback when feature attributes change |
| enableHistory | boolean | true | Enable undo/redo functionality |
| maxHistorySize | number | 50 | Maximum number of history entries |
| onHistoryChange | (canUndo, canRedo) => void | - | Callback when history state changes |$3
`typescript
// Mode management
geoEditor.enableDrawMode('polygon');
geoEditor.enableEditMode('scale');
geoEditor.disableAllModes();// Selection
geoEditor.selectFeatures(features);
geoEditor.clearSelection();
geoEditor.getSelectedFeatures();
geoEditor.getSelectedFeatureCollection();
// Clipboard
geoEditor.copySelectedFeatures();
geoEditor.pasteFeatures();
geoEditor.deleteSelectedFeatures();
// Get all features
geoEditor.getFeatures();
geoEditor.getAllFeatureCollection();
// File operations
geoEditor.openFileDialog(); // Open file picker dialog
geoEditor.loadGeoJson(geoJson); // Load GeoJSON programmatically
geoEditor.saveGeoJson('filename.geojson'); // Save/download GeoJSON
// Map view
geoEditor.fitToAllFeatures(); // Zoom map to show all features
// Operation snapshots
geoEditor.getLastCreatedFeature();
geoEditor.getLastEditedFeature();
geoEditor.getLastDeletedFeature();
geoEditor.getLastDeletedFeatureId();
// Get state
geoEditor.getState();
// Attribute editing
geoEditor.openAttributeEditor(feature); // Open editor for a feature
geoEditor.closeAttributeEditor(); // Close the editor
geoEditor.toggleAttributePanel(); // Toggle visibility
geoEditor.setAttributeSchema(schema); // Update schema dynamically
geoEditor.getAttributeSchema(); // Get current schema
// History (undo/redo)
geoEditor.undo(); // Undo last operation
geoEditor.redo(); // Redo last undone operation
geoEditor.canUndo(); // Check if undo is available
geoEditor.canRedo(); // Check if redo is available
geoEditor.clearHistory(); // Clear all history
geoEditor.getHistoryState(); // Get history state object
`$3
Listen for events on the map container:
`typescript
map.getContainer().addEventListener('gm:union', (e) => {
console.log('Union result:', e.detail);
});map.getContainer().addEventListener('gm:split', (e) => {
console.log('Split result:', e.detail);
});
map.getContainer().addEventListener('gm:simplify', (e) => {
console.log('Simplify result:', e.detail);
});
map.getContainer().addEventListener('gm:lassoend', (e) => {
console.log('Lasso selection:', e.detail);
});
map.getContainer().addEventListener('gm:geojsonload', (e) => {
console.log('GeoJSON loaded:', e.detail);
// detail: { features, count, filename }
});
map.getContainer().addEventListener('gm:geojsonsave', (e) => {
console.log('GeoJSON saved:', e.detail);
// detail: { featureCollection, count, filename }
});
`Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
|
Ctrl+C | Copy selected features |
| Ctrl+V | Paste features |
| Ctrl+Z | Undo last operation |
| Ctrl+Y | Redo last undone operation |
| Delete | Delete selected features |
| Escape | Cancel operation / Clear selection |Logging
GeoEditor logs the current selected FeatureCollection to the console whenever a feature is selected, created, edited, or deleted.
Standalone Feature Classes
You can also use the feature classes directly:
`typescript
import {
CopyFeature,
SimplifyFeature,
UnionFeature,
DifferenceFeature,
ScaleFeature,
SplitFeature,
FreehandFeature,
} from 'maplibre-gl-geo-editor';// Union polygons
const union = new UnionFeature();
const result = union.union([polygon1, polygon2]);
// Simplify a feature
const simplify = new SimplifyFeature();
const simplified = simplify.simplify(feature, { tolerance: 0.01 });
// Get simplification stats
const stats = simplify.getSimplificationStats(feature, 0.01);
console.log(
Reduced vertices by ${stats.reduction}%);
`Development
`bash
Install dependencies
npm installStart development server
npm run devRun tests
npm testBuild
npm run build
`Docker
The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
$3
`bash
Pull the latest image
docker pull ghcr.io/opengeos/maplibre-gl-geo-editor:latestRun the container
docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-geo-editor:latest
`Then open http://localhost:8080/maplibre-gl-geo-editor/ in your browser to view the examples.
$3
`bash
Build the image
docker build -t maplibre-gl-geo-editor .Run the container
docker run -p 8080:80 maplibre-gl-geo-editor
`$3
| Tag | Description |
|-----|-------------|
|
latest | Latest release |
| x.y.z | Specific version (e.g., 1.0.0) |
| x.y | Minor version (e.g., 1.0`) |- MapLibre GL JS - Map rendering
- @geoman-io/maplibre-geoman-free - Basic drawing/editing
- @turf/turf - Geometry operations
MIT License - see LICENSE for details.
- Geoman for the excellent free drawing/editing plugin
- Turf.js for powerful geometry operations
- Inspired by maplibre-gl-layer-control