From eeb63fa51052fb63d0c28657f2bd8a9b5124888c Mon Sep 17 00:00:00 2001 From: thenatespack Date: Mon, 1 Dec 2025 22:44:15 -0700 Subject: [PATCH] Postgres DAL --- public/app.js | 268 +++++++++++++++++++++++++++++++++++++---- public/index.html | 30 +++-- src/dal/postgresDAL.js | 165 +++++++++++++++++++++++++ src/routes/routes.js | 6 +- src/routes/vehicles.js | 6 +- 5 files changed, 437 insertions(+), 38 deletions(-) create mode 100644 src/dal/postgresDAL.js diff --git a/public/app.js b/public/app.js index d110b19..b3a53b1 100644 --- a/public/app.js +++ b/public/app.js @@ -1,49 +1,269 @@ -const map = L.map("map").setView([40.7608, -111.8910], 12); +// ------------------------------ +// MAP SETUP +// ------------------------------ +var streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' +}); -L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - attribution: '© OpenStreetMap', -}).addTo(map); +var satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/' + + 'World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri' +}); +var map = L.map('map', { + center: [40.7608, -111.8910], + zoom: 13, + layers: [streets] +}); + +var baseMaps = { "Streets": streets, "Satellite": satellite }; +L.control.layers(baseMaps).addTo(map); + +const MJR_LINES = ["701", "703", "704", "720", "750"]; +const LRT_LINES = ["701", "703", "704", "720"]; +const FRONTRUNNER = ["750"]; 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] }); +// ------------------------------ +// ROUTE STYLES +// ------------------------------ +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: "🔷" } +}; +// ------------------------------ +// ICON BUILDER +// ------------------------------ +function buildIcon(emoji, bearing) { + const rotate = bearing ? `transform: rotate(${bearing}deg); transform-origin: center;` : ''; + const svg = ` + + + ${emoji} + + `; + + return L.divIcon({ + html: svg, + className: "emoji-icon", + iconSize: [32, 32], + iconAnchor: [16, 16], + popupAnchor: [0, -16] + }); +} + +// ------------------------------ +// MARKER HELPER +// ------------------------------ function addMarker(lat, lon, content, icon) { if (!isNaN(lat) && !isNaN(lon)) { - const marker = L.marker([lat, lon], { icon: icon }).addTo(map); + const marker = L.marker([lat, lon], { icon }); if (content) marker.bindPopup(content); - } else { - console.warn("Invalid coordinates:", latitude, longitude); + return marker; } + console.warn("Invalid coordinates:", lat, lon); } -function getTrainsByRoute(route) { - fetch(API_URL + 'vehicles') +// ------------------------------ +// ROUTE LINES +// ------------------------------ +function drawPolyLine(polylinePoints, color) { + L.polyline(polylinePoints, { color, weight: 4, opacity: 0.8 }).addTo(map); +} + +function drawLine(route) { + fetch(API_URL + "routepaths/" + route) .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); + const points = data.data; + if (!points || points.length === 0) return; + const color = ROUTE_STYLES[route].color; + + let polylinePoints = []; + let currentShape = points[0].shape_id; + + points.forEach(p => { + if (p.shape_id === currentShape) { + polylinePoints.push([p.lat, p.lng]); + } else { + drawPolyLine(polylinePoints, color); + polylinePoints = [[p.lat, p.lng]]; + currentShape = p.shape_id; + } }); + + if (polylinePoints.length > 0) drawPolyLine(polylinePoints, color); }) - .catch(err => console.error("Error fetching trains:", err)); + .catch(err => console.error("Error drawing line:", err)); } +function drawLines() { + MJR_LINES.forEach(drawLine); +} + +// ------------------------------ +// STOPS +// ------------------------------ +let stopMarkers = {}; function getStopsByRoute(route) { - fetch(API_URL + 'stops/' + route) + if (stopMarkers[route]) return; + stopMarkers[route] = []; + const stopIcon = buildIcon(ROUTE_STYLES[route].stop); + + 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); + data.data.forEach(s => { + const lat = parseFloat(s.location.latitude); + const lon = parseFloat(s.location.longitude); + const marker = addMarker( + lat, lon, + `${s.stop_name}
${s.stop_desc}`, + stopIcon + ).addTo(map); + stopMarkers[route].push(marker); }); }) .catch(err => console.error("Error fetching stops:", err)); } -getStopsByRoute("701"); -getTrainsByRoute(); +// ------------------------------ +// VEHICLES +// ------------------------------ +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; + + // Apply filter + 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)); + + // Remove vehicles no longer active + 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}
Vehicle ${vehicleId}
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)); +} + +// ------------------------------ +// USER LOCATION & TRAIN DETECTION +// ------------------------------ +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); + + // Detect if user is on a train + 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."); +} + +// ------------------------------ +// INITIAL LOAD +// ------------------------------ +drawLines(); +MJR_LINES.forEach(r => getStopsByRoute(r)); +refreshVehicles(); +setInterval(refreshVehicles, 1000); diff --git a/public/index.html b/public/index.html index d9ce6b5..62de5ee 100644 --- a/public/index.html +++ b/public/index.html @@ -1,9 +1,9 @@ - - - Traxer + + + Traxer Transit Map + + - -
- - - + + +
+ + +
+ + +
+ + + + + diff --git a/src/dal/postgresDAL.js b/src/dal/postgresDAL.js new file mode 100644 index 0000000..6b0d323 --- /dev/null +++ b/src/dal/postgresDAL.js @@ -0,0 +1,165 @@ +// src/dal/postgresdal.js +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; +} + +/* Vehicles */ + +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), + }, + }; +} + +/* Routes */ + +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; +} + +/* Route paths */ + +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; +} + +/* Stations */ + +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() { + // For compatibility, just return the full array of normalized station rows. + 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 []; + // lines @> ARRAY[$1]::text[] means: lines contains this value + 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]); +} + diff --git a/src/routes/routes.js b/src/routes/routes.js index 1dabdd3..2c3cd19 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -3,8 +3,8 @@ import * as dal from "../dal/staticDal.js"; const router = express.Router(); -router.get("/routes", (req, res) => { - const routes = dal.getRoutes(); +router.get("/routes",async (req, res) => { + const routes = await dal.getRoutes(); res.json({meta: {returned: routes.length}, data: routes}); }); @@ -47,7 +47,7 @@ router.get("/routes/:routeId/stations", (req, res) => { router.get("/stops/:routeId", (req, res) => { const routeId = req.params.routeId; const stations = dal.getStopsByRoute(routeId); - + console.log(stations); res.json({meta: {routeId: String(routeId), returned: stations.length}, data: stations}); }); diff --git a/src/routes/vehicles.js b/src/routes/vehicles.js index a43466a..c2b1e15 100644 --- a/src/routes/vehicles.js +++ b/src/routes/vehicles.js @@ -1,10 +1,10 @@ import express from "express"; -import * as dal from "../dal/staticDal.js"; +import * as dal from "../dal/postgresDAL.js"; const router = express.Router(); -router.get("/", (req, res) => { - let vehicles = dal.getVehicles(); +router.get("/", async (req, res) => { + let vehicles = await dal.getVehicles(); const {routeNum, routeName, destination, minLat, maxLat, minLng, maxLng, limit} = req.query;