Compare commits

...

2 commits

Author SHA1 Message Date
david kiania
b11d8131a9 Merge: distinct marker icons for specialist vehicles (crane/motorbike/pick-up) 2026-06-08 14:40:59 +03:00
david kiania
38fd7551f9 feat(map): distinct marker icons for specialist vehicles
Crane, Motorbike and Pick-Up now render their own white SVG silhouette inside
the department-coloured pin (via fn_live_positions' new `vehicle_type` field).
Specialists keep full size + colour even when parked so they stay legible and
stand out from the field-service swarm. All other vehicles (field-service +
unassigned) are unchanged — they fall through to the existing arrow/square/dot
marker. Icon-only change; no popup/clustering/filter behaviour altered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:39:46 +03:00

View file

@ -269,6 +269,15 @@
} }
.veh-marker.offline .veh-plate { color: var(--muted); } .veh-marker.offline .veh-plate { color: var(--muted); }
/* Specialist vehicle icon (crane / motorbike / pick-up) — white silhouette
centred in the department-coloured pin. */
.veh-type { display: grid; place-items: center; width: 22px; height: 22px; }
.veh-type svg { width: 20px; height: 20px; display: block; }
/* Keep specialists full-size + circular even when parked so the icon stays
legible (overrides the parked half-size square). */
.veh-marker.has-type.parked .veh-pin { transform: scale(1); border-radius: 50%; }
.veh-marker.has-type.offline .veh-type svg { opacity: .85; }
/* ── Persistent POI marker (Fireside HQ) ───────────────────────────── */ /* ── Persistent POI marker (Fireside HQ) ───────────────────────────── */
/* Cluster bubble (zoomed-out): amber circle + white count, tiered by size. /* Cluster bubble (zoomed-out): amber circle + white count, tiered by size.
Click zooms to expand into the individual pins. */ Click zooms to expand into the individual pins. */
@ -753,15 +762,29 @@ function upsertClusterMarker(id, count, coords) {
} }
} }
// Specialist vehicle types get their own marker icon (white silhouette inside
// the department-coloured pin). All other types (field-service + unassigned)
// fall through to the default arrow/square/dot marker, unchanged. Keys must
// match fn_live_positions' `vehicle_type` exactly (migration 16).
const SPECIALIST_ICONS = {
'Crane': '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21V6"/><path d="M3 6h18"/><path d="M9 3l3 3 3-3"/><path d="M18 6v4"/><path d="M17 10h2v1.6h-2z"/><path d="M8 21h8"/></svg>',
'Motorbike': '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="16" r="3.3"/><circle cx="18.5" cy="16" r="3.3"/><path d="M5.5 16h5l3-5h3.5"/><path d="M10.5 16 14 11"/><path d="M14.5 8H18l1 3"/></svg>',
'Pick-Up': '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 15V8h7l3 3h8v4"/><path d="M2 15h2m5 0h6m4 0h2"/><circle cx="6.5" cy="16.5" r="1.9"/><circle cx="17.5" cy="16.5" r="1.9"/></svg>',
};
function upsertLiveMarker(p, coords, feature) { function upsertLiveMarker(p, coords, feature) {
const state = vehicleState(p); const state = vehicleState(p);
// Active (moving now) = full department colour. Parked (reported within 24h) // Active (moving now) = full department colour. Parked (reported within 24h)
// = a PASTEL of that department colour. Offline (>24h silent) = grey. Lets // = a PASTEL of that department colour. Offline (>24h silent) = grey. Lets
// high-level viewers read fleet activity by department at a glance. // high-level viewers read fleet activity by department at a glance.
const ccColor = colorForCostCentre(p.cost_centre); const ccColor = colorForCostCentre(p.cost_centre);
const color = state === 'offline' ? OFFLINE_COLOR const typeIcon = SPECIALIST_ICONS[p && p.vehicle_type];
let color = state === 'offline' ? OFFLINE_COLOR
: state === 'parked' ? pastelColor(ccColor) : state === 'parked' ? pastelColor(ccColor)
: ccColor; : ccColor;
// Specialists keep the full department colour (not the parked pastel) so the
// white icon stays legible and they stand out from the field-service swarm.
if (typeIcon && state !== 'offline') color = ccColor;
const speed = Number(p.speed || 0); const speed = Number(p.speed || 0);
const dir = Number(p.direction || 0); const dir = Number(p.direction || 0);
let m = liveMarkers.get(p.imei); let m = liveMarkers.get(p.imei);
@ -788,19 +811,28 @@ function upsertLiveMarker(p, coords, feature) {
el.classList.add('veh-marker'); el.classList.add('veh-marker');
el.classList.remove('active', 'parked', 'offline'); el.classList.remove('active', 'parked', 'offline');
el.classList.add(state); el.classList.add(state);
el.classList.toggle('has-type', !!typeIcon);
const pin = el.querySelector('.veh-pin'); const pin = el.querySelector('.veh-pin');
pin.style.setProperty('--c', color); pin.style.setProperty('--c', color);
// Direction arrow only for vehicles moving now. Parked = a clean pastel // Specialist vehicles (crane/motorbike/pick-up) show their own icon instead of
// square (no arrow, no dot). Idling/offline keep the neutral dot. // the heading arrow. Everything else: direction arrow only when moving now,
// a clean pastel square when parked, a neutral dot when idling/offline.
const glyph = el.querySelector('.glyph'); const glyph = el.querySelector('.glyph');
if (state === 'active' && speed > 0) { if (typeIcon) {
glyph.className = 'glyph veh-type';
glyph.style.removeProperty('--dir');
glyph.innerHTML = typeIcon;
} else if (state === 'active' && speed > 0) {
glyph.className = 'glyph veh-arrow'; glyph.className = 'glyph veh-arrow';
glyph.innerHTML = '';
glyph.style.setProperty('--dir', dir + 'deg'); glyph.style.setProperty('--dir', dir + 'deg');
} else if (state === 'parked') { } else if (state === 'parked') {
glyph.className = 'glyph'; // empty — just the pastel square glyph.className = 'glyph'; // empty — just the pastel square
glyph.innerHTML = '';
glyph.style.removeProperty('--dir'); glyph.style.removeProperty('--dir');
} else { } else {
glyph.className = 'glyph idle-dot'; glyph.className = 'glyph idle-dot';
glyph.innerHTML = '';
glyph.style.removeProperty('--dir'); glyph.style.removeProperty('--dir');
} }
el.querySelector('.veh-plate').textContent = plateTail(p.vehicle_number); el.querySelector('.veh-plate').textContent = plateTail(p.vehicle_number);