Per-trip colour coding in single-vehicle mode
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:
parent
c7369caf71
commit
281a5ec634
2 changed files with 54 additions and 11 deletions
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue