Files
openjsp/server.ts

261 lines
8.4 KiB
TypeScript

import express, { Request, Response } from 'express';
import GtfsRealtimeBindings from 'gtfs-realtime-bindings';
import { loadGtfsStops, loadGtfsRoutes } from './lib/gtfs';
import { config, StopConfig, RouteConfig } from './config';
const app = express();
// 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')));
// 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,
});
}
}
}
}
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<string>();
const vehicleTripMap = new Map<string, any>();
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`);
res.json(activeVehicles);
} catch (error) {
console.error('Error fetching vehicles:', error);
res.status(500).json({ error: 'Failed to fetch vehicles', details: String(error) });
}
});
// Start server
app.listen(config.server.port, () => {
console.log(`
===========================================================================
BUS TRACKER WEB APP
===========================================================================
Server running at: http://localhost:${config.server.port}
Open your browser and navigate to the URL above to view the map.
Features:
- Real-time bus arrivals
- Live vehicle locations on map
- Interactive map interface
Press Ctrl+C to stop the server
===========================================================================
`);
});