/* ──────────────────────────────────────────────
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 ``;
}).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 ``;
}).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 = '| Station | ';
hours.forEach(h => { html += `${h} | `; });
html += '
';
heatmap.forEach(row => {
html += `| ${row.station} | `;
hours.forEach(h => {
const cell = row.hours[h];
if (cell) {
html += `${LEVEL_SHORT[cell.level] || '?'} | `;
} else {
html += `- | `;
}
});
html += '
';
});
html += '
';
$('heatmapTable').innerHTML = html;
}
// ── Boot ─────────────────────────────────────────
document.addEventListener('DOMContentLoaded', init);