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
#
Trigger
When it runs
How precise
Status
1
On-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 departure
High: follows that exact vehicle run and its stations
LIVE
2
General progress check (section 7)
Automatically every 30 s (15 s with Safety Mode), for all other legs: taxi, walk, estimated routes
Medium: GPS distance and speed
LIVE
3
"What happened?" button (section 8)
User taps it: Missed it / Delayed / Cancelled
Replans from the current position
LIVE
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)
Factor
How it's worked out
Adds
Get off / get on
By the next mode
bus/tram 1 · metro 1.5 · train/coach 2 · ferry 20 · flight 90 min
Walk between the stops
Real 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 crossings
Only 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 level
Underground (metro/subway) ↔ street level, or a train change at an interchange/hub (stairs, escalators, lifts)
+1.5 min
Platform distance
Platform 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 size
From timetable data (transport types, departures) + OpenStreetMap stops worldwide: simple stop / interchange / major hub
0 / +1 / +3 min
Rush hour
Local 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 leg
Minimum change time
Bus / tram / metro
2 min
Train
4 min
Ferry
20 min
Flight
90 min
Other
3 min
Situation
Result
Alternatives searched?
3 min or more to spare
✅ On track
No
0–3 min to spare
⏱ Tight change
Yes, as a safety net
Less than 0 (predicted miss)
🚨 You'll miss it
Yes
Connection cancelled (live)
🚫 Cancelled
Yes
Connection itself running late (live)
Its later departure is used, so it may still be caught
Only if still tight/missed
Already past the planned change stop
🚨 You'll miss it
Yes
4. How the delay is known
Source
When used
How
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 timetable
Otherwise
The 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
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).
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.
Excluded: own car and own bike (someone on a tram doesn't have them).
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).
Each option shows how it compares with the original plan: (−3 min) / (+8 min).
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:
Not moving: after 3 min (or 20% of the leg) on a leg longer than 1 km, the user has moved less than 150 m.
Behind: at half the leg's planned time, less than half its distance has been covered.
Connection at risk: the ETA to the change point at current speed (with live traffic where available) means the next departure will be missed. Checked in absolute time and the stop's real time zone.
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)
Bug
Effect before
Now
Connection times read on the server's European clock
A 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 guess
Germany 1.5 h early, India 15–30 min off, New York 1 h off; the night-time walking check was shifted too
Real time zone of the location (offline geo-tz) ✅
Connection risk measured to the final destination
"Will I catch the bus?" measured the wrong distance
Measured to the change point ✅
Predicted missed connection ignored
Calculated, never acted on
Triggers a replan ✅
10. Tests
Real data: RNV tram 3, MA Hauptbahnhof Süd 20:00 → Freiheitsplatz 20:07. The rider was placed 2 stops behind schedule, with a bus 3 min after the tram's arrival. Delay detected 2 min (GPS), miss predicted 5 stops early, options from the next stations: taxi (20:16), bus (20:33), walk (21:02).
Decision cases: assumed-on-time miss ✅, tight 2 min ✅, comfortable 8 min ✅, connection itself 5 min late → catchable ✅, connection cancelled → alternatives ✅.
Screens in a browser: warning, options, "Use this" → new route (stay-on leg + taxi), no errors.
Live checks: Delhi 20 min read correctly; Mannheim taxi time = real Berlin time.
Change times at real stations: Frankfurt Hbf platform 9 → S-Bahn tief 14.3 min rush / 11.5 Sunday · Mannheim Hbf train → tram 8.6 · tram → bus same corner 3.5 · tram → bus across Paradeplatz 6.4 · NY Penn train → subway rush 12.5. Platform names parsed in German, English, French, Italian, Spanish, Polish/Dutch, Japanese, Chinese, Korean and Hindi. Rush hour checked for Berlin (weekday), Riyadh (Friday = weekend) and Delhi (later peak).
Live dataset: the production server computed and stored rows in transfer_time / station_profile.
Guard: stops placed 6.4 km apart (bad test coordinates) no longer use the station model and are never stored (0 rows over 1 km).
11. Limits & queued
#
Limit
Plan
1
While 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.
2
The 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.
3
Positions are compared with straight lines between stations (no road/track shapes yet).
Shapes are part of the TO-BE plan.
4
A connection without live data is assumed on time (the screen says so).
More GTFS-Realtime feeds (20 today).
5
No alert while the phone app is in the background.
NOT YET needs a native notification update.
6
Only one change ahead is checked (the next leg), not the whole chain.
Full time-dependent multi-leg search is the TO-BE target.
7
Change 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.
8
Walking 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).
9
Station 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.
10
The station model applies only to stops within one station area (≤ 800 m). Further apart, the simple rule is used (walk + minimum change time).