/* This program is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.routing.edgetype;
import java.util.BitSet;
import java.util.Locale;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.opentripplanner.routing.core.RoutingContext;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.ServiceDay;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.StateEditor;
import org.opentripplanner.routing.core.TransferTable;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.routing.vertextype.PatternStopVertex;
import org.opentripplanner.routing.vertextype.TransitStopArrive;
import org.opentripplanner.routing.vertextype.TransitStopDepart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.LineString;
/**
* Models boarding or alighting a vehicle - that is to say, traveling from a state off
* vehicle to a state on vehicle. When traversed forward on a boarding or backwards on an
* alighting, the the resultant state has the time of the next departure, in addition the pattern
* that was boarded. When traversed backward on a boarding or forward on an alighting, the result
* state is unchanged. A boarding penalty can also be applied to discourage transfers. In an on
* the fly reverse-optimization search, the overloaded traverse method can be used to give an
* initial wait time. Also, in reverse-opimization, board costs are correctly applied.
*
* This is the result of combining the classes formerly known as PatternBoard and PatternAlight.
*
* @author mattwigway
*/
public class TransitBoardAlight extends TablePatternEdge implements OnboardEdge {
private static final long serialVersionUID = 1042740795612978747L;
private static final Logger LOG = LoggerFactory.getLogger(TransitBoardAlight.class);
private final int stopIndex;
private int modeMask; // TODO: via TablePatternEdge it should be possible to grab this from the pattern
/** True if this edge represents boarding a vehicle, false if it represents alighting. */
public boolean boarding;
/** Boarding constructor (TransitStopDepart --> PatternStopVertex) */
public TransitBoardAlight (TransitStopDepart fromStopVertex, PatternStopVertex toPatternVertex,
int stopIndex, TraverseMode mode) {
super(fromStopVertex, toPatternVertex);
this.stopIndex = stopIndex;
this.modeMask = new TraverseModeSet(mode).getMask();
this.boarding = true;
}
/** Alighting constructor (PatternStopVertex --> TransitStopArrive) */
public TransitBoardAlight (PatternStopVertex fromPatternStop, TransitStopArrive toStationVertex,
int stopIndex, TraverseMode mode) {
super(fromPatternStop, toStationVertex);
this.stopIndex = stopIndex;
this.modeMask = new TraverseModeSet(mode).getMask();
this.boarding = false;
}
/**
* Find the TripPattern this edge is boarding or alighting from. Overrides the general
* method which always looks at the from-vertex.
* @return the pattern of the to-vertex when boarding, and that of the from-vertex
* when alighting.
*/
@Override
public TripPattern getPattern() {
if (boarding)
return ((PatternStopVertex) tov).getTripPattern();
else
return ((PatternStopVertex) fromv).getTripPattern();
}
public String getDirection() {
return null;
}
public double getDistance() {
return 0;
}
public LineString getGeometry() {
return null;
}
public TraverseMode getMode() {
return TraverseMode.LEG_SWITCH;
}
public String getName() {
return boarding ? "leave street network for transit network" :
"leave transit network for street network";
}
@Override
public String getName(Locale locale) {
//TODO: localize
return this.getName();
}
@Override
public State traverse(State state0) {
return traverse(state0, 0);
}
/**
* NOTE: We do not need to check the pickup/drop off type. TransitBoardAlight edges are simply
* not created for pick/drop type 1 (no pick/drop).
*
* @param arrivalTimeAtStop TODO: clarify what this is.
*/
public State traverse(State s0, long arrivalTimeAtStop) {
RoutingContext rctx = s0.getContext();
RoutingRequest options = s0.getOptions();
// Forbid taking shortcuts composed of two board-alight edges in a row. Also avoids spurious leg transitions.
if (s0.backEdge instanceof TransitBoardAlight) {
return null;
}
/* If the user requested a wheelchair accessible trip, check whether and this stop is not accessible. */
if (options.wheelchairAccessible && ! getPattern().wheelchairAccessible(stopIndex)) {
return null;
};
/*
* Determine whether we are going onto or off of transit. Entering and leaving transit is
* not the same thing as boarding and alighting. When arriveBy == true, we are entering
* transit when traversing an alight edge backward.
*/
boolean leavingTransit =
( boarding && options.arriveBy) ||
(!boarding && !options.arriveBy);
/* TODO pull on/off transit out into two functions. */
if (leavingTransit) {
/* We are leaving transit, not as much to do. */
// When a dwell edge has been eliminated, do not alight immediately after boarding.
// Perhaps this should be handled by PathParser.
if (s0.getBackEdge() instanceof TransitBoardAlight) {
return null;
}
StateEditor s1 = s0.edit(this);
s1.setTripId(null);
s1.setLastAlightedTimeSeconds(s0.getTimeSeconds());
// Store the stop we are alighting at, for computing stop-to-stop transfer times,
// preferences, and permissions.
// The vertices in the transfer table are stop arrives/departs, not pattern
// arrives/departs, so previousStop is direction-dependent.
s1.setPreviousStop(getStop());
s1.setLastPattern(this.getPattern());
if (boarding) {
int boardingTime = options.getBoardTime(this.getPattern().mode);
if (boardingTime != 0) {
// When traveling backwards the time travels also backwards
s1.incrementTimeInSeconds(boardingTime);
s1.incrementWeight(boardingTime * options.waitReluctance);
}
} else {
int alightTime = options.getAlightTime(this.getPattern().mode);
if (alightTime != 0) {
s1.incrementTimeInSeconds(alightTime);
s1.incrementWeight(alightTime * options.waitReluctance);
// TODO: should we have different cost for alighting and boarding compared to regular waiting?
}
}
/* Determine the wait. */
if (arrivalTimeAtStop > 0) { // FIXME what is this arrivalTimeAtStop?
int wait = (int) Math.abs(s0.getTimeSeconds() - arrivalTimeAtStop);
s1.incrementTimeInSeconds(wait);
// this should only occur at the beginning
s1.incrementWeight(wait * options.waitAtBeginningFactor);
s1.setInitialWaitTimeSeconds(wait);
//LOG.debug("Initial wait time set to {} in PatternBoard", wait);
}
// during reverse optimization, board costs should be applied to PatternBoards
// so that comparable trip plans result (comparable to non-optimized plans)
if (options.reverseOptimizing)
s1.incrementWeight(options.getBoardCost(s0.getNonTransitMode()));
if (options.reverseOptimizeOnTheFly) {
TripPattern pattern = getPattern();
int thisDeparture = s0.getTripTimes().getDepartureTime(stopIndex);
int numTrips = getPattern().getNumScheduledTrips();
int nextDeparture;
s1.setLastNextArrivalDelta(Integer.MAX_VALUE);
for (int tripIndex = 0; tripIndex < numTrips; tripIndex++) {
Timetable timetable = pattern.getUpdatedTimetable(options, s0.getServiceDay());
nextDeparture = timetable.getTripTimes(tripIndex).getDepartureTime(stopIndex);
if (nextDeparture > thisDeparture) {
s1.setLastNextArrivalDelta(nextDeparture - thisDeparture);
break;
}
}
}
s1.setBackMode(getMode());
return s1.makeState();
} else {
/* We are going onto transit and must look for a suitable transit trip on this pattern. */
/* Disallow ever re-boarding the same trip pattern. */
if (s0.getLastPattern() == this.getPattern()) {
return null;
}
/* Check this pattern's mode against those allowed in the request. */
if (!options.modes.get(modeMask)) {
return null;
}
/* We assume all trips in a pattern are on the same route. Check if that route is banned. */
if (options.bannedRoutes != null && options.bannedRoutes.matches(getPattern().route)) {
// TODO: remove route checks in/after the trip search
return null;
}
/*
* Find the next boarding/alighting time relative to the current State. Check lists of
* transit serviceIds running yesterday, today, and tomorrow relative to the initial
* state. Choose the closest board/alight time among trips starting yesterday, today, or
* tomorrow. Note that we cannot skip searching on service days that have not started
* yet: Imagine a state at 23:59 Sunday, that should take a bus departing at 00:01
* Monday (and coded on Monday in the GTFS); disallowing Monday's departures would
* produce a strange plan. We also can't break off the search after we find trips today.
* Imagine a trip on a pattern at 25:00 today and another trip on the same pattern at
* 00:30 tommorrow. The 00:30 trip should be taken, but if we stopped the search after
* finding today's 25:00 trip we would never find tomorrow's 00:30 trip.
*/
TripPattern tripPattern = this.getPattern();
int bestWait = -1;
TripTimes bestTripTimes = null;
ServiceDay bestServiceDay = null;
for (ServiceDay sd : rctx.serviceDays) {
/* Find the proper timetable (updated or original) if there is a realtime snapshot. */
Timetable timetable = tripPattern.getUpdatedTimetable(options, sd);
/* Skip this day/timetable if no trip in it could possibly be useful. */
// TODO disabled until frequency representation is stable, and min/max timetable times are set from frequencies
// However, experiments seem to show very little measurable improvement here (due to cache locality?)
// if ( ! timetable.temporallyViable(sd, s0.getTimeSeconds(), bestWait, boarding)) continue;
/* Find the next or prev departure depending on final boolean parameter. */
TripTimes tripTimes = timetable.getNextTrip(s0, sd, stopIndex, boarding);
if (tripTimes != null) {
/* Wait is relative to departures on board and arrivals on alight. */
int wait = boarding ?
(int)(sd.time(tripTimes.getDepartureTime(stopIndex)) - s0.getTimeSeconds()):
(int)(s0.getTimeSeconds() - sd.time(tripTimes.getArrivalTime(stopIndex)));
/* A trip was found. The wait should be non-negative. */
if (wait < 0) LOG.error("Negative wait time when boarding.");
/* Track the soonest departure over all relevant schedules. */
if (bestWait < 0 || wait < bestWait) {
bestWait = wait;
bestServiceDay = sd;
bestTripTimes = tripTimes;
}
}
}
if (bestWait < 0) return null; // no appropriate trip was found
Trip trip = bestTripTimes.trip;
/* check if route and/or Agency are banned for this plan */
// FIXME this should be done WHILE searching for a trip.
if (options.tripIsBanned(trip)) return null;
/* Check if route is preferred by the user. */
long preferences_penalty = options.preferencesPenaltyForRoute(getPattern().route);
/* Compute penalty for non-preferred transfers. */
int transferPenalty = 0;
/* If this is not the first boarding, then we are transferring. */
if (s0.isEverBoarded()) {
TransferTable transferTable = options.getRoutingContext().transferTable;
int transferTime = transferTable.getTransferTime(s0.getPreviousStop(),
getStop(), s0.getPreviousTrip(), trip, boarding);
transferPenalty = transferTable.determineTransferPenalty(transferTime,
options.nonpreferredTransferPenalty);
}
/* Found a trip to board. Now make the child state. */
StateEditor s1 = s0.edit(this);
s1.setBackMode(getMode());
s1.setServiceDay(bestServiceDay);
// Save the trip times in the State to ensure that router has a consistent view
// and constant-time access to them.
s1.setTripTimes(bestTripTimes);
s1.incrementTimeInSeconds(bestWait);
s1.incrementNumBoardings();
s1.setTripId(trip.getId());
s1.setPreviousTrip(trip);
s1.setZone(getPattern().getZone(stopIndex));
s1.setRoute(trip.getRoute().getId());
double wait_cost = bestWait;
if (!s0.isEverBoarded() && !options.reverseOptimizing) {
wait_cost *= options.waitAtBeginningFactor;
s1.setInitialWaitTimeSeconds(bestWait);
} else {
wait_cost *= options.waitReluctance;
}
s1.incrementWeight(preferences_penalty);
s1.incrementWeight(transferPenalty);
// when reverse optimizing, the board cost needs to be applied on
// alight to prevent state domination due to free alights
if (options.reverseOptimizing) {
s1.incrementWeight(wait_cost);
} else {
s1.incrementWeight(wait_cost + options.getBoardCost(s0.getNonTransitMode()));
}
// On-the-fly reverse optimization
// determine if this needs to be reverse-optimized.
// The last alight can be moved forward by bestWait (but no further) without
// impacting the possibility of this trip
if (options.reverseOptimizeOnTheFly &&
!options.reverseOptimizing &&
s0.isEverBoarded() &&
s0.getLastNextArrivalDelta() <= bestWait &&
s0.getLastNextArrivalDelta() > -1) {
// it is re-reversed by optimize, so this still yields a forward tree
State optimized = s1.makeState().optimizeOrReverse(true, true);
if (optimized == null) LOG.error("Null optimized state. This shouldn't happen.");
return optimized;
}
/* If we didn't return an optimized path, return an unoptimized one. */
return s1.makeState();
}
}
/** @return the stop where this board/alight edge is located. */
private Stop getStop() {
PatternStopVertex stopVertex = (PatternStopVertex) (boarding ? tov : fromv);
return stopVertex.getStop();
}
public State optimisticTraverse(State state0) {
StateEditor s1 = state0.edit(this);
// no cost (see patternalight)
s1.setBackMode(getMode());
return s1.makeState();
}
/* See weightLowerBound comment. */
public double timeLowerBound(RoutingRequest options) {
if ((options.arriveBy && boarding) || (!options.arriveBy && !boarding)) {
if (!options.modes.get(modeMask)) {
return Double.POSITIVE_INFINITY;
}
BitSet services = getPattern().services;
for (ServiceDay sd : options.rctx.serviceDays) {
if (sd.anyServiceRunning(services)) return 0;
}
return Double.POSITIVE_INFINITY;
} else {
return 0;
}
}
/* If the main search is proceeding backward, the lower bound search is proceeding forward.
* Check the mode or serviceIds of this pattern at board time to see whether this pattern is
* worth exploring. If the main search is proceeding forward, board cost is added at board
* edges. The lower bound search is proceeding backward, and if it has reached a board edge the
* pattern was already deemed useful. */
public double weightLowerBound(RoutingRequest options) {
// return 0; // for testing/comparison, since 0 is always a valid heuristic value
if ((options.arriveBy && boarding) || (!options.arriveBy && !boarding))
return timeLowerBound(options);
else
return options.getBoardCostLowerBound();
}
@Override
public int getStopIndex() {
return stopIndex;
}
public String toString() {
return "TransitBoardAlight(" +
(boarding ? "boarding " : "alighting ") +
getFromVertex() + " to " + getToVertex() + ")";
}
}