🔄 Safe2Go — Rerouting Logic

← back to app

Admin Login

What this page is. Every way Safe2Go changes a journey while it is under way: how it detects that a connection will be missed, what it offers instead, and what the user sees. Written from server/services/onboardReroute.js, server/services/rePlanner.js, server/routes/journey.js and the tracking screen in public/index.html. Last updated 2026-09-26 (commits 1162e2f, 3b39cfe, e7d61c5, 86e4b8e). Also written from server/services/transferTime.js.
Status labels: LIVE working now · PARTIAL / QUEUED · NOT YET

1. Three ways a journey is rerouted

#TriggerWhen it runsHow preciseStatus
1On-board reroute (section 2–6)Automatically, about once a minute, while riding a real timetabled vehicle (tram, bus, metro, train) whose next leg is a scheduled departureHigh: follows that exact vehicle run and its stationsLIVE
2General progress check (section 7)Automatically every 30 s (15 s with Safety Mode), for all other legs: taxi, walk, estimated routesMedium: GPS distance and speedLIVE
3"What happened?" button (section 8)User taps it: Missed it / Delayed / CancelledReplans from the current positionLIVE
All automatic checks are predictive: they act when the app works out that a connection will be missed, not after it has been.

2. On-board reroute — the flow

flowchart TD
  A["Riding: tram / bus / metro / train (real timetable trip)
next leg = scheduled departure, e.g. Bus 45 at 20:10"] --> B["Where is the vehicle on its trip, how late?
operator live delay OR user GPS vs that trip's own timetable"] B --> C["Predicted arrival at the change stop
= scheduled arrival + delay"] C --> D["Connection's own status
live delay / cancelled (if published)
otherwise assumed on time"] D --> X["Realistic change time at this stop
walk route · street crossings · change of level ·
platform distance · station size · rush hour
(transfer_time dataset)"] X --> E["Spare time = connection departure − predicted arrival
− realistic change time"] E --> F{"Spare time?"} F -- "3 min or more" --> OK["✅ On track"] F -- "0–3 min" --> T["⏱ Tight change + safer options"] F -- "below 0, or connection cancelled" --> M["🚨 You'll miss it (or 🚫 cancelled)"] T --> S M --> S["Stations still ahead: next 1–3 + planned change stop
with the time the vehicle reaches each"] S --> P["From EACH station, at THAT time:
full route search — subway / bus / tram / cab / walk / next bus
(own car & bike excluded)"] P --> R["Rank by arrival at destination
varied: best public transport · best cab · best walk"] R --> U["'Get off at X (next stop, ~20:05) → Taxi → arrive 20:16' [Use this]"] U --> N["Journey switches to: stay on to X → new legs
Safety Mode follows the new route"]

3. The decision

Spare time = connection departure (including its own live delay) − predicted arrival at the change stop − realistic change time at that stop (below).

Realistic change time — worldwide (added 2026-09-26)

FactorHow it's worked outAdds
Get off / get onBy the next modebus/tram 1 · metro 1.5 · train/coach 2 · ferry 20 · flight 90 min
Walk between the stopsReal walking route (self-hosted Valhalla, on foot) in its 8 regions; elsewhere straight line × 1.35 at 4.5 km/h, marked "estimated"walking time
Street crossingsOnly when a street-level stop (bus/tram/taxi) is involved: 1 for a normal walk, 2 for a long one with turns+1 min each
Change of levelUnderground (metro/subway) ↔ street level, or a train change at an interchange/hub (stairs, escalators, lifts)+1.5 min
Platform distancePlatform numbers read from stop names in many languages (Gleis, Bstg, Platform, Bay, Voie, Binario, Andén, Peron, 番線, 站台, 승강장, प्लेटफॉर्म…)+1 min, +0.25 per platform apart (max 3)
Station sizeFrom timetable data (transport types, departures) + OpenStreetMap stops worldwide: simple stop / interchange / major hub0 / +1 / +3 min
Rush hourLocal weekday peaks in the station's own time zone (07:00–09:30 & 16:30–19:00; South Asia 08:30–11:00 & 17:00–20:30), local weekends (Fri–Sat in much of the Middle East, Fri in Iran)× 1.1 / 1.2 / 1.3 by station size

Examples (real stations): Frankfurt Hbf platform 9 → underground S-Bahn 14.3 min rush hour / 11.5 Sunday · Mannheim Hbf train → tram outside 8.6 · tram → bus same corner 3.5 · tram → bus across Paradeplatz 6.4 · NY Penn train → subway rush 12.5.

Stored in its own dataset: tables station_profile and transfer_time (reused 30 days). The screen shows the reason, e.g. "Changing here takes ~14 min (walk 169 m + change of level + platform 9 → 101 + large station + rush hour)".

Queued (Phase 2, after the Europe import): import stations' platform / parent-station details and the operators' own published change times (transfers.txt, pathways.txt) where they exist. Those override the estimates.

Minimum change time (fallback only, if the estimate is unavailable)

Next legMinimum change time
Bus / tram / metro2 min
Train4 min
Ferry20 min
Flight90 min
Other3 min
SituationResultAlternatives searched?
3 min or more to spare✅ On trackNo
0–3 min to spare⏱ Tight changeYes, as a safety net
Less than 0 (predicted miss)🚨 You'll miss itYes
Connection cancelled (live)🚫 CancelledYes
Connection itself running late (live)Its later departure is used, so it may still be caughtOnly if still tight/missed
Already past the planned change stop🚨 You'll miss itYes

4. How the delay is known

SourceWhen usedHow
Operator live data (GTFS-Realtime)When the operator publishes it (20 feeds today)Delay for that exact trip; cancellations / skipped stops
User's GPS vs the trip's timetableOtherwiseThe GPS position is placed on the trip between two stations. The time the timetable says the vehicle should be there is compared with now; the difference is the delay (capped −5 to +120 min).
If the user is more than 1.5 km from the vehicle's route, the check stops ("You don't appear to be on this vehicle's route") rather than guessing.

5. Finding alternatives

  1. Candidate stations: the next 1–3 stations still ahead on the vehicle's trip, plus the planned change stop, with the time the vehicle reaches each (timetable + delay).
  2. From each station, at that time (+1 min to get off): the full Safe2Go route search to the destination, covering real timetables (subway, tram, bus, including the next bus on the same line), cab/taxi, walking and estimated transit.
  3. Excluded: own car and own bike (someone on a tram doesn't have them).
  4. Ranking: by arrival time at the destination. The top 3 are varied: the fastest overall, then the fastest of each other kind (public transport / cab / walk).
  5. Each option shows how it compares with the original plan: (−3 min) / (+8 min).
  6. Results are cached for 60 s per station and time, so repeated checks stay fast.

6. What the user sees

✅ On track for the Bus 45 at 20:10 — your ride is on time; the Bus 45 is assumed on time (no live data for it).
⏱ Tight change for the Bus 45 at 20:14 — about 2 min to spare. Your ride is running ~2 min late (estimated from your GPS)… 🛟 Safer options, in case you'd rather not risk it: …
🚨 You'll miss the Bus 45 at 20:10. Your ride is running ~2 min late; you'd reach Freiheitsplatz ~20:09.
🔄 Faster ways from here:
🚕 Get off at Windeckstraße — the next stop, ~20:05 → then Taxi / Cab → arrive 20:16 (−9 min) [Use this]
🚇 Get off at Windeckstraße → then Walking + Bus (estimated) → arrive 20:33 (+8 min) [Use this]
🚫 The Bus 45 at 20:20 is cancelled … 🔄 Faster ways from here: …
🚨 You'll miss the Train S8 at 08:40. Your ride is running ~3 min late; you'd reach Frankfurt Hbf ~08:31. The Train S8 is assumed on time (no live data for it). Changing here takes ~14 min (2 min to get off/on + 2 min walk (169 m) + change of level (stairs/escalator) + platform 9 → 101 + large station + rush hour).

Every warning and every "On track" message states the change time at that stop and why (walk, crossings, level change, platforms, station size, rush hour).

✅ On track for the Bus 45 at 20:16 — … the Bus 45 is itself running 5 min late (live) — now leaves 20:16.

"Use this" switches the journey to "stay on to Windeckstraße → Taxi", starting from the current position. Tracking restarts on the new route, and Safety Mode (if on) follows it. A predicted miss beeps and vibrates (Android); a tight change beeps once.

7. General progress check (legs without a timetable trip)

POST /api/journey/replan, every 30 s (15 s with Safety Mode). A new plan from the current position is offered when: The screen shows "⚠️ …" with up to 3 alternatives and [Use Route]. If Safety Mode is on, a check also starts the "Are you safe?" prompt.

8. "What happened?" button

On the tracking screen the user can tap ❌ Missed it, ⏳ Delayed or 🚫 Cancelled. The app replans from the current position (POST /api/journey/disruption) and shows up to 3 alternatives. For "Cancelled", routes that still use the same kind of transport are moved to the bottom.

9. Time-zone fixes (2026-09-26)

BugEffect beforeNow
Connection times read on the server's European clockA Delhi bus in 20 min was read as 349 min → "on track"Absolute times + the stop's time zone: 20 min ✅
Route times from a longitude guessGermany 1.5 h early, India 15–30 min off, New York 1 h off; the night-time walking check was shifted tooReal time zone of the location (offline geo-tz) ✅
Connection risk measured to the final destination"Will I catch the bus?" measured the wrong distanceMeasured to the change point ✅
Predicted missed connection ignoredCalculated, never acted onTriggers a replan ✅

10. Tests

11. Limits & queued

#LimitPlan
1While a big OSM import runs, real-timetable searches can time out, so the options may be only cab / walk / estimated.QUEUED after the Europe import: later imports run at low disk priority.
2The on-board reroute needs the current leg to be a real timetable trip ("Live Schedule" routes). Estimated legs use the general check.Grows with timetable coverage.
3Positions are compared with straight lines between stations (no road/track shapes yet).Shapes are part of the TO-BE plan.
4A connection without live data is assumed on time (the screen says so).More GTFS-Realtime feeds (20 today).
5No alert while the phone app is in the background.NOT YET needs a native notification update.
6Only one change ahead is checked (the next leg), not the whole chain.Full time-dependent multi-leg search is the TO-BE target.
7Change times are estimates. There are no station floor plans; platform and parent-station details were dropped at the original timetable import; platforms are only known when they're in the stop name.QUEUED Phase 2 after Europe: re-import station details + operators' official change times (transfers.txt/pathways.txt), which then override the estimates.
8Walking routes come from our own Valhalla only in its 8 regions (Germany, India, USA, Malaysia/Singapore/Brunei, Australia, Canada, Africa, Europe); elsewhere straight line × 1.35, marked "estimated".More Valhalla regions (TO-BE #10).
9Station size thresholds were calibrated on a handful of stations (Frankfurt, Mannheim, New Delhi, NY Penn). Overlapping feeds inflate raw stop counts, so size uses transport types and departures instead.Refine with the Phase 2 station data.
10The station model applies only to stops within one station area (≤ 800 m). Further apart, the simple rule is used (walk + minimum change time).By design (guard against bad data).

12. Technical reference

PieceWhere
On-board reroute engineserver/services/onboardReroute.js → onboardCheck()
APIPOST /api/journey/onboard-check, POST /api/journey/replan, POST /api/journey/disruption (server/routes/journey.js)
Trip stations with absolute timesgetTripStops() in server/services/gtfsQuery.js (stop_times primary-key lookup)
Trip identity on legsleg.gtfs {feedId, tripId, serviceDayMs, boardSeq, alightSeq, …}, set in routePlanner.js
Live delaysgetTripUpdateDelay() in server/services/gtfsRealtime.js
General checkcheckOnTrack(), checkConnectionTiming() in server/services/rePlanner.js
Time zoneslocalOffsetMinutesAt() (geo-tz) in routePlanner.js
Realistic change timeserver/services/transferTime.js → estimateTransfer(), getStationProfile(), platformOf(), isRushHour()
Change-time datasetgtfs DB tables station_profile, transfer_time (schema server/db/transfer_schema.sql), reused 30 days; source = derived / operator / user_report
Walking route between stopsfetchWalkRoute() in server/services/valhallaRoute.js (Valhalla pedestrian)
Station size, worldwidecountOsmStopKindsNear() in server/services/osmQuery.js (OSM transit_stops) + GTFS route types/departures
Country (weekends, rush-hour pattern)getCountryIsoForCoordinate() in server/services/mobilityGraphQuery.js
ScreenautoCheckProgress(), _checkOnboard(), _renderOnboard(), applyOnboardOption() in public/index.html
Written from the source code and test runs on 2026-09-26. Update this page by hand when the rerouting logic changes.