final product
This commit is contained in:
37
Dockerfile
37
Dockerfile
@@ -1,49 +1,28 @@
|
|||||||
# ---- Builder Stage ----
|
|
||||||
# Use official Node.js 20 (Alpine)
|
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
# Install init system
|
|
||||||
RUN apk add --no-cache dumb-init
|
|
||||||
|
|
||||||
# App dir
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install production deps
|
|
||||||
RUN npm ci --only=production && npm cache clean --force
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
# ---- Final Stage ----
|
COPY . .
|
||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
LABEL maintainer="your-email@example.com"
|
RUN npm run build || echo "No build step needed"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/yourname/uta-gtfs-mysql"
|
|
||||||
|
|
||||||
# Install dumb-init + MySQL client (optional but common)
|
FROM node:20-alpine AS production
|
||||||
RUN apk add --no-cache dumb-init mysql-client
|
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN addgroup -g 1001 -S nodejs \
|
|
||||||
&& adduser -S utauser -u 1001
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy node_modules from builder
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app ./
|
||||||
|
|
||||||
# Copy app source
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
COPY . .
|
adduser -S uta-sync -u 1001
|
||||||
|
|
||||||
RUN chown -R utauser:nodejs /app
|
USER uta-sync
|
||||||
USER utauser
|
|
||||||
|
|
||||||
# Healthcheck – you may want to change port if needed
|
EXPOSE 1001
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
|
||||||
CMD wget -qO- http://localhost:3000/ || exit 1
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -1,45 +1,19 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
uta-mysql:
|
redis:
|
||||||
image: mysql:8.0
|
image: redis:7-alpine
|
||||||
container_name: uta-mysql
|
container_name: my-redis
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: uta123
|
|
||||||
MYSQL_DATABASE: uta
|
|
||||||
MYSQL_USER: uta
|
|
||||||
MYSQL_PASSWORD: uta123
|
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
|
||||||
volumes:
|
|
||||||
- mysql_data:/var/lib/mysql
|
|
||||||
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "6379:6379"
|
||||||
healthcheck:
|
restart: unless-stopped
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-puta", "--password=uta123"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
uta-sync:
|
app:
|
||||||
build: .
|
build: .
|
||||||
container_name: uta-sync
|
container_name: my-express-app
|
||||||
ports:
|
ports:
|
||||||
- "1001:1001"
|
- "1001:1001"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=mysql://uta:uta123@uta-mysql:3306/uta
|
REDIS_HOST: redis
|
||||||
- HEALTH_PORT=3000
|
REDIS_PORT: 6379
|
||||||
- NODE_ENV=production
|
|
||||||
depends_on:
|
depends_on:
|
||||||
uta-mysql:
|
- redis
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 15s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mysql_data:
|
|
||||||
|
|||||||
98
init-db.sql
98
init-db.sql
@@ -1,98 +0,0 @@
|
|||||||
CREATE TABLE routes (
|
|
||||||
route_id VARCHAR(50) PRIMARY KEY,
|
|
||||||
agency_id VARCHAR(50),
|
|
||||||
route_short_name VARCHAR(50),
|
|
||||||
route_long_name VARCHAR(255),
|
|
||||||
route_desc TEXT,
|
|
||||||
route_type INT,
|
|
||||||
route_url VARCHAR(255),
|
|
||||||
route_color VARCHAR(10),
|
|
||||||
route_text_color VARCHAR(10)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE trips (
|
|
||||||
trip_id VARCHAR(50) PRIMARY KEY,
|
|
||||||
route_id VARCHAR(50),
|
|
||||||
service_id VARCHAR(50),
|
|
||||||
shape_id VARCHAR(50),
|
|
||||||
trip_headsign VARCHAR(255),
|
|
||||||
trip_short_name VARCHAR(50),
|
|
||||||
direction_id INT,
|
|
||||||
block_id VARCHAR(50),
|
|
||||||
wheelchair_accessible INT,
|
|
||||||
bikes_allowed INT,
|
|
||||||
FOREIGN KEY (route_id) REFERENCES routes(route_id)
|
|
||||||
);
|
|
||||||
CREATE TABLE stops (
|
|
||||||
stop_id VARCHAR(50) PRIMARY KEY,
|
|
||||||
stop_code VARCHAR(50),
|
|
||||||
stop_name VARCHAR(255),
|
|
||||||
stop_desc TEXT,
|
|
||||||
stop_lat DOUBLE,
|
|
||||||
stop_lon DOUBLE,
|
|
||||||
zone_id VARCHAR(50),
|
|
||||||
stop_url VARCHAR(255),
|
|
||||||
location_type INT DEFAULT NULL,
|
|
||||||
parent_station VARCHAR(50),
|
|
||||||
stop_timezone VARCHAR(50),
|
|
||||||
wheelchair_boarding INT DEFAULT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE stop_times (
|
|
||||||
trip_id VARCHAR(50),
|
|
||||||
arrival_time VARCHAR(20),
|
|
||||||
departure_time VARCHAR(20),
|
|
||||||
stop_id VARCHAR(50),
|
|
||||||
stop_sequence INT,
|
|
||||||
stop_headsign VARCHAR(255),
|
|
||||||
pickup_type INT,
|
|
||||||
drop_off_type INT,
|
|
||||||
shape_dist_traveled DOUBLE NOT NULL DEFAULT 0
|
|
||||||
timepoint INT,
|
|
||||||
PRIMARY KEY (trip_id, stop_id, stop_sequence),
|
|
||||||
FOREIGN KEY (trip_id) REFERENCES trips(trip_id),
|
|
||||||
FOREIGN KEY (stop_id) REFERENCES stops(stop_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE shapes (
|
|
||||||
shape_id VARCHAR(50),
|
|
||||||
shape_pt_lat DOUBLE,
|
|
||||||
shape_pt_lon DOUBLE,
|
|
||||||
shape_pt_sequence INT,
|
|
||||||
shape_dist_traveled DOUBLE,
|
|
||||||
PRIMARY KEY (shape_id, shape_pt_sequence)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Realtime: vehicle positions
|
|
||||||
CREATE TABLE rt_vehicle_positions (
|
|
||||||
vehicle_id VARCHAR(50),
|
|
||||||
trip_id VARCHAR(50),
|
|
||||||
route_id VARCHAR(50),
|
|
||||||
lat DOUBLE,
|
|
||||||
lon DOUBLE,
|
|
||||||
bearing INT,
|
|
||||||
speed DOUBLE,
|
|
||||||
timestamp BIGINT,
|
|
||||||
PRIMARY KEY (vehicle_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Realtime: trip updates
|
|
||||||
CREATE TABLE rt_trip_updates (
|
|
||||||
trip_id VARCHAR(50),
|
|
||||||
stop_id VARCHAR(50),
|
|
||||||
arrival_time BIGINT,
|
|
||||||
departure_time BIGINT,
|
|
||||||
delay INT,
|
|
||||||
schedule_relationship VARCHAR(20),
|
|
||||||
PRIMARY KEY (trip_id, stop_id),
|
|
||||||
FOREIGN KEY (stop_id) REFERENCES stops(stop_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Realtime: alerts
|
|
||||||
CREATE TABLE rt_alerts (
|
|
||||||
alert_id VARCHAR(50) PRIMARY KEY,
|
|
||||||
header TEXT,
|
|
||||||
description TEXT,
|
|
||||||
cause VARCHAR(50),
|
|
||||||
effect VARCHAR(50),
|
|
||||||
timestamp BIGINT
|
|
||||||
);
|
|
||||||
87
package-lock.json
generated
87
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"parse": "^7.1.2",
|
"parse": "^7.1.2",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"redis": "^5.10.0",
|
||||||
"unzipper": "^0.12.3"
|
"unzipper": "^0.12.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -154,6 +155,67 @@
|
|||||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@redis/bloom": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/client": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cluster-key-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/json": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/search": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redis/time-series": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redis/client": "^5.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/linkify-it": {
|
"node_modules/@types/linkify-it": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
@@ -362,6 +424,15 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -1785,6 +1856,22 @@
|
|||||||
"util-deprecate": "~1.0.1"
|
"util-deprecate": "~1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redis": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redis/bloom": "5.10.0",
|
||||||
|
"@redis/client": "5.10.0",
|
||||||
|
"@redis/json": "5.10.0",
|
||||||
|
"@redis/search": "5.10.0",
|
||||||
|
"@redis/time-series": "5.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/requizzle": {
|
"node_modules/requizzle": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"parse": "^7.1.2",
|
"parse": "^7.1.2",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"redis": "^5.10.0",
|
||||||
"unzipper": "^0.12.3"
|
"unzipper": "^0.12.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<div class="controls">
|
<div class="controls">
|
||||||
<select id="routeSelect">
|
<select id="routeSelect">
|
||||||
<option value="">All Trains & Buses</option>
|
<option value="">All Trains & Buses</option>
|
||||||
|
<option value="LRTC">LRT + Communter</option>
|
||||||
<option value="701">Blue Line (701)</option>
|
<option value="701">Blue Line (701)</option>
|
||||||
<option value="703">Red Line (703)</option>
|
<option value="703">Red Line (703)</option>
|
||||||
<option value="704">Green Line (704)</option>
|
<option value="704">Green Line (704)</option>
|
||||||
|
|||||||
245
public/index.js
245
public/index.js
@@ -4,18 +4,16 @@ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|||||||
attribution: '© OpenStreetMap contributors'
|
attribution: '© OpenStreetMap contributors'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const trainIcon = L.divIcon({
|
const trainIcon = L.divIcon({
|
||||||
html: '🚂',
|
html: '🚂',
|
||||||
className: "train-icon",
|
className: "train-icon",
|
||||||
iconSize: [30, 30],
|
iconSize: [32, 32],
|
||||||
iconAnchor: [15, 15]
|
iconAnchor: [15, 15]
|
||||||
});
|
});
|
||||||
|
|
||||||
const busIcon = L.divIcon({
|
const busIcon = L.divIcon({
|
||||||
html: '🚌',
|
html: '🚌',
|
||||||
className: "train-icon",
|
className: "bus-icon",
|
||||||
iconSize: [28, 28],
|
iconSize: [28, 28],
|
||||||
iconAnchor: [14, 14]
|
iconAnchor: [14, 14]
|
||||||
});
|
});
|
||||||
@@ -24,7 +22,6 @@ const vehicleMarkers = L.layerGroup().addTo(map);
|
|||||||
const routeShapes = L.layerGroup().addTo(map);
|
const routeShapes = L.layerGroup().addTo(map);
|
||||||
const stopMarkers = L.layerGroup().addTo(map);
|
const stopMarkers = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
|
|
||||||
const API = "http://localhost:1001/api";
|
const API = "http://localhost:1001/api";
|
||||||
const vehicleMarkersMap = {};
|
const vehicleMarkersMap = {};
|
||||||
|
|
||||||
@@ -32,7 +29,6 @@ let stopsVisible = true;
|
|||||||
let currentRoute = "";
|
let currentRoute = "";
|
||||||
|
|
||||||
async function loadStops(routeId = null) {
|
async function loadStops(routeId = null) {
|
||||||
stopMarkers.clearLayers();
|
|
||||||
if (!stopsVisible) return;
|
if (!stopsVisible) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -63,19 +59,14 @@ async function loadStops(routeId = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("toggleStops").addEventListener("click", () => {
|
document.getElementById("toggleStops").addEventListener("click", async () => {
|
||||||
stopsVisible = !stopsVisible;
|
stopsVisible = !stopsVisible;
|
||||||
if (stopsVisible) {
|
const btn = document.getElementById("toggleStops");
|
||||||
document.getElementById("toggleStops").textContent = "Hide Stops";
|
btn.textContent = stopsVisible ? "Hide Stops" : "Show Stops";
|
||||||
// Reload current stops (all or just the selected route)
|
|
||||||
if (currentRoute === "") {
|
|
||||||
loadStops();
|
|
||||||
} else {
|
|
||||||
loadStops(currentRoute);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.getElementById("toggleStops").textContent = "Show Stops";
|
|
||||||
stopMarkers.clearLayers();
|
stopMarkers.clearLayers();
|
||||||
|
|
||||||
|
if (stopsVisible) {
|
||||||
|
await loadStops(currentRoute || null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,92 +84,99 @@ async function showStopSchedule(stopId, stopName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // midnight today
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
|
||||||
// Process and filter valid arrivals
|
const parsed = data.map(item => {
|
||||||
const upcoming = data
|
const raw = item.stopTime?.arrival_time?.trim();
|
||||||
.map(item => {
|
if (!raw) return null;
|
||||||
const arrivalRaw = item.stopTime?.arrival_time?.trim();
|
|
||||||
if (!arrivalRaw || !item.trip || !item.route) return null; // skip invalid
|
|
||||||
|
|
||||||
let [hours, minutes, seconds] = arrivalRaw.split(":").map(Number);
|
let [h, m, s] = raw.split(":").map(Number);
|
||||||
let arrivalDate = new Date(today);
|
const date = new Date(today);
|
||||||
if (hours >= 24) {
|
if (h >= 24) { h -= 24; date.setDate(date.getDate() + 1); }
|
||||||
hours -= 24;
|
date.setHours(h, m, s || 0, 0);
|
||||||
arrivalDate.setDate(arrivalDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
arrivalDate.setHours(hours, minutes, seconds, 0);
|
|
||||||
|
|
||||||
return { ...item, arrivalDate };
|
if (date <= now) return null;
|
||||||
})
|
|
||||||
.filter(item => item && item.arrivalDate > now) // remove past or invalid
|
return {
|
||||||
.sort((a, b) => a.arrivalDate - b.arrivalDate);
|
route: (item.route?.route_short_name || item.route?.route_id || "???").toString(),
|
||||||
|
headsign: (item.trip?.trip_headsign || "").trim(),
|
||||||
|
time: date.getTime(),
|
||||||
|
tripId: item.trip?.trip_id || null,
|
||||||
|
vehicleId: item.vehicle?.vehicle?.id || null
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
// Deduplicate by trip_id + arrival_time
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const uniqueUpcoming = upcoming.filter(item => {
|
const unique = [];
|
||||||
const key = `${item.trip.trip_id}_${item.arrivalDate.getTime()}`;
|
|
||||||
if (seen.has(key)) return false;
|
|
||||||
seen.add(key);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uniqueUpcoming.length === 0) {
|
for (const p of parsed) {
|
||||||
|
if (p.tripId && seen.has(`T${p.tripId}`)) continue;
|
||||||
|
|
||||||
|
const key2 = `${p.route}|${p.headsign}|${p.time}`;
|
||||||
|
if (seen.has(key2)) continue;
|
||||||
|
|
||||||
|
const key3 = p.vehicleId ? `V${p.vehicleId}|${p.time}` : null;
|
||||||
|
if (key3 && seen.has(key3)) continue;
|
||||||
|
|
||||||
|
if (p.tripId) seen.add(`T${p.tripId}`);
|
||||||
|
seen.add(key2);
|
||||||
|
if (key3) seen.add(key3);
|
||||||
|
|
||||||
|
unique.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
unique.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
if (unique.length === 0) {
|
||||||
scheduleDiv.innerHTML = `<p>No upcoming arrivals for <strong>${stopName}</strong></p>`;
|
scheduleDiv.innerHTML = `<p>No upcoming arrivals for <strong>${stopName}</strong></p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build table
|
|
||||||
let html = `<h3>Upcoming at ${stopName}</h3>`;
|
let html = `<h3>Upcoming at ${stopName}</h3>`;
|
||||||
html += `<table style="width:100%; border-collapse:collapse; font-size:0.95em;">
|
html += `<table style="width:100%; border-collapse:collapse; font-size:0.95em;">
|
||||||
<thead style="background:#333; color:white;">
|
<thead style="background:#333; color:white;">
|
||||||
<tr>
|
<tr><th style="padding:8px;">Route</th><th style="padding:8px;">Destination</th><th style="padding:8px;">Arrival</th></tr>
|
||||||
<th style="padding:8px;">Route</th>
|
</thead><tbody>`;
|
||||||
<th style="padding:8px;">Destination</th>
|
|
||||||
<th style="padding:8px;">Arrival</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>`;
|
|
||||||
|
|
||||||
uniqueUpcoming.forEach(item => {
|
unique.forEach(p => {
|
||||||
const { stopTime, trip, route, arrivalDate } = item;
|
const mins = Math.round((p.time - now.getTime()) / 60000);
|
||||||
const routeNum = route?.route_short_name || route?.route_id || "—";
|
const timeStr = new Date(p.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
const headsign = trip?.trip_headsign || "—";
|
|
||||||
|
|
||||||
const minutesUntil = Math.round((arrivalDate - now) / 60000);
|
const timeDisplay = mins <= 30
|
||||||
const displayTime = arrivalDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
? `<strong style="color:#d00;">${timeStr} (${mins === 0 ? "Due" : mins + " min"})</strong>`
|
||||||
|
: timeStr;
|
||||||
|
|
||||||
const timeDisplay = minutesUntil <= 30
|
const soon = mins <= 5 ? ' <small style="opacity:0.8;">(soon)</small>' : '';
|
||||||
? `<strong style="color:#d00;">${displayTime} (${minutesUntil === 0 ? "Due" : minutesUntil + " min"})</strong>`
|
|
||||||
: displayTime;
|
|
||||||
|
|
||||||
const rowColor = route?.route_color && route.route_color !== "000000"
|
const originalItem = data.find(d =>
|
||||||
? `#${route.route_color}`
|
d.trip?.trip_id === p.tripId ||
|
||||||
: "#666";
|
(d.route?.route_short_name === p.route && (d.trip?.trip_headsign || "").trim() === p.headsign)
|
||||||
|
);
|
||||||
|
const color = originalItem?.route?.route_color && originalItem.route.route_color !== "000000"
|
||||||
|
? `#${originalItem.route.route_color}` : "#666";
|
||||||
|
|
||||||
html += `<tr style="border-bottom:1px solid #ddd;">
|
html += `<tr style="border-bottom:1px solid #ddd;">
|
||||||
<td style="padding:6px; text-align:center; background:${rowColor}; color:${getContrastColor(rowColor)}; font-weight:bold;">
|
<td style="padding:6px; text-align:center; background:${color}; color:${getContrastColor(color)}; font-weight:bold;">
|
||||||
${routeNum}
|
${p.route}${soon}
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:6px;">${headsign}</td>
|
<td style="padding:6px;">${p.headsign || "—"}</td>
|
||||||
<td style="padding:6px; text-align:center;">${timeDisplay}</td>
|
<td style="padding:6px; text-align:center;">${timeDisplay}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `</tbody></table>`;
|
html += `</tbody></table>`;
|
||||||
html += `<small style="color:#666; display:block; margin-top:8px;">
|
html += `<small style="color:#666; display:block; margin-top:8px;">
|
||||||
Updated ${now.toLocaleTimeString()}
|
Updated ${now.toLocaleTimeString()} • All duplicates removed
|
||||||
</small>`;
|
</small>`;
|
||||||
|
|
||||||
scheduleDiv.innerHTML = html;
|
scheduleDiv.innerHTML = html;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching stop schedule:", err);
|
console.error(err);
|
||||||
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
|
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getContrastColor(hex) {
|
function getContrastColor(hex) {
|
||||||
if (!hex || hex.length < 6) return "#ffffff";
|
if (!hex || hex.length < 6) return "#ffffff";
|
||||||
const r = parseInt(hex.substr(0, 2), 16);
|
const r = parseInt(hex.substr(0, 2), 16);
|
||||||
@@ -195,16 +193,20 @@ async function updateVehicles() {
|
|||||||
let trains = [];
|
let trains = [];
|
||||||
let buses = [];
|
let buses = [];
|
||||||
|
|
||||||
if (!currentRoute || ["701", "703", "704", "720", "750"].includes(currentRoute)) {
|
|
||||||
const trainRes = currentRoute
|
|
||||||
? await fetch(`${API}/train/${currentRoute}`)
|
|
||||||
: await fetch(`${API}/train`);
|
|
||||||
trains = await trainRes.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentRoute) {
|
if (!currentRoute) {
|
||||||
const busRes = await fetch(`${API}/bus`);
|
const busRes = await fetch(`${API}/bus`);
|
||||||
buses = await busRes.json();
|
buses = await busRes.json();
|
||||||
|
const trainRes = await fetch(`${API}/train`);
|
||||||
|
trains = await trainRes.json();
|
||||||
|
} else if (currentRoute == "LRTC") {
|
||||||
|
const trainRes = await fetch(`${API}/train`);
|
||||||
|
trains = await trainRes.json();
|
||||||
|
} else if (["701", "703", "704", "720", "750"].includes(currentRoute)) {
|
||||||
|
const trainRes = await fetch(`${API}/train/${currentRoute}`);
|
||||||
|
trains = await trainRes.json();
|
||||||
|
} else {
|
||||||
|
const busRes = await fetch(`${API}/bus/${currentRoute}`);
|
||||||
|
buses = await busRes.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
const allVehicles = [...trains, ...buses];
|
const allVehicles = [...trains, ...buses];
|
||||||
@@ -244,65 +246,70 @@ async function updateVehicles() {
|
|||||||
vehicleMarkersMap[id] = marker;
|
vehicleMarkersMap[id] = marker;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(vehicleMarkersMap).forEach(id => {
|
Object.keys(vehicleMarkersMap).forEach(id => {
|
||||||
if (!seenVehicleIds.has(id)) {
|
if (!seenVehicleIds.has(id)) {
|
||||||
vehicleMarkers.removeLayer(vehicleMarkersMap[id]);
|
vehicleMarkers.removeLayer(vehicleMarkersMap[id]);
|
||||||
delete vehicleMarkersMap[id];
|
delete vehicleMarkersMap[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("status").textContent = `${allVehicles.length} vehicles • ${new Date().toLocaleTimeString()}`;
|
document.getElementById("status").textContent = `${allVehicles.length} vehicles • ${new Date().toLocaleTimeString()}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Update failed:", err);
|
console.error("Update failed:", err);
|
||||||
document.getElementById("status").textContent = "Error";
|
document.getElementById("status").textContent = "Error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRouteShape(routeId) {
|
async function loadRouteShape(routeId) {
|
||||||
if (!routeId) {
|
if (!routeId) return;
|
||||||
routeShapes.clearLayers();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API}/shape/${routeId}`);
|
const res = await fetch(`${API}/shape/${routeId}`);
|
||||||
const shapes = await res.json();
|
const shapes = await res.json();
|
||||||
routeShapes.clearLayers();
|
|
||||||
const res2 = await fetch(`${API}/routes`);
|
const res2 = await fetch(`${API}/routes`);
|
||||||
const routes = await res2.json();
|
const routes = await res2.json();
|
||||||
const route = routes.filter(r => r.route_short_name == routeId);
|
const route = routes.find(r => r.route_short_name == routeId);
|
||||||
console.log(route[0])
|
const routeHexColor = route?.route_color ? `#${route.route_color}` : "#888";
|
||||||
const routeHexColor = `#${route[0].route_color}`;
|
|
||||||
|
|
||||||
shapes.forEach(shape => {
|
shapes.forEach(shape => {
|
||||||
const points = shape.points.map(p => [p.lat, p.lon]);
|
const points = shape.points.map(p => [p.lat, p.lon]);
|
||||||
L.polyline(points, { color: routeHexColor, weight: 5, opacity: 0.7 }).addTo(routeShapes);
|
L.polyline(points, { color: routeHexColor, weight: 5, opacity: 0.7 }).addTo(routeShapes);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shapes.length > 0) {
|
if (shapes.length > 0) {
|
||||||
const bounds = L.latLngBounds(shapes[0].points.map(p => [p.lat, p.lon]));
|
const bounds = L.latLngBounds(shapes[0].points.map(p => [p.lat, p.lon]));
|
||||||
map.fitBounds(bounds, { padding: [50, 50] });
|
map.fitBounds(bounds, { padding: [50, 50] });
|
||||||
}
|
}
|
||||||
} catch (e) { console.error("Shape load error:", e); }
|
} catch (e) {
|
||||||
|
console.error("Shape load error:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.getElementById("routeSelect").addEventListener("change", async (e) => {
|
document.getElementById("routeSelect").addEventListener("change", async (e) => {
|
||||||
currentRoute = e.target.value;
|
currentRoute = e.target.value;
|
||||||
console.log(currentRoute == "")
|
console.log("Selected route:", currentRoute);
|
||||||
if (currentRoute === "") {
|
|
||||||
loadAllRoutes();
|
routeShapes.clearLayers();
|
||||||
loadStops();
|
stopMarkers.clearLayers();
|
||||||
|
|
||||||
|
if (!currentRoute) {
|
||||||
|
await loadAllRoutes();
|
||||||
|
await loadStops();
|
||||||
|
} else if (currentRoute === "LRTC") {
|
||||||
|
const combinedRoutes = ["750", "720", "704", "703", "701"];
|
||||||
|
for (const routeId of combinedRoutes) {
|
||||||
|
await loadRouteShape(routeId);
|
||||||
|
await loadStops(routeId);
|
||||||
}
|
}
|
||||||
else {
|
} else {
|
||||||
loadRouteShape(currentRoute);
|
await loadRouteShape(currentRoute);
|
||||||
loadStops(currentRoute);
|
await loadStops(currentRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVehicles();
|
updateVehicles();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function delay(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("refresh").addEventListener("click", updateVehicles);
|
|
||||||
|
|
||||||
async function loadAllRoutes() {
|
async function loadAllRoutes() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API}/routes`);
|
const res = await fetch(`${API}/routes`);
|
||||||
@@ -319,19 +326,9 @@ async function loadAllRoutes() {
|
|||||||
|
|
||||||
shapes.forEach(shape => {
|
shapes.forEach(shape => {
|
||||||
const points = shape.points.map(p => [p.lat, p.lon]);
|
const points = shape.points.map(p => [p.lat, p.lon]);
|
||||||
L.polyline(points, {
|
L.polyline(points, { color: routeHexColor, weight: 4, opacity: 0.6 }).addTo(routeShapes);
|
||||||
color: routeHexColor,
|
|
||||||
weight: 4,
|
|
||||||
opacity: 0.6
|
|
||||||
}).addTo(routeShapes);
|
|
||||||
});
|
});
|
||||||
//await delay(200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPoints = [];
|
|
||||||
routeShapes.eachLayer(layer => {
|
|
||||||
allPoints.push(...layer.getLatLngs());
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading all routes:", e);
|
console.error("Error loading all routes:", e);
|
||||||
}
|
}
|
||||||
@@ -350,17 +347,9 @@ async function showVehicleSchedule(vehicleId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = `
|
let html = `<h3>Schedule for Vehicle ${vehicleId}</h3>
|
||||||
<h3>Schedule for Vehicle ${vehicleId}</h3>
|
|
||||||
<table border="1" cellpadding="5" cellspacing="0">
|
<table border="1" cellpadding="5" cellspacing="0">
|
||||||
<thead>
|
<thead><tr><th>Stop</th><th>Arrival Time</th></tr></thead><tbody>`;
|
||||||
<tr>
|
|
||||||
<th>Stop</th>
|
|
||||||
<th>Arrival Time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
`;
|
|
||||||
|
|
||||||
schedule.forEach(stop => {
|
schedule.forEach(stop => {
|
||||||
const time = new Date(stop.arrival_time * 1000).toLocaleTimeString();
|
const time = new Date(stop.arrival_time * 1000).toLocaleTimeString();
|
||||||
@@ -369,20 +358,22 @@ async function showVehicleSchedule(vehicleId) {
|
|||||||
|
|
||||||
html += `</tbody></table>`;
|
html += `</tbody></table>`;
|
||||||
scheduleDiv.innerHTML = html;
|
scheduleDiv.innerHTML = html;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching schedule:", err);
|
console.error("Error fetching schedule:", err);
|
||||||
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
|
scheduleDiv.innerHTML = "<p>Error loading schedule.</p>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAllRoutes()
|
document.getElementById("refresh").addEventListener("click", updateVehicles);
|
||||||
.then(() => loadStops())
|
|
||||||
.then(() => {
|
(async function init() {
|
||||||
|
try {
|
||||||
|
await loadAllRoutes();
|
||||||
|
await loadStops();
|
||||||
updateVehicles();
|
updateVehicles();
|
||||||
setInterval(updateVehicles, 10000); // Update every 10 seconds
|
setInterval(updateVehicles, 10000);
|
||||||
})
|
} catch (err) {
|
||||||
.catch(err => {
|
|
||||||
console.error("Init failed:", err);
|
console.error("Init failed:", err);
|
||||||
document.getElementById("status").textContent = "Failed to initialize";
|
document.getElementById("status").textContent = "Failed to initialize";
|
||||||
});
|
}
|
||||||
|
})();
|
||||||
|
|||||||
29
server.js
29
server.js
@@ -1,7 +1,9 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const app = express()
|
const app = express()
|
||||||
const gtfsRealtime = require("./tools/gtfsData")
|
const gtfsRealtime = require("./tools/gtfsRedis")
|
||||||
|
const redisDAL = require("./tools/redisDAL");
|
||||||
|
|
||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', '*'); // or 'http://127.0.0.1:1001' for security
|
res.header('Access-Control-Allow-Origin', '*'); // or 'http://127.0.0.1:1001' for security
|
||||||
@@ -88,6 +90,29 @@ app.get('/api/stop/:routeId', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, async() => {
|
||||||
|
await redisDAL.connect()
|
||||||
|
await test();
|
||||||
console.log(`Example app listening on port ${port}`)
|
console.log(`Example app listening on port ${port}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async function test(){
|
||||||
|
console.log("stops")
|
||||||
|
await gtfsRealtime.getStops()
|
||||||
|
await gtfsRealtime.getStopsByRoute()
|
||||||
|
await gtfsRealtime.getRoutes()
|
||||||
|
console.log("trains")
|
||||||
|
await gtfsRealtime.getTrains()
|
||||||
|
await gtfsRealtime.getTrainsByRoute()
|
||||||
|
console.log("buses")
|
||||||
|
await gtfsRealtime.getBuses()
|
||||||
|
await gtfsRealtime.getBusesByRoute()
|
||||||
|
await gtfsRealtime.getShapeByRoute()
|
||||||
|
await console.log("shapes")
|
||||||
|
await gtfsRealtime.getScheduleByRoute()
|
||||||
|
await gtfsRealtime.getScheduleByStationId()
|
||||||
|
await gtfsRealtime.getAlerts()
|
||||||
|
await gtfsRealtime.getAlertsByRoute()
|
||||||
|
console.log("done")
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ async function loadGtfsStaticInMemory() {
|
|||||||
const gtfs = {};
|
const gtfs = {};
|
||||||
|
|
||||||
for (const entry of directory.files) {
|
for (const entry of directory.files) {
|
||||||
if (!entry.path.endsWith(".txt")) continue; // only GTFS .txt files
|
if (!entry.path.endsWith(".txt")) continue;
|
||||||
|
|
||||||
const fileBuffer = await entry.buffer();
|
const fileBuffer = await entry.buffer();
|
||||||
const text = fileBuffer.toString("utf8");
|
const text = fileBuffer.toString("utf8");
|
||||||
@@ -315,10 +315,6 @@ module.exports = {
|
|||||||
getScheduleByRoute,
|
getScheduleByRoute,
|
||||||
getScheduleByStationId,
|
getScheduleByStationId,
|
||||||
getShapeByRoute,
|
getShapeByRoute,
|
||||||
|
|
||||||
updateGtfsRt,
|
updateGtfsRt,
|
||||||
loadGtfsStaticInMemory,
|
loadGtfsStaticInMemory,
|
||||||
gtfs_rt_v,
|
|
||||||
gtfs_rt_t,
|
|
||||||
gtfs_rt_a,
|
|
||||||
}
|
}
|
||||||
|
|||||||
99
tools/gtfsRedis.js
Normal file
99
tools/gtfsRedis.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
const gtfs = require("./gtfsData");
|
||||||
|
const redis = require("./redisDAL");
|
||||||
|
|
||||||
|
const CACHE_PREFIX = "gtfs_cache";
|
||||||
|
const RT_TTL = 3;
|
||||||
|
const STATIC_TTL = 24 * 3600;
|
||||||
|
|
||||||
|
async function getCached(keySuffix, fetchFn, ttl) {
|
||||||
|
await redis.connect();
|
||||||
|
|
||||||
|
const cached = await redis.get(CACHE_PREFIX, { key: keySuffix });
|
||||||
|
if (cached !== null) return cached;
|
||||||
|
|
||||||
|
const data = await fetchFn();
|
||||||
|
await redis.set(CACHE_PREFIX, { key: keySuffix }, data, ttl);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrains() {
|
||||||
|
return getCached("trains", gtfs.getTrains, RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuses() {
|
||||||
|
return getCached("buses", gtfs.getBuses, RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTrainsByRoute(route) {
|
||||||
|
return getCached(`trains_route_${route}`, () => gtfs.getTrainsByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBusesByRoute(route) {
|
||||||
|
return getCached(`buses_route_${route}`, () => gtfs.getBusesByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlerts() {
|
||||||
|
return getCached("alerts", gtfs.getAlerts, RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlertsByRoute(route) {
|
||||||
|
return getCached(`alerts_route_${route}`, () => gtfs.getAlertsByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScheduleByRoute(route) {
|
||||||
|
return getCached(`schedule_route_${route}`, () => gtfs.getScheduleByRoute(route), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScheduleByStationId(stopId) {
|
||||||
|
return getCached(`schedule_stop_${stopId}`, () => gtfs.getScheduleByStationId(stopId), RT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoutes() {
|
||||||
|
return getCached("routes", gtfs.getRoutes, STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStops() {
|
||||||
|
return getCached("stops", gtfs.getStops, STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStopsByRoute(route) {
|
||||||
|
return getCached(`stops_route_${route}`, () => gtfs.getStopsByRoute(route), STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getShapeByRoute(route) {
|
||||||
|
return getCached(`shape_route_${route}`, () => gtfs.getShapeByRoute(route), STATIC_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGtfsRt() {
|
||||||
|
const data = await gtfs.updateGtfsRt();
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "trains" }, await gtfs.getTrains(), RT_TTL);
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "buses" }, await gtfs.getBuses(), RT_TTL);
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "alerts" }, await gtfs.getAlerts(), RT_TTL);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGtfsStaticInMemory() {
|
||||||
|
const data = await gtfs.loadGtfsStaticInMemory();
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "routes" }, data.routes || [], STATIC_TTL);
|
||||||
|
await redis.set(CACHE_PREFIX, { key: "stops" }, data.stops || [], STATIC_TTL);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { gtfs_rt_v, gtfs_rt_t, gtfs_rt_a } = gtfs;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getStops,
|
||||||
|
getStopsByRoute,
|
||||||
|
getRoutes,
|
||||||
|
getTrains,
|
||||||
|
getBuses,
|
||||||
|
getTrainsByRoute,
|
||||||
|
getBusesByRoute,
|
||||||
|
getAlerts,
|
||||||
|
getAlertsByRoute,
|
||||||
|
getScheduleByRoute,
|
||||||
|
getScheduleByStationId,
|
||||||
|
getShapeByRoute,
|
||||||
|
updateGtfsRt,
|
||||||
|
loadGtfsStaticInMemory,
|
||||||
|
};
|
||||||
45
tools/redisDAL.js
Normal file
45
tools/redisDAL.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const { createClient } = require("redis");
|
||||||
|
|
||||||
|
class RedisDAL {
|
||||||
|
constructor() {
|
||||||
|
this.client = createClient({ url:"redis://my-redis:6379" });
|
||||||
|
|
||||||
|
this.client.on("error", (err) => {
|
||||||
|
console.error("Redis Client Error", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (!this.client.isOpen) {
|
||||||
|
await this.client.connect();
|
||||||
|
console.log("Connected to Redis");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateKey(prefix, params) {
|
||||||
|
const paramString = Object.entries(params)
|
||||||
|
.sort()
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join("&");
|
||||||
|
return `${prefix}:${paramString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(prefix, params) {
|
||||||
|
const key = this.generateKey(prefix, params);
|
||||||
|
const cached = await this.client.get(key);
|
||||||
|
if (cached) return JSON.parse(cached);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(prefix, params, data, ttl = 3600) {
|
||||||
|
const key = this.generateKey(prefix, params);
|
||||||
|
await this.client.set(key, JSON.stringify(data), { EX: ttl });
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(prefix, params) {
|
||||||
|
const key = this.generateKey(prefix, params);
|
||||||
|
await this.client.del(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new RedisDAL();
|
||||||
Reference in New Issue
Block a user