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 = `

Upcoming at ${stopName}

`; html += ``; uniqueUpcoming.forEach(item => { const { stopTime, trip, route, arrivalDate } = item; const routeNum = route?.route_short_name || route?.route_id || "—"; const headsign = trip?.trip_headsign || "—"; const minutesUntil = Math.round((arrivalDate - now) / 60000); const displayTime = arrivalDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const timeDisplay = minutesUntil <= 30 ? `${displayTime} (${minutesUntil === 0 ? "Due" : minutesUntil + " min"})` : displayTime; const rowColor = route?.route_color && route.route_color !== "000000" ? `#${route.route_color}` : "#666"; html += ``; }); html += `
Route Destination Arrival
${routeNum} ${headsign} ${timeDisplay}
`; html += ` Updated ${now.toLocaleTimeString()} `; scheduleDiv.innerHTML = html; } catch (err) { console.error("Error fetching stop schedule:", err); scheduleDiv.innerHTML = "

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"}
Vehicle ID: ${id}
Route: ${route}
Speed: ${speed}
Updated: ${new Date(v.timestamp * 1000).toLocaleTimeString()} `; if (vehicleMarkersMap[id]) { vehicleMarkersMap[id].setLatLng([lat, lon]); vehicleMarkersMap[id].setPopupContent(popup); } else { const marker = L.marker([lat, lon], { icon: icon, rotationAngle: v.position.bearing || 0 }).bindPopup(popup) .addTo(vehicleMarkers); vehicleMarkersMap[id] = marker; } }); Object.keys(vehicleMarkersMap).forEach(id => { if (!seenVehicleIds.has(id)) { vehicleMarkers.removeLayer(vehicleMarkersMap[id]); delete vehicleMarkersMap[id]; } }); document.getElementById("status").textContent = `${allVehicles.length} vehicles • ${new Date().toLocaleTimeString()}`; } catch (err) { console.error("Update failed:", err); document.getElementById("status").textContent = "Error"; } } async function loadRouteShape(routeId) { if (!routeId) { routeShapes.clearLayers(); return; } try { const res = await fetch(`${API}/shape/${routeId}`); const shapes = await res.json(); routeShapes.clearLayers(); const res2 = await fetch(`${API}/routes`); const routes = await res2.json(); const route = routes.filter(r => r.route_short_name == routeId); console.log(route[0]) const routeHexColor = `#${route[0].route_color}`; shapes.forEach(shape => { const points = shape.points.map(p => [p.lat, p.lon]); L.polyline(points, { color: routeHexColor, weight: 5, opacity: 0.7 }).addTo(routeShapes); }); if (shapes.length > 0) { const bounds = L.latLngBounds(shapes[0].points.map(p => [p.lat, p.lon])); map.fitBounds(bounds, { padding: [50, 50] }); } } catch (e) { console.error("Shape load error:", e); } } document.getElementById("routeSelect").addEventListener("change", async (e) => { currentRoute = e.target.value; console.log(currentRoute == "") if (currentRoute === "") { loadAllRoutes(); loadStops(); } else { loadRouteShape(currentRoute); loadStops(currentRoute); } updateVehicles(); }); function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } document.getElementById("refresh").addEventListener("click", updateVehicles); async function loadAllRoutes() { try { const res = await fetch(`${API}/routes`); const routes = await res.json(); routeShapes.clearLayers(); for (const route of routes) { const routeId = route.route_short_name; const routeHexColor = route.route_color ? `#${route.route_color}` : "#888"; const shapeRes = await fetch(`${API}/shape/${routeId}`); const shapes = await shapeRes.json(); shapes.forEach(shape => { const points = shape.points.map(p => [p.lat, p.lon]); L.polyline(points, { color: routeHexColor, weight: 4, opacity: 0.6 }).addTo(routeShapes); }); //await delay(200); } const allPoints = []; routeShapes.eachLayer(layer => { allPoints.push(...layer.getLatLngs()); }); } catch (e) { console.error("Error loading all routes:", e); } } async function showVehicleSchedule(vehicleId) { const scheduleDiv = document.getElementById("vehicleSchedule"); scheduleDiv.innerHTML = "Loading schedule..."; try { const res = await fetch(`${API}/schedule/?vehicle=${vehicleId}`); const schedule = await res.json(); if (!schedule.length) { scheduleDiv.innerHTML = "

No schedule available.

"; return; } let html = `

Schedule for Vehicle ${vehicleId}

`; schedule.forEach(stop => { const time = new Date(stop.arrival_time * 1000).toLocaleTimeString(); html += ``; }); html += `
Stop Arrival Time
${stop.stop_name}${time}
`; scheduleDiv.innerHTML = html; } catch (err) { console.error("Error fetching schedule:", err); scheduleDiv.innerHTML = "

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"; });