Skip to content

Block-id "phantom" prediction and real AVL vehicle both fan out as separate arrivals on one block instance #469

Description

@bmander

Summary

When a trip has a real-time prediction ingested with no vehicle.id (an anonymous/schedule-based TripUpdate), the ingestion fallback assigns the block id as the vehicle id. If the real AVL vehicle for that block subsequently reports, both the block-id "phantom" and the real vehicle end up matched to the same BlockInstance, and getArrivalsAndDeparturesForStopInTimeRange fans them out into two ArrivalAndDepartureInstances for the same StopTimeInstance (identical tripId / serviceDate / stopSequence, predicted=true on both). Downstream this surfaces as duplicate arrivals in arrivals-and-departures-for-stop and duplicate vehicles in vehicles-for-agency.

Reproduction (live, Puget Sound, agency 1)

  • arrivals-and-departures-for-stop/1_13330 returned two route-12 entries for trip 1_801565550, one with vehicleId=1_7099 (real coach, lastLocationUpdateTime>0, real lastKnownLocation) and one with vehicleId=1_8110104.
  • 1_8110104 is exactly the block id of trip 1_801565550 (verified via trip-details). It has lastLocationUpdateTime==0 and no lastKnownLocation.
  • vehicles-for-agency/1 showed ~117 such block-id phantoms simultaneously (5+ digit ids == block ids, all lastLocationUpdateTime==0), vs. 4-digit real coaches with real GPS.
  • Upstream King County Metro feed confirms these ids are not real vehicles: VehiclePositions has only 4–5 digit coach numbers (7099 present, 8110104 absent); TripUpdates had 315/568 predictions with no vehicle.id.

Mechanism / relevant code

  1. Block-id fallback (mints the phantom vehicle id):
    GtfsRealtimeTripLibrary.java:554-559
    /** By default, we use the block id as the vehicle id */
    if (record.getVehicleId() == null) {
      record.setVehicleId(record.getBlockId());
    }
  2. Both records cached under one block instance: VehicleLocationRecordCacheImpl keys _vehicleIdsByBlockInstance: BlockInstance -> Set<vehicleId>, so the block-id phantom and the real coach are two distinct entries on the same block.
  3. Fan-out with no dedup: getLocationsForBlockInstance (BlockLocationServiceImpl.java:285) returns one BlockLocation per cached vehicle; ArrivalAndDepartureServiceImpl.java:521 then creates one ArrivalAndDepartureInstance per BlockLocation from the same StopTimeInstance. Nothing collapses the phantom against the real vehicle.

Impact

  • Duplicate arrivals in the REST / arrivals-and-departures responses (visible in OBA apps as twin rows with the same route and ETA).
  • Inflated vehicles-for-agency counts (phantoms ≈ half the "vehicles" at times).
  • Transient (phantom ages out once the prediction reattaches to the real vehicle), but recurs continuously across the fleet.

Suggested fix

When both a block-id phantom (a BlockLocation whose VehicleLocationRecord has timeOfLocationUpdate==0 / vehicleId == blockId, no real position) and a real-AVL BlockLocation are present for the same BlockInstance, suppress the phantom — either in getLocationsForBlockInstance (prefer real-AVL locations, drop a block-id-keyed location that duplicates a real one) or by reconciling the anonymous TripUpdate onto the real vehicle's record once it appears. Alternatively, dedup by StopTimeInstance at applyRealTimeToStopTimeInstance when one location is a schedule-only phantom.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions