/* ────────────────────────────────────────────── Mumbai Local Train — Crowd Predictor JS ────────────────────────────────────────────── */ const API = ''; // ── State ────────────────────────────────────── const state = { line: 'Western', station: '', destination: '', direction: 'Southbound', hour: 8, day: 'Monday', // forecast fcLine: 'Western', fcStation: '', fcDest: '', fcDir: 'Southbound', fcDay: 'Monday', // heatmap hmLine: 'Western', hmDir: 'Southbound', hmDay: 'Monday', }; const DAYS = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']; const HOUR_LABELS = { 5:'Early Morning',6:'Early Morning',7:'Morning Rush',8:'Peak Rush',9:'Morning Rush', 10:'Mid Morning',11:'Late Morning',12:'Noon',13:'Afternoon',14:'Afternoon', 15:'Mid Afternoon',16:'Late Afternoon',17:'Evening Rush',18:'Peak Evening', 19:'Evening Rush',20:'Late Evening',21:'Night',22:'Late Night' }; const COLOR_MAP = { Low: { color: '#10b981', bg: 'rgba(16,185,129,.15)', text: '#10b981' }, Medium: { color: '#f59e0b', bg: 'rgba(245,158,11,.15)', text: '#f59e0b' }, Extreme: { color: '#ef4444', bg: 'rgba(239,68,68,.15)', text: '#ef4444' }, }; // ── DOM Helpers ───────────────────────────────── const $ = id => document.getElementById(id); const qs = sel => document.querySelector(sel); const qsa = sel => [...document.querySelectorAll(sel)]; function showToast(msg, dur = 3000) { let t = qs('.toast'); if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.append(t); } t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), dur); } function showLoading(msg = 'Loading…') { let ov = qs('.loading-overlay'); if (!ov) { ov = document.createElement('div'); ov.className = 'loading-overlay'; ov.innerHTML = `
${msg}
`; document.body.append(ov); } ov.querySelector('.loading-text').textContent = msg; } function hideLoading() { const ov = qs('.loading-overlay'); if (ov) ov.remove(); } async function apiFetch(url, options = {}) { const res = await fetch(API + url, options); if (!res.ok) throw new Error(await res.text()); return res.json(); } // ── Init ──────────────────────────────────────── async function init() { showLoading('🚆 Loading AI model…'); try { // Load stats const stats = await apiFetch('/api/stats'); if (stats.model_accuracy) { $('statAccuracy').textContent = stats.model_accuracy + '%'; $('statRecords').textContent = (stats.total_records / 1000).toFixed(0) + 'K+'; } // Populate days everywhere DAYS.forEach(d => { ['daySelect','fcDay','hmDay'].forEach(id => { const opt = document.createElement('option'); opt.value = opt.textContent = d; $(id)?.append(opt); }); }); // Load stations for default line await loadStations('Western', 'stationSelect', 'destSelect'); await loadStations('Western', 'fcStation', 'fcDest'); setupNavigation(); setupLinePills(); setupSlider(); setupPillGroups(); setupFormEvents(); $('predictBtn').addEventListener('click', handlePredict); $('fcBtn').addEventListener('click', handleForecast); $('hmBtn').addEventListener('click', handleHeatmap); hideLoading(); } catch (e) { hideLoading(); showToast('⚠️ Error connecting to server: ' + e.message, 5000); console.error(e); } } // ── Navigation ────────────────────────────────── function setupNavigation() { qsa('.nav-link').forEach(link => { link.addEventListener('click', e => { e.preventDefault(); const tab = link.dataset.tab; qsa('.nav-link').forEach(l => l.classList.remove('active')); link.classList.add('active'); qsa('.tab-section').forEach(s => { s.classList.remove('active'); s.classList.add('hidden'); }); const section = $('tab-' + tab); if (section) { section.classList.remove('hidden'); section.classList.add('active'); } }); }); } // ── Stations ──────────────────────────────────── async function loadStations(line, fromId, toId) { const data = await apiFetch('/api/stations/' + line); const stations = data.stations; [fromId, toId].forEach(id => { const sel = $(id); if (!sel) return; sel.innerHTML = ''; stations.forEach(s => { const opt = document.createElement('option'); opt.value = opt.textContent = s; sel.append(opt); }); }); // Auto-select defaults if ($(fromId) && stations.length > 0) $(fromId).value = stations[0]; if ($(toId) && stations.length > 1) $(toId).value = stations[stations.length - 1]; } // ── Line Pills ────────────────────────────────── function setupLinePills() { // Main predict qsa('#linePills .pill').forEach(pill => { pill.addEventListener('click', async () => { qsa('#linePills .pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); state.line = pill.dataset.val; await loadStations(state.line, 'stationSelect', 'destSelect'); }); }); // Forecast qsa('#fcLinePills .pill').forEach(pill => { pill.addEventListener('click', async () => { qsa('#fcLinePills .pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); state.fcLine = pill.dataset.val; await loadStations(state.fcLine, 'fcStation', 'fcDest'); }); }); // Heatmap qsa('#hmLinePills .pill').forEach(pill => { pill.addEventListener('click', () => { qsa('#hmLinePills .pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); state.hmLine = pill.dataset.val; }); }); } // ── Pill Groups ───────────────────────────────── function setupPillGroups() { function bindPills(groupId, stateKey) { qsa(`#${groupId} .pill`).forEach(pill => { pill.addEventListener('click', () => { qsa(`#${groupId} .pill`).forEach(p => p.classList.remove('active')); pill.classList.add('active'); state[stateKey] = pill.dataset.val; }); }); } bindPills('dirPills', 'direction'); bindPills('fcDirPills', 'fcDir'); bindPills('hmDirPills', 'hmDir'); } // ── Slider ────────────────────────────────────── function setupSlider() { const slider = $('hourSlider'); function updateHourDisplay(val) { const h = parseInt(val); state.hour = h; const label = HOUR_LABELS[h] || ''; $('hourDisplay').textContent = `${String(h).padStart(2,'0')}:00 (${label})`; } slider.addEventListener('input', e => updateHourDisplay(e.target.value)); updateHourDisplay(slider.value); } // ── Form Events ───────────────────────────────── function setupFormEvents() { $('stationSelect').addEventListener('change', e => state.station = e.target.value); $('destSelect').addEventListener('change', e => state.destination = e.target.value); $('daySelect').addEventListener('change', e => state.day = e.target.value); $('fcStation').addEventListener('change', e => state.fcStation = e.target.value); $('fcDest').addEventListener('change', e => state.fcDest = e.target.value); $('fcDay').addEventListener('change', e => state.fcDay = e.target.value); $('hmDay').addEventListener('change', e => state.hmDay = e.target.value); } // ── Predict ───────────────────────────────────── async function handlePredict() { const station = $('stationSelect').value; const dest = $('destSelect').value; const day = $('daySelect').value; if (!station || !dest) { showToast('⚠️ Please select both stations'); return; } if (station === dest) { showToast('⚠️ Source and destination cannot be the same'); return; } const btn = $('predictBtn'); btn.classList.add('loading'); btn.querySelector('.btn-text').textContent = 'Predicting…'; try { const payload = { line: state.line, station, destination: dest, direction: state.direction, hour: state.hour, day_of_week: day }; const result = await apiFetch('/api/predict', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); renderResult(result); } catch (e) { showToast('❌ Prediction failed: ' + e.message); } finally { btn.classList.remove('loading'); btn.querySelector('.btn-text').textContent = 'Predict Crowd Density'; } } function renderResult(r) { $('resultPlaceholder').classList.add('hidden'); $('resultContent').classList.remove('hidden'); const cm = COLOR_MAP[r.crowd_level] || COLOR_MAP.Low; // Route $('routeDisplay').innerHTML = `${r.input.station} → ${r.input.destination}
${r.input.line} Line · ${r.input.direction} · ${String(r.input.hour).padStart(2,'0')}:00 · ${r.input.day}`; // Badge const badge = $('riskBadge'); badge.textContent = r.risk_emoji + ' ' + r.risk_badge; badge.style.background = cm.bg; badge.style.color = cm.color; badge.style.border = `1px solid ${cm.color}40`; // Meter (score 0-100) const pct = r.risk_score; $('meterFill').style.width = (100 - pct) + '%'; $('meterPointer').style.left = pct + '%'; $('meterScore').textContent = pct + ' / 100'; $('meterScore').style.color = cm.color; // Prob bars const probHTML = Object.entries(r.probabilities).map(([level, pct]) => { const c = COLOR_MAP[level]?.color || '#94a3b8'; return `
${level}
${pct}%
`; }).join(''); $('probBars').innerHTML = probHTML; // Reasons const reasonsHTML = r.reasons.map(re => `
${re}
`).join(''); $('reasonsBox').innerHTML = `
📋 WHY THIS PREDICTION
${reasonsHTML}`; // Tip $('tipBox').innerHTML = `💡${r.travel_tip}`; $('tipBox').style.color = cm.color; $('tipBox').style.borderColor = cm.color + '30'; $('tipBox').style.background = cm.bg; } // ── Forecast ───────────────────────────────────── async function handleForecast() { const station = $('fcStation').value; const dest = $('fcDest').value; const day = $('fcDay').value; if (!station || !dest) { showToast('⚠️ Please select stations'); return; } $('fcBtn').textContent = 'Loading…'; try { const result = await apiFetch('/api/hourly-forecast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ line: state.fcLine, station, destination: dest, direction: state.fcDir, day_of_week: day }) }); renderForecast(result); } catch (e) { showToast('❌ Forecast failed: ' + e.message); } finally { $('fcBtn').textContent = 'View Forecast →'; } } function renderForecast(data) { const { hourly, best_hours, peak_hour } = data; const fr = $('forecastResults'); fr.classList.remove('hidden'); // Best times const bestHTML = `
✅ Best Times to Travel
${best_hours.map((b, i) => { const cm = COLOR_MAP[b.crowd_level] || COLOR_MAP.Low; return `
${b.label}
${b.emoji} ${b.crowd_level}
Option ${i+1}
`; }).join('')}
${peak_hour.label}
🔴 Peak Hour
Avoid
`; $('bestTimes').innerHTML = bestHTML; // Bar chart const maxScore = 100; const chartHTML = hourly.map(h => { const heightPct = (h.score / maxScore) * 140; return `
${h.hour}
`; }).join(''); $('chartWrap').innerHTML = chartHTML; // Hourly grid const gridHTML = hourly.map(h => { const cm = COLOR_MAP[h.crowd_level] || COLOR_MAP.Low; return `
${h.label}
${h.emoji} ${h.crowd_level}
Risk: ${h.score}
`; }).join(''); $('hourlyGrid').innerHTML = gridHTML; } // ── Heatmap ────────────────────────────────────── async function handleHeatmap() { const day = $('hmDay').value; $('hmBtn').textContent = 'Loading…'; try { const result = await apiFetch(`/api/heatmap/${state.hmLine}?direction=${state.hmDir}&day=${day}`); renderHeatmap(result); } catch (e) { showToast('❌ Heatmap failed: ' + e.message); } finally { $('hmBtn').textContent = 'Load Heatmap →'; } } function renderHeatmap(data) { const { heatmap, hours, stations } = data; const wrap = $('heatmapWrap'); wrap.classList.remove('hidden'); const LEVEL_SHORT = { Low: 'L', Medium: 'M', Extreme: 'X' }; let html = ''; hours.forEach(h => { html += ``; }); html += ''; heatmap.forEach(row => { html += ``; hours.forEach(h => { const cell = row.hours[h]; if (cell) { html += ``; } else { html += ``; } }); html += ''; }); html += '
Station${h}
${row.station}${LEVEL_SHORT[cell.level] || '?'}-
'; $('heatmapTable').innerHTML = html; } // ── Boot ───────────────────────────────────────── document.addEventListener('DOMContentLoaded', init);