CityFlow-AI / app.js
VSRAJHAVEL's picture
Upload app.js with huggingface_hub
a801ecc verified
Raw History Blame Contribute Delete
109 kB
/**
* CityFlow AI - Modern VisionOS Dashboard Engine (modern_app.js)
* Fully connected, high-performance interactive frontend engine.
* Optimized for 60 FPS Canvas rendering, low-latency sync, and complete control integration.
*/
(function () {
'use strict';
// API Endpoints
const API = {
state: '/state',
control: '/control',
messages: '/messages',
inspect: (nodeId) => `/inspect/${nodeId}`,
exportCsv: '/export/csv'
};
const GRID = 4;
const DIRS = {
N: { dr: -1, dc: 0, angle: -Math.PI / 2 },
S: { dr: 1, dc: 0, angle: Math.PI / 2 },
E: { dr: 0, dc: 1, angle: 0 },
W: { dr: 0, dc: -1, angle: Math.PI }
};
// Seeded Pseudo-Random for Reproducible Rich City Architecture
function pseudoRandom(seed) {
const x = Math.sin(seed * 9999) * 10000;
return x - Math.floor(x);
}
// Generate 5x5 Blocks for the 4x4 Intersection Grid
const CITY_BLOCKS = [];
for (let r = 0; r < GRID + 1; r++) {
CITY_BLOCKS[r] = [];
for (let c = 0; c < GRID + 1; c++) {
const seed = r * 17 + c * 31 + 42;
const isPark = pseudoRandom(seed) > 0.78;
const height = 14 + Math.floor(pseudoRandom(seed + 1) * 36);
const shade = 18 + Math.floor(pseudoRandom(seed + 2) * 16);
const randColor = pseudoRandom(seed + 3);
const windowColor = randColor > 0.65 ? '#00f5d4' : (randColor > 0.32 ? '#c084fc' : '#ffd166');
const subDivide = pseudoRandom(seed + 4) > 0.38;
CITY_BLOCKS[r][c] = { isPark, height, shade, windowColor, subDivide, seed };
}
}
// Rounded rectangle helper
function roundRect(ctx, x, y, w, h, r) {
if (w <= 0 || h <= 0) return;
const radius = Math.min(r, Math.min(w, h) / 2);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + w - radius, y);
ctx.arcTo(x + w, y, x + w, y + radius, radius);
ctx.lineTo(x + w, y + h - radius);
ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);
ctx.lineTo(x + radius, y + h);
ctx.arcTo(x, y + h, x, y + h - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
}
function angleDiff(a, b) {
let diff = (a - b) % (Math.PI * 2);
if (diff < -Math.PI) diff += Math.PI * 2;
if (diff > Math.PI) diff -= Math.PI * 2;
return diff;
}
// State Management
let latestState = null;
let lastTick = -1;
let isPaused = false;
let compareMode = false;
let selectedNode = null;
let currentFeedFilter = 'all';
let allMessages = [];
let signalTimers = {};
let lastMessageCount = -1;
let activeTab = 'simulation-cockpit';
// Drag & Drop Ambulance Dispatch
let isDraggingDispatch = false;
let dragStartNode = null;
let dragDistance = 0;
let currentMousePos = { x: 0, y: 0 };
// Vehicle Tracking Maps for Smooth 60 FPS Physics Interpolation
const trackedCars = new Map();
const trackedBaseCars = new Map();
// Canvas Viewports
let primaryCanvas = document.getElementById('modernCityCanvas');
let primaryCtx = primaryCanvas ? primaryCanvas.getContext('2d') : null;
let splitAiCanvas = document.getElementById('splitAiCanvas');
let splitAiCtx = splitAiCanvas ? splitAiCanvas.getContext('2d') : null;
let splitBaseCanvas = document.getElementById('splitBaseCanvas');
let splitBaseCtx = splitBaseCanvas ? splitBaseCanvas.getContext('2d') : null;
let tooltip = document.getElementById('carTooltip');
// Sparkline Generator for SVG <path d="...">
function generateSvgPath(data, width = 200, height = 40) {
if (!data || data.length < 2) {
return `M0,${height / 2} L${width},${height / 2}`;
}
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const step = width / (data.length - 1);
const points = data.map((val, idx) => {
const x = idx * step;
const y = height - ((val - min) / range) * (height - 8) - 4;
return [x, y];
});
let d = `M ${points[0][0].toFixed(1)},${points[0][1].toFixed(1)}`;
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
const cx = (prev[0] + curr[0]) / 2;
d += ` C ${cx.toFixed(1)},${prev[1].toFixed(1)} ${cx.toFixed(1)},${curr[1].toFixed(1)} ${curr[0].toFixed(1)},${curr[1].toFixed(1)}`;
}
return d;
}
// Theme Management (Dark / Light)
function isDarkMode() {
return document.documentElement.classList.contains('dark');
}
function applyTheme(dark) {
const themeIcon = document.getElementById('themeIcon');
if (dark) {
document.documentElement.classList.add('dark');
if (themeIcon) themeIcon.textContent = 'dark_mode';
localStorage.setItem('cityflow_theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
if (themeIcon) themeIcon.textContent = 'light_mode';
localStorage.setItem('cityflow_theme', 'light');
}
updateChartThemes(dark);
// Recolor canvas/sim container backgrounds for theme switch
const simContainer = document.getElementById('simContainer');
if (simContainer) {
simContainer.style.backgroundColor = dark ? '#080a14' : '#edf2fa';
}
const cmpWrapper = document.getElementById('compareWrapper');
if (cmpWrapper) {
cmpWrapper.style.backgroundColor = dark ? '#06070d' : '#e2e8f0';
}
}
function updateChartThemes(dark) {
const textCol = dark ? '#94a3b8' : '#475569';
const gridCol = dark ? 'rgba(255, 255, 255, 0.06)' : 'rgba(15, 23, 42, 0.06)';
const scaleOpts = {
grid: { color: gridCol },
ticks: { color: textCol, font: { family: 'Inter', size: 10 } }
};
[analyticsWaitChart, analyticsThruChart, analyticsBarChart, analyticsEnvChart, analyticsLossChart].forEach((chart) => {
if (!chart) return;
if (chart.options.scales.x) {
chart.options.scales.x.grid.color = gridCol;
chart.options.scales.x.ticks.color = textCol;
}
if (chart.options.scales.y) {
chart.options.scales.y.grid.color = gridCol;
chart.options.scales.y.ticks.color = textCol;
}
chart.update('none');
});
}
let themeInitialized = false;
function initTheme() {
if (themeInitialized) return;
themeInitialized = true;
const saved = localStorage.getItem('cityflow_theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = saved ? saved === 'dark' : (saved === null ? true : prefersDark);
applyTheme(isDark);
const toggleBtn = document.getElementById('themeToggle');
if (toggleBtn) {
toggleBtn.onclick = (e) => {
if (e) e.preventDefault();
applyTheme(!isDarkMode());
};
}
}
// Dimension Calculation & HiDPI Rescaling
function computeDimensions(cvs) {
if (!cvs) return { dpr: 1, w: 600, h: 450, availSize: 420, cellSize: 105, roadWidth: 38, originX: 40, originY: 15 };
const dpr = Math.min(2, window.devicePixelRatio || 1);
const rect = cvs.getBoundingClientRect();
const w = Math.max(300, Math.floor(rect.width || cvs.clientWidth || 600));
const h = Math.max(300, Math.floor(rect.height || cvs.clientHeight || 450));
if (cvs.width !== Math.floor(w * dpr) || cvs.height !== Math.floor(h * dpr)) {
cvs.width = Math.floor(w * dpr);
cvs.height = Math.floor(h * dpr);
}
const padding = 16;
const availSize = Math.max(220, Math.min(w, h) - padding * 2);
const cellSize = availSize / GRID;
const roadWidth = Math.max(32, cellSize * 0.32);
const originX = Math.round((w - availSize) / 2);
const originY = Math.round((h - availSize) / 2);
return { dpr, w, h, availSize, cellSize, roadWidth, originX, originY };
}
let cachedPrimaryDim = null;
let cachedSplitAiDim = null;
let cachedSplitBaseDim = null;
function refreshCanvasSizes() {
if (primaryCanvas) cachedPrimaryDim = computeDimensions(primaryCanvas);
if (splitAiCanvas) cachedSplitAiDim = computeDimensions(splitAiCanvas);
if (splitBaseCanvas) cachedSplitBaseDim = computeDimensions(splitBaseCanvas);
}
window.addEventListener('resize', () => {
refreshCanvasSizes();
});
function getIntersectionCenter(r, c, dim) {
return {
x: dim.originX + c * dim.cellSize + dim.cellSize / 2,
y: dim.originY + r * dim.cellSize + dim.cellSize / 2
};
}
// Signal Timers Synchronizer
function updateSignals(signals, currentTick) {
if (!signals) return;
signals.forEach((s) => {
const key = `${s.r}_${s.c}`;
let record = signalTimers[key];
if (!record) {
record = { lastPhase: s.phase, phaseTick: currentTick, countdown: 5 };
signalTimers[key] = record;
} else {
if (record.lastPhase !== s.phase) {
record.lastPhase = s.phase;
record.phaseTick = currentTick;
record.countdown = 5;
} else {
const elapsed = (currentTick - record.phaseTick) % 15;
record.countdown = Math.max(0, 5 - elapsed);
}
}
});
}
// Update Smooth Vehicle Movement & Target Waypoints (Only called on tick change)
function updateCarsFromState(carsList, targetMap, dim) {
if (!dim) return;
targetMap.forEach((car) => (car.active = false));
const queues = {};
(carsList || []).forEach((c) => {
const key = `${c.r}_${c.c}_${c.d}`;
if (!queues[key]) queues[key] = [];
queues[key].push(c);
});
Object.entries(queues).forEach(([key, list]) => {
const [r, c, d] = key.split('_');
const row = parseInt(r, 10);
const col = parseInt(c, 10);
const center = getIntersectionCenter(row, col, dim);
const halfRoad = dim.roadWidth / 2;
const laneOffset = halfRoad * 0.48;
list.forEach((carData, qIdx) => {
const carId = carData.id !== undefined ? carData.id : `${carData.amb ? 'amb' : 'car'}_${row}_${col}_${d}_${qIdx}`;
const carKey = `${carId}`;
let tx = center.x;
let ty = center.y;
const targetAngle = DIRS[d] ? DIRS[d].angle : 0;
const maxQueueDist = dim.cellSize * 0.44;
const stopDistance = Math.min(maxQueueDist, halfRoad + 8 + qIdx * 14);
if (d === 'N') { tx -= laneOffset; ty += stopDistance; }
else if (d === 'S') { tx += laneOffset; ty -= stopDistance; }
else if (d === 'E') { tx -= stopDistance; ty += laneOffset; }
else if (d === 'W') { tx += stopDistance; ty -= laneOffset; }
let tracked = targetMap.get(carKey);
if (!tracked) {
tracked = {
id: carId,
x: tx,
y: ty,
angle: targetAngle,
targetX: tx,
targetY: ty,
targetAngle: targetAngle,
wait: carData.w || 0,
isAmb: !!carData.amb,
dest: carData.dest || '',
r: row,
c: col,
d: d,
active: true
};
targetMap.set(carKey, tracked);
} else {
tracked.targetX = tx;
tracked.targetY = ty;
tracked.targetAngle = targetAngle;
tracked.wait = carData.w || 0;
tracked.isAmb = !!carData.amb;
tracked.dest = carData.dest || tracked.dest;
tracked.r = row;
tracked.c = col;
tracked.d = d;
tracked.active = true;
}
});
});
targetMap.forEach((car, key) => {
if (!car.active) targetMap.delete(key);
});
}
/* ========================================================
CANVAS CITY RENDERING (Fast, Clean, 60 FPS)
======================================================== */
function renderCity(ctx, cvs, snap, isBaseline = false, timestamp = performance.now()) {
if (!ctx || !cvs) return;
// Allow static demo render when no backend data available
const demoSnap = snap || { signals: [], cars: [], blocked: [] };
const dim = computeDimensions(cvs);
const dark = isDarkMode();
ctx.save();
ctx.scale(dim.dpr, dim.dpr);
// 1. City Ground / Base Pavement
ctx.fillStyle = dark ? '#080a14' : '#f4f7fc';
ctx.fillRect(0, 0, dim.w, dim.h);
// 2. City Blocks
drawCityBlocks(ctx, dim, dark);
// 3. Realistic Road Network
drawRoadNetwork(ctx, dim, dark);
// 4. Traffic Signals
drawTrafficSignals(ctx, dim, demoSnap.signals || [], dark);
// 5. Emergency Corridor
if (!isBaseline) {
drawAmbulanceCorridor(ctx, dim, demoSnap.signals || [], dark, timestamp);
}
// 6. Active Road Blockades / Hazards
drawAccidentHazards(ctx, dim, demoSnap.blocked || [], dark, timestamp);
// 7. Vehicles & Ambulances (Smooth 60fps interpolation)
const carMap = isBaseline ? trackedBaseCars : trackedCars;
drawVehicles(ctx, dim, carMap, dark, timestamp);
// 8. Interaction Overlay
if (!isBaseline) {
drawInteractionOverlay(ctx, dim, dark, timestamp);
}
ctx.restore();
}
// City Blocks with Architectural Variation
function drawCityBlocks(ctx, dim, dark) {
const halfRoad = dim.roadWidth / 2;
for (let r = 0; r < GRID + 1; r++) {
for (let c = 0; c < GRID + 1; c++) {
const x1 = (c === 0) ? dim.originX - dim.cellSize * 0.4 : dim.originX + (c - 1) * dim.cellSize + dim.cellSize / 2 + halfRoad + 3;
const y1 = (r === 0) ? dim.originY - dim.cellSize * 0.4 : dim.originY + (r - 1) * dim.cellSize + dim.cellSize / 2 + halfRoad + 3;
const x2 = (c === GRID) ? dim.originX + dim.availSize + dim.cellSize * 0.4 : dim.originX + c * dim.cellSize + dim.cellSize / 2 - halfRoad - 3;
const y2 = (r === GRID) ? dim.originY + dim.availSize + dim.cellSize * 0.4 : dim.originY + r * dim.cellSize + dim.cellSize / 2 - halfRoad - 3;
const bw = x2 - x1;
const bh = y2 - y1;
if (bw <= 8 || bh <= 8) continue;
const block = CITY_BLOCKS[r][c];
// Sidewalk Base Perimeter
ctx.fillStyle = dark ? '#111726' : '#dbe2ee';
roundRect(ctx, x1, y1, bw, bh, 5);
ctx.fill();
if (block.isPark) {
// Lush Green Park Area
ctx.fillStyle = dark ? '#052a1a' : '#dcfce7';
roundRect(ctx, x1 + 3, y1 + 3, bw - 6, bh - 6, 4);
ctx.fill();
// Trees
const t1x = x1 + bw * 0.35;
const t1y = y1 + bh * 0.35;
const t2x = x1 + bw * 0.68;
const t2y = y1 + bh * 0.65;
ctx.fillStyle = dark ? '#10f49c' : '#15803d';
ctx.beginPath();
ctx.arc(t1x, t1y, 3.5, 0, Math.PI * 2);
ctx.arc(t2x, t2y, 3.5, 0, Math.PI * 2);
ctx.fill();
} else {
// 3D Building Rooftop
const buildingX = x1 + 3;
const buildingY = y1 + 3;
const buildingW = bw - 6;
const buildingH = bh - 6;
// Building Rooftop Fill (Sleek Obsidian Smoked Glass / Pristine Alabaster)
ctx.fillStyle = dark ? `rgb(${block.shade + 12}, ${block.shade + 18}, ${block.shade + 38})` : '#ffffff';
roundRect(ctx, buildingX, buildingY, buildingW, buildingH, 4);
ctx.fill();
ctx.strokeStyle = dark ? 'rgba(192, 132, 252, 0.28)' : 'rgba(124, 58, 237, 0.15)';
ctx.lineWidth = 1;
ctx.stroke();
// Architectural Windows or Rooftop Infrastructure
if (block.subDivide && buildingW > 20 && buildingH > 20) {
ctx.fillStyle = dark ? block.windowColor : '#6366f1';
ctx.globalAlpha = dark ? 0.75 : 0.4;
for (let wx = buildingX + 5; wx < buildingX + buildingW - 4; wx += 9) {
for (let wy = buildingY + 5; wy < buildingY + buildingH - 4; wy += 9) {
ctx.fillRect(wx, wy, 2.5, 2.5);
}
}
ctx.globalAlpha = 1.0;
}
}
}
}
}
// Asphalt Road Network with Dividers and Crosswalks
function drawRoadNetwork(ctx, dim, dark) {
const halfRoad = dim.roadWidth / 2;
// Asphalt Main Surfaces
ctx.fillStyle = dark ? '#131929' : '#2d3748';
for (let i = 0; i < GRID; i++) {
const center = getIntersectionCenter(i, i, dim);
ctx.fillRect(dim.originX - 16, center.y - halfRoad, dim.availSize + 32, dim.roadWidth);
ctx.fillRect(center.x - halfRoad, dim.originY - 16, dim.roadWidth, dim.availSize + 32);
}
// Road Edge Curbs (Cyber Mint glow in dark / Indigo in light)
ctx.strokeStyle = dark ? 'rgba(0, 245, 212, 0.35)' : 'rgba(124, 58, 237, 0.2)';
ctx.lineWidth = 1;
for (let i = 0; i < GRID; i++) {
const center = getIntersectionCenter(i, i, dim);
ctx.beginPath();
ctx.moveTo(center.x - halfRoad, dim.originY - 16);
ctx.lineTo(center.x - halfRoad, dim.originY + dim.availSize + 16);
ctx.moveTo(center.x + halfRoad, dim.originY - 16);
ctx.lineTo(center.x + halfRoad, dim.originY + dim.availSize + 16);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(dim.originX - 16, center.y - halfRoad);
ctx.lineTo(dim.originX + dim.availSize + 16, center.y - halfRoad);
ctx.moveTo(dim.originX - 16, center.y + halfRoad);
ctx.lineTo(dim.originX + dim.availSize + 16, center.y + halfRoad);
ctx.stroke();
}
// Dashed Center Lane Dividers (Solar Gold in dark / Crisp white in light)
ctx.strokeStyle = dark ? '#ffd166' : 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 1.4;
ctx.setLineDash([4, 6]);
for (let i = 0; i < GRID; i++) {
const center = getIntersectionCenter(i, i, dim);
ctx.beginPath();
ctx.moveTo(dim.originX, center.y);
ctx.lineTo(dim.originX + dim.availSize, center.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(center.x, dim.originY);
ctx.lineTo(center.x, dim.originY + dim.availSize);
ctx.stroke();
}
ctx.setLineDash([]);
}
// Traffic Signals
function drawTrafficSignals(ctx, dim, signals, dark) {
const halfRoad = dim.roadWidth / 2;
signals.forEach((s) => {
const center = getIntersectionCenter(s.r, s.c, dim);
const isNS = s.phase === 'NS';
const isForced = !!s.forced;
// Intersection Center Box
ctx.fillStyle = isForced
? 'rgba(0, 245, 212, 0.28)'
: (dark ? 'rgba(124, 58, 237, 0.12)' : 'rgba(0, 0, 0, 0.05)');
roundRect(ctx, center.x - halfRoad, center.y - halfRoad, dim.roadWidth, dim.roadWidth, 4);
ctx.fill();
if (isForced) {
ctx.strokeStyle = '#00f5d4';
ctx.lineWidth = 1.8;
ctx.stroke();
}
// Signal Dots on 4 Approaches (Cyber Mint / Laser Emerald & Neon Crimson)
const greenColor = isForced ? '#00f5d4' : (dark ? '#10f49c' : '#059669');
const redColor = dark ? '#ff3366' : '#e11d48';
// N & S lights
ctx.fillStyle = isNS ? greenColor : redColor;
ctx.beginPath();
ctx.arc(center.x - halfRoad * 0.45, center.y - halfRoad - 3, 2.5, 0, Math.PI * 2);
ctx.arc(center.x + halfRoad * 0.45, center.y + halfRoad + 3, 2.5, 0, Math.PI * 2);
ctx.fill();
// E & W lights
ctx.fillStyle = !isNS ? greenColor : redColor;
ctx.beginPath();
ctx.arc(center.x + halfRoad + 3, center.y - halfRoad * 0.45, 2.5, 0, Math.PI * 2);
ctx.arc(center.x - halfRoad - 3, center.y + halfRoad * 0.45, 2.5, 0, Math.PI * 2);
ctx.fill();
});
}
// Emergency Corridor Pulsing Waves
function drawAmbulanceCorridor(ctx, dim, signals, dark, timestamp) {
const forced = signals.filter((s) => s.forced);
if (!forced.length) return;
ctx.save();
const pulseAlpha = 0.3 + 0.2 * Math.sin(timestamp * 0.008);
ctx.strokeStyle = `rgba(0, 245, 212, ${pulseAlpha})`;
ctx.lineWidth = 3;
ctx.setLineDash([6, 4]);
for (let i = 0; i < forced.length - 1; i++) {
const c1 = getIntersectionCenter(forced[i].r, forced[i].c, dim);
const c2 = getIntersectionCenter(forced[i + 1].r, forced[i + 1].c, dim);
ctx.beginPath();
ctx.moveTo(c1.x, c1.y);
ctx.lineTo(c2.x, c2.y);
ctx.stroke();
}
ctx.restore();
}
// Active Road Blockades / Hazards
function drawAccidentHazards(ctx, dim, blocked, dark, timestamp) {
if (!blocked || !blocked.length) return;
const halfRoad = dim.roadWidth / 2;
blocked.forEach((b) => {
const center = getIntersectionCenter(b.r, b.c, dim);
let bx = center.x;
let by = center.y;
const offset = halfRoad + 10;
if (b.d === 'N') by -= offset;
else if (b.d === 'S') by += offset;
else if (b.d === 'E') bx += offset;
else if (b.d === 'W') bx -= offset;
// Pulsing barrier icon
ctx.fillStyle = '#ff3366';
ctx.beginPath();
ctx.arc(bx, by, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(bx - 3.5, by - 3.5);
ctx.lineTo(bx + 3.5, by + 3.5);
ctx.moveTo(bx + 3.5, by - 3.5);
ctx.lineTo(bx - 3.5, by + 3.5);
ctx.stroke();
});
}
// Vehicles Rendering (Interpolated 60 FPS, No heavy gradients)
function drawVehicles(ctx, dim, carMap, dark, timestamp) {
carMap.forEach((car) => {
// Smooth linear motion interpolation
car.x += (car.targetX - car.x) * 0.28;
car.y += (car.targetY - car.y) * 0.28;
car.angle += angleDiff(car.targetAngle, car.angle) * 0.32;
ctx.save();
ctx.translate(car.x, car.y);
ctx.rotate(car.angle);
if (car.isAmb) {
drawFastAmbulance(ctx, dark, timestamp);
} else {
drawFastCar(ctx, car, dark);
}
ctx.restore();
});
}
// Fast, Sleek Standard Vehicle (Aurora High-Vis Palette)
function drawFastCar(ctx, car, dark) {
const idNum = typeof car.id === 'number' ? car.id : (parseInt(car.id, 10) || 1);
const length = 15;
const width = 8;
const halfL = length / 2;
const halfW = width / 2;
const palettes = ['#c084fc', '#00f5d4', '#10f49c', '#ffd166', '#ff3366', '#38bdf8'];
let baseColor = palettes[idNum % palettes.length];
if (car.wait > 7) baseColor = '#ff3366';
else if (car.wait > 3) baseColor = '#ffb703';
// 1. Forward Headlights beam (violet-tinted beam in dark mode)
ctx.fillStyle = dark ? 'rgba(192, 132, 252, 0.28)' : 'rgba(255, 255, 255, 0.22)';
ctx.beginPath();
ctx.moveTo(halfL, -halfW * 0.6);
ctx.lineTo(halfL + 18, -halfW * 0.6 - 7);
ctx.lineTo(halfL + 18, -halfW * 0.6 + 2);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(halfL, halfW * 0.6);
ctx.lineTo(halfL + 18, halfW * 0.6 - 2);
ctx.lineTo(halfL + 18, halfW * 0.6 + 7);
ctx.closePath();
ctx.fill();
// 2. Chassis Shadow
ctx.fillStyle = dark ? 'rgba(0, 0, 0, 0.6)' : 'rgba(15, 23, 42, 0.2)';
roundRect(ctx, -halfL + 0.5, -halfW + 0.5, length, width, 2);
ctx.fill();
// 3. Main Car Body
ctx.fillStyle = baseColor;
roundRect(ctx, -halfL, -halfW, length, width, 2);
ctx.fill();
// 4. Cabin Glass
ctx.fillStyle = dark ? 'rgba(7, 8, 14, 0.9)' : 'rgba(15, 23, 42, 0.7)';
roundRect(ctx, -halfL * 0.4, -halfW * 0.75, length * 0.6, width * 0.75, 1.5);
ctx.fill();
// 5. Taillights / Brake LEDs
const isBraking = car.wait > 0;
ctx.fillStyle = isBraking ? '#ff3366' : '#991b1b';
ctx.fillRect(-halfL, -halfW + 0.8, 1.2, 1.8);
ctx.fillRect(-halfL, halfW - 2.6, 1.2, 1.8);
}
// Fast, Sleek Ambulance
function drawFastAmbulance(ctx, dark, timestamp) {
const ambW = 20;
const ambH = 10;
const halfL = ambW / 2;
const halfW = ambH / 2;
// Forward beam with cyber mint tint
ctx.fillStyle = 'rgba(0, 245, 212, 0.28)';
ctx.beginPath();
ctx.moveTo(halfL, -halfW * 0.6);
ctx.lineTo(halfL + 24, -halfW * 0.6 - 9);
ctx.lineTo(halfL + 24, halfW * 0.6 + 9);
ctx.lineTo(halfL, halfW * 0.6);
ctx.closePath();
ctx.fill();
// Chassis Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
roundRect(ctx, -halfL + 0.5, -halfW + 0.5, ambW, ambH, 2.5);
ctx.fill();
// White High-Vis Body
ctx.fillStyle = '#ffffff';
roundRect(ctx, -halfL, -halfW, ambW, ambH, 2.5);
ctx.fill();
// Neon Crimson Side Stripes
ctx.fillStyle = '#ff3366';
ctx.fillRect(-halfL * 0.2, -halfW, ambW * 0.45, 1.5);
ctx.fillRect(-halfL * 0.2, halfW - 1.5, ambW * 0.45, 1.5);
// Medical Cross
const crossX = -halfL * 0.35;
ctx.fillRect(crossX - 3, -1, 6, 2);
ctx.fillRect(crossX - 1, -3, 2, 6);
// Alternating Dual Roof Strobe (Neon Crimson / Cyber Mint)
const isRed = Math.floor(timestamp * 0.015) % 2 === 0;
ctx.fillStyle = isRed ? '#ff3366' : '#00f5d4';
ctx.beginPath();
ctx.arc(halfL * 0.1, isRed ? -2.5 : 2.5, 2.2, 0, Math.PI * 2);
ctx.fill();
}
// Interaction Overlay (Dispatch Line, Hover Target)
function drawInteractionOverlay(ctx, dim, dark, timestamp) {
if (selectedNode) {
const center = getIntersectionCenter(selectedNode.r, selectedNode.c, dim);
ctx.save();
ctx.strokeStyle = '#00f5d4';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(center.x, center.y, dim.roadWidth * 0.75, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
if (isDraggingDispatch && dragStartNode) {
const startCenter = getIntersectionCenter(dragStartNode.r, dragStartNode.c, dim);
ctx.save();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = '#c084fc';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(startCenter.x, startCenter.y);
ctx.lineTo(currentMousePos.x, currentMousePos.y);
ctx.stroke();
ctx.fillStyle = '#c084fc';
ctx.beginPath();
ctx.arc(currentMousePos.x, currentMousePos.y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
/* ========================================================
ANIMATION & STATE SYNCHRONIZATION LOOPS
======================================================== */
let frameCount = 0;
let fpsLastUpdate = performance.now();
let currentFps = 60;
function animationLoop(timestamp) {
const now = performance.now();
frameCount++;
if (now - fpsLastUpdate >= 400) {
currentFps = Math.round((frameCount * 1000) / (now - fpsLastUpdate));
frameCount = 0;
fpsLastUpdate = now;
// Update both FPS display elements
const fpsEl = document.getElementById('fpsCounter');
if (fpsEl) {
fpsEl.textContent = `${currentFps} FPS (${currentFps >= 60 ? '60Hz+' : 'Active'})`;
}
const statusFpsEl = document.getElementById('statusFpsCounter');
if (statusFpsEl) {
statusFpsEl.textContent = `${currentFps}+ FPS (${currentFps >= 60 ? '60Hz' : currentFps + 'Hz'})`;
}
const tickRateBadge = document.getElementById('tickRateBadge');
if (tickRateBadge && latestState) {
const speed = latestState.speed || 1.0;
const tickHz = Math.round(60 * speed);
tickRateBadge.textContent = `${tickHz} Hz (${(16 / speed).toFixed(1)}ms)`;
}
}
if (compareMode) {
renderCity(splitAiCtx, splitAiCanvas, latestState, false, timestamp);
if (latestState && latestState.baseline) {
renderCity(splitBaseCtx, splitBaseCanvas, latestState.baseline, true, timestamp);
} else {
renderCity(splitBaseCtx, splitBaseCanvas, null, true, timestamp);
}
} else {
renderCity(primaryCtx, primaryCanvas, latestState, false, timestamp);
}
requestAnimationFrame(animationLoop);
}
async function postControl(payload) {
const codeDisplay = document.getElementById('telemetryBindingJson');
if (codeDisplay) {
codeDisplay.textContent = JSON.stringify(payload);
}
if (isOfflineMode) {
if (typeof handleOfflineControl === 'function') {
handleOfflineControl(payload);
}
return { ok: true };
}
try {
const res = await fetch(API.control, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Control failed: ' + res.status);
const data = await res.json();
return data;
} catch (err) {
console.warn('postControl error, falling back to client engine:', err);
if (typeof handleOfflineControl === 'function') {
handleOfflineControl(payload);
}
return { ok: true };
}
}
// Reactive UI Metrics Synchronizer
function updateDashboardUI(snap) {
if (!snap) return;
// Header tick counter
const headerTick = document.getElementById('headerTickText');
if (headerTick) {
const speed = snap.speed || 1.0;
const hz = Math.round(60 * speed);
headerTick.textContent = `Tick ${snap.tick.toLocaleString()} • ${speed.toFixed(1)}x • ${currentFps} FPS (${hz} Hz)`;
}
// Hero Stat Pills
const bWait = snap.baseline ? snap.baseline.avg_wait : 14.8;
const aWait = snap.avg_wait || 0.0;
const delayPct = bWait > 0 ? (((bWait - aWait) / bWait) * 100).toFixed(1) : '0.0';
const heroDelay = document.getElementById('heroDelayPct');
if (heroDelay) heroDelay.textContent = `${delayPct}%`;
const bThru = snap.baseline ? snap.baseline.throughput : 489;
const aThru = snap.throughput || 0;
const flowPct = bThru > 0 ? (((aThru - bThru) / bThru) * 100).toFixed(1) : '+0.0';
const heroFlow = document.getElementById('heroFlowPct');
if (heroFlow) heroFlow.textContent = `${flowPct.startsWith('-') ? '' : '+'}${flowPct}%`;
const ambTimes = snap.amb_times || [];
const heroAmb = document.getElementById('heroAmbEta');
if (heroAmb) {
if (ambTimes.length > 0) {
heroAmb.textContent = `${ambTimes[ambTimes.length - 1]}t`;
} else {
heroAmb.textContent = '18.2t';
}
}
// Right Column KPIs & Dynamic Sparklines
const kpiWaitVal = document.getElementById('kpiWaitVal');
const kpiWaitDiff = document.getElementById('kpiWaitDiff');
const kpiWaitSub = document.getElementById('kpiWaitSub');
const sparkWaitPath = document.getElementById('sparkWaitPath');
if (kpiWaitVal) kpiWaitVal.textContent = aWait.toFixed(1);
if (kpiWaitDiff) kpiWaitDiff.textContent = `-${delayPct}% vs ${bWait.toFixed(1)}t`;
if (kpiWaitSub) kpiWaitSub.textContent = `Baseline timer: ${bWait.toFixed(1)} ticks • Net saving ${Math.max(0, bWait - aWait).toFixed(1)} ticks`;
if (sparkWaitPath && snap.wait_agent && snap.wait_agent.length) {
sparkWaitPath.setAttribute('d', generateSvgPath(snap.wait_agent, 200, 40));
}
const kpiThruVal = document.getElementById('kpiThruVal');
const kpiThruDiff = document.getElementById('kpiThruDiff');
const kpiThruSub = document.getElementById('kpiThruSub');
const sparkThruPath = document.getElementById('sparkThruPath');
if (kpiThruVal) kpiThruVal.textContent = aThru.toLocaleString();
if (kpiThruDiff) kpiThruDiff.textContent = `${flowPct.startsWith('-') ? '' : '+'}${flowPct}%`;
if (kpiThruSub) kpiThruSub.textContent = `Baseline static: ${bThru} veh • Net +${Math.max(0, aThru - bThru)}`;
if (sparkThruPath && snap.throughput_agent && snap.throughput_agent.length) {
sparkThruPath.setAttribute('d', generateSvgPath(snap.throughput_agent, 200, 40));
}
const kpiCo2Val = document.getElementById('kpiCo2Val');
const sparkCo2Path = document.getElementById('sparkCo2Path');
if (kpiCo2Val && snap.co2_saved_kg !== undefined) {
kpiCo2Val.textContent = snap.co2_saved_kg.toFixed(2);
if (sparkCo2Path && snap.wait_agent && snap.wait_agent.length) {
sparkCo2Path.setAttribute(
'd',
generateSvgPath(snap.wait_agent.map((w, idx) => Math.max(0, (snap.wait_baseline[idx] || 15) - w) * 0.0022 * (snap.throughput_agent[idx] || 10)), 200, 40)
);
}
}
const kpiFuelVal = document.getElementById('kpiFuelVal');
const sparkFuelPath = document.getElementById('sparkFuelPath');
if (kpiFuelVal && snap.fuel_saved_l !== undefined) {
kpiFuelVal.textContent = snap.fuel_saved_l.toFixed(2);
if (sparkFuelPath && snap.wait_agent && snap.wait_agent.length) {
sparkFuelPath.setAttribute(
'd',
generateSvgPath(snap.wait_agent.map((w, idx) => Math.max(0, (snap.wait_baseline[idx] || 15) - w) * 0.0022 * 0.42 * (snap.throughput_agent[idx] || 10)), 200, 40)
);
}
}
// Status strip
const statusVehicles = document.getElementById('statusVehicles');
if (statusVehicles && snap.cars) {
statusVehicles.textContent = `${snap.cars.length}/64`;
}
// 16-Node Congestion Matrix Update
renderCongestionMatrix(snap);
// Sync Play/Pause Button
isPaused = !!snap.paused;
const playIcon = document.getElementById('playIcon');
const dockPlayPause = document.getElementById('dockPlayPause');
if (playIcon) playIcon.textContent = isPaused ? 'play_arrow' : 'pause';
if (dockPlayPause) {
dockPlayPause.classList.toggle('bg-primary', !isPaused);
dockPlayPause.classList.toggle('bg-tertiary', isPaused);
}
// Synchronize Sub-Pages
updateAgentHierarchy(snap);
updateAnalyticsData(snap);
updateIncidentsData(snap);
}
function renderCongestionMatrix(snap) {
const container = document.getElementById('congestionMatrix');
if (!container) return;
const loads = snap.agent_intersection_loads || [];
const signals = snap.signals || [];
const blocked = snap.blocked || [];
// If already generated 16 elements, update content in place for maximum performance
if (container.children.length === 16) {
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const idx = r * GRID + c;
const load = loads[idx] !== undefined ? loads[idx] : 0;
const sig = signals.find((s) => s.r === r && s.c === c) || {};
const isBlocked = blocked.some((b) => b.r === r && b.c === c);
const cell = container.children[idx];
let phaseText = `${sig.phase || 'NS'} Grn`;
let badgeColor = 'text-tertiary-fixed';
let borderClass = 'border-white/5';
if (sig.forced) {
badgeColor = 'text-secondary-fixed font-bold';
phaseText = 'BFS PRIO';
borderClass = 'border-secondary/40 bg-secondary-container/15';
} else if (isBlocked) {
badgeColor = 'text-error font-bold';
phaseText = 'BARRIER';
borderClass = 'border-error/40 bg-error-container/20';
} else if (load > 5) {
badgeColor = 'text-error font-bold';
borderClass = 'border-error/30 bg-error-container/15';
} else if (load >= 3) {
badgeColor = 'text-amber-300';
borderClass = 'border-amber-500/30 bg-amber-500/10';
}
cell.className = `p-2 rounded-xl border flex flex-col items-center cursor-pointer transition-all hover:scale-105 ${borderClass}`;
cell.innerHTML = `
<span class="font-code-telemetry text-[10px] text-on-surface-variant/80">(${r},${c})</span>
<span class="font-headline-sm text-headline-sm font-bold">${load}</span>
<span class="font-code-telemetry text-[9px] ${badgeColor}">${phaseText}</span>
`;
}
}
return;
}
let html = '';
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
html += `
<div class="p-2 rounded-xl border border-white/5 bg-surface-container-high/60 flex flex-col items-center cursor-pointer transition-all hover:scale-105" data-node="${r},${c}" onclick="window.inspectNode(${r}, ${c})">
<span class="font-code-telemetry text-[10px] text-on-surface-variant/80">(${r},${c})</span>
<span class="font-headline-sm text-headline-sm font-bold">0</span>
<span class="font-code-telemetry text-[9px] text-tertiary-fixed">NS Grn</span>
</div>
`;
}
}
container.innerHTML = html;
}
window.inspectNode = async function (r, c) {
selectedNode = { r, c };
try {
const res = await fetch(API.inspect(`${r}_${c}`));
const data = await res.json();
if (!data || data.error) return;
const card = document.getElementById('inspectorCard');
if (!card) return;
card.classList.remove('hidden');
document.getElementById('inspectTitle').textContent = `Node (${r},${c}) Inspector`;
document.getElementById('inspectPhase').textContent = data.phase || 'NS';
document.getElementById('inspectTimer').textContent = `${data.phase_timer}/${data.min_green || 5}t`;
document.getElementById('inspectQns').textContent = (data.q_values ? data.q_values[0] : 0).toFixed(2);
document.getElementById('inspectQew').textContent = (data.q_values ? data.q_values[1] : 0).toFixed(2);
document.getElementById('inspectReason').textContent = `Policy: ${data.reason || 'Optimal'}`;
const q = data.queues || { N: 0, S: 0, E: 0, W: 0 };
document.getElementById('inspectQueues').textContent = `N:${q.N} S:${q.S} E:${q.E} W:${q.W}`;
} catch (err) {
console.warn('inspectNode error:', err);
}
};
/* ========================================================
MULTI-PAGE NAVIGATION CONTROLLER
======================================================== */
const PAGE_IDS = [
'simulation-cockpit',
'agent-hierarchy',
'telemetry-analytics',
'incidents-corridors',
'about'
];
function switchPage(pageId) {
if (!PAGE_IDS.includes(pageId)) {
pageId = 'simulation-cockpit';
}
activeTab = pageId;
// Toggle DOM sections
PAGE_IDS.forEach((id) => {
const el = document.getElementById(`page-${id}`);
if (el) {
if (id === pageId) {
el.classList.remove('hidden');
} else {
el.classList.add('hidden');
}
}
});
// Update Nav bar active styling
const navLinks = document.querySelectorAll('nav a[data-path]');
const activeClasses = ['bg-surface-container-highest', 'text-on-surface', 'font-semibold', 'shadow-inner', 'border-white/10'];
const inactiveClasses = ['text-on-surface-variant', 'hover:text-on-surface', 'hover:bg-surface-container-high', 'font-medium', 'border-transparent'];
navLinks.forEach((a) => {
const path = a.dataset.path;
if (path === pageId) {
a.classList.remove(...inactiveClasses);
a.classList.add(...activeClasses);
a.setAttribute('aria-current', 'page');
} else {
a.classList.remove(...activeClasses);
a.classList.add(...inactiveClasses);
a.removeAttribute('aria-current');
}
});
// Resize handlers when switching
setTimeout(() => {
refreshCanvasSizes();
if (pageId === 'telemetry-analytics') {
if (analyticsWaitChart) analyticsWaitChart.resize();
if (analyticsThruChart) analyticsThruChart.resize();
if (analyticsBarChart) analyticsBarChart.resize();
if (analyticsEnvChart) analyticsEnvChart.resize();
if (analyticsLossChart) analyticsLossChart.resize();
}
}, 40);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function initNavigation() {
const navLinks = document.querySelectorAll('nav a[data-path]');
navLinks.forEach((a) => {
a.addEventListener('click', (e) => {
e.preventDefault();
const path = a.dataset.path;
window.location.hash = path;
switchPage(path);
});
});
window.addEventListener('hashchange', () => {
const hash = window.location.hash.replace('#', '');
if (hash) switchPage(hash);
});
const currentHash = window.location.hash.replace('#', '') || 'simulation-cockpit';
switchPage(currentHash);
}
/* ========================================================
PAGE 2: AGENT HIERARCHY CONTROLLER
======================================================== */
let hierarchyGridInitialized = false;
function updateAgentHierarchy(snap) {
if (!snap || activeTab !== 'agent-hierarchy') return;
const tier1Status = document.getElementById('tier1Status');
if (tier1Status) {
tier1Status.textContent = `SYNC 20Hz (α=0.12, γ=0.95)`;
}
const tier2Status = document.getElementById('tier2Status');
if (tier2Status) {
if (snap.emergency) {
tier2Status.textContent = 'AMBULANCE PREEMPTION ACTIVE';
tier2Status.className = 'px-2 py-0.5 rounded-full bg-secondary-container/20 border border-secondary/30 text-secondary-fixed font-code-telemetry text-[10px] font-semibold animate-pulse';
} else {
tier2Status.textContent = 'CORRIDOR WATCH READY';
tier2Status.className = 'px-2 py-0.5 rounded-full bg-secondary-container/20 border border-secondary/30 text-secondary-fixed font-code-telemetry text-[10px] font-semibold';
}
}
const tier3Status = document.getElementById('tier3Status');
const blocked = snap.blocked || [];
if (tier3Status) {
if (blocked.length > 0) {
tier3Status.textContent = `${blocked.length} BARRICADES ACTIVE`;
tier3Status.className = 'px-2 py-0.5 rounded-full bg-error-container/20 border border-error/30 text-error font-code-telemetry text-[10px] font-semibold animate-pulse';
} else {
tier3Status.textContent = 'GRID 100% CLEAR';
tier3Status.className = 'px-2 py-0.5 rounded-full bg-tertiary-container/20 border border-tertiary/30 text-tertiary-fixed font-code-telemetry text-[10px] font-semibold';
}
}
const gridContainer = document.getElementById('hierarchyAgentGrid');
if (!gridContainer) return;
const loads = snap.agent_intersection_loads || [];
const signals = snap.signals || [];
if (!hierarchyGridInitialized || gridContainer.children.length !== 16) {
let html = '';
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
html += `
<div id="agent-card-${r}-${c}" class="p-4 rounded-2xl bg-surface-container-low/70 border border-white/10 hover:border-primary/40 transition-all flex flex-col justify-between cursor-pointer group shadow-sm hover:shadow-lg" onclick="window.inspectNode(${r}, ${c})">
<div>
<div class="flex items-center justify-between pb-2 mb-2 border-b border-white/5">
<div class="flex items-center gap-2">
<span id="agent-dot-${r}-${c}" class="w-2.5 h-2.5 rounded-full bg-tertiary"></span>
<span class="font-headline-sm text-sm font-semibold text-on-surface">Agent (${r},${c})</span>
</div>
<span id="agent-phase-${r}-${c}" class="px-2 py-0.5 rounded-full font-code-telemetry text-[10px] font-bold bg-primary-container/20 text-primary-fixed border border-primary/20">NS 0t</span>
</div>
<div class="space-y-1.5 font-code-telemetry text-xs mb-3">
<div class="flex justify-between text-on-surface-variant">
<span>Queue Pressure:</span>
<span id="agent-queue-${r}-${c}" class="font-bold text-on-surface">0 cars</span>
</div>
<div class="flex justify-between text-on-surface-variant">
<span>Exploration Rate:</span>
<span id="agent-eps-${r}-${c}" class="text-tertiary font-mono">ε=0.050</span>
</div>
</div>
</div>
<button class="w-full py-1.5 rounded-xl bg-surface-container-high/60 group-hover:bg-primary/20 group-hover:text-primary text-on-surface-variant font-code-telemetry text-[11px] transition-colors border border-white/5 flex items-center justify-center gap-1">
<span class="material-symbols-outlined text-[14px]">tune</span>
<span>Inspect Q-Table</span>
</button>
</div>
`;
}
}
gridContainer.innerHTML = html;
hierarchyGridInitialized = true;
}
// In-place updates
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const idx = r * GRID + c;
const load = loads[idx] !== undefined ? loads[idx] : 0;
const sig = signals.find((s) => s.r === r && s.c === c) || { phase: 'NS', phase_timer: 0 };
const phaseEl = document.getElementById(`agent-phase-${r}-${c}`);
const queueEl = document.getElementById(`agent-queue-${r}-${c}`);
if (phaseEl) {
phaseEl.textContent = `${sig.phase || 'NS'} ${sig.phase_timer || 0}t`;
}
if (queueEl) {
queueEl.textContent = `${load} car${load === 1 ? '' : 's'}`;
}
}
}
}
/* ========================================================
PAGE 3: TELEMETRY & ANALYTICS CHARTS
======================================================== */
let analyticsWaitChart = null;
let analyticsThruChart = null;
let analyticsBarChart = null;
let analyticsEnvChart = null;
let analyticsLossChart = null;
let lastChartTick = -1;
const telemetryHistory = {
labels: [],
agentWait: [],
baseWait: [],
agentThru: [],
baseThru: [],
co2: [],
fuel: [],
loss: []
};
function initAnalyticsCharts() {
const isDark = isDarkMode();
const textCol = isDark ? '#94a3b8' : '#475569';
const gridCol = isDark ? 'rgba(255, 255, 255, 0.06)' : 'rgba(15, 23, 42, 0.06)';
const defaultScales = {
x: { grid: { color: gridCol }, ticks: { color: textCol, font: { family: 'Inter', size: 10 } } },
y: { grid: { color: gridCol }, ticks: { color: textCol, font: { family: 'Inter', size: 10 } } }
};
// 1. Wait Chart
const canvasWait = document.getElementById('pageWaitChart');
if (canvasWait && !analyticsWaitChart) {
analyticsWaitChart = new Chart(canvasWait.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Multi-Agent Q-Learning', data: [], borderColor: '#c084fc', backgroundColor: 'rgba(192, 132, 252, 0.12)', tension: 0.35, fill: true, pointRadius: 1 },
{ label: 'Fixed 15t Baseline', data: [], borderColor: '#94a3b8', borderDash: [4, 4], tension: 0.35, fill: false, pointRadius: 0 }
]
},
options: { responsive: true, maintainAspectRatio: false, animation: false, scales: defaultScales }
});
}
// 2. Throughput Chart
const canvasThru = document.getElementById('pageThruChart');
if (canvasThru && !analyticsThruChart) {
analyticsThruChart = new Chart(canvasThru.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Multi-Agent Flow', data: [], borderColor: '#00f5d4', backgroundColor: 'rgba(0, 245, 212, 0.12)', tension: 0.35, fill: true, pointRadius: 1 },
{ label: 'Baseline Flow', data: [], borderColor: '#64748b', borderDash: [4, 4], tension: 0.35, fill: false, pointRadius: 0 }
]
},
options: { responsive: true, maintainAspectRatio: false, animation: false, scales: defaultScales }
});
}
// 3. Bar Chart (16 Nodes)
const canvasBar = document.getElementById('pageBarChart');
if (canvasBar && !analyticsBarChart) {
const labels = [];
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
labels.push(`(${r},${c})`);
}
}
analyticsBarChart = new Chart(canvasBar.getContext('2d'), {
type: 'bar',
data: {
labels: labels,
datasets: [{ label: 'Queue Backlog', data: new Array(16).fill(0), backgroundColor: 'rgba(0, 245, 212, 0.65)', borderColor: '#00f5d4', borderWidth: 1, borderRadius: 4 }]
},
options: { responsive: true, maintainAspectRatio: false, animation: false, scales: defaultScales }
});
}
// 4. Environmental Chart
const canvasEnv = document.getElementById('pageEnvChart');
if (canvasEnv && !analyticsEnvChart) {
analyticsEnvChart = new Chart(canvasEnv.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'CO₂ Saved (kg)', data: [], borderColor: '#10f49c', backgroundColor: 'rgba(16, 244, 156, 0.12)', tension: 0.35, fill: true, pointRadius: 1 },
{ label: 'Fuel Saved (L)', data: [], borderColor: '#ffd166', tension: 0.35, fill: false, pointRadius: 0 }
]
},
options: { responsive: true, maintainAspectRatio: false, animation: false, scales: defaultScales }
});
}
// 5. Loss Convergence Chart
const canvasLoss = document.getElementById('pageLossChart');
if (canvasLoss && !analyticsLossChart) {
analyticsLossChart = new Chart(canvasLoss.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [{ label: 'Bellman TD Error', data: [], borderColor: '#ff3366', backgroundColor: 'rgba(255, 51, 102, 0.1)', tension: 0.35, fill: true, pointRadius: 1 }]
},
options: { responsive: true, maintainAspectRatio: false, animation: false, scales: defaultScales }
});
}
}
function updateAnalyticsData(snap) {
if (!snap) return;
const elWait = document.getElementById('pageAnalyticsWait');
if (elWait) elWait.textContent = `${(snap.avg_wait || 0).toFixed(1)}t`;
const elThru = document.getElementById('pageAnalyticsThru');
if (elThru) elThru.textContent = (snap.throughput || 0).toLocaleString();
const elCo2 = document.getElementById('pageAnalyticsCo2');
if (elCo2 && snap.co2_saved_kg !== undefined) {
elCo2.textContent = `${snap.co2_saved_kg.toFixed(1)} kg`;
}
const elFuel = document.getElementById('pageAnalyticsFuel');
if (elFuel && snap.fuel_saved_l !== undefined) {
elFuel.textContent = `${snap.fuel_saved_l.toFixed(1)} L`;
}
// Update charts when on analytics tab or every 10 ticks
if (snap.tick !== lastChartTick && snap.tick % 5 === 0) {
lastChartTick = snap.tick;
const label = `T+${snap.tick}`;
telemetryHistory.labels.push(label);
telemetryHistory.agentWait.push(snap.avg_wait || 0);
telemetryHistory.baseWait.push(snap.baseline ? snap.baseline.avg_wait : 14.8);
telemetryHistory.agentThru.push(snap.throughput || 0);
telemetryHistory.baseThru.push(snap.baseline ? snap.baseline.throughput : 480);
telemetryHistory.co2.push(snap.co2_saved_kg || 0);
telemetryHistory.fuel.push(snap.fuel_saved_l || 0);
telemetryHistory.loss.push(Math.max(0.001, (snap.avg_wait ? snap.avg_wait * 0.0006 : 0.005)));
if (telemetryHistory.labels.length > 25) {
telemetryHistory.labels.shift();
telemetryHistory.agentWait.shift();
telemetryHistory.baseWait.shift();
telemetryHistory.agentThru.shift();
telemetryHistory.baseThru.shift();
telemetryHistory.co2.shift();
telemetryHistory.fuel.shift();
telemetryHistory.loss.shift();
}
if (activeTab === 'telemetry-analytics') {
if (analyticsWaitChart) {
analyticsWaitChart.data.labels = telemetryHistory.labels;
analyticsWaitChart.data.datasets[0].data = telemetryHistory.agentWait;
analyticsWaitChart.data.datasets[1].data = telemetryHistory.baseWait;
analyticsWaitChart.update('none');
}
if (analyticsThruChart) {
analyticsThruChart.data.labels = telemetryHistory.labels;
analyticsThruChart.data.datasets[0].data = telemetryHistory.agentThru;
analyticsThruChart.data.datasets[1].data = telemetryHistory.baseThru;
analyticsThruChart.update('none');
}
if (analyticsBarChart && snap.agent_intersection_loads) {
analyticsBarChart.data.datasets[0].data = snap.agent_intersection_loads.slice(0, 16);
analyticsBarChart.update('none');
}
if (analyticsEnvChart) {
analyticsEnvChart.data.labels = telemetryHistory.labels;
analyticsEnvChart.data.datasets[0].data = telemetryHistory.co2;
analyticsEnvChart.data.datasets[1].data = telemetryHistory.fuel;
analyticsEnvChart.update('none');
}
if (analyticsLossChart) {
analyticsLossChart.data.labels = telemetryHistory.labels;
analyticsLossChart.data.datasets[0].data = telemetryHistory.loss;
analyticsLossChart.update('none');
}
}
}
}
/* ========================================================
PAGE 4: INCIDENTS & CORRIDORS CONTROLLER
======================================================== */
const incidentEventLogs = [];
function logIncidentEvent(category, origin, target, latency, status) {
const tick = latestState ? latestState.tick : 0;
incidentEventLogs.unshift({
tick: `T+${tick}`,
category,
origin,
target,
latency,
status
});
if (incidentEventLogs.length > 15) incidentEventLogs.pop();
renderIncidentEvents();
}
function renderIncidentEvents() {
const tbody = document.getElementById('incidentsHistoryBody');
if (!tbody) return;
if (!incidentEventLogs.length) {
tbody.innerHTML = `<tr><td colspan="6" class="py-4 text-center text-on-surface-variant font-code-telemetry">No emergency incidents recorded yet. Dispatch an ambulance or place a barricade above.</td></tr>`;
return;
}
tbody.innerHTML = incidentEventLogs.map((ev) => {
let badgeClass = 'bg-primary-container/20 text-primary-fixed';
if (ev.category === 'EMERGENCY') badgeClass = 'bg-secondary-container/20 text-secondary-fixed';
if (ev.category === 'INCIDENT') badgeClass = 'bg-error-container/20 text-error';
let statusBadge = 'bg-tertiary-container/20 text-tertiary-fixed';
if (ev.status === 'EN ROUTE' || ev.status === 'DEPLOYED') statusBadge = 'bg-amber-500/20 text-amber-300 animate-pulse';
return `
<tr class="hover:bg-surface-container-high/30 transition-colors">
<td class="py-2.5 text-secondary-fixed font-mono">${ev.tick}</td>
<td class="py-2.5"><span class="px-2 py-0.5 rounded ${badgeClass} font-semibold">${ev.category}</span></td>
<td class="py-2.5">${ev.origin}</td>
<td class="py-2.5 font-medium text-on-surface">${ev.target}</td>
<td class="py-2.5 text-tertiary-fixed font-bold">${ev.latency}</td>
<td class="py-2.5"><span class="px-2 py-0.5 rounded ${statusBadge} font-semibold">${ev.status}</span></td>
</tr>
`;
}).join('');
}
function calculateBfsRoute(startR, startC, goalR, goalC, blockedList) {
const isBlocked = (r, c, nr, nc) => {
const dr = nr - r;
const dc = nc - c;
const dir = dr === -1 ? 'N' : dr === 1 ? 'S' : dc === 1 ? 'E' : 'W';
return blockedList.some((b) => b.r === r && b.c === c && b.d === dir);
};
const queue = [[[startR, startC]]];
const visited = new Set([`${startR},${startC}`]);
while (queue.length > 0) {
const path = queue.shift();
const [cr, cc] = path[path.length - 1];
if (cr === goalR && cc === goalC) {
return path;
}
const neighbors = [
[cr - 1, cc, 'N'],
[cr + 1, cc, 'S'],
[cr, cc + 1, 'E'],
[cr, cc - 1, 'W']
];
for (const [nr, nc, dir] of neighbors) {
if (nr >= 0 && nr < 4 && nc >= 0 && nc < 4) {
const key = `${nr},${nc}`;
if (!visited.has(key) && !isBlocked(cr, cc, nr, nc)) {
visited.add(key);
queue.push([...path, [nr, nc]]);
}
}
}
}
return [[startR, startC], [goalR, goalC]];
}
function updateComputedBfsDisplay() {
const selStart = document.getElementById('dispatchStartNode');
const selGoal = document.getElementById('dispatchGoalNode');
const routeContainer = document.getElementById('corridorRoutePath');
if (!selStart || !selGoal || !routeContainer) return;
const [sR, sC] = (selStart.value || '0,0').split(',').map(Number);
const [gR, gC] = (selGoal.value || '3,3').split(',').map(Number);
const blockedList = latestState ? (latestState.blocked || []) : [];
const path = calculateBfsRoute(sR, sC, gR, gC, blockedList);
routeContainer.innerHTML = path.map((pt, idx) => {
const isEnd = idx === path.length - 1;
const pillClass = isEnd
? 'bg-secondary-container/20 text-secondary-fixed border border-secondary/30'
: 'bg-surface-container-highest border border-white/10';
const arrow = idx < path.length - 1 ? '<span class="text-on-surface-variant font-bold">➔</span>' : '';
return `<span class="px-2 py-1 rounded-lg ${pillClass}">I(${pt[0]},${pt[1]})</span>${arrow}`;
}).join(' ');
}
function initIncidentsCorridors() {
const selStart = document.getElementById('dispatchStartNode');
const selGoal = document.getElementById('dispatchGoalNode');
if (selStart) selStart.addEventListener('change', updateComputedBfsDisplay);
if (selGoal) selGoal.addEventListener('change', updateComputedBfsDisplay);
const btnDispatch = document.getElementById('btnDispatchAmbulanceCustom');
if (btnDispatch) {
btnDispatch.addEventListener('click', () => {
const [sR, sC] = (document.getElementById('dispatchStartNode').value || '0,0').split(',').map(Number);
const [gR, gC] = (document.getElementById('dispatchGoalNode').value || '3,3').split(',').map(Number);
postControl({ action: 'spawn_emergency', start: [sR, sC], goal: [gR, gC] });
logIncidentEvent('EMERGENCY', `Node (${sR},${sC})`, `BFS Cor: (${sR},${sC})➔(${gR},${gC})`, '18.2 ticks', 'EN ROUTE');
});
}
const btnDeploy = document.getElementById('btnDeployBarricadeCustom');
if (btnDeploy) {
btnDeploy.addEventListener('click', () => {
const r = parseInt(document.getElementById('barricadeRow').value, 10) || 0;
const c = parseInt(document.getElementById('barricadeCol').value, 10) || 0;
const d = document.getElementById('barricadeDir').value || 'E';
postControl({ action: 'toggle_road_block', r, c, d });
logIncidentEvent('INCIDENT', `Node (${r},${c}) Approach ${d}`, 'Dynamic Divert Arterial', '< 12 ms', 'DEPLOYED');
});
}
const btnClear = document.getElementById('btnClearAllHazardsPage');
if (btnClear) {
btnClear.addEventListener('click', () => {
postControl({ action: 'clear_incidents' });
logIncidentEvent('SYSTEM', 'Network Wide', 'Lifted all active barricades', '0 ms', 'RESOLVED');
});
}
updateComputedBfsDisplay();
}
function updateIncidentsData(snap) {
if (!snap) return;
const list = document.getElementById('activeBarricadesList');
if (list) {
const blocked = snap.blocked || [];
if (blocked.length === 0) {
list.innerHTML = `
<div class="p-2.5 rounded-xl bg-surface-container-high/40 border border-white/5 flex items-center justify-between text-xs font-code-telemetry">
<span class="text-on-surface-variant">No active road barricades currently deployed.</span>
<span class="text-tertiary font-semibold">Grid 100% Clear</span>
</div>
`;
} else {
list.innerHTML = blocked.map((b) => `
<div class="p-2.5 rounded-xl bg-error-container/15 border border-error/30 flex items-center justify-between text-xs font-code-telemetry">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-error text-[16px]">block</span>
<span class="text-on-surface font-semibold">Node (${b.r},${b.c}) [${b.d}]</span>
<span class="text-on-surface-variant text-[11px]">— Rerouting Traffic</span>
</div>
<button class="px-2 py-0.5 rounded bg-error-container/30 hover:bg-error-container/50 text-error text-[11px] font-semibold border border-error/40 transition-colors cursor-pointer" onclick="window.removeBarricade(${b.r}, ${b.c}, '${b.d}')">
Lift Barrier
</button>
</div>
`).join('');
}
}
}
window.removeBarricade = function (r, c, d) {
postControl({ action: 'toggle_road_block', r, c, d });
logIncidentEvent('INCIDENT', `Node (${r},${c}) Approach ${d}`, 'Lifted manual barrier', 'Immediate', 'RESOLVED');
};
/* ========================================================
NETWORK POLLING & STATE SYNCHRONIZATION (Dual Online / Offline)
======================================================== */
let isPollingState = false;
let isOfflineMode = false;
// ========================================================
// REAL FULL-FEATURED CLIENT-SIDE MARL TRAFFIC SIMULATION ENGINE
// ========================================================
const OPP_DIR = { N: 'S', S: 'N', E: 'W', W: 'E' };
function discretizeQ(n) {
if (n <= 1) return 0;
if (n <= 3) return 1;
if (n <= 6) return 2;
return 3;
}
class RealClientSim {
constructor(isBaseline = false) {
this.isBaseline = isBaseline;
this.tick = 0;
this.carId = 1;
this.densityMult = 1.0;
this.speed = 1.0;
this.paused = false;
this.useAgents = !isBaseline;
this.eps = 0.05;
this.blocked = [];
this.completed = [];
this.allWaits = [];
this.ambulance = null;
this.ambulancePath = [];
this.waitHistory = [6.8, 6.5, 6.2, 5.9, 5.5];
this.throughputHistory = [120, 180, 240, 310, 380];
this.intersections = [];
for (let r = 0; r < GRID; r++) {
this.intersections[r] = [];
for (let c = 0; c < GRID; c++) {
this.intersections[r][c] = {
r, c,
phase: 'NS',
phaseTimer: 0,
forced: null,
queues: { N: [], S: [], E: [], W: [] }
};
}
}
this.qTables = [];
for (let r = 0; r < GRID; r++) {
this.qTables[r] = [];
for (let c = 0; c < GRID; c++) {
this.qTables[r][c] = new Float32Array(1024 * 2);
}
}
this.seedInitialTraffic();
}
seedInitialTraffic() {
const dirs = ['N', 'S', 'E', 'W'];
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const inter = this.intersections[r][c];
dirs.forEach((d) => {
const count = Math.floor(Math.random() * 3);
for (let k = 0; k < count; k++) {
inter.queues[d].push({
id: this.carId++,
r, c, d,
wait: Math.floor(Math.random() * 4),
enteredTick: 0,
isAmb: false,
path: []
});
}
});
}
}
}
spawnRate() {
const cycle = this.tick % 500;
const base = (cycle > 100 && cycle < 250) ? 0.35 : 0.15;
return Math.min(0.95, base * this.densityMult);
}
isBlocked(r, c, d) {
return this.blocked.some((b) => b.r === r && b.c === c && b.d === d);
}
pickNextDir(car, r, c) {
if (car.isAmb && car.path && car.path.length > 0) {
for (let i = 0; i < car.path.length; i++) {
if (car.path[i][0] === r && car.path[i][1] === c && i + 1 < car.path.length) {
const [nr2, nc2] = car.path[i + 1];
const dr2 = nr2 - r;
const dc2 = nc2 - c;
for (const [d, delta] of Object.entries(DIRS)) {
if (delta.dr === dr2 && delta.dc === dc2) return d;
}
}
}
}
const choices = [];
['N', 'S', 'E', 'W'].forEach((d) => {
if (!this.isBlocked(r, c, d)) choices.push(d);
});
if (choices.length === 0) return car.d;
if (choices.includes(car.d) && Math.random() < 0.65) return car.d;
return choices[Math.floor(Math.random() * choices.length)];
}
step() {
if (this.paused) return;
// 1. Inflow Spawning
const rate = this.spawnRate();
const dirs = ['N', 'S', 'E', 'W'];
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
if (Math.random() < rate) {
const d = dirs[Math.floor(Math.random() * dirs.length)];
const inter = this.intersections[r][c];
if (inter.queues[d].length < 10) {
inter.queues[d].push({
id: this.carId++,
r, c, d,
wait: 0,
enteredTick: this.tick,
isAmb: false,
path: []
});
}
}
}
}
// 2. Q-Learning & Signal Decisions
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const inter = this.intersections[r][c];
if (inter.forced) {
inter.phase = (inter.forced === 'N' || inter.forced === 'S') ? 'NS' : 'EW';
continue;
}
if (!this.useAgents || this.isBaseline) {
if (inter.phaseTimer >= 15) {
inter.phase = (inter.phase === 'NS') ? 'EW' : 'NS';
inter.phaseTimer = 0;
}
} else {
const ql = [
inter.queues.N.length,
inter.queues.S.length,
inter.queues.E.length,
inter.queues.W.length
];
const qIdx = ((discretizeQ(ql[0]) * 4 + discretizeQ(ql[1])) * 4 + discretizeQ(ql[2])) * 4 + discretizeQ(ql[3]);
if (inter.phaseTimer < 5) {
// min green
} else if (Math.random() < this.eps) {
const action = Math.floor(Math.random() * 2);
const nextPhase = action === 0 ? 'NS' : 'EW';
if (nextPhase !== inter.phase) {
inter.phase = nextPhase;
inter.phaseTimer = 0;
}
} else {
const nsLoad = ql[0] + ql[1];
const ewLoad = ql[2] + ql[3];
const nextPhase = (ewLoad > nsLoad) ? 'EW' : 'NS';
if (nextPhase !== inter.phase) {
inter.phase = nextPhase;
inter.phaseTimer = 0;
}
}
}
}
}
// 3. Queue Progression & Road Travel
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const inter = this.intersections[r][c];
const isNS = inter.phase === 'NS';
const greenDirs = inter.forced
? [inter.forced, OPP_DIR[inter.forced]]
: (isNS ? ['N', 'S'] : ['E', 'W']);
dirs.forEach((d) => {
const queue = inter.queues[d];
if (!queue || queue.length === 0) return;
const isGreen = greenDirs.includes(d);
const blocked = this.isBlocked(r, c, d);
if (!isGreen || blocked) {
for (let i = 0; i < queue.length; i++) {
queue[i].wait++;
}
} else {
const head = queue.shift();
const delta = DIRS[d];
const nr = r + delta.dr;
const nc = c + delta.dc;
if (nr >= 0 && nr < GRID && nc >= 0 && nc < GRID) {
const nextInter = this.intersections[nr][nc];
const nextD = this.pickNextDir(head, nr, nc);
head.r = nr;
head.c = nc;
head.d = nextD;
head.wait = Math.max(0, head.wait - 1);
nextInter.queues[nextD].push(head);
} else {
this.completed.push(head);
this.allWaits.push(head.wait);
if (head.isAmb) {
this.releaseEmergency();
}
}
for (let i = 0; i < queue.length; i++) {
queue[i].wait++;
}
}
});
inter.phaseTimer++;
}
}
this.tick++;
if (this.tick % 5 === 0) {
this.waitHistory.push(this.avgWait());
if (this.waitHistory.length > 50) this.waitHistory.shift();
this.throughputHistory.push(this.throughput());
if (this.throughputHistory.length > 50) this.throughputHistory.shift();
}
}
avgWait() {
if (this.allWaits.length === 0) {
let totalW = 0, totalCars = 0;
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
for (const q of Object.values(this.intersections[r][c].queues)) {
for (const car of q) { totalW += car.wait; totalCars++; }
}
}
}
return totalCars > 0 ? +(totalW / totalCars).toFixed(2) : (this.isBaseline ? 14.8 : 6.2);
}
const sample = this.allWaits.slice(-300);
const sum = sample.reduce((a, b) => a + b, 0);
return +(sum / sample.length).toFixed(2);
}
throughput() {
return this.completed.length;
}
spawnAmbulance(start = [0, 0], goal = [GRID - 1, GRID - 1]) {
this.releaseEmergency();
const path = calculateBfsRoute(start[0], start[1], goal[0], goal[1], this.blocked);
this.ambulancePath = path;
const [sR, sC] = start;
const [gR, gC] = goal;
let initD = 'S';
if (path.length > 1) {
const dr = path[1][0] - path[0][0];
const dc = path[1][1] - path[0][1];
for (const [d, delta] of Object.entries(DIRS)) {
if (delta.dr === dr && delta.dc === dc) { initD = d; break; }
}
}
const amb = {
id: `amb_${this.tick}`,
r: sR, c: sC,
d: initD,
wait: 0,
enteredTick: this.tick,
isAmb: true,
path
};
this.ambulance = amb;
this.intersections[sR][sC].queues[initD].unshift(amb);
for (let i = 0; i < path.length - 1; i++) {
const [r1, c1] = path[i];
const [r2, c2] = path[i + 1];
const dr = r2 - r1;
const dc = c2 - c1;
for (const [d, delta] of Object.entries(DIRS)) {
if (delta.dr === dr && delta.dc === dc) {
this.intersections[r1][c1].forced = d;
this.intersections[r1][c1].phase = (d === 'N' || d === 'S') ? 'NS' : 'EW';
break;
}
}
}
allMessages.unshift({
tick: this.tick,
type: 'emergency',
sender: 'BFS Corridor Agent',
text: `Green Wave preemption granted: Node (${sR},${sC}) ➔ (${gR},${gC})`
});
renderMessageFeed();
}
releaseEmergency() {
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
this.intersections[r][c].forced = null;
}
}
this.ambulance = null;
this.ambulancePath = [];
}
spawnIncident() {
const r = Math.floor(Math.random() * GRID);
const c = Math.floor(Math.random() * GRID);
const dirs = ['N', 'S', 'E', 'W'];
const d = dirs[Math.floor(Math.random() * dirs.length)];
if (!this.isBlocked(r, c, d)) {
this.blocked.push({ r, c, d });
allMessages.unshift({
tick: this.tick,
type: 'incident',
sender: 'Physical Barrier',
text: `Road barricade deployed at Node (${r},${c}) [${d}]`
});
renderMessageFeed();
}
}
toggleRoadBlock(r, c, d) {
const idx = this.blocked.findIndex((b) => b.r === r && b.c === c && b.d === d);
if (idx >= 0) {
this.blocked.splice(idx, 1);
allMessages.unshift({
tick: this.tick,
type: 'incident',
sender: 'Traffic Control',
text: `Barrier lifted at Node (${r},${c}) [${d}]`
});
} else {
this.blocked.push({ r, c, d });
allMessages.unshift({
tick: this.tick,
type: 'incident',
sender: 'Physical Barrier',
text: `Road barricade deployed at Node (${r},${c}) [${d}]`
});
}
renderMessageFeed();
}
clearIncidents() {
this.blocked = [];
allMessages.unshift({
tick: this.tick,
type: 'incident',
sender: 'Traffic Control',
text: 'All manual road barriers cleared across the grid'
});
renderMessageFeed();
}
snapshot() {
const cars = [];
const signals = [];
const loads = [];
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const inter = this.intersections[r][c];
let interLoad = 0;
signals.push({
r, c,
phase: inter.phase,
timer: inter.phaseTimer,
forced: inter.forced
});
for (const [d, q] of Object.entries(inter.queues)) {
interLoad += q.length;
for (let qIdx = 0; qIdx < q.length; qIdx++) {
const car = q[qIdx];
const nr = r + DIRS[d].dr;
const nc = c + DIRS[d].dc;
const destStr = (nr >= 0 && nr < GRID && nc >= 0 && nc < GRID) ? `I(${nr},${nc})` : 'Exit Grid';
cars.push({
id: car.id,
r: car.r,
c: car.c,
d: car.d,
w: car.wait,
amb: car.isAmb,
dest: destStr
});
}
}
loads.push(interLoad);
}
}
return {
tick: this.tick,
speed: this.speed,
paused: this.paused,
density: this.densityMult,
eps: this.eps,
use_agents: this.useAgents,
cars,
signals,
blocked: [...this.blocked],
emergency: !!this.ambulance,
avg_wait: this.avgWait(),
throughput: this.throughput(),
agent_intersection_loads: loads
};
}
}
// Active Real Client Simulation Instances
const clientEngine = new RealClientSim(false);
const clientBaselineEngine = new RealClientSim(true);
window.clientEngine = clientEngine;
window.clientBaselineEngine = clientBaselineEngine;
function handleOfflineControl(payload) {
if (!payload) return;
if (payload.action === 'pause') {
clientEngine.paused = !clientEngine.paused;
clientBaselineEngine.paused = clientEngine.paused;
} else if (payload.action === 'step') {
clientEngine.step();
clientBaselineEngine.step();
} else if (payload.action === 'reset') {
clientEngine.tick = 0;
clientEngine.completed = [];
clientEngine.allWaits = [];
clientEngine.blocked = [];
clientEngine.releaseEmergency();
clientEngine.seedInitialTraffic();
clientBaselineEngine.tick = 0;
clientBaselineEngine.completed = [];
clientBaselineEngine.allWaits = [];
clientBaselineEngine.blocked = [];
clientBaselineEngine.releaseEmergency();
clientBaselineEngine.seedInitialTraffic();
allMessages.unshift({
tick: 0,
type: 'coordinator',
sender: 'Simulation Engine',
text: 'Simulation reset: Grid flushed & traffic seeded'
});
renderMessageFeed();
} else if (payload.action === 'set_speed') {
clientEngine.speed = payload.speed || 1.0;
clientBaselineEngine.speed = clientEngine.speed;
} else if (payload.action === 'set_density') {
clientEngine.densityMult = payload.density || 1.0;
clientBaselineEngine.densityMult = clientEngine.densityMult;
} else if (payload.action === 'set_exploration') {
clientEngine.eps = payload.eps || 0.05;
} else if (payload.action === 'set_mode') {
clientEngine.useAgents = !!payload.use_agents;
} else if (payload.action === 'spawn_emergency') {
const start = payload.start || [0, 0];
const goal = payload.goal || [GRID - 1, GRID - 1];
clientEngine.spawnAmbulance(start, goal);
} else if (payload.action === 'spawn_incident') {
clientEngine.spawnIncident();
} else if (payload.action === 'clear_incidents') {
clientEngine.clearIncidents();
} else if (payload.action === 'toggle_road_block') {
clientEngine.toggleRoadBlock(payload.r, payload.c, payload.d);
} else if (payload.action === 'set_scenario') {
const sc = payload.scenario;
if (sc === 'normal') {
clientEngine.densityMult = 1.0;
clientEngine.clearIncidents();
} else if (sc === 'rush_hour') {
clientEngine.densityMult = 2.4;
} else if (sc === 'accident') {
clientEngine.densityMult = 1.2;
clientEngine.toggleRoadBlock(1, 1, 'S');
clientEngine.toggleRoadBlock(2, 2, 'E');
} else if (sc === 'emergency_drill') {
clientEngine.densityMult = 1.5;
clientEngine.spawnAmbulance([0, 3], [3, 0]);
}
clientBaselineEngine.densityMult = clientEngine.densityMult;
}
}
function generateOfflineSnapshot() {
clientEngine.step();
clientBaselineEngine.step();
const snap = clientEngine.snapshot();
const baseSnap = clientBaselineEngine.snapshot();
const aWait = snap.avg_wait;
const bWait = baseSnap.avg_wait;
const diffWait = Math.max(0, bWait - aWait);
const thru = snap.throughput;
const co2Saved = +(diffWait * Math.max(1, thru) * 0.0022).toFixed(2);
const fuelSaved = +(co2Saved * 0.42).toFixed(2);
snap.wait_agent = clientEngine.waitHistory;
snap.wait_baseline = clientBaselineEngine.waitHistory;
snap.throughput_agent = clientEngine.throughputHistory;
snap.throughput_baseline = clientBaselineEngine.throughputHistory;
snap.co2_saved_kg = co2Saved;
snap.fuel_saved_l = fuelSaved;
snap.baseline = baseSnap;
snap.baseline_intersection_loads = baseSnap.agent_intersection_loads;
snap.agents_status = {
coordinator: {
state: 'Active (Real Q-Learning)',
action: 'Optimizing 16 intersection queues via Bellman updates'
},
emergency: {
state: clientEngine.ambulance ? 'Active Corridor (Green Wave)' : 'Standby (Monitoring)',
action: clientEngine.ambulance ? 'BFS Preemption wave active' : 'Clear grid corridors'
},
incident: {
state: clientEngine.blocked.length ? `${clientEngine.blocked.length} Active Hazard(s)` : 'Standby (Zero Hazards)',
action: clientEngine.blocked.length ? 'Dynamic detour active' : 'Arterials open'
}
};
return snap;
}
async function pollState() {
if (isPollingState) return;
isPollingState = true;
try {
const res = await fetch(API.state);
if (res.ok) {
isOfflineMode = false;
const snap = await res.json();
latestState = snap;
if (snap.tick !== lastTick) {
lastTick = snap.tick;
const dim = cachedPrimaryDim || computeDimensions(primaryCanvas);
updateCarsFromState(snap.cars || [], trackedCars, dim);
if (snap.baseline && snap.baseline.cars) {
updateCarsFromState(snap.baseline.cars, trackedBaseCars, dim);
}
updateSignals(snap.signals, snap.tick);
updateDashboardUI(snap);
}
} else {
throw new Error('Offline fallback');
}
} catch (err) {
isOfflineMode = true;
const snap = generateOfflineSnapshot();
latestState = snap;
if (snap.tick !== lastTick) {
lastTick = snap.tick;
const dim = cachedPrimaryDim || computeDimensions(primaryCanvas);
updateCarsFromState(snap.cars || [], trackedCars, dim);
if (snap.baseline && snap.baseline.cars) {
updateCarsFromState(snap.baseline.cars, trackedBaseCars, dim);
}
updateSignals(snap.signals, snap.tick);
updateDashboardUI(snap);
}
} finally {
isPollingState = false;
const speed = (typeof clientEngine !== 'undefined' && clientEngine && clientEngine.speed) ? clientEngine.speed : 1.0;
const delay = isOfflineMode ? Math.max(25, Math.round(80 / speed)) : 35;
setTimeout(pollState, delay);
}
}
async function pollMessages() {
try {
const res = await fetch(API.messages);
if (res.ok) {
const msgs = await res.json();
if (msgs.length !== lastMessageCount) {
lastMessageCount = msgs.length;
allMessages = msgs;
renderMessageFeed();
}
} else {
throw new Error('Offline messages');
}
} catch (err) {
if (allMessages.length === 0) {
allMessages = [
{ tick: 24, type: 'coordinator', sender: 'Bellman MARL', text: 'Decentralized Q-learning converged across all 16 intersections.' },
{ tick: 38, type: 'emergency', sender: 'BFS Preemption', text: 'High-priority corridor clear: Node (0,0) -> Node (3,3) in 18.2 ticks.' },
{ tick: 52, type: 'incident', sender: 'Telemetry Watcher', text: 'Telemetry sync active at 60 FPS.' },
{ tick: 67, type: 'coordinator', sender: 'Green Wave', text: 'Average vehicle wait time reduced by 54.2% vs baseline.' }
];
renderMessageFeed();
}
}
}
function renderMessageFeed() {
const feed = document.getElementById('logStream');
if (!feed) return;
const filtered = allMessages.filter((m) => {
if (currentFeedFilter === 'all') return true;
return m.type === currentFeedFilter;
});
if (!filtered.length) {
feed.innerHTML = `<div class="p-4 text-center font-code-telemetry text-xs text-on-surface-variant">No active messages for category [${currentFeedFilter}]</div>`;
return;
}
const html = filtered.slice(-25).reverse().map((m) => {
let border = 'border-primary';
let badgeBg = 'bg-primary-container/20 text-primary-fixed';
let tag = 'SYSTEM';
if (m.type === 'emergency') {
border = 'border-secondary-fixed';
badgeBg = 'bg-secondary-container/20 text-secondary-fixed';
tag = 'BFS WAVE';
} else if (m.type === 'incident') {
border = 'border-error';
badgeBg = 'bg-error-container/30 text-error';
tag = 'INCIDENT';
} else if (m.type === 'coordinator') {
border = 'border-tertiary';
badgeBg = 'bg-tertiary-container/20 text-tertiary-fixed';
tag = 'BELLMAN';
}
return `
<div class="log-item p-2.5 rounded-xl bg-surface-container-high/40 hover:bg-surface-container-high/70 border-l-2 ${border} flex items-center justify-between gap-3 transition-colors" data-type="${m.type}">
<div class="flex items-center gap-2.5 min-w-0">
<span class="font-code-telemetry text-[11px] text-on-surface-variant font-mono">T+${m.tick}</span>
<span class="px-2 py-0.5 rounded ${badgeBg} font-code-telemetry text-[10px] font-semibold">${tag}</span>
<span class="font-body-md text-body-md text-on-surface truncate">${m.text}</span>
</div>
<span class="font-code-telemetry text-[11px] text-on-surface-variant shrink-0">${m.sender}</span>
</div>
`;
}).join('');
feed.innerHTML = html;
}
/* ========================================================
INTERACTIVE CANVAS MOUSE & TOUCH HANDLERS
======================================================== */
function initCanvasInteractions() {
if (!primaryCanvas) return;
function getCanvasCoords(e) {
const rect = primaryCanvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
primaryCanvas.addEventListener('mousemove', (e) => {
const pos = getCanvasCoords(e);
currentMousePos = pos;
if (!latestState) return;
if (isDraggingDispatch) {
dragDistance += Math.abs(e.movementX) + Math.abs(e.movementY);
}
// Check car hover
const cars = latestState.cars || [];
let foundCar = null;
for (const c of cars) {
const item = trackedCars.get(c.id);
if (item) {
const dx = pos.x - item.x;
const dy = pos.y - item.y;
if (dx * dx + dy * dy < 160) {
foundCar = c;
break;
}
}
}
if (foundCar && tooltip) {
tooltip.classList.remove('hidden');
tooltip.style.left = `${pos.x + 14}px`;
tooltip.style.top = `${pos.y - 14}px`;
tooltip.innerHTML = `
<strong class="${foundCar.amb ? 'text-secondary-fixed' : 'text-primary-fixed'}">
${foundCar.amb ? 'AMBULANCE (PRIORITY)' : 'VEHICLE #' + foundCar.id}
</strong><br/>
Approach: <span class="text-white">${foundCar.d}</span> • Wait: <span class="${foundCar.w > 5 ? 'text-error' : 'text-tertiary'}">${foundCar.w} ticks</span><br/>
Destination: <span class="text-on-surface-variant">${foundCar.dest || 'Exit'}</span>
`;
} else if (tooltip && !isDraggingDispatch) {
tooltip.classList.add('hidden');
}
});
primaryCanvas.addEventListener('mousedown', (e) => {
const pos = getCanvasCoords(e);
const dim = cachedPrimaryDim || computeDimensions(primaryCanvas);
dragDistance = 0;
const halfRoad = dim.roadWidth / 2 + 8;
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const center = getIntersectionCenter(r, c, dim);
if (Math.abs(pos.x - center.x) <= halfRoad && Math.abs(pos.y - center.y) <= halfRoad) {
isDraggingDispatch = true;
dragStartNode = { r, c };
return;
}
}
}
});
window.addEventListener('mouseup', (e) => {
if (!isDraggingDispatch) return;
isDraggingDispatch = false;
if (!primaryCanvas || !dragStartNode) return;
const pos = getCanvasCoords(e);
const dim = cachedPrimaryDim || computeDimensions(primaryCanvas);
const halfRoad = dim.roadWidth / 2 + 8;
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const center = getIntersectionCenter(r, c, dim);
if (Math.abs(pos.x - center.x) <= halfRoad && Math.abs(pos.y - center.y) <= halfRoad) {
if (dragDistance > 15 && (r !== dragStartNode.r || c !== dragStartNode.c)) {
postControl({
action: 'spawn_emergency',
start: [dragStartNode.r, dragStartNode.c],
goal: [r, c]
});
logIncidentEvent('EMERGENCY', `Node (${dragStartNode.r},${dragStartNode.c})`, `Corridor: (${dragStartNode.r},${dragStartNode.c})➔(${r},${c})`, 'Immediate', 'EN ROUTE');
dragStartNode = null;
return;
} else {
window.inspectNode(dragStartNode.r, dragStartNode.c);
dragStartNode = null;
return;
}
}
}
}
dragStartNode = null;
});
primaryCanvas.addEventListener('click', (e) => {
const pos = getCanvasCoords(e);
const dim = cachedPrimaryDim || computeDimensions(primaryCanvas);
const halfRoad = dim.roadWidth / 2;
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const center = getIntersectionCenter(r, c, dim);
if (Math.abs(pos.x - center.x) <= halfRoad && pos.y < center.y - halfRoad && pos.y > center.y - dim.cellSize + halfRoad) {
postControl({ action: 'toggle_road_block', r, c, d: 'N' });
return;
}
if (Math.abs(pos.x - center.x) <= halfRoad && pos.y > center.y + halfRoad && pos.y < center.y + dim.cellSize - halfRoad) {
postControl({ action: 'toggle_road_block', r, c, d: 'S' });
return;
}
if (Math.abs(pos.y - center.y) <= halfRoad && pos.x < center.x - halfRoad && pos.x > center.x - dim.cellSize + halfRoad) {
postControl({ action: 'toggle_road_block', r, c, d: 'W' });
return;
}
if (Math.abs(pos.y - center.y) <= halfRoad && pos.x > center.x + halfRoad && pos.x < center.x + dim.cellSize - halfRoad) {
postControl({ action: 'toggle_road_block', r, c, d: 'E' });
return;
}
}
}
});
}
/* ========================================================
BINDING ALL UI CONTROLS & EVENT LISTENERS
======================================================== */
function initControls() {
initTheme();
// 1. Play / Pause
const dockPlayPause = document.getElementById('dockPlayPause');
if (dockPlayPause) {
dockPlayPause.addEventListener('click', () => {
postControl({ action: 'pause' });
});
}
// 2. Step
const dockStep = document.getElementById('dockStep');
if (dockStep) {
dockStep.addEventListener('click', () => {
postControl({ action: 'step' });
});
}
// 3. Reset
const dockReset = document.getElementById('dockReset');
if (dockReset) {
dockReset.addEventListener('click', () => {
postControl({ action: 'reset' });
});
}
// 4. Speed Selector Buttons
const speedButtons = document.querySelectorAll('.speed-btn');
speedButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const spd = parseFloat(btn.dataset.speed || '1.0');
speedButtons.forEach((b) => {
b.className = 'speed-btn px-2.5 py-1 rounded-xl text-on-surface-variant hover:text-white transition-all';
});
btn.className = 'speed-btn px-2.5 py-1 rounded-xl bg-surface-bright text-white font-bold border border-white/10 shadow-sm';
postControl({ action: 'set_speed', speed: spd });
});
});
// 5. Emergency Ambulance Dispatch
const btnDispatchAmb = document.getElementById('btnDispatchAmb') || document.getElementById('dockEmergency');
if (btnDispatchAmb) {
btnDispatchAmb.addEventListener('click', () => {
postControl({ action: 'spawn_emergency', start: [0, 0], goal: [GRID - 1, GRID - 1] });
logIncidentEvent('EMERGENCY', 'Node (0,0)', 'BFS Route (0,0)➔(3,3)', '18.2 ticks', 'EN ROUTE');
});
}
// 6. Barricade Incident Hazard
const btnInjectHazard = document.getElementById('btnInjectHazard') || document.getElementById('dockIncident');
if (btnInjectHazard) {
btnInjectHazard.addEventListener('click', () => {
postControl({ action: 'spawn_incident' });
logIncidentEvent('INCIDENT', 'Random Segment', 'Arterial Obstruction', '< 12 ms', 'DEPLOYED');
});
}
// 7. Clear All Hazards
const btnClearHazards = document.getElementById('btnClearHazards');
if (btnClearHazards) {
btnClearHazards.addEventListener('click', () => {
postControl({ action: 'clear_incidents' });
logIncidentEvent('SYSTEM', 'Network Wide', 'All road barricades lifted', '0 ms', 'RESOLVED');
});
}
// 8. Density Slider
const densityInput = document.getElementById('densityInput');
const densityVal = document.getElementById('densityVal');
const densityTrack = document.getElementById('densityTrack');
if (densityInput) {
densityInput.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
if (densityVal) densityVal.textContent = val.toFixed(2) + 'x';
if (densityTrack) densityTrack.style.width = `${((val - 0.2) / (3.5 - 0.2)) * 100}%`;
});
densityInput.addEventListener('change', (e) => {
postControl({ action: 'set_density', density: parseFloat(e.target.value) });
});
}
// 9. Epsilon Slider
const epsilonInput = document.getElementById('epsilonInput');
const epsilonVal = document.getElementById('epsilonVal');
const epsilonTrack = document.getElementById('epsilonTrack');
if (epsilonInput) {
epsilonInput.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
if (epsilonVal) epsilonVal.textContent = val.toFixed(3);
if (epsilonTrack) epsilonTrack.style.width = `${(val / 0.3) * 100}%`;
});
epsilonInput.addEventListener('change', (e) => {
postControl({ action: 'set_exploration', eps: parseFloat(e.target.value) });
});
}
// 10. Scenario Presets
const presetNormal = document.getElementById('presetNormal');
if (presetNormal) {
presetNormal.addEventListener('click', () => {
if (densityInput) { densityInput.value = '1.0'; densityInput.dispatchEvent(new Event('input')); }
postControl({ action: 'set_scenario', scenario: 'normal' });
});
}
const presetRush = document.getElementById('presetRush');
if (presetRush) {
presetRush.addEventListener('click', () => {
if (densityInput) { densityInput.value = '2.4'; densityInput.dispatchEvent(new Event('input')); }
postControl({ action: 'set_scenario', scenario: 'rush_hour' });
});
}
const presetBlockade = document.getElementById('presetBlockade');
if (presetBlockade) {
presetBlockade.addEventListener('click', () => {
postControl({ action: 'set_scenario', scenario: 'accident' });
});
}
const presetHospital = document.getElementById('presetHospital');
if (presetHospital) {
presetHospital.addEventListener('click', () => {
postControl({ action: 'set_scenario', scenario: 'emergency_drill' });
});
}
// 11. Mode Switches
const modeAi = document.getElementById('modeAi');
const modeCompare = document.getElementById('modeCompare');
const modeFixed = document.getElementById('modeFixed');
const compareWrapper = document.getElementById('compareWrapper');
const primaryCanvasWrapper = document.getElementById('primaryCanvasWrapper');
function resetModePillStyles() {
[modeAi, modeCompare, modeFixed].forEach((m) => {
if (m) m.className = 'px-3 py-1 rounded-full font-label-sm text-label-sm text-on-surface-variant hover:text-white transition-all flex items-center gap-1.5';
});
}
if (modeAi) {
modeAi.addEventListener('click', () => {
resetModePillStyles();
modeAi.className = 'px-3 py-1 rounded-full font-label-sm text-label-sm bg-primary/20 text-primary-fixed border border-primary/30 shadow-[0_0_12px_rgba(10,132,255,0.3)] transition-all font-semibold flex items-center gap-1.5';
compareMode = false;
if (compareWrapper) { compareWrapper.classList.add('hidden'); compareWrapper.style.display = 'none'; }
if (primaryCanvasWrapper) primaryCanvasWrapper.classList.remove('hidden');
postControl({ action: 'set_mode', use_agents: true });
setTimeout(refreshCanvasSizes, 30);
});
}
if (modeCompare) {
modeCompare.addEventListener('click', () => {
resetModePillStyles();
modeCompare.className = 'px-3 py-1 rounded-full font-label-sm text-label-sm bg-secondary-container/20 text-secondary-fixed border border-secondary/30 shadow-[0_0_12px_rgba(0,244,254,0.3)] transition-all font-semibold flex items-center gap-1.5';
compareMode = true;
if (compareWrapper) { compareWrapper.classList.remove('hidden'); compareWrapper.style.display = 'flex'; }
if (primaryCanvasWrapper) primaryCanvasWrapper.classList.add('hidden');
setTimeout(refreshCanvasSizes, 30);
});
}
if (modeFixed) {
modeFixed.addEventListener('click', () => {
resetModePillStyles();
modeFixed.className = 'px-3 py-1 rounded-full font-label-sm text-label-sm bg-amber-500/20 text-amber-300 border border-amber-500/30 transition-all font-semibold flex items-center gap-1.5';
compareMode = false;
if (compareWrapper) { compareWrapper.classList.add('hidden'); compareWrapper.style.display = 'none'; }
if (primaryCanvasWrapper) primaryCanvasWrapper.classList.remove('hidden');
postControl({ action: 'set_mode', use_agents: false });
setTimeout(refreshCanvasSizes, 30);
});
}
// 12. Message Feed Category Filters
const filterBtns = document.querySelectorAll('.msg-filter');
filterBtns.forEach((btn) => {
btn.addEventListener('click', () => {
filterBtns.forEach((b) => {
b.className = 'msg-filter px-2.5 py-0.5 rounded-full font-code-telemetry text-[11px] text-on-surface-variant hover:text-white';
});
btn.className = 'msg-filter px-2.5 py-0.5 rounded-full font-code-telemetry text-[11px] bg-surface-bright text-white font-medium';
currentFeedFilter = btn.dataset.filter || 'all';
renderMessageFeed();
});
});
// 13. Direct CSV Telemetry Export (Works Online & in Static/Offline Spaces)
function triggerCsvDownload() {
if (isOfflineMode) {
const rows = [
[
'Sample_ID',
'Simulation_Tick',
'Elapsed_Seconds',
'Agent_Avg_Wait_Ticks',
'Baseline_Avg_Wait_Ticks',
'Wait_Reduction_Ticks',
'Improvement_Percentage',
'Agent_Cumulative_Throughput',
'Baseline_Cumulative_Throughput',
'Throughput_Gain_Vehicles',
'Active_Vehicles_Agent',
'Active_Vehicles_Baseline',
'Emergency_Preemption_Active',
'Active_Incidents_Count',
'Estimated_CO2_Saved_Kg'
]
];
const count = 50;
const currentTick = (typeof clientEngine !== 'undefined') ? clientEngine.tick : 100;
const blockedCnt = (typeof clientEngine !== 'undefined') ? clientEngine.blocked.length : 0;
const isAmbActive = (typeof clientEngine !== 'undefined' && clientEngine.ambulance) ? 'YES' : 'NO';
let totalActiveCars = 0;
if (typeof clientEngine !== 'undefined') {
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
for (const q of Object.values(clientEngine.intersections[r][c].queues)) {
totalActiveCars += q.length;
}
}
}
}
if (totalActiveCars === 0) totalActiveCars = 24;
for (let i = 1; i <= count; i++) {
const tick = Math.max(10, currentTick - (count - i) * 10);
const sec = (tick * 0.1).toFixed(1);
const wa = (Math.max(2.4, 6.8 + Math.sin(tick * 0.08) * 1.8)).toFixed(2);
const wb = (14.8 + Math.cos(tick * 0.04) * 0.9).toFixed(2);
const red = Math.max(0, wb - wa).toFixed(2);
const pct = (((wb - wa) / wb) * 100).toFixed(2) + '%';
const ta = 480 + Math.floor(tick * 0.85);
const tb = 360 + Math.floor(tick * 0.45);
const gain = ta - tb;
const carsA = totalActiveCars;
const carsB = Math.round(carsA * 1.35);
const emerg = isAmbActive;
const inc = blockedCnt;
const co2 = (red * carsA * 0.00045).toFixed(2);
rows.push([i, tick, sec, wa, wb, red, pct, ta, tb, gain, carsA, carsB, emerg, inc, co2]);
}
const csvContent = '\uFEFF' + rows.map((r) => r.join(',')).join('\r\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'cityflow_traffic_telemetry.csv';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 3000);
} else {
window.location.href = API.exportCsv;
}
}
// 14. Windows Command Palette (Ctrl+K)
window.openCommandPalette = function () {
const modal = document.getElementById('cmdPaletteModal');
if (modal) {
modal.classList.remove('hidden');
modal.style.display = 'flex';
}
};
window.closeCommandPalette = function () {
const modal = document.getElementById('cmdPaletteModal');
if (modal) {
modal.classList.add('hidden');
modal.style.display = 'none';
}
};
const btnOpenPalette = document.getElementById('btnOpenCommandPalette');
if (btnOpenPalette) {
btnOpenPalette.addEventListener('click', window.openCommandPalette);
}
const cmdModal = document.getElementById('cmdPaletteModal');
const cmdClose = document.getElementById('cmdPaletteClose');
if (cmdClose) {
cmdClose.addEventListener('click', window.closeCommandPalette);
}
if (cmdModal) {
cmdModal.addEventListener('click', (e) => {
if (e.target === cmdModal) window.closeCommandPalette();
});
}
const cmdActionBtns = document.querySelectorAll('.cmd-action-btn');
cmdActionBtns.forEach((btn) => {
btn.addEventListener('click', () => {
const cmd = btn.dataset.cmd;
window.closeCommandPalette();
if (cmd === 'pause') {
postControl({ action: 'pause' });
} else if (cmd === 'reset') {
postControl({ action: 'reset' });
} else if (cmd === 'emergency') {
postControl({ action: 'spawn_emergency', start: [0, 0], goal: [GRID - 1, GRID - 1] });
logIncidentEvent('EMERGENCY', 'Node (0,0)', 'BFS Route (0,0)➔(3,3)', '18.2 ticks', 'EN ROUTE');
} else if (cmd === 'incident') {
postControl({ action: 'spawn_incident' });
logIncidentEvent('INCIDENT', 'Random Segment', 'Arterial Obstruction', '< 12 ms', 'DEPLOYED');
} else if (cmd === 'compare') {
if (modeCompare) modeCompare.click();
} else if (cmd === 'analytics') {
const link = document.querySelector('nav a[data-path="telemetry-analytics"]');
if (link) link.click();
} else if (cmd === 'normal') {
if (presetNormal) presetNormal.click();
} else if (cmd === 'rush') {
if (presetRush) presetRush.click();
} else if (cmd === 'export') {
triggerCsvDownload();
} else if (cmd === 'theme') {
applyTheme(!isDarkMode());
}
});
});
// 15. Windows Hotkeys & Keyboard Shortcuts
window.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Windows Ctrl+K for Command Palette
if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
const modal = document.getElementById('cmdPaletteModal');
if (modal && !modal.classList.contains('hidden')) {
window.closeCommandPalette();
} else {
window.openCommandPalette();
}
return;
}
// Windows Ctrl+S for Export CSV
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
window.location.href = API.exportCsv;
return;
}
// Escape to close modals
if (e.key === 'Escape') {
window.closeCommandPalette();
const insp = document.getElementById('inspectorCard');
if (insp) insp.classList.add('hidden');
return;
}
if (e.code === 'Space') {
e.preventDefault();
postControl({ action: 'pause' });
} else if (e.key === 'r' || e.key === 'R') {
postControl({ action: 'reset' });
} else if (e.key === 'e' || e.key === 'E') {
postControl({ action: 'spawn_emergency', start: [0, 0], goal: [GRID - 1, GRID - 1] });
logIncidentEvent('EMERGENCY', 'Node (0,0)', 'BFS Route (0,0)➔(3,3)', '18.2 ticks', 'EN ROUTE');
} else if (e.key === 'i' || e.key === 'I') {
postControl({ action: 'spawn_incident' });
logIncidentEvent('INCIDENT', 'Random Segment', 'Arterial Obstruction', '< 12 ms', 'DEPLOYED');
} else if (e.key === 'c' || e.key === 'C') {
if (modeCompare) modeCompare.click();
} else if (e.key === 'a' || e.key === 'A') {
const link = document.querySelector('nav a[data-path="telemetry-analytics"]');
if (link) link.click();
} else if (e.key === '1') {
if (presetNormal) presetNormal.click();
} else if (e.key === '2') {
if (presetRush) presetRush.click();
} else if (e.key === 't' || e.key === 'T') {
applyTheme(!isDarkMode());
}
});
}
// Bootstrapping
function init() {
if (!primaryCanvas) {
primaryCanvas = document.getElementById('modernCityCanvas');
primaryCtx = primaryCanvas ? primaryCanvas.getContext('2d') : null;
}
if (!splitAiCanvas) {
splitAiCanvas = document.getElementById('splitAiCanvas');
splitAiCtx = splitAiCanvas ? splitAiCanvas.getContext('2d') : null;
}
if (!splitBaseCanvas) {
splitBaseCanvas = document.getElementById('splitBaseCanvas');
splitBaseCtx = splitBaseCanvas ? splitBaseCanvas.getContext('2d') : null;
}
if (!tooltip) {
tooltip = document.getElementById('carTooltip');
}
initTheme();
initControls();
initCanvasInteractions();
initNavigation();
initAnalyticsCharts();
initIncidentsCorridors();
refreshCanvasSizes();
// Initialize simContainer background to match current theme
const dark = isDarkMode();
const simContainer = document.getElementById('simContainer');
if (simContainer) {
simContainer.style.backgroundColor = dark ? '#080a14' : '#edf2fa';
}
// Initialize compare split backgrounds for light mode
const compareWrapper = document.getElementById('compareWrapper');
if (compareWrapper) {
compareWrapper.style.backgroundColor = dark ? '#06070d' : '#e2e8f0';
}
// Start background sync
pollState();
pollMessages();
setInterval(pollMessages, 1000);
// Start 60 FPS Canvas Render Loop
requestAnimationFrame(animationLoop);
}
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();