Skip to content

perf(prediction): ETA reads too long for vehicles physically near the stop (likely polyline loop / projection sanity) #225

Description

@ciotlosm

Background

Captured 2026-07-06 during field testing alongside #206. Two snapshots from the rider's device showed the prediction pipeline producing 7–8 minute ETAs for vehicles that physically have one or zero stops to go — i.e. a bus that should read "1–2 minutes".

Symptoms

  1. IMG_1056.PNG — vehicle ~1 stop away, live GPS, ETA reads "arriving in 8 min". Expected: 1–2 min given the speed config.
  2. IMG_1058.PNG — vehicle past the last station (geographically shown south of the user's stop) still appears in the drop-off section as "in 7 min". Expected: bucketed as departed (negative signed distance along the trip shape).

The drop-off vs arriving/incoming distinction means a fast rider would never board that vehicle anyway, but the displayed "arrives in N min" is the user-facing surface of the same bug: the ETA computation thinks the bus is somewhere it isn't.

Likely cause

predictArrivalAlongShape.ts uses signedDistM = stopProj.distAlongM - vehProj.distAlongM along the trip's polyline. When the polyline contains a terminal loop or a small post-terminus turnaround, a vehicle physically past the stop can project onto a polyline segment that lies before the stop in distAlongM terms — so the function reads a phantom future distance and times it against the time-of-day fallback (default kmh_offpeak: 25, "outskirts" tier 4 even lower).

The same loop distorts ETA in #1: the bus is close in haversine distance but the cumdistM walk winds through the loop and produces several minutes worth of time at the conservative TOD-offpeak speed.

Repro ingredients

  • An active trip shape whose tail end includes a small loop (the GTFS-RT vehicle.position lives south of the user's stop on the loop-back segment).
  • Per-feed _neary_config.timing.city_centre either absent or the stop outside the centre radius, so tier 3 / tier 4 wins instead of tier 1 (vehicle's own reported speedMs).

Proposed fix (not yet implemented)

Two layers, both pure-function:

(a) Distance sanity clamp. When signedDistM > haversineMeters(veh, stop) × SAFETY_MARGIN (SAFETY_MARGIN ~3), the polyline projection has wandered; fall back to the haversine distance with estimateSegmentSpeed directly.

(b) Tier-2 wiring. Pass nearbyVehicles (a snapshot of the current fleet with matching direction) into the speed cascade so a bus moving at 40 km/h within 1 km promotes tier 2 → tier 1 wins. Bigger lift — touches the worker pipeline.

I'd lean toward shipping (a) with a unit test that drives the "vehicle projects past due to wrap" scenario, then filing (b) separately once the cascade plumbing is ready.

Acceptance

  • Tests cover the projection-vs-haversine sanity check on a synthetic shape with a terminal loop.
  • Field re-test of IMG_1056 / IMG_1058 routes shows ETAs in the 1–2 min band when the vehicle is one stop away with active GPS.

Out of scope here: same idea generalises to map view (/map/route/[id]), which uses the same watchPosition but has a different render cadence. File a follow-up if the stall recurs on the map.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingperfPerformance / render pipeline

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions