183 lines
6.5 KiB
JavaScript
183 lines
6.5 KiB
JavaScript
// Route 5 bus monitor — parses GTFS-RT feed for real-time vehicle positions
|
|
// Usage: node monitor-route5.js
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const protobuf = require('/tmp/gtfs-rt/node_modules/protobufjs');
|
|
|
|
const FEED_URL = 'https://data.texas.gov/download/i5qp-g5fd/application/octet-stream';
|
|
const ROUTE_ID = '5';
|
|
const FIRST_STOP = '5854'; // Anderson/Northcross
|
|
const USER_STOP = '964'; // Woodrow/Choquette
|
|
const TRAVEL_SECS = 446; // 7 min 26 sec from first stop to user stop
|
|
|
|
// GTFS-RT proto definition (minimal, inline)
|
|
const PROTO = `
|
|
syntax = "proto2";
|
|
package transit_realtime;
|
|
|
|
message FeedMessage {
|
|
required FeedHeader header = 1;
|
|
repeated FeedEntity entity = 2;
|
|
}
|
|
message FeedHeader {
|
|
required string gtfs_realtime_version = 1;
|
|
optional uint64 timestamp = 2;
|
|
}
|
|
message FeedEntity {
|
|
required string id = 1;
|
|
optional TripUpdate trip_update = 3;
|
|
optional VehiclePosition vehicle = 4;
|
|
optional Alert alert = 5;
|
|
}
|
|
message TripUpdate {
|
|
optional TripDescriptor trip = 1;
|
|
optional VehicleDescriptor vehicle = 3;
|
|
repeated StopTimeUpdate stop_time_update = 2;
|
|
optional uint64 timestamp = 4;
|
|
message StopTimeUpdate {
|
|
optional uint32 stop_sequence = 1;
|
|
optional string stop_id = 4;
|
|
optional StopTimeEvent arrival = 2;
|
|
optional StopTimeEvent departure = 3;
|
|
enum ScheduleRelationship { SCHEDULED = 0; SKIPPED = 1; NO_DATA = 2; }
|
|
optional ScheduleRelationship schedule_relationship = 5;
|
|
}
|
|
}
|
|
message StopTimeEvent {
|
|
optional int32 delay = 1;
|
|
optional int64 time = 2;
|
|
optional int32 uncertainty = 3;
|
|
}
|
|
message VehiclePosition {
|
|
optional TripDescriptor trip = 1;
|
|
optional VehicleDescriptor vehicle = 8;
|
|
optional Position position = 2;
|
|
optional uint32 current_stop_sequence = 3;
|
|
optional string stop_id = 7;
|
|
enum VehicleStopStatus { INCOMING_AT = 0; STOPPED_AT = 1; IN_TRANSIT_TO = 2; }
|
|
optional VehicleStopStatus current_status = 4;
|
|
optional uint64 timestamp = 5;
|
|
enum CongestionLevel { UNKNOWN = 0; RUNNING_SMOOTHLY = 1; STOP_AND_GO = 2; CONGESTION = 3; SEVERE_CONGESTION = 4; }
|
|
optional CongestionLevel congestion_level = 6;
|
|
enum OccupancyStatus { EMPTY = 0; MANY_SEATS = 1; FEW_SEATS = 2; STANDING_ROOM = 3; CRUSHED = 4; FULL = 5; NOT_ACCEPTING = 6; }
|
|
optional OccupancyStatus occupancy_status = 9;
|
|
}
|
|
message TripDescriptor {
|
|
optional string trip_id = 1;
|
|
optional string route_id = 5;
|
|
optional uint32 direction_id = 6;
|
|
optional string start_time = 2;
|
|
optional string start_date = 3;
|
|
enum ScheduleRelationship { SCHEDULED = 0; ADDED = 1; UNSCHEDULED = 2; CANCELED = 3; }
|
|
optional ScheduleRelationship schedule_relationship = 4;
|
|
}
|
|
message VehicleDescriptor {
|
|
optional string id = 1;
|
|
optional string label = 2;
|
|
optional string license_plate = 3;
|
|
}
|
|
message Position {
|
|
required float latitude = 1;
|
|
required float longitude = 2;
|
|
optional float bearing = 3;
|
|
optional double odometer = 4;
|
|
optional float speed = 5;
|
|
}
|
|
message Alert {
|
|
repeated TimeRange active_period = 1;
|
|
repeated EntitySelector informed_entity = 5;
|
|
optional TranslatedString header_text = 10;
|
|
optional TranslatedString description_text = 11;
|
|
}
|
|
message TimeRange { optional uint64 start = 1; optional uint64 end = 2; }
|
|
message EntitySelector { optional string agency_id = 1; optional string route_id = 3; optional TripDescriptor trip = 4; optional string stop_id = 6; }
|
|
message TranslatedString { repeated Translation translation = 1; message Translation { required string text = 1; optional string language = 2; } }
|
|
`;
|
|
|
|
function fetch(url) {
|
|
return new Promise((resolve, reject) => {
|
|
https.get(url, (res) => {
|
|
const chunks = [];
|
|
res.on('data', c => chunks.push(c));
|
|
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
res.on('error', reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const root = protobuf.parse(PROTO, { keepCase: true }).root;
|
|
const FeedMessage = root.lookupType('transit_realtime.FeedMessage');
|
|
|
|
const buf = await fetch(FEED_URL);
|
|
const feed = FeedMessage.decode(buf);
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const cst = new Date((now - 6*3600) * 1000); // CST offset
|
|
const timeStr = cst.toISOString().replace('T', ' ').substring(0, 19) + ' CST';
|
|
|
|
// Filter Route 5 vehicles
|
|
const vehicles = feed.entity
|
|
.filter(e => e.vehicle && e.vehicle.trip && e.vehicle.trip.route_id === ROUTE_ID)
|
|
.map(e => {
|
|
const v = e.vehicle;
|
|
const age = now - (v.timestamp?.low || v.timestamp || 0);
|
|
const status = ['INCOMING_AT', 'STOPPED_AT', 'IN_TRANSIT_TO'][v.current_status] || 'UNKNOWN';
|
|
return {
|
|
vehicleId: v.vehicle?.label || v.vehicle?.id || 'unknown',
|
|
tripId: v.trip?.trip_id,
|
|
directionId: v.trip?.direction_id,
|
|
stopId: v.stop_id,
|
|
stopSequence: v.current_stop_sequence,
|
|
status,
|
|
lat: v.position?.latitude,
|
|
lon: v.position?.longitude,
|
|
speed: v.position?.speed,
|
|
bearing: v.position?.bearing,
|
|
ageSec: age,
|
|
timestamp: v.timestamp
|
|
};
|
|
});
|
|
|
|
// Filter Route 5 trip updates
|
|
const tripUpdates = feed.entity
|
|
.filter(e => e.trip_update && e.trip_update.trip && e.trip_update.trip.route_id === ROUTE_ID)
|
|
.map(e => {
|
|
const tu = e.trip_update;
|
|
const userStopUpdate = tu.stop_time_update?.find(s => s.stop_id === USER_STOP);
|
|
const firstStopUpdate = tu.stop_time_update?.find(s => s.stop_id === FIRST_STOP);
|
|
return {
|
|
tripId: tu.trip?.trip_id,
|
|
directionId: tu.trip?.direction_id,
|
|
vehicleId: tu.vehicle?.label || tu.vehicle?.id,
|
|
userStopDelay: userStopUpdate?.departure?.delay || userStopUpdate?.arrival?.delay || null,
|
|
userStopTime: userStopUpdate?.arrival?.time || userStopUpdate?.departure?.time || null,
|
|
firstStopDelay: firstStopUpdate?.departure?.delay || null,
|
|
firstStopTime: firstStopUpdate?.departure?.time || null,
|
|
totalStops: tu.stop_time_update?.length || 0
|
|
};
|
|
});
|
|
|
|
// Eastbound (direction 0) only
|
|
const ebVehicles = vehicles.filter(v => v.directionId === 0);
|
|
const ebUpdates = tripUpdates.filter(t => t.directionId === 0);
|
|
|
|
const result = {
|
|
timestamp: timeStr,
|
|
feedTimestamp: feed.header?.timestamp?.toString(),
|
|
route5_eastbound: {
|
|
activeVehicles: ebVehicles.length,
|
|
vehicles: ebVehicles,
|
|
tripUpdates: ebUpdates.filter(t => t.userStopDelay !== null || t.userStopTime !== null)
|
|
},
|
|
route5_all: {
|
|
totalVehicles: vehicles.length,
|
|
totalTripUpdates: tripUpdates.length
|
|
}
|
|
};
|
|
|
|
console.log(JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
main().catch(e => console.error(JSON.stringify({ error: e.message })));
|