Files
Project/public/app.js
2025-12-01 23:00:34 -07:00

243 lines
7.0 KiB
JavaScript

var streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
});
var satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/' +
'World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri'
});
var map = L.map('map', {
center: [40.7608, -111.8910],
zoom: 13,
layers: [streets]
});
var baseMaps = { "Streets": streets, "Satellite": satellite };
L.control.layers(baseMaps).addTo(map);
const MJR_LINES = ["701", "703", "704", "720", "750"];
const LRT_LINES = ["701", "703", "704", "720"];
const FRONTRUNNER = ["750"];
const API_URL = "http://localhost:7653/api/v0/";
const ROUTE_STYLES = {
"701": { color: "#0074D9", train: "🔵", stop: "🔹" },
"704": { color: "#2ECC40", train: "🟢", stop: "🟩" },
"703": { color: "#FF4136", train: "🔴", stop: "🔺" },
"750": { color: "#B10DC9", train: "🟣", stop: "🔮" },
"720": { color: "#000000", train: "⚪", stop: "🔷" }
};
function buildIcon(emoji, bearing) {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<text x="50%" y="50%" text-anchor="middle"
dominant-baseline="central" font-size="26"
font-family="Apple Color Emoji, Segoe UI Emoji, NotoColorEmoji, sans-serif"
>
${emoji}
</text>
</svg>`;
return L.divIcon({
html: svg,
className: "emoji-icon",
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16]
});
}
function addMarker(lat, lon, content, icon) {
if (!isNaN(lat) && !isNaN(lon)) {
const marker = L.marker([lat, lon], { icon });
if (content) marker.bindPopup(content);
return marker;
}
console.warn("Invalid coordinates:", lat, lon);
}
function drawPolyLine(polylinePoints, color) {
L.polyline(polylinePoints, { color, weight: 4, opacity: 0.8 }).addTo(map);
}
function drawLine(route) {
fetch(API_URL + "routepaths/" + route)
.then(res => res.json())
.then(data => {
const points = data.data;
if (!points || points.length === 0) return;
const color = ROUTE_STYLES[route].color;
let polylinePoints = [];
let currentShape = points[0].shape_id;
points.forEach(p => {
if (p.shape_id === currentShape) {
polylinePoints.push([p.lat, p.lng]);
} else {
drawPolyLine(polylinePoints, color);
polylinePoints = [[p.lat, p.lng]];
currentShape = p.shape_id;
}
});
if (polylinePoints.length > 0) drawPolyLine(polylinePoints, color);
})
.catch(err => console.error("Error drawing line:", err));
}
function drawLines() {
MJR_LINES.forEach(drawLine);
}
let stopMarkers = {};
function getStopsByRoute(route) {
if (stopMarkers[route]) return;
stopMarkers[route] = [];
const stopIcon = buildIcon(ROUTE_STYLES[route].stop);
fetch(API_URL + "stops/" + route)
.then(res => res.json())
.then(data => {
data.data.forEach(s => {
const lat = parseFloat(s.location.latitude);
const lon = parseFloat(s.location.longitude);
const marker = addMarker(
lat, lon,
`${s.stop_name}<br>${s.stop_desc}`,
stopIcon
).addTo(map);
stopMarkers[route].push(marker);
});
})
.catch(err => console.error("Error fetching stops:", err));
}
let trainMarkers = {};
let filterType = "all";
const filterSelect = document.getElementById("filterSelect");
if (filterSelect) {
filterSelect.addEventListener("change", (e) => {
filterType = e.target.value;
});
}
function refreshVehicles() {
fetch(API_URL + "vehicles")
.then(res => res.json())
.then(data => {
let vehicles = data.data;
if (filterType === "lrt") {
vehicles = vehicles.filter(v => LRT_LINES.includes(v.routeNum) || FRONTRUNNER.includes(v.routeNum));
}
vehicles = vehicles.filter(v => MJR_LINES.includes(v.routeNum));
Object.keys(trainMarkers).forEach(id => {
if (!vehicles.find(v => v.vehicleId == id)) {
map.removeLayer(trainMarkers[id]);
delete trainMarkers[id];
}
});
vehicles.forEach(v => {
const { vehicleId, routeNum, routeName, speed, bearing } = v;
const lat = v.location.latitude;
const lon = v.location.longitude;
const icon = buildIcon(ROUTE_STYLES[routeNum].train, bearing);
if (!trainMarkers[vehicleId]) {
const marker = L.marker([lat, lon], { icon })
.bindPopup(`${routeName}<br>Vehicle ${vehicleId}<br>Speed: ${(speed*2.23694).toFixed(1)} mph`)
.addTo(map);
marker.vehicleData = v;
trainMarkers[vehicleId] = marker;
} else {
if (trainMarkers[vehicleId].slideTo) {
trainMarkers[vehicleId].slideTo([lat, lon], { duration: 1000 });
} else {
trainMarkers[vehicleId].setLatLng([lat, lon]);
}
trainMarkers[vehicleId].setIcon(icon);
trainMarkers[vehicleId].vehicleData = v;
}
});
})
.catch(err => console.error("Error refreshing vehicles:", err));
}
let userMarker, accuracyCircle;
const ON_BOARD_RADIUS = 50; // meters
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
userMarker = L.circleMarker([lat, lon], {
radius: 8,
fillColor: "#007AFF",
color: "#fff",
weight: 2,
opacity: 1,
fillOpacity: 0.9
}).addTo(map);
userMarker.bindPopup("You are here").openPopup();
accuracyCircle = L.circle([lat, lon], {
radius: position.coords.accuracy,
color: "#007AFF",
fillColor: "#007AFF",
fillOpacity: 0.2
}).addTo(map);
map.setView([lat, lon], 13);
},
(err) => console.warn("Geolocation error:", err.message),
{ enableHighAccuracy: true, timeout: 500, maximumAge: 0 }
);
navigator.geolocation.watchPosition(
(position) => {
const userLat = position.coords.latitude;
const userLon = position.coords.longitude;
if (userMarker) userMarker.setLatLng([userLat, userLon]);
if (accuracyCircle) accuracyCircle.setLatLng([userLat, userLon]).setRadius(position.coords.accuracy);
const vehicles = Object.values(trainMarkers).map(m => m.vehicleData);
let closest = null, minDistance = Infinity;
vehicles.forEach(v => {
const distance = map.distance([userLat, userLon], [v.location.latitude, v.location.longitude]);
if (distance < minDistance) {
minDistance = distance;
closest = v;
}
});
if (closest && minDistance <= ON_BOARD_RADIUS) {
console.log(`User is on vehicle ${closest.vehicleId} (${closest.routeNum})`);
}
},
(err) => console.warn("Geolocation watch error:", err.message),
{ enableHighAccuracy: true, maximumAge: 0 }
);
} else {
console.warn("Geolocation not supported by this browser.");
}
drawLines();
MJR_LINES.forEach(r => getStopsByRoute(r));
refreshVehicles();
setInterval(refreshVehicles, 1000);