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; };