revert eeb63fa510
Some checks failed
Deploy Express API / deploy (push) Has been cancelled
Some checks failed
Deploy Express API / deploy (push) Has been cancelled
revert Postgres DAL
This commit is contained in:
280
public/app.js
280
public/app.js
@@ -1,269 +1,49 @@
|
||||
// ------------------------------
|
||||
// MAP SETUP
|
||||
// ------------------------------
|
||||
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);
|
||||
|
||||
var satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/' +
|
||||
'World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).addTo(map);
|
||||
|
||||
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/";
|
||||
|
||||
// ------------------------------
|
||||
// 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: "🔷" }
|
||||
};
|
||||
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] });
|
||||
|
||||
// ------------------------------
|
||||
// ICON BUILDER
|
||||
// ------------------------------
|
||||
function buildIcon(emoji, bearing) {
|
||||
const rotate = bearing ? `transform: rotate(${bearing}deg); transform-origin: center;` : '';
|
||||
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]
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// MARKER HELPER
|
||||
// ------------------------------
|
||||
function addMarker(lat, lon, content, icon) {
|
||||
if (!isNaN(lat) && !isNaN(lon)) {
|
||||
const marker = L.marker([lat, lon], { icon });
|
||||
const marker = L.marker([lat, lon], { icon: icon }).addTo(map);
|
||||
if (content) marker.bindPopup(content);
|
||||
return marker;
|
||||
}
|
||||
console.warn("Invalid coordinates:", lat, lon);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// 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 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;
|
||||
console.warn("Invalid coordinates:", latitude, longitude);
|
||||
}
|
||||
});
|
||||
|
||||
if (polylinePoints.length > 0) drawPolyLine(polylinePoints, color);
|
||||
})
|
||||
.catch(err => console.error("Error drawing line:", err));
|
||||
}
|
||||
|
||||
function drawLines() {
|
||||
MJR_LINES.forEach(drawLine);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// STOPS
|
||||
// ------------------------------
|
||||
let stopMarkers = {};
|
||||
function getStopsByRoute(route) {
|
||||
if (stopMarkers[route]) return;
|
||||
stopMarkers[route] = [];
|
||||
const stopIcon = buildIcon(ROUTE_STYLES[route].stop);
|
||||
|
||||
fetch(API_URL + "stops/" + route)
|
||||
function getTrainsByRoute(route) {
|
||||
fetch(API_URL + 'vehicles')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.data.forEach(s => {
|
||||
const lat = parseFloat(s.location.latitude);
|
||||
const lon = parseFloat(s.location.longitude);
|
||||
const marker = addMarker(
|
||||
lat, lon,
|
||||
`${s.stop_name}<br>${s.stop_desc}`,
|
||||
stopIcon
|
||||
).addTo(map);
|
||||
stopMarkers[route].push(marker);
|
||||
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));
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// 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}<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));
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// 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);
|
||||
getStopsByRoute("701");
|
||||
getTrainsByRoute();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Traxer Transit Map</title>
|
||||
<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"
|
||||
@@ -14,25 +14,11 @@
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
crossorigin=""
|
||||
></script>
|
||||
<!-- SlideTo plugin -->
|
||||
<script src='https://unpkg.com/leaflet.marker.slideto@0.2.0/Leaflet.Marker.SlideTo.js'></script>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
|
||||
<!-- Filter UI -->
|
||||
<div class="p-4 bg-gray-100">
|
||||
<label class="mr-2 font-semibold">Show Vehicles:</label>
|
||||
<select id="filterSelect" class="border p-1 rounded">
|
||||
<option value="all">All Routes</option>
|
||||
<option value="lrt">LRT + FrontRunner</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Map -->
|
||||
<div id="map" class="w-full h-[70vh]"></div>
|
||||
|
||||
<!-- App JS -->
|
||||
<script src="app.js"></script>
|
||||
<body>
|
||||
<div id="map" class="w-full h-screen"></div>
|
||||
|
||||
</body>
|
||||
<script src="app.js"></script>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
// 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]);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import * as dal from "../dal/staticDal.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/routes",async (req, res) => {
|
||||
const routes = await dal.getRoutes();
|
||||
router.get("/routes", (req, res) => {
|
||||
const routes = 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});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import express from "express";
|
||||
import * as dal from "../dal/postgresDAL.js";
|
||||
import * as dal from "../dal/staticDal.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
let vehicles = await dal.getVehicles();
|
||||
router.get("/", (req, res) => {
|
||||
let vehicles = dal.getVehicles();
|
||||
|
||||
const {routeNum, routeName, destination, minLat, maxLat, minLng, maxLng, limit} = req.query;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user