209 lines
6.3 KiB
JavaScript
209 lines
6.3 KiB
JavaScript
|
|
var streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
});
|
|
|
|
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 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: icon }).addTo(map);
|
|
if (content) marker.bindPopup(content);
|
|
} else {
|
|
console.warn("Invalid coordinates:", latitude, longitude);
|
|
}
|
|
}
|
|
|
|
|
|
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 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 drawLines() {
|
|
MJR_LINES.forEach(drawLine);
|
|
}
|
|
|
|
|
|
let stopMarkers = {};
|
|
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));
|
|
}
|
|
|
|
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);
|