/* 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 org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.Trip; import org.opentripplanner.routing.core.RoutingContext; 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.core.RoutingRequest; 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; import lombok.Getter; /** * 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 OnBoardForwardEdge { private static final long serialVersionUID = 1042740795612978747L; private static final Logger LOG = LoggerFactory.getLogger(TransitBoardAlight.class); private int stopIndex; private int modeMask; @Getter private boolean boarding; 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; } 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 TableTripPattern 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 TableTripPattern 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 State traverse(State state0) { return traverse(state0, 0); } public State traverse(State state0, long arrivalTimeAtStop) { RoutingContext rctx = state0.getContext(); RoutingRequest options = state0.getOptions(); // this method is on State not RoutingRequest because we care whether the user is in // possession of a rented bike. TraverseMode mode = state0.getNonTransitMode(); // Determine whether we are going onto or off of transit. // We are leaving transit iff the edge is a boarding and the search is arrive-by, // or the edge is not a boarding and the search is not arrive-by. boolean offTransit = (boarding && options.isArriveBy()) || (!boarding && !options.isArriveBy()); if (offTransit) { /* We are leaving transit, not as much to do. */ // do not alight immediately when arrive-depart dwell has been eliminated // this affects multi-itinerary searches (should be handled by PathParser) if (state0.getBackEdge() instanceof TransitBoardAlight) { return null; } StateEditor s1 = state0.edit(this); int type; if (boarding) type = getPattern().getBoardType(stopIndex); else type = getPattern().getAlightType(stopIndex + 1); if (TransitUtils.handleBoardAlightType(s1, type)) { return null; } s1.setTripId(null); s1.setLastAlightedTimeSeconds(state0.getTimeSeconds()); // For stop-to-stop transfer time, preference, and permission checking. // Vertices in transfer table are stop arrive/depart not pattern arrive/depart, // so previousStop is direction-dependent. s1.setPreviousStop(getStop()); s1.setLastPattern(this.getPattern()); // determine the wait if (arrivalTimeAtStop > 0) { int wait = (int) Math.abs(state0.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.isReverseOptimizing()) s1.incrementWeight(options.getBoardCost(mode)); if (options.isReverseOptimizeOnTheFly()) { int thisDeparture = state0.getTripTimes().getDepartureTime(stopIndex); int numTrips = getPattern().getNumScheduledTrips(); int nextDeparture; s1.setLastNextArrivalDelta(Integer.MAX_VALUE); for (int tripIndex = 0; tripIndex < numTrips; tripIndex++) { nextDeparture = getPattern().getDepartureTime(stopIndex, tripIndex); 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. */ if (state0.getLastPattern() == this.getPattern()) { return null; // to disallow ever re-boarding the same trip pattern } if (!options.getModes().get(modeMask)) { return null; } // TODO: assuming all trips within a pattern have the same route and agency, // we could check route and agency up front ("is pattern suitable") up front, rather than // below after the trip search. /* 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). If this pattern's serviceId is running, look for * the closest boarding/alighting time. 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. This proved to be a problem when reverse-optimizing * arrive-by trips; trips would get moved earlier for transfer purposes and then * the future days would not be considered. * 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. */ long current_time = state0.getTimeSeconds(); int bestWait = -1; TripTimes bestTripTimes = null; int serviceId = getPattern().getServiceId(); TripTimes tripTimes; // this method is on State not RoutingRequest because we care whether the user is in // possession of a rented bike. ServiceDay serviceDay = null; for (ServiceDay sd : rctx.serviceDays) { int wait; int secondsSinceMidnight = sd.secondsSinceMidnight(current_time); if (sd.serviceIdRunning(serviceId)) { // getNextTrip will find next or prev departure depending on final boolean parameter tripTimes = getPattern().getNextTrip(stopIndex, secondsSinceMidnight, state0, sd, mode == TraverseMode.BICYCLE, boarding); if (tripTimes != null) { wait = boarding ? // we care about departures on board and arrivals on alight (int)(sd.time(tripTimes.getDepartureTime(stopIndex)) - current_time): (int)(current_time - sd.time(tripTimes.getArrivalTime(stopIndex))); // a trip was found and the index is valid, so the wait should be non-negative if (wait < 0) LOG.error("negative wait time on board"); if (bestWait < 0 || wait < bestWait) { // track the soonest departure over all relevant schedules bestWait = wait; serviceDay = sd; bestTripTimes = tripTimes; } } } } if (bestWait < 0) { return null; // no appropriate trip was found } Trip trip = bestTripTimes.getTrip(); /* check if route and/or Agency are banned for this plan */ if (options.tripIsBanned(trip)) return null; /* check if route is preferred for this plan */ long preferences_penalty = options.preferencesPenaltyForTrip(trip); /* check whether this is a preferred transfer */ int transferPenalty = 0; if (state0.getNumBoardings() > 0) { // This is not the first boarding, thus a transfer TransferTable transferTable = options.getRoutingContext().transferTable; // Get the transfer time int transferTime = transferTable.getTransferTime(state0.getPreviousStop(), getStop(), state0.getPreviousTrip(), trip, boarding); // Determine transfer penalty transferPenalty = transferTable.determineTransferPenalty(transferTime, options.nonpreferredTransferPenalty); } StateEditor s1 = state0.edit(this); s1.setBackMode(getMode()); int type; if (boarding) type = getPattern().getBoardType(stopIndex); else type = getPattern().getAlightType(stopIndex + 1); // check: isn't this now handled inside the trip search? (AMB) if (TransitUtils.handleBoardAlightType(s1, type)) { return null; } s1.setServiceDay(serviceDay); // save the trip times 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 (state0.getNumBoardings() == 0 && !options.isReverseOptimizing()) { wait_cost *= options.waitAtBeginningFactor; // this is subtracted out in Analyst searches in lieu of reverse optimization 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.isReverseOptimizing()) s1.incrementWeight(wait_cost); else s1.incrementWeight(wait_cost + options.getBoardCost(mode)); // 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.isReverseOptimizeOnTheFly() && !options.isReverseOptimizing() && state0.getNumBoardings() > 0 && state0.getLastNextArrivalDelta() <= bestWait && state0.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 associated with this edge. * @return the stop associated with this edge */ private Stop getStop() { PatternStopVertex stopVertex; if (boarding) { stopVertex = (PatternStopVertex) tov; } else { stopVertex = (PatternStopVertex) 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(RoutingContext rctx) { if ((rctx.opt.isArriveBy() && boarding) || (!rctx.opt.isArriveBy() && !boarding)) { if (!rctx.opt.getModes().get(modeMask)) { return Double.POSITIVE_INFINITY; } int serviceId = getPattern().getServiceId(); for (ServiceDay sd : rctx.serviceDays) if (sd.serviceIdRunning(serviceId)) 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.isArriveBy() && boarding) || (!options.isArriveBy() && !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() + ")"; } }