rinde.sim.pdptw.common.RouteFollowingVehicle Maven / Gradle / Ivy
package rinde.sim.pdptw.common;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newLinkedList;
import static java.util.Collections.unmodifiableCollection;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.Collections;
import java.util.Queue;
import java.util.Set;
import javax.annotation.Nullable;
import javax.measure.Measure;
import javax.measure.quantity.Duration;
import javax.measure.quantity.Length;
import javax.measure.quantity.Velocity;
import javax.measure.unit.Unit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rinde.sim.core.TimeLapse;
import rinde.sim.core.graph.Point;
import rinde.sim.core.model.pdp.PDPModel;
import rinde.sim.core.model.pdp.PDPModel.ParcelState;
import rinde.sim.core.model.pdp.PDPModel.VehicleState;
import rinde.sim.core.model.pdp.Parcel;
import rinde.sim.core.model.road.RoadModel;
import rinde.sim.event.Event;
import rinde.sim.event.Listener;
import rinde.sim.pdptw.central.Solvers;
import rinde.sim.util.fsm.AbstractState;
import rinde.sim.util.fsm.StateMachine;
import rinde.sim.util.fsm.StateMachine.StateMachineEvent;
import rinde.sim.util.fsm.StateMachine.StateTransitionEvent;
import com.google.common.base.Optional;
import com.google.common.math.DoubleMath;
/**
* A simple vehicle implementation that follows a route comprised of
* {@link DefaultParcel}s. At every stop in the route, the corresponding parcel
* is serviced (either picked up or delivered). The route can be set via
* {@link #setRoute(Collection)}. The vehicle attempts route diversion when the
* underlying {@link PDPRoadModel} allows it, otherwise it will change its route
* at the next possible instant.
*
* This vehicle uses a strategy that postpones traveling towards a parcel such
* that any waiting time at the parcel's site is minimized.
*
* If it is the end of the day (as defined by {@link #isEndOfDay(TimeLapse)})
* and the route is empty, the vehicle will automatically return to the depot.
*
* Extension The behavior of this vehicle can be altered by modifying the
* state machine that is used internally. This can be done by overriding
* {@link #createStateMachine()}.
* @author Rinde van Lon
*/
public class RouteFollowingVehicle extends DefaultVehicle {
/**
* The logger of the vehicle.
*/
protected static final Logger LOGGER = LoggerFactory
.getLogger(RouteFollowingVehicle.class);
/**
* The state machine that defines the states and the allowed transitions
* between them.
*/
protected final StateMachine stateMachine;
/**
* The wait state: {@link Wait}.
*/
protected final Wait waitState;
/**
* The goto state: {@link Goto}.
*/
protected final Goto gotoState;
/**
* The wait at service state: {@link WaitAtService}.
*/
protected final WaitAtService waitForServiceState;
/**
* The service state: {@link Service}.
*/
protected final Service serviceState;
Queue route;
Optional extends Queue> newRoute;
Optional depot;
Optional currentTime;
boolean isDiversionAllowed;
private Optional> speed;
private final boolean allowDelayedRouteChanges;
/**
* Initializes the vehicle.
* @param pDto The {@link VehicleDTO} that defines this vehicle.
* @param allowDelayedRouteChanging This boolean changes the behavior of the
* {@link #setRoute(Collection)} method.
*/
public RouteFollowingVehicle(VehicleDTO pDto,
boolean allowDelayedRouteChanging) {
super(pDto);
depot = Optional.absent();
speed = Optional.absent();
route = newLinkedList();
newRoute = Optional.absent();
currentTime = Optional.absent();
allowDelayedRouteChanges = allowDelayedRouteChanging;
stateMachine = createStateMachine();
waitState = stateMachine.getStateOfType(Wait.class);
gotoState = stateMachine.getStateOfType(Goto.class);
waitForServiceState = stateMachine.getStateOfType(WaitAtService.class);
serviceState = stateMachine.getStateOfType(Service.class);
final String v = Integer.toHexString(hashCode());
stateMachine.getEventAPI().addListener(new Listener() {
@Override
public void handleEvent(Event e) {
@SuppressWarnings("unchecked")
final StateTransitionEvent event = (StateTransitionEvent) e;
LOGGER.trace("vehicle({}) - {} + {} -> {}", v, event.previousState,
event.event, event.newState);
}
}, StateMachineEvent.STATE_TRANSITION);
}
/**
* @return true
if this vehicle is allowed to divert.
*/
public boolean isDiversionAllowed() {
return isDiversionAllowed;
}
/**
* Change the route this vehicle is following. The route must adhere to the
* following requirements:
*
* - Parcels that have not yet been picked up can at maximum occur twice in
* the route.
* - Parcels that have been picked up can occur at maximum once in the
* route.
* - Parcels that are delivered may not occur in the route.
*
* These requirements are not checked defensively! It is the callers
* responsibility to make sure this is the case. Note that the underlying
* models normally should throw exceptions whenever a vehicle attempts
* to revisit an already delivered parcel.
*
* In some case the models do not allow this vehicle to change its route
* immediately. If this is the case the route is changed the next time this
* vehicle enters its {@link #waitState}. If
* allowDelayedRouteChanging
is set to false
any
* attempts to to this will result in an runtime exception, in this case the
* caller must ensure that a route is always changed immediately. The
* situations when the route is changed immediately are:
*
* - If the vehicle is waiting.
* - If diversion is allowed and the vehicle is not currently servicing.
* - If the current route is empty.
* - If the first destination in the new route equals the first destination
* of the current route.
*
* @param r The route to set. The elements are copied from the
* {@link Collection} using its iteration order.
*/
public void setRoute(Collection r) {
// note: the following checks can not detect if a parcel has been set to
// multiple vehicles at the same time
for (final DefaultParcel dp : r) {
final ParcelState state = pdpModel.get().getParcelState(dp);
checkArgument(
!state.isDelivered(),
"A parcel that is already delivered can not be part of a route. Parcel %s in route %s.",
dp, r);
if (state.isTransitionState()) {
if (state == ParcelState.PICKING_UP) {
checkArgument(
pdpModel.get().getVehicleState(this) == VehicleState.PICKING_UP,
"When a parcel in the route is in PICKING UP state the vehicle must also be in that state.");
} else {
checkArgument(
pdpModel.get().getVehicleState(this) == VehicleState.DELIVERING,
"When a parcel in the route is in DELIVERING state the vehicle must also be in that state.");
}
checkArgument(
pdpModel.get().getVehicleActionInfo(this).getParcel() == dp,
"A parcel in the route that is being serviced should be serviced by this truck. This truck is servicing %s.",
pdpModel.get().getVehicleActionInfo(this).getParcel());
}
final int frequency = Collections.frequency(r, dp);
if (state.isPickedUp()) {
checkArgument(pdpModel.get().getContents(this).contains(dp),
"A parcel that is in cargo state must be in cargo of this vehicle.");
checkArgument(
frequency <= 1,
"A parcel that is in cargo may not occur more than once in a route, found %s instance(s) of %s.",
frequency, dp, state);
} else {
checkArgument(
frequency <= 2,
"A parcel that is available may not occur more than twice in a route, found %s instance(s).",
frequency);
}
}
final boolean firstEqualsFirst = firstEqualsFirstInRoute(r);
final boolean divertable = isDiversionAllowed
&& !stateMachine.stateIs(serviceState);
if (stateMachine.stateIs(waitState) || route.isEmpty()
|| divertable || firstEqualsFirst) {
route = newLinkedList(r);
newRoute = Optional.absent();
} else {
checkArgument(
allowDelayedRouteChanges,
"Diversion is not allowed and delayed route changes are also not allowed, rejected route: %s.",
r);
newRoute = Optional.of(newLinkedList(r));
}
}
/**
* @return The route that is currently being followed.
*/
public Collection getRoute() {
return unmodifiableCollection(route);
}
/**
* Helper method for checking whether the first parcels in two routes are
* equal.
* @param r The route to compare with the current route in
* {@link RouteFollowingVehicle#getRoute()}.
* @return true
if the first item in r
equals the
* first item in {@link RouteFollowingVehicle#getRoute()}. If not
* equal or if either of the routes are empty false
is
* returned.
*/
protected final boolean firstEqualsFirstInRoute(Collection r) {
return !r.isEmpty() && !route.isEmpty()
&& r.iterator().next().equals(route.element());
}
@Override
public void initRoadPDP(RoadModel pRoadModel, PDPModel pPdpModel) {
super.initRoadPDP(pRoadModel, pPdpModel);
final Set depots = roadModel.get().getObjectsOfType(
DefaultDepot.class);
checkArgument(depots.size() == 1,
"This vehicle requires exactly 1 depot, found %s depots.",
depots.size());
checkArgument(roadModel.get() instanceof PDPRoadModel,
"This vehicle requires the PDPRoadModel.");
isDiversionAllowed = ((PDPRoadModel) roadModel.get())
.isVehicleDiversionAllowed();
depot = Optional.of(depots.iterator().next());
speed = Optional.of(Measure.valueOf(getSpeed(), roadModel.get()
.getSpeedUnit()));
}
/**
* This method can optionally be overridden to change route of this vehicle by
* calling {@link #setRoute(Collection)} from within this method.
* @param time The current time.
*/
protected void preTick(TimeLapse time) {}
@Override
protected final void tickImpl(TimeLapse time) {
currentTime = Optional.of(time);
preTick(time);
stateMachine.handle(this);
}
/**
* Check if leaving in the specified {@link TimeLapse} to the specified
* {@link Parcel} would mean a too early arrival time. When this method
* returns true
it is not necessary to leave already, when
* false
is returned the vehicle should leave as soon as
* possible.
*
* Calculates the latest time to leave (lttl) to be just in time at the parcel
* location. In case lttl is in this {@link TimeLapse} or has already passed,
* this method returns false
, returns true
* otherwise.
* @param p The parcel to travel to.
* @param time The current time.
* @return true
when leaving in this tick would mean arriving too
* early, false
otherwise.
*/
protected boolean isTooEarly(Parcel p, TimeLapse time) {
final ParcelState parcelState = pdpModel.get().getParcelState(p);
checkArgument(
!parcelState.isTransitionState() && !parcelState.isDelivered(),
"Parcel state may not be a transition state nor may it be delivered, it is %s.",
parcelState, parcelState.isTransitionState() ? pdpModel.get()
.getVehicleActionInfo(this).timeNeeded() : null);
final boolean isPickup = !parcelState.isPickedUp();
// if it is available, we know we can't be too early
if (isPickup && parcelState == ParcelState.AVAILABLE) {
return false;
}
final Point loc = isPickup ? ((DefaultParcel) p).dto.pickupLocation : p
.getDestination();
final long travelTime = computeTravelTimeTo(loc, time.getTimeUnit());
final long openingTime = isPickup ? p.getPickupTimeWindow().begin : p
.getDeliveryTimeWindow().begin;
final long latestTimeToLeave = openingTime - travelTime;
return latestTimeToLeave >= time.getEndTime();
}
/**
* Computes the travel time for this vehicle to any point.
* @param p The point to calculate travel time to.
* @param timeUnit The time unit used in the simulation.
* @return The travel time in the used time unit.
*/
protected long computeTravelTimeTo(Point p, Unit timeUnit) {
final Measure distance = Measure.valueOf(Point.distance(
roadModel.get().getPosition(this), p), roadModel.get()
.getDistanceUnit());
return DoubleMath.roundToLong(
Solvers.computeTravelTime(speed.get(), distance, timeUnit),
RoundingMode.CEILING);
}
/**
* @param time The time to use as 'now'.
* @return true
if it is the end of the day or if this vehicle
* has to leave before the end of this tick to arrive back at the
* depot right before the end of the day, false
* otherwise.
*/
protected boolean isEndOfDay(TimeLapse time) {
final long travelTime = computeTravelTimeTo(
roadModel.get().getPosition(depot.get()), time.getTimeUnit());
return time.getEndTime() - 1 >= dto.availabilityTimeWindow.end - travelTime;
}
/**
* @return the depot
*/
protected DefaultDepot getDepot() {
return depot.get();
}
/**
* @return the currentTime
*/
protected TimeLapse getCurrentTime() {
return currentTime.get();
}
/**
* Creates the {@link StateMachine} that is used in this vehicle. This method
* is (and should) called only once during the life time of a vehicle.
*
* ExtensionThis method can optionally be overridden to change the
* behavior of the vehicle. When overriding make sure that:
*
* - The resulting state machine contains at least four states of the
* following types: {@link Wait}, {@link Goto}, {@link WaitAtService} and
* {@link Service}. Subclasses are allowed, multiple instances of the same
* type may result in unexpected behavior.
* - This method does not have any side effects. It should not call any
* instance methods or set any global variables.
*
* @return A newly created {@link StateMachine} that controls this vehicle.
*/
protected StateMachine createStateMachine() {
final Wait wait = new Wait();
final Goto gotos = new Goto();
final WaitAtService waitAtService = new WaitAtService();
final Service service = new Service();
return StateMachine.create(wait)
.explicitRecursiveTransitions()
.addTransition(wait, DefaultEvent.GOTO, gotos)
.addTransition(gotos, DefaultEvent.NOGO, wait)
.addTransition(gotos, DefaultEvent.ARRIVED, waitAtService)
.addTransition(gotos, DefaultEvent.REROUTE, gotos)
.addTransition(waitAtService, DefaultEvent.REROUTE, gotos)
.addTransition(waitAtService, DefaultEvent.NOGO, wait)
.addTransition(waitAtService, DefaultEvent.READY_TO_SERVICE, service)
.addTransition(service, DefaultEvent.DONE, wait).build();
}
void checkCurrentParcelOwnership() {
checkState(
!pdpModel.get().getParcelState(route.peek()).isTransitionState(),
"Parcel is already being serviced by another vehicle. Parcel state: %s",
pdpModel.get().getParcelState(route.peek()));
}
/**
* Marker interface for events. When defining new events simply implement this
* interface.
* @author Rinde van Lon
*/
protected interface StateEvent {}
/**
* The default event types of the state machine.
* @author Rinde van Lon
*/
protected enum DefaultEvent implements StateEvent {
/**
* Indicates that waiting is over, the vehicle is going to a parcel.
*/
GOTO,
/**
* Indicates that the vehicle no longer has a destination.
*/
NOGO,
/**
* Indicates that the vehicle has arrived at a service location.
*/
ARRIVED,
/**
* Indicates that the vehicle is at a service location and that the vehicle
* and the parcel are both ready to start the servicing.
*/
READY_TO_SERVICE,
/**
* Indicates that the vehicle is going to a new destination. This event only
* occurs when the vehicle was previously waiting at a service point.
*/
REROUTE,
/**
* Indicates that servicing is finished.
*/
DONE;
}
/**
* Base state class, can be subclassed to define custom states.
* @author Rinde van Lon
*/
protected abstract class AbstractTruckState extends
AbstractState {
@Override
public String toString() {
return this.getClass().getSimpleName();
}
}
/**
* Implementation of waiting state, is also responsible for driving back to
* the depot.
* @author Rinde van Lon
*/
protected class Wait extends AbstractTruckState {
/**
* New instance.
*/
protected Wait() {}
@Override
public void onEntry(StateEvent event, RouteFollowingVehicle context) {
checkState(
pdpModel.get().getVehicleState(context) == VehicleState.IDLE,
"We can only be in Wait state when the vehicle is idle, vehicle is %s.",
pdpModel.get().getVehicleState(context));
if (event == DefaultEvent.NOGO) {
checkArgument(isDiversionAllowed);
}
if (context.newRoute.isPresent()) {
context.setRoute(context.newRoute.get());
}
}
@Nullable
@Override
public DefaultEvent handle(@Nullable StateEvent event,
RouteFollowingVehicle context) {
if (!route.isEmpty()) {
checkCurrentParcelOwnership();
if (!isTooEarly(route.peek(), currentTime.get())) {
return DefaultEvent.GOTO;
}
// else it is too early, and we do nothing
}
// check if it is time to go back to the depot
else if (currentTime.get().hasTimeLeft() && isEndOfDay(currentTime.get())
&& !roadModel.get().equalPosition(context, depot.get())) {
roadModel.get().moveTo(context, depot.get(), currentTime.get());
}
currentTime.get().consumeAll();
return null;
}
}
/**
* State responsible for moving to a service location.
* @author Rinde van Lon
*/
protected class Goto extends AbstractTruckState {
/**
* Field for storing the destination.
*/
protected Optional destination;
/**
* Field for storing the previous destination.
*/
protected Optional prevDestination;
/**
* New instance.
*/
protected Goto() {
destination = Optional.absent();
prevDestination = Optional.absent();
}
@Override
public void onEntry(StateEvent event, RouteFollowingVehicle context) {
if (event == DefaultEvent.REROUTE) {
checkArgument(isDiversionAllowed);
}
checkCurrentParcelOwnership();
destination = Optional.of(route.element());
}
@Nullable
@Override
public DefaultEvent handle(@Nullable StateEvent event,
RouteFollowingVehicle context) {
if (route.isEmpty()) {
return DefaultEvent.NOGO;
} else if (destination.get() != route.element()) {
return DefaultEvent.REROUTE;
}
final DefaultParcel cur = route.element();
if (roadModel.get().equalPosition(context, cur)) {
return DefaultEvent.ARRIVED;
}
roadModel.get().moveTo(context, cur, currentTime.get());
if (roadModel.get().equalPosition(context, cur)
&& currentTime.get().hasTimeLeft()) {
return DefaultEvent.ARRIVED;
}
return null;
}
@Override
public void onExit(StateEvent event, RouteFollowingVehicle context) {
prevDestination = destination;
destination = Optional.absent();
}
/**
* @return The destination of the vehicle.
* @throws IllegalStateException if there is no destination.
*/
public DefaultParcel getDestination() {
return destination.get();
}
/**
* @return The previous destination of the vehicle.
* @throws IllegalStateException if there is no previous destination.
*/
public DefaultParcel getPreviousDestination() {
return prevDestination.get();
}
}
/**
* State responsible for waiting at a service location to become available.
* @author Rinde van Lon
*/
protected class WaitAtService extends AbstractTruckState {
/**
* New instance.
*/
protected WaitAtService() {}
@Nullable
@Override
public DefaultEvent handle(@Nullable StateEvent event,
RouteFollowingVehicle context) {
// the route has changed (there is no destination anymore)
if (route.isEmpty()) {
return DefaultEvent.NOGO;
}
checkCurrentParcelOwnership();
final PDPModel pm = pdpModel.get();
final TimeLapse time = currentTime.get();
final DefaultParcel cur = route.element();
// we are not at the parcel's position, this means the next parcel has
// changed in the mean time, so we have to reroute.
if (!roadModel.get().equalPosition(context, cur)) {
return DefaultEvent.REROUTE;
}
// if parcel is not ready yet, wait
final boolean pickup = !pm.getContents(context).contains(cur);
final long timeUntilReady = (pickup ? cur.dto.pickupTimeWindow.begin
: cur.dto.deliveryTimeWindow.begin) - time.getTime();
if (timeUntilReady > 0) {
if (time.getTimeLeft() < timeUntilReady) {
// in this case we can not yet start servicing
time.consumeAll();
return null;
} else {
time.consume(timeUntilReady);
}
}
if (time.hasTimeLeft()) {
return DefaultEvent.READY_TO_SERVICE;
} else {
return null;
}
}
}
/**
* State responsible for servicing a parcel.
* @author Rinde van Lon
*/
protected class Service extends AbstractTruckState {
/**
* New instance.
*/
protected Service() {}
@Override
public void onEntry(StateEvent event, RouteFollowingVehicle context) {
pdpModel.get().service(context, route.peek(), currentTime.get());
}
@Nullable
@Override
public DefaultEvent handle(@Nullable StateEvent event,
RouteFollowingVehicle context) {
if (pdpModel.get().getVehicleState(context) == VehicleState.IDLE) {
route.remove();
return DefaultEvent.DONE;
}
return null;
}
}
}