Compare commits
5 Commits
backend
...
e96807612b
| Author | SHA1 | Date | |
|---|---|---|---|
| e96807612b | |||
| 6b746d466f | |||
| d9bddd8715 | |||
| a709c80f38 | |||
| 456d486d39 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/traxer_api/.codeedit
|
/traxer_api/.codeedit
|
||||||
|
.DS_Store
|
||||||
|
|||||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build || echo "No build step needed"
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app ./
|
||||||
|
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S uta-sync -u 1001
|
||||||
|
|
||||||
|
USER uta-sync
|
||||||
|
|
||||||
|
EXPOSE 1001
|
||||||
|
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
4059
alerts.json
4059
alerts.json
File diff suppressed because it is too large
Load Diff
22
app.js
22
app.js
@@ -1,22 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import cors from "cors";
|
|
||||||
import vehiclesRouter from "./src/routes/vehicles.js";
|
|
||||||
import routesRouter from "./src/routes/routes.js";
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
const base = "/api/v0";
|
|
||||||
|
|
||||||
app.get(`${base}/`, (req, res) => {
|
|
||||||
res.json({status: "ok", version: "v0", ts: new Date().toISOString()});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(`${base}/vehicles`, vehiclesRouter);
|
|
||||||
|
|
||||||
app.use(base, routesRouter);
|
|
||||||
|
|
||||||
app.get("/", (req, res) => res.redirect(`${base}/`));
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
19
docker-compose.yaml
Normal file
19
docker-compose.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: my-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: my-express-app
|
||||||
|
ports:
|
||||||
|
- "1001:1001"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
1567
package-lock.json
generated
1567
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,22 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "project",
|
"name": "traxer2",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "app.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
"start": "node start.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.nathanspackman.com/PRO150-Group/Project"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0"
|
"csv-parse": "^6.1.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"gtfs-realtime-bindings": "^1.1.1",
|
||||||
|
"mysql2": "^3.15.3",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"parse": "^7.1.2",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"redis": "^5.10.0",
|
||||||
|
"unzipper": "^0.12.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Traxer</title>
|
<title>Traxer</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
<link
|
<link rel="stylesheet" href="style.css" />
|
||||||
rel="stylesheet"
|
|
||||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
||||||
crossorigin=""
|
|
||||||
/>
|
|
||||||
<script
|
|
||||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
||||||
crossorigin=""
|
|
||||||
></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="map" class="w-full h-screen"></div>
|
|
||||||
|
<div id="map"></div>
|
||||||
|
<div
|
||||||
|
style="position:absolute; top:10px; right:10px; z-index:1000; background:white; padding:8px; border-radius:4px; box-shadow:0 0 10px rgba(0,0,0,0.3);">
|
||||||
|
<button id="toggleStops">Hide Stops</button>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<option value="720">S-Line (720)</option>
|
||||||
|
<option value="750">FrontRunner (750)</option>
|
||||||
|
</select>
|
||||||
|
<button id="refresh">Refresh Now</button>
|
||||||
|
<span class="status" id="status">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="vehicleSchedule" style="margin-top: 20px;">
|
||||||
|
</div>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="index.js"> </script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
<script src="app.js"></script>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
379
public/index.js
Normal file
379
public/index.js
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
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(`<b>${stop.stop_name}${wheelchair}</b><br>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 = `<p>No upcoming arrivals for <strong>${stopName}</strong></p>`;
|
||||||
|
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 = `<p>No upcoming arrivals for <strong>${stopName}</strong></p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
|
||||||
|
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
|
||||||
|
? `<strong style="color:#d00;">${timeStr} (${mins === 0 ? "Due" : mins + " min"})</strong>`
|
||||||
|
: timeStr;
|
||||||
|
|
||||||
|
const soon = mins <= 5 ? ' <small style="opacity:0.8;">(soon)</small>' : '';
|
||||||
|
|
||||||
|
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:${color}; color:${getContrastColor(color)}; font-weight:bold;">
|
||||||
|
${p.route}${soon}
|
||||||
|
</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()} • All duplicates removed
|
||||||
|
</small>`;
|
||||||
|
|
||||||
|
scheduleDiv.innerHTML = html;
|
||||||
|
|
||||||
|
} catch (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);
|
||||||
|
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 = `
|
||||||
|
<b>${item.route?.route_long_name || route + " Service"}</b><br/>
|
||||||
|
Vehicle ID: ${id}<br/>
|
||||||
|
Route: ${route}<br/>
|
||||||
|
Speed: ${speed}<br/>
|
||||||
|
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 = "<p>No schedule available.</p>";
|
||||||
|
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>`;
|
||||||
|
|
||||||
|
schedule.forEach(stop => {
|
||||||
|
const time = new Date(stop.arrival_time * 1000).toLocaleTimeString();
|
||||||
|
html += `<tr><td>${stop.stop_name}</td><td>${time}</td></tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</tbody></table>`;
|
||||||
|
scheduleDiv.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching schedule:", err);
|
||||||
|
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
})();
|
||||||
63
public/style.css
Normal file
63
public/style.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
width: 100vw;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #0052a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
118
server.js
Normal file
118
server.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const path = require('path');
|
||||||
|
const app = express()
|
||||||
|
const gtfsRealtime = require("./tools/gtfsRedis")
|
||||||
|
const redisDAL = require("./tools/redisDAL");
|
||||||
|
|
||||||
|
app.use(express.static('public'))
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*'); // or 'http://127.0.0.1:1001' for security
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
const port = 1001
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/', (req, res) => {
|
||||||
|
res.json({ version: "0.0.1", status: "healthy" })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/train/', async (req, res) => {
|
||||||
|
res.json(await gtfsRealtime.getTrains())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/train/:routeId', async (req, res) => {
|
||||||
|
const routeId = req.params.routeId;
|
||||||
|
res.json(await gtfsRealtime.getTrainsByRoute(routeId))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/bus/', async (req, res) => {
|
||||||
|
res.json(await gtfsRealtime.getBuses())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/bus/:routeId', async (req, res) => {
|
||||||
|
const routeId = req.params.routeId;
|
||||||
|
res.json(await gtfsRealtime.getBusesByRoute(routeId))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/alert', async (req, res) => {
|
||||||
|
res.json(await gtfsRealtime.getAlerts())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/alert/:routeId', async (req, res) => {
|
||||||
|
const routeId = req.params.routeId;
|
||||||
|
res.json(await gtfsRealtime.getAlertsByRoute(routeId))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/schedule', async (req, res) => {
|
||||||
|
const { route, station } = req.query;
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
if (route) {
|
||||||
|
result = await gtfsRealtime.getScheduleByRoute(route);
|
||||||
|
} else if (station) {
|
||||||
|
result = await gtfsRealtime.getScheduleByStationId(station);
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: "Please provide route, or station query parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result || (Array.isArray(result) && result.length === 0)) {
|
||||||
|
return res.status(404).json({ error: "No schedule found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/routes', async (req, res) => {
|
||||||
|
res.json(await gtfsRealtime.getRoutes())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/shape/:routeId', async (req, res) => {
|
||||||
|
const routeId = req.params.routeId;
|
||||||
|
res.json(await gtfsRealtime.getShapeByRoute(routeId))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/stop', async (req, res) => {
|
||||||
|
res.json(await gtfsRealtime.getStops())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/stop/:routeId', async (req, res) => {
|
||||||
|
const routeId = req.params.routeId;
|
||||||
|
res.json(await gtfsRealtime.getStopsByRoute(routeId))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
app.listen(port, async() => {
|
||||||
|
await redisDAL.connect()
|
||||||
|
await test();
|
||||||
|
console.log(`Example app listening on port ${port}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async function test(){
|
||||||
|
console.log("stops")
|
||||||
|
await gtfsRealtime.getStops()
|
||||||
|
await gtfsRealtime.getStopsByRoute()
|
||||||
|
await gtfsRealtime.getRoutes()
|
||||||
|
console.log("trains")
|
||||||
|
await gtfsRealtime.getTrains()
|
||||||
|
await gtfsRealtime.getTrainsByRoute()
|
||||||
|
console.log("buses")
|
||||||
|
await gtfsRealtime.getBuses()
|
||||||
|
await gtfsRealtime.getBusesByRoute()
|
||||||
|
await gtfsRealtime.getShapeByRoute()
|
||||||
|
await console.log("shapes")
|
||||||
|
await gtfsRealtime.getScheduleByRoute()
|
||||||
|
await gtfsRealtime.getScheduleByStationId()
|
||||||
|
await gtfsRealtime.getAlerts()
|
||||||
|
await gtfsRealtime.getAlertsByRoute()
|
||||||
|
console.log("done")
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"route_id": "701",
|
|
||||||
"start_location": "Draper City Center",
|
|
||||||
"end_location": "Salt Lake Central"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"route_id": "703",
|
|
||||||
"start_location": "Airport Station",
|
|
||||||
"end_location": "West Valley Central Station"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"route_id": "704",
|
|
||||||
"start_location": "Daybreak Parkway Station",
|
|
||||||
"end_location": "U. Of U. Medical Center Station"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"route_id": "720",
|
|
||||||
"start_location": "Provo Central Station",
|
|
||||||
"end_location": "Ogden Station"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"route_id": "750",
|
|
||||||
"start_location": "Fairmont Station",
|
|
||||||
"end_location": "Central Pointe Station"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"vehicleId": "1003B",
|
|
||||||
"routeNum": "701",
|
|
||||||
"routeName": "Blue Line",
|
|
||||||
"destination": "To Salt Lake Central",
|
|
||||||
"bearing": 0.3,
|
|
||||||
"location": {
|
|
||||||
"latitude": 40.67723,
|
|
||||||
"longitude": -111.8941
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vehicleId": "1016B",
|
|
||||||
"routeNum": "701",
|
|
||||||
"routeName": "Blue Line",
|
|
||||||
"destination": "To Draper",
|
|
||||||
"bearing": 89.9,
|
|
||||||
"location": {
|
|
||||||
"latitude": 40.76929,
|
|
||||||
"longitude": -111.9005
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vehicleId": "1006A",
|
|
||||||
"routeNum": "701",
|
|
||||||
"routeName": "Blue Line",
|
|
||||||
"destination": "To Salt Lake Central",
|
|
||||||
"bearing": 358.6,
|
|
||||||
"location": {
|
|
||||||
"latitude": 40.76017,
|
|
||||||
"longitude": -111.8911
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vehicleId": "1028A",
|
|
||||||
"routeNum": "701",
|
|
||||||
"routeName": "Blue Line",
|
|
||||||
"destination": "To Draper",
|
|
||||||
"bearing": 179.9,
|
|
||||||
"location": {
|
|
||||||
"latitude": 40.74292,
|
|
||||||
"longitude": -111.8969
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vehicleId": "1004A",
|
|
||||||
"routeNum": "701",
|
|
||||||
"routeName": "Blue Line",
|
|
||||||
"destination": "To Salt Lake Central",
|
|
||||||
"bearing": 349,
|
|
||||||
"location": {
|
|
||||||
"latitude": 40.57289,
|
|
||||||
"longitude": -111.8852
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vehicleId": "1008A",
|
|
||||||
"routeNum": "701",
|
|
||||||
"routeName": "Blue Line",
|
|
||||||
"destination": "To Draper",
|
|
||||||
"bearing": 180.6,
|
|
||||||
"location": {
|
|
||||||
"latitude": 40.65089,
|
|
||||||
"longitude": -111.8982
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const dataDir = path.join(process.cwd(), "src", "dal", "data");
|
|
||||||
|
|
||||||
function readJSON(name) {
|
|
||||||
try {
|
|
||||||
const p = path.join(dataDir, name);
|
|
||||||
|
|
||||||
if (!fs.existsSync(p)) return null;
|
|
||||||
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVehicles() {
|
|
||||||
const raw = readJSON("vehicles.json");
|
|
||||||
|
|
||||||
if (!Array.isArray(raw)) return [];
|
|
||||||
return raw.map(v => {
|
|
||||||
const loc = v.location || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
vehicleId: v.vehicleId,
|
|
||||||
routeNum: v.routeNum,
|
|
||||||
routeName: v.routeName,
|
|
||||||
destination: v.destination,
|
|
||||||
bearing: v.bearing == null ? undefined : Number(v.bearing),
|
|
||||||
speed: v.speed == null ? undefined : Number(v.speed),
|
|
||||||
location: {
|
|
||||||
latitude: loc.latitude == null ? undefined : Number(loc.latitude),
|
|
||||||
longitude: loc.longitude == null ? undefined : Number(loc.longitude)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVehicleById(id) {
|
|
||||||
if (id == null) return null;
|
|
||||||
const vehicles = getVehicles();
|
|
||||||
|
|
||||||
return vehicles.find(v => String(v.vehicleId) === String(id)) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRoutes() {
|
|
||||||
const raw = readJSON("routes.json");
|
|
||||||
|
|
||||||
if (!Array.isArray(raw)) return [];
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRouteById(routeId) {
|
|
||||||
if (routeId == null) return null;
|
|
||||||
const routes = getRoutes();
|
|
||||||
|
|
||||||
return routes.find(r => String(r.route_id) === String(routeId)) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRoutePathsMap() {
|
|
||||||
const raw = readJSON("routepaths.json");
|
|
||||||
|
|
||||||
if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRoutePath(routeId) {
|
|
||||||
if (routeId == null) return null;
|
|
||||||
const map = getRoutePathsMap();
|
|
||||||
|
|
||||||
return Object.prototype.hasOwnProperty.call(map, routeId) ? map[routeId] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStationsRaw() {
|
|
||||||
return readJSON("stations.json") || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStations() {
|
|
||||||
const raw = getStationsRaw();
|
|
||||||
|
|
||||||
if (raw == null) return [];
|
|
||||||
if (Array.isArray(raw)) {
|
|
||||||
const isArrayOfMaps = raw.every(item => typeof item === "object" && !Array.isArray(item) && Object.values(item).every(v => Array.isArray(v)));
|
|
||||||
if (isArrayOfMaps) return raw.flatMap(item => Object.values(item).flat());
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
if (typeof raw === "object") {
|
|
||||||
return Object.values(raw).flat();
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStopsByRoute(routeId) {
|
|
||||||
if (routeId == null) return [];
|
|
||||||
const raw = getStationsRaw();
|
|
||||||
|
|
||||||
if (raw == null) return [];
|
|
||||||
if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
||||||
return Array.isArray(raw[routeId]) ? raw[routeId] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(raw)) {
|
|
||||||
const matchesFromArrayMaps = [];
|
|
||||||
for (const item of raw) {
|
|
||||||
if (typeof item === "object" && !Array.isArray(item) && Object.prototype.hasOwnProperty.call(item, routeId)) {
|
|
||||||
const arr = item[routeId];
|
|
||||||
if (Array.isArray(arr)) matchesFromArrayMaps.push(...arr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matchesFromArrayMaps.length) return matchesFromArrayMaps;
|
|
||||||
|
|
||||||
return raw.filter(s => Array.isArray(s.lines) && s.lines.map(String).includes(String(routeId)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStationById(id) {
|
|
||||||
if (id == null) return null;
|
|
||||||
const raw = getStationsRaw();
|
|
||||||
if (raw == null) return null;
|
|
||||||
|
|
||||||
if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
||||||
for (const key of Object.keys(raw)) {
|
|
||||||
const arr = raw[key];
|
|
||||||
if (!Array.isArray(arr)) continue;
|
|
||||||
const found = arr.find(s => String(s.stop_id) === String(id) || String(s.stationId) === String(id));
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(raw)) {
|
|
||||||
const isArrayOfMaps = raw.every(item => typeof item === "object" && !Array.isArray(item) && Object.values(item).every(v => Array.isArray(v)));
|
|
||||||
if (isArrayOfMaps) {
|
|
||||||
for (const item of raw) {
|
|
||||||
for (const key of Object.keys(item)) {
|
|
||||||
const arr = item[key];
|
|
||||||
const found = arr.find(s => String(s.stop_id) === String(id) || String(s.stationId) === String(id));
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return raw.find(s => String(s.stop_id) === String(id) || String(s.stationId) === String(id)) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import * as dal from "../dal/staticDal.js";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/routes", (req, res) => {
|
|
||||||
const routes = dal.getRoutes();
|
|
||||||
|
|
||||||
res.json({meta: {returned: routes.length}, data: routes});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/routes/:routeId", (req, res) => {
|
|
||||||
const routeId = req.params.routeId;
|
|
||||||
const route = dal.getRouteById(routeId);
|
|
||||||
|
|
||||||
if (!route) return res.status(404).json({error: "Route Was Not Found"});
|
|
||||||
const routePath = dal.getRoutePath(routeId);
|
|
||||||
const stations = dal.getStopsByRoute(routeId);
|
|
||||||
|
|
||||||
res.json({data: {route, routePath, stations}});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/route/:routeId", (req, res) => {
|
|
||||||
const routeId = req.params.routeId;
|
|
||||||
const route = dal.getRouteById(routeId);
|
|
||||||
|
|
||||||
if (!route) return res.status(404).json({error: "Route Was Not Found"});
|
|
||||||
const routePath = dal.getRoutePath(routeId);
|
|
||||||
const stations = dal.getStopsByRoute(routeId);
|
|
||||||
|
|
||||||
res.json({data: {route, routePath, stations}});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/routes/:routeId/stations", (req, res) => {
|
|
||||||
const routeId = req.params.routeId;
|
|
||||||
const stations = dal.getStopsByRoute(routeId);
|
|
||||||
|
|
||||||
res.json({meta: {routeId: String(routeId), returned: stations.length}, data: stations});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/stops/:routeId", (req, res) => {
|
|
||||||
const routeId = req.params.routeId;
|
|
||||||
const stations = dal.getStopsByRoute(routeId);
|
|
||||||
|
|
||||||
res.json({meta: {routeId: String(routeId), returned: stations.length}, data: stations});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/stations", (req, res) => {
|
|
||||||
const route = req.query.route;
|
|
||||||
const stations = route ? dal.getStopsByRoute(route) : dal.getStations();
|
|
||||||
|
|
||||||
res.json({meta: {returned: stations.length}, data: stations});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/station/:stationId", (req, res) => {
|
|
||||||
const stationId = req.params.stationId;
|
|
||||||
const station = dal.getStationById(stationId);
|
|
||||||
|
|
||||||
if (!station) return res.status(404).json({error: "Station Was Not Found"});
|
|
||||||
res.json({data: station});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import * as dal from "../dal/staticDal.js";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/", (req, res) => {
|
|
||||||
let vehicles = dal.getVehicles();
|
|
||||||
|
|
||||||
const {routeNum, routeName, destination, minLat, maxLat, minLng, maxLng, limit} = req.query;
|
|
||||||
|
|
||||||
if (routeNum) vehicles = vehicles.filter(v => String(v.routeNum) === String(routeNum));
|
|
||||||
if (routeName) vehicles = vehicles.filter(v => v.routeName && v.routeName.toLowerCase().includes(String(routeName).toLowerCase()));
|
|
||||||
if (destination) vehicles = vehicles.filter(v => v.destination && v.destination.toLowerCase().includes(String(destination).toLowerCase()));
|
|
||||||
|
|
||||||
if (minLat || maxLat || minLng || maxLng) {
|
|
||||||
const minLatN = parseFloat(minLat ?? -90);
|
|
||||||
const maxLatN = parseFloat(maxLat ?? 90);
|
|
||||||
const minLngN = parseFloat(minLng ?? -180);
|
|
||||||
const maxLngN = parseFloat(maxLng ?? 180);
|
|
||||||
|
|
||||||
vehicles = vehicles.filter(v => {
|
|
||||||
const lat = parseFloat(v.location?.latitude);
|
|
||||||
const lng = parseFloat(v.location?.longitude);
|
|
||||||
|
|
||||||
return lat >= minLatN && lat <= maxLatN && lng >= minLngN && lng <= maxLngN;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let returned = vehicles.length;
|
|
||||||
if (limit) {
|
|
||||||
const lim = Math.max(1, Math.min(1000, parseInt(limit, 10) || 100));
|
|
||||||
|
|
||||||
vehicles = vehicles.slice(0, lim);
|
|
||||||
returned = vehicles.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({meta: {returned}, data: vehicles});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/:id", (req, res) => {
|
|
||||||
const v = dal.getVehicleById(req.params.id);
|
|
||||||
if (!v) return res.status(404).json({error: "Vehicle Was Not Found"});
|
|
||||||
|
|
||||||
res.json({data: v});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
12
start.js
12
start.js
@@ -1,12 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import app from "./app.js";
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 7653;
|
|
||||||
|
|
||||||
// Serve static files from "public" folder
|
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Transit API running at http://localhost:${PORT}`);
|
|
||||||
});
|
|
||||||
320
tools/gtfsData.js
Normal file
320
tools/gtfsData.js
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
const GtfsRealtimeBindings = require("gtfs-realtime-bindings");
|
||||||
|
const unzipper = require("unzipper");
|
||||||
|
const parse = require("csv-parse/sync").parse;
|
||||||
|
|
||||||
|
const TRAIN_ROUTES = ["701", "703", "704", "720", "750"];
|
||||||
|
const URL = "https://apps.rideuta.com/tms/gtfs/";
|
||||||
|
const URL_ROUTES = ["Vehicle", "TripUpdate", "Alert"];
|
||||||
|
const RT_POLLING = 3000;
|
||||||
|
|
||||||
|
let gtfs_data = null;
|
||||||
|
let gtfs_rt_v = null;
|
||||||
|
let gtfs_rt_t = null;
|
||||||
|
let gtfs_rt_a = null;
|
||||||
|
let gtfs_timestamp = null;
|
||||||
|
let updatePromise = null;
|
||||||
|
|
||||||
|
|
||||||
|
async function applyTripUpdates(stopTimes, tripId) {
|
||||||
|
await updateGtfsRt();
|
||||||
|
if (!gtfs_rt_t) return stopTimes;
|
||||||
|
|
||||||
|
const tripUpdateEntity = gtfs_rt_t.entity.find(e => e.tripUpdate?.trip?.tripId === tripId);
|
||||||
|
if (!tripUpdateEntity || !tripUpdateEntity.tripUpdate) return stopTimes;
|
||||||
|
|
||||||
|
const updates = tripUpdateEntity.tripUpdate.stopTimeUpdate || [];
|
||||||
|
|
||||||
|
const updateMap = new Map();
|
||||||
|
updates.forEach(u => {
|
||||||
|
if (u.stopId) updateMap.set(u.stopId, u);
|
||||||
|
});
|
||||||
|
|
||||||
|
return stopTimes.map(st => {
|
||||||
|
const update = updateMap.get(st.stop_id);
|
||||||
|
if (!update) return { ...st };
|
||||||
|
|
||||||
|
return {
|
||||||
|
...st,
|
||||||
|
arrival: update.arrival ? {
|
||||||
|
time: update.arrival.time,
|
||||||
|
delay: update.arrival.delay
|
||||||
|
} : undefined,
|
||||||
|
departure: update.departure ? {
|
||||||
|
time: update.departure.time,
|
||||||
|
delay: update.departure.delay
|
||||||
|
} : undefined,
|
||||||
|
scheduleRelationship: update.scheduleRelationship || 'SCHEDULED'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGtfsStaticInMemory() {
|
||||||
|
const url = "https://apps.rideuta.com/tms/gtfs/Static";
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(`Failed: ${res.status} ${res.statusText}`);
|
||||||
|
|
||||||
|
const zipBuffer = Buffer.from(await res.arrayBuffer());
|
||||||
|
const directory = await unzipper.Open.buffer(zipBuffer);
|
||||||
|
const gtfs = {};
|
||||||
|
|
||||||
|
for (const entry of directory.files) {
|
||||||
|
if (!entry.path.endsWith(".txt")) continue;
|
||||||
|
|
||||||
|
const fileBuffer = await entry.buffer();
|
||||||
|
const text = fileBuffer.toString("utf8");
|
||||||
|
const rows = parse(text, { columns: true, skip_empty_lines: true });
|
||||||
|
const name = entry.path.replace(".txt", "");
|
||||||
|
gtfs[name] = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrains() {
|
||||||
|
await updateGtfsRt();
|
||||||
|
|
||||||
|
if (!gtfs_rt_v) return [];
|
||||||
|
const trains = [];
|
||||||
|
gtfs_rt_v.entity.forEach(entity => {
|
||||||
|
if (entity.vehicle) {
|
||||||
|
const tripId = entity.vehicle.trip.tripId;
|
||||||
|
const trip = gtfs_data.trips.find(t => t.trip_id === tripId);
|
||||||
|
//const stopTimes = gtfs_data.stop_times.filter(st => st.trip_id === tripId);
|
||||||
|
|
||||||
|
let route = null;
|
||||||
|
if (trip) {
|
||||||
|
route = gtfs_data.routes.find(r => r.route_id === trip.route_id);
|
||||||
|
}
|
||||||
|
if (route && TRAIN_ROUTES.find(r => r === route.route_short_name)) {
|
||||||
|
trains.push({
|
||||||
|
vehicle: entity.vehicle,
|
||||||
|
trip,
|
||||||
|
//stopTimes,
|
||||||
|
route
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return trains;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrainsByRoute(route) {
|
||||||
|
const trains = await getTrains();
|
||||||
|
return trains.filter(t => t.route && t.route.route_short_name === route);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuses() {
|
||||||
|
await updateGtfsRt();
|
||||||
|
|
||||||
|
if (!gtfs_rt_v) return [];
|
||||||
|
const buses = [];
|
||||||
|
|
||||||
|
gtfs_rt_v.entity.forEach(entity => {
|
||||||
|
if (entity.vehicle) {
|
||||||
|
const tripId = entity.vehicle.trip.tripId;
|
||||||
|
const trip = gtfs_data.trips.find(t => t.trip_id === tripId);
|
||||||
|
//const stopTimes = gtfs_data.stop_times.filter(st => st.trip_id === tripId);
|
||||||
|
|
||||||
|
let route = null;
|
||||||
|
if (trip) {
|
||||||
|
route = gtfs_data.routes.find(r => r.route_id === trip.route_id);
|
||||||
|
}
|
||||||
|
if (route && !TRAIN_ROUTES.find(r => r === route.route_short_name)) {
|
||||||
|
buses.push({
|
||||||
|
vehicle: entity.vehicle,
|
||||||
|
trip,
|
||||||
|
//stopTimes,
|
||||||
|
route
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return buses;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBusesByRoute(route) {
|
||||||
|
const buses = await getBuses();
|
||||||
|
return buses.filter(b => b.route && b.route.route_short_name === route);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGtfsRt() {
|
||||||
|
if (!gtfs_data) gtfs_data = await loadGtfsStaticInMemory();
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (!gtfs_timestamp || now - gtfs_timestamp >= RT_POLLING) {
|
||||||
|
if (!updatePromise) {
|
||||||
|
updatePromise = (async () => {
|
||||||
|
gtfs_timestamp = now;
|
||||||
|
try {
|
||||||
|
[gtfs_rt_v, gtfs_rt_t, gtfs_rt_a] = await Promise.all([
|
||||||
|
loadGtfsRt("Vehicle"),
|
||||||
|
loadGtfsRt("TripUpdate"),
|
||||||
|
loadGtfsRt("Alert")
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update RT feeds:", e);
|
||||||
|
} finally {
|
||||||
|
updatePromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
await updatePromise;
|
||||||
|
}
|
||||||
|
return { gtfs_rt_v,gtfs_rt_t,gtfs_rt_a };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGtfsRt(feedType = URL_ROUTES[0]) {
|
||||||
|
const response = await fetch(URL + feedType);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const buffer = new Uint8Array(await response.arrayBuffer());
|
||||||
|
return GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlerts() {
|
||||||
|
await updateGtfsRt();
|
||||||
|
|
||||||
|
const alerts = [];
|
||||||
|
if (!gtfs_rt_a) return [];
|
||||||
|
const routeMap = new Map(gtfs_data.routes.map(r => [r.route_id, r]));
|
||||||
|
|
||||||
|
gtfs_rt_a.entity.forEach(entity => {
|
||||||
|
|
||||||
|
const informedEntities = entity.alert?.informedEntity || [];
|
||||||
|
|
||||||
|
const routes = informedEntities
|
||||||
|
.map(e => e.routeId)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(routeId => routeMap.get(routeId))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
alerts.push({ alert: entity.alert, routes });
|
||||||
|
});
|
||||||
|
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlertsByRoute(route) {
|
||||||
|
const alerts = await getAlerts();
|
||||||
|
return alerts.filter(a =>
|
||||||
|
a.routes && a.routes.some(r => r.route_short_name === route)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScheduleByRoute(route) {
|
||||||
|
await updateGtfsRt();
|
||||||
|
|
||||||
|
const matchingRoutes = gtfs_data.routes.filter(r => r.route_short_name === route);
|
||||||
|
if (!matchingRoutes.length) return [];
|
||||||
|
|
||||||
|
const trips = gtfs_data.trips.filter(t => matchingRoutes.some(r => r.route_id === t.route_id));
|
||||||
|
const schedulePromises = trips.map(async trip => {
|
||||||
|
let stopTimes = gtfs_data.stop_times.filter(st => st.trip_id === trip.trip_id);
|
||||||
|
stopTimes = await applyTripUpdates(stopTimes, trip.trip_id);
|
||||||
|
const routeObj = matchingRoutes.find(r => r.route_id === trip.route_id);
|
||||||
|
|
||||||
|
return { trip, stopTimes, route: routeObj };
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(schedulePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScheduleByStationId(stopId) {
|
||||||
|
await updateGtfsRt();
|
||||||
|
|
||||||
|
const stopTimes = gtfs_data.stop_times.filter(st => st.stop_id === stopId);
|
||||||
|
const schedulePromises = stopTimes.map(async st => {
|
||||||
|
const trip = gtfs_data.trips.find(t => t.trip_id === st.trip_id);
|
||||||
|
const route = trip ? gtfs_data.routes.find(r => r.route_id === trip.route_id) : null;
|
||||||
|
const updatedStopTimes = await applyTripUpdates([st], st.trip_id);
|
||||||
|
|
||||||
|
return { stopTime: updatedStopTimes[0], trip, route };
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(schedulePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getShapeByRoute(route) {
|
||||||
|
if (!gtfs_data) {
|
||||||
|
await loadGtfsStaticInMemory();
|
||||||
|
};
|
||||||
|
|
||||||
|
const routes = gtfs_data.routes.filter(r => r.route_short_name === route);
|
||||||
|
if (!routes.length) return [];
|
||||||
|
|
||||||
|
const shapeIds = new Set();
|
||||||
|
gtfs_data.trips.forEach(trip => {
|
||||||
|
if (routes.some(r => r.route_id === trip.route_id) && trip.shape_id) {
|
||||||
|
shapeIds.add(trip.shape_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shapes = [];
|
||||||
|
shapeIds.forEach(shapeId => {
|
||||||
|
const points = gtfs_data.shapes
|
||||||
|
.filter(s => s.shape_id === shapeId)
|
||||||
|
.sort((a, b) => parseInt(a.shape_pt_sequence) - parseInt(b.shape_pt_sequence))
|
||||||
|
.map(s => ({ lat: parseFloat(s.shape_pt_lat), lon: parseFloat(s.shape_pt_lon) }));
|
||||||
|
|
||||||
|
shapes.push({ shapeId, points });
|
||||||
|
});
|
||||||
|
|
||||||
|
return shapes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoutes() {
|
||||||
|
if (!gtfs_data) {
|
||||||
|
gtfs_data = await loadGtfsStaticInMemory();
|
||||||
|
}
|
||||||
|
return gtfs_data.routes || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStops() {
|
||||||
|
if (!gtfs_data) {
|
||||||
|
gtfs_data = await loadGtfsStaticInMemory();
|
||||||
|
}
|
||||||
|
return gtfs_data.stops || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStopsByRoute(route) {
|
||||||
|
if (!gtfs_data) {
|
||||||
|
gtfs_data = await loadGtfsStaticInMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingRoutes = gtfs_data.routes.filter(r => r.route_short_name === route);
|
||||||
|
if (!matchingRoutes.length) return [];
|
||||||
|
|
||||||
|
const trips = gtfs_data.trips.filter(t => matchingRoutes.some(r => r.route_id === t.route_id));
|
||||||
|
const stopIds = new Set(
|
||||||
|
gtfs_data.stop_times
|
||||||
|
.filter(st => trips.some(trip => trip.trip_id === st.trip_id))
|
||||||
|
.map(st => st.stop_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return gtfs_data.stops.filter(s => stopIds.has(s.stop_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
console.log("Refreshing static GTFS...");
|
||||||
|
gtfs_data = await loadGtfsStaticInMemory();
|
||||||
|
}, 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getStops,
|
||||||
|
getStopsByRoute,
|
||||||
|
getRoutes,
|
||||||
|
getTrains,
|
||||||
|
getBuses,
|
||||||
|
getTrainsByRoute,
|
||||||
|
getBusesByRoute,
|
||||||
|
getAlerts,
|
||||||
|
getAlertsByRoute,
|
||||||
|
getScheduleByRoute,
|
||||||
|
getScheduleByStationId,
|
||||||
|
getShapeByRoute,
|
||||||
|
updateGtfsRt,
|
||||||
|
loadGtfsStaticInMemory,
|
||||||
|
}
|
||||||
99
tools/gtfsRedis.js
Normal file
99
tools/gtfsRedis.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
const gtfs = require("./gtfsData");
|
||||||
|
const redis = require("./redisDAL");
|
||||||
|
|
||||||
|
const CACHE_PREFIX = "gtfs_cache";
|
||||||
|
const RT_TTL = 3;
|
||||||
|
const STATIC_TTL = 24 * 3600;
|
||||||
|
|
||||||
|
async function getCached(keySuffix, fetchFn, ttl) {
|
||||||
|
await redis.connect();
|
||||||
|
|
||||||
|
const cached = await redis.get(CACHE_PREFIX, { key: keySuffix });
|
||||||
|
if (cached !== null) return cached;
|
||||||
|
|
||||||
|
const data = await fetchFn();
|
||||||
|
await redis.set(CACHE_PREFIX, { key: keySuffix }, data, ttl);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrains() {
|
||||||
|
return getCached("trains", gtfs.getTrains, RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuses() {
|
||||||
|
return getCached("buses", gtfs.getBuses, RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrainsByRoute(route) {
|
||||||
|
return getCached(`trains_route_${route}`, () => gtfs.getTrainsByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBusesByRoute(route) {
|
||||||
|
return getCached(`buses_route_${route}`, () => gtfs.getBusesByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlerts() {
|
||||||
|
return getCached("alerts", gtfs.getAlerts, RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlertsByRoute(route) {
|
||||||
|
return getCached(`alerts_route_${route}`, () => gtfs.getAlertsByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScheduleByRoute(route) {
|
||||||
|
return getCached(`schedule_route_${route}`, () => gtfs.getScheduleByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScheduleByStationId(stopId) {
|
||||||
|
return getCached(`schedule_stop_${stopId}`, () => gtfs.getScheduleByStationId(stopId), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoutes() {
|
||||||
|
return getCached("routes", gtfs.getRoutes, STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStops() {
|
||||||
|
return getCached("stops", gtfs.getStops, STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStopsByRoute(route) {
|
||||||
|
return getCached(`stops_route_${route}`, () => gtfs.getStopsByRoute(route), STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getShapeByRoute(route) {
|
||||||
|
return getCached(`shape_route_${route}`, () => gtfs.getShapeByRoute(route), STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGtfsRt() {
|
||||||
|
const data = await gtfs.updateGtfsRt();
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "trains" }, await gtfs.getTrains(), RT_TTL);
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "buses" }, await gtfs.getBuses(), RT_TTL);
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "alerts" }, await gtfs.getAlerts(), RT_TTL);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGtfsStaticInMemory() {
|
||||||
|
const data = await gtfs.loadGtfsStaticInMemory();
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "routes" }, data.routes || [], STATIC_TTL);
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "stops" }, data.stops || [], STATIC_TTL);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { gtfs_rt_v, gtfs_rt_t, gtfs_rt_a } = gtfs;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getStops,
|
||||||
|
getStopsByRoute,
|
||||||
|
getRoutes,
|
||||||
|
getTrains,
|
||||||
|
getBuses,
|
||||||
|
getTrainsByRoute,
|
||||||
|
getBusesByRoute,
|
||||||
|
getAlerts,
|
||||||
|
getAlertsByRoute,
|
||||||
|
getScheduleByRoute,
|
||||||
|
getScheduleByStationId,
|
||||||
|
getShapeByRoute,
|
||||||
|
updateGtfsRt,
|
||||||
|
loadGtfsStaticInMemory,
|
||||||
|
};
|
||||||
45
tools/redisDAL.js
Normal file
45
tools/redisDAL.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const { createClient } = require("redis");
|
||||||
|
|
||||||
|
class RedisDAL {
|
||||||
|
constructor() {
|
||||||
|
this.client = createClient({ url:"redis://my-redis:6379" });
|
||||||
|
|
||||||
|
this.client.on("error", (err) => {
|
||||||
|
console.error("Redis Client Error", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (!this.client.isOpen) {
|
||||||
|
await this.client.connect();
|
||||||
|
console.log("Connected to Redis");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateKey(prefix, params) {
|
||||||
|
const paramString = Object.entries(params)
|
||||||
|
.sort()
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join("&");
|
||||||
|
return `${prefix}:${paramString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(prefix, params) {
|
||||||
|
const key = this.generateKey(prefix, params);
|
||||||
|
const cached = await this.client.get(key);
|
||||||
|
if (cached) return JSON.parse(cached);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(prefix, params, data, ttl = 3600) {
|
||||||
|
const key = this.generateKey(prefix, params);
|
||||||
|
await this.client.set(key, JSON.stringify(data), { EX: ttl });
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(prefix, params) {
|
||||||
|
const key = this.generateKey(prefix, params);
|
||||||
|
await this.client.del(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new RedisDAL();
|
||||||
69024
tripUpdates.json
69024
tripUpdates.json
File diff suppressed because it is too large
Load Diff
6689
vehicles.json
6689
vehicles.json
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user