/* 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.graph_builder.impl.raptor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import org.opentripplanner.routing.core.ServiceDay;
import org.opentripplanner.routing.edgetype.TableTripPattern;
import org.opentripplanner.routing.edgetype.TransitBoardAlight;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.impl.raptor.RaptorRoute;
class DurationAndDistance {
int duration;
double distance;
int round;
public DurationAndDistance(int round, int duration, double distance) {
this.round = round;
this.duration = duration;
this.distance = distance;
}
public String toString() {
return "[" + duration + ", " + distance + "]";
}
}
/** A codominant set of duration/distance costs at a certain time */
class CostsAtTime {
int time;
ArrayBag<DurationAndDistance> costs;
public CostsAtTime(int time) {
this.time = time;
costs = new ArrayBag<DurationAndDistance>();
}
public void forceAdd(int round, int duration, double distance) {
costs.add(new DurationAndDistance(round, duration, distance));
}
public void forceAdd(DurationAndDistance cost) {
costs.add(cost);
}
public boolean add(int round, int duration, double distance) {
for (Iterator<DurationAndDistance> it = costs.iterator(); it.hasNext();) {
DurationAndDistance existing = it.next();
if (existing.duration <= duration && existing.distance <= distance * 1.05) {
return false;
} else if (existing.duration >= duration && existing.distance * 1.05 >= distance) {
it.remove();
}
}
costs.add(new DurationAndDistance(round, duration, distance));
return true;
}
public boolean strictlyDominates(int duration, double distance) {
for (DurationAndDistance existing : costs) {
if (existing.duration <= duration && existing.distance <= distance * 1.05) {
return true;
}
if (existing.duration > duration && existing.distance * 1.05 > distance) {
return false;
}
}
return false;
}
public String toString() {
String costStr = "";
int i = 1;
for (DurationAndDistance cost : costs) {
costStr += cost;
costStr += "@" + (cost.duration + time);
if (i++ < costs.size()) {
if (i > 30) {
costStr += "...";
break;
}
costStr += ", ";
}
}
return "[@" + time + ": " + costStr + "]";
}
public int size() {
return costs.size();
}
public boolean isEmpty() {
return costs.isEmpty();
}
}
class CostsAtTimeComparator implements Comparator<Object> {
@Override
public int compare(Object arg0, Object arg1) {
int t0, t1;
if (arg0 instanceof Integer) {
t0 = (Integer) arg0;
} else if (arg0 instanceof CostsAtTime) {
t0 = ((CostsAtTime) arg0).time;
} else {
throw new UnsupportedOperationException();
}
if (arg1 instanceof Integer) {
t1 = (Integer) arg1;
} else if (arg1 instanceof CostsAtTime) {
t1 = ((CostsAtTime) arg1).time;
} else {
throw new UnsupportedOperationException();
}
if (t0 > t1) {
return 1;
} else if (t0 < t1) {
return -1;
} else {
return 0;
}
}
}
/**
* A profile of trip duration/walk distance for a given stop across a span of time
*
* @author novalis
*
*/
public class StopProfile {
ArrayList<CostsAtTime> profile;
Vertex vertex;
private Comparator<? super Object> comparator = new CostsAtTimeComparator();
boolean atDestination;
public StopProfile(Vertex vertex) {
this(vertex, false);
}
public StopProfile(Vertex vertex, boolean atDestination) {
this.vertex = vertex;
profile = new ArrayList<CostsAtTime>();
this.atDestination = atDestination;
}
public boolean transitTo(StopProfile destination, RaptorRoute route, int originStopIndex,
int destinationStopIndex, ArrayList<ServiceDay> days, long startTime, int round) {
boolean anyBetter = false;
for (TransitBoardAlight board : route.boards[originStopIndex]) {
anyBetter |= transitTo(destination, board.getPattern(), originStopIndex,
destinationStopIndex, days, startTime, round);
}
return anyBetter;
}
/**
* Fill in this stop profile with data about trips from the origin stop to the destination stop.
* This profile is associated with the origin stop.
*/
public boolean transitTo(StopProfile destination, TableTripPattern pattern,
int originStopIndex, int destinationStopIndex, ArrayList<ServiceDay> days,
long startTime, int round) {
final int nTrips = pattern.getTrips().size();
ArrayList<ServiceDay> runningList = new ArrayList<ServiceDay>();
for (ServiceDay day : days) {
if (day.serviceIdRunning(pattern.getServiceId()))
runningList.add(day);
}
ServiceDay[] runningDays = runningList.toArray(new ServiceDay[0]);
boolean anyBetter = false;
for (int trip = 0; trip < nTrips; ++trip) {
int departureSecondsSinceMidnight = pattern.getDepartureTime(originStopIndex, trip);
int arrivalSecondsSinceMidnight = pattern
.getArrivalTime(destinationStopIndex - 1, trip);
final int duration = arrivalSecondsSinceMidnight - departureSecondsSinceMidnight;
for (ServiceDay day : runningDays) {
int arrivalTime = (int) (day.time(arrivalSecondsSinceMidnight) - startTime);
int departureTime = arrivalTime - duration;
if (departureTime > 86400 * 2) {
continue;
}
// find the costs for a trip arriving at arrivalTime
CostsAtTime costsAtDestination;
if (destination.atDestination) {
costsAtDestination = new CostsAtTime(arrivalTime);
costsAtDestination.forceAdd(-1, 0, 0.0);
} else {
int arrivalTimeInsertionPoint = Collections.binarySearch(destination.profile,
arrivalTime, comparator);
if (arrivalTimeInsertionPoint < 0) {
arrivalTimeInsertionPoint = -arrivalTimeInsertionPoint - 1;
}
if (arrivalTimeInsertionPoint == destination.profile.size()) {
// this trip actually arrives after the last thing we know about
// at the destination.
/*
* System.out.println(
* "Unexpectedly found end of destination list; I think it is safe" +
* " to throw this away");
*/
continue;
}
costsAtDestination = destination.profile.get(arrivalTimeInsertionPoint);
}
// find when a trip departing at departureTime would fit in this profile
int originIndex = Collections.binarySearch(profile, departureTime, comparator);
if (originIndex < 0) {
originIndex = -originIndex - 1;
}
// System.out.println("At " + nextDestination.time);
// find the next (or same-time) departure from here
if (originIndex == profile.size()) {
CostsAtTime toInsert = new CostsAtTime(departureTime);
for (DurationAndDistance atDestination : costsAtDestination.costs) {
if (atDestination.round != round - 1) continue;
int walkDuration = atDestination.duration + duration;
double walkDistance = atDestination.distance;
toInsert.forceAdd(round, walkDuration, walkDistance);
anyBetter = true;
}
profile.add(toInsert);
} else {
CostsAtTime costsAtNextTime = profile.get(originIndex);
if (costsAtNextTime.time >= departureTime) {
if (!addIfNotDominated(costsAtDestination, departureTime, originIndex,
duration, 0, round)) {
continue;
}
anyBetter = true;
}
}
if (originIndex > 0)
dominatePreviousDepartures(originIndex, round);
}
}
return anyBetter;
}
/**
* Walk to a stop distance/time away, with the profile destination.
*/
public boolean walkTo(StopProfile destination, int duration, double distance, int round) {
if (profile.size() > 1000) {
//System.out.println("large profile");
}
boolean anyBetter = false;
Iterator<CostsAtTime> destinationIt = destination.profile.iterator();
// System.out.println("Walk from " + destination.vertex + " to " + vertex);
while (destinationIt.hasNext()) {
CostsAtTime nextDestination = destinationIt.next();
int timeAtOrigin = nextDestination.time - duration;
// find the next (or same-time) departure from here
int originIndex = Collections.binarySearch(profile, timeAtOrigin, comparator);
if (originIndex < 0) {
originIndex = -originIndex - 1;
}
if (originIndex == profile.size()) {
CostsAtTime toInsert = new CostsAtTime(timeAtOrigin);
for (DurationAndDistance atDestination : nextDestination.costs) {
if (atDestination.round != round - 1) continue;
int walkDuration = atDestination.duration + duration;
double walkDistance = atDestination.distance + distance;
if (walkDistance > 3218)
continue;
toInsert.forceAdd(round, walkDuration, walkDistance);
}
profile.add(toInsert);
} else {
CostsAtTime costsAtNextTime = profile.get(originIndex);
if (costsAtNextTime.time >= timeAtOrigin) {
if (!addIfNotDominated(nextDestination, timeAtOrigin, originIndex, duration,
distance, round)) {
continue;
}
}
}
anyBetter = true;
if (originIndex > 0)
dominatePreviousDepartures(originIndex, round);
}
return anyBetter;
}
private void dominatePreviousDepartures(int originIndex, int round) {
CostsAtTime inserted = profile.get(originIndex);
CostsAtTime costsAtDeparture = new CostsAtTime(inserted.time);
for (DurationAndDistance d : inserted.costs) {
costsAtDeparture.forceAdd(d);
}
originIndex -= 1;
int timeAtOrigin = inserted.time;
int index = originIndex;
while (index >= 0) {
boolean shouldContinue = false;
CostsAtTime costsAtPrevTime = profile.get(index);
int waitTime = timeAtOrigin - costsAtPrevTime.time;
ArrayList<DurationAndDistance> toAdd = new ArrayList<DurationAndDistance>();
ArrayList<DurationAndDistance> duplicates = new ArrayList<DurationAndDistance>();
for (Iterator<DurationAndDistance> costsIt = costsAtDeparture.costs.iterator(); costsIt
.hasNext();) {
DurationAndDistance costs = costsIt.next();
int duration = costs.duration + waitTime;
boolean add = false;
for (Iterator<DurationAndDistance> it = costsAtPrevTime.costs.iterator(); it.hasNext();) {
DurationAndDistance prevCost = it.next();
if (prevCost.duration == duration && prevCost.distance == costs.distance) {
add = false;
duplicates.add(prevCost);
break;
} else if (prevCost.duration >= duration && prevCost.distance >= costs.distance * 1.05) {
// this walk totally dominates this old way
add = true;
it.remove();
} else if (prevCost.duration > duration
|| prevCost.distance > costs.distance * 1.05
&& (prevCost.duration != duration || prevCost.distance != costs.distance)) {
// this walk at least partially dominates the old way
add = true;
} else {
//the old way completely dominates this walk
add = false;
//costsIt.remove();
break;
}
}
if (add) {
shouldContinue = true;
toAdd.add(new DurationAndDistance(round, duration, costs.distance));
}
}
if (duplicates.size() == costsAtPrevTime.size() || costsAtPrevTime.isEmpty()) {
profile.remove(index);
} else {
for (DurationAndDistance newCosts : toAdd) {
costsAtPrevTime.forceAdd(newCosts);
}
}
if (shouldContinue && ! costsAtDeparture.isEmpty()) {
index--;
} else {
break;
}
}
}
private boolean addIfNotDominated(CostsAtTime destinationCosts, int timeAtOrigin,
int originIndex, int duration, double distance, int round) {
CostsAtTime toInsert = new CostsAtTime(timeAtOrigin);
for (DurationAndDistance atDestination : destinationCosts.costs) {
double walkDistance = distance + atDestination.distance;
if (walkDistance > 3218)
continue;
int walkDuration = duration + atDestination.duration;
toInsert.add(round, walkDuration, walkDistance);
}
for (Iterator<DurationAndDistance> costsIt = toInsert.costs.iterator(); costsIt.hasNext();) {
DurationAndDistance costs = costsIt.next();
int index = originIndex;
FORWARDSEARCH: while (index < profile.size()) {
CostsAtTime costsAtNextTime = profile.get(index);
int waitTime = costsAtNextTime.time - timeAtOrigin;
for (DurationAndDistance nextCosts : costsAtNextTime.costs) {
if (costs.distance * 1.05 >= nextCosts.distance) {
if (costs.duration - waitTime >= nextCosts.duration) {
costsIt.remove();
break FORWARDSEARCH;
}
}
}
++index;
}
}
if (toInsert.isEmpty()) {
return false;
}
CostsAtTime costsAtNextTime = profile.get(originIndex);
if (costsAtNextTime.time - timeAtOrigin == 0) {
boolean better = false;
for (DurationAndDistance costs : toInsert.costs) {
better |= costsAtNextTime.add(round, costs.duration, costs.distance);
}
return better;
} else {
profile.add(originIndex, toInsert);
return true;
}
}
public int getMaxDuration(int startTime, int endTime) {
int worstDuration = 0;
int lastTime = 0;
for (CostsAtTime costs : profile) {
// consider only arrivals within the one-day period
if (costs.time < startTime) {
continue;
}
//we want the best duration among codominant states
int bestDuration = Integer.MAX_VALUE;
for (DurationAndDistance departure : costs.costs) {
final int time = departure.duration + costs.time - lastTime;
if (time < bestDuration) {
bestDuration = time;
}
}
//but the worst duration over the course of the day
if (bestDuration > worstDuration) {
worstDuration = bestDuration;
}
lastTime = costs.time;
if (costs.time >= endTime) {
break;
}
}
return worstDuration;
}
}