/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * Copyright (C) 2011 <inf71391@gmail.com> * Copyright (C) 2012 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; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.onebusaway.collections.CollectionsLibrary; import org.onebusaway.collections.FactoryMap; import org.onebusaway.collections.Min; import org.onebusaway.collections.Range; import org.onebusaway.container.ConfigurationParameter; import org.onebusaway.geospatial.model.CoordinatePoint; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.realtime.api.TimepointPredictionRecord; import org.onebusaway.realtime.api.VehicleLocationRecord; import org.onebusaway.transit_data_federation.model.TargetTime; import org.onebusaway.transit_data_federation.services.blocks.BlockCalendarService; import org.onebusaway.transit_data_federation.services.blocks.BlockInstance; import org.onebusaway.transit_data_federation.services.blocks.BlockVehicleLocationListener; import org.onebusaway.transit_data_federation.services.blocks.ScheduledBlockLocation; import org.onebusaway.transit_data_federation.services.blocks.ScheduledBlockLocationService; import org.onebusaway.transit_data_federation.services.realtime.BlockLocation; import org.onebusaway.transit_data_federation.services.realtime.BlockLocationListener; import org.onebusaway.transit_data_federation.services.realtime.BlockLocationService; import org.onebusaway.transit_data_federation.services.realtime.RealTimeHistoryService; import org.onebusaway.transit_data_federation.services.realtime.ScheduleDeviationSamples; import org.onebusaway.transit_data_federation.services.realtime.VehicleLocationCacheElement; import org.onebusaway.transit_data_federation.services.realtime.VehicleLocationCacheElements; import org.onebusaway.transit_data_federation.services.realtime.VehicleLocationRecordCache; 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.StopEntry; import org.onebusaway.transit_data_federation.services.transit_graph.StopTimeEntry; import org.onebusaway.transit_data_federation.services.transit_graph.TransitGraphDao; import org.onebusaway.transit_data_federation.services.transit_graph.TripEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.stereotype.Component; /** * Implementation for {@link BlockLocationService}. Keeps a recent cache of * {@link BlockLocationRecord} records for current queries and can access * database persisted records for queries in the past. * * @author bdferris * @see BlockLocationService */ @Component @ManagedResource("org.onebusaway.transit_data_federation.impl.realtime:name=BlockLocationServiceImpl") public class BlockLocationServiceImpl implements BlockLocationService, BlockVehicleLocationListener { private static Logger _log = LoggerFactory.getLogger(BlockLocationServiceImpl.class); private VehicleLocationRecordCache _cache; private BlockLocationRecordDao _blockLocationRecordDao; private TransitGraphDao _transitGraphDao; private ScheduledBlockLocationService _scheduledBlockLocationService; private BlockCalendarService _blockCalendarService; private RealTimeHistoryService _realTimeHistoryService; private List<BlockLocationListener> _blockLocationListeners = Collections.emptyList(); /** * By default, we keep around 20 minutes of cache entries */ private int _blockLocationRecordCacheWindowSize = 20 * 60; private int _predictionCacheMaxOffset = 5 * 60; private int _blockInstanceMatchingWindow = 60 * 60 * 1000; /** * When true, we will interpolate the current location of a transit vehicle * based on estimated schedule deviation information. If false, we will use * the last known location of the bus as the position. */ private boolean _locationInterpolation = true; /** * When true, we attempt to interpolate the current location of the vehicle * given the most recent distance-along-block update. This parameter has been * deprecated in favor of the more-general {@link #_locationInterpolation} * parameter. */ @Deprecated private boolean _distanceAlongBlockLocationInterpolation = false; /** * Should block location records be stored to the database? */ private boolean _persistBlockLocationRecords = false; /** * We queue up block location records so they can be bulk persisted to the * database */ private List<BlockLocationRecord> _recordPersistenceQueue = new ArrayList<BlockLocationRecord>(); /** * Used to schedule periodic flushes to the database of the block location * records queue */ private ScheduledExecutorService _executor = Executors.newSingleThreadScheduledExecutor(); /** * Block location record persistence stats - last record insert duration */ private volatile long _lastInsertDuration = 0; /** * Block location record persistence stats - last record insert count */ private volatile long _lastInsertCount = 0; /** * Records the number of times block location record cache requests fall * through to the database */ private AtomicInteger _blockLocationRecordPersistentStoreAccessCount = new AtomicInteger(); @Autowired public void setVehicleLocationRecordCache(VehicleLocationRecordCache cache) { _cache = cache; } @Autowired public void setBlockLocationRecordDao( BlockLocationRecordDao blockLocationRecordDao) { _blockLocationRecordDao = blockLocationRecordDao; } @Autowired public void setTransitGraphDao(TransitGraphDao transitGraphDao) { _transitGraphDao = transitGraphDao; } @Autowired public void setScheduledBlockLocationService( ScheduledBlockLocationService scheduleBlockLocationService) { _scheduledBlockLocationService = scheduleBlockLocationService; } @Autowired public void setBlockCalendarService(BlockCalendarService blockCalendarService) { _blockCalendarService = blockCalendarService; } @Autowired public void setRealTimeHistoryService( RealTimeHistoryService realTimeHistoryService) { _realTimeHistoryService = realTimeHistoryService; } @Autowired public void setBlockLocationListeners(List<BlockLocationListener> listeners) { _blockLocationListeners = listeners; } /** * Controls how far back in time we include records in the * {@link BlockLocationRecordCollection} for each active trip. * * @param windowSize in seconds */ @ConfigurationParameter public void setBlockLocationRecordCacheWindowSize(int windowSize) { _blockLocationRecordCacheWindowSize = windowSize; } /** * Should we persist {@link BlockLocationRecord} records to an underlying * datastore. Useful if you wish to query trip status for historic analysis. * * @param persistTripTimePredictions */ @ConfigurationParameter public void setPersistBlockLocationRecords(boolean persistBlockLocationRecords) { _persistBlockLocationRecords = persistBlockLocationRecords; } /** * When true, we will interpolate the current location of a transit vehicle * based on the last know location of the bus and the schedule deviation of * the bus at the time. If false, we will use the last known location of the * bus as the current location. * * @param locationInterpolation */ @ConfigurationParameter public void setLocationInterpolation(boolean locationInterpolation) { _locationInterpolation = locationInterpolation; } /** * @param distanceAlongBlockLocationInterpolation * * @deprecated in favor of the more general * {@link #setLocationInterpolation(boolean)} configuration * method. */ @Deprecated public void setDistanceAlongBlockLocationInterpolation( boolean distanceAlongBlockLocationInterpolation) { _distanceAlongBlockLocationInterpolation = distanceAlongBlockLocationInterpolation; } /**** * JMX Attributes ****/ @ManagedAttribute public long getLastInsertDuration() { return _lastInsertDuration; } @ManagedAttribute public long getLastInsertCount() { return _lastInsertCount; } @ManagedAttribute public long getBlockLocationRecordPersistentStoreAccessCount() { return _blockLocationRecordPersistentStoreAccessCount.get(); } /**** * Setup and Teardown ****/ @PostConstruct public void start() { if (_persistBlockLocationRecords) _executor.scheduleAtFixedRate(new PredictionWriter(), 0, 1, TimeUnit.SECONDS); } @PreDestroy public void stop() { _executor.shutdownNow(); } /**** * {@link BlockVehicleLocationListener} Interface ****/ @Override public void handleVehicleLocationRecord(VehicleLocationRecord record) { BlockInstance instance = getVehicleLocationRecordAsBlockInstance(record); if (instance != null) { ScheduledBlockLocation scheduledBlockLocation = getScheduledBlockLocationForVehicleLocationRecord( record, instance); if (!record.isScheduleDeviationSet()) { int deviation = (int) ((record.getTimeOfRecord() - record.getServiceDate()) / 1000 - scheduledBlockLocation.getScheduledTime()); record.setScheduleDeviation(deviation); } ScheduleDeviationSamples samples = _realTimeHistoryService.sampleScheduleDeviationsForVehicle( instance, record, scheduledBlockLocation); putBlockLocationRecord(instance, record, scheduledBlockLocation, samples); } } @Override public void resetVehicleLocation(AgencyAndId vehicleId) { _cache.clearRecordsForVehicleId(vehicleId); } /**** * {@link BlockLocationService} Interface ****/ @Override public BlockLocation getLocationForBlockInstance(BlockInstance blockInstance, TargetTime time) { List<VehicleLocationCacheElements> records = getBlockLocationRecordCollectionForBlock( blockInstance, time); VehicleLocationCacheElements record = null; if (!records.isEmpty()) record = records.get(0); // TODO : find a better way to pick? return getBlockLocation(blockInstance, record, null, time.getTargetTime()); } @Override public BlockLocation getLocationForBlockInstanceAndScheduledBlockLocation( BlockInstance blockInstance, ScheduledBlockLocation scheduledLocation, long targetTime) { return getBlockLocation(blockInstance, null, scheduledLocation, targetTime); } @Override public List<BlockLocation> getLocationsForBlockInstance( BlockInstance blockInstance, TargetTime time) { List<VehicleLocationCacheElements> records = getBlockLocationRecordCollectionForBlock( blockInstance, time); List<BlockLocation> locations = new ArrayList<BlockLocation>(); for (VehicleLocationCacheElements cacheRecord : records) { BlockLocation location = getBlockLocation(blockInstance, cacheRecord, null, time.getTargetTime()); if (location != null) locations.add(location); } return locations; } @Override public Map<AgencyAndId, List<BlockLocation>> getLocationsForBlockInstance( BlockInstance blockInstance, List<Date> times, long currentTime) { Map<AgencyAndId, List<BlockLocation>> locationsByVehicleId = new FactoryMap<AgencyAndId, List<BlockLocation>>( new ArrayList<BlockLocation>()); for (Date time : times) { TargetTime tt = new TargetTime(time.getTime(), currentTime); List<VehicleLocationCacheElements> records = getBlockLocationRecordCollectionForBlock( blockInstance, tt); for (VehicleLocationCacheElements cacheRecord : records) { BlockLocation location = getBlockLocation(blockInstance, cacheRecord, null, time.getTime()); if (location != null) { locationsByVehicleId.get(location.getVehicleId()).add(location); } } } return locationsByVehicleId; } @Override public BlockLocation getScheduledLocationForBlockInstance( BlockInstance blockInstance, long targetTime) { return getBlockLocation(blockInstance, null, null, targetTime); } @Override public BlockLocation getLocationForVehicleAndTime(AgencyAndId vehicleId, TargetTime targetTime) { List<VehicleLocationCacheElements> cacheRecords = getBlockLocationRecordCollectionForVehicle( vehicleId, targetTime); // TODO : We might take a bit more care in picking the collection if // multiple collections are returned for (VehicleLocationCacheElements cacheRecord : cacheRecords) { BlockInstance blockInstance = cacheRecord.getBlockInstance(); BlockLocation location = getBlockLocation(blockInstance, cacheRecord, null, targetTime.getTargetTime()); if (location != null) return location; } return null; } /**** * Private Methods ****/ private BlockInstance getVehicleLocationRecordAsBlockInstance( VehicleLocationRecord record) { AgencyAndId blockId = record.getBlockId(); if (blockId == null) { AgencyAndId tripId = record.getTripId(); if (tripId == null) throw new IllegalArgumentException( "at least one of blockId or tripId must be specified for VehicleLocationRecord"); TripEntry tripEntry = _transitGraphDao.getTripEntryForId(tripId); if (tripEntry == null) throw new IllegalArgumentException("trip not found with id=" + tripId); BlockEntry block = tripEntry.getBlock(); blockId = block.getId(); } if (record.getServiceDate() == 0) throw new IllegalArgumentException("you must specify a serviceDate"); if (record.getTimeOfRecord() == 0) throw new IllegalArgumentException("you must specify a record time"); BlockInstance blockInstance = getBestBlockForRecord(blockId, record.getServiceDate(), record.getTimeOfRecord()); return blockInstance; } private BlockInstance getBestBlockForRecord(AgencyAndId blockId, long serviceDate, long timeOfRecord) { long timeFrom = timeOfRecord - _blockInstanceMatchingWindow; long timeTo = timeOfRecord + _blockInstanceMatchingWindow; List<BlockInstance> blocks = _blockCalendarService.getActiveBlocks(blockId, timeFrom, timeTo); if (blocks.isEmpty()) return null; else if (blocks.size() == 1) return blocks.get(0); Min<BlockInstance> m = new Min<BlockInstance>(); for (BlockInstance block : blocks) { long delta = Math.abs(block.getServiceDate() - serviceDate); m.add(delta, block); } return m.getMinElement(); } /** * We add the {@link BlockPositionRecord} to the local cache and persist it to * a back-end data-store if necessary * * @param scheduledBlockLocation TODO * @param samples */ private void putBlockLocationRecord(BlockInstance blockInstance, VehicleLocationRecord record, ScheduledBlockLocation scheduledBlockLocation, ScheduleDeviationSamples samples) { // Cache the result VehicleLocationCacheElements elements = _cache.addRecord(blockInstance, record, scheduledBlockLocation, samples); if (!CollectionsLibrary.isEmpty(_blockLocationListeners)) { /** * We only fill in the block location if we have listeners */ BlockLocation location = getBlockLocation(blockInstance, elements, scheduledBlockLocation, record.getTimeOfRecord()); if (location != null) { for (BlockLocationListener listener : _blockLocationListeners) { listener.handleBlockLocation(location); } } } if (_persistBlockLocationRecords) { List<BlockLocationRecord> blockLocationRecords = getVehicleLocationRecordAsBlockLocationRecord( blockInstance, record, scheduledBlockLocation); addPredictionToPersistenceQueue(blockLocationRecords); } } /** * * @param blockInstance * @param scheduledBlockLocation TODO * @param targetTime * @param record * @return null if the effective scheduled block location cannot be determined */ private BlockLocation getBlockLocation(BlockInstance blockInstance, VehicleLocationCacheElements cacheElements, ScheduledBlockLocation scheduledLocation, long targetTime) { BlockLocation location = new BlockLocation(); location.setTime(targetTime); location.setBlockInstance(blockInstance); VehicleLocationCacheElement cacheElement = null; if (cacheElements != null) cacheElement = cacheElements.getElementForTimestamp(targetTime); if (cacheElement != null) { VehicleLocationRecord record = cacheElement.getRecord(); if (scheduledLocation == null) scheduledLocation = getScheduledBlockLocationForVehicleLocationCacheRecord( blockInstance, cacheElement, targetTime); if (scheduledLocation != null) { location.setEffectiveScheduleTime(scheduledLocation.getScheduledTime()); location.setDistanceAlongBlock(scheduledLocation.getDistanceAlongBlock()); } location.setPredicted(true); location.setLastUpdateTime(record.getTimeOfRecord()); location.setLastLocationUpdateTime(record.getTimeOfLocationUpdate()); location.setScheduleDeviation(record.getScheduleDeviation()); location.setScheduleDeviations(cacheElement.getScheduleDeviations()); if (record.isCurrentLocationSet()) { CoordinatePoint p = new CoordinatePoint(record.getCurrentLocationLat(), record.getCurrentLocationLon()); location.setLastKnownLocation(p); } location.setOrientation(record.getCurrentOrientation()); location.setPhase(record.getPhase()); location.setStatus(record.getStatus()); location.setVehicleId(record.getVehicleId()); List<TimepointPredictionRecord> timepointPredictions = record.getTimepointPredictions(); location.setTimepointPredictions(timepointPredictions); if (timepointPredictions != null && !timepointPredictions.isEmpty()) { SortedMap<Integer, Double> scheduleDeviations = new TreeMap<Integer, Double>(); BlockConfigurationEntry blockConfig = blockInstance.getBlock(); for (TimepointPredictionRecord tpr : timepointPredictions) { AgencyAndId stopId = tpr.getTimepointId(); long predictedTime; if (tpr.getTimepointPredictedDepartureTime() != -1) { predictedTime = tpr.getTimepointPredictedDepartureTime(); } else { predictedTime = tpr.getTimepointPredictedArrivalTime(); } if (stopId == null || predictedTime == 0) continue; for (BlockStopTimeEntry blockStopTime : blockConfig.getStopTimes()) { StopTimeEntry stopTime = blockStopTime.getStopTime(); StopEntry stop = stopTime.getStop(); // StopSequence equals to -1 when there is no stop sequence in the GTFS-rt if (stopId.equals(stop.getId()) && stopTime.getTrip().getId().equals(tpr.getTripId()) && (tpr.getStopSequence() == -1 || stopTime.getSequence() == tpr.getStopSequence())) { int arrivalOrDepartureTime; // We currently use the scheduled arrival time of the stop as the search index // This MUST be consistent with the index search in ArrivalAndSepartureServiceImpl.getBestScheduleDeviation() int index = stopTime.getArrivalTime(); if (tpr.getTimepointPredictedDepartureTime() != -1) { // Prefer departure time, because if both exist departure deviations should be the ones propagated downstream arrivalOrDepartureTime = stopTime.getDepartureTime(); } else { arrivalOrDepartureTime = stopTime.getArrivalTime(); } int deviation = (int) ((predictedTime - blockInstance.getServiceDate()) / 1000 - arrivalOrDepartureTime); scheduleDeviations.put(index, (double) deviation); } } } double[] scheduleTimes = new double[scheduleDeviations.size()]; double[] scheduleDeviationMus = new double[scheduleDeviations.size()]; double[] scheduleDeviationSigmas = new double[scheduleDeviations.size()]; int index = 0; for (Map.Entry<Integer, Double> entry : scheduleDeviations.entrySet()) { scheduleTimes[index] = entry.getKey(); scheduleDeviationMus[index] = entry.getValue(); index++; } ScheduleDeviationSamples samples = new ScheduleDeviationSamples( scheduleTimes, scheduleDeviationMus, scheduleDeviationSigmas); location.setScheduleDeviations(samples); } } else { if (scheduledLocation == null) scheduledLocation = getScheduledBlockLocationForBlockInstance( blockInstance, targetTime); } /** * Will be null in the following cases: * * 1) When the effective schedule time is beyond the last scheduled stop * time for the block. * * 2) When the effective distance along block is outside the range of the * block's shape. */ if (scheduledLocation == null) return null; location.setInService(scheduledLocation.isInService()); location.setActiveTrip(scheduledLocation.getActiveTrip()); location.setLocation(scheduledLocation.getLocation()); location.setOrientation(scheduledLocation.getOrientation()); location.setScheduledDistanceAlongBlock(scheduledLocation.getDistanceAlongBlock()); location.setClosestStop(scheduledLocation.getClosestStop()); location.setClosestStopTimeOffset(scheduledLocation.getClosestStopTimeOffset()); location.setNextStop(scheduledLocation.getNextStop()); location.setNextStopTimeOffset(scheduledLocation.getNextStopTimeOffset()); location.setPreviousStop(scheduledLocation.getPreviousStop()); return location; } /**** * {@link ScheduledBlockLocation} Methods ****/ private ScheduledBlockLocation getScheduledBlockLocationForVehicleLocationRecord( VehicleLocationRecord record, BlockInstance blockInstance) { BlockConfigurationEntry blockConfig = blockInstance.getBlock(); long serviceDate = blockInstance.getServiceDate(); long targetTime = record.getTimeOfRecord(); int scheduledTime = (int) ((targetTime - serviceDate) / 1000); if (record.isScheduleDeviationSet()) { /** * Effective scheduled time is the point that a transit vehicle is at on * its schedule, with schedule deviation taken into account. So if it's * 100 minutes into the current service date and the bus is running 10 * minutes late, it's actually at the 90 minute point in its scheduled * operation. */ int effectiveScheduledTime = (int) (scheduledTime - record.getScheduleDeviation()); return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( blockConfig, effectiveScheduledTime); } if (record.isDistanceAlongBlockSet()) { return _scheduledBlockLocationService.getScheduledBlockLocationFromDistanceAlongBlock( blockConfig, record.getDistanceAlongBlock()); } return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( blockConfig, scheduledTime); } private ScheduledBlockLocation getScheduledBlockLocationForBlockInstance( BlockInstance blockInstance, long targetTime) { BlockConfigurationEntry blockConfig = blockInstance.getBlock(); long serviceDate = blockInstance.getServiceDate(); int scheduledTime = (int) ((targetTime - serviceDate) / 1000); return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( blockConfig, scheduledTime); } private ScheduledBlockLocation getScheduledBlockLocationForVehicleLocationCacheRecord( BlockInstance blockInstance, VehicleLocationCacheElement cacheElement, long targetTime) { VehicleLocationRecord record = cacheElement.getRecord(); ScheduledBlockLocation scheduledBlockLocation = cacheElement.getScheduledBlockLocation(); BlockConfigurationEntry blockConfig = blockInstance.getBlock(); long serviceDate = blockInstance.getServiceDate(); int scheduledTime = (int) ((targetTime - serviceDate) / 1000); /** * If location interpolation has been turned off, then we assume the vehicle * is at its last known location, so we return that if it's been stored with * the cache element. */ if (!_locationInterpolation && scheduledBlockLocation != null) { return scheduledBlockLocation; } if (record.isScheduleDeviationSet()) { /** * Effective scheduled time is the point that a transit vehicle is at on * its schedule, with schedule deviation taken into account. So if it's * 100 minutes into the current service date and the bus is running 10 * minutes late, it's actually at the 90 minute point in its scheduled * operation. */ int effectiveScheduledTime = (int) (scheduledTime - record.getScheduleDeviation()); if (scheduledBlockLocation != null && scheduledBlockLocation.getScheduledTime() <= effectiveScheduledTime) { return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( scheduledBlockLocation, effectiveScheduledTime); } return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( blockConfig, effectiveScheduledTime); } if (record.isDistanceAlongBlockSet()) { if ((_locationInterpolation || _distanceAlongBlockLocationInterpolation) && scheduledBlockLocation != null && scheduledBlockLocation.getDistanceAlongBlock() <= record.getDistanceAlongBlock()) { int ellapsedTime = (int) ((targetTime - record.getTimeOfRecord()) / 1000); if (ellapsedTime >= 0) { int effectiveScheduledTime = scheduledBlockLocation.getScheduledTime() + ellapsedTime; return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( blockConfig, effectiveScheduledTime); } return _scheduledBlockLocationService.getScheduledBlockLocationFromDistanceAlongBlock( scheduledBlockLocation, record.getDistanceAlongBlock()); } return _scheduledBlockLocationService.getScheduledBlockLocationFromDistanceAlongBlock( blockConfig, record.getDistanceAlongBlock()); } return _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime( blockConfig, scheduledTime); } private List<VehicleLocationCacheElements> getBlockLocationRecordCollectionForBlock( BlockInstance blockInstance, TargetTime time) { return getBlockLocationRecordCollections(new BlockInstanceStrategy( blockInstance), time); } private List<VehicleLocationCacheElements> getBlockLocationRecordCollectionForVehicle( AgencyAndId vehicleId, TargetTime time) { return getBlockLocationRecordCollections(new VehicleIdRecordStrategy( vehicleId), time); } private List<VehicleLocationCacheElements> getBlockLocationRecordCollections( RecordStrategy strategy, TargetTime time) { List<VehicleLocationCacheElements> entries = strategy.getRecordsFromCache(); if (!entries.isEmpty()) { List<VehicleLocationCacheElements> inRange = new ArrayList<VehicleLocationCacheElements>(); long offset = _predictionCacheMaxOffset * 1000; for (VehicleLocationCacheElements elements : entries) { if (elements.isEmpty()) continue; Range range = elements.getTimeRange(); long tFrom = (long) (range.getMin() - offset); long tTo = (long) (range.getMax() + offset); if (tFrom <= time.getCurrentTime() && time.getCurrentTime() <= tTo) inRange.add(elements); } if (!inRange.isEmpty()) return inRange; } long offset = _blockLocationRecordCacheWindowSize * 1000 / 2; // We only consult persisted cache entries if the requested target time is // not within our current cache window boolean outOfRange = time.getTargetTime() + offset < time.getCurrentTime() || time.getCurrentTime() < time.getTargetTime() - offset; if (outOfRange && _persistBlockLocationRecords) { _blockLocationRecordPersistentStoreAccessCount.incrementAndGet(); long fromTime = time.getTargetTime() - offset; long toTime = time.getTargetTime() + offset; List<BlockLocationRecord> predictions = strategy.getRecordsFromDao( fromTime, toTime); if (!predictions.isEmpty()) { Map<BlockLocationRecordKey, List<BlockLocationRecord>> recordsByKey = groupRecord(predictions); List<VehicleLocationCacheElements> allCollections = new ArrayList<VehicleLocationCacheElements>(); for (Map.Entry<BlockLocationRecordKey, List<BlockLocationRecord>> entry : recordsByKey.entrySet()) { BlockLocationRecordKey key = entry.getKey(); List<BlockLocationRecord> blockLocationRecords = entry.getValue(); List<VehicleLocationCacheElements> someRecords = getBlockLocationRecordsAsVehicleLocationRecords( key.getBlockInstance(), blockLocationRecords); allCollections.addAll(someRecords); } return allCollections; } } return Collections.emptyList(); } private List<BlockLocationRecord> getVehicleLocationRecordAsBlockLocationRecord( BlockInstance blockInstance, VehicleLocationRecord record, ScheduledBlockLocation scheduledBlockLocation) { BlockLocationRecord.Builder builder = BlockLocationRecord.builder(); if (scheduledBlockLocation != null) { BlockTripEntry activeTrip = scheduledBlockLocation.getActiveTrip(); builder.setTripId(activeTrip.getTrip().getId()); builder.setBlockId(activeTrip.getBlockConfiguration().getBlock().getId()); double distanceAlongBlock = scheduledBlockLocation.getDistanceAlongBlock(); builder.setDistanceAlongBlock(distanceAlongBlock); double distanceAlongTrip = distanceAlongBlock - activeTrip.getDistanceAlongBlock(); builder.setDistanceAlongTrip(distanceAlongTrip); } if (record.getBlockId() != null) builder.setBlockId(record.getBlockId()); if (record.getTripId() != null) builder.setTripId(record.getTripId()); builder.setTime(record.getTimeOfRecord()); builder.setServiceDate(record.getServiceDate()); if (record.isScheduleDeviationSet()) builder.setScheduleDeviation(record.getScheduleDeviation()); if (record.isDistanceAlongBlockSet()) { double distanceAlongBlock = record.getDistanceAlongBlock(); builder.setDistanceAlongBlock(distanceAlongBlock); AgencyAndId tripId = record.getTripId(); if (tripId != null) { BlockConfigurationEntry block = blockInstance.getBlock(); for (BlockTripEntry blockTrip : block.getTrips()) { TripEntry trip = blockTrip.getTrip(); if (trip.getId().equals(tripId)) { double distanceAlongTrip = distanceAlongBlock - blockTrip.getDistanceAlongBlock(); builder.setDistanceAlongTrip(distanceAlongTrip); } } } } if (record.isCurrentLocationSet()) { builder.setLocationLat(record.getCurrentLocationLat()); builder.setLocationLon(record.getCurrentLocationLon()); } if (record.isCurrentOrientationSet()) builder.setOrientation(record.getCurrentOrientation()); builder.setPhase(record.getPhase()); builder.setStatus(record.getStatus()); builder.setVehicleId(record.getVehicleId()); List<TimepointPredictionRecord> predictions = record.getTimepointPredictions(); if (predictions == null || predictions.isEmpty()) return Arrays.asList(builder.create()); List<BlockLocationRecord> results = new ArrayList<BlockLocationRecord>(); for (TimepointPredictionRecord tpr : predictions) { builder.setTimepointId(tpr.getTimepointId()); builder.setTimepointScheduledTime(tpr.getTimepointScheduledTime()); builder.setTimepointPredictedArrivalTime(tpr.getTimepointPredictedArrivalTime()); builder.setTimepointPredictedDepartureTime(tpr.getTimepointPredictedDepartureTime()); results.add(builder.create()); } return results; } private List<VehicleLocationCacheElements> getBlockLocationRecordsAsVehicleLocationRecords( BlockInstance blockInstance, List<BlockLocationRecord> records) { List<VehicleLocationCacheElements> results = new ArrayList<VehicleLocationCacheElements>(); for (BlockLocationRecord record : records) { VehicleLocationRecord vlr = new VehicleLocationRecord(); vlr.setBlockId(blockInstance.getBlock().getBlock().getId()); if (record.isLocationSet()) { vlr.setCurrentLocationLat(record.getLocationLat()); vlr.setCurrentLocationLon(record.getLocationLon()); } if (record.isOrientationSet()) vlr.setCurrentOrientation(record.getOrientation()); if (record.isDistanceAlongBlockSet()) vlr.setDistanceAlongBlock(record.getDistanceAlongBlock()); vlr.setPhase(record.getPhase()); if (record.isScheduleDeviationSet()) vlr.setScheduleDeviation(record.getScheduleDeviation()); vlr.setServiceDate(record.getServiceDate()); vlr.setStatus(record.getStatus()); vlr.setTimeOfRecord(record.getTime()); vlr.setVehicleId(record.getVehicleId()); VehicleLocationCacheElement element = new VehicleLocationCacheElement( vlr, null, null); VehicleLocationCacheElements elements = new VehicleLocationCacheElements( blockInstance, element); results.add(elements); } return results; } private Map<BlockLocationRecordKey, List<BlockLocationRecord>> groupRecord( List<BlockLocationRecord> predictions) { Map<BlockLocationRecordKey, List<BlockLocationRecord>> recordsByKey = new FactoryMap<BlockLocationRecordKey, List<BlockLocationRecord>>( new ArrayList<BlockLocationRecord>()); for (BlockLocationRecord record : predictions) { AgencyAndId blockId = record.getBlockId(); long serviceDate = record.getServiceDate(); BlockInstance blockInstance = _blockCalendarService.getBlockInstance( blockId, serviceDate); if (blockInstance != null) { BlockLocationRecordKey key = new BlockLocationRecordKey(blockInstance, record.getVehicleId()); recordsByKey.get(key).add(record); } } return recordsByKey; } private void addPredictionToPersistenceQueue(List<BlockLocationRecord> records) { synchronized (_recordPersistenceQueue) { _recordPersistenceQueue.addAll(records); } } private List<BlockLocationRecord> getPredictionPersistenceQueue() { synchronized (_recordPersistenceQueue) { List<BlockLocationRecord> queue = new ArrayList<BlockLocationRecord>( _recordPersistenceQueue); _recordPersistenceQueue.clear(); return queue; } } private class PredictionWriter implements Runnable { @Override public void run() { try { List<BlockLocationRecord> queue = getPredictionPersistenceQueue(); if (queue.isEmpty()) return; long t1 = System.currentTimeMillis(); _blockLocationRecordDao.saveBlockLocationRecords(queue); long t2 = System.currentTimeMillis(); _lastInsertDuration = t2 - t1; _lastInsertCount = queue.size(); } catch (Throwable ex) { _log.error("error writing block location records to dao", ex); } } } private interface RecordStrategy { public List<VehicleLocationCacheElements> getRecordsFromCache(); public List<BlockLocationRecord> getRecordsFromDao(long fromTime, long toTime); } private class BlockInstanceStrategy implements RecordStrategy { private BlockInstance _blockInstance; public BlockInstanceStrategy(BlockInstance blockInstance) { _blockInstance = blockInstance; } @Override public List<VehicleLocationCacheElements> getRecordsFromCache() { return _cache.getRecordsForBlockInstance(_blockInstance); } @Override public List<BlockLocationRecord> getRecordsFromDao(long fromTime, long toTime) { BlockConfigurationEntry blockConfig = _blockInstance.getBlock(); BlockEntry block = blockConfig.getBlock(); return _blockLocationRecordDao.getBlockLocationRecordsForBlockServiceDateAndTimeRange( block.getId(), _blockInstance.getServiceDate(), fromTime, toTime); } } private class VehicleIdRecordStrategy implements RecordStrategy { private AgencyAndId _vehicleId; public VehicleIdRecordStrategy(AgencyAndId vehicleId) { _vehicleId = vehicleId; } public List<VehicleLocationCacheElements> getRecordsFromCache() { VehicleLocationCacheElements elementsForVehicleId = _cache.getRecordForVehicleId(_vehicleId); if (elementsForVehicleId == null) return Collections.emptyList(); return Arrays.asList(elementsForVehicleId); } public List<BlockLocationRecord> getRecordsFromDao(long fromTime, long toTime) { return _blockLocationRecordDao.getBlockLocationRecordsForVehicleAndTimeRange( _vehicleId, fromTime, toTime); } } }