Java tutorial
/** * Copyright (C) 2014 Kurt Raschke <kurt@kurtraschke.com> * Copyright (C) 2011 Google, Inc. * Copyright (C) 2015 University of South Florida * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onebusaway.transit_data_federation.impl.realtime.gtfs_realtime; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringUtils; import org.onebusaway.collections.MappingLibrary; import org.onebusaway.collections.Min; import org.onebusaway.geospatial.model.CoordinatePoint; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.calendar.ServiceDate; import org.onebusaway.gtfs.serialization.mappings.InvalidStopTimeException; import org.onebusaway.gtfs.serialization.mappings.StopTimeFieldMappingFactory; import org.onebusaway.realtime.api.EVehiclePhase; import org.onebusaway.realtime.api.TimepointPredictionRecord; import org.onebusaway.realtime.api.VehicleLocationRecord; import org.onebusaway.transit_data_federation.services.blocks.BlockCalendarService; import org.onebusaway.transit_data_federation.services.blocks.BlockGeospatialService; import org.onebusaway.transit_data_federation.services.blocks.BlockInstance; import org.onebusaway.transit_data_federation.services.blocks.ScheduledBlockLocation; import org.onebusaway.transit_data_federation.services.transit_graph.BlockConfigurationEntry; import org.onebusaway.transit_data_federation.services.transit_graph.BlockEntry; import org.onebusaway.transit_data_federation.services.transit_graph.BlockStopTimeEntry; import org.onebusaway.transit_data_federation.services.transit_graph.BlockTripEntry; import org.onebusaway.transit_data_federation.services.transit_graph.StopTimeEntry; import org.onebusaway.transit_data_federation.services.transit_graph.TripEntry; import org.onebusaway.util.SystemTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.transit.realtime.GtfsRealtime.FeedEntity; import com.google.transit.realtime.GtfsRealtime.FeedMessage; import com.google.transit.realtime.GtfsRealtime.Position; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; import com.google.transit.realtime.GtfsRealtime.VehiclePosition; import com.google.transit.realtime.GtfsRealtimeOneBusAway; import com.google.transit.realtime.GtfsRealtimeOneBusAway.OneBusAwayTripUpdate; public class GtfsRealtimeTripLibrary { private static final Logger _log = LoggerFactory.getLogger(GtfsRealtimeTripLibrary.class); private GtfsRealtimeEntitySource _entitySource; private BlockCalendarService _blockCalendarService; private BlockGeospatialService _blockGeospatialService; /** * This is primarily here to assist with unit testing. */ private long _currentTime = 0; private boolean _validateCurrentTime = true; public void setValidateCurrentTime(boolean validate) { _validateCurrentTime = validate; } private boolean validateCurrentTime() { return _validateCurrentTime; } private StopModificationStrategy _stopModificationStrategy = null; private boolean _scheduleAdherenceFromLocation = false; public void setEntitySource(GtfsRealtimeEntitySource entitySource) { _entitySource = entitySource; } public void setBlockCalendarService(BlockCalendarService blockCalendarService) { _blockCalendarService = blockCalendarService; } public long getCurrentTime() { return _currentTime; } public void setCurrentTime(long currentTime) { setCurrentTime(currentTime, 0); } public void setCurrentTime(long currentTime, int originOffsetHours) { if (originOffsetHours != 0) { Calendar c = Calendar.getInstance(); c.setTime(new Date(currentTime)); c.roll(Calendar.HOUR, originOffsetHours); _currentTime = c.getTimeInMillis(); _log.info("currentTime set to " + new Date(_currentTime) + " from offset " + originOffsetHours); } else { _currentTime = currentTime; } } public void setStopModificationStrategy(StopModificationStrategy strategy) { _stopModificationStrategy = strategy; } public void setScheduleAdherenceFromLocation(boolean scheduleAdherenceFromLocation) { _scheduleAdherenceFromLocation = scheduleAdherenceFromLocation; } public void setBlockGeospatialService(BlockGeospatialService blockGeospatialService) { _blockGeospatialService = blockGeospatialService; } /** * Trip updates describe a trip which is undertaken by a vehicle (which is * itself described in vehicle positions), but GTFS-realtime does not demand * that the two messages be related to each other. Where trip updates and * vehicle positions both contain a vehicle ID, we use those vehicle IDs to * join the messages together. * * Otherwise, where vehicle IDs are not provided, we join trip updates and * vehicle positions based on trip descriptors. If multiple trip updates * are provided for a block, they are all used, but cannot be mapped to * vehicle positions. * * @param tripUpdates * @param vehiclePositions * @return */ public List<CombinedTripUpdatesAndVehiclePosition> groupTripUpdatesAndVehiclePositions( FeedMessage tripUpdateMessage, FeedMessage vehiclePositionsMessage) { return groupTripUpdatesAndVehiclePositions(null, tripUpdateMessage, vehiclePositionsMessage); } public List<CombinedTripUpdatesAndVehiclePosition> groupTripUpdatesAndVehiclePositions(MonitoredResult result, FeedMessage tripUpdateMessage, FeedMessage vehiclePositionsMessage) { List<CombinedTripUpdatesAndVehiclePosition> updates = new ArrayList<CombinedTripUpdatesAndVehiclePosition>(); Map<String, TripUpdate> bestTripByVehicleId = new HashMap<String, TripUpdate>(); ListMultimap<String, TripUpdate> tripUpdatesByVehicleId = ArrayListMultimap.create(); Map<String, VehiclePosition> vehiclePositionsByVehicleId = new HashMap<String, VehiclePosition>(); ListMultimap<BlockDescriptor, TripUpdate> anonymousTripUpdatesByBlock = ArrayListMultimap .<BlockDescriptor, TripUpdate>create(); Map<BlockDescriptor, VehiclePosition> anonymousVehiclePositionsByBlock = new HashMap<BlockDescriptor, VehiclePosition>(); Set<BlockDescriptor> badAnonymousVehiclePositions = new HashSet<BlockDescriptor>(); for (FeedEntity fe : tripUpdateMessage.getEntityList()) { if (!fe.hasTripUpdate()) { continue; } TripUpdate tu = fe.getTripUpdate(); if (tu.hasVehicle() && tu.getVehicle().hasId() && StringUtils.isNotBlank(tu.getVehicle().getId())) { // Trip update has a vehicle ID - index by vehicle ID String vehicleId = tu.getVehicle().getId(); tripUpdatesByVehicleId.put(vehicleId, tu); if (!bestTripByVehicleId.containsKey(vehicleId)) { bestTripByVehicleId.put(vehicleId, tu); } else { // upcoming merge will fix this _log.debug("Multiple TripUpdates for vehicle {}; taking best.", vehicleId); if (tripMoreAppropriate(tu, bestTripByVehicleId.get(vehicleId), vehicleId)) { bestTripByVehicleId.put(vehicleId, tu); } } } else { /* * Trip update does not have a vehicle ID - index by TripDescriptor * (includes start date and time). */ TripDescriptor td = tu.getTrip(); long time = tu.hasTimestamp() ? tu.getTimestamp() * 1000 : currentTime(); BlockDescriptor bd = getTripDescriptorAsBlockDescriptor(result, td, time); if (bd == null) { continue; } if (!anonymousTripUpdatesByBlock.containsKey(bd)) { anonymousTripUpdatesByBlock.put(bd, tu); } else { _log.warn("Multiple anonymous TripUpdates for trip {}; will not map to VehiclePosition.", td.getTripId()); anonymousTripUpdatesByBlock.put(bd, tu); } } } for (FeedEntity fe : vehiclePositionsMessage.getEntityList()) { if (!fe.hasVehicle()) { continue; } VehiclePosition vp = fe.getVehicle(); if (vp.hasVehicle() && vp.getVehicle().hasId()) { // Vehicle position has a vehicle ID - index by vehicle ID String vehicleId = vp.getVehicle().getId(); if (!vehiclePositionsByVehicleId.containsKey(vehicleId)) { vehiclePositionsByVehicleId.put(vehicleId, vp); } else { _log.warn("Multiple updates for vehicle {}; taking newest.", vehicleId); VehiclePosition otherUpdate = vehiclePositionsByVehicleId.get(vehicleId); long otherTimestamp = otherUpdate.getTimestamp(); if (vp.getTimestamp() > otherTimestamp) { vehiclePositionsByVehicleId.put(vehicleId, vp); } } } else if (vp.hasTrip()) { /* * Vehicle position does not have vehicle ID but has TripDescriptor, so * use that, but only if there is only one. */ TripDescriptor td = vp.getTrip(); long time = vp.hasTimestamp() ? vp.getTimestamp() * 1000 : currentTime(); BlockDescriptor bd = getTripDescriptorAsBlockDescriptor(result, td, time); if (bd == null) { continue; } if (!anonymousVehiclePositionsByBlock.containsKey(bd)) { anonymousVehiclePositionsByBlock.put(bd, vp); } else { /* * When we have multiple VehiclePositions for a block but no way to * uniquely distinguish them there is nothing useful or reasonable we * can do with the data. */ _log.warn("Multiple anonymous VehiclePositions for trip {}; giving up.", td.getTripId()); badAnonymousVehiclePositions.add(bd); } } else { /* * Pathological VehiclePosition contains no identifying information; * skip. */ continue; } } // Remove multiple vehicles where multiple anonymous vehicles are present in // a block for (BlockDescriptor bd : badAnonymousVehiclePositions) { anonymousVehiclePositionsByBlock.remove(bd); } // Map updates by vehicle ID for (Map.Entry<String, Collection<TripUpdate>> e : tripUpdatesByVehicleId.asMap().entrySet()) { CombinedTripUpdatesAndVehiclePosition update = new CombinedTripUpdatesAndVehiclePosition(); String vehicleId = e.getKey(); Collection<TripUpdate> tripUpdates = e.getValue(); TripUpdate tu = bestTripByVehicleId.get(vehicleId); long time = tu.hasTimestamp() ? tu.getTimestamp() * 1000 : currentTime(); update.block = getTripDescriptorAsBlockDescriptor(result, tu.getTrip(), time); update.tripUpdates = new ArrayList<TripUpdate>(tripUpdates); update.bestTrip = tu.getTrip().getTripId(); if (vehiclePositionsByVehicleId.containsKey(vehicleId)) { update.vehiclePosition = vehiclePositionsByVehicleId.get(vehicleId); } updates.add(update); } // Map anonymous updates by block descriptor for (Entry<BlockDescriptor, Collection<TripUpdate>> e : anonymousTripUpdatesByBlock.asMap().entrySet()) { CombinedTripUpdatesAndVehiclePosition update = new CombinedTripUpdatesAndVehiclePosition(); BlockDescriptor bd = e.getKey(); update.block = bd; update.tripUpdates = new ArrayList<TripUpdate>(e.getValue()); if (update.tripUpdates.size() == 1 && anonymousVehiclePositionsByBlock.containsKey(bd)) { update.vehiclePosition = anonymousVehiclePositionsByBlock.get(bd); } updates.add(update); } // Set vehicle ID in block if possible for (CombinedTripUpdatesAndVehiclePosition update : updates) { String vehicleId = null; for (TripUpdate tu : update.tripUpdates) { if (tu.hasVehicle() && tu.getVehicle().hasId()) { vehicleId = tu.getVehicle().getId(); break; } } if (vehicleId == null && update.vehiclePosition != null && update.vehiclePosition.hasVehicle() && update.vehiclePosition.getVehicle().hasId()) { vehicleId = update.vehiclePosition.getVehicle().getId(); } if (vehicleId != null && update.block != null) { update.block.setVehicleId(vehicleId); } } return updates; } private long getTripStartTime(String tripId) { TripEntry tripEntry = _entitySource.getTrip(tripId); long min = Long.MAX_VALUE; if (tripEntry == null) return min; for (StopTimeEntry stopTime : tripEntry.getStopTimes()) { if (stopTime.getArrivalTime() < min) min = stopTime.getArrivalTime(); } return min; } private boolean tripMoreAppropriate(TripUpdate newTrip, TripUpdate original, String vehicleId) { long closestTemporalUpdateNewTrip = closestTemporalUpdate(newTrip); long closestTemporalUpdateOriginal = closestTemporalUpdate(original); if (closestTemporalUpdateNewTrip < closestTemporalUpdateOriginal) return true; return false; } private long closestTemporalUpdate(TripUpdate t) { long closest = Long.MAX_VALUE; for (StopTimeUpdate stu : t.getStopTimeUpdateList()) { if (stu.hasArrival()) { long delta = Math.abs(stu.getArrival().getTime() * 1000 - getCurrentTime()); if (delta < closest) { closest = delta; } } else if (stu.hasDeparture()) { long delta = Math.abs(stu.getDeparture().getTime() * 1000 - getCurrentTime()); if (delta < closest) { closest = delta; } } } return closest; } /** * The {@link VehicleLocationRecord} is guaranteed to have a * {@link VehicleLocationRecord#getVehicleId()} value. * * @param update * @return */ public VehicleLocationRecord createVehicleLocationRecordForUpdate( CombinedTripUpdatesAndVehiclePosition update) { return createVehicleLocationRecordForUpdate(null, update); } public VehicleLocationRecord createVehicleLocationRecordForUpdate(MonitoredResult result, CombinedTripUpdatesAndVehiclePosition update) { VehicleLocationRecord record = new VehicleLocationRecord(); record.setTimeOfRecord(currentTime()); // this is just the default -- if we have tripUpdates this will be re-written BlockDescriptor blockDescriptor = update.block; if (update.block == null) return null; String vehicleId = update.block.getVehicleId(); record.setBlockId(blockDescriptor.getBlockInstance().getBlock().getBlock().getId()); record.setStatus(blockDescriptor.getScheduleRelationship().toString()); applyTripUpdatesToRecord(result, blockDescriptor, update.tripUpdates, record, vehicleId, update.bestTrip); if (update.vehiclePosition != null) { applyVehiclePositionToRecord(result, blockDescriptor, update.vehiclePosition, record); } /** * By default, we use the block id as the vehicle id */ record.setVehicleId(record.getBlockId()); if (result != null) { if (record.getTripId() != null) { result.addMatchedTripId(record.getTripId().toString()); } else if (record.getBlockId() != null) { // here we take a matched block as if it were a trip result.addMatchedTripId(record.getBlockId().toString()); } else { // we don't have a tripId, use the BlockId instead result.addMatchedTripId(record.getBlockId().toString()); } } if (blockDescriptor.getVehicleId() != null) { String agencyId = record.getBlockId().getAgencyId(); record.setVehicleId(new AgencyAndId(agencyId, blockDescriptor.getVehicleId())); } return record; } private int getBlockStartTimeForTripStartTime(BlockInstance instance, AgencyAndId tripId, int tripStartTime) { BlockConfigurationEntry block = instance.getBlock(); Map<AgencyAndId, BlockTripEntry> blockTripsById = MappingLibrary.mapToValue(block.getTrips(), "trip.id"); int rawBlockStartTime = block.getDepartureTimeForIndex(0); int rawTripStartTime = blockTripsById.get(tripId).getDepartureTimeForIndex(0); int adjustedBlockStartTime = rawBlockStartTime + (tripStartTime - rawTripStartTime); return adjustedBlockStartTime; } private BlockDescriptor getTripDescriptorAsBlockDescriptor(MonitoredResult result, TripDescriptor trip, long currentTime) { if (!trip.hasTripId()) { return null; } TripEntry tripEntry = _entitySource.getTrip(trip.getTripId()); if (tripEntry == null) { if (result != null) { _log.debug("reporting unmatched trip with id=" + trip.getTripId()); result.addUnmatchedTripId(trip.getTripId()); } else { _log.warn("no trip found with id=" + trip.getTripId()); } return null; } ServiceDate serviceDate = null; BlockInstance instance; BlockEntry block = tripEntry.getBlock(); if (trip.hasStartDate() && !"0".equals(trip.getStartDate())) { try { serviceDate = ServiceDate.parseString(trip.getStartDate()); } catch (ParseException ex) { _log.warn("Could not parse service date " + trip.getStartDate(), ex); } } if (serviceDate != null) { instance = _blockCalendarService.getBlockInstance(block.getId(), serviceDate.getAsDate().getTime()); if (instance == null) { _log.warn("block " + block.getId() + " does not exist on service date " + serviceDate); return null; } } else { long timeFrom = currentTime - 30 * 60 * 1000; long timeTo = currentTime + 30 * 60 * 1000; List<BlockInstance> instances = _blockCalendarService.getActiveBlocks(block.getId(), timeFrom, timeTo); if (instances.isEmpty()) { instances = _blockCalendarService.getClosestActiveBlocks(block.getId(), currentTime); } if (instances.isEmpty()) { _log.warn("could not find any active instances for the specified block=" + block.getId() + " trip=" + trip); return null; } instance = instances.get(0); } if (serviceDate == null) { serviceDate = new ServiceDate(new Date(instance.getServiceDate())); } BlockDescriptor blockDescriptor = new BlockDescriptor(); blockDescriptor.setBlockInstance(instance); blockDescriptor.setStartDate(serviceDate); if (trip.hasScheduleRelationship()) { blockDescriptor.setScheduleRelationshipValue(trip.getScheduleRelationship().toString()); } int tripStartTime = 0; int blockStartTime = 0; if (trip.hasStartTime() && !"0".equals(trip.getStartTime())) { try { tripStartTime = StopTimeFieldMappingFactory.getStringAsSeconds(trip.getStartTime()); } catch (InvalidStopTimeException iste) { _log.error("invalid stopTime of " + trip.getStartTime() + " for trip " + trip); } blockStartTime = getBlockStartTimeForTripStartTime(instance, tripEntry.getId(), tripStartTime); blockDescriptor.setStartTime(blockStartTime); } return blockDescriptor; } private void applyTripUpdatesToRecord(MonitoredResult result, BlockDescriptor blockDescriptor, List<TripUpdate> tripUpdates, VehicleLocationRecord record, String vehicleId, String bestTripId) { BlockInstance instance = blockDescriptor.getBlockInstance(); BlockConfigurationEntry blockConfiguration = instance.getBlock(); List<BlockTripEntry> blockTrips = blockConfiguration.getTrips(); Map<String, List<TripUpdate>> tripUpdatesByTripId = MappingLibrary.mapToValueList(tripUpdates, "trip.tripId"); long t = currentTime(); int currentTime = (int) ((t - instance.getServiceDate()) / 1000); BestScheduleDeviation best = new BestScheduleDeviation(); List<TimepointPredictionRecord> timepointPredictions = new ArrayList<TimepointPredictionRecord>(); for (BlockTripEntry blockTrip : blockTrips) { TripEntry trip = blockTrip.getTrip(); AgencyAndId tripId = trip.getId(); List<TripUpdate> updatesForTrip = tripUpdatesByTripId.get(tripId.getId()); boolean tripUpdateHasDelay = false; // onBestTrip is only relevant if bestTripId is set, which indicates that the TripUpdates // came from the vehicleId map (as opposed to block index). boolean onBestTrip = bestTripId == null || tripId.getId().equals(bestTripId); if (updatesForTrip != null) { for (TripUpdate tripUpdate : updatesForTrip) { /** * TODO: delete this code once all upstream systems have been * migrated the new "delay" and "timestamp" fields. */ if (tripUpdate.hasExtension(GtfsRealtimeOneBusAway.obaTripUpdate) && onBestTrip) { OneBusAwayTripUpdate obaTripUpdate = tripUpdate .getExtension(GtfsRealtimeOneBusAway.obaTripUpdate); if (obaTripUpdate.hasDelay()) { /** * TODO: Improved logic around picking the "best" schedule deviation */ int delay = obaTripUpdate.getDelay(); best.delta = 0; best.isInPast = false; best.scheduleDeviation = delay; best.tripId = tripId; tripUpdateHasDelay = true; } if (obaTripUpdate.hasTimestamp() && onBestTrip) { best.timestamp = obaTripUpdate.getTimestamp() * 1000; } } if (tripUpdate.hasDelay() && onBestTrip) { /** * TODO: Improved logic around picking the "best" schedule deviation */ best.delta = 0; best.isInPast = false; best.scheduleDeviation = tripUpdate.getDelay(); best.tripId = tripId; tripUpdateHasDelay = true; } if (tripUpdate.hasTimestamp() && onBestTrip) { best.timestamp = tripUpdate.getTimestamp() * 1000; } for (StopTimeUpdate stopTimeUpdate : tripUpdate.getStopTimeUpdateList()) { BlockStopTimeEntry blockStopTime = getBlockStopTimeForStopTimeUpdate(result, tripUpdate, stopTimeUpdate, blockTrip.getStopTimes(), instance.getServiceDate()); if (blockStopTime == null) continue; StopTimeEntry stopTime = blockStopTime.getStopTime(); TimepointPredictionRecord tpr = new TimepointPredictionRecord(); tpr.setTimepointId(stopTime.getStop().getId()); tpr.setTripId(stopTime.getTrip().getId()); if (stopTimeUpdate.hasStopSequence()) { tpr.setStopSequence(stopTimeUpdate.getStopSequence()); } int currentArrivalTime = computeArrivalTime(stopTime, stopTimeUpdate, instance.getServiceDate()); int currentDepartureTime = computeDepartureTime(stopTime, stopTimeUpdate, instance.getServiceDate()); if (currentArrivalTime >= 0) { if (onBestTrip) { updateBestScheduleDeviation(currentTime, stopTime.getArrivalTime(), currentArrivalTime, best, tripId, vehicleId); } long timepointPredictedTime = instance.getServiceDate() + (currentArrivalTime * 1000L); tpr.setTimepointPredictedArrivalTime(timepointPredictedTime); } if (currentDepartureTime >= 0) { if (onBestTrip) { updateBestScheduleDeviation(currentTime, stopTime.getDepartureTime(), currentDepartureTime, best, tripId, vehicleId); } long timepointPredictedTime = instance.getServiceDate() + (currentDepartureTime * 1000L); tpr.setTimepointPredictedDepartureTime(timepointPredictedTime); } if (tpr.getTimepointPredictedArrivalTime() != -1 || tpr.getTimepointPredictedDepartureTime() != -1) { timepointPredictions.add(tpr); } } } } // If we have a TripUpdate delay and timepoint predictions, interpolate // timepoint predictions for close, unserved stops. See GtfsRealtimeTripLibraryTest // for full explanation // tripUpdateHasDelay = true => best.scheduleDeviation is TripUpdate delay if (timepointPredictions.size() > 0 && tripUpdateHasDelay) { Set<AgencyAndId> records = new HashSet<AgencyAndId>(); for (TimepointPredictionRecord tpr : timepointPredictions) { records.add(tpr.getTimepointId()); } long tprStartTime = getEarliestTimeInRecords(timepointPredictions); for (StopTimeEntry stopTime : trip.getStopTimes()) { if (records.contains(stopTime.getStop().getId())) { continue; } long predictionOffset = instance.getServiceDate() + (best.scheduleDeviation * 1000L); long predictedDepartureTime = (stopTime.getDepartureTime() * 1000L) + predictionOffset; long predictedArrivalTime = (stopTime.getArrivalTime() * 1000L) + predictionOffset; long time = best.timestamp != 0 ? best.timestamp : currentTime(); if (predictedDepartureTime > time && predictedDepartureTime < tprStartTime) { TimepointPredictionRecord tpr = new TimepointPredictionRecord(); tpr.setTimepointId(stopTime.getStop().getId()); tpr.setTripId(stopTime.getTrip().getId()); tpr.setStopSequence(stopTime.getGtfsSequence()); tpr.setTimepointPredictedArrivalTime(predictedArrivalTime); tpr.setTimepointPredictedDepartureTime(predictedDepartureTime); timepointPredictions.add(tpr); } } } } record.setServiceDate(instance.getServiceDate()); if (blockDescriptor.getStartTime() != null) { record.setBlockStartTime(blockDescriptor.getStartTime()); } record.setScheduleDeviation(best.scheduleDeviation); if (best.timestamp != 0) { record.setTimeOfRecord(best.timestamp); } record.setTimepointPredictions(timepointPredictions); } private BlockStopTimeEntry getBlockStopTimeForStopTimeUpdate(MonitoredResult result, TripUpdate tripUpdate, StopTimeUpdate stopTimeUpdate, List<BlockStopTimeEntry> stopTimes, long serviceDate) { if (stopTimeUpdate.hasStopSequence()) { int stopSequence = stopTimeUpdate.getStopSequence(); Map<Integer, BlockStopTimeEntry> sequenceToStopTime = MappingLibrary.mapToValue(stopTimes, "stopTime.gtfsSequence"); if (sequenceToStopTime.containsKey(stopSequence)) { BlockStopTimeEntry blockStopTime = sequenceToStopTime.get(stopSequence); if (!stopTimeUpdate.hasStopId()) { if (result != null) { result.addMatchedStopId(blockStopTime.getStopTime().getStop().getId().getId()); } return blockStopTime; } String stopTimeUpdateStopId = convertStopId(stopTimeUpdate.getStopId()); if (blockStopTime.getStopTime().getStop().getId().getId().equals(stopTimeUpdateStopId)) { if (result != null) { result.addMatchedStopId(blockStopTime.getStopTime().getStop().getId().getId()); } return blockStopTime; } // The stop sequence and stop id didn't match, so we fall through to // match by stop id if possible // we do not log this as it still may match later } else { _log.debug("StopTimeSequence is out of bounds: stopSequence=" + stopSequence + " tripUpdate=\n" + tripUpdate); // sadly we can't report an invalid stop sequence -- we need a stopId } } if (stopTimeUpdate.hasStopId()) { int time = getTimeForStopTimeUpdate(stopTimeUpdate, serviceDate); String stopId = convertStopId(stopTimeUpdate.getStopId()); // There could be loops, meaning a stop could appear multiple times along // a trip. To get around this. Min<BlockStopTimeEntry> bestMatches = new Min<BlockStopTimeEntry>(); for (BlockStopTimeEntry blockStopTime : stopTimes) { if (blockStopTime.getStopTime().getStop().getId().getId().equals(stopId)) { StopTimeEntry stopTime = blockStopTime.getStopTime(); int departureDelta = Math.abs(stopTime.getDepartureTime() - time); int arrivalDelta = Math.abs(stopTime.getArrivalTime() - time); bestMatches.add(departureDelta, blockStopTime); bestMatches.add(arrivalDelta, blockStopTime); } } if (!bestMatches.isEmpty()) { if (result != null) { result.addMatchedStopId(convertStopId(stopId)); } return bestMatches.getMinElement(); } } if (result != null) { // if we are here, the stop did not fall on that block result.addUnmatchedStopId(convertStopId(stopTimeUpdate.getStopId())); } return null; } private String convertStopId(String stopId) { if (this._stopModificationStrategy == null) { return stopId; } return _stopModificationStrategy.convertStopId(stopId); } private int getTimeForStopTimeUpdate(StopTimeUpdate stopTimeUpdate, long serviceDate) { long t = currentTime(); if (stopTimeUpdate.hasArrival()) { StopTimeEvent arrival = stopTimeUpdate.getArrival(); // note that we prefer time over delay if both are present if (arrival.hasTime()) { return (int) (arrival.getTime() - serviceDate / 1000); } if (arrival.hasDelay()) { return (int) ((t - serviceDate) / 1000 - arrival.getDelay()); } } if (stopTimeUpdate.hasDeparture()) { StopTimeEvent departure = stopTimeUpdate.getDeparture(); // again we prefer time over delay if both are present if (departure.hasTime()) return (int) (departure.getTime() - serviceDate / 1000); if (departure.hasDelay()) { return (int) ((t - serviceDate) / 1000 - departure.getDelay()); } } // instead of illegal state exception we return -1 to not corrupt the read _log.debug("expected at least an arrival or departure time or delay for update: " + stopTimeUpdate); return -1; } private int computeArrivalTime(StopTimeEntry stopTime, StopTimeUpdate stopTimeUpdate, long serviceDate) { if (!stopTimeUpdate.hasArrival()) return -1; StopTimeEvent arrival = stopTimeUpdate.getArrival(); if (arrival.hasDelay()) return stopTime.getArrivalTime() + arrival.getDelay(); if (arrival.hasTime()) return (int) (arrival.getTime() - serviceDate / 1000); // instead of illegal state exception we return -1 to not corrupt the read return -1; } private int computeDepartureTime(StopTimeEntry stopTime, StopTimeUpdate stopTimeUpdate, long serviceDate) { if (!stopTimeUpdate.hasDeparture()) return -1; StopTimeEvent departure = stopTimeUpdate.getDeparture(); if (departure.hasDelay()) return stopTime.getDepartureTime() + departure.getDelay(); if (departure.hasTime()) return (int) (departure.getTime() - serviceDate / 1000); // instead of throwing an exception here, simply return -1 // so as to not stop the rest of the processing return -1; } private void updateBestScheduleDeviation(int currentTime, int expectedStopTime, int actualStopTime, BestScheduleDeviation best, AgencyAndId tripId, String vehicleId) { int delta = Math.abs(currentTime - actualStopTime); boolean isInPast = currentTime > actualStopTime; int scheduleDeviation = actualStopTime - expectedStopTime; if (delta < best.delta || (!isInPast && best.isInPast)) { best.delta = delta; best.isInPast = isInPast; best.scheduleDeviation = scheduleDeviation; best.tripId = tripId; } } private void applyVehiclePositionToRecord(MonitoredResult result, BlockDescriptor blockDescriptor, VehiclePosition vehiclePosition, VehicleLocationRecord record) { Position position = vehiclePosition.getPosition(); if (vehiclePosition.hasTimestamp()) { record.setTimeOfLocationUpdate(TimeUnit.SECONDS.toMillis(vehiclePosition.getTimestamp())); //vehicle timestamp is in seconds } record.setCurrentLocationLat(position.getLatitude()); record.setCurrentLocationLon(position.getLongitude()); if (result != null) { result.addLatLon(position.getLatitude(), position.getLongitude()); } if (_scheduleAdherenceFromLocation) { CoordinatePoint location = new CoordinatePoint(position.getLatitude(), position.getLongitude()); double totalDistance = blockDescriptor.getBlockInstance().getBlock().getTotalBlockDistance(); long timestamp = vehiclePosition.hasTimestamp() ? record.getTimeOfLocationUpdate() : record.getTimeOfRecord(); ScheduledBlockLocation loc = _blockGeospatialService.getBestScheduledBlockLocationForLocation( blockDescriptor.getBlockInstance(), location, timestamp, 0, totalDistance); long serviceDateTime = record.getServiceDate(); long effectiveScheduleTime = loc.getScheduledTime() + (serviceDateTime / 1000); double deviation = timestamp / 1000 - effectiveScheduleTime; record.setScheduleDeviation(deviation); } } private static long getEarliestTimeInRecords(Collection<TimepointPredictionRecord> records) { long min = Long.MAX_VALUE; for (TimepointPredictionRecord tpr : records) { if (tpr.getTimepointPredictedArrivalTime() != -1) { min = Math.min(min, tpr.getTimepointPredictedArrivalTime()); } else if (tpr.getTimepointPredictedDepartureTime() != -1) { min = Math.min(min, tpr.getTimepointPredictedDepartureTime()); } } return min; } private long currentTime() { if (_currentTime != 0) { // if the feed clock is off by more than an hour we most likely have a timezone issue if (validateCurrentTime() && Math.abs(_currentTime - SystemTime.currentTimeMillis()) > 60 * 60 * 1000) { _log.error("timestamp invalid at " + new Date(_currentTime) + ", overriding with system time"); _currentTime = SystemTime.currentTimeMillis(); } return _currentTime; } return SystemTime.currentTimeMillis(); } private static class BestScheduleDeviation { public int delta = Integer.MAX_VALUE; public int scheduleDeviation = 0; public boolean isInPast = true; public long timestamp = 0; public AgencyAndId tripId = null; } }