npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

maplibre-gl-geo-editor

v0.5.0

Published

Advanced geometry editing plugin for MapLibre GL with Geoman integration

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.

npm version npm downloads License: MIT Open in CodeSandbox Open in StackBlitz

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-gl

Usage

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 object

Events

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 build

Docker

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:latest

Then 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-editor

Available 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

License

MIT License - see LICENSE for details.

Credits