maplibre-gl-geo-editor
v0.5.0
Published
Advanced geometry editing plugin for MapLibre GL with Geoman integration
Maintainers
Readme
maplibre-gl-geo-editor
A 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.
Features
Draw Tools
- Polygon - Draw polygons by clicking points
- Line - Draw polylines
- Rectangle - Draw rectangles
- Circle - Draw circles
- Marker - Place point markers
- Freehand - Draw shapes by dragging (custom implementation)
Basic Edit Tools (via Geoman Free)
- Drag - Move features on the map
- Edit - Modify feature vertices
- Rotate - Rotate features
- Cut - Cut holes in polygons
- Delete - Remove selected features (supports multi-select)
Advanced Edit Tools (Custom Implementation)
- Select - Click to select features (shows properties popup when enabled)
- Scale - Resize features with interactive handles
- Copy - Duplicate features (Ctrl+C/V support)
- Split - Split polygons/lines with a drawn line
- Union - Merge multiple polygons into one
- Difference - Subtract one polygon from another
- Simplify - Reduce vertices using Douglas-Peucker algorithm
- Lasso - Select multiple features by drawing a polygon (supports union/difference/drag)
- Reset - Clear selection and disable active tools (toolbar button)
Attribute Editing
- Side Panel - Edit feature properties in a slide-out panel
- Schema Support - Define field types per geometry (polygon, line, point)
- Field Types - String, number, boolean, select, date, color, textarea
- Default Values - Auto-apply defaults to newly created features
- Validation - Required field validation with error display
- Extra Properties - Non-schema properties shown as read-only
File Operations
- Open - Load GeoJSON file from disk (auto-zooms to extent when enabled)
- Save - Download current features as GeoJSON file
History (Undo/Redo)
- Undo - Revert the last create, edit, or delete operation (Ctrl+Z)
- Redo - Reapply the last undone operation (Ctrl+Y)
- Configurable history size (default: 50 operations)
Installation
npm install maplibre-gl-geo-editor @geoman-io/maplibre-geoman-free maplibre-glUsage
Basic Usage (Vanilla JS/TS)
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');
});
});Attribute Editing
Enable the attribute editing panel to edit feature properties with a schema-based form:
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
// 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();React Usage
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 (
<div style={{ width: '100%', height: '100vh' }}>
<div ref={mapContainer} style={{ width: '100%', height: '100%' }} />
{map && geoman && (
<GeoEditorReact
map={map}
geoman={geoman}
position="top-left"
drawModes={['polygon', 'line', 'marker', 'freehand']}
editModes={['select', 'drag', 'change', 'scale', 'copy', 'union', 'split']}
showFeatureProperties={true}
fitBoundsOnLoad={true}
/>
)}
</div>
);
}API Reference
GeoEditorOptions
| 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 |
Methods
// 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 objectEvents
Listen for events on the map container:
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:
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
# Install dependencies
npm install
# Start development server
npm run dev
# Run tests
npm test
# Build
npm run buildDocker
The examples can be run using Docker. The image is automatically built and published to GitHub Container Registry.
Pull and Run
# Pull the latest image
docker pull ghcr.io/opengeos/maplibre-gl-geo-editor:latest
# Run the container
docker run -p 8080:80 ghcr.io/opengeos/maplibre-gl-geo-editor:latestThen open http://localhost:8080/maplibre-gl-geo-editor/ in your browser to view the examples.
Build Locally
# Build the image
docker build -t maplibre-gl-geo-editor .
# Run the container
docker run -p 8080:80 maplibre-gl-geo-editorAvailable Tags
| Tag | Description |
|-----|-------------|
| latest | Latest release |
| x.y.z | Specific version (e.g., 1.0.0) |
| x.y | Minor version (e.g., 1.0) |
Dependencies
- MapLibre GL JS - Map rendering
- @geoman-io/maplibre-geoman-free - Basic drawing/editing
- @turf/turf - Geometry operations
License
MIT License - see LICENSE for details.
Credits
- Geoman for the excellent free drawing/editing plugin
- Turf.js for powerful geometry operations
- Inspired by maplibre-gl-layer-control
