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
- 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());
}
- 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.
- 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.
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 sameBlockInstance, andgetArrivalsAndDeparturesForStopInTimeRangefans them out into twoArrivalAndDepartureInstances for the sameStopTimeInstance(identical tripId / serviceDate / stopSequence,predicted=trueon both). Downstream this surfaces as duplicate arrivals inarrivals-and-departures-for-stopand duplicate vehicles invehicles-for-agency.Reproduction (live, Puget Sound, agency
1)arrivals-and-departures-for-stop/1_13330returned two route-12 entries for trip1_801565550, one withvehicleId=1_7099(real coach,lastLocationUpdateTime>0, reallastKnownLocation) and one withvehicleId=1_8110104.1_8110104is exactly the block id of trip1_801565550(verified viatrip-details). It haslastLocationUpdateTime==0and nolastKnownLocation.vehicles-for-agency/1showed ~117 such block-id phantoms simultaneously (5+ digit ids == block ids, alllastLocationUpdateTime==0), vs. 4-digit real coaches with real GPS.7099present,8110104absent); TripUpdates had 315/568 predictions with novehicle.id.Mechanism / relevant code
GtfsRealtimeTripLibrary.java:554-559VehicleLocationRecordCacheImplkeys_vehicleIdsByBlockInstance: BlockInstance -> Set<vehicleId>, so the block-id phantom and the real coach are two distinct entries on the same block.getLocationsForBlockInstance(BlockLocationServiceImpl.java:285) returns oneBlockLocationper cached vehicle;ArrivalAndDepartureServiceImpl.java:521then creates oneArrivalAndDepartureInstanceperBlockLocationfrom the sameStopTimeInstance. Nothing collapses the phantom against the real vehicle.Impact
arrivals-and-departuresresponses (visible in OBA apps as twin rows with the same route and ETA).vehicles-for-agencycounts (phantoms ≈ half the "vehicles" at times).Suggested fix
When both a block-id phantom (a
BlockLocationwhoseVehicleLocationRecordhastimeOfLocationUpdate==0/vehicleId == blockId, no real position) and a real-AVLBlockLocationare present for the sameBlockInstance, suppress the phantom — either ingetLocationsForBlockInstance(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 byStopTimeInstanceatapplyRealTimeToStopTimeInstancewhen one location is a schedule-only phantom.