/**
* 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.bundle.tasks.stop_transfers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.container.refresh.RefreshService;
import org.onebusaway.geospatial.model.CoordinateBounds;
import org.onebusaway.geospatial.services.SphericalGeometryLibrary;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.transit_data_federation.impl.RefreshableResources;
import org.onebusaway.transit_data_federation.impl.tripplanner.StopHopData;
import org.onebusaway.transit_data_federation.impl.tripplanner.StopTransferAndHopData;
import org.onebusaway.transit_data_federation.impl.tripplanner.StopTransferData;
import org.onebusaway.transit_data_federation.model.tripplanner.TripPlannerConstants;
import org.onebusaway.transit_data_federation.services.FederatedTransitDataBundle;
import org.onebusaway.transit_data_federation.services.blocks.BlockIndexService;
import org.onebusaway.transit_data_federation.services.blocks.BlockStopTimeIndex;
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.onebusaway.utility.ObjectSerializationLibrary;
import org.opentripplanner.routing.services.PathService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
public class StopTransfersTask implements Runnable {
public static final String PROPERTY_PRINT_TRANSFERS = StopTransfersTask.class.getName()
+ ".printTransfers";
private TripPlannerConstants _constants = new TripPlannerConstants();
private StopWalkPlanCache _cachedStopTransferWalkPlanner = new StopWalkPlanCache();
private Map<Pair<AgencyAndId>, Double> _cachedStopDistance = new HashMap<Pair<AgencyAndId>, Double>();
private FederatedTransitDataBundle _bundle;
private TransitGraphDao _transitGraphDao;
private BlockIndexService _blockIndexService;
private RefreshService _refreshService;
private PathService _pathService;
@Autowired
public void setBundle(FederatedTransitDataBundle bundle) {
_bundle = bundle;
}
@Autowired
public void setTransitGraphDao(TransitGraphDao transitGraphDao) {
_transitGraphDao = transitGraphDao;
}
@Autowired
public void setBlockS(BlockIndexService blockIndexService) {
_blockIndexService = blockIndexService;
}
@Autowired
public void setRefreshService(RefreshService refreshService) {
_refreshService = refreshService;
}
@Autowired
public void setPathService(PathService pathService) {
_pathService = pathService;
}
@Transactional
public void run() {
_cachedStopTransferWalkPlanner.setPathService(_pathService);
try {
Map<AgencyAndId, List<StopHopData>> stopHops = getStopHopData();
Map<AgencyAndId, List<StopTransferData>> stopTransfers = getStopTransferData();
StopTransferAndHopData allData = new StopTransferAndHopData();
allData.setTransferData(stopTransfers);
allData.setHopData(stopHops);
ObjectSerializationLibrary.writeObject(_bundle.getStopTransfersPath(),
allData);
_refreshService.refresh(RefreshableResources.STOP_TRANSFER_DATA);
} catch (Exception ex) {
throw new IllegalStateException("error in stop transfers task", ex);
}
}
/****
* Private Methods
****/
private Map<AgencyAndId, List<StopTransferData>> getStopTransferData() {
StopSequenceData data = new StopSequenceData();
for (TripEntry trip : _transitGraphDao.getAllTrips())
generateStatsForTrip(trip, data);
System.out.println("stopSequences=" + data.size());
Map<AgencyAndId, List<StopTransferData>> stopTransfers = processStopTransfers(data);
System.out.println("stop walk cache: "
+ _cachedStopTransferWalkPlanner.getCacheHits() + " / "
+ _cachedStopTransferWalkPlanner.getTotalHits());
if (System.getProperties().containsKey(PROPERTY_PRINT_TRANSFERS))
printTransfers(stopTransfers);
return stopTransfers;
}
private void printTransfers(
Map<AgencyAndId, List<StopTransferData>> stopTransfers) {
for (Map.Entry<AgencyAndId, List<StopTransferData>> entry : stopTransfers.entrySet()) {
AgencyAndId stopFromId = entry.getKey();
StopEntry stopFrom = _transitGraphDao.getStopEntryForId(stopFromId);
List<StopTransferData> datas = entry.getValue();
for (StopTransferData stopTransfer : datas) {
AgencyAndId toStopId = stopTransfer.getStopId();
StopEntry stopTo = _transitGraphDao.getStopEntryForId(toStopId);
System.out.println(stopFrom.getStopLat() + "," + stopFrom.getStopLon()
+ "," + stopTo.getStopLat() + "," + stopTo.getStopLon());
}
}
}
private void generateStatsForTrip(TripEntry trip, StopSequenceData data) {
List<StopEntry> stops = new ArrayList<StopEntry>();
List<StopTimeEntry> stopTimes = trip.getStopTimes();
for (StopTimeEntry stopTime : stopTimes)
stops.add(stopTime.getStop());
StopSequenceKey key = new StopSequenceKey(stops);
StopSequenceStats stats = data.getStatsForSequence(key);
key = stats.getKey();
stats.addTrip(trip);
data.putSequenceForTrip(trip, stats.getKey());
}
/**
* General idea:
*
* For a given block of trips, we examine each stop along the block. For each
* stop, we compute the sets of reachable stops based on the max transfer
* distance. For each potential transfer, we prune out unnecessary transfers,
* keeping only the best. See getBestTransfers for more info.
*
* @param tripsByBlockId
* @return
*/
private Map<AgencyAndId, List<StopTransferData>> processStopTransfers(
StopSequenceData data) {
Map<Pair<StopEntry>, Transfer> activeTransfers = new HashMap<Pair<StopEntry>, Transfer>();
int entryIndex = 0;
for (StopSequenceKey key : data.getSequences()) {
if (entryIndex % 20 == 0)
System.out.println("stopSequences=" + entryIndex + "/" + data.size());
entryIndex++;
// Compute the potential transfers to another stop, specif
Map<StopEntry, List<StopEntry>> potentialTransfers = computePotentialTransfers(key);
// Keep only the best transfers
Map<Pair<StopEntry>, Transfer> bestTransfers = getBestTransfers(
potentialTransfers, data, key);
activeTransfers.putAll(bestTransfers);
}
Map<AgencyAndId, List<StopTransferData>> stopTransfersBySourceStop = new FactoryMap<AgencyAndId, List<StopTransferData>>(
new ArrayList<StopTransferData>());
// Store all the transfers
for (Map.Entry<Pair<StopEntry>, Transfer> entry : activeTransfers.entrySet()) {
Pair<StopEntry> transfer = entry.getKey();
Transfer t = entry.getValue();
double distance = t.getWalkingDistance();
StopEntry fromStop = transfer.getFirst();
StopEntry toStop = transfer.getSecond();
StopTransferData stopTransfer = new StopTransferData(toStop.getId(), 0,
distance);
stopTransfersBySourceStop.get(fromStop.getId()).add(stopTransfer);
}
return stopTransfersBySourceStop;
}
/**
* Compute the set of potential transfers for a given trip block
*
* @param key
*
* @param key
* @param transfers
* @return
* @return the set of potential transfer end-points, along with the list of
* potential transfer start-points for each end-point as a list of
* StopTimes
*/
private Map<StopEntry, List<StopEntry>> computePotentialTransfers(
StopSequenceKey key) {
Map<StopEntry, List<StopEntry>> transfers = new FactoryMap<StopEntry, List<StopEntry>>(
new ArrayList<StopEntry>());
for (StopEntry stop : key.getStops()) {
CoordinateBounds bounds = SphericalGeometryLibrary.bounds(
stop.getStopLocation(), _constants.getMaxTransferDistance());
List<StopEntry> nearbyStops = _transitGraphDao.getStopsByLocation(bounds);
for (StopEntry nearbyStop : nearbyStops) {
transfers.get(nearbyStop).add(stop);
}
}
return transfers;
}
private Map<Pair<StopEntry>, Transfer> getBestTransfers(
Map<StopEntry, List<StopEntry>> potentialTransferStartPointsByEndPoint,
StopSequenceData data, StopSequenceKey key) {
/**
* Finds a set of new stops that aren't reachable in our current block, but
* ARE reachable with a transfer to another route. Compute the set of new
* stops, along with the transfer end-points from our current block along
* with the travel time from the transfer end-point to the new stop
*/
Map<StopEntry, Map<StopEntry, Integer>> newStopsAndPotentialTransferEndPoints = getPotentialTransfersToNewRoutes(
potentialTransferStartPointsByEndPoint, data, key);
Map<Pair<StopEntry>, Transfer> bestTransfers = new HashMap<Pair<StopEntry>, Transfer>();
StopSequenceStats stats = data.getStatsForSequence(key);
/**
* Note that we don't really care about the new stop that is the eventual
* destination for out transfer. We just care that it exists and we know the
* travel times to it
*/
for (Map<StopEntry, Integer> potentialTransferEndPointsWithTravelTimes : newStopsAndPotentialTransferEndPoints.values()) {
List<Transfer> transfers = computeFastestTransferToNewStops(
potentialTransferStartPointsByEndPoint,
potentialTransferEndPointsWithTravelTimes, key, stats);
optimisticallyUpdateTransferWalkingDistanceAndKeepBest(transfers,
bestTransfers);
}
return bestTransfers;
}
/**
* Given a set of potential transfers to nearby stops and the list of
* StopTimes in the current block that can transfer to that nearby stop,
* return the set of transfers that actually reach a new stop not already
* reachable by the current block trip
*
* @param graph
* @param transferStartPointsByEndPoint
*
* @return a map from stops not currently reachable in the current block trip
* along with the set of stops that are transfer points to that stop
* and the time in seconds for travel to the new stop
*/
private Map<StopEntry, Map<StopEntry, Integer>> getPotentialTransfersToNewRoutes(
Map<StopEntry, List<StopEntry>> potentialTransferStartPointsByEndPoint,
StopSequenceData data, StopSequenceKey key) {
// The set of already seen stops is the set of stops that can be transferred
// to directly by walking
Set<StopEntry> alreadySeenStops = Collections.unmodifiableSet(potentialTransferStartPointsByEndPoint.keySet());
// Map from stops not in the already-seen set to stops in the already-seen
// set that can reach the new stop, along with travel times in seconds
Map<StopEntry, Map<StopEntry, Integer>> potentialTransfersWithTravelTimes = new HashMap<StopEntry, Map<StopEntry, Integer>>();
// For a potential transfer destination
for (StopEntry stopEntry : potentialTransferStartPointsByEndPoint.keySet()) {
// Consider all the stop times at that stop
Set<StopSequenceKey> outboundSequences = new HashSet<StopSequenceKey>();
List<BlockStopTimeIndex> blockStopTimeIndices = _blockIndexService.getStopTimeIndicesForStop(stopEntry);
for (BlockStopTimeIndex index : blockStopTimeIndices) {
for (BlockStopTimeEntry blockStopTime : index.getStopTimes()) {
BlockTripEntry blockTrip = blockStopTime.getTrip();
StopSequenceKey outboundSequence = data.getSequenceForTrip(blockTrip.getTrip());
outboundSequences.add(outboundSequence);
}
}
for (StopSequenceKey outboundSequence : outboundSequences) {
int stopIndex = outboundSequence.getIndexOfStop(stopEntry);
StopSequenceStats outboundStats = data.getStatsForSequence(outboundSequence);
findNewStopAlongTrip(outboundSequence, outboundStats, stopIndex,
alreadySeenStops, potentialTransfersWithTravelTimes);
}
}
return potentialTransfersWithTravelTimes;
}
/**
* Given a position along a Trip, follow along the Trip and any subsequent
* trips joined by a trip-block to find a new stop not in the already-seen
* set.
*
* The TripEntry + index specifies a position along a trip, where index
* specifies an index into the List of StopTimes for the specified TripEntry.
*
* During the search for a new stop, if we encounter stops that are in the
* already-seen set, we add them to the output map with the already-seen stop
* as key and the travel time in seconds from the already-seen stop to the new
* stop as the value.
*
* @param outboundSequence - the trip entry of the current trip
* @param fromIndex - index into the list of StopTimes for the current trip
* @param alreadySeen - the set of already-seen stops
* @param potentialTransfersWithTravelTimes - map from already-seen stops
* along the specified trip along with their travel times in seconds
* to the new stop
*
* @return the new stop not in the already-seen set
*/
private void findNewStopAlongTrip(StopSequenceKey outboundSequence,
StopSequenceStats stats, int fromIndex, Set<StopEntry> alreadySeen,
Map<StopEntry, Map<StopEntry, Integer>> potentialTransfersWithTravelTimes) {
List<StopEntry> outboundStops = outboundSequence.getStops();
StopEntry fromStop = outboundStops.get(fromIndex);
int toIndex = fromIndex;
while (toIndex < outboundStops.size()) {
StopEntry toNewStopEntry = outboundStops.get(toIndex);
if (!alreadySeen.contains(toNewStopEntry)) {
int time = stats.getTimeFromStart(toIndex)
- stats.getTimeFromStart(fromIndex);
Map<StopEntry, Integer> a = potentialTransfersWithTravelTimes.get(toNewStopEntry);
if (a == null) {
a = new HashMap<StopEntry, Integer>();
potentialTransfersWithTravelTimes.put(toNewStopEntry, a);
}
Integer t = a.get(fromStop);
if (t == null || time < t)
a.put(fromStop, time);
return;
}
toIndex++;
}
}
private List<Transfer> computeFastestTransferToNewStops(
Map<StopEntry, List<StopEntry>> potentialTransferStartPointsByEndPoint,
Map<StopEntry, Integer> potentialTransferEndPointsWithTravelTimes,
StopSequenceKey key, StopSequenceStats stats) {
Map<Pair<Stop>, Min<Transfer>> minsByTransferPoint = new FactoryMap<Pair<Stop>, Min<Transfer>>(
new Min<Transfer>());
for (Map.Entry<StopEntry, Integer> entry : potentialTransferEndPointsWithTravelTimes.entrySet()) {
StopEntry transferToEntry = entry.getKey();
double busTimeSecondTrip = entry.getValue() * 1000;
for (StopEntry transferFromEntry : potentialTransferStartPointsByEndPoint.get(transferToEntry)) {
Pair<StopEntry> pair = Tuples.pair(transferFromEntry, transferToEntry);
int index = key.getIndexOfStop(transferFromEntry);
int busTimeFirstTrip = stats.getTimeFromStart(index) * 1000;
double walkingDistance = getMinimumWalkDistanceBetweenStops(
transferFromEntry, transferToEntry);
Transfer transfer = new Transfer(pair, busTimeFirstTrip
+ busTimeSecondTrip, walkingDistance);
minsByTransferPoint.get(pair).add(transfer.getTime(), transfer);
}
}
List<Transfer> transfers = new ArrayList<Transfer>();
for (Map.Entry<Pair<Stop>, Min<Transfer>> entry : minsByTransferPoint.entrySet()) {
Min<Transfer> min = entry.getValue();
transfers.addAll(min.getMinElements());
}
Collections.sort(transfers);
return transfers;
}
private double getMinimumWalkDistanceBetweenStops(StopEntry stopA,
StopEntry stopB) {
AgencyAndId stopIdA = stopA.getId();
AgencyAndId stopIdB = stopB.getId();
Pair<AgencyAndId> pair = Tuples.pair(stopIdA, stopIdB);
if (stopIdA.compareTo(stopIdB) > 0)
pair = pair.swap();
Double distance = _cachedStopDistance.get(pair);
if (distance == null) {
distance = SphericalGeometryLibrary.distance(stopA.getStopLocation(),
stopB.getStopLocation());
_cachedStopDistance.put(pair, distance);
}
return distance;
}
private void optimisticallyUpdateTransferWalkingDistanceAndKeepBest(
List<Transfer> transfers,
Map<Pair<StopEntry>, Transfer> resultingBestTransfers) {
double minT = Double.POSITIVE_INFINITY;
Transfer minTransfer = null;
for (Transfer transfer : transfers) {
if (transfer.getTime() > minT)
break;
double distance = _cachedStopTransferWalkPlanner.getWalkPlanDistanceForStopToStop(transfer.getStops());
if (distance < 0 || distance > _constants.getMaxTransferDistance())
continue;
transfer.setWalkingDistance(distance);
if (transfer.getTime() < minT) {
minTransfer = transfer;
minT = transfer.getTime();
}
}
if (minTransfer != null) {
Pair<StopEntry> stops = minTransfer.getStops();
resultingBestTransfers.put(stops, minTransfer);
}
}
private Map<AgencyAndId, List<StopHopData>> getStopHopData() {
Map<Pair<StopEntry>, Integer> minTravelTimes = new HashMap<Pair<StopEntry>, Integer>();
for (BlockEntry block : _transitGraphDao.getAllBlocks()) {
for (BlockConfigurationEntry blockConfig : block.getConfigurations()) {
List<BlockStopTimeEntry> stopTimes = blockConfig.getStopTimes();
BlockStopTimeEntry prevBlockStopTime = null;
for (BlockStopTimeEntry blockStopTime : stopTimes) {
if (prevBlockStopTime != null) {
StopTimeEntry from = prevBlockStopTime.getStopTime();
StopTimeEntry to = blockStopTime.getStopTime();
int time = to.getArrivalTime() - from.getDepartureTime();
StopEntry stopFrom = from.getStop();
StopEntry stopTo = to.getStop();
Pair<StopEntry> stopPair = Tuples.pair(stopFrom, stopTo);
Integer prevTime = minTravelTimes.get(stopPair);
if (prevTime == null || time < prevTime)
minTravelTimes.put(stopPair, time);
}
prevBlockStopTime = blockStopTime;
}
}
}
Map<AgencyAndId, List<StopHopData>> hopsByStop = new HashMap<AgencyAndId, List<StopHopData>>();
for (Map.Entry<Pair<StopEntry>, Integer> entry : minTravelTimes.entrySet()) {
Pair<StopEntry> pair = entry.getKey();
AgencyAndId fromStopId = pair.getFirst().getId();
AgencyAndId toStopId = pair.getSecond().getId();
int minTravelTime = entry.getValue();
List<StopHopData> hops = hopsByStop.get(fromStopId);
if (hops == null) {
hops = new ArrayList<StopHopData>();
hopsByStop.put(fromStopId, hops);
}
hops.add(new StopHopData(toStopId, minTravelTime));
if( fromStopId.toString().equals("1_75403") || fromStopId.toString().equals("1_75414") || fromStopId.toString().equals("1_18120") )
System.out.println(fromStopId + " " + toStopId + " " + minTravelTime );
}
return hopsByStop;
}
/****
* Private Classes
****/
private class Transfer implements Comparable<Transfer> {
private Pair<StopEntry> _stops;
private double _busTime;
private double _walkingDistance;
public Transfer(Pair<StopEntry> pair, double busTime, double walkingDistance) {
_stops = pair;
_busTime = busTime;
_walkingDistance = walkingDistance;
}
public void setWalkingDistance(double walkingDistance) {
_walkingDistance = walkingDistance;
}
public double getWalkingDistance() {
return _walkingDistance;
}
public Pair<StopEntry> getStops() {
return _stops;
}
public double getTime() {
return _busTime
+ (_walkingDistance == 0.0 ? 0 : _walkingDistance
/ _constants.getWalkingVelocity());
}
public int compareTo(Transfer o) {
double t1 = getTime();
double t2 = o.getTime();
return t1 == t2 ? 0 : (t1 < t2 ? -1 : 1);
}
}
private static class StopSequenceData {
private Map<StopSequenceKey, StopSequenceStats> _statsBySequence = new HashMap<StopSequenceKey, StopSequenceStats>();
private Map<TripEntry, StopSequenceKey> _stopSequencesByTrip = new HashMap<TripEntry, StopSequenceKey>();
public Set<StopSequenceKey> getSequences() {
return _statsBySequence.keySet();
}
public StopSequenceKey getSequenceForTrip(TripEntry trip) {
return _stopSequencesByTrip.get(trip);
}
public StopSequenceStats getStatsForSequence(StopSequenceKey key) {
StopSequenceStats stats = _statsBySequence.get(key);
if (stats == null) {
stats = new StopSequenceStats(key);
_statsBySequence.put(key, stats);
}
return stats;
}
public int size() {
return _statsBySequence.size();
}
public void putSequenceForTrip(TripEntry trip, StopSequenceKey key) {
_stopSequencesByTrip.put(trip, key);
}
}
private static class StopSequenceKey {
private List<StopEntry> _stops;
public StopSequenceKey(List<StopEntry> stops) {
_stops = stops;
}
public int getIndexOfStop(StopEntry stop) {
return _stops.indexOf(stop);
}
public List<StopEntry> getStops() {
return _stops;
}
@Override
public int hashCode() {
return _stops.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
StopSequenceKey other = (StopSequenceKey) obj;
return _stops.equals(other._stops);
}
}
private static class StopSequenceStats {
private int[] _minTravelTime = null;
private int[] _cumulativeTravelTime = null;
private List<TripEntry> _trips = new ArrayList<TripEntry>();
private StopSequenceKey _key;
public StopSequenceStats(StopSequenceKey key) {
_key = key;
}
public StopSequenceKey getKey() {
return _key;
}
public void addTrip(TripEntry trip) {
List<StopTimeEntry> stopTimes = trip.getStopTimes();
ensureData(stopTimes);
for (int i = 0; i < stopTimes.size() - 1; i++) {
StopTimeEntry from = stopTimes.get(i);
StopTimeEntry to = stopTimes.get(i);
int time = to.getArrivalTime() - from.getDepartureTime();
_minTravelTime[i] = Math.min(_minTravelTime[i], time);
}
_trips.add(trip);
}
public void pack() {
_cumulativeTravelTime = new int[_minTravelTime.length + 1];
_cumulativeTravelTime[0] = 0;
for (int i = 0; i < _minTravelTime.length; i++)
_cumulativeTravelTime[i + 1] = _cumulativeTravelTime[i]
+ _minTravelTime[i];
}
public int getTimeFromStart(int stopIndex) {
if (_cumulativeTravelTime == null)
pack();
return _cumulativeTravelTime[stopIndex];
}
private void ensureData(List<StopTimeEntry> stopTimes) {
if (stopTimes.isEmpty())
return;
int n = stopTimes.size() - 1;
if (_minTravelTime == null) {
_minTravelTime = new int[n];
for (int i = 0; i < n; i++)
_minTravelTime[i] = Integer.MAX_VALUE;
}
}
}
}