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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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