Partially working api and web
This commit is contained in:
347
bus-tracker-json.ts
Normal file
347
bus-tracker-json.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user