package org.opentrafficsim.road.gtu.lane;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.media.j3d.Bounds;
import javax.vecmath.Point3d;
import org.djunits.unit.LengthUnit;
import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Length;
import org.djunits.value.vdouble.scalar.Speed;
import org.djunits.value.vdouble.scalar.Time;
import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
import org.opentrafficsim.core.geometry.OTSGeometryException;
import org.opentrafficsim.core.gtu.AbstractGTU;
import org.opentrafficsim.core.gtu.GTUDirectionality;
import org.opentrafficsim.core.gtu.GTUException;
import org.opentrafficsim.core.gtu.GTUType;
import org.opentrafficsim.core.gtu.RelativePosition;
import org.opentrafficsim.core.gtu.behavioralcharacteristics.BehavioralCharacteristics;
import org.opentrafficsim.core.gtu.behavioralcharacteristics.ParameterException;
import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
import org.opentrafficsim.core.network.LateralDirectionality;
import org.opentrafficsim.core.network.Link;
import org.opentrafficsim.core.network.NetworkException;
import org.opentrafficsim.core.network.Node;
import org.opentrafficsim.core.network.OTSNetwork;
import org.opentrafficsim.road.gtu.lane.perception.categories.DefaultSimplePerception;
import org.opentrafficsim.road.gtu.lane.tactical.LaneBasedTacticalPlanner;
import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlanner;
import org.opentrafficsim.road.gtu.strategical.route.LaneBasedStrategicalRoutePlanner;
import org.opentrafficsim.road.network.lane.CrossSectionElement;
import org.opentrafficsim.road.network.lane.CrossSectionLink;
import org.opentrafficsim.road.network.lane.DirectedLanePosition;
import org.opentrafficsim.road.network.lane.Lane;
import nl.tudelft.simulation.dsol.SimRuntimeException;
import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
import nl.tudelft.simulation.language.Throw;
import nl.tudelft.simulation.language.d3.BoundingBox;
import nl.tudelft.simulation.language.d3.DirectedPoint;
/**
* This class contains most of the code that is needed to run a lane based GTU.
* The starting point of a LaneBasedTU is that it can be in multiple lanes at the same time. This can be due to a lane
* change (lateral), or due to crossing a link (front of the GTU is on another Lane than rear of the GTU). If a Lane is shorter
* than the length of the GTU (e.g. when we do node expansion on a crossing, this is very well possible), a GTU could occupy
* dozens of Lanes at the same time.
*
* When calculating a headway, the GTU has to look in successive lanes. When Lanes (or underlying CrossSectionLinks) diverge,
* the headway algorithms have to look at multiple Lanes and return the minimum headway in each of the Lanes. When the Lanes (or
* underlying CrossSectionLinks) converge, "parallel" traffic is not taken into account in the headway calculation. Instead, gap
* acceptance algorithms or their equivalent should guide the merging behavior.
*
* To decide its movement, an AbstractLaneBasedGTU applies its car following algorithm and lane change algorithm to set the
* acceleration and any lane change operation to perform. It then schedules the triggers that will add it to subsequent lanes
* and remove it from current lanes as needed during the time step that is has committed to. Finally, it re-schedules its next
* movement evaluation with the simulator.
*
* Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License.
*
* @version $Revision: 1408 $, $LastChangedDate: 2015-09-24 15:17:25 +0200 (Thu, 24 Sep 2015) $, by $Author: pknoppers $,
* initial version Oct 22, 2014
* @author Alexander Verbraeck
* @author Peter Knoppers
*/
public abstract class AbstractLaneBasedGTU extends AbstractGTU implements LaneBasedGTU
{
/** */
private static final long serialVersionUID = 20140822L;
/**
* Fractional longitudinal positions of the reference point of the GTU on one or more links at the start of the current
* operational plan. Because the reference point of the GTU might not be on all the links the GTU is registered on, the
* fractional longitudinal positions can be more than one, or less than zero.
*/
private Map fractionalLinkPositions = new LinkedHashMap<>();
/**
* The lanes the GTU is registered on. Each lane has to have its link registered in the fractionalLinkPositions as well to
* keep consistency. Each link from the fractionalLinkPositions can have one or more Lanes on which the vehicle is
* registered. This is a list to improve reproducibility: The 'oldest' lanes on which the vehicle is registered are at the
* front of the list, the later ones more to the back.
*/
private final Map lanesCurrentOperationalPlan = new HashMap<>();
/** Pending triggers for each lane. */
private Map>> pendingTriggers = new HashMap<>();
/** The object to lock to make the GTU thread safe. */
private Object lock = new Object();
/** The threshold distance for differences between initial locations of the GTU on different lanes. */
@SuppressWarnings("checkstyle:visibilitymodifier")
public static Length initialLocationThresholdDifference = new Length(1.0, LengthUnit.MILLIMETER);
/** Caching on or off. */
// TODO: should be indicated with a Parameter
public static boolean CACHING = true;
/** cached position count. */
// TODO: can be removed after testing period
public static int CACHED_POSITION = 0;
/** cached position count. */
// TODO: can be removed after testing period
public static int NON_CACHED_POSITION = 0;
/**
* Construct a Lane Based GTU.
* @param id the id of the GTU
* @param gtuType the type of GTU, e.g. TruckType, CarType, BusType
* @param simulator to initialize the move method and to get the current time
* @param network the network that the GTU is initially registered in
* @throws GTUException when initial values are not correct
*/
public AbstractLaneBasedGTU(final String id, final GTUType gtuType, final OTSDEVSSimulatorInterface simulator,
final OTSNetwork network) throws GTUException
{
super(id, gtuType, simulator, network);
}
/**
* @param strategicalPlanner the strategical planner (e.g., route determination) to use
* @param initialLongitudinalPositions the initial positions of the car on one or more lanes with their directions
* @param initialSpeed the initial speed of the car on the lane
* @throws NetworkException when the GTU cannot be placed on the given lane
* @throws SimRuntimeException when the move method cannot be scheduled
* @throws GTUException when initial values are not correct
* @throws OTSGeometryException when the initial path is wrong
*/
@SuppressWarnings("checkstyle:designforextension")
public void init(final LaneBasedStrategicalPlanner strategicalPlanner,
final Set initialLongitudinalPositions, final Speed initialSpeed)
throws NetworkException, SimRuntimeException, GTUException, OTSGeometryException
{
Throw.when(null == initialLongitudinalPositions, GTUException.class, "InitialLongitudinalPositions is null");
Throw.when(0 == initialLongitudinalPositions.size(), GTUException.class, "InitialLongitudinalPositions is empty set");
DirectedPoint lastPoint = null;
for (DirectedLanePosition pos : initialLongitudinalPositions)
{
Throw.when(lastPoint != null && pos.getLocation().distance(lastPoint) > initialLocationThresholdDifference.si,
GTUException.class, "initial locations for GTU have distance > " + initialLocationThresholdDifference);
lastPoint = pos.getLocation();
}
DirectedPoint initialLocation = lastPoint;
// register the GTU on the lanes
for (DirectedLanePosition directedLanePosition : initialLongitudinalPositions)
{
Lane lane = directedLanePosition.getLane();
enterLane(lane, directedLanePosition.getPosition(), directedLanePosition.getGtuDirection());
}
// TODO The above enterLane creates an event with speed = 0.0, while the below init creates a move event with speed at
// the same time.
// TODO The above enterLane creates link/lane messages for GTU's that other software might not be aware of, as no
// new/init message it given at that point. Problem: init event needs a referenceLane which is determined if the GTU
// is on the network, i.e. after the enterLane.
// initiate the actual move
super.init(strategicalPlanner, initialLocation, initialSpeed);
Lane referenceLane = getReferencePosition().getLane();
fireTimedEvent(LaneBasedGTU.LANEBASED_INIT_EVENT,
new Object[] { getId(), initialLocation, getLength(), getWidth(), getBaseColor(), referenceLane,
position(referenceLane, getReference()), this.lanesCurrentOperationalPlan.get(referenceLane),
getGTUType() },
getSimulator().getSimulatorTime());
}
/**
* Hack method. TODO remove and solve better
* @return safe to change
* @throws GTUException on error
*/
public final boolean isSafeToChange() throws GTUException
{
return this.fractionalLinkPositions.get(getReferencePosition().getLane().getParentLink()) > 0.0;
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public void enterLane(final Lane lane, final Length position, final GTUDirectionality gtuDirection) throws GTUException
{
if (lane == null || gtuDirection == null || position == null)
{
throw new GTUException("enterLane - one of the arguments is null");
}
if (this.lanesCurrentOperationalPlan.containsKey(lane))
{
System.err.println(this + " is already registered on lane: " + lane + " at fractional position "
+ this.fractionalPosition(lane, RelativePosition.REFERENCE_POSITION) + " intended position is " + position
+ " length of lane is " + lane.getLength());
return;
}
// if the GTU is already registered on a lane of the same link, do not change its fractional position, as
// this might lead to a "jump".
if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
{
this.fractionalLinkPositions.put(lane.getParentLink(), lane.fraction(position));
}
this.lanesCurrentOperationalPlan.put(lane, gtuDirection);
lane.addGTU(this, position);
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public void leaveLane(final Lane lane) throws GTUException
{
leaveLane(lane, false);
}
/**
* Leave a lane but do not complain about having no lanes left when beingDestroyed is true.
* @param lane the lane to leave
* @param beingDestroyed if true, no complaints about having no lanes left
* @throws GTUException in case leaveLane should not be called
*/
@SuppressWarnings("checkstyle:designforextension")
public void leaveLane(final Lane lane, final boolean beingDestroyed) throws GTUException
{
Length position = position(lane, getReference());
this.lanesCurrentOperationalPlan.remove(lane);
List> pending = this.pendingTriggers.get(lane);
if (null != pending)
{
for (SimEvent event : pending)
{
if (event.getAbsoluteExecutionTime().get().ge(getSimulator().getSimulatorTime().get()))
{
boolean result = getSimulator().cancelEvent(event);
if (!result && event.getAbsoluteExecutionTime().get().ne(getSimulator().getSimulatorTime().get()))
{
System.err.println("leaveLane, trying to remove event: NOTHING REMOVED -- result=" + result
+ ", simTime=" + getSimulator().getSimulatorTime().get() + ", eventTime="
+ event.getAbsoluteExecutionTime().get());
}
}
}
this.pendingTriggers.remove(lane);
}
// check of there are any lanes for this link left. If not, remove the link.
boolean found = false;
for (Lane l : this.lanesCurrentOperationalPlan.keySet())
{
if (l.getParentLink().equals(lane.getParentLink()))
{
found = true;
}
}
if (!found)
{
this.fractionalLinkPositions.remove(lane.getParentLink());
}
lane.removeGTU(this, !found, position);
if (this.lanesCurrentOperationalPlan.size() == 0 && !beingDestroyed)
{
System.err.println("leaveLane: lanes.size() = 0 for GTU " + getId());
}
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public void changeLaneInstantaneously(final LateralDirectionality laneChangeDirection) throws GTUException
{
// keep a copy of the lanes and directions (!)
Map lanesCopy = new HashMap<>(this.lanesCurrentOperationalPlan);
Set lanesToBeRemoved = new HashSet<>();
Map fractionalLanePositions = new HashMap<>();
for (Lane lane : lanesCopy.keySet())
{
fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
lanesToBeRemoved.add(lane);
}
// store the new positions, and sample statistics
// start with current link position, these will be overwritten, except if from a lane no adjacent lane is found, i.e.
// changing over a continuous line when probably the reference point is past the line
Map newLinkPositionsLC = new HashMap<>(this.fractionalLinkPositions);
for (Lane lane : lanesCopy.keySet())
{
Set laneSet = lane.accessibleAdjacentLanes(laneChangeDirection, getGTUType());
if (laneSet.size() > 0)
{
Lane adjacentLane = laneSet.iterator().next();
try
{
Length pos = adjacentLane.position(lane.fraction(position(lane, getReference())));
Length adjustedStart =
pos.minus(getOperationalPlan().getTraveledDistance(getSimulator().getSimulatorTime().getTime()));
newLinkPositionsLC.put(lane.getParentLink(), adjustedStart.si / adjacentLane.getLength().si);
}
catch (OperationalPlanException exception)
{
exception.printStackTrace();
}
enterLane(adjacentLane, adjacentLane.getLength().multiplyBy(fractionalLanePositions.get(lane)),
lanesCopy.get(lane));
lanesToBeRemoved.remove(adjacentLane); // don't remove lanes we might end up
}
}
// update the positions on the lanes we are registered on
this.fractionalLinkPositions = newLinkPositionsLC;
for (Lane lane : lanesToBeRemoved)
{
leaveLane(lane);
}
}
/**
* Register on lanes in target lane.
* @param laneChangeDirection direction of lane change
* @throws GTUException exception
*/
@SuppressWarnings("checkstyle:designforextension")
public void initLaneChange(final LateralDirectionality laneChangeDirection) throws GTUException
{
Map lanesCopy = new HashMap<>(this.lanesCurrentOperationalPlan);
Map fractionalLanePositions = new HashMap<>();
for (Lane lane : lanesCopy.keySet())
{
fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
}
int numRegistered = 0;
for (Lane lane : lanesCopy.keySet())
{
Set laneSet = lane.accessibleAdjacentLanes(laneChangeDirection, getGTUType());
if (laneSet.size() > 0)
{
numRegistered++;
Lane adjacentLane = laneSet.iterator().next();
enterLane(adjacentLane, adjacentLane.getLength().multiplyBy(fractionalLanePositions.get(lane)),
lanesCopy.get(lane));
}
}
Throw.when(numRegistered == 0, GTUException.class, "Gtu %s starting %s lane change, but no adjacent lane found.",
getId(), laneChangeDirection);
}
/**
* Unregister on lanes in source lane.
* @param laneChangeDirection direction of lane change
* @param time time to leave lanes
* @throws GTUException exception
*/
@SuppressWarnings("checkstyle:designforextension")
public void finalizeLaneChange(final LateralDirectionality laneChangeDirection, final Time time) throws GTUException
{
try
{
getSimulator().scheduleEventAbs(time, this, this, "executeLaneChangeFinalization",
new Object[] { laneChangeDirection });
}
catch (SimRuntimeException exception)
{
throw new RuntimeException("Error when finalizing lane change.", exception);
}
}
/**
* Performs the finalization of a lane change by leaving the from lanes.
* @param laneChangeDirection direction of lane change
*/
@SuppressWarnings("checkstyle:designforextension")
protected void executeLaneChangeFinalization(final LateralDirectionality laneChangeDirection)
{
Map lanesCopy = new HashMap<>(this.lanesCurrentOperationalPlan);
Set lanesToBeRemoved = new HashSet<>();
Map fractionalLanePositions = new HashMap<>();
try
{
for (Lane lane : lanesCopy.keySet())
{
fractionalLanePositions.put(lane, fractionalPosition(lane, getReference()));
}
for (Lane lane : lanesCopy.keySet())
{
Iterator iterator = lane.accessibleAdjacentLanes(laneChangeDirection, getGTUType()).iterator();
if (iterator.hasNext() && lanesCopy.keySet().contains(iterator.next()))
{
lanesToBeRemoved.add(lane);
}
}
for (Lane lane : lanesToBeRemoved)
{
leaveLane(lane);
}
}
catch (GTUException exception)
{
// should not happen, lane was obtained from GTU
throw new RuntimeException("fractionalPosition on lane not possible", exception);
}
}
/** {@inheritDoc} */
@Override
public final GTUDirectionality getDirection(final Lane lane) throws GTUException
{
Throw.when(!this.lanesCurrentOperationalPlan.containsKey(lane), GTUException.class,
"getDirection: Lanes %s does not contain %s", this.lanesCurrentOperationalPlan.keySet(), lane);
return this.lanesCurrentOperationalPlan.get(lane);
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
protected void move(final DirectedPoint fromLocation)
throws SimRuntimeException, GTUException, OperationalPlanException, NetworkException, ParameterException
{
// Only carry out move() if we still have lane(s) to drive on.
// Note: a (Sink) trigger can have 'destroyed' us between the previous evaluation step and this one.
if (this.lanesCurrentOperationalPlan.isEmpty())
{
destroy();
return; // Done; do not re-schedule execution of this move method.
}
// store the new positions, and sample statistics
Map newLinkPositions = new HashMap<>();
for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
{
newLinkPositions.put(lane.getParentLink(), lane.fraction(position(lane, getReference())));
}
// generate the next operational plan and carry it out
super.move(fromLocation);
// update the positions on the lanes we are registered on
this.fractionalLinkPositions = newLinkPositions;
DirectedLanePosition dlp = getReferencePosition();
fireTimedEvent(
LaneBasedGTU.LANEBASED_MOVE_EVENT, new Object[] { getId(), fromLocation, getSpeed(), getAcceleration(),
getTurnIndicatorStatus(), getOdometer(), dlp.getLane(), dlp.getPosition(), dlp.getGtuDirection() },
getSimulator().getSimulatorTime());
if (getOperationalPlan().getAcceleration(Duration.ZERO).si < -10
&& getOperationalPlan().getSpeed(Duration.ZERO).si > 2.5)
{
System.err.println("GTU: " + getId() + " - getOperationalPlan().getAcceleration(Duration.ZERO).si < -10)");
System.err.println("Lanes in current plan: " + this.lanesCurrentOperationalPlan.keySet());
if (getTacticalPlanner().getPerception().contains(DefaultSimplePerception.class))
{
DefaultSimplePerception p =
getTacticalPlanner().getPerception().getPerceptionCategory(DefaultSimplePerception.class);
System.err.println("HeadwayGTU: " + p.getForwardHeadwayGTU());
System.err.println("HeadwayObject: " + p.getForwardHeadwayObject());
}
}
// schedule triggers and determine when to enter lanes with front and leave lanes with rear
scheduleEnterLeaveTriggers();
}
/** {@inheritDoc} */
@Override
public final Map positions(final RelativePosition relativePosition) throws GTUException
{
return positions(relativePosition, getSimulator().getSimulatorTime().getTime());
}
/** {@inheritDoc} */
@Override
public final Map positions(final RelativePosition relativePosition, final Time when) throws GTUException
{
Map positions = new LinkedHashMap<>();
for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
{
positions.put(lane, position(lane, relativePosition, when));
}
return positions;
}
/** {@inheritDoc} */
@Override
public final Length position(final Lane lane, final RelativePosition relativePosition) throws GTUException
{
return position(lane, relativePosition, getSimulator().getSimulatorTime().getTime());
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public Length translatedPosition(final Lane projectionLane, final RelativePosition relativePosition, final Time when)
throws GTUException
{
CrossSectionLink link = projectionLane.getParentLink();
for (CrossSectionElement cse : link.getCrossSectionElementList())
{
if (cse instanceof Lane)
{
Lane cseLane = (Lane) cse;
if (null != this.lanesCurrentOperationalPlan.get(cseLane))
{
double fractionalPosition = fractionalPosition(cseLane, RelativePosition.REFERENCE_POSITION, when);
Length pos = new Length(projectionLane.getLength().getSI() * fractionalPosition, LengthUnit.SI);
if (this.lanesCurrentOperationalPlan.get(cseLane).isPlus())
{
return pos.plus(relativePosition.getDx());
}
return pos.minus(relativePosition.getDx());
}
}
}
throw new GTUException(this + " is not on any lane of Link " + link);
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public Length projectedPosition(final Lane projectionLane, final RelativePosition relativePosition, final Time when)
throws GTUException
{
CrossSectionLink link = projectionLane.getParentLink();
for (CrossSectionElement cse : link.getCrossSectionElementList())
{
if (cse instanceof Lane)
{
Lane cseLane = (Lane) cse;
if (null != this.lanesCurrentOperationalPlan.get(cseLane))
{
double fractionalPosition = fractionalPosition(cseLane, relativePosition, when);
return new Length(projectionLane.getLength().getSI() * fractionalPosition, LengthUnit.SI);
}
}
}
throw new GTUException(this + " is not on any lane of Link " + link);
}
/** caching of time field for last stored position(s). */
private double cachePositionsTime = Double.NaN;
/** caching of last stored position(s). */
private Map cachedPositions = new HashMap<>();
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public Length position(final Lane lane, final RelativePosition relativePosition, final Time when) throws GTUException
{
if (null == lane)
{
throw new GTUException("lane is null");
}
int cacheIndex = 0;
if (CACHING)
{
cacheIndex = 17 * lane.hashCode() + relativePosition.hashCode();
if (when.si == this.cachePositionsTime && this.cachedPositions.containsKey(cacheIndex))
{
CACHED_POSITION++;
return this.cachedPositions.get(cacheIndex);
}
if (when.si != this.cachePositionsTime)
{
this.cachedPositions.clear();
this.cachePositionsTime = when.si;
}
}
NON_CACHED_POSITION++;
synchronized (this.lock)
{
if (!this.lanesCurrentOperationalPlan.containsKey(lane))
{
throw new GTUException("position() : GTU " + toString() + " is not on lane " + lane);
}
if (!this.fractionalLinkPositions.containsKey(lane.getParentLink()))
{
// DO NOT USE toString() here, as it will cause an endless loop...
throw new GTUException(this + " does not have a fractional position on " + lane.toString());
}
double longitudinalPosition = lane.positionSI(this.fractionalLinkPositions.get(lane.getParentLink()));
if (getOperationalPlan() == null)
{
// no valid operational plan, e.g. during generation of a new plan
return new Length(longitudinalPosition + relativePosition.getDx().si, LengthUnit.SI);
}
double loc;
try
{
if (this.lanesCurrentOperationalPlan.get(lane).isPlus())
{
loc = longitudinalPosition + getOperationalPlan().getTraveledDistanceSI(when) + relativePosition.getDx().si;
}
else
{
loc = longitudinalPosition - getOperationalPlan().getTraveledDistanceSI(when) - relativePosition.getDx().si;
}
}
catch (Exception e)
{
System.err.println(toString());
System.err.println(this.lanesCurrentOperationalPlan);
System.err.println(this.fractionalLinkPositions);
throw new GTUException(e);
}
if (Double.isNaN(loc))
{
System.out.println("loc is NaN");
}
Length length = new Length(loc, LengthUnit.SI);
if (CACHING)
{
this.cachedPositions.put(cacheIndex, length);
}
return length;
}
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public DirectedLanePosition getReferencePosition() throws GTUException
{
for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
{
double fraction = fractionalPosition(lane, getReference());
if (fraction >= 0.0 && fraction <= 1.0)
{
// TODO widest lane in case we are registered on more than one lane with the reference point
// TODO lane that leads to our location or not if we are registered on parallel lanes?
return new DirectedLanePosition(lane, position(lane, getReference()), this.getDirection(lane));
}
}
throw new GTUException("The reference point of GTU " + this + " is not on any of the lanes on which it is registered");
}
/**
* Schedule the triggers for this GTU that are going to happen until the next evaluation time. Also schedule the
* registration and deregistration of lanes when the vehicle enters or leaves them, at the exact right time.
* Note: when the GTU makes a lane change, the vehicle will be registered for both lanes during the entire maneuver.
* @throws NetworkException on network inconsistency
* @throws SimRuntimeException should never happen
* @throws GTUException when a branch is reached where the GTU does not know where to go next
*/
@SuppressWarnings("checkstyle:designforextension")
protected void scheduleEnterLeaveTriggers() throws NetworkException, SimRuntimeException, GTUException
{
/*
* Move the vehicle into any new lanes with the front, and schedule entrance during this time step and calculate the
* current position based on the fractional position, because THE POSITION METHOD DOES NOT WORK FOR THIS. IT CALCULATES
* THE POSITION BASED ON THE NEWLY CALCULATED ACCELERATION AND SPEED AND CAN THEREFORE MAKE AN ERROR.
*/
double moveSI = getOperationalPlan().getTotalLength().si;
Map lanesCopy = new LinkedHashMap<>(this.lanesCurrentOperationalPlan);
for (Lane lane : lanesCopy.keySet()) // use a copy because this.lanes can change
{
// schedule triggers on this lane
double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
double sign = lanesCopy.get(lane).equals(GTUDirectionality.DIR_PLUS) ? 1.0 : -1.0;
if (lanesCopy.get(lane).equals(GTUDirectionality.DIR_PLUS))
{
lane.scheduleSensorTriggers(this, referenceStartSI, moveSI);
}
else
{
// TODO extra argument for DIR_MINUS driving direction?
lane.scheduleSensorTriggers(this, referenceStartSI - moveSI, moveSI);
}
// determine when our FRONT will pass the end of this registered lane in case of driving DIR_PLUS or
// the start of this registered lane when driving DIR_MINUS.
// if the time is earlier than the end of the timestep: schedule the enterLane method.
double frontPosSI = referenceStartSI + sign * getFront().getDx().getSI();
double nextFrontPosSI = frontPosSI + sign * moveSI;
// LANE WE COME FROM IS IN PLUS DIRECTION
if (lanesCopy.get(lane).equals(GTUDirectionality.DIR_PLUS))
{
while (frontPosSI <= lane.getLength().si && nextFrontPosSI > lane.getLength().si)
{
// TODO PK: this goes very wrong for networks with short lanes with alternating GTUDireationality
Lane nextLane = determineNextLane(lane);
GTUDirectionality direction = lane.nextLanes(getGTUType()).get(nextLane);
/*
* We have to register the position at the previous timestep to keep calculations consistent. And we have to
* correct for the position of the reference point. The idea is that we register the vehicle 'before' the
* entrance of the new lane at the time of the last timestep, so for a DIR_PLUS on a negative position, and
* for a DIR_MINUS on a position beyond the length of the next lane.
*/
if (direction.equals(GTUDirectionality.DIR_PLUS))
{
Length refPosAtLastTimestep =
new Length(-(lane.getLength().si - frontPosSI) - getFront().getDx().si, LengthUnit.SI);
enterLane(nextLane, refPosAtLastTimestep, direction);
// schedule any sensor triggers on this lane for the remainder time
nextLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI(), moveSI);
}
else if (direction.equals(GTUDirectionality.DIR_MINUS))
{
Length refPosAtLastTimestep =
new Length(nextLane.getLength().si + (lane.getLength().si - frontPosSI) + getFront().getDx().si,
LengthUnit.SI);
enterLane(nextLane, refPosAtLastTimestep, direction);
// schedule any sensor triggers on this lane for the remainder time
// TODO extra argument for DIR_MINUS driving direction?
nextLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI() - moveSI, moveSI);
}
else
{
throw new NetworkException("scheduleTriggers DIR_PLUS for GTU " + toString() + ", nextLane " + nextLane
+ ", direction not DIR_PLUS or DIR_MINUS");
}
// TODO this does not account for the GTUDirectionality
// next lane
frontPosSI -= lane.getLength().si;
nextFrontPosSI -= lane.getLength().si;
lane = nextLane;
}
}
// LANE WE COME FROM IS IN MINUS DIRECTION
else if (lanesCopy.get(lane).equals(GTUDirectionality.DIR_MINUS))
{
if (frontPosSI >= 0.0 && nextFrontPosSI < 0.0)
{
Lane prevLane = determinePrevLane(lane);
GTUDirectionality direction = lane.prevLanes(getGTUType()).get(prevLane);
/*
* We have to register the position at the previous timestep to keep calculations consistent. And we have to
* correct for the position of the reference point. The idea is that we register the vehicle 'before' the
* entrance of the new lane at the time of the last timestep, so for a DIR_MINUS on a negative position, and
* for a DIR_PLUS on a position beyond the length of the next lane.
*/
if (direction.equals(GTUDirectionality.DIR_MINUS))
{
// TODO do this at the right time, scheduled with triggers. 4x
Length refPosAtLastTimestep =
new Length(prevLane.getLength().si + frontPosSI + getFront().getDx().si, LengthUnit.SI);
enterLane(prevLane, refPosAtLastTimestep, direction);
// schedule any sensor triggers on this lane for the remainder time
prevLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI() - moveSI, moveSI);
}
else if (direction.equals(GTUDirectionality.DIR_PLUS))
{
Length refPosAtLastTimestep = new Length(-frontPosSI - getFront().getDx().si, LengthUnit.SI);
enterLane(prevLane, refPosAtLastTimestep, direction);
// schedule any sensor triggers on this lane for the remainder time
// TODO extra argument for DIR_MINUS driving direction?
prevLane.scheduleSensorTriggers(this, refPosAtLastTimestep.getSI(), moveSI);
}
else
{
throw new NetworkException("scheduleTriggers DIR_MINUS for GTU " + toString() + ", prevLane " + prevLane
+ ", direction not DIR_PLUS or DIR_MINUS");
}
}
}
else
{
throw new NetworkException(
"scheduleTriggers for GTU " + toString() + ", lane " + lane + ", direction not DIR_PLUS or DIR_MINUS");
}
}
// move the vehicle out of any lanes with the BACK, and schedule exit during this time step
for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
{
// Determine when our REAR will pass the end of this registered lane.
// TODO look if more lanes are exited in one timestep, and continue the algorithm with the remainder of the time...
double referenceStartSI = this.fractionalLinkPositions.get(lane.getParentLink()) * lane.getLength().getSI();
double sign = this.lanesCurrentOperationalPlan.get(lane).equals(GTUDirectionality.DIR_PLUS) ? 1.0 : -1.0;
double rearPosSI = referenceStartSI + sign * getRear().getDx().getSI();
if (this.lanesCurrentOperationalPlan.get(lane).equals(GTUDirectionality.DIR_PLUS))
{
if (rearPosSI <= lane.getLength().si && rearPosSI + moveSI > lane.getLength().si)
{
double distanceToLeave = lane.getLength().si - rearPosSI;
Time exitTime = getOperationalPlan().timeAtDistance(new Length(distanceToLeave, LengthUnit.SI));
SimEvent event = new SimEvent<>(new OTSSimTimeDouble(exitTime), this, this, "leaveLane",
new Object[] { lane, new Boolean(false) });
getSimulator().scheduleEvent(event);
addTrigger(lane, event);
// XXXXXXXXXXXXXXXXXX Minus one ULP is not safe if you want to add the current time
// XXXXXXXXXXXXXXXXXX Should compute the time time at which the rear of the GTU exits the lane???
// getSimulator().scheduleEventRel(new Duration(timestep - Math.ulp(timestep), TimeUnit.SI), this, this,
// "leaveLane", new Object[] { lane, new Boolean(true) }); // TODO should be false?
}
}
else
{
if (rearPosSI >= 0.0 && rearPosSI - moveSI < 0.0)
{
double distanceToLeave = rearPosSI;
Time exitTime = getOperationalPlan().timeAtDistance(new Length(distanceToLeave, LengthUnit.SI));
SimEvent event = new SimEvent<>(new OTSSimTimeDouble(exitTime), this, this, "leaveLane",
new Object[] { lane, new Boolean(false) });
getSimulator().scheduleEvent(event);
addTrigger(lane, event);
// getSimulator().scheduleEventRel(new Duration(timestep - Math.ulp(timestep), TimeUnit.SI), this, this,
// "leaveLane", new Object[] { lane, new Boolean(true) }); // XXX: should be false?
}
}
}
}
/**
* XXX: direction dependent...
* @param lane the lane to find the successor for
* @return the next lane for this GTU
* @throws NetworkException when no next lane exists or the route branches into multiple next lanes
* @throws GTUException when no route could be found or the routeNavigator returns null
*/
@SuppressWarnings("checkstyle:designforextension")
protected Lane determineNextLane(final Lane lane) throws NetworkException, GTUException
{
Lane nextLane = null;
if (lane.nextLanes(getGTUType()).size() == 0)
{
throw new NetworkException(this + " - lane " + lane + " does not have a successor");
}
if (lane.nextLanes(getGTUType()).size() == 1)
{
nextLane = lane.nextLanes(getGTUType()).keySet().iterator().next();
}
else
{
if (!(getStrategicalPlanner() instanceof LaneBasedStrategicalRoutePlanner))
{
throw new GTUException(this + " reaches branch but has no route navigator ");
}
Node nextNode = ((LaneBasedStrategicalRoutePlanner) getStrategicalPlanner()).nextNode(lane.getParentLink(),
GTUDirectionality.DIR_PLUS, getGTUType());
if (null == nextNode)
{
throw new GTUException(this + " reaches branch and the route returns null as nextNodeToVisit");
}
int continuingLaneCount = 0;
for (Lane candidateLane : lane.nextLanes(getGTUType()).keySet())
{
if (null != this.lanesCurrentOperationalPlan.get(candidateLane))
{
continue; // Already on this lane
}
// XXX Hack - this should be done more considerate -- fails at loops...
if (nextNode == candidateLane.getParentLink().getEndNode()
|| nextNode == candidateLane.getParentLink().getStartNode())
{
nextLane = candidateLane;
continuingLaneCount++;
}
}
if (continuingLaneCount == 0)
{
throw new NetworkException(this + " reached branch and the route specifies a nextNodeToVisit (" + nextNode
+ ") that is not a next node " + "at this branch at (" + lane.getParentLink().getEndNode() + ")");
}
if (continuingLaneCount > 1)
{
throw new NetworkException(
this + " reached branch and the route specifies multiple lanes to continue on at this branch ("
+ lane.getParentLink().getEndNode() + "). This is not yet supported");
}
}
return nextLane;
}
/**
* XXX: direction dependent...
* @param lane the lane to find the predecessor for
* @return the next lane for this GTU
* @throws NetworkException when no next lane exists or the route branches into multiple next lanes
* @throws GTUException when no route could be found or the routeNavigator returns null
*/
@SuppressWarnings("checkstyle:designforextension")
protected Lane determinePrevLane(final Lane lane) throws NetworkException, GTUException
{
Lane prevLane = null;
if (lane.prevLanes(getGTUType()).size() == 0)
{
throw new NetworkException(this + " - lane " + lane + " does not have a predecessor");
}
if (lane.prevLanes(getGTUType()).size() == 1)
{
prevLane = lane.prevLanes(getGTUType()).keySet().iterator().next();
}
else
{
if (!(getStrategicalPlanner() instanceof LaneBasedStrategicalRoutePlanner))
{
throw new GTUException(this + " reaches branch but has no route navigator");
}
Node prevNode = ((LaneBasedStrategicalRoutePlanner) getStrategicalPlanner()).nextNode(lane.getParentLink(),
GTUDirectionality.DIR_MINUS, getGTUType());
if (null == prevNode)
{
throw new GTUException(this + " reaches branch and the route returns null as nextNodeToVisit");
}
int continuingLaneCount = 0;
for (Lane candidateLane : lane.prevLanes(getGTUType()).keySet())
{
if (null != this.lanesCurrentOperationalPlan.get(candidateLane))
{
continue; // Already on this lane
}
// XXX Hack - this should be done more considerate -- fails at loops...
if (prevNode == candidateLane.getParentLink().getEndNode()
|| prevNode == candidateLane.getParentLink().getStartNode())
{
prevLane = candidateLane;
continuingLaneCount++;
}
}
if (continuingLaneCount == 0)
{
throw new NetworkException(this + " reached branch and the route specifies a nextNodeToVisit (" + prevNode
+ ") that is not a next node " + "at this branch at (" + lane.getParentLink().getStartNode() + ")");
}
if (continuingLaneCount > 1)
{
throw new NetworkException(
this + " reached branch and the route specifies multiple lanes to continue on at this branch ("
+ lane.getParentLink().getStartNode() + "). This is not yet supported");
}
}
return prevLane;
}
/** {@inheritDoc} */
@Override
public final Map fractionalPositions(final RelativePosition relativePosition) throws GTUException
{
return fractionalPositions(relativePosition, getSimulator().getSimulatorTime().getTime());
}
/** {@inheritDoc} */
@Override
public final Map fractionalPositions(final RelativePosition relativePosition, final Time when)
throws GTUException
{
Map positions = new LinkedHashMap<>();
for (Lane lane : this.lanesCurrentOperationalPlan.keySet())
{
positions.put(lane, fractionalPosition(lane, relativePosition, when));
}
return positions;
}
/** {@inheritDoc} */
@Override
public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition, final Time when)
throws GTUException
{
return position(lane, relativePosition, when).getSI() / lane.getLength().getSI();
}
/** {@inheritDoc} */
@Override
public final double fractionalPosition(final Lane lane, final RelativePosition relativePosition) throws GTUException
{
return position(lane, relativePosition).getSI() / lane.getLength().getSI();
}
/** {@inheritDoc} */
@Override
public final LaneBasedStrategicalPlanner getStrategicalPlanner()
{
return (LaneBasedStrategicalPlanner) super.getStrategicalPlanner();
}
/** {@inheritDoc} */
@Override
public final BehavioralCharacteristics getBehavioralCharacteristics()
{
return getStrategicalPlanner().getBehavioralCharacteristics();
}
/** {@inheritDoc} */
@Override
public final LaneBasedTacticalPlanner getTacticalPlanner()
{
return (LaneBasedTacticalPlanner) super.getTacticalPlanner();
}
/** {@inheritDoc} */
@Override
public final void addTrigger(final Lane lane, final SimEvent event)
{
List> list = this.pendingTriggers.get(lane);
if (null == list)
{
list = new ArrayList<>();
}
list.add(event);
this.pendingTriggers.put(lane, list);
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public void destroy()
{
DirectedLanePosition dlp = null;
try
{
dlp = getReferencePosition();
}
catch (GTUException e)
{
// ignore. not important at destroy
}
DirectedPoint location = this.getOperationalPlan() == null ? new DirectedPoint(0.0, 0.0, 0.0) : getLocation();
synchronized (this.lock)
{
Set laneSet = new HashSet<>(this.lanesCurrentOperationalPlan.keySet()); // Operate on a copy of the key set
for (Lane lane : laneSet)
{
try
{
leaveLane(lane, true);
}
catch (GTUException e)
{
// ignore. not important at destroy
}
}
}
if (dlp != null && dlp.getLane() != null)
{
Lane referenceLane = dlp.getLane();
fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
new Object[] { getId(), location, getOdometer(), referenceLane, dlp.getPosition(), dlp.getGtuDirection() },
getSimulator().getSimulatorTime());
}
else
{
fireTimedEvent(LaneBasedGTU.LANEBASED_DESTROY_EVENT,
new Object[] { getId(), location, getOdometer(), null, Length.ZERO, null },
getSimulator().getSimulatorTime());
}
super.destroy();
}
/** {@inheritDoc} */
@Override
public final Bounds getBounds()
{
double dx = 0.5 * getLength().doubleValue();
double dy = 0.5 * getWidth().doubleValue();
return new BoundingBox(new Point3d(-dx, -dy, 0.0), new Point3d(dx, dy, 0.0));
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("checkstyle:designforextension")
public String toString()
{
return String.format("GTU " + getId());
}
}