/** * 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 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 = ` (${r},${c}) ${load} ${phaseText} `; } } return; } let html = ''; for (let r = 0; r < GRID; r++) { for (let c = 0; c < GRID; c++) { html += `
(${r},${c}) 0 NS Grn
`; } } 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 += `
Agent (${r},${c})
NS 0t
Queue Pressure: 0 cars
Exploration Rate: ε=0.050
`; } } 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 = `No emergency incidents recorded yet. Dispatch an ambulance or place a barricade above.`; 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 ` ${ev.tick} ${ev.category} ${ev.origin} ${ev.target} ${ev.latency} ${ev.status} `; }).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 ? '➔' : ''; return `I(${pt[0]},${pt[1]})${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 = `
No active road barricades currently deployed. Grid 100% Clear
`; } else { list.innerHTML = blocked.map((b) => `
block Node (${b.r},${b.c}) [${b.d}] — Rerouting Traffic
`).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 = `
No active messages for category [${currentFeedFilter}]
`; 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 `
T+${m.tick} ${tag} ${m.text}
${m.sender}
`; }).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 = ` ${foundCar.amb ? 'AMBULANCE (PRIORITY)' : 'VEHICLE #' + foundCar.id}
Approach: ${foundCar.d} • Wait: ${foundCar.w} ticks
Destination: ${foundCar.dest || 'Exit'} `; } 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(); } })();