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
.
- API docs: geoblocks.github.io/edittrack/api
- Live demos: simple and schm
Install
npm install @geoblocks/edittrack ol
- 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)
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, orawait 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)
- Add:
- Save/restore state:
const snapshot = [
...tm.getControlPoints(),
...tm.getSegments(),
...tm.getPOIs(),
];
await tm.restoreFeatures(snapshot);
- 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)
});
Constraining drawing and visual aids
- Draw extent and mask: pass
drawExtent
and optionallydrawMaskColor
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; supportsradius
,extraParams
, and pixel‑basedmaxRoutingTolerance
inherited fromRouterBase
- GraphHopper (
- 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 (seedemos/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
- Docs: geoblocks.github.io/edittrack/api
- Demos: simple, schm
- Source: github.com/geoblocks/edittrack
- npm: @geoblocks/edittrack
License
BSD‑3‑Clause
Top comments (0)