Compare commits
1 Commits
c5abf60bff
...
postgres
| Author | SHA1 | Date | |
|---|---|---|---|
| fbb99f21da |
150
package-lock.json
generated
150
package-lock.json
generated
@@ -10,7 +10,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0"
|
||||
"express": "^5.1.0",
|
||||
"pg": "^8.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -581,6 +582,135 @@
|
||||
"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": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -786,6 +916,15 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"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",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0"
|
||||
"express": "^5.1.0",
|
||||
"pg": "^8.16.3"
|
||||
}
|
||||
}
|
||||
|
||||
173
public/app.js
173
public/app.js
@@ -1,41 +1,13 @@
|
||||
|
||||
var streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
});
|
||||
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 ROUTE_STYLES = {
|
||||
"701": { color: "#0074D9", train: "🔵", stop: "🔹" },
|
||||
"704": { color: "#2ECC40", train: "🟢", stop: "🟩" },
|
||||
"703": { color: "#FF4136", train: "🔴", stop: "🔺" },
|
||||
"750": { color: "#B10DC9", train: "🟣", stop: "🔮" },
|
||||
"720": { color: "#000000", train: "⚪", stop: "🔷" }
|
||||
};
|
||||
|
||||
|
||||
function buildIcon(emoji, bearing) {
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
||||
<text x="50%" y="50%" text-anchor="middle"
|
||||
dominant-baseline="central" font-size="26"
|
||||
font-family="Apple Color Emoji, Segoe UI Emoji, NotoColorEmoji, sans-serif"
|
||||
>
|
||||
${emoji}
|
||||
</text>
|
||||
</svg>`;
|
||||
|
||||
return L.divIcon({
|
||||
html: svg,
|
||||
className: "emoji-icon",
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
popupAnchor: [0, -16]
|
||||
});
|
||||
}
|
||||
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)) {
|
||||
@@ -46,13 +18,8 @@ function addMarker(lat, lon, content, icon) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawPolyLine(polylinePoints, color) {
|
||||
L.polyline(polylinePoints, { color, weight: 4, opacity: 0.8 }).addTo(map);
|
||||
}
|
||||
|
||||
function drawLine(route) {
|
||||
fetch(API_URL + "routepaths/" + route)
|
||||
function getTrainsByRoute(route) {
|
||||
fetch(API_URL + 'vehicles')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const trains = data.data;
|
||||
@@ -64,12 +31,6 @@ function drawLine(route) {
|
||||
.catch(err => console.error("Error fetching trains:", err));
|
||||
}
|
||||
|
||||
function drawLines() {
|
||||
MJR_LINES.forEach(drawLine);
|
||||
}
|
||||
|
||||
|
||||
let stopMarkers = {};
|
||||
function getStopsByRoute(route) {
|
||||
fetch(API_URL + 'stops/' + route)
|
||||
.then(res => res.json())
|
||||
@@ -84,125 +45,5 @@ function getStopsByRoute(route) {
|
||||
.catch(err => console.error("Error fetching stops:", err));
|
||||
}
|
||||
|
||||
let trainMarkers = {};
|
||||
let filterType = "all";
|
||||
|
||||
const filterSelect = document.getElementById("filterSelect");
|
||||
if (filterSelect) {
|
||||
filterSelect.addEventListener("change", (e) => {
|
||||
filterType = e.target.value;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshVehicles() {
|
||||
fetch(API_URL + "vehicles")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
let vehicles = data.data;
|
||||
|
||||
if (filterType === "lrt") {
|
||||
vehicles = vehicles.filter(v => LRT_LINES.includes(v.routeNum) || FRONTRUNNER.includes(v.routeNum));
|
||||
}
|
||||
|
||||
vehicles = vehicles.filter(v => MJR_LINES.includes(v.routeNum));
|
||||
|
||||
Object.keys(trainMarkers).forEach(id => {
|
||||
if (!vehicles.find(v => v.vehicleId == id)) {
|
||||
map.removeLayer(trainMarkers[id]);
|
||||
delete trainMarkers[id];
|
||||
}
|
||||
});
|
||||
|
||||
vehicles.forEach(v => {
|
||||
const { vehicleId, routeNum, routeName, speed, bearing } = v;
|
||||
const lat = v.location.latitude;
|
||||
const lon = v.location.longitude;
|
||||
const icon = buildIcon(ROUTE_STYLES[routeNum].train, bearing);
|
||||
|
||||
if (!trainMarkers[vehicleId]) {
|
||||
const marker = L.marker([lat, lon], { icon })
|
||||
.bindPopup(`${routeName}<br>Vehicle ${vehicleId}<br>Speed: ${(speed*2.23694).toFixed(1)} mph`)
|
||||
.addTo(map);
|
||||
marker.vehicleData = v;
|
||||
trainMarkers[vehicleId] = marker;
|
||||
} else {
|
||||
if (trainMarkers[vehicleId].slideTo) {
|
||||
trainMarkers[vehicleId].slideTo([lat, lon], { duration: 1000 });
|
||||
} else {
|
||||
trainMarkers[vehicleId].setLatLng([lat, lon]);
|
||||
}
|
||||
trainMarkers[vehicleId].setIcon(icon);
|
||||
trainMarkers[vehicleId].vehicleData = v;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => console.error("Error refreshing vehicles:", err));
|
||||
}
|
||||
|
||||
|
||||
let userMarker, accuracyCircle;
|
||||
const ON_BOARD_RADIUS = 50; // meters
|
||||
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
|
||||
userMarker = L.circleMarker([lat, lon], {
|
||||
radius: 8,
|
||||
fillColor: "#007AFF",
|
||||
color: "#fff",
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.9
|
||||
}).addTo(map);
|
||||
userMarker.bindPopup("You are here").openPopup();
|
||||
|
||||
accuracyCircle = L.circle([lat, lon], {
|
||||
radius: position.coords.accuracy,
|
||||
color: "#007AFF",
|
||||
fillColor: "#007AFF",
|
||||
fillOpacity: 0.2
|
||||
}).addTo(map);
|
||||
|
||||
map.setView([lat, lon], 13);
|
||||
},
|
||||
(err) => console.warn("Geolocation error:", err.message),
|
||||
{ enableHighAccuracy: true, timeout: 500, maximumAge: 0 }
|
||||
);
|
||||
|
||||
navigator.geolocation.watchPosition(
|
||||
(position) => {
|
||||
const userLat = position.coords.latitude;
|
||||
const userLon = position.coords.longitude;
|
||||
|
||||
if (userMarker) userMarker.setLatLng([userLat, userLon]);
|
||||
if (accuracyCircle) accuracyCircle.setLatLng([userLat, userLon]).setRadius(position.coords.accuracy);
|
||||
|
||||
const vehicles = Object.values(trainMarkers).map(m => m.vehicleData);
|
||||
let closest = null, minDistance = Infinity;
|
||||
|
||||
vehicles.forEach(v => {
|
||||
const distance = map.distance([userLat, userLon], [v.location.latitude, v.location.longitude]);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closest = v;
|
||||
}
|
||||
});
|
||||
|
||||
if (closest && minDistance <= ON_BOARD_RADIUS) {
|
||||
console.log(`User is on vehicle ${closest.vehicleId} (${closest.routeNum})`);
|
||||
}
|
||||
},
|
||||
(err) => console.warn("Geolocation watch error:", err.message),
|
||||
{ enableHighAccuracy: true, maximumAge: 0 }
|
||||
);
|
||||
} else {
|
||||
console.warn("Geolocation not supported by this browser.");
|
||||
}
|
||||
|
||||
drawLines();
|
||||
MJR_LINES.forEach(r => getStopsByRoute(r));
|
||||
refreshVehicles();
|
||||
setInterval(refreshVehicles, 1000);
|
||||
getStopsByRoute("701");
|
||||
getTrainsByRoute();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import pg from "pg";
|
||||
|
||||
const pool = new pg.Pool({
|
||||
connectionString: "postgresql://nate@localhost:5432/gtfs"
|
||||
});
|
||||
|
||||
async function dbQuery(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const res = await client.query(sql, params);
|
||||
return res.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
function toNumber(v) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
|
||||
export async function getVehicles() {
|
||||
const rows = await dbQuery(
|
||||
`SELECT vehicle_id, route_num, route_name, destination,
|
||||
bearing, speed, latitude, longitude
|
||||
FROM vehicles`
|
||||
);
|
||||
|
||||
return rows.map((v) => ({
|
||||
vehicleId: v.vehicle_id,
|
||||
routeNum: v.route_num,
|
||||
routeName: v.route_name,
|
||||
destination: v.destination,
|
||||
bearing: v.bearing == null ? undefined : toNumber(v.bearing),
|
||||
speed: v.speed == null ? undefined : toNumber(v.speed),
|
||||
location: {
|
||||
latitude: toNumber(v.latitude),
|
||||
longitude: toNumber(v.longitude),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getVehicleById(id) {
|
||||
if (id == null) return null;
|
||||
|
||||
const rows = await dbQuery(
|
||||
`SELECT vehicle_id, route_num, route_name, destination,
|
||||
bearing, speed, latitude, longitude
|
||||
FROM vehicles
|
||||
WHERE vehicle_id = $1`,
|
||||
[String(id)]
|
||||
);
|
||||
|
||||
if (!rows.length) return null;
|
||||
const v = rows[0];
|
||||
|
||||
return {
|
||||
vehicleId: v.vehicle_id,
|
||||
routeNum: v.route_num,
|
||||
routeName: v.route_name,
|
||||
destination: v.destination,
|
||||
bearing: v.bearing == null ? undefined : toNumber(v.bearing),
|
||||
speed: v.speed == null ? undefined : toNumber(v.speed),
|
||||
location: {
|
||||
latitude: toNumber(v.latitude),
|
||||
longitude: toNumber(v.longitude),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export async function getRoutes() {
|
||||
const rows = await dbQuery(`SELECT data FROM routes ORDER BY route_id`);
|
||||
return rows.map((r) => r.data);
|
||||
}
|
||||
|
||||
export async function getRouteById(routeId) {
|
||||
if (routeId == null) return null;
|
||||
|
||||
const rows = await dbQuery(
|
||||
`SELECT data FROM routes WHERE route_id = $1`,
|
||||
[String(routeId)]
|
||||
);
|
||||
|
||||
return rows[0]?.data || null;
|
||||
}
|
||||
|
||||
|
||||
export async function getRoutePathsMap() {
|
||||
const rows = await dbQuery(`SELECT route_id, path FROM route_paths`);
|
||||
const map = {};
|
||||
for (const row of rows) {
|
||||
map[String(row.route_id)] = row.path;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export async function getRoutePath(routeId) {
|
||||
if (routeId == null) return null;
|
||||
|
||||
const rows = await dbQuery(
|
||||
`SELECT path FROM route_paths WHERE route_id = $1`,
|
||||
[String(routeId)]
|
||||
);
|
||||
|
||||
return rows[0]?.path || null;
|
||||
}
|
||||
|
||||
|
||||
function transformationStationRow(row) {
|
||||
return {
|
||||
stop_id: row.stop_id,
|
||||
stop_code: row.stop_code,
|
||||
stop_name: row.stop_name,
|
||||
stop_desc: row.stop_desc,
|
||||
location: {
|
||||
latitude: toNumber(row.latitude),
|
||||
longitude: toNumber(row.longitude),
|
||||
},
|
||||
stop_url: row.stop_url,
|
||||
location_type: row.location_type,
|
||||
parent_station: row.parent_station,
|
||||
lines: row.lines, // text[]
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStationsRaw() {
|
||||
const rows = await dbQuery(`SELECT * FROM stations`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function getStations() {
|
||||
const rows = await dbQuery(`SELECT * FROM stations`);
|
||||
return rows.map(transformationStationRow);
|
||||
}
|
||||
|
||||
export async function getStopsByRoute(routeId) {
|
||||
if (routeId == null) return [];
|
||||
const rows = await dbQuery(
|
||||
`SELECT * FROM stations WHERE lines @> ARRAY[$1]::text[]`,
|
||||
[String(routeId)]
|
||||
);
|
||||
return rows.map(transformationStationRow);
|
||||
}
|
||||
|
||||
export async function getStationById(id) {
|
||||
if (id == null) return null;
|
||||
|
||||
const rows = await dbQuery(
|
||||
`SELECT * FROM stations WHERE stop_id = $1`,
|
||||
[String(id)]
|
||||
);
|
||||
|
||||
if (!rows.length) return null;
|
||||
return transformationStationRow(rows[0]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user