feat(live): zoom-relative marker sizing + live plate/cost-centre filtering

- Markers now scale with zoom (--veh-scale, ~0.42 at z5 → 1.20 at z14) via a
  transform on .veh-inner, so they no longer bloat at country zoom; pins stay
  anchored on their coordinate (verified 0px drift).
- Selecting a plate or cost centre now filters the LIVE markers immediately and
  recomputes the header KPIs (previously the filter card only fed Show trips, so
  selections didn't reflect on the live map). Time period still applies to trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kianiadee 2026-06-05 23:01:30 +03:00
parent 50163536e3
commit 135253d37d

View file

@ -181,7 +181,12 @@
positioning context is the inner wrapper below, so a class change can't
reflow the markers. */
.veh-marker { cursor: pointer; will-change: transform; }
.veh-inner { position: relative; width: 32px; height: 32px; }
/* Scaled by zoom via --veh-scale (set in updateVehScale). transform-origin
centre keeps the pin anchored on its coordinate as it grows/shrinks. */
.veh-inner {
position: relative; width: 32px; height: 32px;
transform: scale(var(--veh-scale, 1)); transform-origin: center center;
}
.veh-pin {
width: 32px; height: 32px; border-radius: 50%;
background: var(--c, var(--parked));
@ -385,7 +390,8 @@ function ensureMap() {
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right');
popup = new maplibregl.Popup({ closeButton: true, closeOnClick: false, offset: 20 });
popup.on('close', () => { openPopupImei = null; popupStuck = false; });
map.on('load', () => POIS.forEach(addPoiMarker));
map.on('load', () => { POIS.forEach(addPoiMarker); updateVehScale(); });
map.on('zoom', updateVehScale);
map.on('click', e => {
if (mode !== 'live') return;
if (!popupStuck) return;
@ -496,6 +502,9 @@ function renderLive() {
if (still) showLivePopup(still, true);
else { popup.remove(); openPopupImei = null; popupStuck = false; }
}
// Honour the current plate/cost-centre filter + size markers for this zoom.
applyLiveFilters();
updateVehScale();
};
if (map.isStyleLoaded()) drawMarkers(); else map.once('load', drawMarkers);
@ -714,13 +723,46 @@ function applyVehicleAutoFilter() {
const metas = selected.map(p => VEHICLE_META.get(p)).filter(Boolean);
const sharedCC = collapse(metas.map(m => m.cost_centre));
setSelectValue('f-cc', sharedCC ?? '');
applyLiveFilters(); // reflect the plate selection on the live map immediately
}
function collapse(values) { if (!values.length) return null; const f = values[0]; return values.every(v => v === f) ? f : null; }
function setSelectValue(id, value) { const el = document.getElementById(id); const opt = Array.from(el.options).find(o => o.value === value); el.value = opt ? value : ''; }
// Scale the live markers with the zoom level so they don't bloat at country
// zoom or vanish when zoomed in. Linear from z5 → z14.
function updateVehScale() {
if (!map) return;
const t = Math.max(0, Math.min(1, (map.getZoom() - 5) / 9));
document.getElementById('map').style.setProperty('--veh-scale', (0.42 + t * 0.78).toFixed(3));
}
// Filter the LIVE markers by the selected plate(s) + cost centre, and recompute
// the header KPIs to match. Time period only applies to trips, not live.
function applyLiveFilters() {
if (mode !== 'live') return;
const plates = new Set(Array.from(document.getElementById('f-vehicle').selectedOptions).map(o => o.value).filter(Boolean));
const cc = document.getElementById('f-cc').value;
let total = 0, moving = 0, parked = 0, offline = 0; const speeds = [];
(lastLivePayload?.geojson?.features || []).forEach(f => {
const p = f.properties; const m = liveMarkers.get(p.imei); if (!m) return;
const pass = (plates.size === 0 || plates.has(p.vehicle_number)) && (!cc || p.cost_centre === cc);
m.getElement().style.display = pass ? '' : 'none';
if (!pass) return;
total++;
const st = vehicleState(p);
if (st === 'offline') offline++;
else if (st === 'active') { moving++; const sp = Number(p.speed || 0); if (sp > 0) speeds.push(sp); }
else parked++;
});
speeds.sort((a, b) => a - b);
renderLiveKPIs({ total, moving, parked, offline, median: speeds.length ? speeds[Math.floor(speeds.length / 2)] : null, last_batch_utc: lastLivePayload?.summary?.last_batch_utc });
}
document.getElementById('f-period').addEventListener('change', e => {
document.getElementById('custom').classList.toggle('show', e.target.value === 'custom');
});
// Selecting a cost centre filters the live map immediately.
document.getElementById('f-cc').addEventListener('change', applyLiveFilters);
function periodToRange(period, cs, ce) {
const today = new Date(); const fmt = d => d.toISOString().slice(0, 10);
const minus = n => { const x = new Date(today); x.setDate(x.getDate() - n); return x; };