Compare commits

..

No commits in common. "fc5a7ed31b562a46f4e9f3efa0466f48eeb9de48" and "b11d8131a96e2b2eeb4825965c82e48a34c17bd6" have entirely different histories.

View file

@ -278,24 +278,6 @@
.veh-marker.has-type.parked .veh-pin { transform: scale(1); border-radius: 50%; }
.veh-marker.has-type.offline .veh-type svg { opacity: .85; }
/* ── Cost-centre colour key (collapsible, tidy) ────────────────────── */
#legend { position: absolute; left: 10px; bottom: 12px; z-index: 5;
font: 600 11px system-ui; color: #fff; user-select: none; }
.legend-toggle { cursor: pointer; border: 1px solid var(--border);
background: rgba(15,18,23,.92); color: #fff; font: 600 11px system-ui;
padding: 4px 10px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,.45); }
.legend-toggle::before { content: '◑'; color: var(--accent); margin-right: 5px; }
.legend-body { margin-top: 6px; background: rgba(15,18,23,.92);
border: 1px solid var(--border); border-radius: 8px; padding: 7px 9px;
box-shadow: 0 4px 14px rgba(0,0,0,.5); max-height: 38vh; overflow-y: auto;
display: grid; gap: 4px; min-width: 124px; }
#legend.collapsed .legend-body { display: none; }
.legend-row { display: flex; align-items: center; gap: 7px; }
.legend-dot { width: 11px; height: 11px; border-radius: 3px; flex: 0 0 auto;
border: 1px solid rgba(255,255,255,.5); }
.legend-lbl { flex: 1; text-transform: uppercase; letter-spacing: .02em; }
.legend-n { color: var(--muted); font-weight: 700; }
/* ── Persistent POI marker (Fireside HQ) ───────────────────────────── */
/* Cluster bubble (zoomed-out): amber circle + white count, tiered by size.
Click zooms to expand into the individual pins. */
@ -397,13 +379,6 @@
<div id="map">
<div class="placeholder" id="placeholder">Loading live fleet…</div>
<!-- Cost-centre colour key — collapsed by default to keep the map tidy.
Lists only centres currently on screen; safe to delete this block + its
CSS/JS (renderLegend) to remove the feature entirely. -->
<div id="legend" class="collapsed" aria-label="Cost-centre colour key">
<button type="button" class="legend-toggle" id="legend-toggle">Key</button>
<div class="legend-body" id="legend-body"></div>
</div>
</div>
<!-- Bottom dock: two tiers — filter tier (top) + trip-card tier (beneath) -->
@ -480,26 +455,10 @@ const STALE_GPS_MS = 10 * 60 * 1000;
const OFFLINE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
const POIS = [{ name: 'Fireside HQ', lng: 36.728785, lat: -1.2411485 }];
// Deliberate, distinct colour per cost centre so all vehicles in a centre share
// one colour and different centres are easy to tell apart at a glance. Keys are
// normalised (lowercase, trimmed). Anything not listed falls back to a stable
// hash of COST_CENTRE_PALETTE.
const COST_CENTRE_COLORS = {
'isp': '#3b82f6', // blue
'osp': '#E8954A', // brand amber
'osp patrol': '#f97316', // orange (OSP sibling)
'fds': '#22c55e', // green
'roll out': '#a855f7', // purple
'general': '#fbbf24', // gold
'regional': '#ec4899', // pink
'planning': '#06b6d4', // cyan
'deliveries': '#84cc16', // lime
'qehs': '#14b8a6', // teal
'airtel': '#ef4444', // brand red
};
// Fallback palette for any centre not in COST_CENTRE_COLORS (stable per name via hash).
// Cost-centre palette — categorical, brand colours lead. Stable per centre via hash.
const COST_CENTRE_PALETTE = [
'#2dd4a7', '#8b5cf6', '#f43f5e', '#0ea5e9', '#eab308', '#d946ef', '#10b981',
'#E8954A', '#2dd4a7', '#3b82f6', '#a855f7', '#f43f5e', '#06b6d4',
'#84cc16', '#ec4899', '#fbbf24', '#14b8a6', '#8b5cf6', '#f97316', '#22c55e',
];
const UNKNOWN_CC_COLOR = '#9ca3af';
const PARKED_COLOR = '#6b7280';
@ -513,10 +472,8 @@ const seqColor = n => SEQ_PALETTE[((n || 1) - 1 + SEQ_PALETTE.length * 100) % SE
function colorForCostCentre(cc) {
if (!cc) return UNKNOWN_CC_COLOR;
const key = String(cc).trim().toLowerCase();
if (COST_CENTRE_COLORS[key]) return COST_CENTRE_COLORS[key];
let h = 0;
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0;
for (let i = 0; i < cc.length; i++) h = (h * 31 + cc.charCodeAt(i)) | 0;
return COST_CENTRE_PALETTE[Math.abs(h) % COST_CENTRE_PALETTE.length];
}
// Pastel tint of a #rrggbb colour — blended toward white. Used for recently-
@ -757,27 +714,6 @@ function applyLiveFilters() {
});
speeds.sort((a, b) => a - b);
renderLiveKPIs({ total: filtered.length, moving, parked, offline, median: speeds.length ? speeds[Math.floor(speeds.length / 2)] : null, last_batch_utc: lastLivePayload?.summary?.last_batch_utc });
renderLegend(filtered);
}
// Compact colour key — lists only the cost centres currently on the map (respects
// the active filter), sorted by count. Rebuilt on every live render so it stays
// in sync. Collapsed by default; toggled by the "Key" pill.
function renderLegend(features) {
const body = document.getElementById('legend-body');
const wrap = document.getElementById('legend');
if (!body || !wrap) return;
const counts = new Map();
(features || []).forEach(f => {
const key = ((f.properties && f.properties.cost_centre) || '').trim() || '(none)';
counts.set(key, (counts.get(key) || 0) + 1);
});
const rows = [...counts.entries()].sort((a, b) => b[1] - a[1]);
wrap.style.display = rows.length ? '' : 'none';
body.innerHTML = rows.map(([cc, n]) => {
const color = cc === '(none)' ? UNKNOWN_CC_COLOR : colorForCostCentre(cc);
return `<div class="legend-row"><span class="legend-dot" style="background:${color}"></span><span class="legend-lbl">${escapeHtml(cc)}</span><span class="legend-n">${n}</span></div>`;
}).join('');
}
// Query the cluster index for the current viewport+zoom and draw either count
@ -1460,9 +1396,6 @@ function backToLive() {
startPolling();
}
document.getElementById('live-pill').addEventListener('click', backToLive);
document.getElementById('legend-toggle').addEventListener('click', () => {
document.getElementById('legend').classList.toggle('collapsed');
});
// ============================================================================
// Reverse-geocoding (Nominatim) — queued, 1 req/sec, in-memory cache