Compare commits
10 Commits
b9a63aa6d7
...
postgres
| Author | SHA1 | Date | |
|---|---|---|---|
| fbb99f21da | |||
|
|
06f3129fb9 | ||
| bfe47fb443 | |||
| a5fceda42f | |||
|
|
eaf76f94a1 | ||
| 7cb29179a2 | |||
| 8a5db40d1d | |||
| 9c51163505 | |||
| ba32d9d482 | |||
| a7aff8e169 |
41
.gitea/workflows/deploy.yaml
Normal file
41
.gitea/workflows/deploy.yaml
Normal 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 &
|
||||||
|
|
||||||
4059
alerts.json
Normal file
4059
alerts.json
Normal file
File diff suppressed because it is too large
Load Diff
150
package-lock.json
generated
150
package-lock.json
generated
@@ -10,7 +10,8 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0",
|
||||||
|
"pg": "^8.16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
@@ -581,6 +582,135 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pg": {
|
||||||
|
"version": "8.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||||
|
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"pg-connection-string": "^2.9.1",
|
||||||
|
"pg-pool": "^3.10.1",
|
||||||
|
"pg-protocol": "^1.10.3",
|
||||||
|
"pg-types": "2.2.0",
|
||||||
|
"pgpass": "1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"pg-cloudflare": "^1.2.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg-native": ">=3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"pg-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-cloudflare": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/pg-connection-string": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-int8": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-pool": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-protocol": {
|
||||||
|
"version": "1.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||||
|
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pgpass": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -786,6 +916,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -841,6 +980,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0",
|
||||||
|
"pg": "^8.16.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
public/app.js
Normal file
49
public/app.js
Normal 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
24
public/index.html
Normal 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>
|
||||||
@@ -6020,7 +6020,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"701":
|
"703":
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"shape_id": "238255",
|
"shape_id": "238255",
|
||||||
|
|||||||
142
src/dal/postgisdDal.js
Normal file
142
src/dal/postgisdDal.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import pkg from "pg";
|
||||||
|
const { Pool } = pkg;
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
user: "nate",
|
||||||
|
host: "localhost",
|
||||||
|
database: "gtfs",
|
||||||
|
password: "",
|
||||||
|
port: 5432,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------- VEHICLES ----------------------
|
||||||
|
export async function getVehicles({ minLat, maxLat, minLng, maxLng, routeNum } = {}) {
|
||||||
|
let sql = `
|
||||||
|
SELECT vehicle_id, trip_id, route_id, ts, speed,
|
||||||
|
ST_Y(geom::geometry) AS latitude,
|
||||||
|
ST_X(geom::geometry) AS longitude
|
||||||
|
FROM rt_vehicles
|
||||||
|
`;
|
||||||
|
const params = [];
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
|
if (routeNum) {
|
||||||
|
conditions.push(`route_id = $${params.length + 1}`);
|
||||||
|
params.push(routeNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minLat != null) {
|
||||||
|
conditions.push(`ST_Y(geom::geometry) >= $${params.length + 1}`);
|
||||||
|
params.push(minLat);
|
||||||
|
}
|
||||||
|
if (maxLat != null) {
|
||||||
|
conditions.push(`ST_Y(geom::geometry) <= $${params.length + 1}`);
|
||||||
|
params.push(maxLat);
|
||||||
|
}
|
||||||
|
if (minLng != null) {
|
||||||
|
conditions.push(`ST_X(geom::geometry) >= $${params.length + 1}`);
|
||||||
|
params.push(minLng);
|
||||||
|
}
|
||||||
|
if (maxLng != null) {
|
||||||
|
conditions.push(`ST_X(geom::geometry) <= $${params.length + 1}`);
|
||||||
|
params.push(maxLng);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length) {
|
||||||
|
sql += " WHERE " + conditions.join(" AND ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows } = await pool.query(sql, params);
|
||||||
|
return rows.map(v => ({
|
||||||
|
vehicleId: v.vehicle_id,
|
||||||
|
tripId: v.trip_id,
|
||||||
|
routeId: v.route_id,
|
||||||
|
ts: Number(v.ts),
|
||||||
|
speed: v.speed,
|
||||||
|
location: { latitude: v.latitude, longitude: v.longitude }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVehicleById(vehicleId) {
|
||||||
|
const sql = `
|
||||||
|
SELECT vehicle_id, trip_id, route_id, ts, speed,
|
||||||
|
ST_Y(geom::geometry) AS latitude,
|
||||||
|
ST_X(geom::geometry) AS longitude
|
||||||
|
FROM rt_vehicles
|
||||||
|
WHERE vehicle_id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [vehicleId]);
|
||||||
|
if (!rows[0]) return null;
|
||||||
|
|
||||||
|
const v = rows[0];
|
||||||
|
return {
|
||||||
|
vehicleId: v.vehicle_id,
|
||||||
|
tripId: v.trip_id,
|
||||||
|
routeId: v.route_id,
|
||||||
|
ts: Number(v.ts),
|
||||||
|
speed: v.speed,
|
||||||
|
location: { latitude: v.latitude, longitude: v.longitude }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- ROUTES ----------------------
|
||||||
|
export async function getRoutes() {
|
||||||
|
const { rows } = await pool.query(`SELECT route_id, short_name, long_name, name FROM gtfs_routes`);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRouteById(routeId) {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT route_id, short_name, long_name, name FROM gtfs_routes WHERE route_id = $1 LIMIT 1`,
|
||||||
|
[routeId]
|
||||||
|
);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- STOPS ----------------------
|
||||||
|
export async function getStops() {
|
||||||
|
const { rows } = await pool.query(`
|
||||||
|
SELECT stop_id, stop_name, stop_lat, stop_lon
|
||||||
|
FROM gtfs_stops
|
||||||
|
`);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStopById(stopId) {
|
||||||
|
const { rows } = await pool.query(`
|
||||||
|
SELECT stop_id, stop_name, stop_lat, stop_lon
|
||||||
|
FROM gtfs_stops
|
||||||
|
WHERE stop_id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`, [stopId]);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- STOPS BY ROUTE ----------------------
|
||||||
|
export async function getStopsByRoute(routeId) {
|
||||||
|
const sql = `
|
||||||
|
SELECT s.stop_id, s.stop_name, s.stop_lat, s.stop_lon
|
||||||
|
FROM gtfs_stop_times st
|
||||||
|
JOIN gtfs_trips t ON st.trip_id = t.trip_id
|
||||||
|
JOIN gtfs_stops s ON st.stop_id = s.stop_id
|
||||||
|
WHERE t.route_id = $1
|
||||||
|
GROUP BY s.stop_id, s.stop_name, s.stop_lat, s.stop_lon
|
||||||
|
ORDER BY MIN(st.stop_sequence)
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [routeId]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- TRIP SCHEDULE ----------------------
|
||||||
|
export async function getScheduleByTrip(tripId) {
|
||||||
|
const sql = `
|
||||||
|
SELECT st.stop_sequence, s.stop_id, s.stop_name, st.arrival_time, st.departure_time
|
||||||
|
FROM gtfs_stop_times st
|
||||||
|
JOIN gtfs_stops s ON st.stop_id = s.stop_id
|
||||||
|
WHERE st.trip_id = $1
|
||||||
|
ORDER BY st.stop_sequence
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [tripId]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
@@ -6,101 +6,187 @@ const dataDir = path.join(process.cwd(), "src", "dal", "data");
|
|||||||
function readJSON(name) {
|
function readJSON(name) {
|
||||||
try {
|
try {
|
||||||
const p = path.join(dataDir, name);
|
const p = path.join(dataDir, name);
|
||||||
|
|
||||||
if (!fs.existsSync(p)) return null;
|
if (!fs.existsSync(p)) return null;
|
||||||
const raw = fs.readFileSync(p, "utf8");
|
return JSON.parse(fs.readFileSync(p, "utf8"));
|
||||||
return JSON.parse(raw);
|
} catch {
|
||||||
} catch (err) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toNumber(v) {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getVehicles() {
|
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 : toNumber(v.bearing),
|
||||||
|
speed: v.speed == null ? undefined : toNumber(v.speed),
|
||||||
|
location: {
|
||||||
|
latitude: toNumber(loc.latitude),
|
||||||
|
longitude: toNumber(loc.longitude)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getVehicleById(id) {
|
export function getVehicleById(id) {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
const vehicles = getVehicles();
|
const vehicles = getVehicles();
|
||||||
|
|
||||||
return vehicles.find(v => String(v.vehicleId) === String(id)) || null;
|
return vehicles.find(v => String(v.vehicleId) === String(id)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoutes() {
|
export function getRoutes() {
|
||||||
const explicit = readJSON("routes.json");
|
const raw = readJSON("routes.json");
|
||||||
if (Array.isArray(explicit) && explicit.length) return explicit;
|
|
||||||
|
|
||||||
const vehicles = getVehicles();
|
if (!Array.isArray(raw)) return [];
|
||||||
const map = new Map();
|
return raw;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRouteById(routeId) {
|
export function getRouteById(routeId) {
|
||||||
if (routeId == null) return null;
|
if (routeId == null) return null;
|
||||||
const routes = getRoutes();
|
const routes = getRoutes();
|
||||||
|
|
||||||
return routes.find(r => String(r.routeId) === String(routeId)) || null;
|
return routes.find(r => String(r.route_id) === String(routeId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRoutepathsRaw() {
|
||||||
|
const raw = readJSON("routePath.json");
|
||||||
|
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoutePathsMap() {
|
export function getRoutePathsMap() {
|
||||||
const raw = readJSON("routepaths.json");
|
const raw = readRoutepathsRaw();
|
||||||
|
const map = {};
|
||||||
|
|
||||||
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
for (const item of raw) {
|
||||||
|
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||||
|
const keys = Object.keys(item);
|
||||||
|
|
||||||
|
if (keys.length !== 1) continue;
|
||||||
|
const k = String(keys[0]);
|
||||||
|
|
||||||
|
if (Array.isArray(item[k])) map[k] = item[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoutePath(routeId) {
|
export function getRoutePath(routeId) {
|
||||||
if (routeId == null) return null;
|
if (routeId == null) return null;
|
||||||
const map = getRoutePathsMap();
|
const map = getRoutePathsMap();
|
||||||
const keys = Object.keys(map || {});
|
const key = String(routeId);
|
||||||
const foundKey = keys.find(k => String(k) === String(routeId));
|
|
||||||
|
|
||||||
return foundKey ? map[foundKey] : null;
|
return Object.prototype.hasOwnProperty.call(map, key) ? map[key] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStationsRaw() {
|
||||||
|
return readJSON("stations.json") || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStations() {
|
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 [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformationStation(s) {
|
||||||
|
return {
|
||||||
|
stop_id: s.stop_id,
|
||||||
|
stop_code: s.stop_code,
|
||||||
|
stop_name: s.stop_name,
|
||||||
|
stop_desc: s.stop_desc,
|
||||||
|
location: { latitude: toNumber(s.stop_lat), longitude: toNumber(s.stop_lon) },
|
||||||
|
stop_url: s.stop_url,
|
||||||
|
location_type: s.location_type,
|
||||||
|
parent_station: s.parent_station,
|
||||||
|
lines: s.lines
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStopsByRoute(routeId) {
|
export function getStopsByRoute(routeId) {
|
||||||
if (routeId == null) return [];
|
if (routeId == null) return [];
|
||||||
const stations = getStations();
|
const raw = getStationsRaw();
|
||||||
|
|
||||||
|
if (raw == null) return [];
|
||||||
|
if (typeof raw === "object" && !Array.isArray(raw)) {
|
||||||
|
const arr = raw[routeId];
|
||||||
|
if (!Array.isArray(arr)) return [];
|
||||||
|
return arr.map(transformationStation);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(stations)) return [];
|
if (Array.isArray(raw)) {
|
||||||
return stations.filter(s => {
|
const matchesFromArrayMaps = [];
|
||||||
const lines = s.lines ?? s.lines_arr ?? s.line_ids ?? null;
|
for (const item of raw) {
|
||||||
if (!Array.isArray(lines)) return false;
|
if (typeof item === "object" && !Array.isArray(item) && Object.prototype.hasOwnProperty.call(item, routeId)) {
|
||||||
|
const arr = item[routeId];
|
||||||
|
if (Array.isArray(arr)) matchesFromArrayMaps.push(...arr.map(transformationStation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchesFromArrayMaps.length) return matchesFromArrayMaps;
|
||||||
|
|
||||||
return lines.map(String).includes(String(routeId));
|
return raw.filter(s => Array.isArray(s.lines) && s.lines.map(String).includes(String(routeId))).map(transformationStation);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStationById(stationId) {
|
export function getStationById(id) {
|
||||||
if (stationId == null) return null;
|
if (id == null) return null;
|
||||||
const stations = getStations();
|
const raw = getStationsRaw();
|
||||||
|
if (raw == null) return null;
|
||||||
|
|
||||||
if (!Array.isArray(stations)) return null;
|
if (typeof raw === "object" && !Array.isArray(raw)) {
|
||||||
return stations.find(s => {
|
for (const key of Object.keys(raw)) {
|
||||||
if (s.stop_id && String(s.stop_id) === String(stationId)) return true;
|
const arr = raw[key];
|
||||||
if (s.stationId && String(s.stationId) === String(stationId)) return true;
|
if (!Array.isArray(arr)) continue;
|
||||||
if (s.id && String(s.id) === String(stationId)) return true;
|
const found = arr.find(s => String(s.stop_id) === String(id) || String(s.stationId) === String(id));
|
||||||
|
if (found) return transformationStation(found);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
if (Array.isArray(raw)) {
|
||||||
}) || null;
|
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));
|
||||||
|
if (found) return transformationStation(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const found = raw.find(s => String(s.stop_id) === String(id) || String(s.stationId) === String(id));
|
||||||
|
return found ? transformationStation(found) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,38 +5,55 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.get("/routes", (req, res) => {
|
router.get("/routes", (req, res) => {
|
||||||
const routes = dal.getRoutes();
|
const routes = dal.getRoutes();
|
||||||
|
|
||||||
res.json({meta: {returned: routes.length}, data: routes});
|
res.json({meta: {returned: routes.length}, data: routes});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/routes/:routeId", (req, res) => {
|
router.get("/routes/:routeId", (req, res) => {
|
||||||
const routeId = req.params.routeId;
|
const routeId = req.params.routeId;
|
||||||
let route = dal.getRouteById(routeId) ?? null;
|
const route = dal.getRouteById(routeId);
|
||||||
|
|
||||||
if (route === null) {
|
if (!route) return res.status(404).json({error: "Route Was Not Found"});
|
||||||
const all = dal.getRoutes();
|
const stations = dal.getStopsByRoute(routeId);
|
||||||
route = all.find(r => {
|
|
||||||
const rid = r.route_id ?? r.routeId ?? r.route;
|
|
||||||
return rid != null && String(rid) === String(routeId);
|
|
||||||
}) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routePath = dal.getRoutePath(routeId) ?? null;
|
res.json({data: {route, stations}});
|
||||||
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 stations = dal.getStopsByRoute(routeId);
|
||||||
|
|
||||||
|
res.json({data: {route, stations}});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/routepaths/:routeId", (req, res) => {
|
||||||
|
const routeId = req.params.routeId;
|
||||||
|
const rp = dal.getRoutePath(routeId);
|
||||||
|
|
||||||
|
if (!rp) return res.status(404).json({error: "RoutePath Was Not Found"});
|
||||||
|
res.json({meta: {routeId: String(routeId), returned: Array.isArray(rp) ? rp.length : 0}, data: rp});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/routes/:routeId/stations", (req, res) => {
|
router.get("/routes/:routeId/stations", (req, res) => {
|
||||||
const routeId = req.params.routeId;
|
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});
|
res.json({meta: {routeId: String(routeId), returned: stations.length}, data: stations});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/stations", (req, res) => {
|
router.get("/stations", (req, res) => {
|
||||||
const route = req.query.route;
|
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});
|
res.json({meta: {returned: stations.length}, data: stations});
|
||||||
});
|
});
|
||||||
@@ -44,8 +61,8 @@ router.get("/stations", (req, res) => {
|
|||||||
router.get("/station/:stationId", (req, res) => {
|
router.get("/station/:stationId", (req, res) => {
|
||||||
const stationId = req.params.stationId;
|
const stationId = req.params.stationId;
|
||||||
const station = dal.getStationById(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});
|
res.json({data: station});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
start.js
13
start.js
@@ -1,7 +1,12 @@
|
|||||||
//Starts
|
import express from "express";
|
||||||
import app from "./app.js";
|
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, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Transit API http://localhost:${PORT}`);
|
console.log(`Transit API running at http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
69024
tripUpdates.json
Normal file
69024
tripUpdates.json
Normal file
File diff suppressed because it is too large
Load Diff
6689
vehicles.json
Normal file
6689
vehicles.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user