DEV Community

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)