package org.opentripplanner.routing.spt;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.edgetype.SimpleTransfer;
import org.opentripplanner.routing.edgetype.StreetEdge;
import java.io.Serializable;
import java.util.Objects;
/**
* A class that determines when one search branch prunes another at the same Vertex, and ultimately which solutions
* are retained. In the general case, one branch does not necessarily win out over the other, i.e. multiple states can
* coexist at a single Vertex.
*
* Even functions where one state always wins (least weight, fastest travel time) are applied within a multi-state
* shortest path tree because bike rental, car or bike parking, and turn restrictions all require multiple incomparable
* states at the same vertex. These need the graph to be "replicated" into separate layers, which is achieved by
* applying the main dominance logic (lowest weight, lowest cost, Pareto) conditionally, only when the two states
* have identical bike/car/turn direction status.
*
* Dominance functions are serializable so that routing requests may passed between machines in different JVMs, for instance
* in OTPA Cluster.
*/
public abstract class DominanceFunction implements Serializable {
private static final long serialVersionUID = 1;
/**
* Return true if the first state "defeats" the second state or at least ties with it in terms of suitability.
* In the case that they are tied, we still want to return true so that an existing state will kick out a new one.
* Provide this custom logic in subclasses. You would think this could be static, but in Java for some reason
* calling a static function will call the one on the declared type, not the runtime instance type.
*/
protected abstract boolean betterOrEqual(State a, State b);
/**
* For bike rental, parking, and approaching turn-restricted intersections states are incomparable:
* they exist on separate planes. The core state dominance logic is wrapped in this public function and only
* applied when the two states have all these variables in common (are on the same plane).
*/
public boolean betterOrEqualAndComparable(State a, State b) {
// States before boarding transit and after riding transit are incomparable.
// This allows returning transit options even when walking to the destination is the optimal strategy.
if (a.isEverBoarded() != b.isEverBoarded()) {
return false;
}
// The result of a SimpleTransfer must not block alighting normally from transit. States that are results of
// SimpleTransfers are incomparable with states that are not the result of SimpleTransfers.
if ((a.backEdge instanceof SimpleTransfer) != (b.backEdge instanceof SimpleTransfer)) {
return false;
}
// Does one state represent riding a rented bike and the other represent walking before/after rental?
if (a.isBikeRenting() != b.isBikeRenting()) {
return false;
}
// In case of bike renting, different networks (ie incompatible bikes) are not comparable
if (a.isBikeRenting()) {
if (!Objects.equals(a.getBikeRentalNetworks(), b.getBikeRentalNetworks()))
return false;
}
// Does one state represent driving a car and the other represent walking after the car was parked?
if (a.isCarParked() != b.isCarParked()) {
return false;
}
// Does one state represent riding a bike and the other represent walking after the bike was parked?
if (a.isBikeParked() != b.isBikeParked()) {
return false;
}
// Are the two states arriving at a vertex from two different directions where turn restrictions apply?
if (a.backEdge != b.getBackEdge() && (a.backEdge instanceof StreetEdge)) {
if (! a.getOptions().getRoutingContext().graph.getTurnRestrictions(a.backEdge).isEmpty()) {
return false;
}
}
// These two states are comparable (they are on the same "plane" or "copy" of the graph).
return betterOrEqual(a, b);
}
/**
* Create a new shortest path tree using this function, considering whether it allows co-dominant States.
* MultiShortestPathTree is the general case -- it will work with both single- and multi-state functions.
*/
public ShortestPathTree getNewShortestPathTree(RoutingRequest routingRequest) {
return new ShortestPathTree(routingRequest, this);
}
public static class MinimumWeight extends DominanceFunction {
/** Return true if the first state has lower weight than the second state. */
@Override
public boolean betterOrEqual (State a, State b) { return a.weight <= b.weight; }
}
/**
* This approach is more coherent in Analyst when we are extracting travel times from the optimal
* paths. It also leads to less branching and faster response times when building large shortest path trees.
*/
public static class EarliestArrival extends DominanceFunction {
/** Return true if the first state has lower elapsed time than the second state. */
@Override
public boolean betterOrEqual (State a, State b) { return a.getElapsedTimeSeconds() <= b.getElapsedTimeSeconds(); }
}
/**
* A dominance function that prefers the least walking. This should only be used with walk-only searches because
* it does not include any functions of time, and once transit is boarded walk distance is constant.
*
* It is used when building stop tree caches for egress from transit stops.
*/
public static class LeastWalk extends DominanceFunction {
@Override
protected boolean betterOrEqual(State a, State b) {
return a.getWalkDistance() <= b.getWalkDistance();
}
}
/** In this implementation the relation is not symmetric. There are sets of mutually co-dominant states. */
public static class Pareto extends DominanceFunction {
@Override
public boolean betterOrEqual (State a, State b) {
// The key problem in pareto-dominance in OTP is that the elements of the state vector are not orthogonal.
// When walk distance increases, weight increases. When time increases weight increases.
// It's easy to get big groups of very similar states that don't represent significantly different outcomes.
// Our solution to this is to give existing states some slack to dominate new states more easily.
final double EPSILON = 1e-4;
return (a.getElapsedTimeSeconds() <= (b.getElapsedTimeSeconds() + EPSILON)
&& a.getWeight() <= (b.getWeight() + EPSILON));
}
}
}