Per-trip colour coding in single-vehicle mode
Some checks are pending
build / lint-test (push) Waiting to run
build / build-push (push) Blocked by required conditions

When viewing one driver's day, each trip is drawn in its own colour
from a 12-colour palette (cycling past 12). The matching colour shows
as the left edge of the trip card so card ↔ polyline pair up at a
glance.

Trip selection moves from border-swap to an outline, so the
trip-colour swatch stays visible when a card is selected.

Multi-mode (>1 vehicle) still uses per-vehicle SELECTION_PALETTE so a
fleet comparison reads as one colour per driver.
This commit is contained in:
kianiadee 2026-05-27 23:31:57 +03:00
parent c7369caf71
commit 281a5ec634
2 changed files with 54 additions and 11 deletions

View file

@ -623,6 +623,26 @@ const SELECTION_PALETTE = [
'#84cc16', // lime '#84cc16', // lime
]; ];
// 12-colour palette for per-trip colouring inside a single vehicle's day.
// Cycles past 12 (rare).
const TRIP_PALETTE = [
'#ef4444', // red
'#f97316', // orange
'#f59e0b', // amber
'#84cc16', // lime
'#10b981', // emerald
'#06b6d4', // cyan
'#3b82f6', // blue
'#8b5cf6', // violet
'#ec4899', // pink
'#facc15', // yellow
'#14b8a6', // teal
'#a855f7', // purple
];
function _tripColor(tripId) {
return TRIP_PALETTE[((tripId - 1) % TRIP_PALETTE.length + TRIP_PALETTE.length) % TRIP_PALETTE.length];
}
let _tripAnimRAF = null; let _tripAnimRAF = null;
// vehicleId → { plate, driver, color, payload | null } // vehicleId → { plate, driver, color, payload | null }
const _selection = new Map(); const _selection = new Map();
@ -723,20 +743,27 @@ async function _fetchAndDraw(map, vid) {
); );
} catch (err) { } catch (err) {
entry.payload = { error: err.message || String(err), trips: [] }; entry.payload = { error: err.message || String(err), trips: [] };
return;
} }
// Draw this vehicle's all-day routes as a static overlay. // Drawing decision is mode-dependent and happens in _renderDock,
_drawVehicleDayPaths(map, vid, entry.payload, entry.color); // so just stash the payload here.
} }
function _drawVehicleDayPaths(map, vid, payload, color) { function _drawVehicleDayPaths(map, vid, payload, fixedColor) {
// If `fixedColor` is set (multi-mode), every trip uses that colour and the
// vehicle reads as one route. If null (single-mode), each trip is coloured
// independently from TRIP_PALETTE so the day's segments are visually
// distinguishable.
_clearVehicleLayers(map, vid); _clearVehicleLayers(map, vid);
const trips = (payload.trips || []).filter(t => t.path && t.path.coordinates); const trips = (payload.trips || []).filter(t => t.path && t.path.coordinates);
if (trips.length === 0) return; if (trips.length === 0) return;
const features = trips.map(t => ({ const features = trips.map(t => ({
type: 'Feature', type: 'Feature',
geometry: t.path, geometry: t.path,
properties: { trip_id: t.trip_id, vehicle_id: vid }, properties: {
trip_id: t.trip_id,
vehicle_id: vid,
color: fixedColor || _tripColor(t.trip_id),
},
})); }));
const srcId = `vroute-${vid}`; const srcId = `vroute-${vid}`;
const layerId = `vroute-line-${vid}`; const layerId = `vroute-line-${vid}`;
@ -750,7 +777,7 @@ function _drawVehicleDayPaths(map, vid, payload, color) {
source: srcId, source: srcId,
layout: { 'line-join': 'round', 'line-cap': 'round' }, layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { paint: {
'line-color': color, 'line-color': ['get', 'color'],
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1.5, 14, 3, 17, 5], 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1.5, 14, 3, 17, 5],
'line-opacity': 0.85, 'line-opacity': 0.85,
}, },
@ -773,12 +800,23 @@ function _renderDock(map, els) {
return; return;
} }
// Redraw all polylines from scratch so we transition cleanly between
// single-mode (per-trip colours) and multi-mode (per-vehicle colour).
for (const [vid] of _selection) _clearVehicleLayers(map, vid);
if (_selection.size === 1) { if (_selection.size === 1) {
const [[vid, entry]] = _selection;
if (entry.payload && !entry.payload.error) {
_drawVehicleDayPaths(map, vid, entry.payload, null); // per-trip palette
}
_renderSingle(map, els); _renderSingle(map, els);
} else { } else {
for (const [vid, entry] of _selection) {
if (entry.payload && !entry.payload.error) {
_drawVehicleDayPaths(map, vid, entry.payload, entry.color);
}
}
_renderMulti(map, els); _renderMulti(map, els);
} }
// Fit map to union of all selected vehicles' route bounds
_fitSelectionBounds(map); _fitSelectionBounds(map);
} }
@ -808,8 +846,10 @@ function _renderSingle(map, els) {
els.list.innerHTML = '<div class="trip-list-empty">No trips on this day.</div>'; els.list.innerHTML = '<div class="trip-list-empty">No trips on this day.</div>';
return; return;
} }
els.list.innerHTML = trips.map(trip => ` els.list.innerHTML = trips.map(trip => {
<div class="trip-card" data-trip-id="${trip.trip_id}"> const color = _tripColor(trip.trip_id);
return `
<div class="trip-card" data-trip-id="${trip.trip_id}" style="border-left:3px solid ${color}">
<div class="trip-card-top"> <div class="trip-card-top">
<span>Trip ${trip.trip_id}</span> <span>Trip ${trip.trip_id}</span>
<span>${_fmtNum(trip.distance_km, 1)} km</span> <span>${_fmtNum(trip.distance_km, 1)} km</span>
@ -823,7 +863,8 @@ function _renderSingle(map, els) {
${_reasonLabel(trip.end_reason)} ${_reasonLabel(trip.end_reason)}
</div> </div>
</div> </div>
`).join(''); `;
}).join('');
for (const card of els.list.querySelectorAll('.trip-card')) { for (const card of els.list.querySelectorAll('.trip-card')) {
card.addEventListener('click', () => { card.addEventListener('click', () => {
els.list.querySelectorAll('.trip-card.selected') els.list.querySelectorAll('.trip-card.selected')

View file

@ -165,7 +165,9 @@
cursor: pointer; border-left: 3px solid transparent; cursor: pointer; border-left: 3px solid transparent;
} }
.trip-card:hover { background: #111a2c; } .trip-card:hover { background: #111a2c; }
.trip-card.selected { border-left-color: var(--accent); } /* Don't change border-left on selection — that's the per-trip colour swatch.
Use an outline instead so the trip colour stays readable. */
.trip-card.selected { background: #111a2c; outline: 2px solid var(--accent); outline-offset: 1px; }
.trip-card-top { .trip-card-top {
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
font-size: 12.5px; font-weight: 600; margin-bottom: 4px; font-size: 12.5px; font-weight: 600; margin-bottom: 4px;