From 135253d37de762bd50019fd150a7eefdfe16a3fe Mon Sep 17 00:00:00 2001 From: kianiadee Date: Fri, 5 Jun 2026 23:01:30 +0300 Subject: [PATCH] feat(live): zoom-relative marker sizing + live plate/cost-centre filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- index.html | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 36c251d..d79c29d 100644 --- a/index.html +++ b/index.html @@ -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; };