diff --git a/package-lock.json b/package-lock.json index 2f68396..dcbeaf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index 3a017ee..73a8911 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "type": "module", "dependencies": { "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "pg": "^8.16.3" } } diff --git a/src/dal/postgisdDal.js b/src/dal/postgisdDal.js new file mode 100644 index 0000000..b140657 --- /dev/null +++ b/src/dal/postgisdDal.js @@ -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; +}