Documentation

Pro handbook

Getting started with Leaflet

Installation#

Not yet published on npm. The command below is what installation will look like once the packages are released. Until then, work inside this repository (a pnpm workspace) or link the packages from it. Their entry points are TypeScript sources, so your bundler must compile TypeScript (Vite does).

npm install @vectorstream/core @vectorstream/editor @vectorstream/leaflet leaflet

leaflet is a peer dependency (^1.9.4); @vectorstream/adapter comes along with @vectorstream/leaflet.

The complete example#

The example below builds everything an editing application needs. It is a real file of this repository (docs/handbuch/beispiele/leaflet-quickstart.ts); a test draws a triangle through it, exports, undoes and switches tools by key.

leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { exportGeoJSON, importGeoJSON, type FeatureCollection } from '@vectorstream/core';
import { Editor, Toolbar, defaultToolbarEntries } from '@vectorstream/editor';
import { LeafletAdapter } from '@vectorstream/leaflet';

export function startEditor(container: HTMLElement, data: FeatureCollection) {
  // 1. The map. x is longitude, y is latitude; Leaflet takes [lat, lng].
  const map = L.map(container).setView([51.5112, 7.4555], 17);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap contributors',
  }).addTo(map);

  // 2. The adapter draws the editor's scene on the map and reports pointer and key events.
  const adapter = new LeafletAdapter(map);

  // 3. The editor works on a partition — here one built from GeoJSON.
  const editor = new Editor(importGeoJSON(data), adapter);

  // 4. The toolbar is state, not UI: it knows the tools and their shortcut keys.
  const toolbar = new Toolbar(editor, defaultToolbarEntries());
  toolbar.select('draw');

  // 5. Every transaction ends in exactly one of three events.
  editor.on('change', ({ label }) => console.log('changed:', label));
  editor.on('unchanged', ({ label }) => console.log('nothing to do:', label));
  editor.on('rejected', ({ label, violations }) => {
    console.warn('rejected:', label, violations.map((v) => v.detail));
  });

  return {
    map,
    editor,
    toolbar,
    // 6. Export the active layer as GeoJSON.
    save: (): FeatureCollection => exportGeoJSON(editor.partition),
    // 7. Undo and redo; `false` means there was nothing to do, or the step was rejected.
    undo: (): boolean => editor.undo(),
    redo: (): boolean => editor.redo(),
    destroy(): void {
      toolbar.destroy();
      editor.destroy();
      adapter.destroy();
      map.remove();
    },
  };
}

The sample data it is tested with — two adjacent parcels:

import type { FeatureCollection } from '@vectorstream/core';

/**
 * Two adjacent parcels in longitude/latitude (x = longitude, y = latitude). They share the
 * boundary at longitude 7.4555 — after import that boundary is one edge, not two.
 */
export const parcels: FeatureCollection = {
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      properties: { parcel: '101', use: 'field' },
      geometry: {
        type: 'Polygon',
        coordinates: [[[7.4525, 51.5105], [7.4555, 51.5105], [7.4555, 51.5119], [7.4525, 51.5119], [7.4525, 51.5105]]],
      },
    },
    {
      type: 'Feature',
      properties: { parcel: '102', use: 'meadow' },
      geometry: {
        type: 'Polygon',
        coordinates: [[[7.4555, 51.5105], [7.4585, 51.5105], [7.4585, 51.5119], [7.4555, 51.5119], [7.4555, 51.5105]]],
      },
    },
  ],
};

/** A building across the boundary between parcel 101 and parcel 102. */
export const buildings: FeatureCollection = {
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      properties: { use: 'house', built: 1965, text: 'House 7' },
      geometry: {
        type: 'Polygon',
        coordinates: [[[7.4543, 51.5109], [7.4567, 51.5109], [7.4567, 51.5115], [7.4543, 51.5115], [7.4543, 51.5109]]],
      },
    },
  ],
};

Step by step#

1. The map. A normal Leaflet map with its default projection. The editor computes in the coordinates the adapter supplies: x is longitude and y is latitude. Leaflet itself takes [lat, lng], which is why setView reads the other way round. The container must have a size, as for any Leaflet map.

2. The adapter. new LeafletAdapter(map, options?) adds its own layer groups to the map, listens to mouse events on the map and to keydown on the document. Keys typed into an input field, a text area, a select box or a contenteditable element are ignored. The only option is ebenenFarbe: (layerId) => string | undefined, the color of a layer; without it, layers get colors from a fixed palette by their ID.

3. The editor. new Editor(partitionOrLayers, adapter, options?) takes either one partition (as here, from importGeoJSON) or a list of layers. Options are tolerancePx (snap radius in pixels, default 8), selection (an initial selection) and geofencing (default true; false leaves rules between layers unchecked) and erweiterungen (a list of extensions plugged into this editor; none by default). The editor draws immediately.

4. The toolbar. new Toolbar(editor, defaultToolbarEntries()) knows the 23 default tools and their shortcut keys. toolbar.select('draw') activates the polygon tool. Until a mode is set, the editor has none and clicks do nothing. See Toolbar for drawing buttons and Modes for the tools.

5. Events. Every edit ends in exactly one of change, unchanged or rejected. See Events.

6. Export. exportGeoJSON(editor.partition) returns the active layer as a GeoJSON FeatureCollection. See GeoJSON.

7. Undo. editor.undo() and editor.redo() return false when there is nothing to do or the step was rejected. The editor binds no keys for them; Undo and redo shows how to bind Ctrl+Z.

Tearing down. Destroy the toolbar, then the editor, then the adapter, then the map. editor.destroy() switches map panning back on if a mode had switched it off and resets the cursor. adapter.destroy() removes its layers and listeners.

Leaflet specifics#

  • Box zoom. Leaflet zooms to a box on Shift+drag. The cut and difference tools use Shift themselves and switch box zoom off while they are active.
  • Container size changes. Leaflet only notices window resizes. If your layout changes the map container's size, call map.invalidateSize(); Leaflet then reports a move, the adapter a view change, and the editor redraws.
  • More than one editor per page. Each adapter listens to keydown on the whole document, so two editors on one page both receive every key.

Next: Faces and edges, Modes, API reference: LeafletAdapter.

See it on a map.