Merge pull request 'Actually have A UI for the first time in 21 weeks' (#5) from backend into main
Some checks failed
Deploy Express API / deploy (push) Has been cancelled

Reviewed-on: #5

🫃
This commit is contained in:
2025-11-24 18:24:56 -08:00
10 changed files with 80016 additions and 68 deletions

View File

@@ -0,0 +1,41 @@
name: Deploy Express API
on:
push:
branches:
- backend
- main
jobs:
deploy:
runs-on: linux_amd64
steps:
# Checkout code
- uses: actions/checkout@v3
# Pull latest code
- name: Pull latest code
run: |
cd /var/www/myapp/Project
git reset --hard
git pull origin main
# Install dependencies
- name: Install dependencies
run: |
cd /var/www/myapp/Project
npm install
# Kill existing process (optional)
- name: Stop previous instance
run: |
pkill -f "node .*app.js" || true
pkill -f "node .*start.js" || true
# Start app in background
- name: Start app
run: |
cd /var/www/myapp/Project
nohup npm start > app.log 2>&1 &

View File

@@ -64,4 +64,4 @@
67
6767

4059
alerts.json Normal file

File diff suppressed because it is too large Load Diff

49
public/app.js Normal file
View File

@@ -0,0 +1,49 @@
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();

24
public/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Traxer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link
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>
<body>
<div id="map" class="w-full h-screen"></div>
</body>
<script src="app.js"></script>
</html>

View File

@@ -6,16 +6,36 @@ 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;
const raw = fs.readFileSync(p, "utf8");
return JSON.parse(raw);
} catch (err) {
return JSON.parse(fs.readFileSync(p, "utf8"));
} catch {
return null;
}
}
export function getVehicles() {
return readJSON("vehicles.json") || [];
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) {
@@ -26,81 +46,107 @@ export function getVehicleById(id) {
}
export function getRoutes() {
const explicit = readJSON("routes.json");
if (Array.isArray(explicit) && explicit.length) return explicit;
const raw = readJSON("routes.json");
const vehicles = getVehicles();
const map = new Map();
vehicles.forEach(v => {
const key = String(v.routeNum ?? "");
if (!map.has(key)) {
map.set(key, {
routeId: key,
routeName: v.routeName ?? null,
startTime: null,
endTime: null,
trains: []
});
}
map.get(key).trains.push(v.vehicleId);
});
return Array.from(map.values());
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.routeId) === String(routeId)) || null;
return routes.find(r => String(r.route_id) === String(routeId)) || null;
}
export function getRoutePathsMap() {
const raw = readJSON("routepaths.json");
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
return {};
}
export function getRoutePath(routeId) {
if (routeId == null) return null;
const map = getRoutePathsMap();
const keys = Object.keys(map || {});
const foundKey = keys.find(k => String(k) === String(routeId));
return foundKey ? map[foundKey] : null;
return Object.prototype.hasOwnProperty.call(map, routeId) ? map[routeId] : null;
}
export function getStationsRaw() {
return readJSON("stations.json") || null;
}
export function getStations() {
return readJSON("stations.json") || [];
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 stations = getStations();
const raw = getStationsRaw();
if (!Array.isArray(stations)) return [];
return stations.filter(s => {
const lines = s.lines ?? s.lines_arr ?? s.line_ids ?? null;
if (!Array.isArray(lines)) return false;
if (raw == null) return [];
if (typeof raw === "object" && !Array.isArray(raw)) {
return Array.isArray(raw[routeId]) ? raw[routeId] : [];
}
return lines.map(String).includes(String(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(stationId) {
if (stationId == null) return null;
const stations = getStations();
export function getStationById(id) {
if (id == null) return null;
const raw = getStationsRaw();
if (raw == null) return null;
if (!Array.isArray(stations)) return null;
return stations.find(s => {
if (s.stop_id && String(s.stop_id) === String(stationId)) return true;
if (s.stationId && String(s.stationId) === String(stationId)) return true;
if (s.id && String(s.id) === String(stationId)) return true;
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;
}
return false;
}) || 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;
}

View File

@@ -11,32 +11,43 @@ router.get("/routes", (req, res) => {
router.get("/routes/:routeId", (req, res) => {
const routeId = req.params.routeId;
let route = dal.getRouteById(routeId) ?? null;
const route = dal.getRouteById(routeId);
if (route === null) {
const all = dal.getRoutes();
route = all.find(r => {
const rid = r.route_id ?? r.routeId ?? r.route;
return rid != null && String(rid) === String(routeId);
}) ?? null;
}
if (!route) return res.status(404).json({error: "Route Was Not Found"});
const routePath = dal.getRoutePath(routeId);
const stations = dal.getStopsByRoute(routeId);
const routePath = dal.getRoutePath(routeId) ?? null;
let 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) ?? [];
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() ?? []);
const stations = route ? dal.getStopsByRoute(route) : dal.getStations();
res.json({meta: {returned: stations.length}, data: stations});
});
@@ -44,8 +55,8 @@ router.get("/stations", (req, res) => {
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"});
if (!station) return res.status(404).json({error: "Station Was Not Found"});
res.json({data: station});
});

View File

@@ -1,7 +1,12 @@
//Starts
import express from "express";
import app from "./app.js";
const PORT = process.env.PORT || 3000;
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 http://localhost:${PORT}`);
console.log(`Transit API running at http://localhost:${PORT}`);
});

69024
tripUpdates.json Normal file

File diff suppressed because it is too large Load Diff

6689
vehicles.json Normal file

File diff suppressed because it is too large Load Diff