const map = L.map("map").setView([40.7608, -111.8910], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const trainIcon = L.divIcon({
html: '🚂',
className: "train-icon",
iconSize: [32, 32],
iconAnchor: [15, 15]
});
const busIcon = L.divIcon({
html: '🚌',
className: "bus-icon",
iconSize: [28, 28],
iconAnchor: [14, 14]
});
const vehicleMarkers = L.layerGroup().addTo(map);
const routeShapes = L.layerGroup().addTo(map);
const stopMarkers = L.layerGroup().addTo(map);
const API = "http://localhost:1001/api";
const vehicleMarkersMap = {};
let stopsVisible = true;
let currentRoute = "";
async function loadStops(routeId = null) {
if (!stopsVisible) return;
try {
let url = `${API}/stop`;
if (routeId) url = `${API}/stop/${routeId}`;
const res = await fetch(url);
const stops = await res.json();
stops.forEach(stop => {
if (!stop.stop_lat || !stop.stop_lon) return;
const wheelchair = (stop.wheelchair_boarding === "1" || stop.wheelchair_boarding === 1) ? " ♿" : "";
const marker = L.circleMarker([stop.stop_lat, stop.stop_lon], {
radius: 6,
color: "#0077ff",
fillColor: "#0077ff",
fillOpacity: 0.8
})
.bindPopup(`${stop.stop_name}${wheelchair}
ID: ${stop.stop_desc || stop.stop_id}`)
.addTo(stopMarkers);
marker.on('click', () => showStopSchedule(stop.stop_id, stop.stop_name));
});
} catch (err) {
console.error("Error loading stops:", err);
}
}
document.getElementById("toggleStops").addEventListener("click", async () => {
stopsVisible = !stopsVisible;
const btn = document.getElementById("toggleStops");
btn.textContent = stopsVisible ? "Hide Stops" : "Show Stops";
stopMarkers.clearLayers();
if (stopsVisible) {
await loadStops(currentRoute || null);
}
});
async function showStopSchedule(stopId, stopName) {
const scheduleDiv = document.getElementById("vehicleSchedule");
scheduleDiv.innerHTML = "Loading stop schedule...";
try {
const res = await fetch(`${API}/schedule/?station=${stopId}`);
const data = await res.json();
if (!data || data.length === 0) {
scheduleDiv.innerHTML = `
No upcoming arrivals for ${stopName}
`; return; } const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const parsed = data.map(item => { const raw = item.stopTime?.arrival_time?.trim(); if (!raw) return null; let [h, m, s] = raw.split(":").map(Number); const date = new Date(today); if (h >= 24) { h -= 24; date.setDate(date.getDate() + 1); } date.setHours(h, m, s || 0, 0); if (date <= now) return null; return { route: (item.route?.route_short_name || item.route?.route_id || "???").toString(), headsign: (item.trip?.trip_headsign || "").trim(), time: date.getTime(), tripId: item.trip?.trip_id || null, vehicleId: item.vehicle?.vehicle?.id || null }; }).filter(Boolean); const seen = new Set(); const unique = []; for (const p of parsed) { if (p.tripId && seen.has(`T${p.tripId}`)) continue; const key2 = `${p.route}|${p.headsign}|${p.time}`; if (seen.has(key2)) continue; const key3 = p.vehicleId ? `V${p.vehicleId}|${p.time}` : null; if (key3 && seen.has(key3)) continue; if (p.tripId) seen.add(`T${p.tripId}`); seen.add(key2); if (key3) seen.add(key3); unique.push(p); } unique.sort((a, b) => a.time - b.time); if (unique.length === 0) { scheduleDiv.innerHTML = `No upcoming arrivals for ${stopName}
`; return; } let html = `| Route | Destination | Arrival |
|---|---|---|
| ${p.route}${soon} | ${p.headsign || "—"} | ${timeDisplay} |
Error loading schedule.
"; } } function getContrastColor(hex) { if (!hex || hex.length < 6) return "#ffffff"; const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? "#000000" : "#ffffff"; } async function updateVehicles() { document.getElementById("status").textContent = "Updating..."; try { let trains = []; let buses = []; if (!currentRoute) { const busRes = await fetch(`${API}/bus`); buses = await busRes.json(); const trainRes = await fetch(`${API}/train`); trains = await trainRes.json(); } else if (currentRoute == "LRTC") { const trainRes = await fetch(`${API}/train`); trains = await trainRes.json(); } else if (["701", "703", "704", "720", "750"].includes(currentRoute)) { const trainRes = await fetch(`${API}/train/${currentRoute}`); trains = await trainRes.json(); } else { const busRes = await fetch(`${API}/bus/${currentRoute}`); buses = await busRes.json(); } const allVehicles = [...trains, ...buses]; const seenVehicleIds = new Set(); allVehicles.forEach(item => { const v = item.vehicle; if (!v?.position?.latitude) return; const id = v.vehicle.id; seenVehicleIds.add(id); const lat = v.position.latitude; const lon = v.position.longitude; const route = item.route?.route_short_name || "???"; const isTrain = trains.includes(item); const icon = isTrain ? trainIcon : busIcon; const speed = v.position.speed ? (v.position.speed * 2.237).toFixed(0) + " mph" : "Stopped"; const popup = ` ${item.route?.route_long_name || route + " Service"}No schedule available.
"; return; } let html = `| Stop | Arrival Time |
|---|---|
| ${stop.stop_name} | ${time} |
Error loading schedule.
"; } } document.getElementById("refresh").addEventListener("click", updateVehicles); (async function init() { try { await loadAllRoutes(); await loadStops(); updateVehicles(); setInterval(updateVehicles, 10000); } catch (err) { console.error("Init failed:", err); document.getElementById("status").textContent = "Failed to initialize"; } })();