Compare commits
2 commits
b11d8131a9
...
fc5a7ed31b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5a7ed31b | ||
|
|
e55cfadb1c |
1 changed files with 71 additions and 4 deletions
75
index.html
75
index.html
|
|
@ -278,6 +278,24 @@
|
||||||
.veh-marker.has-type.parked .veh-pin { transform: scale(1); border-radius: 50%; }
|
.veh-marker.has-type.parked .veh-pin { transform: scale(1); border-radius: 50%; }
|
||||||
.veh-marker.has-type.offline .veh-type svg { opacity: .85; }
|
.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) ───────────────────────────── */
|
/* ── 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. */
|
||||||
|
|
@ -379,6 +397,13 @@
|
||||||
|
|
||||||
<div id="map">
|
<div id="map">
|
||||||
<div class="placeholder" id="placeholder">Loading live fleet…</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom dock: two tiers — filter tier (top) + trip-card tier (beneath) -->
|
<!-- Bottom dock: two tiers — filter tier (top) + trip-card tier (beneath) -->
|
||||||
|
|
@ -455,10 +480,26 @@ const STALE_GPS_MS = 10 * 60 * 1000;
|
||||||
const OFFLINE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
const OFFLINE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
||||||
const POIS = [{ name: 'Fireside HQ', lng: 36.728785, lat: -1.2411485 }];
|
const POIS = [{ name: 'Fireside HQ', lng: 36.728785, lat: -1.2411485 }];
|
||||||
|
|
||||||
// Cost-centre palette — categorical, brand colours lead. Stable per centre via hash.
|
// 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).
|
||||||
const COST_CENTRE_PALETTE = [
|
const COST_CENTRE_PALETTE = [
|
||||||
'#E8954A', '#2dd4a7', '#3b82f6', '#a855f7', '#f43f5e', '#06b6d4',
|
'#2dd4a7', '#8b5cf6', '#f43f5e', '#0ea5e9', '#eab308', '#d946ef', '#10b981',
|
||||||
'#84cc16', '#ec4899', '#fbbf24', '#14b8a6', '#8b5cf6', '#f97316', '#22c55e',
|
|
||||||
];
|
];
|
||||||
const UNKNOWN_CC_COLOR = '#9ca3af';
|
const UNKNOWN_CC_COLOR = '#9ca3af';
|
||||||
const PARKED_COLOR = '#6b7280';
|
const PARKED_COLOR = '#6b7280';
|
||||||
|
|
@ -472,8 +513,10 @@ const seqColor = n => SEQ_PALETTE[((n || 1) - 1 + SEQ_PALETTE.length * 100) % SE
|
||||||
|
|
||||||
function colorForCostCentre(cc) {
|
function colorForCostCentre(cc) {
|
||||||
if (!cc) return UNKNOWN_CC_COLOR;
|
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;
|
let h = 0;
|
||||||
for (let i = 0; i < cc.length; i++) h = (h * 31 + cc.charCodeAt(i)) | 0;
|
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0;
|
||||||
return COST_CENTRE_PALETTE[Math.abs(h) % COST_CENTRE_PALETTE.length];
|
return COST_CENTRE_PALETTE[Math.abs(h) % COST_CENTRE_PALETTE.length];
|
||||||
}
|
}
|
||||||
// Pastel tint of a #rrggbb colour — blended toward white. Used for recently-
|
// Pastel tint of a #rrggbb colour — blended toward white. Used for recently-
|
||||||
|
|
@ -714,6 +757,27 @@ function applyLiveFilters() {
|
||||||
});
|
});
|
||||||
speeds.sort((a, b) => a - b);
|
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 });
|
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
|
// Query the cluster index for the current viewport+zoom and draw either count
|
||||||
|
|
@ -1396,6 +1460,9 @@ function backToLive() {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
document.getElementById('live-pill').addEventListener('click', backToLive);
|
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
|
// Reverse-geocoding (Nominatim) — queued, 1 req/sec, in-memory cache
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue