/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * * 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.io.Serializable; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; import org.onebusaway.collections.adapter.AdapterLibrary; import org.onebusaway.collections.adapter.IAdapter; import org.onebusaway.geospatial.model.CoordinatePoint; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.realtime.api.EVehiclePhase; import org.onebusaway.transit_data_federation.services.blocks.BlockInstance; import org.onebusaway.utility.EOutOfRangeStrategy; import org.onebusaway.utility.InterpolationLibrary; /** * A collection of block location records from the same block/trip/vehicle over * a time range, designed to maintain a cache of recent records and easily * interpolate location and schedule deviations from specific timestamps * * @author bdferris */ public final class BlockLocationRecordCollection implements Serializable { private static final long serialVersionUID = 1L; private static final ScheduleDeviationAdapter _scheduleDeviationAdapter = new ScheduleDeviationAdapter(); private static final DistanceAlongBlockAdapter _distanceAlongBlockAdapter = new DistanceAlongBlockAdapter(); private BlockInstance blockInstance; private AgencyAndId vehicleId; private final long fromTime; private final long toTime; /** * When we are running in simulator mode, we might be simulating a trip that * occurred in the past, which makes pruning records based on the currnt time * tricky. To deal with this, we record the measured time of the last updaate * according to our local clock, as opposed to whatever the record indicated. */ private final long measuredLastUpdateTime; /** * Vehicle location records at particular points in time. Key = unix time ms */ private final SortedMap<Long, BlockLocationRecord> records; public BlockLocationRecordCollection(long fromTime, long toTime, SortedMap<Long, BlockLocationRecord> records) { this.fromTime = fromTime; this.toTime = toTime; this.records = records; this.measuredLastUpdateTime = System.currentTimeMillis(); } public BlockLocationRecordCollection(long fromTime, long toTime) { this(fromTime, toTime, new TreeMap<Long, BlockLocationRecord>()); } /** * Convenience method that creates a link * {@link BlockLocationRecordCollection} from a list of records * * @param records * @return a collection instance from the specified records */ public static BlockLocationRecordCollection createFromRecords( BlockInstance blockInstance, List<BlockLocationRecord> records) { if (records.isEmpty()) return null; long fromTime = Long.MAX_VALUE; long toTime = Long.MIN_VALUE; SortedMap<Long, BlockLocationRecord> map = new TreeMap<Long, BlockLocationRecord>(); AgencyAndId vehicleId = null; for (BlockLocationRecord record : records) { fromTime = Math.min(fromTime, record.getTime()); toTime = Math.max(toTime, record.getTime()); map.put(record.getTime(), record); vehicleId = checkVehicleId(vehicleId, record); } BlockLocationRecordCollection collection = new BlockLocationRecordCollection( fromTime, toTime, map); collection.blockInstance = blockInstance; collection.vehicleId = vehicleId; return collection; } public BlockInstance getBlockInstance() { return blockInstance; } public AgencyAndId getVehicleId() { return vehicleId; } public long getFromTime() { return fromTime; } public long getToTime() { return toTime; } public long getMeasuredLastUpdateTime() { return measuredLastUpdateTime; } public boolean isEmpty() { return records.isEmpty(); } public double getScheduleDeviationForTargetTime(long targetTime) { if (records.isEmpty()) return Double.NaN; SortedMap<Long, Double> m = AdapterLibrary.adaptSortedMap(records, _scheduleDeviationAdapter); return Math.round(InterpolationLibrary.interpolate(m, targetTime, EOutOfRangeStrategy.LAST_VALUE)); } public double getDistanceAlongBlockForTargetTime(long targetTime) { if (records.isEmpty()) return Double.NaN; SortedMap<Long, Double> m = AdapterLibrary.adaptSortedMap(records, _distanceAlongBlockAdapter); return InterpolationLibrary.interpolate(m, targetTime, EOutOfRangeStrategy.INTERPOLATE); } public CoordinatePoint getLastLocationForTargetTime(long targetTime) { BlockLocationRecord record = previousRecord(targetTime); if (record == null) return null; return record.getLocation(); } public double getLastOrientationForTargetTime(long targetTime) { BlockLocationRecord record = previousRecord(targetTime); if (record == null) return Double.NaN; return record.getOrientation(); } public EVehiclePhase getPhaseForTargetTime(long targetTime) { BlockLocationRecord record = previousRecord(targetTime); if (record == null) return null; return record.getPhase(); } public String getStatusForTargetTime(long targetTime) { BlockLocationRecord record = previousRecord(targetTime); if (record == null) return null; return record.getStatus(); } public long getLastUpdateTime(long targetTime) { if (records.isEmpty()) return 0; SortedMap<Long, BlockLocationRecord> headMap = records.headMap(targetTime + 1); if (headMap.isEmpty()) return records.firstKey(); return headMap.lastKey(); } public BlockLocationRecordCollection addRecord(BlockInstance blockInstance, BlockLocationRecord record, long windowSize) { AgencyAndId vehicleId = checkVehicleId(this.vehicleId, record); blockInstance = checkBlockInstance(this.blockInstance, blockInstance); long time = record.getTime(); long updatedFromTime = Math.min(fromTime, time); long updatedToTime = Math.max(toTime, time); long updatedWindowSize = updatedToTime - updatedFromTime; SortedMap<Long, BlockLocationRecord> updatedRecords = new TreeMap<Long, BlockLocationRecord>( this.records); updatedRecords.put(record.getTime(), record); if (updatedWindowSize > windowSize) { double ratio = ((double) windowSize) / updatedWindowSize; updatedFromTime = (long) (time - (time - updatedFromTime) * ratio); updatedToTime = (long) (time + (updatedToTime - time) * ratio); updatedRecords = submap(updatedFromTime, updatedToTime, updatedRecords); } BlockLocationRecordCollection collection = new BlockLocationRecordCollection( updatedFromTime, updatedToTime, updatedRecords); collection.blockInstance = blockInstance; collection.vehicleId = vehicleId; return collection; } /**** * Private Methods ****/ private <T> SortedMap<Long, T> submap(long updatedFromTime, long updatedToTime, SortedMap<Long, T> map) { // The +1 makes sure that we included the updatedToTime in the submap map = map.subMap(updatedFromTime, updatedToTime + 1); return new TreeMap<Long, T>(map); } private BlockLocationRecord previousRecord(long targetTime) { if (records.isEmpty()) return null; SortedMap<Long, BlockLocationRecord> headMap = records.headMap(targetTime + 1); if (headMap.isEmpty()) return null; return headMap.get(headMap.lastKey()); } private static BlockInstance checkBlockInstance(BlockInstance existing, BlockInstance blockInstance) { if (existing == null) return blockInstance; else if (!existing.equals(blockInstance)) { throw new IllegalArgumentException("blockInstance mismatch: expected=" + existing + " actual=" + blockInstance); } return blockInstance; } private static AgencyAndId checkVehicleId(AgencyAndId existing, BlockLocationRecord record) { if (existing == null) return record.getVehicleId(); else if (!existing.equals(record.getVehicleId())) throw new IllegalArgumentException("vehicleId mismatch: expected=" + existing + " actual=" + record.getVehicleId()); return existing; } /**** * Static Classes ****/ private static class ScheduleDeviationAdapter implements IAdapter<BlockLocationRecord, Double> { @Override public Double adapt(BlockLocationRecord source) { return source.getScheduleDeviation(); } } private static class DistanceAlongBlockAdapter implements IAdapter<BlockLocationRecord, Double> { @Override public Double adapt(BlockLocationRecord source) { return source.getDistanceAlongBlock(); } } }