/** * 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.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.onebusaway.geospatial.model.CoordinateBounds; import org.onebusaway.geospatial.model.CoordinatePoint; import org.onebusaway.geospatial.services.SphericalGeometryLibrary; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.transit_data.model.ListBean; import org.onebusaway.transit_data.model.realtime.CurrentVehicleEstimateBean; import org.onebusaway.transit_data.model.realtime.CurrentVehicleEstimateQueryBean; import org.onebusaway.transit_data.model.realtime.CurrentVehicleEstimateQueryBean.Record; import org.onebusaway.transit_data.model.trips.TripStatusBean; import org.onebusaway.transit_data_federation.impl.probability.DeviationModel; import org.onebusaway.transit_data_federation.model.TargetTime; import org.onebusaway.transit_data_federation.services.AgencyAndIdLibrary; import org.onebusaway.transit_data_federation.services.beans.TripDetailsBeanService; 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.BlockSequenceIndex; import org.onebusaway.transit_data_federation.services.blocks.BlockStatusService; import org.onebusaway.transit_data_federation.services.blocks.ScheduledBlockLocation; 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.CurrentVehicleEstimationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import cern.colt.list.DoubleArrayList; import cern.jet.stat.Descriptive; @Component class CurrentVehicleEstimationServiceImpl implements CurrentVehicleEstimationService { private static NumberFormat _format = new DecimalFormat("0.00"); private BlockCalendarService _blockCalendarService; private BlockGeospatialService _blockGeospatialService; private BlockStatusService _blockStatusService; private TripDetailsBeanService _tripStatusBeanService; private BlockLocationService _blockLocationService; @Autowired public void setBlockCalendarService(BlockCalendarService blockCalendarService) { _blockCalendarService = blockCalendarService; } @Autowired public void setBlockGeospatialService( BlockGeospatialService blockGeospatialService) { _blockGeospatialService = blockGeospatialService; } @Autowired public void setBlockStatusService(BlockStatusService blockStatusService) { _blockStatusService = blockStatusService; } @Autowired public void setTripStatusBeanService( TripDetailsBeanService tripStatusBeanService) { _tripStatusBeanService = tripStatusBeanService; } @Autowired public void setBlockLocationService(BlockLocationService blockLocationService) { _blockLocationService = blockLocationService; } /** * If a record is older than max window (in minutes), we don't consider it */ private int _maxWindow = 5; /** * The location accuracy must be at least this good (in meters) for us to * consider the record */ private double _maxAccuracy = 250; private DeviationModel _realTimeLocationDeviationModel = new DeviationModel( 200); private DeviationModel _scheduleOnlyLocationDeviationModel = new DeviationModel( 500); private DeviationModel _scheduleDeviationLateModel = new DeviationModel( 15 * 60); private DeviationModel _scheduleDeviationEarlyModel = new DeviationModel( 4 * 60); private int _maxTravelBackwardsTime = 2 * 60; /** * For every minute between two location updates, the amount of backwards * travel time is reduced by this much */ private double _maxTravelBackwardsDecayFactor = 0.4; private double _shortCutProbability = 0.5; @Override public ListBean<CurrentVehicleEstimateBean> getCurrentVehicleEstimates( CurrentVehicleEstimateQueryBean query) { long minT = System.currentTimeMillis() - _maxWindow * 60 * 1000; minT = 0; List<Record> records = getRecords(query.getRecords(), minT); if (records.isEmpty()) return new ListBean<CurrentVehicleEstimateBean>(); List<CurrentVehicleEstimateBean> beans = new ArrayList<CurrentVehicleEstimateBean>(); if (tryDirectMatchAgainstVehicleId(query, records, beans)) return new ListBean<CurrentVehicleEstimateBean>(beans, true); Map<Date, Record> recordsByTime = getRecordsByTimestamp(records); List<Date> timestamps = new ArrayList<Date>(recordsByTime.keySet()); Collections.sort(timestamps); if (tryDirectMatchAgainstBlockId(query, records, recordsByTime, timestamps, query.getMinProbability(), beans)) return new ListBean<CurrentVehicleEstimateBean>(beans, true); Set<BlockSequenceIndex> allIndices = getBlockSequenceIndicesForRecords(recordsByTime); for (BlockSequenceIndex index : allIndices) { Map<BlockInstance, List<List<BlockLocation>>> allLocations = _blockStatusService.getBlocksForIndex( index, timestamps); for (Map.Entry<BlockInstance, List<List<BlockLocation>>> entry : allLocations.entrySet()) { BlockInstance blockInstance = entry.getKey(); List<List<BlockLocation>> realTimeLocations = entry.getValue(); computeEstimatesForBlockInstance(records, recordsByTime, blockInstance, realTimeLocations, query.getMinProbability(), beans); } } Collections.sort(beans); return new ListBean<CurrentVehicleEstimateBean>(beans, false); } private void computeEstimatesForBlockInstance(List<Record> records, Map<Date, Record> recordsByTime, BlockInstance blockInstance, Collection<List<BlockLocation>> realTimeLocations, double minProbabilityForConsideration, List<CurrentVehicleEstimateBean> beans) { if (realTimeLocations.isEmpty()) { System.out.println(blockInstance.getBlock().getBlock().getId()); if (blockInstance.getBlock().getBlock().getId().toString().equals( "1_2535404")) System.out.println(" here"); computeCumulativeProbabilityForScheduledBlockLocations(records, blockInstance, minProbabilityForConsideration, beans); } else { /** * Iterate over the locations, with each grouping corresponding to a * particular vehicle */ for (List<BlockLocation> locations : realTimeLocations) { computeCumulativeProbabilityForRealTimeBlockLocations(recordsByTime, locations, minProbabilityForConsideration, beans); } } } /**** * Private Methods ****/ private boolean tryDirectMatchAgainstVehicleId( CurrentVehicleEstimateQueryBean query, List<Record> records, List<CurrentVehicleEstimateBean> beans) { if (query.getVehicleId() == null) return false; Record record = records.get(records.size() - 1); AgencyAndId vehicleId = AgencyAndIdLibrary.convertFromString(query.getVehicleId()); BlockLocation location = _blockLocationService.getLocationForVehicleAndTime( vehicleId, new TargetTime(record.getTimestamp())); if (location == null) return false; double d = SphericalGeometryLibrary.distance(record.getLocation(), location.getLocation()); double p = _realTimeLocationDeviationModel.probability(d); if (p < _shortCutProbability) return false; CurrentVehicleEstimateBean bean = new CurrentVehicleEstimateBean(); bean.setProbability(p); bean.setTripStatus(_tripStatusBeanService.getBlockLocationAsStatusBean( location, query.getTime())); beans.add(bean); return true; } private boolean tryDirectMatchAgainstBlockId( CurrentVehicleEstimateQueryBean query, List<Record> records, Map<Date, Record> recordsByTime, List<Date> timestamps, double minProbabilityForConsideration, List<CurrentVehicleEstimateBean> beans) { String blockIdAsString = query.getBlockId(); long serviceDate = query.getServiceDate(); if (blockIdAsString == null || serviceDate == 0) return false; AgencyAndId blockId = AgencyAndIdLibrary.convertFromString(blockIdAsString); BlockInstance blockInstance = _blockCalendarService.getBlockInstance( blockId, serviceDate); if (blockInstance == null) return false; Map<AgencyAndId, List<BlockLocation>> locationsForBlockInstance = _blockLocationService.getLocationsForBlockInstance( blockInstance, timestamps, query.getTime()); Collection<List<BlockLocation>> realTimeLocations = locationsForBlockInstance.values(); computeEstimatesForBlockInstance(records, recordsByTime, blockInstance, realTimeLocations, minProbabilityForConsideration, beans); return !beans.isEmpty(); } private void addResult(BlockLocation location, double cumulativeP, String debug, double minProbabilityForConsideration, List<CurrentVehicleEstimateBean> beans) { if (cumulativeP >= minProbabilityForConsideration) { CurrentVehicleEstimateBean bean = new CurrentVehicleEstimateBean(); bean.setProbability(cumulativeP); TripStatusBean status = _tripStatusBeanService.getBlockLocationAsStatusBean( location, location.getTime()); bean.setTripStatus(status); bean.setDebug(debug); beans.add(bean); } } private List<Record> getRecords(List<Record> records, long minT) { List<Record> pruned = new ArrayList<Record>(); for (Record record : records) { if (record.getTimestamp() < minT) continue; if (record.getAccuracy() > _maxAccuracy) continue; pruned.add(record); } Collections.sort(pruned); return pruned; } private Map<Date, Record> getRecordsByTimestamp(List<Record> records) { Map<Date, Record> recordsByTime = new HashMap<Date, Record>(); for (Record record : records) { Date timestamp = new Date(record.getTimestamp()); recordsByTime.put(timestamp, record); } return recordsByTime; } private Set<BlockSequenceIndex> getBlockSequenceIndicesForRecords( Map<Date, Record> recordsByTime) { Set<BlockSequenceIndex> allIndices = null; for (Record record : recordsByTime.values()) { CoordinateBounds bounds = SphericalGeometryLibrary.bounds( record.getLocation(), record.getAccuracy()); Set<BlockSequenceIndex> indices = _blockGeospatialService.getBlockSequenceIndexPassingThroughBounds(bounds); if (allIndices == null) allIndices = indices; else allIndices.retainAll(indices); } return allIndices; } private void computeCumulativeProbabilityForRealTimeBlockLocations( Map<Date, Record> recordsByTime, List<BlockLocation> locations, double minProbabilityForConsideration, List<CurrentVehicleEstimateBean> beans) { DoubleArrayList ps = new DoubleArrayList(); for (BlockLocation location : locations) { Date t = new Date(location.getTime()); Record record = recordsByTime.get(t); CoordinatePoint userLocation = record.getLocation(); CoordinatePoint vehicleLocation = location.getLocation(); double d = SphericalGeometryLibrary.distance(userLocation, vehicleLocation); double p = _realTimeLocationDeviationModel.probability(d); ps.add(p); } BlockLocation last = locations.get(locations.size() - 1); double mu = Descriptive.mean(ps); String debug = asString(ps); addResult(last, mu, debug, minProbabilityForConsideration, beans); } private void computeCumulativeProbabilityForScheduledBlockLocations( List<Record> records, BlockInstance blockInstance, double minProbabilityForConsideration, List<CurrentVehicleEstimateBean> beans) { DoubleArrayList ps = new DoubleArrayList(); List<ScheduledBlockLocation> blockLocations = new ArrayList<ScheduledBlockLocation>(); Record firstRecord = records.get(0); ScheduledBlockLocation firstLocation = _blockGeospatialService.getBestScheduledBlockLocationForLocation( blockInstance, firstRecord.getLocation(), firstRecord.getTimestamp(), 0, Double.POSITIVE_INFINITY); blockLocations.add(firstLocation); ps.add(updateScheduledBlockLocationProbability(blockInstance, firstRecord, firstLocation)); Record lastRecord = records.get(records.size() - 1); ScheduledBlockLocation lastLocation = _blockGeospatialService.getBestScheduledBlockLocationForLocation( blockInstance, lastRecord.getLocation(), lastRecord.getTimestamp(), 0, Double.POSITIVE_INFINITY); ps.add(updateScheduledBlockLocationProbability(blockInstance, lastRecord, lastLocation)); if (Descriptive.mean(ps) < minProbabilityForConsideration) return; /** * If the vehicle is traveling backwards in time, we kill the prediction */ int maxTravelBackwardsTime = computeMaxTravelBackwardsTime(lastRecord.getTimestamp() - firstRecord.getTimestamp()); if (lastLocation.getScheduledTime() < firstLocation.getScheduledTime() - maxTravelBackwardsTime) return; double minDistanceAlongBlock = Math.min( firstLocation.getDistanceAlongBlock(), lastLocation.getDistanceAlongBlock()) - 500; double maxDistanceAlongBlock = Math.max( firstLocation.getDistanceAlongBlock(), lastLocation.getDistanceAlongBlock()) + 500; for (int i = 1; i < records.size() - 1; i++) { Record record = records.get(i); ScheduledBlockLocation location = _blockGeospatialService.getBestScheduledBlockLocationForLocation( blockInstance, record.getLocation(), record.getTimestamp(), minDistanceAlongBlock, maxDistanceAlongBlock); blockLocations.add(location); ps.add(updateScheduledBlockLocationProbability(blockInstance, record, location)); if (Descriptive.mean(ps) < minProbabilityForConsideration) return; } blockLocations.add(lastLocation); updateProbabilitiesWithScheduleDeviations(records, blockLocations, ps); BlockLocation location = _blockLocationService.getLocationForBlockInstanceAndScheduledBlockLocation( blockInstance, lastLocation, lastRecord.getTimestamp()); double mu = Descriptive.mean(ps); String debug = asString(ps); addResult(location, mu, debug, minProbabilityForConsideration, beans); } private double updateScheduledBlockLocationProbability( BlockInstance blockInstance, Record record, ScheduledBlockLocation location) { double locationDelta = SphericalGeometryLibrary.distance( record.getLocation(), location.getLocation()); double locationP = _scheduleOnlyLocationDeviationModel.probability(locationDelta); long serviceDate = blockInstance.getServiceDate(); int timeDelta = (int) ((record.getTimestamp() - serviceDate) / 1000 - location.getScheduledTime()); double scheduleP = 0.0; if (timeDelta < 0) { scheduleP = _scheduleDeviationEarlyModel.probability(-timeDelta); } else { scheduleP = _scheduleDeviationLateModel.probability(timeDelta); } return locationP * scheduleP; } private void updateProbabilitiesWithScheduleDeviations(List<Record> records, List<ScheduledBlockLocation> blockLocations, DoubleArrayList ps) { if (records.size() != blockLocations.size()) throw new IllegalStateException(); if (records.size() != ps.size()) throw new IllegalStateException(); for (int i = 1; i < records.size(); i++) { Record prevRecord = records.get(i - 1); Record nextRecord = records.get(i); long recordDeltaT = (nextRecord.getTimestamp() - prevRecord.getTimestamp()); if (recordDeltaT <= 0) continue; int maxTravelBackwardsTime = computeMaxTravelBackwardsTime(recordDeltaT); ScheduledBlockLocation prevLocation = blockLocations.get(i - 1); ScheduledBlockLocation nextLocation = blockLocations.get(i); int locationDeltaT = nextLocation.getScheduledTime() - prevLocation.getScheduledTime(); if (locationDeltaT < 0 && Math.abs(locationDeltaT) > maxTravelBackwardsTime) ps.set(i, 0.0); } } private int computeMaxTravelBackwardsTime(long t) { return (int) Math.max(0, _maxTravelBackwardsTime - _maxTravelBackwardsDecayFactor * t / 1000); } private String asString(DoubleArrayList values) { StringBuilder b = new StringBuilder(); for (int i = 0; i < values.size(); i++) { if (i > 0) b.append(','); b.append(_format.format(values.get(i))); } return b.toString(); } }