/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * 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; import org.onebusaway.collections.CollectionsLibrary; import org.onebusaway.collections.FactoryMap; import org.onebusaway.collections.Min; import org.onebusaway.collections.tuple.Pair; import org.onebusaway.collections.tuple.Tuples; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.transit_data.model.TimeIntervalBean; import org.onebusaway.transit_data_federation.model.TargetTime; import org.onebusaway.transit_data_federation.services.ArrivalAndDeparturePairQuery; import org.onebusaway.transit_data_federation.services.ArrivalAndDepartureQuery; import org.onebusaway.transit_data_federation.services.ArrivalAndDepartureService; import org.onebusaway.transit_data_federation.services.StopTimeService; import org.onebusaway.transit_data_federation.services.StopTimeService.EFrequencyStopTimeBehavior; import org.onebusaway.transit_data_federation.services.blocks.BlockInstance; import org.onebusaway.transit_data_federation.services.blocks.BlockStatusService; import org.onebusaway.transit_data_federation.services.blocks.BlockTripInstance; import org.onebusaway.transit_data_federation.services.blocks.BlockTripInstanceLibrary; import org.onebusaway.transit_data_federation.services.blocks.InstanceState; import org.onebusaway.transit_data_federation.services.realtime.ArrivalAndDepartureInstance; import org.onebusaway.transit_data_federation.services.realtime.ArrivalAndDepartureTime; import org.onebusaway.transit_data_federation.services.realtime.BlockLocation; import org.onebusaway.transit_data_federation.services.realtime.BlockLocationService; import org.onebusaway.transit_data_federation.services.realtime.ScheduleDeviationSamples; import org.onebusaway.transit_data_federation.services.transit_graph.BlockConfigurationEntry; 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.FrequencyEntry; import org.onebusaway.transit_data_federation.services.transit_graph.StopEntry; import org.onebusaway.transit_data_federation.services.transit_graph.StopTimeEntry; import org.onebusaway.transit_data_federation.services.transit_graph.TripEntry; import org.onebusaway.transit_data_federation.services.tripplanner.StopTimeInstance; import org.onebusaway.transit_data_federation.services.tripplanner.StopTransfer; import org.onebusaway.transit_data_federation.services.tripplanner.StopTransferService; import org.onebusaway.utility.EInRangeStrategy; import org.onebusaway.utility.EOutOfRangeStrategy; import org.onebusaway.utility.InterpolationLibrary; import org.onebusaway.utility.TransitInterpolationLibrary; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; 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.Set; @Component class ArrivalAndDepartureServiceImpl implements ArrivalAndDepartureService { private StopTimeService _stopTimeService; private BlockLocationService _blockLocationService; private BlockStatusService _blockStatusService; private StopTransferService _stopTransferService; @Autowired public void setStopTimeService(StopTimeService stopTimeService) { _stopTimeService = stopTimeService; } @Autowired public void setBlockLocationService(BlockLocationService blockLocationService) { _blockLocationService = blockLocationService; } @Autowired public void setBlockStatusService(BlockStatusService blockStatusService) { _blockStatusService = blockStatusService; } @Autowired public void setStopTransferService(StopTransferService stopTransferService) { _stopTransferService = stopTransferService; } @Override public List<ArrivalAndDepartureInstance> getArrivalsAndDeparturesForStopInTimeRange( StopEntry stop, TargetTime targetTime, long fromTime, long toTime) { // We add a buffer before and after to catch late and early buses Date fromTimeBuffered = new Date(fromTime - _blockStatusService.getRunningLateWindow() * 1000); Date toTimeBuffered = new Date(toTime + _blockStatusService.getRunningEarlyWindow() * 1000); List<StopTimeInstance> stis = _stopTimeService.getStopTimeInstancesInTimeRange( stop, fromTimeBuffered, toTimeBuffered, EFrequencyStopTimeBehavior.INCLUDE_UNSPECIFIED); long frequencyOffsetTime = Math.max(targetTime.getTargetTime(), fromTime); Map<BlockInstance, List<StopTimeInstance>> stisByBlockId = getStopTimeInstancesByBlockInstance(stis); List<ArrivalAndDepartureInstance> instances = new ArrayList<ArrivalAndDepartureInstance>(); for (Map.Entry<BlockInstance, List<StopTimeInstance>> entry : stisByBlockId.entrySet()) { BlockInstance blockInstance = entry.getKey(); List<BlockLocation> locations = _blockLocationService.getLocationsForBlockInstance( blockInstance, targetTime); List<StopTimeInstance> stisForBlock = entry.getValue(); for (StopTimeInstance sti : stisForBlock) { applyRealTimeToStopTimeInstance(sti, targetTime, fromTime, toTime, frequencyOffsetTime, blockInstance, locations, instances); } } return instances; } @Override public List<ArrivalAndDepartureInstance> getScheduledArrivalsAndDeparturesForStopInTimeRange( StopEntry stop, long currentTime, long fromTime, long toTime) { List<StopTimeInstance> stis = _stopTimeService.getStopTimeInstancesInTimeRange( stop, new Date(fromTime), new Date(toTime), EFrequencyStopTimeBehavior.INCLUDE_UNSPECIFIED); List<ArrivalAndDepartureInstance> instances = new ArrayList<ArrivalAndDepartureInstance>(); long prevFrequencyTime = Math.max(currentTime, fromTime); for (StopTimeInstance sti : stis) { BlockInstance blockInstance = sti.getBlockInstance(); ArrivalAndDepartureInstance instance = createArrivalAndDepartureForStopTimeInstance( sti, prevFrequencyTime); if (sti.getFrequency() == null) { /** * We don't need to get the scheduled location of a vehicle unless its * in our arrival window */ if (isArrivalAndDepartureBeanInRange(instance, fromTime, toTime)) { BlockLocation scheduledLocation = _blockLocationService.getScheduledLocationForBlockInstance( blockInstance, currentTime); if (scheduledLocation != null) applyBlockLocationToInstance(instance, scheduledLocation, currentTime); instances.add(instance); } } else { if (isFrequencyBasedArrivalInRange(blockInstance, sti.getFrequency(), fromTime, toTime)) { instances.add(instance); } } } return instances; } @Override public List<ArrivalAndDepartureInstance> getNextScheduledBlockTripDeparturesForStop( StopEntry stop, long time, boolean includePrivateService) { List<StopTimeInstance> stopTimes = _stopTimeService.getNextBlockSequenceDeparturesForStop( stop, time, includePrivateService); List<ArrivalAndDepartureInstance> instances = new ArrayList<ArrivalAndDepartureInstance>(); for (StopTimeInstance sti : stopTimes) { ArrivalAndDepartureInstance instance = createArrivalAndDepartureForStopTimeInstance( sti, time); instances.add(instance); } return instances; } @Override public ArrivalAndDepartureInstance getArrivalAndDepartureForStop( ArrivalAndDepartureQuery query) { StopEntry stop = query.getStop(); int stopSequence = query.getStopSequence(); TripEntry trip = query.getTrip(); long serviceDate = query.getServiceDate(); AgencyAndId vehicleId = query.getVehicleId(); long time = query.getTime(); Map<BlockInstance, List<BlockLocation>> locationsByInstance = _blockStatusService.getBlocks( trip.getBlock().getId(), serviceDate, vehicleId, time); if (locationsByInstance.isEmpty()) return null; Map.Entry<BlockInstance, List<BlockLocation>> entry = locationsByInstance.entrySet().iterator().next(); BlockInstance blockInstance = entry.getKey(); List<BlockLocation> locations = entry.getValue(); int timeOfServiceDate = (int) ((time - serviceDate) / 1000); ArrivalAndDepartureInstance instance = createArrivalAndDeparture( blockInstance, trip.getId(), stop.getId(), stopSequence, serviceDate, timeOfServiceDate, time); if (!locations.isEmpty()) { /** * What if there are multiple locations? Pick the first? */ BlockLocation location = locations.get(0); applyBlockLocationToInstance(instance, location, time); } return instance; } @Override public ArrivalAndDepartureInstance getPreviousStopArrivalAndDeparture( ArrivalAndDepartureInstance instance) { BlockStopTimeEntry stopTime = instance.getBlockStopTime(); BlockTripEntry trip = stopTime.getTrip(); BlockConfigurationEntry blockConfig = trip.getBlockConfiguration(); List<BlockStopTimeEntry> stopTimes = blockConfig.getStopTimes(); int index = stopTime.getBlockSequence() - 1; if (index < 0) return null; BlockStopTimeEntry prevStopTime = stopTimes.get(index); InstanceState state = instance.getStopTimeInstance().getState(); ArrivalAndDepartureTime scheduledTime = ArrivalAndDepartureTime.getScheduledTime( state, prevStopTime); if (instance.getFrequency() != null) { StopTimeEntry pStopTime = prevStopTime.getStopTime(); int betweenStopDelta = stopTime.getStopTime().getArrivalTime() - pStopTime.getDepartureTime(); int atStopDelta = pStopTime.getDepartureTime() - pStopTime.getArrivalTime(); long scheduledDepartureTime = instance.getScheduledArrivalTime() - betweenStopDelta * 1000; long scheduledArrivalTime = scheduledDepartureTime - atStopDelta * 1000; scheduledTime.setArrivalTime(scheduledArrivalTime); scheduledTime.setDepartureTime(scheduledDepartureTime); } StopTimeInstance prevStopTimeInstance = new StopTimeInstance(prevStopTime, state); ArrivalAndDepartureInstance prevInstance = new ArrivalAndDepartureInstance( prevStopTimeInstance, scheduledTime); if (instance.isPredictedArrivalTimeSet()) { int scheduledDeviation = (int) ((instance.getPredictedArrivalTime() - instance.getScheduledArrivalTime()) / 1000); int departureDeviation = propagateScheduleDeviationBackwardBetweenStops( prevStopTime, stopTime, scheduledDeviation); int arrivalDeviation = propagateScheduleDeviationBackwardAcrossStop( prevStopTime, departureDeviation); setPredictedArrivalTimeForInstance(prevInstance, prevInstance.getScheduledArrivalTime() + arrivalDeviation * 1000); setPredictedDepartureTimeForInstance(prevInstance, prevInstance.getScheduledDepartureTime() + departureDeviation * 1000); } return prevInstance; } @Override public ArrivalAndDepartureInstance getNextStopArrivalAndDeparture( ArrivalAndDepartureInstance instance) { BlockStopTimeEntry stopTime = instance.getBlockStopTime(); BlockTripEntry trip = stopTime.getTrip(); BlockConfigurationEntry blockConfig = trip.getBlockConfiguration(); List<BlockStopTimeEntry> stopTimes = blockConfig.getStopTimes(); int index = stopTime.getBlockSequence() + 1; if (index >= stopTimes.size()) return null; BlockStopTimeEntry nextStopTime = stopTimes.get(index); InstanceState state = instance.getStopTimeInstance().getState(); ArrivalAndDepartureTime scheduledTime = ArrivalAndDepartureTime.getScheduledTime( state, nextStopTime); if (state.getFrequency() != null) { StopTimeEntry nStopTime = nextStopTime.getStopTime(); int betweenStopDelta = nStopTime.getArrivalTime() - stopTime.getStopTime().getDepartureTime(); int atStopDelta = nStopTime.getDepartureTime() - nStopTime.getArrivalTime(); long scheduledArrivalTime = instance.getScheduledDepartureTime() + betweenStopDelta * 1000; long scheduledDepartureTime = scheduledArrivalTime + atStopDelta * 1000; scheduledTime.setArrivalTime(scheduledArrivalTime); scheduledTime.setDepartureTime(scheduledDepartureTime); } StopTimeInstance nextStopTimeInstance = new StopTimeInstance(stopTime, state); ArrivalAndDepartureInstance nextInstance = new ArrivalAndDepartureInstance( nextStopTimeInstance, scheduledTime); if (instance.isPredictedDepartureTimeSet()) { int scheduledDeviation = (int) ((instance.getPredictedDepartureTime() - instance.getScheduledDepartureTime()) / 1000); int arrivalDeviation = propagateScheduleDeviationForwardBetweenStops( stopTime, nextStopTime, scheduledDeviation); int departureDeviation = propagateScheduleDeviationForwardAcrossStop( nextStopTime, arrivalDeviation); setPredictedArrivalTimeForInstance(nextInstance, nextInstance.getScheduledArrivalTime() + arrivalDeviation * 1000); setPredictedDepartureTimeForInstance(nextInstance, nextInstance.getScheduledDepartureTime() + departureDeviation * 1000); } return nextInstance; } @Override public ArrivalAndDepartureInstance getNextTransferStopArrivalAndDeparture( ArrivalAndDepartureInstance instance) { BlockStopTimeEntry blockStopTime = instance.getBlockStopTime(); BlockTripEntry trip = blockStopTime.getTrip(); BlockConfigurationEntry blockConfig = trip.getBlockConfiguration(); List<BlockStopTimeEntry> stopTimes = blockConfig.getStopTimes(); int index = blockStopTime.getBlockSequence() + 1; while (true) { if (index >= stopTimes.size()) return null; BlockStopTimeEntry nextBlockStopTime = stopTimes.get(index); StopTimeEntry nextStopTime = nextBlockStopTime.getStopTime(); StopEntry nextStop = nextStopTime.getStop(); List<StopTransfer> transfers = _stopTransferService.getTransfersFromStop(nextStop); if (!transfers.isEmpty()) { InstanceState state = instance.getStopTimeInstance().getState(); StopTimeInstance nextStopTimeInstance = new StopTimeInstance( nextBlockStopTime, state); ArrivalAndDepartureTime nextScheduledTime = ArrivalAndDepartureTime.getScheduledTime( state, nextBlockStopTime); ArrivalAndDepartureInstance nextInstance = new ArrivalAndDepartureInstance( nextStopTimeInstance, nextScheduledTime); if (state.getFrequency() != null) { int betweenStopDelta = nextStopTime.getArrivalTime() - blockStopTime.getStopTime().getDepartureTime(); int atStopDelta = nextStopTime.getDepartureTime() - nextStopTime.getArrivalTime(); long scheduledArrivalTime = instance.getScheduledDepartureTime() + betweenStopDelta * 1000; long scheduledDepartureTime = scheduledArrivalTime + atStopDelta * 1000; nextInstance.setScheduledArrivalTime(scheduledArrivalTime); nextInstance.setScheduledDepartureTime(scheduledDepartureTime); } return nextInstance; } index++; } } @Override public List<Pair<ArrivalAndDepartureInstance>> getNextDeparturesForStopPair( StopEntry fromStop, StopEntry toStop, TargetTime targetTime, ArrivalAndDeparturePairQuery query) { Date tFrom = new Date(targetTime.getTargetTime()); boolean applyRealTime = query.isApplyRealTime(); int lookaheadTime = query.getLookaheadTime(); int resultCount = query.getResultCount(); boolean includePrivateService = query.isIncludePrivateService(); int runningEarlySlack = applyRealTime ? _blockStatusService.getRunningEarlyWindow() : 0; int runningLateSlack = (applyRealTime ? _blockStatusService.getRunningLateWindow() : 0) + lookaheadTime; List<Pair<StopTimeInstance>> pairs = _stopTimeService.getNextDeparturesBetweenStopPair( fromStop, toStop, tFrom, runningEarlySlack, runningLateSlack, resultCount, includePrivateService); Date tShifted = new Date(targetTime.getTargetTime() - lookaheadTime * 1000); return getArrivalsAndDeparturesFromStopTimeInstancePairs(targetTime, pairs, tShifted, null, applyRealTime, true, false); } @Override public List<Pair<ArrivalAndDepartureInstance>> getPreviousArrivalsForStopPair( StopEntry fromStop, StopEntry toStop, TargetTime targetTime, ArrivalAndDeparturePairQuery query) { Date tTo = new Date(targetTime.getTargetTime()); boolean applyRealTime = query.isApplyRealTime(); int resultCount = query.getResultCount(); boolean includePrivateService = query.isIncludePrivateService(); int runningEarlySlack = applyRealTime ? _blockStatusService.getRunningEarlyWindow() : 0; int runningLateSlack = applyRealTime ? _blockStatusService.getRunningLateWindow() : 0; List<Pair<StopTimeInstance>> pairs = _stopTimeService.getPreviousArrivalsBetweenStopPair( fromStop, toStop, tTo, runningEarlySlack, runningLateSlack, resultCount, includePrivateService); return getArrivalsAndDeparturesFromStopTimeInstancePairs(targetTime, pairs, null, tTo, applyRealTime, false, false); } /**** * Private Methods ****/ private List<Pair<ArrivalAndDepartureInstance>> getArrivalsAndDeparturesFromStopTimeInstancePairs( TargetTime targetTime, List<Pair<StopTimeInstance>> pairs, Date tFrom, Date tTo, boolean applyRealTime, boolean findDepartures, boolean fillBlockLocations) { long frequencyOffsetTime = Math.max(targetTime.getTargetTime(), targetTime.getCurrentTime()); List<Pair<ArrivalAndDepartureInstance>> results = new ArrayList<Pair<ArrivalAndDepartureInstance>>(); Map<BlockInstance, List<BlockLocation>> blockLocationsByBlockInstance = getBlockLocationInformationForPairs( pairs, targetTime, applyRealTime); for (Pair<StopTimeInstance> pair : pairs) { StopTimeInstance stiFrom = pair.getFirst(); StopTimeInstance stiTo = pair.getSecond(); BlockInstance blockInstance = stiFrom.getBlockInstance(); List<BlockLocation> locations = blockLocationsByBlockInstance.get(blockInstance); applyRealTimeToStopTimeInstancePair(stiFrom, stiTo, targetTime, tFrom, tTo, frequencyOffsetTime, blockInstance, locations, results, findDepartures, fillBlockLocations); } return results; } private Map<BlockInstance, List<BlockLocation>> getBlockLocationInformationForPairs( List<Pair<StopTimeInstance>> pairs, TargetTime targetTime, boolean applyRealTime) { if (!applyRealTime) return Collections.emptyMap(); Set<BlockInstance> blockInstances = new HashSet<BlockInstance>(); for (Pair<StopTimeInstance> pair : pairs) blockInstances.add(pair.getFirst().getBlockInstance()); Map<BlockInstance, List<BlockLocation>> blockLocationsByBlockInstance = new HashMap<BlockInstance, List<BlockLocation>>(); for (BlockInstance blockInstance : blockInstances) { List<BlockLocation> locations = _blockLocationService.getLocationsForBlockInstance( blockInstance, targetTime); blockLocationsByBlockInstance.put(blockInstance, locations); } return blockLocationsByBlockInstance; } private Map<BlockInstance, List<StopTimeInstance>> getStopTimeInstancesByBlockInstance( List<StopTimeInstance> stopTimes) { Map<BlockInstance, List<StopTimeInstance>> r = new FactoryMap<BlockInstance, List<StopTimeInstance>>( new ArrayList<StopTimeInstance>()); for (StopTimeInstance stopTime : stopTimes) { BlockStopTimeEntry blockStopTime = stopTime.getStopTime(); BlockTripEntry blockTrip = blockStopTime.getTrip(); BlockConfigurationEntry blockConfiguration = blockTrip.getBlockConfiguration(); long serviceDate = stopTime.getServiceDate(); BlockInstance blockInstance = new BlockInstance(blockConfiguration, serviceDate, stopTime.getFrequency()); r.get(blockInstance).add(stopTime); } return r; } private void applyRealTimeToStopTimeInstance(StopTimeInstance sti, TargetTime targetTime, long fromTime, long toTime, long frequencyOffsetTime, BlockInstance blockInstance, List<BlockLocation> locations, List<ArrivalAndDepartureInstance> results) { for (BlockLocation location : locations) { ArrivalAndDepartureInstance instance = createArrivalAndDepartureForStopTimeInstance( sti, frequencyOffsetTime); applyBlockLocationToInstance(instance, location, targetTime.getTargetTime()); if (isArrivalAndDepartureBeanInRange(instance, fromTime, toTime)) results.add(instance); } if (locations.isEmpty()) { ArrivalAndDepartureInstance instance = createArrivalAndDepartureForStopTimeInstance( sti, frequencyOffsetTime); if (sti.getFrequency() == null) { /** * We don't need to get the scheduled location of a vehicle unless its * in our arrival window */ if (isArrivalAndDepartureBeanInRange(instance, fromTime, toTime)) { BlockLocation scheduledLocation = _blockLocationService.getScheduledLocationForBlockInstance( blockInstance, targetTime.getTargetTime()); if (scheduledLocation != null) applyBlockLocationToInstance(instance, scheduledLocation, targetTime.getTargetTime()); results.add(instance); } } else { if (isFrequencyBasedArrivalInRange(blockInstance, sti.getFrequency(), fromTime, toTime)) { results.add(instance); } } } } private void applyRealTimeToStopTimeInstancePair(StopTimeInstance stiFrom, StopTimeInstance stiTo, TargetTime targetTime, Date fromTime, Date toTime, long frequencyOffsetTime, BlockInstance blockInstance, List<BlockLocation> locations, List<Pair<ArrivalAndDepartureInstance>> results, boolean findDepartures, boolean fillBlockLocations) { if (CollectionsLibrary.isEmpty(locations)) { ArrivalAndDepartureInstance instanceFrom = createArrivalAndDepartureForStopTimeInstance( stiFrom, frequencyOffsetTime); ArrivalAndDepartureInstance instanceTo = createArrivalAndDepartureForStopTimeInstance( stiTo, frequencyOffsetTime); /** * We don't need to get the scheduled location of a vehicle unless its in * our arrival window */ if (isArrivalAndDeparturePairInRange(instanceFrom, instanceTo, fromTime, toTime, findDepartures)) { if (fillBlockLocations) { BlockLocation scheduledLocation = _blockLocationService.getScheduledLocationForBlockInstance( blockInstance, targetTime.getTargetTime()); if (scheduledLocation != null) { applyBlockLocationToInstance(instanceFrom, scheduledLocation, targetTime.getTargetTime()); applyBlockLocationToInstance(instanceTo, scheduledLocation, targetTime.getTargetTime()); } } results.add(Tuples.pair(instanceFrom, instanceTo)); } } else { for (BlockLocation location : locations) { ArrivalAndDepartureInstance instanceFrom = createArrivalAndDepartureForStopTimeInstance( stiFrom, frequencyOffsetTime); ArrivalAndDepartureInstance instanceTo = createArrivalAndDepartureForStopTimeInstance( stiTo, frequencyOffsetTime); applyBlockLocationToInstance(instanceFrom, location, targetTime.getTargetTime()); applyBlockLocationToInstance(instanceTo, location, targetTime.getTargetTime()); if (isArrivalAndDeparturePairInRange(instanceFrom, instanceTo, fromTime, toTime, findDepartures)) results.add(Tuples.pair(instanceFrom, instanceTo)); } } } private void applyBlockLocationToInstance( ArrivalAndDepartureInstance instance, BlockLocation blockLocation, long targetTime) { instance.setBlockLocation(blockLocation); if (blockLocation.isScheduleDeviationSet() || blockLocation.areScheduleDeviationsSet()) { Double scheduleDeviation = getBestScheduleDeviation(instance, blockLocation); if (scheduleDeviation != null) { setPredictedTimesFromScheduleDeviation(instance, blockLocation, scheduleDeviation.intValue(), targetTime); } } } /** * Returns the best schedule deviation for this stop, given the scheduleDeviations * stored in blockLocation. {@link TransitInterpolationLibrary} is used to find * the best deviation, which interpolates/extrapolates values consistent with the * GTFS-realtime spec (https://developers.google.com/transit/gtfs-realtime/) when * using the {@link EInRangeStrategy.PREVIOUS_VALUE} and {@link EOutOfRangeStrategy.LAST_VALUE} * strategies. null is returned if no real-time deviations were found and the scheduled * arrival time should be used. * @param instance * @param blockLocation * @return the best deviation for this stop, or null if no real-time deviations were found * and the scheduled arrival time should be used. */ private Double getBestScheduleDeviation(ArrivalAndDepartureInstance instance, BlockLocation blockLocation) { ScheduleDeviationSamples scheduleDeviations = blockLocation.getScheduleDeviations(); if (scheduleDeviations != null && !scheduleDeviations.isEmpty()) { // We currently use the scheduled arrival time of the stop as the search index // This MUST be consistent with the index set in BlockLocationServiceImpl.getBlockLocation() Integer arrivalTime = instance.getBlockStopTime().getStopTime().getArrivalTime(); // Determine which real-time deviation should be used for this stop, if any return TransitInterpolationLibrary.interpolate( scheduleDeviations.getScheduleTimes(), scheduleDeviations.getScheduleDeviationMus(), arrivalTime, EOutOfRangeStrategy.LAST_VALUE, EInRangeStrategy.PREVIOUS_VALUE); } else if (blockLocation.isScheduleDeviationSet()) { return blockLocation.getScheduleDeviation(); } else { return 0.0; } } private void setPredictedTimesFromScheduleDeviation( ArrivalAndDepartureInstance instance, BlockLocation blockLocation, int scheduleDeviation, long targetTime) { BlockStopTimeEntry blockStopTime = instance.getBlockStopTime(); int effectiveScheduleTime = (int) (((targetTime - instance.getServiceDate()) / 1000) - scheduleDeviation); int arrivalDeviation = calculateArrivalDeviation( blockLocation.getNextStop(), blockStopTime, effectiveScheduleTime, scheduleDeviation); int departureDeviation = calculateDepartureDeviation( blockLocation.getNextStop(), blockStopTime, effectiveScheduleTime, scheduleDeviation); /** * Why don't we use the ArrivalAndDepartureTime scheduled arrival and * departures here? Because they may have been artificially shifted for a * frequency-based method */ InstanceState state = instance.getStopTimeInstance().getState(); ArrivalAndDepartureTime schedule = ArrivalAndDepartureTime.getScheduledTime( state, instance.getBlockStopTime()); long arrivalTime = schedule.getArrivalTime() + arrivalDeviation * 1000; setPredictedArrivalTimeForInstance(instance, arrivalTime); TimeIntervalBean predictedArrivalTimeInterval = computePredictedArrivalTimeInterval( instance, blockLocation, targetTime); instance.setPredictedArrivalInterval(predictedArrivalTimeInterval); long departureTime = schedule.getDepartureTime() + departureDeviation * 1000; setPredictedDepartureTimeForInstance(instance, departureTime); TimeIntervalBean predictedDepartureTimeInterval = computePredictedDepartureTimeInterval( instance, blockLocation, targetTime); instance.setPredictedDepartureInterval(predictedDepartureTimeInterval); } /** * This method both sets the predicted arrival time for an instance, but also * updates the scheduled arrival time for a frequency-based instance * * @param instance * @param arrivalTime */ private void setPredictedArrivalTimeForInstance( ArrivalAndDepartureInstance instance, long arrivalTime) { instance.setPredictedArrivalTime(arrivalTime); if (instance.getFrequency() != null) instance.setScheduledArrivalTime(arrivalTime); } /** * This method both sets the predicted departure time for an instance, but * also updates the scheduled departure time for a frequency-based instance * * @param instance * @param departureTime */ private void setPredictedDepartureTimeForInstance( ArrivalAndDepartureInstance instance, long departureTime) { instance.setPredictedDepartureTime(departureTime); if (instance.getFrequency() != null) instance.setScheduledDepartureTime(departureTime); } private int calculateArrivalDeviation(BlockStopTimeEntry nextBlockStopTime, BlockStopTimeEntry targetBlockStopTime, int effectiveScheduleTime, int scheduleDeviation) { // TargetStopTime if (nextBlockStopTime == null || nextBlockStopTime.getBlockSequence() > targetBlockStopTime.getBlockSequence()) { return scheduleDeviation; } int a = targetBlockStopTime.getAccumulatedSlackTime(); int b = nextBlockStopTime.getAccumulatedSlackTime(); double slack = a - b; StopTimeEntry nextStopTime = nextBlockStopTime.getStopTime(); if (nextStopTime.getArrivalTime() <= effectiveScheduleTime && effectiveScheduleTime <= nextStopTime.getDepartureTime()) { slack -= (effectiveScheduleTime - nextStopTime.getArrivalTime()); } slack = Math.max(slack, 0); if (slack > 0 && scheduleDeviation > 0) scheduleDeviation -= Math.min(scheduleDeviation, slack); return scheduleDeviation; } private int calculateDepartureDeviation(BlockStopTimeEntry nextBlockStopTime, BlockStopTimeEntry targetBlockStopTime, int effectiveScheduleTime, int scheduleDeviation) { // TargetStopTime if (nextBlockStopTime == null || nextBlockStopTime.getBlockSequence() > targetBlockStopTime.getBlockSequence()) { return scheduleDeviation; } StopTimeEntry nextStopTime = nextBlockStopTime.getStopTime(); StopTimeEntry targetStopTime = targetBlockStopTime.getStopTime(); double slack = targetBlockStopTime.getAccumulatedSlackTime() - nextBlockStopTime.getAccumulatedSlackTime(); slack += targetStopTime.getSlackTime(); if (nextStopTime.getArrivalTime() <= effectiveScheduleTime && effectiveScheduleTime <= nextStopTime.getDepartureTime()) { slack -= (effectiveScheduleTime - nextStopTime.getArrivalTime()); } slack = Math.max(slack, 0); if (slack > 0 && scheduleDeviation > 0) scheduleDeviation -= Math.min(scheduleDeviation, slack); return scheduleDeviation; } private int propagateScheduleDeviationForwardBetweenStops( BlockStopTimeEntry prevStopTime, BlockStopTimeEntry nextStopTime, int scheduleDeviation) { int slack = nextStopTime.getAccumulatedSlackTime() - prevStopTime.getAccumulatedSlackTime(); slack -= prevStopTime.getStopTime().getSlackTime(); return propagateScheduleDeviationForwardWithSlack(scheduleDeviation, slack); } private int propagateScheduleDeviationForwardAcrossStop( BlockStopTimeEntry stopTime, int scheduleDeviation) { int slack = stopTime.getStopTime().getSlackTime(); return propagateScheduleDeviationForwardWithSlack(scheduleDeviation, slack); } private int propagateScheduleDeviationBackwardBetweenStops( BlockStopTimeEntry prevStopTime, BlockStopTimeEntry nextStopTime, int scheduleDeviation) { // TODO: Need to think about this return scheduleDeviation; } private int propagateScheduleDeviationBackwardAcrossStop( BlockStopTimeEntry stopTime, int scheduleDeviation) { return scheduleDeviation; } private int propagateScheduleDeviationForwardWithSlack(int scheduleDeviation, int slack) { /** * If the vehicle is running early and there is slack built into the * schedule, we guess that the vehicle will take that opportunity to pause * and let the schedule catch back up. If there is no slack, assume we'll * continue to run early. */ if (scheduleDeviation < 0) { if (slack > 0) return 0; return scheduleDeviation; } /** * If we're running behind schedule, we allow any slack to eat up part of * our delay. */ return Math.max(0, scheduleDeviation - slack); } private TimeIntervalBean computePredictedArrivalTimeInterval( ArrivalAndDepartureInstance instance, BlockLocation blockLocation, long targetTime) { BlockStopTimeEntry blockStopTime = instance.getBlockStopTime(); StopTimeEntry stopTime = blockStopTime.getStopTime(); // If the vehicle has already passed the stop, then there is no prediction // interval if (stopTime.getArrivalTime() <= blockLocation.getEffectiveScheduleTime()) return null; ScheduleDeviationSamples samples = blockLocation.getScheduleDeviations(); if (samples == null || samples.isEmpty()) return null; double mu = InterpolationLibrary.interpolate(samples.getScheduleTimes(), samples.getScheduleDeviationMus(), stopTime.getArrivalTime(), EOutOfRangeStrategy.LAST_VALUE, EInRangeStrategy.INTERPOLATE); double sigma = InterpolationLibrary.interpolate(samples.getScheduleTimes(), samples.getScheduleDeviationSigmas(), stopTime.getArrivalTime(), EOutOfRangeStrategy.LAST_VALUE, EInRangeStrategy.INTERPOLATE); long from = (long) (instance.getScheduledArrivalTime() + (mu - sigma) * 1000); long to = (long) (instance.getScheduledArrivalTime() + (mu + sigma) * 1000); return new TimeIntervalBean(from, to); } private TimeIntervalBean computePredictedDepartureTimeInterval( ArrivalAndDepartureInstance instance, BlockLocation blockLocation, long targetTime) { BlockStopTimeEntry blockStopTime = instance.getBlockStopTime(); StopTimeEntry stopTime = blockStopTime.getStopTime(); // If the vehicle has already passed the stop, then there is no prediction // interval if (stopTime.getDepartureTime() <= blockLocation.getEffectiveScheduleTime()) return null; ScheduleDeviationSamples samples = blockLocation.getScheduleDeviations(); if (samples == null || samples.isEmpty()) return null; double mu = InterpolationLibrary.interpolate(samples.getScheduleTimes(), samples.getScheduleDeviationMus(), stopTime.getDepartureTime(), EOutOfRangeStrategy.LAST_VALUE, EInRangeStrategy.INTERPOLATE); double sigma = InterpolationLibrary.interpolate(samples.getScheduleTimes(), samples.getScheduleDeviationSigmas(), stopTime.getDepartureTime(), EOutOfRangeStrategy.LAST_VALUE, EInRangeStrategy.INTERPOLATE); long from = (long) (instance.getScheduledDepartureTime() + (mu - sigma) * 1000); long to = (long) (instance.getScheduledDepartureTime() + (mu + sigma) * 1000); return new TimeIntervalBean(from, to); } private boolean isArrivalAndDepartureBeanInRange( ArrivalAndDepartureInstance instance, long timeFrom, long timeTo) { if (timeFrom <= instance.getScheduledArrivalTime() && instance.getScheduledArrivalTime() <= timeTo) return true; if (timeFrom <= instance.getScheduledDepartureTime() && instance.getScheduledDepartureTime() <= timeTo) return true; if (instance.isPredictedArrivalTimeSet() && timeFrom <= instance.getPredictedArrivalTime() && instance.getPredictedArrivalTime() <= timeTo) return true; if (instance.isPredictedDepartureTimeSet() && timeFrom <= instance.getPredictedDepartureTime() && instance.getPredictedDepartureTime() <= timeTo) return true; return false; } private boolean isFrequencyBasedArrivalInRange(BlockInstance blockInstance, FrequencyEntry frequency, long fromReduced, long toReduced) { long startTime = blockInstance.getServiceDate() + frequency.getStartTime() * 1000; long endTime = blockInstance.getServiceDate() + frequency.getEndTime() * 1000; return fromReduced <= endTime && startTime <= toReduced; } private boolean isArrivalAndDeparturePairInRange( ArrivalAndDepartureInstance instanceFrom, ArrivalAndDepartureInstance instanceTo, Date timeFrom, Date timeTo, boolean findDepartures) { ArrivalAndDepartureInstance instance = findDepartures ? instanceFrom : instanceTo; if (timeFrom != null) { boolean schedInRange = instance.getScheduledDepartureTime() >= timeFrom.getTime(); boolean realTimeInRange = true; if (instance.isPredictedDepartureTimeSet()) realTimeInRange = instance.getPredictedDepartureTime() >= timeFrom.getTime(); if (!(schedInRange || realTimeInRange)) return false; } if (timeTo != null) { boolean schedInRange = instance.getScheduledArrivalTime() <= timeTo.getTime(); boolean realTimeInRange = true; if (instance.isPredictedArrivalTimeSet()) realTimeInRange = instance.getPredictedArrivalTime() <= timeTo.getTime(); if (!(schedInRange || realTimeInRange)) return false; } return true; } private ArrivalAndDepartureInstance createArrivalAndDepartureForStopTimeInstance( StopTimeInstance sti, long prevFrequencyTime) { ArrivalAndDepartureInstance instance = createArrivalAndDeparture(sti, prevFrequencyTime, sti.getFrequencyOffset()); instance.setBlockSequence(sti.getBlockSequence()); return instance; } private ArrivalAndDepartureInstance createArrivalAndDeparture( BlockInstance blockInstance, AgencyAndId tripId, AgencyAndId stopId, int stopSequence, long serviceDate, int timeOfServiceDate, long prevFrequencyTime) { BlockTripInstance blockTripInstance = BlockTripInstanceLibrary.getBlockTripInstance( blockInstance, tripId); if (blockTripInstance == null) return null; BlockStopTimeEntry blockStopTime = getBlockStopTime(blockTripInstance, stopId, stopSequence, timeOfServiceDate); StopTimeInstance stopTimeInstance = new StopTimeInstance(blockStopTime, blockTripInstance.getState()); return createArrivalAndDeparture(stopTimeInstance, prevFrequencyTime, StopTimeInstance.UNSPECIFIED_FREQUENCY_OFFSET); } private BlockStopTimeEntry getBlockStopTime( BlockTripInstance blockTripInstance, AgencyAndId stopId, int stopSequence, int timeOfServiceDate) { /** * We don't iterate over block stop times directly because there is * performance penalty with instantiating each. Also note that this will * currently miss the case where a stop is visited twice in the same trip. */ BlockTripEntry blockTrip = blockTripInstance.getBlockTrip(); TripEntry trip = blockTrip.getTrip(); List<StopTimeEntry> stopTimes = trip.getStopTimes(); if (stopSequence > -1) { /** * If a stop sequence has been specified, we start our search at the * specified index, expanding our search until we find the target stop. We * allow this flexibility in the case of a bookmarked arrival-departure * where the stop sequence has changed slightly due to the addition or * subtraction of a previous stop. */ int offset = 0; while (true) { int before = stopSequence - offset; if (isMatch(stopTimes, stopId, before)) { return blockTrip.getStopTimes().get(before); } int after = stopSequence + offset; if (isMatch(stopTimes, stopId, after)) { return blockTrip.getStopTimes().get(after); } if (before < 0 && after >= stopTimes.size()) return null; offset++; } } else { Min<BlockStopTimeEntry> m = new Min<BlockStopTimeEntry>(); int index = 0; for (StopTimeEntry stopTime : stopTimes) { if (stopTime.getStop().getId().equals(stopId)) { int a = Math.abs(timeOfServiceDate - stopTime.getArrivalTime()); int b = Math.abs(timeOfServiceDate - stopTime.getDepartureTime()); int delta = Math.min(a, b); m.add(delta, blockTrip.getStopTimes().get(index)); } index++; } if (m.isEmpty()) return null; return m.getMinElement(); } } private boolean isMatch(List<StopTimeEntry> stopTimes, AgencyAndId stopId, int index) { if (index < 0 || index >= stopTimes.size()) return false; StopTimeEntry stopTime = stopTimes.get(index); StopEntry stop = stopTime.getStop(); return stop.getId().equals(stopId); } private ArrivalAndDepartureInstance createArrivalAndDeparture( StopTimeInstance stopTimeInstance, long prevFrequencyTime, int frequencyOffset) { ArrivalAndDepartureTime scheduledTime = getScheduledTime(stopTimeInstance, prevFrequencyTime, frequencyOffset); return new ArrivalAndDepartureInstance(stopTimeInstance, scheduledTime); } /** * Constructs an {@link ArrivalAndDepartureTime} object for the specified * {@link BlockInstance} and {@link BlockStopTimeEntry}. * * For frequency-based trips, the calculation is a bit complicated. * * * @param blockInstance * @param blockStopTime * @param prevFrequencyTime * @param frequencyOffset * @return */ private ArrivalAndDepartureTime getScheduledTime( StopTimeInstance stopTimeInstance, long prevFrequencyTime, int frequencyOffset) { FrequencyEntry frequency = stopTimeInstance.getFrequency(); if (frequency == null) { return ArrivalAndDepartureTime.getScheduledTime(stopTimeInstance); } else if (StopTimeInstance.isFrequencyOffsetSpecified(frequencyOffset)) { return ArrivalAndDepartureTime.getScheduledTime( stopTimeInstance.getServiceDate(), stopTimeInstance.getStopTime(), frequencyOffset); } else { long departureTime = prevFrequencyTime + frequency.getHeadwaySecs() * 1000 / 2; long freqStartTime = stopTimeInstance.getServiceDate() + frequency.getStartTime() * 1000; long freqEndTime = stopTimeInstance.getServiceDate() + frequency.getEndTime() * 1000; if (departureTime < freqStartTime) departureTime = freqStartTime; if (departureTime > freqEndTime) departureTime = freqEndTime; /** * We need to make sure the arrival time is adjusted relative to the * departure time and the layover at the stop. */ BlockStopTimeEntry blockStopTime = stopTimeInstance.getStopTime(); StopTimeEntry stopTime = blockStopTime.getStopTime(); int delta = stopTime.getDepartureTime() - stopTime.getArrivalTime(); long arrivalTime = departureTime - delta * 1000; return new ArrivalAndDepartureTime(arrivalTime, departureTime); } } }