Problem
The bus timing row currently calculates the distance between the live bus coordinate and the queried bus stop as a straight line:
This can significantly underestimate the remaining distance when the service follows winding roads, loops, or travels away from the stop before returning.
The userLatLng property in BusTimingRow is also misleadingly named: both call sites pass the bus stop coordinate, not the user's location.
Constraint
Do not add a network request to the timing-screen hot path.
LTA arrivals are fetched directly from DataMall and refreshed every 20 seconds. Calling the Transito server in US-east—or a third-party routing API—when opening or refreshing the screen would add latency and another failure point.
Relevant DataMall behaviour
The LTA Bus Routes API exposes a cumulative Distance value for every stop:
Distance travelled by bus from starting location to this bus stop (in kilometres)
Source: LTA DataMall API User Guide — Bus Routes, page 22
The app already models this as BusRouteInfo.distance.
Proposed approach
Generate a compact route-distance index from the relatively static LTA BusRoutes and BusStops datasets:
service number + direction
→ ordered stops
→ stop code
→ latitude/longitude
→ cumulative distance from route origin
Make this data available locally:
- Bundle a baseline snapshot with the app so the first calculation requires no download.
- Optionally refresh and version it in the background, then cache it locally.
- Never block the timing screen on the refresh.
Runtime calculation
For each live arrival:
-
Select the correct route using the service number and the arrival’s OriginCode and DestinationCode.
-
Select the correct occurrence of the queried stop, accounting for VisitNumber and loop services.
-
Consider route segments before that target occurrence.
-
Find the closest plausible segment to the live bus coordinate.
-
Project the bus coordinate onto that segment and interpolate between the cumulative distances of its two stops.
-
Calculate:
remaining distance =
target stop cumulative distance − estimated bus cumulative distance
Only the bus’s current stop-to-stop segment is approximated. The remainder uses LTA’s cumulative route distance, which should be materially better than measuring directly to the destination stop.
Fallbacks and edge cases
Fall back to the existing straight-line calculation when:
- Latitude or longitude is missing or zero.
- No matching route or direction is available.
- The bus cannot be matched to a route segment with reasonable confidence.
- The calculated remaining distance is negative or otherwise invalid.
- The provider is not LTA, including NUS services until equivalent route data is supported.
Special cases to test:
- Direction 1 versus direction 2.
- Loop routes and repeated stops.
VisitNumber == 2.
- Short-working-trip and service variants.
- Routes whose outbound and inbound sections overlap.
- GPS jitter between 20-second refreshes.
- A bus at or immediately past the queried stop.
If matching is ambiguous, prefer showing the existing approximate value over confidently displaying an incorrect route distance.
Cleanup
- Rename
BusTimingRow.userLatLng to busStopLocation.
- Move the distance calculation out of the widget into a testable utility or service.
Acceptance criteria
- Opening or refreshing bus timings makes no additional network request for route distance.
- The direct LTA arrival request remains the only required live request for LTA services.
- Remaining distance follows the service route when local route data is available.
- Existing straight-line behaviour remains as a safe fallback.
- Loop routes, both directions, invalid GPS data, and missing route data are covered by tests.
- Distance generally decreases across successive monitored arrivals without large route-matching jumps.
Problem
The bus timing row currently calculates the distance between the live bus coordinate and the queried bus stop as a straight line:
BusTimingRow.calculateDistanceAway()nextBus.latitude/longitude→busStopLocationThis can significantly underestimate the remaining distance when the service follows winding roads, loops, or travels away from the stop before returning.
The
userLatLngproperty inBusTimingRowis also misleadingly named: both call sites pass the bus stop coordinate, not the user's location.Constraint
Do not add a network request to the timing-screen hot path.
LTA arrivals are fetched directly from DataMall and refreshed every 20 seconds. Calling the Transito server in US-east—or a third-party routing API—when opening or refreshing the screen would add latency and another failure point.
Relevant DataMall behaviour
The LTA Bus Routes API exposes a cumulative
Distancevalue for every stop:Source: LTA DataMall API User Guide — Bus Routes, page 22
The app already models this as
BusRouteInfo.distance.Proposed approach
Generate a compact route-distance index from the relatively static LTA
BusRoutesandBusStopsdatasets:Make this data available locally:
Runtime calculation
For each live arrival:
Select the correct route using the service number and the arrival’s
OriginCodeandDestinationCode.Select the correct occurrence of the queried stop, accounting for
VisitNumberand loop services.Consider route segments before that target occurrence.
Find the closest plausible segment to the live bus coordinate.
Project the bus coordinate onto that segment and interpolate between the cumulative distances of its two stops.
Calculate:
remaining distance =
target stop cumulative distance − estimated bus cumulative distance
Only the bus’s current stop-to-stop segment is approximated. The remainder uses LTA’s cumulative route distance, which should be materially better than measuring directly to the destination stop.
Fallbacks and edge cases
Fall back to the existing straight-line calculation when:
Special cases to test:
VisitNumber == 2.If matching is ambiguous, prefer showing the existing approximate value over confidently displaying an incorrect route distance.
Cleanup
BusTimingRow.userLatLngtobusStopLocation.Acceptance criteria