// ------------------------------ // MAP SETUP // ------------------------------ 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/"; // ------------------------------ // ROUTE STYLES // ------------------------------ 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: "🔷" } }; // ------------------------------ // ICON BUILDER // ------------------------------ function buildIcon(emoji, bearing) { const rotate = bearing ? `transform: rotate(${bearing}deg); transform-origin: center;` : ''; const svg = ` ${emoji} `; return L.divIcon({ html: svg, className: "emoji-icon", iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -16] }); } // ------------------------------ // MARKER HELPER // ------------------------------ 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); } // ------------------------------ // ROUTE LINES // ------------------------------ 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); } // ------------------------------ // STOPS // ------------------------------ 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}
${s.stop_desc}`, stopIcon ).addTo(map); stopMarkers[route].push(marker); }); }) .catch(err => console.error("Error fetching stops:", err)); } // ------------------------------ // VEHICLES // ------------------------------ 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; // Apply filter 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)); // Remove vehicles no longer active 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}
Vehicle ${vehicleId}
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)); } // ------------------------------ // USER LOCATION & TRAIN DETECTION // ------------------------------ 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); // Detect if user is on a train 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."); } // ------------------------------ // INITIAL LOAD // ------------------------------ drawLines(); MJR_LINES.forEach(r => getStopsByRoute(r)); refreshVehicles(); setInterval(refreshVehicles, 1000);