import express, { Request, Response } from 'express'; import GtfsRealtimeBindings from 'gtfs-realtime-bindings'; import { loadGtfsStops, loadGtfsRoutes } from './lib/gtfs'; import { config, StopConfig, RouteConfig } from './config'; import { initDatabase, logVehiclePosition, logVehiclePositions, logArrival, getDatabaseStats, cleanupOldData, getVehicleHistory, getRouteVehiclePositions, getStopArrivalHistory, getRouteDelayStats, getStopDelayStats, getRouteHourlyPattern } from './lib/database'; const app = express(); // Initialize database try { initDatabase(); console.log('Database initialized successfully'); } catch (error) { console.error('Failed to initialize database:', error); console.log('Continuing without data logging...'); } // Load GTFS data const stops = loadGtfsStops(); const routes = loadGtfsRoutes(); // Serve static files import * as path from 'path'; app.use(express.static(path.join(__dirname, 'public'))); // Serve OpenAPI specification app.get('/openapi.yaml', (req: Request, res: Response) => { res.sendFile(path.join(__dirname, 'openapi.yaml')); }); // API Endpoints app.get('/api/config', (req: Request, res: Response) => { const stopId = (req.query.stopId as string) || config.defaultStop.stopId; const routeId = (req.query.routeId as string) || config.defaultRoute.routeId; const stop = stops.get(stopId); const route = routes.get(routeId); res.json({ stop: { id: stopId, code: stop?.stop_code, name: stop?.stop_name, lat: stop?.stop_lat, lon: stop?.stop_lon, }, route: { id: routeId, shortName: route?.route_short_name, longName: route?.route_long_name, }, defaults: { stopId: config.defaultStop.stopId, routeId: config.defaultRoute.routeId, } }); }); app.get('/api/stops', (req: Request, res: Response) => { const stopsList = Array.from(stops.values()).map(stop => ({ id: stop.stop_id, code: stop.stop_code, name: stop.stop_name, lat: stop.stop_lat, lon: stop.stop_lon, })); res.json(stopsList); }); app.get('/api/routes', (req: Request, res: Response) => { const routesList = Array.from(routes.values()).map(route => ({ id: route.route_id, shortName: route.route_short_name, longName: route.route_long_name, })); res.json(routesList); }); app.get('/api/arrivals', async (req: Request, res: Response) => { try { const stopId = (req.query.stopId as string) || config.defaultStop.stopId; const routeId = (req.query.routeId as string) || config.defaultRoute.routeId; const stop = stops.get(stopId); if (!stop) { return res.status(404).json({ error: `Stop ${stopId} not found` }); } const radius = 50; const nearbyUrl = `${config.baseUrl}/transport/planner/stops/nearbyTimes?latitude=${stop.stop_lat}&longitude=${stop.stop_lon}&radius=${radius}`; const response = await fetch(nearbyUrl); const nearbyData = await response.json() as any[]; const now = new Date(); const arrivals: any[] = []; for (const stopData of nearbyData) { if (stopData.id.toString() !== stopId) continue; for (const pattern of stopData.patterns) { if (pattern.routeId.toString() !== routeId) continue; const routeInfo = routes.get(pattern.routeId.toString()); for (const stopTime of pattern.stopTimes) { 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); const minutesUntil = Math.floor((arrivalTime.getTime() - now.getTime()) / 60000); if (minutesUntil >= -2 && minutesUntil <= config.tracking.minutesAhead) { arrivals.push({ arrivalTime: arrivalTime.toISOString(), scheduledTime: scheduledTime.toISOString(), minutesUntil: minutesUntil, delaySeconds: stopTime.arrivalDelay, headsign: stopTime.headsign, isRealtime: stopTime.realtime, realtimeState: stopTime.realtimeState, }); // Log arrival to database (use scheduled time as primary timestamp to avoid duplicates) try { logArrival({ timestamp: scheduledTime.getTime(), stopId, routeId, scheduledTime: scheduledTime.getTime(), predictedTime: arrivalTime.getTime(), delaySeconds: stopTime.arrivalDelay, isRealtime: stopTime.realtime, headsign: stopTime.headsign, }); } catch (dbError) { // Fail silently if database is not available } } } } } arrivals.sort((a, b) => new Date(a.arrivalTime).getTime() - new Date(b.arrivalTime).getTime()); res.json(arrivals); } catch (error) { res.status(500).json({ error: 'Failed to fetch arrivals' }); } }); app.get('/api/vehicles', async (req: Request, res: Response) => { try { const routeId = (req.query.routeId as string) || config.defaultRoute.routeId; // Fetch all vehicles from JSON API const vehiclesResponse = await fetch(config.apiEndpoints.vehiclesJson); if (!vehiclesResponse.ok) { throw new Error(`HTTP error! status: ${vehiclesResponse.status}`); } const allVehicles = await vehiclesResponse.json() as any[]; // Fetch trip updates to find which vehicles are on our route const tripUpdatesResponse = await fetch(config.apiEndpoints.gtfsRtTripUpdates); if (!tripUpdatesResponse.ok) { console.warn('Could not fetch trip updates, returning all vehicles'); // Return all vehicles with basic info res.json(allVehicles.slice(0, 20).map((v: any) => ({ id: v.id, vehicleId: v.identificationNumber, label: v.inventoryNumber, lat: v.positionLatitude, lon: v.positionLongitude, bearing: v.positionBearing, speed: v.positionSpeed, timestamp: v.positionModifiedAt, tripId: '', currentStopSequence: 0, currentStatus: 'UNKNOWN', }))); return; } const buffer = await tripUpdatesResponse.arrayBuffer(); if (buffer.byteLength === 0) { console.warn('Empty trip updates feed'); res.json([]); return; } let feed; try { feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode( new Uint8Array(buffer) ); } catch (decodeError) { console.error('Failed to decode GTFS-RT feed:', decodeError); res.json([]); return; } // Find trip IDs and vehicle IDs for our route const routeVehicleIds = new Set(); const vehicleTripMap = new Map(); for (const entity of feed.entity) { if (!entity.tripUpdate) continue; const tripUpdate = entity.tripUpdate; // Filter by target route if (tripUpdate.trip?.routeId !== routeId) continue; // Get vehicle ID if available if (tripUpdate.vehicle?.id) { routeVehicleIds.add(tripUpdate.vehicle.id); vehicleTripMap.set(tripUpdate.vehicle.id, { tripId: tripUpdate.trip.tripId, routeId: tripUpdate.trip.routeId, }); } // Also try vehicle label if (tripUpdate.vehicle?.label) { routeVehicleIds.add(tripUpdate.vehicle.label); vehicleTripMap.set(tripUpdate.vehicle.label, { tripId: tripUpdate.trip.tripId, routeId: tripUpdate.trip.routeId, }); } } // Filter vehicles by route const activeVehicles: any[] = []; for (const vehicle of allVehicles) { // Try to match by identification number or inventory number const vehicleId = vehicle.identificationNumber || vehicle.inventoryNumber?.toString(); if (vehicleId && (routeVehicleIds.has(vehicleId) || routeVehicleIds.has(vehicle.inventoryNumber?.toString()))) { const tripInfo = vehicleTripMap.get(vehicleId) || vehicleTripMap.get(vehicle.inventoryNumber?.toString()); activeVehicles.push({ id: vehicle.id, vehicleId: vehicle.identificationNumber, label: vehicle.inventoryNumber, lat: vehicle.positionLatitude, lon: vehicle.positionLongitude, bearing: vehicle.positionBearing, speed: vehicle.positionSpeed, timestamp: vehicle.positionModifiedAt, tripId: tripInfo?.tripId || '', currentStopSequence: 0, currentStatus: vehicle.status === 2 ? 'IN_TRANSIT_TO' : 'UNKNOWN', }); } } console.log(`Found ${routeVehicleIds.size} vehicle IDs for route ${routeId}`); console.log(`Matched ${activeVehicles.length} vehicles from JSON API`); // Log vehicle positions to database try { const positions = activeVehicles.map(v => ({ timestamp: Date.now(), vehicleId: v.vehicleId || v.label, routeId, tripId: v.tripId, latitude: v.lat, longitude: v.lon, speed: v.speed, bearing: v.bearing, currentStatus: v.currentStatus, })); logVehiclePositions(positions); } catch (dbError) { // Fail silently if database is not available } res.json(activeVehicles); } catch (error) { console.error('Error fetching vehicles:', error); res.status(500).json({ error: 'Failed to fetch vehicles', details: String(error) }); } }); // Historical Data API Endpoints app.get('/api/stats/db', async (req: Request, res: Response) => { try { const stats = await getDatabaseStats(); res.json(stats); } catch (error) { res.status(500).json({ error: 'Database not available' }); } }); app.get('/api/history/vehicle/:vehicleId', async (req: Request, res: Response) => { try { const vehicleId = req.params.vehicleId; const hours = parseInt(req.query.hours as string) || 24; const endTime = Date.now(); const startTime = endTime - (hours * 60 * 60 * 1000); const history = await getVehicleHistory(vehicleId, startTime, endTime); res.json(history); } catch (error) { res.status(500).json({ error: 'Failed to fetch vehicle history' }); } }); app.get('/api/history/route/:routeId/vehicles', async (req: Request, res: Response) => { try { const routeId = req.params.routeId; const hours = parseInt(req.query.hours as string) || 24; const endTime = Date.now(); const startTime = endTime - (hours * 60 * 60 * 1000); const positions = await getRouteVehiclePositions(routeId, startTime, endTime); res.json(positions); } catch (error) { res.status(500).json({ error: 'Failed to fetch route vehicle history' }); } }); app.get('/api/history/stop/:stopId/arrivals', async (req: Request, res: Response) => { try { const stopId = req.params.stopId; const routeId = (req.query.routeId as string) || ''; const hours = parseInt(req.query.hours as string) || 24; const endTime = Date.now(); const startTime = endTime - (hours * 60 * 60 * 1000); const arrivals = await getStopArrivalHistory(stopId, routeId, startTime, endTime); res.json(arrivals); } catch (error) { res.status(500).json({ error: 'Failed to fetch arrival history' }); } }); app.get('/api/stats/route/:routeId/delays', async (req: Request, res: Response) => { try { const routeId = req.params.routeId; const hours = parseInt(req.query.hours as string) || 24; const stats = await getRouteDelayStats(routeId, hours); res.json(stats); } catch (error) { res.status(500).json({ error: 'Failed to fetch delay stats' }); } }); app.get('/api/stats/stop/:stopId/delays', async (req: Request, res: Response) => { try { const stopId = req.params.stopId; const hours = parseInt(req.query.hours as string) || 24; const stats = await getStopDelayStats(stopId, hours); res.json(stats); } catch (error) { res.status(500).json({ error: 'Failed to fetch delay stats' }); } }); app.get('/api/stats/route/:routeId/hourly', async (req: Request, res: Response) => { try { const routeId = req.params.routeId; const days = parseInt(req.query.days as string) || 7; const pattern = await getRouteHourlyPattern(routeId, days); res.json(pattern); } catch (error) { res.status(500).json({ error: 'Failed to fetch hourly pattern' }); } }); // Start server app.listen(config.server.port, () => { console.log(` =========================================================================== BUS TRACKER WEB APP =========================================================================== Server running at: http://localhost:${config.server.port} Pages: - Main Tracker: http://localhost:${config.server.port} - Analytics: http://localhost:${config.server.port}/analytics.html - API Docs: http://localhost:${config.server.port}/api-docs.html Features: - Real-time bus arrivals and vehicle tracking - Interactive map interface - Historical analytics and delay statistics - Complete REST API with OpenAPI specification Press Ctrl+C to stop the server =========================================================================== `); });