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: [30, 30],
iconAnchor: [15, 15]
});
const busIcon = L.divIcon({
html: '🚌',
className: "train-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) {
stopMarkers.clearLayers();
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", () => {
stopsVisible = !stopsVisible;
if (stopsVisible) {
document.getElementById("toggleStops").textContent = "Hide Stops";
// Reload current stops (all or just the selected route)
if (currentRoute === "") {
loadStops();
} else {
loadStops(currentRoute);
}
} else {
document.getElementById("toggleStops").textContent = "Show Stops";
stopMarkers.clearLayers();
}
});
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()); // midnight today // Process and filter valid arrivals const upcoming = data .map(item => { const arrivalRaw = item.stopTime?.arrival_time?.trim(); if (!arrivalRaw || !item.trip || !item.route) return null; // skip invalid let [hours, minutes, seconds] = arrivalRaw.split(":").map(Number); let arrivalDate = new Date(today); if (hours >= 24) { hours -= 24; arrivalDate.setDate(arrivalDate.getDate() + 1); } arrivalDate.setHours(hours, minutes, seconds, 0); return { ...item, arrivalDate }; }) .filter(item => item && item.arrivalDate > now) // remove past or invalid .sort((a, b) => a.arrivalDate - b.arrivalDate); // Deduplicate by trip_id + arrival_time const seen = new Set(); const uniqueUpcoming = upcoming.filter(item => { const key = `${item.trip.trip_id}_${item.arrivalDate.getTime()}`; if (seen.has(key)) return false; seen.add(key); return true; }); if (uniqueUpcoming.length === 0) { scheduleDiv.innerHTML = `No upcoming arrivals for ${stopName}
`; return; } // Build table let html = `| Route | Destination | Arrival |
|---|---|---|
| ${routeNum} | ${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 || ["701", "703", "704", "720", "750"].includes(currentRoute)) { const trainRes = currentRoute ? await fetch(`${API}/train/${currentRoute}`) : await fetch(`${API}/train`); trains = await trainRes.json(); } if (!currentRoute) { const busRes = await fetch(`${API}/bus`); 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.
"; } } loadAllRoutes() .then(() => loadStops()) .then(() => { updateVehicles(); setInterval(updateVehicles, 10000); // Update every 10 seconds }) .catch(err => { console.error("Init failed:", err); document.getElementById("status").textContent = "Failed to initialize"; });