final product

This commit is contained in:
2025-12-04 15:37:56 -07:00
parent d9bddd8715
commit 6b746d466f
12 changed files with 399 additions and 348 deletions

View File

@@ -1,49 +0,0 @@
const map = L.map("map").setView([40.7608, -111.8910], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
const API_URL = "http://localhost:7653/api/v0/";
const trainEmojiIcon = L.divIcon({ html: "🔵", className: "", iconSize: [64,64], iconAnchor: [16,32], popupAnchor: [0,-32] });
const stopsEmojiIcon = L.divIcon({ html: "🫃", className: "", iconSize: [64,64], iconAnchor: [16,32], popupAnchor: [0,-32] });
function addMarker(lat, lon, content, icon) {
if (!isNaN(lat) && !isNaN(lon)) {
const marker = L.marker([lat, lon], { icon: icon }).addTo(map);
if (content) marker.bindPopup(content);
} else {
console.warn("Invalid coordinates:", latitude, longitude);
}
}
function getTrainsByRoute(route) {
fetch(API_URL + 'vehicles')
.then(res => res.json())
.then(data => {
const trains = data.data;
const filtered = route ? trains.filter(t => t.routeId == route) : trains;
filtered.forEach(t => {
addMarker(t.location.latitude, t.location.longitude, t.routeName + ": Vehicle " + t.vehicleId, trainEmojiIcon);
});
})
.catch(err => console.error("Error fetching trains:", err));
}
function getStopsByRoute(route) {
fetch(API_URL + 'stops/' + route)
.then(res => res.json())
.then(data => {
const stops = data.data;
stops.forEach(s => {
const lat = parseFloat(s.stop_lat);
const lon = parseFloat(s.stop_lon);
addMarker(lat,lon, s.stop_name + " - " + s.stop_desc, stopsEmojiIcon);
});
})
.catch(err => console.error("Error fetching stops:", err));
}
getStopsByRoute("701");
getTrainsByRoute();

View File

@@ -19,6 +19,7 @@
<div class="controls">
<select id="routeSelect">
<option value="">All Trains & Buses</option>
<option value="LRTC">LRT + Communter</option>
<option value="701">Blue Line (701)</option>
<option value="703">Red Line (703)</option>
<option value="704">Green Line (704)</option>

View File

@@ -4,18 +4,16 @@ 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],
iconSize: [32, 32],
iconAnchor: [15, 15]
});
const busIcon = L.divIcon({
html: '🚌',
className: "train-icon",
className: "bus-icon",
iconSize: [28, 28],
iconAnchor: [14, 14]
});
@@ -24,7 +22,6 @@ 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 = {};
@@ -32,7 +29,6 @@ let stopsVisible = true;
let currentRoute = "";
async function loadStops(routeId = null) {
stopMarkers.clearLayers();
if (!stopsVisible) return;
try {
@@ -63,19 +59,14 @@ async function loadStops(routeId = null) {
}
}
document.getElementById("toggleStops").addEventListener("click", () => {
document.getElementById("toggleStops").addEventListener("click", async () => {
stopsVisible = !stopsVisible;
const btn = document.getElementById("toggleStops");
btn.textContent = stopsVisible ? "Hide Stops" : "Show Stops";
stopMarkers.clearLayers();
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();
await loadStops(currentRoute || null);
}
});
@@ -93,92 +84,99 @@ async function showStopSchedule(stopId, stopName) {
}
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // midnight today
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// 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
const parsed = data.map(item => {
const raw = item.stopTime?.arrival_time?.trim();
if (!raw) return null;
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);
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);
return { ...item, arrivalDate };
})
.filter(item => item && item.arrivalDate > now) // remove past or invalid
.sort((a, b) => a.arrivalDate - b.arrivalDate);
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);
// 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;
});
const unique = [];
if (uniqueUpcoming.length === 0) {
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 = `<p>No upcoming arrivals for <strong>${stopName}</strong></p>`;
return;
}
// Build table
let html = `<h3>Upcoming at ${stopName}</h3>`;
html += `<table style="width:100%; border-collapse:collapse; font-size:0.95em;">
<thead style="background:#333; color:white;">
<tr>
<th style="padding:8px;">Route</th>
<th style="padding:8px;">Destination</th>
<th style="padding:8px;">Arrival</th>
</tr>
</thead>
<tbody>`;
<tr><th style="padding:8px;">Route</th><th style="padding:8px;">Destination</th><th style="padding:8px;">Arrival</th></tr>
</thead><tbody>`;
uniqueUpcoming.forEach(item => {
const { stopTime, trip, route, arrivalDate } = item;
const routeNum = route?.route_short_name || route?.route_id || "—";
const headsign = trip?.trip_headsign || "—";
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 minutesUntil = Math.round((arrivalDate - now) / 60000);
const displayTime = arrivalDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const timeDisplay = mins <= 30
? `<strong style="color:#d00;">${timeStr} (${mins === 0 ? "Due" : mins + " min"})</strong>`
: timeStr;
const timeDisplay = minutesUntil <= 30
? `<strong style="color:#d00;">${displayTime} (${minutesUntil === 0 ? "Due" : minutesUntil + " min"})</strong>`
: displayTime;
const soon = mins <= 5 ? ' <small style="opacity:0.8;">(soon)</small>' : '';
const rowColor = route?.route_color && route.route_color !== "000000"
? `#${route.route_color}`
: "#666";
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 += `<tr style="border-bottom:1px solid #ddd;">
<td style="padding:6px; text-align:center; background:${rowColor}; color:${getContrastColor(rowColor)}; font-weight:bold;">
${routeNum}
<td style="padding:6px; text-align:center; background:${color}; color:${getContrastColor(color)}; font-weight:bold;">
${p.route}${soon}
</td>
<td style="padding:6px;">${headsign}</td>
<td style="padding:6px;">${p.headsign || "—"}</td>
<td style="padding:6px; text-align:center;">${timeDisplay}</td>
</tr>`;
});
html += `</tbody></table>`;
html += `<small style="color:#666; display:block; margin-top:8px;">
Updated ${now.toLocaleTimeString()}
Updated ${now.toLocaleTimeString()} • All duplicates removed
</small>`;
scheduleDiv.innerHTML = html;
} catch (err) {
console.error("Error fetching stop schedule:", err);
console.error(err);
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
}
}
function getContrastColor(hex) {
if (!hex || hex.length < 6) return "#ffffff";
const r = parseInt(hex.substr(0, 2), 16);
@@ -195,16 +193,20 @@ async function updateVehicles() {
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 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];
@@ -244,65 +246,70 @@ async function updateVehicles() {
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;
}
if (!routeId) 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}`;
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); }
} 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);
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();
});
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`);
@@ -319,19 +326,9 @@ async function loadAllRoutes() {
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);
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);
}
@@ -350,17 +347,9 @@ async function showVehicleSchedule(vehicleId) {
return;
}
let html = `
<h3>Schedule for Vehicle ${vehicleId}</h3>
<table border="1" cellpadding="5" cellspacing="0">
<thead>
<tr>
<th>Stop</th>
<th>Arrival Time</th>
</tr>
</thead>
<tbody>
`;
let html = `<h3>Schedule for Vehicle ${vehicleId}</h3>
<table border="1" cellpadding="5" cellspacing="0">
<thead><tr><th>Stop</th><th>Arrival Time</th></tr></thead><tbody>`;
schedule.forEach(stop => {
const time = new Date(stop.arrival_time * 1000).toLocaleTimeString();
@@ -369,20 +358,22 @@ async function showVehicleSchedule(vehicleId) {
html += `</tbody></table>`;
scheduleDiv.innerHTML = html;
} catch (err) {
console.error("Error fetching schedule:", err);
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
}
}
loadAllRoutes()
.then(() => loadStops())
.then(() => {
document.getElementById("refresh").addEventListener("click", updateVehicles);
(async function init() {
try {
await loadAllRoutes();
await loadStops();
updateVehicles();
setInterval(updateVehicles, 10000); // Update every 10 seconds
})
.catch(err => {
setInterval(updateVehicles, 10000);
} catch (err) {
console.error("Init failed:", err);
document.getElementById("status").textContent = "Failed to initialize";
});
}
})();