Standalone transit vehicle simulator. Given a GTFS static feed (zip or extracted directory), it simulates vehicles following their scheduled routes and serves a GTFS-RT VehiclePositions feed over HTTP.
Designed as the sibling of DM (transit-detour-manager): the simulator produces
the real-time vehicle layer, while DM continues to emit TripUpdates /
TripModifications / Alerts. The detours.json format and the vendored
proto/gtfs-realtime.proto are shared and round-trip compatible with DM.
- Output is VehiclePositions only — no TripUpdates / Alerts / TripModifications.
- Vehicles follow prescribed routing only: the GTFS shape, or the stitched detour shape when a detour is active. No off-route deviation.
- Deterministic when a
--seedis provided: the same seed and clock produce identical trajectories.
npm install
npm run dev -- serve test/fixtures/gtfs-minimal --time 2026-07-15T12:00:00Or build and run the compiled CLI:
npm run build
npm start -- serve path/to/gtfs.zip --port 4100Open http://localhost:4100/health to confirm it is running.
gtfs-rt-simulator serve <gtfs.zip|dir> [options]
| Option | Description |
|---|---|
<gtfs.zip|dir> |
Path to a GTFS static feed, either a zip file or an extracted directory. |
--time <iso> |
As-of start time (ISO 8601). The simulation clock starts there and advances with wall time. Defaults to wall-clock now. Validated against the feed's calendar range; errors if outside it, warns if no service runs that day. |
--port <number> |
HTTP port (default 4100). |
--seed <number> |
Deterministic RNG seed. |
--config <path> |
Path to a JSON config file (see below). |
--detours <file> |
Path to a DM-compatible detours.json file (re-read when the file changes). |
{
"port": 4100,
"tickIntervalMs": 1000,
"seed": 42,
"speedNoise": { "enabled": true, "std": 0.1, "clamp": [0.7, 1.3] },
"dwellMs": 15000,
"delay": { "probability": 0.2, "minS": 30, "maxS": 120 }
}speedNoise— per-vehicle speed variation (normal distribution clamped toclamp).dwellMs— base dwell time at each stop.delay— random extra dwell on stop arrivals: probability0..1, uniform seconds within[minS, maxS]. All randomness flows through the seeded RNG.
| Method | Path | Description |
|---|---|---|
| GET | /gtfs-rt |
GTFS-RT VehiclePositions feed (application/x-protobuf). Cached ~15s. |
| GET | /gtfs-rt.json |
Same feed as JSON, for debugging. |
| GET | /health |
Vehicle count, uptime, and config summary. |
| GET | /api/vehicles |
All simulated vehicles. |
| GET | /api/vehicles/:id |
A single vehicle (404 if unknown). |
| POST | /api/vehicles/:id/hold |
Body { "seconds": 60 } — hold at the next stop. |
| POST | /api/vehicles/:id/release |
Release an active hold. |
| POST | /api/vehicles/:id/breakdown |
Body { "seconds": 60 } — freeze the vehicle in place. |
| POST | /api/vehicles/:id/speed |
Body { "factor": 0.8 } — rescale the vehicle's segment speeds. |
| POST | /api/vehicles/:id/teleport |
Body { "lat": .., "lon": .. } or { "distanceAlongShape": .. } — reposition on the current shape. |
| GET/PUT | /api/config |
Read / update runtime tunables (dwellMs, speedNoise, delay, seed). |
No authentication — this is a local tool.
A DM-compatible array of detour objects. Only fields used by the simulation are required; extra DM fields round-trip unchanged.
[
{
"id": "detour-1",
"routeId": "route-1",
"directionId": 0,
"startStopId": "stop-b",
"endStopId": "stop-d",
"replacementStops": [{ "stopId": "stop-c", "stopName": "Stop C", "lat": 41.882, "lon": -87.63, "travelTimeFromPrevious": 300 }],
"detourShape": [[41.881, -87.63], [41.883, -87.63]],
"startTime": "2026-07-15T00:00:00",
"endTime": "2026-07-15T23:59:59",
"description": "Road work on Main St",
"createdAt": "2026-07-01T00:00:00"
}
]A vehicle switches to the stitched detour shape when it is still before the
startStopId diverge point; if the detour is cleared or expires before the
vehicle reaches that point, it reverts to the original shape.
npm run dev # tsx watch
npm run build # tsc
npm start # node dist/cli.js
npm test # vitest (watch)
npm run test:run # vitest once
npm run test:coverage
npm run lint # eslint
npm run lint:fix
npm run format # prettier
npm run format:checkTests run fully offline against the committed fixture in
test/fixtures/gtfs-minimal (Vitest, test/unit). Verify = build + test:run +
lint all green.
src/gtfs/— GTFS static loading (zip or dir) into SQLite (better-sqlite3).src/sim/— simulation engine, seeded RNG (mulberry32), vehicle types. No HTTP, no fs.src/detours/—DetourSpectype plus file and in-memory providers.src/feed/— protobuf encode/decode and the VehiclePositions feed emitter.src/api/— express: feed endpoints plus the JSON control API.src/cli.ts— commander entry point.
Dependency direction: cli → api → {feed, sim} → {gtfs, detours}. The simulation
engine never imports the API or feed modules.