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

Upcoming at ${stopName}

`; html += ``; unique.forEach(p => { const mins = Math.round((p.time - now.getTime()) / 60000); const timeStr = new Date(p.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const timeDisplay = mins <= 30 ? `${timeStr} (${mins === 0 ? "Due" : mins + " min"})` : timeStr; const soon = mins <= 5 ? ' (soon)' : ''; const originalItem = data.find(d => d.trip?.trip_id === p.tripId || (d.route?.route_short_name === p.route && (d.trip?.trip_headsign || "").trim() === p.headsign) ); const color = originalItem?.route?.route_color && originalItem.route.route_color !== "000000" ? `#${originalItem.route.route_color}` : "#666"; html += ``; }); html += `
RouteDestinationArrival
${p.route}${soon} ${p.headsign || "—"} ${timeDisplay}
`; html += ` Updated ${now.toLocaleTimeString()} • All duplicates removed `; scheduleDiv.innerHTML = html; } catch (err) { console.error(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) { 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"}
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) return; try { const res = await fetch(`${API}/shape/${routeId}`); const shapes = await res.json(); const res2 = await fetch(`${API}/routes`); const routes = await res2.json(); const route = routes.find(r => r.route_short_name == routeId); const routeHexColor = route?.route_color ? `#${route.route_color}` : "#888"; 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("Selected route:", currentRoute); routeShapes.clearLayers(); stopMarkers.clearLayers(); if (!currentRoute) { await loadAllRoutes(); await loadStops(); } else if (currentRoute === "LRTC") { const combinedRoutes = ["750", "720", "704", "703", "701"]; for (const routeId of combinedRoutes) { await loadRouteShape(routeId); await loadStops(routeId); } } else { await loadRouteShape(currentRoute); await loadStops(currentRoute); } 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); }); } } 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 += `
StopArrival Time
${stop.stop_name}${time}
`; scheduleDiv.innerHTML = html; } catch (err) { console.error("Error fetching schedule:", err); scheduleDiv.innerHTML = "

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