View Javadoc
1   package org.opentrafficsim.core.gtu;
2   
3   import java.util.ArrayList;
4   import java.util.LinkedHashMap;
5   import java.util.LinkedHashSet;
6   import java.util.List;
7   import java.util.Map;
8   import java.util.Objects;
9   import java.util.Optional;
10  import java.util.Set;
11  
12  import org.djunits.unit.DirectionUnit;
13  import org.djunits.unit.DurationUnit;
14  import org.djunits.unit.PositionUnit;
15  import org.djunits.value.vdouble.scalar.Acceleration;
16  import org.djunits.value.vdouble.scalar.Direction;
17  import org.djunits.value.vdouble.scalar.Duration;
18  import org.djunits.value.vdouble.scalar.Length;
19  import org.djunits.value.vdouble.scalar.Speed;
20  import org.djunits.value.vdouble.vector.PositionVector;
21  import org.djutils.base.Identifiable;
22  import org.djutils.draw.bounds.Bounds2d;
23  import org.djutils.draw.line.Polygon2d;
24  import org.djutils.draw.point.DirectedPoint2d;
25  import org.djutils.draw.point.Point2d;
26  import org.djutils.event.EventType;
27  import org.djutils.event.LocalEventProducer;
28  import org.djutils.exceptions.Throw;
29  import org.djutils.exceptions.Try;
30  import org.djutils.immutablecollections.Immutable;
31  import org.djutils.immutablecollections.ImmutableLinkedHashMap;
32  import org.djutils.immutablecollections.ImmutableMap;
33  import org.djutils.metadata.MetaData;
34  import org.djutils.metadata.ObjectDescriptor;
35  import org.opentrafficsim.base.HierarchicallyTyped;
36  import org.opentrafficsim.base.OtsRuntimeException;
37  import org.opentrafficsim.base.geometry.OffsetRectangleShape;
38  import org.opentrafficsim.base.geometry.OtsLine2d;
39  import org.opentrafficsim.base.geometry.OtsShape;
40  import org.opentrafficsim.base.geometry.PolygonShape;
41  import org.opentrafficsim.base.logger.Logger;
42  import org.opentrafficsim.base.parameters.ParameterException;
43  import org.opentrafficsim.base.parameters.Parameters;
44  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
45  import org.opentrafficsim.core.gtu.RelativePosition.Type;
46  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
47  import org.opentrafficsim.core.gtu.plan.operational.OperationalPlanException;
48  import org.opentrafficsim.core.gtu.plan.strategical.StrategicalPlanner;
49  import org.opentrafficsim.core.gtu.plan.tactical.TacticalPlanner;
50  import org.opentrafficsim.core.network.NetworkException;
51  import org.opentrafficsim.core.perception.Historical;
52  import org.opentrafficsim.core.perception.HistoricalValue;
53  import org.opentrafficsim.core.perception.HistoryManager;
54  import org.opentrafficsim.core.perception.PerceivableContext;
55  
56  import nl.tudelft.simulation.dsol.SimRuntimeException;
57  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
58  
59  /**
60   * Implements the basic functionalities of any GTU: the ability to move on 3D-space according to a plan.
61   * <p>
62   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
63   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
64   * </p>
65   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
66   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
67   */
68  public class Gtu extends LocalEventProducer implements HierarchicallyTyped<GtuType, Gtu>, OtsShape, Identifiable
69  {
70      /** The id of the GTU. */
71      private final String id;
72  
73      /** unique number of the GTU. */
74      private final int uniqueNumber;
75  
76      /** the unique number counter. */
77      private static int staticUNIQUENUMBER = 0;
78  
79      /** The type of GTU, e.g. TruckType, CarType, BusType. */
80      private final GtuType gtuType;
81  
82      /** The simulator to schedule activities on. */
83      private final OtsSimulatorInterface simulator;
84  
85      /** Model parameters. */
86      private Parameters parameters;
87  
88      /** The maximum acceleration. */
89      private Acceleration maximumAcceleration;
90  
91      /** The maximum deceleration, stored as a negative number. */
92      private Acceleration maximumDeceleration;
93  
94      /**
95       * The odometer which measures how much distance have we covered between instantiation and the last completed operational
96       * plan. In order to get a complete odometer reading, the progress of the current plan execution has to be added to this
97       * value.
98       */
99      private Historical<Length> odometer;
100 
101     /** The strategical planner that can instantiate tactical planners to determine mid-term decisions. */
102     private final Historical<StrategicalPlanner> strategicalPlanner;
103 
104     /** The tactical planner that can generate an operational plan. */
105     private final Historical<TacticalPlanner<?, ?>> tacticalPlanner;
106 
107     /** The current operational plan, which provides a short-term movement over time. */
108     private final Historical<OperationalPlan> operationalPlan;
109 
110     /** The next move event as scheduled on the simulator, can be used for interrupting the current move. */
111     private SimEventInterface<Duration> nextMoveEvent;
112 
113     /** The model in which this GTU is registered. */
114     private PerceivableContext perceivableContext;
115 
116     /** Is this GTU destroyed? */
117     private boolean destroyed = false;
118 
119     /** Align step. */
120     private double alignStep = Double.NaN;
121 
122     /** Cache location time. */
123     private Duration cacheLocationTime = Duration.NaN;
124 
125     /** Cached location at that time. */
126     private DirectedPoint2d cacheLocation = null;
127 
128     /** Cached speed time. */
129     private double cachedSpeedTime = Double.NaN;
130 
131     /** Cached speed. */
132     private Speed cachedSpeed = null;
133 
134     /** Cached acceleration time. */
135     private double cachedAccelerationTime = Double.NaN;
136 
137     /** Cached acceleration. */
138     private Acceleration cachedAcceleration = null;
139 
140     /** Parent GTU. */
141     private Gtu parent = null;
142 
143     /** Children GTU's. */
144     private Set<Gtu> children = new LinkedHashSet<>();
145 
146     /** Error handler. */
147     private GtuErrorHandler errorHandler = GtuErrorHandler.THROW;
148 
149     /** Shape. */
150     private final OtsShape shape;
151 
152     /** Relative positions to the reference point of type RelativePosition.REFERENCE. */
153     private final Map<RelativePosition.Type, RelativePosition> relativePositions = new LinkedHashMap<>();
154 
155     /** The maximum length of the GTU (parallel with driving direction). */
156     private final Length length;
157 
158     /** The maximum width of the GTU (perpendicular to driving direction). */
159     private final Length width;
160 
161     /** The maximum speed of the GTU (in the driving direction). */
162     private final Speed maximumSpeed;
163 
164     /** Tags of the GTU, these are used for specific use cases of any sort. */
165     private final Map<String, String> tags = new LinkedHashMap<>();
166 
167     /**
168      * Constructor using shape.
169      * @param id the id of the GTU
170      * @param gtuType the type of GTU, e.g. TruckType, CarType, BusType
171      * @param simulator the simulator to schedule plan changes on
172      * @param perceivableContext the perceivable context in which this GTU will be registered
173      * @param length the maximum length of the GTU (parallel with driving direction)
174      * @param width the maximum width of the GTU (perpendicular to driving direction)
175      * @param front front distance relative to the reference position
176      * @param contour contour relative to reference position, may be {@code null}
177      * @param maximumSpeed the maximum speed of the GTU (in the driving direction)
178      * @throws GtuException when id already exists in the context
179      * @throws NullPointerException when any input is null
180      */
181     @SuppressWarnings("checkstyle:parameternumber")
182     private Gtu(final String id, final GtuType gtuType, final OtsSimulatorInterface simulator,
183             final PerceivableContext perceivableContext, final Length length, final Length width, final Length front,
184             final Polygon2d contour, final Speed maximumSpeed) throws GtuException
185     {
186         Throw.whenNull(id, "id");
187         Throw.whenNull(gtuType, "gtuType");
188         Throw.whenNull(simulator, "simulator");
189         Throw.whenNull(perceivableContext, "perceivableContext");
190         Throw.when(perceivableContext.containsGtuId(id), GtuException.class,
191                 "GTU with id %s already registered in perceivableContext %s", id, perceivableContext.getId());
192         Throw.whenNull(maximumSpeed, "maximumSpeed");
193         this.maximumSpeed = maximumSpeed;
194 
195         HistoryManager historyManager = simulator.getReplication().getHistoryManager(simulator);
196         this.id = id;
197         this.uniqueNumber = ++staticUNIQUENUMBER;
198         this.gtuType = gtuType;
199         this.simulator = simulator;
200         this.odometer = new HistoricalValue<>(historyManager, this, Length.ZERO);
201         this.perceivableContext = perceivableContext;
202         this.perceivableContext.addGTU(this);
203         this.strategicalPlanner = new HistoricalValue<>(historyManager, this);
204         this.tacticalPlanner = new HistoricalValue<>(historyManager, this, null);
205         this.operationalPlan = new HistoricalValue<>(historyManager, this, null);
206 
207         this.length = length;
208         this.width = width;
209         if (contour == null)
210         {
211             this.shape =
212                     new OffsetRectangleShape(front.si - this.length.si, front.si, -this.width.si / 2.0, this.width.si / 2.0)
213                     {
214                         @Override
215                         public DirectedPoint2d getLocation()
216                         {
217                             return Gtu.this.getLocation();
218                         }
219                     };
220         }
221         else
222         {
223             this.shape = new PolygonShape(contour)
224             {
225                 @Override
226                 public DirectedPoint2d getLocation()
227                 {
228                     return Gtu.this.getLocation();
229                 }
230             };
231         }
232 
233         this.relativePositions.put(RelativePosition.REFERENCE, RelativePosition.REFERENCE_POSITION);
234         this.relativePositions.put(RelativePosition.FRONT,
235                 new RelativePosition(front, Length.ZERO, Length.ZERO, RelativePosition.FRONT));
236         this.relativePositions.put(RelativePosition.REAR,
237                 new RelativePosition(front.minus(this.length), Length.ZERO, Length.ZERO, RelativePosition.REAR));
238         Point2d midPoint = this.shape.getRelativeBounds().midPoint();
239         this.relativePositions.put(RelativePosition.CENTER,
240                 new RelativePosition(Length.ofSI(midPoint.x), Length.ofSI(midPoint.y), Length.ZERO, RelativePosition.CENTER));
241     }
242 
243     /**
244      * Constructor using contour.
245      * @param id the id of the GTU
246      * @param gtuType the type of GTU, e.g. TruckType, CarType, BusType
247      * @param simulator the simulator to schedule plan changes on
248      * @param perceivableContext the perceivable context in which this GTU will be registered
249      * @param contour contour relative to reference position
250      * @param maximumSpeed the maximum speed of the GTU (in the driving direction)
251      * @throws GtuException when id already exists in the context
252      * @throws NullPointerException when any input is null
253      */
254     public Gtu(final String id, final GtuType gtuType, final OtsSimulatorInterface simulator,
255             final PerceivableContext perceivableContext, final Polygon2d contour, final Speed maximumSpeed) throws GtuException
256     {
257         this(id, gtuType, simulator, perceivableContext, Length.ofSI(contour.getAbsoluteBounds().getDeltaX()),
258                 Length.ofSI(contour.getAbsoluteBounds().getDeltaY()), Length.ofSI(contour.getAbsoluteBounds().getMaxX()),
259                 contour, maximumSpeed);
260     }
261 
262     /**
263      * Constructor using length, width and front.
264      * @param id the id of the GTU
265      * @param gtuType the type of GTU, e.g. NL.CAR or NL.TRUCK
266      * @param simulator the simulator to schedule plan changes on
267      * @param perceivableContext the perceivable context in which this GTU will be registered
268      * @param length the maximum length of the GTU (parallel with driving direction)
269      * @param width the maximum width of the GTU (perpendicular to driving direction)
270      * @param front front distance relative to the reference position
271      * @param maximumSpeed the maximum speed of the GTU (in the driving direction)
272      * @throws GtuException when id already exists in the context
273      * @throws NullPointerException when any input is null
274      */
275     @SuppressWarnings("checkstyle:parameternumber")
276     public Gtu(final String id, final GtuType gtuType, final OtsSimulatorInterface simulator,
277             final PerceivableContext perceivableContext, final Length length, final Length width, final Length front,
278             final Speed maximumSpeed) throws GtuException
279     {
280         this(id, gtuType, simulator, perceivableContext, length, width, front, null, maximumSpeed);
281     }
282 
283     /**
284      * Initialize the GTU at a location and speed, and give it a mission to fulfill through the strategical planner.
285      * @param strategicalPlanner the strategical planner responsible for the overall 'mission' of the GTU, usually indicating
286      *            where it needs to go. It operates by instantiating tactical planners to do the work.
287      * @param initialLocation the initial location (and direction) of the GTU
288      * @param initialSpeed the initial speed of the GTU
289      * @throws SimRuntimeException when scheduling after the first move fails
290      * @throws GtuException when the preconditions of the parameters are not met or when the construction of the original
291      *             waiting path fails
292      */
293     @SuppressWarnings({"checkstyle:hiddenfield", "checkstyle:designforextension"})
294     public void init(final StrategicalPlanner strategicalPlanner, final DirectedPoint2d initialLocation,
295             final Speed initialSpeed) throws SimRuntimeException, GtuException
296     {
297         Throw.whenNull(strategicalPlanner, "strategicalPlanner");
298         Throw.whenNull(initialLocation, "Initial location of GTU cannot be null");
299         Throw.when(Double.isNaN(initialLocation.x) || Double.isNaN(initialLocation.y), GtuException.class,
300                 "initialLocation %s invalid for GTU with id %s", initialLocation, this.id);
301         Throw.whenNull(initialSpeed, "initialSpeed");
302         Throw.when(!getId().equals(strategicalPlanner.getGtu().getId()), GtuException.class,
303                 "GTU %s is initialized with a strategical planner for GTU %s", getId(), strategicalPlanner.getGtu().getId());
304 
305         this.strategicalPlanner.set(strategicalPlanner);
306         this.tacticalPlanner.set(strategicalPlanner.getTacticalPlanner());
307 
308         try
309         {
310             move(initialLocation);
311         }
312         catch (OperationalPlanException | NetworkException | ParameterException exception)
313         {
314             throw new GtuException("Failed to create OperationalPlan for GTU " + this.id, exception);
315         }
316     }
317 
318     /**
319      * Get front.
320      * @return the front position of the GTU, relative to its reference point.
321      */
322     public final RelativePosition getFront()
323     {
324         return this.relativePositions.get(RelativePosition.FRONT);
325     }
326 
327     /**
328      * Get rear.
329      * @return the rear position of the GTU, relative to its reference point.
330      */
331     public final RelativePosition getRear()
332     {
333         return this.relativePositions.get(RelativePosition.REAR);
334     }
335 
336     /**
337      * Get center.
338      * @return the center position of the GTU, relative to its reference point.
339      */
340     public final RelativePosition getCenter()
341     {
342         return this.relativePositions.get(RelativePosition.CENTER);
343     }
344 
345     /**
346      * Get relative positions.
347      * @return the positions for this GTU, but not the contour points.
348      */
349     public final ImmutableMap<Type, RelativePosition> getRelativePositions()
350     {
351         return new ImmutableLinkedHashMap<>(this.relativePositions, Immutable.WRAP);
352     }
353 
354     /**
355      * Get length.
356      * @return the maximum length of the GTU (parallel with driving direction).
357      */
358     public final Length getLength()
359     {
360         return this.length;
361     }
362 
363     /**
364      * Get width.
365      * @return the maximum width of the GTU (perpendicular to driving direction).
366      */
367     public final Length getWidth()
368     {
369         return this.width;
370     }
371 
372     /**
373      * Get maximum speed.
374      * @return the maximum speed of the GTU, in the direction of movement.
375      */
376     public final Speed getMaximumSpeed()
377     {
378         return this.maximumSpeed;
379     }
380 
381     @Override
382     public final Bounds2d getRelativeBounds()
383     {
384         return this.shape.getRelativeBounds();
385     }
386 
387     /**
388      * Destructor. Don't forget to call with super.destroy() from any override to avoid memory leaks in the network.
389      */
390     @SuppressWarnings("checkstyle:designforextension")
391     public void destroy()
392     {
393         DirectedPoint2d location = getLocation();
394         fireTimedEvent(Gtu.DESTROY_EVENT,
395                 new Object[] {getId(), new PositionVector(new double[] {location.x, location.y}, PositionUnit.METER),
396                         new Direction(location.getDirZ(), DirectionUnit.EAST_RADIAN), getOdometer()},
397                 this.simulator.getSimulatorTime());
398 
399         // cancel the next move
400         if (this.nextMoveEvent != null)
401         {
402             this.simulator.cancelEvent(this.nextMoveEvent);
403             this.nextMoveEvent = null;
404         }
405 
406         this.perceivableContext.removeGTU(this);
407         this.destroyed = true;
408     }
409 
410     /**
411      * Move from the current location according to an operational plan to a location that will bring us nearer to reaching the
412      * location provided by the strategical planner. <br>
413      * This method can be overridden to carry out specific behavior during the execution of the plan (e.g., scheduling of
414      * triggers, entering or leaving lanes, etc.). Please bear in mind that the call to super.move() is essential, and that one
415      * has to take care to handle the situation that the plan gets interrupted.
416      * @param fromLocation the last known location (initial location, or end location of the previous operational plan)
417      * @return whether an exception occurred
418      * @throws SimRuntimeException when scheduling of the next move fails
419      * @throws GtuException when there is a problem with the state of the GTU when planning a path
420      * @throws NetworkException in case of a problem with the network, e.g., a dead end where it is not expected
421      * @throws ParameterException in there is a parameter problem
422      */
423     @SuppressWarnings("checkstyle:designforextension")
424     protected boolean move(final DirectedPoint2d fromLocation)
425             throws SimRuntimeException, GtuException, NetworkException, ParameterException
426     {
427         try
428         {
429             Duration now = this.simulator.getSimulatorTime();
430 
431             // Add the odometer distance from the currently running operational plan.
432             // Because a plan can be interrupted, we explicitly calculate the covered distance till 'now'
433             Length currentOdometer;
434             if (this.operationalPlan.get() != null)
435             {
436                 currentOdometer = this.odometer.get().plus(this.operationalPlan.get().getTraveledDistance(now));
437             }
438             else
439             {
440                 currentOdometer = this.odometer.get();
441             }
442 
443             // Do we have an operational plan?
444             TacticalPlanner<?, ?> tactPlanner = this.tacticalPlanner.get();
445             if (tactPlanner == null)
446             {
447                 // Tell the strategical planner to provide a tactical planner
448                 tactPlanner = this.strategicalPlanner.get().getTacticalPlanner();
449                 this.tacticalPlanner.set(tactPlanner);
450             }
451             synchronized (this)
452             {
453                 tactPlanner.getPerception().perceive();
454             }
455             OperationalPlan newOperationalPlan = tactPlanner.generateOperationalPlan(now, fromLocation);
456             synchronized (this)
457             {
458                 this.operationalPlan.set(newOperationalPlan);
459                 this.cachedSpeedTime = Double.NaN;
460                 this.cachedAccelerationTime = Double.NaN;
461                 this.odometer.set(currentOdometer);
462             }
463 
464             if (!Double.isNaN(this.alignStep))
465             {
466                 // store the event, so it can be cancelled in case the plan has to be interrupted and changed halfway
467                 double tNext = Math.floor(now.si / this.alignStep + 1.0) * this.alignStep;
468                 DirectedPoint2d p = (tNext - now.si < this.alignStep) ? newOperationalPlan.getEndLocation()
469                         : newOperationalPlan.getLocationFromStart(new Duration(tNext - now.si, DurationUnit.SI));
470                 this.nextMoveEvent = this.simulator.scheduleEventRel(Duration.ofSI(tNext),
471                         () -> Try.execute(() -> move(p), "ParameterException in move"));
472             }
473             else
474             {
475                 // schedule the next move at the end of the current operational plan
476                 // store the event, so it can be cancelled in case the plan has to be interrupted and changed halfway
477                 this.nextMoveEvent = this.simulator.scheduleEventRel(newOperationalPlan.getTotalDuration(),
478                         () -> Try.execute(() -> move(newOperationalPlan.getEndLocation()), "ParameterException in move"));
479             }
480 
481             fireTimedEvent(Gtu.MOVE_EVENT,
482                     new Object[] {getId(),
483                             new PositionVector(new double[] {fromLocation.x, fromLocation.y}, PositionUnit.METER),
484                             new Direction(fromLocation.getDirZ(), DirectionUnit.EAST_RADIAN), getSpeed(), getAcceleration(),
485                             getOdometer()},
486                     this.simulator.getSimulatorTime());
487 
488             return false;
489         }
490         catch (Exception ex)
491         {
492             try
493             {
494                 this.errorHandler.handle(this, ex);
495             }
496             catch (Exception exception)
497             {
498                 throw new GtuException(exception);
499             }
500             return true;
501         }
502     }
503 
504     /**
505      * Interrupt the move and ask for a new plan. This method can be overridden to carry out the bookkeeping needed when the
506      * current plan gets interrupted.
507      * @throws SimRuntimeException when scheduling of the next move fails
508      * @throws GtuException when there is a problem with the state of the GTU when planning a path
509      * @throws NetworkException in case of a problem with the network, e.g., unreachability of a certain point
510      * @throws ParameterException when there is a problem with a parameter
511      */
512     @SuppressWarnings("checkstyle:designforextension")
513     protected void interruptMove() throws SimRuntimeException, GtuException, NetworkException, ParameterException
514     {
515         this.simulator.cancelEvent(this.nextMoveEvent);
516         move(this.operationalPlan.get().getLocation(this.simulator.getSimulatorTime()));
517     }
518 
519     @Override
520     public final String getId()
521     {
522         return this.id;
523     }
524 
525     /**
526      * Sets a tag, these are used for specific use cases of any sort.
527      * @param tag name of the tag.
528      * @param value value of the tag.
529      */
530     public void setTag(final String tag, final String value)
531     {
532         this.tags.put(tag, value);
533     }
534 
535     /**
536      * Returns the value for the given tag, these are used for specific use cases of any sort.
537      * @param tag name of the tag.
538      * @return value of the tag, empty if it is not given to the GTU.
539      */
540     public Optional<String> getTag(final String tag)
541     {
542         return Optional.ofNullable(this.tags.get(tag));
543     }
544 
545     @Override
546     public GtuType getType()
547     {
548         return this.gtuType;
549     }
550 
551     /**
552      * Get reference.
553      * @return the reference position of the GTU, by definition (0, 0, 0).
554      */
555     public final RelativePosition getReference()
556     {
557         return RelativePosition.REFERENCE_POSITION;
558     }
559 
560     /**
561      * Get simulator.
562      * @return the simulator of the GTU.
563      */
564     public final OtsSimulatorInterface getSimulator()
565     {
566         return this.simulator;
567     }
568 
569     /**
570      * Get parameters.
571      * @return Parameters.
572      */
573     public final Parameters getParameters()
574     {
575         return this.parameters;
576     }
577 
578     /**
579      * Set parameters. This method clears any existing parameter history and should normally only be invoked for initialization.
580      * @param parameters parameters
581      */
582     public final void setParameters(final Parameters parameters)
583     {
584         this.parameters = parameters;
585     }
586 
587     /**
588      * Get strategical planner.
589      * @return the planner responsible for the overall 'mission' of the GTU, usually indicating where it needs to go. It
590      *         operates by instantiating tactical planners to do the work.
591      */
592     public StrategicalPlanner getStrategicalPlanner()
593     {
594         return this.strategicalPlanner.get();
595     }
596 
597     /**
598      * Get strategical planner at time.
599      * @param time simulation time to obtain the strategical planner at
600      * @return the planner responsible for the overall 'mission' of the GTU, usually indicating where it needs to go. It
601      *         operates by instantiating tactical planners to do the work.
602      */
603     public StrategicalPlanner getStrategicalPlanner(final Duration time)
604     {
605         return this.strategicalPlanner.get(time);
606     }
607 
608     /**
609      * Get tactical planner.
610      * @return the current tactical planner that can generate an operational plan
611      */
612     public TacticalPlanner<?, ?> getTacticalPlanner()
613     {
614         return getStrategicalPlanner().getTacticalPlanner();
615     }
616 
617     /**
618      * Get tactical planner at time.
619      * @param time simulation time to obtain the tactical planner at
620      * @return the tactical planner that can generate an operational plan at the given time
621      */
622     public TacticalPlanner<?, ?> getTacticalPlanner(final Duration time)
623     {
624         return getStrategicalPlanner(time).getTacticalPlanner(time);
625     }
626 
627     /**
628      * Get operational plan.
629      * @return the current operational plan for the GTU
630      */
631     public final OperationalPlan getOperationalPlan()
632     {
633         return this.operationalPlan.get();
634     }
635 
636     /**
637      * Get operational plan at time.
638      * @param time simulation time to obtain the operational plan at
639      * @return the operational plan for the GTU at the given time.
640      */
641     public final OperationalPlan getOperationalPlan(final Duration time)
642     {
643         return this.operationalPlan.get(time);
644     }
645 
646     /**
647      * Set the operational plan. This method is for sub classes.
648      * @param operationalPlan operational plan.
649      */
650     protected void setOperationalPlan(final OperationalPlan operationalPlan)
651     {
652         this.operationalPlan.set(operationalPlan);
653     }
654 
655     /**
656      * Get odometer.
657      * @return the current odometer value.
658      */
659     public final Length getOdometer()
660     {
661         return getOdometer(this.simulator.getSimulatorTime());
662     }
663 
664     /**
665      * Get odometer at time.
666      * @param time simulation time to obtain the odometer at
667      * @return the odometer value at given time.
668      */
669     public final Length getOdometer(final Duration time)
670     {
671         synchronized (this)
672         {
673             OperationalPlan historicalPlan = getOperationalPlan(time);
674             if (historicalPlan == null || historicalPlan.getStartTime().gt(time) || historicalPlan.getEndTime().lt(time))
675             {
676                 return this.odometer.get(time);
677             }
678             try
679             {
680                 return this.odometer.get(time).plus(getOperationalPlan(time).getTraveledDistance(time));
681             }
682             catch (OperationalPlanException ope)
683             {
684                 Logger.ots().warn("OperationalPlan could not give a traveled distance it the requested time.");
685                 return this.odometer.get(time);
686             }
687         }
688     }
689 
690     /**
691      * Get speed.
692      * @return the current speed of the GTU, along the direction of movement.
693      */
694     public final Speed getSpeed()
695     {
696         synchronized (this)
697         {
698             return getSpeed(this.simulator.getSimulatorTime());
699         }
700     }
701 
702     /**
703      * Get speed at time.
704      * @param time simulation time at which to obtain the speed
705      * @return the current speed of the GTU, along the direction of movement.
706      */
707     public final Speed getSpeed(final Duration time)
708     {
709         synchronized (this)
710         {
711             if (this.cachedSpeedTime != time.si)
712             {
713                 // Invalidate everything
714                 this.cachedSpeedTime = Double.NaN;
715                 this.cachedSpeed = null;
716                 OperationalPlan plan = getOperationalPlan(time);
717                 if (plan == null)
718                 {
719                     this.cachedSpeed = Speed.ZERO;
720                 }
721                 else if (time.si < plan.getStartTime().si)
722                 {
723                     this.cachedSpeed = plan.getStartSpeed();
724                 }
725                 else if (time.si > plan.getEndTime().si)
726                 {
727                     if (time.si - plan.getEndTime().si < 1e-6)
728                     {
729                         this.cachedSpeed = Try.assign(() -> plan.getSpeed(plan.getEndTime()),
730                                 "getSpeed() could not derive a valid speed for the current operationalPlan");
731                     }
732                     else
733                     {
734                         throw new IllegalStateException("Requesting speed value beyond plan.");
735                     }
736                 }
737                 else
738                 {
739                     this.cachedSpeed = Try.assign(() -> plan.getSpeed(time),
740                             "getSpeed() could not derive a valid speed for the current operationalPlan");
741                 }
742                 this.cachedSpeedTime = time.si; // Do this last
743             }
744             return this.cachedSpeed;
745         }
746     }
747 
748     /**
749      * Get acceleration.
750      * @return the current acceleration of the GTU, along the direction of movement.
751      */
752     public final Acceleration getAcceleration()
753     {
754         synchronized (this)
755         {
756             return getAcceleration(this.simulator.getSimulatorTime());
757         }
758     }
759 
760     /**
761      * Get acceleration at time.
762      * @param time simulation time at which to obtain the acceleration
763      * @return the current acceleration of the GTU, along the direction of movement.
764      */
765     public final Acceleration getAcceleration(final Duration time)
766     {
767         synchronized (this)
768         {
769             if (this.cachedAccelerationTime != time.si)
770             {
771                 // Invalidate everything
772                 this.cachedAccelerationTime = Double.NaN;
773                 this.cachedAcceleration = null;
774                 OperationalPlan plan = getOperationalPlan(time);
775                 if (plan == null)
776                 {
777                     this.cachedAcceleration = Acceleration.ZERO;
778                 }
779                 else if (time.si < plan.getStartTime().si)
780                 {
781                     this.cachedAcceleration =
782                             Try.assign(() -> plan.getAcceleration(plan.getStartTime()), "Exception obtaining acceleration.");
783                 }
784                 else if (time.si > plan.getEndTime().si)
785                 {
786                     if (time.si - plan.getEndTime().si < 1e-6)
787                     {
788                         this.cachedAcceleration = Try.assign(() -> plan.getAcceleration(plan.getEndTime()),
789                                 "getAcceleration() could not derive a valid acceleration for the current operationalPlan");
790                     }
791                     else
792                     {
793                         throw new IllegalStateException("Requesting acceleration value beyond plan.");
794                     }
795                 }
796                 else
797                 {
798                     this.cachedAcceleration = Try.assign(() -> plan.getAcceleration(time),
799                             "getAcceleration() could not derive a valid acceleration for the current operationalPlan");
800                 }
801                 this.cachedAccelerationTime = time.si;
802             }
803             return this.cachedAcceleration;
804         }
805     }
806 
807     /**
808      * Get maximum acceleration.
809      * @return maximumAcceleration
810      */
811     public final Acceleration getMaximumAcceleration()
812     {
813         return this.maximumAcceleration;
814     }
815 
816     /**
817      * Set maximum deceleration.
818      * @param maximumAcceleration set maximumAcceleration
819      */
820     public final void setMaximumAcceleration(final Acceleration maximumAcceleration)
821     {
822         if (maximumAcceleration.le(Acceleration.ZERO))
823         {
824             throw new OtsRuntimeException("Maximum acceleration of GTU " + this.id + " set to value <= 0");
825         }
826         this.maximumAcceleration = maximumAcceleration;
827     }
828 
829     /**
830      * Get maximum deceleration.
831      * @return maximumDeceleration
832      */
833     public final Acceleration getMaximumDeceleration()
834     {
835         return this.maximumDeceleration;
836     }
837 
838     /**
839      * Set the maximum deceleration.
840      * @param maximumDeceleration set maximumDeceleration, must be a negative number
841      */
842     public final void setMaximumDeceleration(final Acceleration maximumDeceleration)
843     {
844         if (maximumDeceleration.ge(Acceleration.ZERO))
845         {
846             throw new OtsRuntimeException("Cannot set maximum deceleration of GTU " + this.id + " to " + maximumDeceleration
847                     + " (value must be negative)");
848         }
849         this.maximumDeceleration = maximumDeceleration;
850     }
851 
852     @Override
853     public synchronized DirectedPoint2d getLocation()
854     {
855         Duration locationTime = this.simulator.getSimulatorTime();
856         if (null == this.cacheLocationTime || this.cacheLocationTime.si != locationTime.si)
857         {
858             this.cacheLocation = getLocation(locationTime);
859             this.cacheLocationTime = locationTime;
860         }
861         return this.cacheLocation;
862     }
863 
864     /**
865      * Returns the location of the GTU at the given time.
866      * @param time simulation time
867      * @return location of the GTU at the given time
868      */
869     public synchronized DirectedPoint2d getLocation(final Duration time)
870     {
871         try
872         {
873             return this.operationalPlan.get(time).getLocation(time);
874         }
875         catch (OperationalPlanException exception)
876         {
877             return new DirectedPoint2d(0, 0, 0);
878         }
879     }
880 
881     @Override
882     public double signedDistance(final Point2d point)
883     {
884         return this.shape.signedDistance(point);
885     }
886 
887     /**
888      * Return the shape of a dynamic object at time 'time'. Note that the getContour() method without a time returns the
889      * Minkowski sum of all shapes of the spatial object for a validity time window, e.g., a contour that describes all
890      * locations of a GTU for the next time step, i.e., the contour of the GTU belonging to the next operational plan.
891      * @param time simulation time for which we want the shape
892      * @return the shape of the object at time 'time'
893      */
894     @Override
895     public Polygon2d getAbsoluteContour(final Duration time)
896     {
897         try
898         {
899             return new Polygon2d(OtsShape.toAbsoluteTransform(this.operationalPlan.get(time).getLocation(time))
900                     .transform(getRelativeContour().iterator()));
901         }
902         catch (OperationalPlanException exception)
903         {
904             throw new OtsRuntimeException(exception);
905         }
906     }
907 
908     /**
909      * Return the shape of the GTU for the validity time of the operational plan. Note that this method without a time returns
910      * the Minkowski sum of all shapes of the spatial object for a validity time window, e.g., a contour that describes all
911      * locations of a GTU for the next time step, i.e., the contour of the GTU belonging to the next operational plan.
912      * @return the shape of the object over the validity of the operational plan
913      */
914     @Override
915     public Polygon2d getAbsoluteContour()
916     {
917         try
918         {
919             // TODO: the actual contour of the GTU has to be moved over the path
920             OtsLine2d path = this.operationalPlan.get().getPath();
921             // part of the Gtu length has to be added before the start and after the end of the path.
922             // we assume the reference point is within the contour of the Gtu.
923             double rear = Math.max(0.0, getReference().dx().si - getRear().dx().si);
924             double front = path.getLength() + Math.max(0.0, getFront().dx().si - getReference().dx().si);
925             Point2d p0 = path.getLocationExtendedSI(-rear);
926             Point2d pn = path.getLocationExtendedSI(front);
927             List<Point2d> pList = path.getPointList();
928             pList.add(0, p0);
929             pList.add(pn);
930             OtsLine2d extendedPath = new OtsLine2d(pList);
931             List<Point2d> swath = new ArrayList<>();
932             swath.addAll(extendedPath.offsetLine(getWidth().si / 2.0).getPointList());
933             swath.addAll(extendedPath.offsetLine(-getWidth().si / 2.0).reverse().getPointList());
934             Polygon2d s = new Polygon2d(swath);
935             return s;
936         }
937         catch (Exception e)
938         {
939             throw new OtsRuntimeException(e);
940         }
941     }
942 
943     @Override
944     public Polygon2d getRelativeContour()
945     {
946         return this.shape.getRelativeContour();
947     }
948 
949     /**
950      * Returns whether the GTU is destroyed.
951      * @return whether the GTU is destroyed
952      */
953     public final boolean isDestroyed()
954     {
955         return this.destroyed;
956     }
957 
958     /**
959      * Return perceivable context.
960      * @return the context to which the GTU belongs
961      */
962     public PerceivableContext getPerceivableContext()
963     {
964         return this.perceivableContext;
965     }
966 
967     /**
968      * Adds the provided GTU to this GTU, meaning it moves with this GTU.
969      * @param gtu gtu to enter this GTU
970      * @throws GtuException if the gtu already has a parent
971      */
972     public void addGtu(final Gtu gtu) throws GtuException
973     {
974         this.children.add(gtu);
975         gtu.setParent(this);
976     }
977 
978     /**
979      * Removes the provided GTU from this GTU, meaning it no longer moves with this GTU.
980      * @param gtu gtu to exit this GTU
981      */
982     public void removeGtu(final Gtu gtu)
983     {
984         this.children.remove(gtu);
985         try
986         {
987             gtu.setParent(null);
988         }
989         catch (GtuException exception)
990         {
991             // cannot happen, setting null is always ok
992         }
993     }
994 
995     /**
996      * Set the parent GTU.
997      * @param gtu parent GTU, may be {@code null}
998      * @throws GtuException if the gtu already has a parent
999      */
1000     public void setParent(final Gtu gtu) throws GtuException
1001     {
1002         Throw.when(gtu != null && this.parent != null, GtuException.class, "GTU %s already has a parent.", this);
1003         this.parent = gtu;
1004     }
1005 
1006     /**
1007      * Returns the parent GTU, or {@code null} if this GTU has no parent.
1008      * @return parent GTU, empty if this GTU has no parent
1009      */
1010     public Optional<Gtu> getParent()
1011     {
1012         return Optional.ofNullable(this.parent);
1013     }
1014 
1015     /**
1016      * Returns the children GTU's.
1017      * @return children GTU's
1018      */
1019     public Set<Gtu> getChildren()
1020     {
1021         return new LinkedHashSet<>(this.children); // safe copy
1022     }
1023 
1024     /**
1025      * Get error handler.
1026      * @return errorHandler.
1027      */
1028     protected GtuErrorHandler getErrorHandler()
1029     {
1030         return this.errorHandler;
1031     }
1032 
1033     /**
1034      * Sets the error handler.
1035      * @param errorHandler error handler
1036      */
1037     public void setErrorHandler(final GtuErrorHandler errorHandler)
1038     {
1039         this.errorHandler = errorHandler;
1040     }
1041 
1042     /**
1043      * Returns the align step.
1044      * @return align step, NaN if not present
1045      */
1046     public double getAlignStep()
1047     {
1048         return this.alignStep;
1049     }
1050 
1051     /**
1052      * Set align step, use NaN to not align.
1053      * @param alignStep align step
1054      */
1055     public void setAlignStep(final double alignStep)
1056     {
1057         this.alignStep = alignStep;
1058     }
1059 
1060     /**
1061      * Note that destroying the next move event of the GTU can be dangerous!
1062      * @return nextMoveEvent the next move event of the GTU, e.g. to cancel it from outside.
1063      */
1064     public final SimEventInterface<Duration> getNextMoveEvent()
1065     {
1066         return this.nextMoveEvent;
1067     }
1068 
1069     @Override
1070     public int hashCode()
1071     {
1072         return Objects.hash(this.uniqueNumber);
1073     }
1074 
1075     @Override
1076     @SuppressWarnings("checkstyle:needbraces")
1077     public boolean equals(final Object obj)
1078     {
1079         if (this == obj)
1080             return true;
1081         if (obj == null)
1082             return false;
1083         if (getClass() != obj.getClass())
1084             return false;
1085         Gtu other = (Gtu) obj;
1086         return this.uniqueNumber == other.uniqueNumber;
1087     }
1088 
1089     /**
1090      * The event type for pub/sub indicating a move. <br>
1091      * Payload: [String id, DirectedPoint position, Speed speed, Acceleration acceleration, Length odometer]
1092      */
1093     public static final EventType MOVE_EVENT = new EventType("GTU.MOVE",
1094             new MetaData("GTU move", "GTU id, position, speed, acceleration, odometer",
1095                     new ObjectDescriptor[] {new ObjectDescriptor("Id", "GTU Id", String.class),
1096                             new ObjectDescriptor("position", "position", PositionVector.class),
1097                             new ObjectDescriptor("direction", "direction", Direction.class),
1098                             new ObjectDescriptor("speed", "speed", Speed.class),
1099                             new ObjectDescriptor("acceleration", "acceleration", Acceleration.class),
1100                             new ObjectDescriptor("Odometer", "Total distance travelled since incarnation", Length.class)}));
1101 
1102     /**
1103      * The event type for pub/sub indicating destruction of the GTU. <br>
1104      * Payload: [String id, DirectedPoint lastPosition, Length odometer]
1105      */
1106     public static final EventType DESTROY_EVENT = new EventType("GTU.DESTROY",
1107             new MetaData("GTU destroy", "GTU id, final position, final odometer",
1108                     new ObjectDescriptor[] {new ObjectDescriptor("Id", "GTU Id", String.class),
1109                             new ObjectDescriptor("position", "position", PositionVector.class),
1110                             new ObjectDescriptor("direction", "direction", Direction.class),
1111                             new ObjectDescriptor("Odometer", "Total distance travelled since incarnation", Length.class)}));
1112 
1113 }