diff --git a/src/main/java/com/conveyal/r5/common/Util.java b/src/main/java/com/conveyal/r5/common/Util.java index 25f55b085..8028adac1 100644 --- a/src/main/java/com/conveyal/r5/common/Util.java +++ b/src/main/java/com/conveyal/r5/common/Util.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.Arrays; +import java.util.Map; public abstract class Util { @@ -34,6 +35,14 @@ public static boolean notNullOrEmpty (Collection collection) { return !isNullOrEmpty(collection); } + public static boolean isNullOrEmpty (Map map) { + return map == null || map.isEmpty(); + } + + public static boolean notNullOrEmpty (Map map) { + return !isNullOrEmpty(map); + } + public static boolean isNullOrEmpty (T[] array) { return array == null || array.length == 0; } diff --git a/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java b/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java index c66f5cc34..90105c5c2 100644 --- a/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java +++ b/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java @@ -61,7 +61,8 @@ public class FastRaptorWorker { /** * Minimum time between alighting from one vehicle and boarding another, in seconds. - * TODO make this configurable, and use loop-transfers from transfers.txt. + * FIXME strangely this appears to only be used in classes for displaying paths, not for routing. + * Apply when finding soonest viable departure. */ public static final int BOARD_SLACK_SECONDS = 60; @@ -369,7 +370,7 @@ private int[][] runRaptorForDepartureMinute (int departureTime) { // The transfer step can be skipped in the last round. if (round < request.maxRides) { raptorTimer.scheduledSearchTransfers.start(); - doTransfers(scheduleState[round]); + doTransfersRefactored(scheduleState[round]); raptorTimer.scheduledSearchTransfers.stop(); } } @@ -411,7 +412,7 @@ private int[][] runRaptorForDepartureMinute (int departureTime) { if (round < request.maxRides) { // Transfers not needed after last round raptorTimer.frequencySearchTransfers.start(); - doTransfers(frequencyState[round]); + doTransfersRefactored(frequencyState[round]); raptorTimer.frequencySearchTransfers.stop(); } @@ -877,6 +878,38 @@ private void doTransfers (RaptorState state) { } } + /** + * This has a few differences: staying at the stop is treated as a "loop transfer", which can simplify the algorithm + * and allows imposing minimum transfer times when waiting for another vehicle at the same stop. + * For now we're only applying minimum transfer times as an exhaustive list of available transfers, but eventually + * we'll want to merge them with transfers found through the OSM network. + * THIS DOES NOT WORK WITH SCENARIOS THAT ADD STOPS. + */ + private void doTransfersRefactored (RaptorState state) { + // Compute transfers only from stops updated pre-transfer within this departure minute / randomized schedule. + // These transfers then update the post-transfers bitset (stopsUpdated) to avoid concurrent modification while + // iterating. + final int maxWalkTimeSeconds = request.maxWalkTime * SECONDS_PER_MINUTE; + for (int stop = state.nonTransferStopsUpdated.nextSetBit(0); + stop >= 0; + stop = state.nonTransferStopsUpdated.nextSetBit(stop + 1) + ) { + TIntList minTimesFromStop = transit.minTransferTimesFromStop[stop]; + if (minTimesFromStop != null) { + for (int i = 0; i < minTimesFromStop.size(); i += 2) { + int targetStop = minTimesFromStop.get(i); + int minTimeSeconds = minTimesFromStop.get(i + 1); + if (minTimeSeconds < BOARD_SLACK_SECONDS) { + minTimeSeconds = BOARD_SLACK_SECONDS; + } + // if (minTimeSeconds < maxWalkTimeSeconds) { + int timeAtTargetStop = state.bestNonTransferTimes[stop] + minTimeSeconds; + state.setTimeAtStop(targetStop, timeAtTargetStop, -1, stop, 0, 0, true); + } + } + } + } + /** * Find all patterns that could lead to improvements in the next raptor round after the given state's round. * Specifically, these are the patterns passing through all stops that were updated in the given state's round. diff --git a/src/main/java/com/conveyal/r5/profile/RaptorState.java b/src/main/java/com/conveyal/r5/profile/RaptorState.java index 1adfc1f8b..56e8aa1d3 100644 --- a/src/main/java/com/conveyal/r5/profile/RaptorState.java +++ b/src/main/java/com/conveyal/r5/profile/RaptorState.java @@ -214,75 +214,66 @@ public void minMergePrevious () { /** * Check a time against the best known times at a transit stop, and record the new time if it is optimal. * This same method is used to handle both transit arrivals and transfers, according to the transfer parameter. - * When transfer is false, times can update both the bestNonTransferTime and the bestTime; when transfer is true, - * only bestTimes can be updated. - * + * When transfer is false, times can only update bestNonTransferTime. + * When transfer is true, times can only update bestTimes. + * Note that staying at the same stop to board in the next round is now treated as a trivial "transfer". * @param transfer if true, we are recording a time obtained via a transfer or the initial access leg in round 0 * @return true if the new time was optimal and the state was updated, false if the existing values were better */ public boolean setTimeAtStop(int stop, int time, int fromPattern, int fromStop, int waitTime, int inVehicleTime, boolean transfer) { + // First check whether the supplied travel time exceeds the specified maximum for this search. if (time >= departureTime + maxDurationSeconds) { return false; } - // Method return value: was the new time optimal, leading to a state update? - boolean optimal = false; - - // If this is "not a transfer" it is a transit arrival. If it is better than any known transit arrival, - // update the non-transfer time and path information, then consider updating the bestTimes. - // We may want to consider splitting the post-transfer updating out into its own method to make this clearer. - if (!transfer && time < bestNonTransferTimes[stop]) { - bestNonTransferTimes[stop] = time; - previousPatterns[stop] = fromPattern; - previousStop[stop] = fromStop; - - // Carry the travel time components (wait and in-vehicle time) from the previous leg and increment them. - int totalWaitTime, totalInVehicleTime; - if (previous == null) { - // first round, there is no previous wait time or in vehicle time - // TODO how and when can this happen? Round zero contains only the access leg and has no transit. - totalWaitTime = waitTime; - totalInVehicleTime = inVehicleTime; - } else { - // TODO it seems like this whole block and the assignment below can be condensed significantly. - if (previous.transferStop[fromStop] != -1) { - // The fromSop was optimally reached via a transfer at the end of the previous round. - // Get the wait and in-vehicle time from the source stop of that transfer. - int preTransferStop = previous.transferStop[fromStop]; - totalWaitTime = previous.nonTransferWaitTime[preTransferStop] + waitTime; - totalInVehicleTime = previous.nonTransferInVehicleTravelTime[preTransferStop] + inVehicleTime; + + if (transfer) { + if (time < bestTimes[stop]) { + bestTimes[stop] = time; + transferStop[stop] = fromStop; + stopsUpdated.set(stop); + return true; + } + } else { + // If this is "not a transfer" it is a transit arrival. If it is better than any known transit arrival, + // update the non-transfer time and path information. + if (time < bestNonTransferTimes[stop]) { + bestNonTransferTimes[stop] = time; + previousPatterns[stop] = fromPattern; + previousStop[stop] = fromStop; + + // Carry the travel time components (wait and in-vehicle time) from the previous leg and increment them. + int totalWaitTime, totalInVehicleTime; + if (previous == null) { + // first round, there is no previous wait time or in vehicle time + // TODO how and when can this happen? Round zero contains only the access leg and has no transit. + totalWaitTime = waitTime; + totalInVehicleTime = inVehicleTime; } else { - // The stop we boarded at was reached directly by transit in the previous round. - totalWaitTime = previous.nonTransferWaitTime[fromStop] + waitTime; - totalInVehicleTime = previous.nonTransferInVehicleTravelTime[fromStop] + inVehicleTime; + // TODO it seems like this whole block and the assignment below can be condensed significantly. + if (previous.transferStop[fromStop] != -1) { + // The fromSop was optimally reached via a transfer at the end of the previous round. + // Get the wait and in-vehicle time from the source stop of that transfer. + int preTransferStop = previous.transferStop[fromStop]; + totalWaitTime = previous.nonTransferWaitTime[preTransferStop] + waitTime; + totalInVehicleTime = previous.nonTransferInVehicleTravelTime[preTransferStop] + inVehicleTime; + } else { + // The stop we boarded at was reached directly by transit in the previous round. + totalWaitTime = previous.nonTransferWaitTime[fromStop] + waitTime; + totalInVehicleTime = previous.nonTransferInVehicleTravelTime[fromStop] + inVehicleTime; + } } + nonTransferWaitTime[stop] = totalWaitTime; + nonTransferInVehicleTravelTime[stop] = totalInVehicleTime; + checkState(totalInVehicleTime + totalWaitTime <= (time - departureTime), "Components of travel time are greater than total travel time."); + transferStop[stop] = -1; // reached by transit, not transfer + nonTransferStopsUpdated.set(stop); + return true; } - nonTransferWaitTime[stop] = totalWaitTime; - nonTransferInVehicleTravelTime[stop] = totalInVehicleTime; - - checkState(totalInVehicleTime + totalWaitTime <= (time - departureTime), - "Components of travel time are greater than total travel time."); - - optimal = true; - nonTransferStopsUpdated.set(stop); } - // At a given stop, bestTimes is always less than or equal to bestNonTransferTimes. It will always be equal to - // the bestNonTransferTimes unless a transfer from some other stop yields an earlier time. - // If bestTimes is updated due to a transit arrival, the travel time components are already updated by the - // transit-handling block above. If it's due to a transfer, the travel time components were already recorded - // by an optimal arrival at the source station of the transfer. - if (time < bestTimes[stop]) { - bestTimes[stop] = time; - if (transfer) { - transferStop[stop] = fromStop; - } else { - transferStop[stop] = -1; - } - optimal = true; - stopsUpdated.set(stop); - } - return optimal; + // If we reach here, no new optimum was found. + return false; } /** Debug function: dump the path to a particular stop as a String. */ diff --git a/src/main/java/com/conveyal/r5/transit/TransitLayer.java b/src/main/java/com/conveyal/r5/transit/TransitLayer.java index 3195e7f46..0d762d0f9 100644 --- a/src/main/java/com/conveyal/r5/transit/TransitLayer.java +++ b/src/main/java/com/conveyal/r5/transit/TransitLayer.java @@ -9,6 +9,7 @@ import com.conveyal.gtfs.model.Shape; import com.conveyal.gtfs.model.Stop; import com.conveyal.gtfs.model.StopTime; +import com.conveyal.gtfs.model.Transfer; import com.conveyal.gtfs.model.Trip; import com.conveyal.r5.api.util.TransitModes; import com.conveyal.r5.common.GeometryUtils; @@ -54,6 +55,11 @@ import java.util.stream.IntStream; import java.util.stream.StreamSupport; +import static com.conveyal.r5.common.Util.isNullOrEmpty; +import static com.conveyal.r5.common.Util.notNullOrEmpty; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + /** * A key simplifying factor is that we don't handle overnight trips. This is fine for analysis at usual times of day. @@ -110,10 +116,14 @@ public class TransitLayer implements Serializable, Cloneable { // Inverse map of streetVertexForStop, and reconstructed from that list. public transient TIntIntMap stopForStreetVertex; - // For each stop, a packed list of transfers to other stops - // FIXME we may currently be storing weight or time to reach other stop, which we did to avoid floating point division. Instead, store distances in millimeters, and divide by speed in mm/sec. + // For each stop, a packed list of transfers to other stops. + // The list for each origin stop is a series of pairs of (destination stop index, distance in millimeters). public List transfersForStop = new ArrayList<>(); + // For each stop, a packed list of minimum times required to reach other stops. This reflects GTFS transfer type 2. + // The list for each origin stop is a series of pairs of (destination stop index, minimum time in seconds). + public TIntList[] minTransferTimesFromStop; + /** Information about a route */ public List routes = new ArrayList<>(); @@ -458,14 +468,38 @@ public void loadFromGtfs (GTFSFeed gtfs, LoadLevel level) throws DuplicateFeedEx this.fares = new HashMap<>(gtfs.fares); } - // Will be useful in naming patterns. -// LOG.info("Finding topology of each route/direction..."); -// Multimap, TripPattern> patternsForRouteDirection = HashMultimap.create(); -// tripPatterns.forEach(tp -> patternsForRouteDirection.put(new T2(tp.routeId, tp.directionId), tp)); -// for (T2 routeAndDirection : patternsForRouteDirection.keySet()) { -// RouteTopology topology = new RouteTopology(routeAndDirection.first, routeAndDirection.second, patternsForRouteDirection.get(routeAndDirection)); -// } - + // TODO check how min transfer slack is applied on top of transfer times + // TODO try storing these minimum times separately so they're not affected by walk speed + if (notNullOrEmpty(gtfs.transfers)) { + LOG.info("Loading {} transfers from transfers.txt...", gtfs.transfers.size()); + // Currently this only handles type 2 "minimum time to make a transfer" + minTransferTimesFromStop = new TIntList[stopForIndex.size()]; + for (Transfer transfer : gtfs.transfers.values()) { + checkState(transfer.transfer_type == 2); + checkState(transfer.min_transfer_time > 0); + checkState(transfer.from_route_id == null); + checkState(transfer.from_trip_id == null); + checkState(transfer.to_route_id == null); + checkState(transfer.to_trip_id == null); + checkNotNull(transfer.from_stop_id); + checkNotNull(transfer.to_stop_id); + int fromStopIndex = indexForUnscopedStopId.get(transfer.from_stop_id); + int toStopIndex = indexForUnscopedStopId.get(transfer.to_stop_id); + // Do not (yet?) support minimum times between stations (rather than stops). + checkState(stopForIndex.get(fromStopIndex).location_type == 0); + checkState(stopForIndex.get(toStopIndex).location_type == 0); + if (minTransferTimesFromStop[fromStopIndex] == null) { + minTransferTimesFromStop[fromStopIndex] = new TIntArrayList(); + } + // final double defaultWalkSpeedMetersPerSecond = 1.3888888888888888; + // int millimeters = (int) (transfer.min_transfer_time * defaultWalkSpeedMetersPerSecond * 1000); + // minTransferTimesFromStop[fromStopIndex].add(millimeters); + // LOG.info("Normalized distance from stop {} to stop {} is {} meters.", + // fromStopIndex, toStopIndex, millimeters / 1000d); + minTransferTimesFromStop[fromStopIndex].add(toStopIndex); + minTransferTimesFromStop[fromStopIndex].add(transfer.min_transfer_time); + } + } } // The median of all stopTimes would be best but that involves sorting a huge list of numbers. diff --git a/src/main/java/com/conveyal/r5/transit/TransportNetwork.java b/src/main/java/com/conveyal/r5/transit/TransportNetwork.java index 2aa95850a..7171d07ca 100644 --- a/src/main/java/com/conveyal/r5/transit/TransportNetwork.java +++ b/src/main/java/com/conveyal/r5/transit/TransportNetwork.java @@ -33,6 +33,8 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static com.conveyal.r5.common.Util.isNullOrEmpty; + /** * This is a completely new replacement for Graph, Router etc. * It uses a lot less object pointers and can be built, read, and written orders of magnitude faster. @@ -178,9 +180,12 @@ private static TransportNetwork fromFiles (String osmSourceFile, List gt streetLayer.buildEdgeLists(); transitLayer.rebuildTransientIndexes(); - // Create transfers - new TransferFinder(transportNetwork).findTransfers(); - new TransferFinder(transportNetwork).findParkRideTransfer(); + // Create transfers vis OSM network if none were imported from transfers.txt. Eventually the two should be + // combined, but for now we're doing analyses with an exhaustive list in transfers.txt. + if (isNullOrEmpty(transitLayer.minTransferTimesFromStop)) { + new TransferFinder(transportNetwork).findTransfers(); + new TransferFinder(transportNetwork).findParkRideTransfer(); + } transportNetwork.fareCalculator = tnBuilderConfig.analysisFareCalculator;