DEV Community

Cover image for Draw and edit network‑snapped tracks in OpenLayers with edittrack
Frédéric Junod for Camptocamp Geospatial Solutions

Posted on Edited on

Draw and edit network‑snapped tracks in OpenLayers with edittrack

Planning a hike, a bike ride, or any route that should follow a real‑world network? edittrack lets users draw and edit tracks that snap to routing services , enrich them with elevation profiles, and manage POIs—all directly on an OpenLayers map.

This post gives you a quick tour, shows how to wire it up, and highlights tips from the built‑in demos.

What edittrack does

edittrack is a lightweight UI library focused on interactive track editing:

  • Snapped segments: draw lines that follow a network via routers (GraphHopper or OSRM)
  • Control points: add/move/delete points that define and modify segments
  • POIs: add metadata‑bearing points anywhere along your track
  • Elevation profiles: compute per‑segment XYZM profiles via profilers (Swisstopo, extract from geometry, or a fallback chain)
  • History: undo/redo across all edits
  • Densification: optional point insertion to improve geometry quality between router samples
  • Shadow track and mask: visualize original track state while editing and constrain drawing to an extent

The core class you’ll interact with is TrackManager.

Install

npm install @geoblocks/edittrack ol
Enter fullscreen mode Exit fullscreen mode
  • Edittrack is ESM and ships TypeScript types
  • ol (OpenLayers) is a peer dependency

Quickstart

Below is a minimal setup using OSRM for routing and a profiler chain that tries to reuse segment Z values first, then falls back to Swisstopo for high‑quality elevation (if you work in Switzerland and have EPSG:2056 registered).

import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import {Style, Stroke, Circle as CircleStyle, Fill} from 'ol/style';

import {
  TrackManager,
  OSRMRouter,
  ExtractFromSegmentProfiler,
  FallbackProfiler,
  SwisstopoProfiler,
  SnappedDensifier,
} from '@geoblocks/edittrack';

// Basic map and layers
const trackLayer = new VectorLayer({ source: new VectorSource() });
const shadowTrackLayer = new VectorLayer({ source: new VectorSource() });
const map = new Map({
  target: 'map',
  view: new View({ center: [0, 0], zoom: 2 }),
  layers: [
    new TileLayer({ source: new OSM() }),
    shadowTrackLayer,
    trackLayer
  ],
});

// Router (provide your own OSRM/GraphHopper URL)
const router = new OSRMRouter({
  map,
  url: 'https://router.project-osrm.org/route/v1/driving',
  // optional:
  // extraParams: 'annotations=true',
  // radius: 10000,
});

// Profiler chain (first try existing Z, then Swisstopo)
const profiler = new FallbackProfiler({
  profilers: [
    new ExtractFromSegmentProfiler({ projection: map.getView().getProjection() }),
    new SwisstopoProfiler({ projection: map.getView().getProjection() }),
  ],
});

// Optional densifier to insert points between router samples
const densifier = new SnappedDensifier({
  optimalPointDistance: 10, // meters
  maxPointDistance: 80,
  maxPoints: 8000,
});

// Style (use your own StyleLike/FlatStyleLike)
const style = [
  new Style({ stroke: new Stroke({ color: '#2563eb', width: 3 }) }),
  new Style({ image: new CircleStyle({ radius: 5, fill: new Fill({ color: '#111827' }) }) }),
];

// Track manager
const tm = new TrackManager({
  map,
  trackLayer,
  shadowTrackLayer,
  router,
  profiler,
  densifier,
  style,
  hitTolerance: 10, // px
  // deleteCondition, addLastPointCondition, addControlPointCondition are optional
  // drawExtent, drawMaskColor are optional
});

// Start editing
tm.mode = 'edit';

// Listen to changes (update a profile or UI)
tm.addTrackChangeEventListener(() => {
  // e.g., recompute a merged elevation profile using tm.getSegments()
});

// Add an initial point by letting users click on the map (TrackInteraction handles it)
Enter fullscreen mode Exit fullscreen mode

Working with the track

  • Add/move points: in edit mode, click to add control points; drag points or segments to update routing
  • Toggle snapping: tm.snapping = true | false (if false, segments become straight lines)
  • Undo/Redo: await tm.undo() / await tm.redo()
  • Reverse: await tm.reverse(true) to re‑route, or await tm.reverse(false) to just flip geometry and refresh profiles
  • POIs:
    • Add: tm.addPOI([x, y], {name: 'Café'})
    • Update meta: tm.updatePOIMeta(index, meta)
    • Delete: tm.deletePOI(index)
  • Save/restore state:
  const snapshot = [
    ...tm.getControlPoints(),
    ...tm.getSegments(),
    ...tm.getPOIs(),
  ];
  await tm.restoreFeatures(snapshot);
Enter fullscreen mode Exit fullscreen mode
  • Hover feedback: subscribe and map distance to your UI (e.g., highlight point on a chart)
  tm.addTrackHoverEventListener((distanceFromStart) => {
    // distanceFromStart in meters along the full track (if available)
  });
Enter fullscreen mode Exit fullscreen mode

Constraining drawing and visual aids

  • Draw extent and mask: pass drawExtent and optionally drawMaskColor to restrict editing to a region and render a mask overlay
  • Shadow track: when entering edit mode, the current track is cloned into shadowTrackLayer so you can see what changed

Routers and profilers

  • Routers:
    • GraphHopper (GraphHopperRouter): snaps segments to a GraphHopper backend
    • OSRM (OSRMRouter): snaps via OSRM; supports radius, extraParams, and pixel‑based maxRoutingTolerance inherited from RouterBase
  • Profilers:
    • SwisstopoProfiler: high‑quality elevation profile for CH (register EPSG:2056)
    • ExtractFromSegmentProfiler: reuse geometry Z values if present
    • FallbackProfiler: try multiple profilers in order and use the first that succeeds

Each routed segment stores:

  • segment.get('snapped'): boolean
  • segment.get('profile'): Coordinate[] with [x, y, z, m], where m is cumulative distance from the segment start
  • segment.get('surfaces'): optional array of surface ranges (when supported by the router)

Tips from the demos

  • Densification improves elevation smoothness and downstream analytics by adding intermediate points while keeping a hard cap (maxPoints) to avoid oversized geometries
  • Keep hitTolerance between 10–20 px for easier selection on touch devices
  • If you want to require a modifier key to delete, pass a deleteCondition that checks the event (see demos/simple/demo.js)

TypeScript and modules

  • Full type definitions are published under lib/types
  • Package is ESM‑only; use modern bundlers (Vite, Parcel, Webpack 5+, etc.)

Links

License

BSD‑3‑Clause

Top comments (0)