Files
openjsp/bus-tracker-json.ts

348 lines
11 KiB
TypeScript

import { loadGtfsStops, loadGtfsRoutes, GtfsStop, GtfsRoute } from './lib/gtfs';
import { config } from './config';
// ============================================================================
// CLI Arguments
// ============================================================================
function parseArgs() {
const args = process.argv.slice(2);
let stopId = config.defaultStop.stopId;
let routeId = config.defaultRoute.routeId;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--stop' && args[i + 1]) {
stopId = args[i + 1];
i++;
} else if (args[i] === '--route' && args[i + 1]) {
routeId = args[i + 1];
i++;
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
Usage: npm run tracker [options]
Options:
--stop <stopId> Stop ID to track (default: ${config.defaultStop.stopId})
--route <routeId> Route ID to track (default: ${config.defaultRoute.routeId})
--help, -h Show this help message
Examples:
npm run tracker
npm run tracker -- --stop 1571 --route 125
`);
process.exit(0);
}
}
return { stopId, routeId };
}
const { stopId: TARGET_STOP_ID, routeId: TARGET_ROUTE_ID } = parseArgs();
// ============================================================================
// Configuration
// ============================================================================
// ============================================================================
// Additional Types
// ============================================================================
interface StopTime {
scheduledArrival: number;
scheduledDeparture: number;
realtimeArrival: number;
realtimeDeparture: number;
arrivalDelay: number;
departureDelay: number;
timepoint: boolean;
realtime: boolean;
realtimeState: string;
serviceDay: number;
headsign: string;
}
interface Pattern {
routeId: number;
index: number;
stopTimes: StopTime[];
}
interface StopArrivalData {
id: number;
patterns: Pattern[];
distance: number;
}
interface BusArrival {
tripId: string;
routeName: string;
stopName: string;
headsign: string;
arrivalTime: Date;
scheduledTime: Date;
delaySeconds: number;
minutesUntilArrival: number;
isApproaching: boolean;
realtimeState: string;
isRealtime: boolean;
}
// ============================================================================
// Real-time Data Fetching
// ============================================================================
async function getNextBuses(
stops: Map<string, GtfsStop>,
routes: Map<string, GtfsRoute>
): Promise<BusArrival[]> {
const targetStop = stops.get(TARGET_STOP_ID);
if (!targetStop) {
throw new Error(`Stop ${TARGET_STOP_ID} not found in GTFS data`);
}
// Fetch nearby arrivals using JSON API
const radius = 50; // 50 meters
const nearbyUrl = `${config.baseUrl}/transport/planner/stops/nearbyTimes?latitude=${targetStop.stop_lat}&longitude=${targetStop.stop_lon}&radius=${radius}`;
const response = await fetch(nearbyUrl);
const nearbyData = await response.json() as StopArrivalData[];
const now = new Date();
const maxTime = new Date(now.getTime() + config.tracking.minutesAhead * 60000);
const arrivals: BusArrival[] = [];
// Get route info
const targetRoute = routes.get(TARGET_ROUTE_ID);
// Process the nearby data
for (const stopData of nearbyData) {
if (stopData.id.toString() !== TARGET_STOP_ID) continue;
for (const pattern of stopData.patterns) {
// Filter by target route
if (pattern.routeId.toString() !== TARGET_ROUTE_ID) continue;
const routeInfo = routes.get(pattern.routeId.toString());
if (!routeInfo) continue;
for (const stopTime of pattern.stopTimes) {
// Convert service day + seconds to actual timestamp
const serviceDay = new Date(stopTime.serviceDay * 1000);
const arrivalTime = new Date(serviceDay.getTime() + stopTime.realtimeArrival * 1000);
const scheduledTime = new Date(serviceDay.getTime() + stopTime.scheduledArrival * 1000);
// Only include buses arriving within our time window
if (arrivalTime > maxTime || arrivalTime < now) continue;
const minutesUntil = Math.floor((arrivalTime.getTime() - now.getTime()) / 60000);
arrivals.push({
tripId: `${pattern.routeId}.${pattern.index}`,
routeName: `${routeInfo.route_short_name} - ${routeInfo.route_long_name}`,
stopName: targetStop.stop_name,
headsign: stopTime.headsign,
arrivalTime: arrivalTime,
scheduledTime: scheduledTime,
delaySeconds: stopTime.arrivalDelay,
minutesUntilArrival: minutesUntil,
isApproaching: minutesUntil <= 5 && minutesUntil >= 0,
realtimeState: stopTime.realtimeState,
isRealtime: stopTime.realtime,
});
}
}
}
// Sort by arrival time
arrivals.sort((a, b) => a.arrivalTime.getTime() - b.arrivalTime.getTime());
return arrivals;
}
// ============================================================================
// Display Functions
// ============================================================================
function formatArrival(arrival: BusArrival, index: number): string {
const delayMin = Math.floor(arrival.delaySeconds / 60);
let delayText = "";
if (delayMin > 0) {
delayText = ` [${delayMin}min LATE]`;
} else if (delayMin < -3) {
delayText = ` [${Math.abs(delayMin)}min EARLY]`;
} else {
delayText = " [ON TIME]";
}
const timeStr = arrival.arrivalTime.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
let status = "";
if (arrival.minutesUntilArrival <= 0) {
status = " >> ARRIVING NOW!";
} else if (arrival.isApproaching) {
status = " >> APPROACHING";
}
const minutesText = arrival.minutesUntilArrival === 0
? "NOW"
: `${arrival.minutesUntilArrival} min`;
const realtimeIndicator = arrival.isRealtime ? "[LIVE]" : "[SCHED]";
return ` ${index + 1}. ${timeStr} (in ${minutesText}) ${delayText} ${realtimeIndicator}${status}`;
}
async function displayBusSchedule(
stops: Map<string, GtfsStop>,
routes: Map<string, GtfsRoute>
) {
console.clear();
const targetStop = stops.get(TARGET_STOP_ID);
const targetRoute = routes.get(TARGET_ROUTE_ID);
console.log("=".repeat(75));
console.log(` BUS TRACKER - SKOPJE PUBLIC TRANSPORT`);
console.log("=".repeat(75));
console.log(` Route: ${targetRoute?.route_short_name} - ${targetRoute?.route_long_name}`);
console.log(` Stop: ${targetStop?.stop_name} (Code: ${targetStop?.stop_code})`);
console.log(` Location: ${targetStop?.stop_lat.toFixed(5)}, ${targetStop?.stop_lon.toFixed(5)}`);
console.log("=".repeat(75));
console.log(` Updated: ${new Date().toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})}\n`);
try {
const arrivals = await getNextBuses(stops, routes);
if (arrivals.length === 0) {
console.log(` No buses scheduled in the next ${config.tracking.minutesAhead} minutes.\n`);
console.log(" This could mean:");
console.log(" - No buses are currently running on this route");
console.log(" - The next bus is more than 90 minutes away");
console.log(" - Service has ended for the day\n");
return;
}
console.log(` Found ${arrivals.length} upcoming bus${arrivals.length > 1 ? 'es' : ''}:\n`);
// Show all buses (or first 10 if too many)
const showCount = Math.min(arrivals.length, 10);
arrivals.slice(0, showCount).forEach((arrival, index) => {
console.log(formatArrival(arrival, index));
if (arrival.headsign && index < 5) {
console.log(` Direction: ${arrival.headsign}`);
}
});
if (arrivals.length > showCount) {
console.log(`\n ... and ${arrivals.length - showCount} more later`);
}
// Highlight the next bus
const nextBus = arrivals[0];
if (nextBus) {
console.log("\n" + "-".repeat(75));
console.log(" NEXT BUS:");
console.log("-".repeat(75));
if (nextBus.minutesUntilArrival <= 0) {
console.log(" >> BUS IS ARRIVING NOW! HEAD TO THE STOP! <<");
} else if (nextBus.minutesUntilArrival <= 2) {
console.log(` >> Bus arriving in ${nextBus.minutesUntilArrival} minute${nextBus.minutesUntilArrival > 1 ? 's' : ''}! Run! <<`);
} else if (nextBus.minutesUntilArrival <= 5) {
console.log(` >> Bus arriving in ${nextBus.minutesUntilArrival} minutes - Time to head to the stop <<`);
} else if (nextBus.minutesUntilArrival <= 15) {
console.log(` >> ${nextBus.minutesUntilArrival} minutes - Start getting ready <<`);
} else {
console.log(` >> ${nextBus.minutesUntilArrival} minutes - You have time to relax <<`);
}
console.log(` Direction: ${nextBus.headsign}`);
console.log(` Data source: ${nextBus.isRealtime ? 'Real-time tracking' : 'Scheduled times'}`);
const delayMin = Math.floor(nextBus.delaySeconds / 60);
if (delayMin > 5) {
console.log(` WARNING: Bus is running ${delayMin} minutes LATE`);
} else if (delayMin < -3) {
console.log(` NOTE: Bus is ${Math.abs(delayMin)} minutes EARLY`);
}
}
} catch (error) {
console.log(`\n ERROR: ${error}\n`);
console.log(" Please check:");
console.log(" - Your internet connection");
console.log(" - The API is accessible");
console.log(" - The GTFS files are present in ./gtfs/ directory\n");
}
console.log("\n" + "=".repeat(75));
console.log(` Auto-refresh: Every ${config.tracking.refreshInterval.terminal / 1000} seconds`);
console.log(" Press Ctrl+C to stop");
console.log("=".repeat(75));
}
// ============================================================================
// Main Program
// ============================================================================
async function startMonitoring() {
console.log("Loading GTFS data...\n");
try {
const stops = loadGtfsStops();
const routes = loadGtfsRoutes();
console.log(`Loaded ${stops.size} stops`);
console.log(`Loaded ${routes.size} routes\n`);
// Verify target stop and route exist
const targetStop = stops.get(TARGET_STOP_ID);
const targetRoute = routes.get(TARGET_ROUTE_ID);
if (!targetStop) {
console.error(`ERROR: Stop ${TARGET_STOP_ID} not found in GTFS data`);
process.exit(1);
}
if (!targetRoute) {
console.error(`ERROR: Route ${TARGET_ROUTE_ID} not found in GTFS data`);
process.exit(1);
}
console.log("Configuration validated");
console.log("Starting bus monitor...\n");
// Initial display
await displayBusSchedule(stops, routes);
// Set up periodic refresh
setInterval(async () => {
await displayBusSchedule(stops, routes);
}, config.tracking.refreshInterval.terminal);
} catch (error) {
console.error(`Failed to start: ${error}`);
console.error("\nPlease ensure:");
console.error(" 1. GTFS files exist in ./gtfs/ directory");
console.error(" 2. Files include: stops.txt and routes.txt");
console.error(" 3. Node.js has permission to read the files");
process.exit(1);
}
}
// Start the application
startMonitoring();