/* 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.core;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.opentripplanner.routing.algorithm.NegativeWeightException;
import org.opentripplanner.routing.automata.AutomatonState;
import org.opentripplanner.routing.edgetype.OnBoardForwardEdge;
import org.opentripplanner.routing.edgetype.TablePatternEdge;
import org.opentripplanner.routing.edgetype.PlainStreetEdge;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.TransitBoardAlight;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.patch.Alert;
import org.opentripplanner.routing.pathparser.PathParser;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class State implements Cloneable {
/* Data which is likely to change at most traversals */
// the current time at this state, in milliseconds
protected long time;
// accumulated weight up to this state
protected double weight;
// associate this state with a vertex in the graph
protected Vertex vertex;
// allow path reconstruction from states
protected State backState;
protected Edge backEdge;
// allow traverse result chaining (multiple results)
protected State next;
/* StateData contains data which is unlikely to change as often */
public StateData stateData;
// how far have we walked
// TODO(flamholz): this is a very confusing name as it actually applies to all non-transit modes.
// we should DEFINITELY rename this variable and the associated methods.
protected double walkDistance;
// track the states of all path parsers -- probably changes frequently
protected int[] pathParserStates;
private static final Logger LOG = LoggerFactory.getLogger(State.class);
/* CONSTRUCTORS */
/**
* Create an initial state representing the beginning of a search for the given routing context.
* Initial "parent-less" states can only be created at the beginning of a trip. elsewhere, all
* states must be created from a parent and associated with an edge.
*/
public State(RoutingRequest opt) {
this(opt.rctx.origin, opt.rctx.originBackEdge, opt.getSecondsSinceEpoch(), opt);
}
/**
* Create an initial state, forcing vertex to the specified value. Useful for reusing a
* RoutingContext in TransitIndex, tests, etc.
*/
public State(Vertex vertex, RoutingRequest opt) {
// Since you explicitly specify, the vertex, we don't set the backEdge.
this(vertex, opt.getSecondsSinceEpoch(), opt);
}
/**
* Create an initial state, forcing vertex and time to the specified values. Useful for reusing
* a RoutingContext in TransitIndex, tests, etc.
*/
public State(Vertex vertex, long timeSeconds, RoutingRequest options) {
// Since you explicitly specify, the vertex, we don't set the backEdge.
this(vertex, null, timeSeconds, options);
}
/**
* Create an initial state, forcing vertex, back edge and time to the specified values. Useful for reusing
* a RoutingContext in TransitIndex, tests, etc.
*/
public State(Vertex vertex, Edge backEdge, long timeSeconds, RoutingRequest options) {
this.weight = 0;
this.vertex = vertex;
this.backEdge = backEdge;
this.backState = null;
this.stateData = new StateData(options);
// note that here we are breaking the circular reference between rctx and options
// this should be harmless since reversed clones are only used when routing has finished
this.stateData.opt = options;
this.stateData.startTime = timeSeconds;
this.stateData.usingRentedBike = false;
this.walkDistance = 0;
this.time = timeSeconds * 1000;
if (options.rctx != null) {
this.pathParserStates = new int[options.rctx.pathParsers.length];
Arrays.fill(this.pathParserStates, AutomatonState.START);
}
stateData.routeSequence = new AgencyAndId[0];
}
/**
* Create a state editor to produce a child of this state, which will be the result of
* traversing the given edge.
*
* @param e
* @return
*/
public StateEditor edit(Edge e) {
return new StateEditor(this, e);
}
protected State clone() {
State ret;
try {
ret = (State) super.clone();
} catch (CloneNotSupportedException e1) {
throw new IllegalStateException("This is not happening");
}
return ret;
}
/*
* FIELD ACCESSOR METHODS States are immutable, so they have only get methods. The corresponding
* set methods are in StateEditor.
*/
/**
* Retrieve a State extension based on its key.
*
* @param key - An Object that is a key in this State's extension map
* @return - The extension value for the given key, or null if not present
*/
public Object getExtension(Object key) {
if (stateData.extensions == null) {
return null;
}
return stateData.extensions.get(key);
}
public String toString() {
return "<State " + new Date(getTimeInMillis()) + " [" + weight + "] " + (isBikeRenting() ? "BIKE_RENT " : "") + vertex + ">";
}
public String toStringVerbose() {
return "<State " + new Date(getTimeInMillis()) +
" w=" + this.getWeight() +
" t=" + this.getElapsedTimeSeconds() +
" d=" + this.getWalkDistance() +
" b=" + this.getNumBoardings() + ">";
}
/** Returns time in seconds since epoch */
public long getTimeSeconds() {
return time / 1000;
}
/** returns the length of the trip in seconds up to this state */
public long getElapsedTimeSeconds() {
return Math.abs(getTimeSeconds() - stateData.startTime);
}
public TripTimes getTripTimes() {
return stateData.tripTimes;
}
/**
* Returns the length of the trip in seconds up to this time, not including the initial wait.
* It subtracts out the initial wait, up to a clamp value specified in the request.
* If the clamp value is set to -1, no clamping will occur.
* If the clamp value is set to 0, the initial wait time will not be subtracted out
* (i.e. it will be clamped to zero).
* This is used in lieu of reverse optimization in Analyst.
*/
public long getActiveTime () {
long clampInitialWait = stateData.opt.clampInitialWait;
long initialWait = stateData.initialWaitTime;
// only subtract up the clamp value
if (clampInitialWait >= 0 && initialWait > clampInitialWait)
initialWait = clampInitialWait;
long activeTime = getElapsedTimeSeconds() - initialWait;
// TODO: what should be done here? (Does this ever happen?)
if (activeTime < 0) {
LOG.warn("initial wait was greater than elapsed time.");
activeTime = getElapsedTimeSeconds();
}
return activeTime;
}
public AgencyAndId getTripId() {
return stateData.tripId;
}
public Trip getPreviousTrip() {
return stateData.previousTrip;
}
public String getZone() {
return stateData.zone;
}
public AgencyAndId getRoute() {
return stateData.route;
}
public int getNumBoardings() {
return stateData.numBoardings;
}
public boolean isAlightedLocal() {
return stateData.alightedLocal;
}
/**
* Whether this path has ever previously boarded (or alighted from, in a reverse search) a
* transit vehicle
*/
public boolean isEverBoarded() {
return stateData.everBoarded;
}
public boolean isBikeRenting() {
return stateData.usingRentedBike;
}
/**
* @return True if the state at vertex can be the end of path.
*/
public boolean isFinal() {
return !isBikeRenting();
}
public Stop getPreviousStop() {
return stateData.previousStop;
}
public long getLastAlightedTimeSeconds() {
return stateData.lastAlightedTime;
}
public double getWalkDistance() {
return walkDistance;
}
public Vertex getVertex() {
return this.vertex;
}
public int getLastNextArrivalDelta () {
return stateData.lastNextArrivalDelta;
}
/**
* Multicriteria comparison of states.
* @return True if this state is better than the other one (or equal)
* both in terms of time and weight.
*/
public boolean dominates(State other) {
if (other.weight == 0) {
return false;
}
// Multi-state (bike rental) - no domination for different states
if (isBikeRenting() != other.isBikeRenting())
return false;
if (backEdge != other.getBackEdge() && ((backEdge instanceof PlainStreetEdge)
&& (!((PlainStreetEdge) backEdge).getTurnRestrictions().isEmpty())))
return false;
if (this.similarRouteSequence(other)) {
return this.weight <= other.weight;
}
double weightDiff = this.weight / other.weight;
return walkDistance <= other.getWalkDistance() * 1.05
&& (weightDiff < 1.02 && this.weight - other.weight < 30)
&& this.getElapsedTimeSeconds() - other.getElapsedTimeSeconds() <= 30;
}
/**
* Returns true if this state's weight is lower than the other one. Considers only weight and
* not time or other criteria.
*/
public boolean betterThan(State other) {
return this.weight < other.weight;
}
public double getWeight() {
return this.weight;
}
public int getTimeDeltaSeconds() {
return (int) (getTimeSeconds() - backState.getTimeSeconds());
}
public int getAbsTimeDeltaSeconds() {
return (int) Math.abs(getTimeSeconds() - backState.getTimeSeconds());
}
public double getWalkDistanceDelta () {
if (backState != null)
return Math.abs(this.walkDistance - backState.walkDistance);
else
return 0.0;
}
public double getWeightDelta() {
return this.weight - backState.weight;
}
public void checkNegativeWeight() {
double dw = this.weight - backState.weight;
if (dw < 0) {
throw new NegativeWeightException(String.valueOf(dw) + " on edge " + backEdge);
}
}
public boolean isOnboard() {
return this.backEdge instanceof OnBoardForwardEdge;
}
public State getBackState() {
return this.backState;
}
public TraverseMode getBackMode () {
return stateData.backMode;
}
public boolean isBackWalkingBike () {
return stateData.backWalkingBike;
}
public Set<Alert> getBackAlerts () {
return stateData.notes;
}
/**
* Get the name of the direction used to get to this state. For transit, it is the headsign,
* while for other things it is what you would expect.
*/
public String getBackDirection () {
// This can happen when stop_headsign says different things at two trips on the same
// pattern and at the same stop.
if (backEdge instanceof TablePatternEdge) {
return stateData.tripTimes.getHeadsign(((TablePatternEdge)backEdge).getStopIndex());
}
else {
return backEdge.getDirection();
}
}
/**
* Get the back trip of the given state. For time dependent transit, State will find the
* right thing to do.
*/
public Trip getBackTrip () {
if (backEdge instanceof TablePatternEdge) {
return stateData.tripTimes.getTrip();
}
else {
return backEdge.getTrip();
}
}
public Edge getBackEdge() {
return this.backEdge;
}
public boolean exceedsWeightLimit(double maxWeight) {
return weight > maxWeight;
}
public long getStartTimeSeconds() {
return stateData.startTime;
}
/**
* Optional next result that allows {@link Edge} to return multiple results from
* {@link Edge#traverse(State, RoutingRequest)} or
* {@link Edge#traverseBack(State, RoutingRequest)}
*
* @return the next additional result from an edge traversal, or null if no more results
*/
public State getNextResult() {
return next;
}
/**
* Extend an exiting result chain by appending this result to the existing chain. The usage
* model looks like this:
*
* <code>
* TraverseResult result = null;
*
* for( ... ) {
* TraverseResult individualResult = ...;
* result = individualResult.addToExistingResultChain(result);
* }
*
* return result;
* </code>
*
* @param existingResultChain the tail of an existing result chain, or null if the chain has not
* been started
* @return
*/
public State addToExistingResultChain(State existingResultChain) {
if (this.getNextResult() != null)
throw new IllegalStateException("this result already has a next result set");
next = existingResultChain;
return this;
}
public State detachNextResult() {
State ret = this.next;
this.next = null;
return ret;
}
public RoutingContext getContext() {
return stateData.opt.rctx;
}
public RoutingRequest getOptions () {
return stateData.opt;
}
/* will return BICYCLE if routing with an owned bicycle, or if at this state the user is holding
* on to a rented bicycle */
public TraverseMode getNonTransitMode() {
return stateData.nonTransitMode;
}
public State reversedClone() {
// We no longer compensate for schedule slack (minTransferTime) here.
// It is distributed symmetrically over all preboard and prealight edges.
State newState = new State(this.vertex, getTimeSeconds(), stateData.opt.reversedClone());
newState.stateData.tripTimes = stateData.tripTimes;
newState.stateData.initialWaitTime = stateData.initialWaitTime;
return newState;
}
public void dumpPath() {
System.out.printf("---- FOLLOWING CHAIN OF STATES ----\n");
State s = this;
while (s != null) {
System.out.printf("%s via %s by %s\n", s, s.backEdge, s.getBackMode());
s = s.backState;
}
System.out.printf("---- END CHAIN OF STATES ----\n");
}
public long getTimeInMillis() {
return time;
}
public boolean similarRouteSequence(State that) {
AgencyAndId[] rs0 = this.stateData.routeSequence;
AgencyAndId[] rs1 = that.stateData.routeSequence;
if (rs0 == rs1)
return true;
int n = rs0.length < rs1.length ? rs0.length : rs1.length;
for (int i = 0; i < n; i++)
if (rs0[i] != rs1[i])
return false;
return true;
}
public double getWalkSinceLastTransit() {
return walkDistance - stateData.lastTransitWalk;
}
public double getWalkAtLastTransit() {
return stateData.lastTransitWalk;
}
public boolean multipleOptionsBefore() {
boolean foundAlternatePaths = false;
TraverseMode requestedMode = getNonTransitMode();
for (Edge out : backState.vertex.getOutgoing()) {
if (out == backEdge) {
continue;
}
if (!(out instanceof StreetEdge)) {
continue;
}
State outState = out.traverse(backState);
if (outState == null) {
continue;
}
if (!outState.getBackMode().equals(requestedMode)) {
//walking a bike, so, not really an exit
continue;
}
// this section handles the case of an option which is only an option if you walk your
// bike. It is complicated because you will not need to walk your bike until one
// edge after the current edge.
//now, from here, try a continuing path.
Vertex tov = outState.getVertex();
boolean found = false;
for (Edge out2 : tov.getOutgoing()) {
State outState2 = out2.traverse(outState);
if (outState2 != null && !outState2.getBackMode().equals(requestedMode)) {
// walking a bike, so, not really an exit
continue;
}
found = true;
break;
}
if (!found) {
continue;
}
// there were paths we didn't take.
foundAlternatePaths = true;
break;
}
return foundAlternatePaths;
}
public boolean allPathParsersAccept() {
PathParser[] parsers = this.stateData.opt.rctx.pathParsers;
for (int i = 0; i < parsers.length; i++)
if ( ! parsers[i].accepts(pathParserStates[i]))
return false;
return true;
}
public String getPathParserStates() {
StringBuilder sb = new StringBuilder();
sb.append("( ");
for (int i : pathParserStates)
sb.append(String.format("%02d ", i));
sb.append(")");
return sb.toString();
}
/** @return the last TripPattern used in this path (which is set when leaving the vehicle). */
public TripPattern getLastPattern() {
return stateData.lastPattern;
}
public ServiceDay getServiceDay() {
return stateData.serviceDay;
}
public void setServiceDay(ServiceDay sd) {
stateData.serviceDay = sd;
}
public Set<String> getBikeRentalNetworks() {
return stateData.bikeRentalNetworks;
}
/**
* Reverse the path implicit in the given state, re-traversing all edges in the opposite
* direction so as to remove any unnecessary waiting in the resulting itinerary. This produces a
* path that passes through all the same edges, but which may have a shorter overall duration
* due to different weights on time-dependent (e.g. transit boarding) edges. If the optimize
* parameter is false, the path will be reversed but will have the same duration. This is the
* result of combining the functions from GraphPath optimize and reverse.
*
* @param optimize Should this path be optimized or just reversed?
* @param forward Is this an on-the-fly reverse search in the midst of a forward search?
* @returns a state at the other end (or this end, in the case of a forward search)
* of a reversed, optimized path
*/
public State optimizeOrReverse (boolean optimize, boolean forward) {
State orig = this;
State unoptimized = orig;
State ret = orig.reversedClone();
long newInitialWaitTime = this.stateData.initialWaitTime;
PathParser pathParsers[];
// disable path parsing temporarily
pathParsers = stateData.opt.rctx.pathParsers;
stateData.opt.rctx.pathParsers = new PathParser[0];
Edge edge = null;
while (orig.getBackState() != null) {
edge = orig.getBackEdge();
if (optimize) {
// first board/last alight: figure in wait time in on the fly optimization
if (edge instanceof TransitBoardAlight &&
forward &&
orig.getNumBoardings() == 1 &&
(
// boarding in a forward main search
(((TransitBoardAlight) edge).isBoarding() &&
!stateData.opt.isArriveBy()) ||
// alighting in a reverse main search
(!((TransitBoardAlight) edge).isBoarding() &&
stateData.opt.isArriveBy())
)
) {
ret = ((TransitBoardAlight) edge).traverse(ret, orig.getBackState().getTimeSeconds());
newInitialWaitTime = ret.stateData.initialWaitTime;
}
else
ret = edge.traverse(ret);
if (ret == null) {
LOG.warn("Cannot reverse path at edge: " + edge +
", returning unoptimized path. If edge is a " +
"PatternInterlineDwell or if there is a time-dependent turn " +
"restriction here, this is not totally unexpected; " +
"otherwise, you might want to look into it");
// re-enable path parsing
stateData.opt.rctx.pathParsers = pathParsers;
if (forward)
return this;
else
return unoptimized.reverse();
}
}
else {
StateEditor editor = ret.edit(edge);
// note the distinction between setFromState and setBackState
editor.setFromState(orig);
editor.incrementTimeInSeconds(orig.getAbsTimeDeltaSeconds());
editor.incrementWeight(orig.getWeightDelta());
editor.incrementWalkDistance(orig.getWalkDistanceDelta());
// propagate the modes and alerts through to the reversed edge
editor.setBackMode(orig.getBackMode());
editor.addAlerts(orig.getBackAlerts());
if (orig.isBikeRenting() != orig.getBackState().isBikeRenting())
editor.setBikeRenting(!orig.isBikeRenting());
editor.setNumBoardings(getNumBoardings() - orig.getNumBoardings());
ret = editor.makeState();
//EdgeNarrative origNarrative = orig.getBackEdgeNarrative();
//EdgeNarrative retNarrative = ret.getBackEdgeNarrative();
//copyExistingNarrativeToNewNarrativeAsAppropriate(origNarrative, retNarrative);
}
orig = orig.getBackState();
}
// re-enable path parsing
stateData.opt.rctx.pathParsers = pathParsers;
if (forward) {
State reversed = ret.reverse();
if (getWeight() <= reversed.getWeight())
LOG.warn("Optimization did not decrease weight: before " + this.getWeight()
+ " after " + reversed.getWeight());
if (getElapsedTimeSeconds() != reversed.getElapsedTimeSeconds())
LOG.warn("Optimization changed time: before " + this.getElapsedTimeSeconds() + " after "
+ reversed.getElapsedTimeSeconds());
if (getActiveTime() <= reversed.getActiveTime())
// NOTE: this can happen and it isn't always bad (i.e. it doesn't always mean that
// reverse-opt got called when it shouldn't have). Imagine three lines A, B and C
// A trip takes line A at 7:00 and arrives at the first transit center at 7:30, where line
// B is boarded at 7:40 to another transit center with an arrival at 8:00. At 8:30, line C
// is boarded. Suppose line B runs every ten minutes and the other two run every hour. The
// optimizer will optimize the B->C connection, moving the trip on line B forward
// ten minutes. However, it will not be able to move the trip on Line A forward because
// there is not another possible trip. The waiting time will get pushed towards the
// the beginning, but not all the way.
LOG.warn("Optimization did not decrease active time: before "
+ this.getActiveTime() + " after " + reversed.getActiveTime()
+ ", boardings: " + this.getNumBoardings());
if (reversed.getWeight() < this.getBackState().getWeight())
// This is possible; imagine a trip involving three lines, line A, line B and
// line C. Lines A and C run hourly while Line B runs every ten minute starting
// at 8:55. The user boards line A at 7:00 and gets off at the first transfer point
// (point u) at 8:00. The user then boards the first run of line B at 8:55, an optimal
// transfer since there is no later trip on line A that could have been taken. The user
// deboards line B at point v at 10:00, and boards line C at 10:15. This is a
// non-optimal transfer; the trip on line B can be moved forward 10 minutes. When
// that happens, the first transfer becomes non-optimal (8:00 to 9:05) and the trip
// on line A can be moved forward an hour, thus moving 55 minutes of waiting time
// from a previous state to the beginning of the trip where it is significantly
// cheaper.
LOG.warn("Weight has been reduced enough to make it run backwards, now:"
+ reversed.getWeight() + " backState " + getBackState().getWeight() + ", "
+ "number of boardings: " + getNumBoardings());
if (getTimeSeconds() != reversed.getTimeSeconds())
LOG.warn("Times do not match");
if (Math.abs(getWeight() - reversed.getWeight()) > 1
&& newInitialWaitTime == stateData.initialWaitTime)
LOG.warn("Weight is changed (before: " + getWeight() + ", after: "
+ reversed.getWeight() + "), initial wait times " + "constant at "
+ newInitialWaitTime);
if (newInitialWaitTime != reversed.stateData.initialWaitTime)
LOG.warn("Initial wait time not propagated: is "
+ reversed.stateData.initialWaitTime + ", should be " + newInitialWaitTime);
// copy the path parser states so this path is not thrown out going forward
// reversed.pathParserStates =
// Arrays.copyOf(this.pathParserStates, this.pathParserStates.length, newLength);
// copy things that didn't get copied
reversed.initializeFieldsFrom(this);
return reversed;
}
else
return ret;
}
/**
* Reverse-optimize a path after it is complete, by default
*/
public State optimize() {
return optimizeOrReverse(true, false);
}
/**
* Reverse a path
*/
public State reverse() {
return optimizeOrReverse(false, false);
}
/**
* After reverse-optimizing, many things are not set. Set them from the unoptimized state.
* @param o The other state to initialize things from.
*/
private void initializeFieldsFrom (State o) {
StateData currentStateData = this.stateData;
// easier to clone and copy back, plus more future proof
this.stateData = o.stateData.clone();
this.stateData.initialWaitTime = currentStateData.initialWaitTime;
// this will get re-set on the next alight (or board in a reverse search)
this.stateData.lastNextArrivalDelta = -1;
}
public boolean getReverseOptimizing () {
return stateData.opt.reverseOptimizing;
}
public double getOptimizedElapsedTimeSeconds() {
return getElapsedTimeSeconds() - stateData.initialWaitTime;
}
}