Nine task files matching Phase 1's shape (Goal / Deliverables / Spec / Acceptance / Risks / Done). README updated with full sequencing diagram, files-modified outline, tech stack additions, design rules, and phase acceptance. | # | Task | | --- | --------------------------------------------------------------------- | | 2.1 | MapView singleton + mapReady gate | | 2.2 | Tile-source switcher (Esri / OpenTopoMap / OSM / optional Google) | | 2.3 | Sprite preload — 7 racing categories x 4 colour variants | | 2.4 | WS client + rAF coalescer + Zustand position store + connection store | | 2.5 | MapPositions — clustered + selected sources | | 2.6 | MapTrails — bounded ring buffer, polyline rendering | | 2.7 | Event picker — TanStack Query + WS subscription orchestration | | 2.8 | Camera control trio — default-fit / selected-follow / one-shot | | 2.9 | Connection status + per-device last-seen indicators | Sequencing: 2.1 and 2.4 are parallel foundations (singleton vs data pipeline). Once both land, 2.5 / 2.6 / 2.7 / 2.9 fan out independently. 2.2 / 2.3 only need 2.1. 2.8 sits at the end on top of 2.1 + 2.5. Each task documents its deliverables down to file paths + interface shapes, includes concrete code sketches in the Specification, lists explicit out-of-scope items, and surfaces risks for the implementer to think about. An agent (or future me) can pick up any single task and ship it without re-deriving the design from the wiki. Resolved Phase 2 design decisions baked into the task files: - Trails: flat-colour-per-device for v1, defer speed-coloured segments to a Phase 3 polish task. - Cluster params: 14/50 (traccar default); tune after seeing real data. - Event picker placement: top-left dropdown. - Multi-event: out — single-select, one event at a time. - Stale-position visual: fade icon opacity; defer warning badges.
7.5 KiB
Task 2.6 — MapTrails (bounded ring buffer, polyline rendering)
Phase: 2 — Live monitoring map
Status: ⬜ Not started
Depends on: 2.1, 2.4.
Wiki refs: docs/wiki/concepts/maps-architecture.md §"Routes (history and live trails)"; docs/wiki/sources/traccar-maps-architecture.md §8.
Goal
Render the trailing path of every (or only the selected) device as a polyline. Reads from the per-device ring buffer in the position store (2.4 capped at 200 points). Visible as a thin line behind the moving marker — operators see "where this racer has been in the last few minutes."
After this task lands, the live map shows motion history, not just current position. That's what makes "live tracking" feel live.
Deliverables
src/map/layers/map-trails.tsx—<MapTrails />side-effect-only component:- Returns
null. - Two-effect pattern: setup adds one GeoJSON source + one line layer; update calls
setData. - Reads
trailsByDeviceandselectedDeviceIdfrom the position store. - Honours a user preference:
none/selected/allfor which devices' trails render.
- Returns
src/live/position-store.tsupdated to exposetrailModepreference ('none' | 'selected' | 'all', default'selected') and a setter. Persisted via Zustandpersistmiddleware on the prefs slice (or a separate prefs store — same shape as the map-pref store from 2.2).- A toggle UI control in the basemap-switcher card (or a sibling control): "Trails: none / selected / all". 3-state toggle.
- Decision: speed-coloured per-segment. Two flavours possible (see Specification below). For v1 of this task, start with flat colour per device (the simpler path). Add a feature-flagged speed-coloured variant if there's appetite during the build; defer to a Phase 3 polish task otherwise.
Specification
Single source, single layer
// Setup effect:
map.addSource(trailsId, {
type: 'geojson',
data: emptyFC,
});
map.addLayer(
{
id: trailsLineId,
source: trailsId,
type: 'line',
layout: {
'line-join': 'round',
'line-cap': 'round',
},
paint: {
'line-color': ['get', 'color'],
'line-width': 2,
'line-opacity': 0.85,
},
},
// Insert BEFORE map-positions' icon layers so trails sit under markers.
// The id of the first non-selected symbol layer from 2.5 is the anchor.
firstSymbolLayerId,
);
The third arg to addLayer puts the line layer below the position symbols; trails draw under markers without obscuring them.
Feature builder — flat colour per device
function buildTrailFeature(deviceId: string, points: PositionEntry[], device?: Device): Feature | null {
if (points.length < 2) return null;
return {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: points.map((p) => [p.lon, p.lat]),
},
properties: {
deviceId,
// Flat colour per device's status (or category-driven palette).
color: deviceColour(device),
},
};
}
function deviceColour(device?: Device): string {
// Defer to a per-class palette later. For v1, a small rotating set keyed by deviceId hash:
const palette = ['#2563C8', '#2E8C4A', '#6B46C1', '#188C8A', '#C9296F', '#5A5A53'];
const h = hashString(device?.id ?? 'default');
return palette[h % palette.length];
}
Per-device colour means operators can visually track who's whose trail without reading labels.
Update effect
useEffect(() => {
const features: Feature[] = [];
if (trailMode === 'none') {
// empty FC
} else if (trailMode === 'selected' && selectedDeviceId) {
const points = trailsByDevice.get(selectedDeviceId) ?? [];
const f = buildTrailFeature(selectedDeviceId, points, devices.get(selectedDeviceId));
if (f) features.push(f);
} else if (trailMode === 'all') {
for (const [deviceId, points] of trailsByDevice) {
const f = buildTrailFeature(deviceId, points, devices.get(deviceId));
if (f) features.push(f);
}
}
(map.getSource(trailsId) as maplibregl.GeoJSONSource | undefined)?.setData({
type: 'FeatureCollection',
features,
});
}, [trailMode, selectedDeviceId, trailsByDevice, devices]);
Speed-coloured per-segment (deferred decision)
Traccar's replay mode renders one line segment per consecutive position pair, coloured by the second point's speed. For live trails this would mean N-1 features per device per update — heavier but visually richer. Operators glance at colour to read "fast / slow / stopped."
If we decide to ship this in v1: replace LineString with multiple two-point LineStrings and colour per segment. The setData payload grows; the line layer's 'line-color' becomes ['get', 'segmentColor'] instead of the device's flat colour.
For 2.6 v1: flat colour, deferred speed-colouring to a Phase 3 polish task. Decision rationale documented in the open question below.
Trail length
MAX_TRAIL_LENGTH = 200 (from 2.4's constants). At 1Hz position rate, that's 200 seconds (~3.3 minutes) of trail. At 0.2Hz (default Teltonika rate), it's ~17 minutes. Both are reasonable.
If operators want a longer-trail mode: a slider in the prefs (50 / 200 / 500 / 1000). Out of scope for 2.6 — defer to a Phase 3 polish task.
What this task does NOT include
- Speed-coloured segments. Decided as flat for v1; revisit in Phase 3 polish.
- Trail-length user control. Hardcoded to 200 via 2.4's constant.
- Trail persistence across page reloads. Trails reset on every reload (live state, not durable).
- Snapshotting old trails. No "show me the last 24h of this device" — that's replay (Phase 4).
Acceptance criteria
pnpm typecheck,pnpm lint,pnpm format:check,pnpm buildclean.- With the dogfood seed and synthetic positions,
/monitorshows a polyline trailing each device as it moves. - Switching the trail-mode toggle (none / selected / all) immediately changes which trails render. None → empty layer. Selected → only the selected device. All → every device.
- Trail follows the device's most recent N positions; old points fall off the front as new ones append (verify trail is always exactly the configured length once steady-state).
- Trails sit underneath markers (verifiable visually — markers paint over the polyline).
- Style swap → trails reappear after the basemap change.
- No
setDatawarnings (e.g. coordinate-array mismatch).
Risks / open questions
- Speed-coloured segments — ship in v1? Decided "no, defer". Reasons: (a) flat colour is simpler and visually cleaner; (b) operators don't actually need a real-time speed colourmap when they have the moving marker's status colour; (c) the per-segment feature explosion at high update rates is a minor perf concern. Revisit if early dogfood feedback says the trail isn't informative enough.
- Visual clutter at
trailMode: 'all'. With 50+ devices in a tight rally area, all trails on screen is noisy. Operators will probably default to'selected'. The default is'selected'. Acceptable. - Trail accuracy. With
faultyfiltering on the snapshot but NOT on streaming positions (per processor-ws-contract §"Faulty-flag visibility"), trails can include positions that an operator later flags faulty. Live trail = observed-as-streamed; correction is post-hoc, not retro-active. Document. - Trail-mode toggle placement. Adjacent to the basemap-switcher (2.2) is the natural home — same floating-card pattern. UI-wise, three small radio buttons. Operator-facing label: "Trails: none / selected / all" or "Trail mode: …".
Done
(Filled in when the task lands.)