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-2026 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 Alexander Verbraeck
66   * @author Peter Knoppers
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 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 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 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 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 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 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 Speed getMaximumSpeed()
377     {
378         return this.maximumSpeed;
379     }
380 
381     @Override
382     public 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                 {
472                     try
473                     {
474                         move(p);
475                     }
476                     catch (SimRuntimeException | GtuException | NetworkException | ParameterException exception)
477                     {
478                         throw new OtsRuntimeException("Exception during move.", exception);
479                     }
480                 });
481             }
482             else
483             {
484                 // schedule the next move at the end of the current operational plan
485                 // store the event, so it can be cancelled in case the plan has to be interrupted and changed halfway
486                 this.nextMoveEvent = this.simulator.scheduleEventRel(newOperationalPlan.getTotalDuration(), () ->
487                 {
488                     try
489                     {
490                         move(newOperationalPlan.getEndLocation());
491                     }
492                     catch (SimRuntimeException | GtuException | NetworkException | ParameterException exception)
493                     {
494                         throw new OtsRuntimeException("Exception during move.", exception);
495                     }
496                 });
497             }
498 
499             fireTimedEvent(Gtu.MOVE_EVENT,
500                     new Object[] {getId(),
501                             new PositionVector(new double[] {fromLocation.x, fromLocation.y}, PositionUnit.METER),
502                             new Direction(fromLocation.getDirZ(), DirectionUnit.EAST_RADIAN), getSpeed(), getAcceleration(),
503                             getOdometer()},
504                     this.simulator.getSimulatorTime());
505 
506             return false;
507         }
508         catch (RuntimeException ex)
509         {
510             try
511             {
512                 this.errorHandler.handle(this, ex);
513             }
514             catch (Exception exception)
515             {
516                 throw new GtuException(exception);
517             }
518             return true;
519         }
520     }
521 
522     /**
523      * Interrupt the move and ask for a new plan. This method can be overridden to carry out the bookkeeping needed when the
524      * current plan gets interrupted.
525      * @throws SimRuntimeException when scheduling of the next move fails
526      * @throws GtuException when there is a problem with the state of the GTU when planning a path
527      * @throws NetworkException in case of a problem with the network, e.g., unreachability of a certain point
528      * @throws ParameterException when there is a problem with a parameter
529      */
530     @SuppressWarnings("checkstyle:designforextension")
531     protected void interruptMove() throws SimRuntimeException, GtuException, NetworkException, ParameterException
532     {
533         this.simulator.cancelEvent(this.nextMoveEvent);
534         move(this.operationalPlan.get().getLocation(this.simulator.getSimulatorTime()));
535     }
536 
537     @Override
538     public String getId()
539     {
540         return this.id;
541     }
542 
543     /**
544      * Sets a tag, these are used for specific use cases of any sort.
545      * @param tag name of the tag.
546      * @param value value of the tag.
547      */
548     public void setTag(final String tag, final String value)
549     {
550         this.tags.put(tag, value);
551     }
552 
553     /**
554      * Returns the value for the given tag, these are used for specific use cases of any sort.
555      * @param tag name of the tag.
556      * @return value of the tag, empty if it is not given to the GTU.
557      */
558     public Optional<String> getTag(final String tag)
559     {
560         return Optional.ofNullable(this.tags.get(tag));
561     }
562 
563     @Override
564     public GtuType getType()
565     {
566         return this.gtuType;
567     }
568 
569     /**
570      * Get reference.
571      * @return the reference position of the GTU, by definition (0, 0, 0).
572      */
573     public RelativePosition getReference()
574     {
575         return RelativePosition.REFERENCE_POSITION;
576     }
577 
578     /**
579      * Get simulator.
580      * @return the simulator of the GTU.
581      */
582     public OtsSimulatorInterface getSimulator()
583     {
584         return this.simulator;
585     }
586 
587     /**
588      * Get parameters.
589      * @return Parameters.
590      */
591     public Parameters getParameters()
592     {
593         return this.parameters;
594     }
595 
596     /**
597      * Set parameters. This method clears any existing parameter history and should normally only be invoked for initialization.
598      * @param parameters parameters
599      */
600     public void setParameters(final Parameters parameters)
601     {
602         this.parameters = parameters;
603     }
604 
605     /**
606      * Get strategical planner.
607      * @return the planner responsible for the overall 'mission' of the GTU, usually indicating where it needs to go. It
608      *         operates by instantiating tactical planners to do the work.
609      */
610     public StrategicalPlanner getStrategicalPlanner()
611     {
612         return this.strategicalPlanner.get();
613     }
614 
615     /**
616      * Get strategical planner at time.
617      * @param time simulation time to obtain the strategical planner at
618      * @return the planner responsible for the overall 'mission' of the GTU, usually indicating where it needs to go. It
619      *         operates by instantiating tactical planners to do the work.
620      */
621     public StrategicalPlanner getStrategicalPlanner(final Duration time)
622     {
623         return this.strategicalPlanner.get(time);
624     }
625 
626     /**
627      * Get tactical planner.
628      * @return the current tactical planner that can generate an operational plan
629      */
630     public TacticalPlanner<?, ?> getTacticalPlanner()
631     {
632         return getStrategicalPlanner().getTacticalPlanner();
633     }
634 
635     /**
636      * Get tactical planner at time.
637      * @param time simulation time to obtain the tactical planner at
638      * @return the tactical planner that can generate an operational plan at the given time
639      */
640     public TacticalPlanner<?, ?> getTacticalPlanner(final Duration time)
641     {
642         return getStrategicalPlanner(time).getTacticalPlanner(time);
643     }
644 
645     /**
646      * Get operational plan.
647      * @return the current operational plan for the GTU
648      */
649     public OperationalPlan getOperationalPlan()
650     {
651         return this.operationalPlan.get();
652     }
653 
654     /**
655      * Get operational plan at time.
656      * @param time simulation time to obtain the operational plan at
657      * @return the operational plan for the GTU at the given time.
658      */
659     public OperationalPlan getOperationalPlan(final Duration time)
660     {
661         return this.operationalPlan.get(time);
662     }
663 
664     /**
665      * Set the operational plan. This method is for sub classes.
666      * @param operationalPlan operational plan.
667      */
668     protected void setOperationalPlan(final OperationalPlan operationalPlan)
669     {
670         this.operationalPlan.set(operationalPlan);
671     }
672 
673     /**
674      * Get odometer.
675      * @return the current odometer value.
676      */
677     public Length getOdometer()
678     {
679         return getOdometer(this.simulator.getSimulatorTime());
680     }
681 
682     /**
683      * Get odometer at time.
684      * @param time simulation time to obtain the odometer at
685      * @return the odometer value at given time.
686      */
687     public Length getOdometer(final Duration time)
688     {
689         synchronized (this)
690         {
691             OperationalPlan historicalPlan = getOperationalPlan(time);
692             if (historicalPlan == null || historicalPlan.getStartTime().gt(time) || historicalPlan.getEndTime().lt(time))
693             {
694                 return this.odometer.get(time);
695             }
696             try
697             {
698                 return this.odometer.get(time).plus(getOperationalPlan(time).getTraveledDistance(time));
699             }
700             catch (OperationalPlanException ope)
701             {
702                 Logger.ots().warn("OperationalPlan could not give a traveled distance it the requested time.");
703                 return this.odometer.get(time);
704             }
705         }
706     }
707 
708     /**
709      * Get speed.
710      * @return the current speed of the GTU, along the direction of movement.
711      */
712     public Speed getSpeed()
713     {
714         synchronized (this)
715         {
716             return getSpeed(this.simulator.getSimulatorTime());
717         }
718     }
719 
720     /**
721      * Get speed at time.
722      * @param time simulation time at which to obtain the speed
723      * @return the current speed of the GTU, along the direction of movement.
724      */
725     public Speed getSpeed(final Duration time)
726     {
727         synchronized (this)
728         {
729             if (this.cachedSpeedTime != time.si)
730             {
731                 // Invalidate everything
732                 this.cachedSpeedTime = Double.NaN;
733                 this.cachedSpeed = null;
734                 OperationalPlan plan = getOperationalPlan(time);
735                 if (plan == null)
736                 {
737                     this.cachedSpeed = Speed.ZERO;
738                 }
739                 else if (time.si < plan.getStartTime().si)
740                 {
741                     this.cachedSpeed = plan.getStartSpeed();
742                 }
743                 else if (time.si > plan.getEndTime().si)
744                 {
745                     if (time.si - plan.getEndTime().si < 1e-6)
746                     {
747                         this.cachedSpeed = Try.assign(() -> plan.getSpeed(plan.getEndTime()),
748                                 "getSpeed() could not derive a valid speed for the current operationalPlan");
749                     }
750                     else
751                     {
752                         throw new IllegalStateException("Requesting speed value beyond plan.");
753                     }
754                 }
755                 else
756                 {
757                     this.cachedSpeed = Try.assign(() -> plan.getSpeed(time),
758                             "getSpeed() could not derive a valid speed for the current operationalPlan");
759                 }
760                 this.cachedSpeedTime = time.si; // Do this last
761             }
762             return this.cachedSpeed;
763         }
764     }
765 
766     /**
767      * Get acceleration.
768      * @return the current acceleration of the GTU, along the direction of movement.
769      */
770     public Acceleration getAcceleration()
771     {
772         synchronized (this)
773         {
774             return getAcceleration(this.simulator.getSimulatorTime());
775         }
776     }
777 
778     /**
779      * Get acceleration at time.
780      * @param time simulation time at which to obtain the acceleration
781      * @return the current acceleration of the GTU, along the direction of movement.
782      */
783     public Acceleration getAcceleration(final Duration time)
784     {
785         synchronized (this)
786         {
787             if (this.cachedAccelerationTime != time.si)
788             {
789                 // Invalidate everything
790                 this.cachedAccelerationTime = Double.NaN;
791                 this.cachedAcceleration = null;
792                 OperationalPlan plan = getOperationalPlan(time);
793                 if (plan == null)
794                 {
795                     this.cachedAcceleration = Acceleration.ZERO;
796                 }
797                 else if (time.si < plan.getStartTime().si)
798                 {
799                     this.cachedAcceleration =
800                             Try.assign(() -> plan.getAcceleration(plan.getStartTime()), "Exception obtaining acceleration.");
801                 }
802                 else if (time.si > plan.getEndTime().si)
803                 {
804                     if (time.si - plan.getEndTime().si < 1e-6)
805                     {
806                         this.cachedAcceleration = Try.assign(() -> plan.getAcceleration(plan.getEndTime()),
807                                 "getAcceleration() could not derive a valid acceleration for the current operationalPlan");
808                     }
809                     else
810                     {
811                         throw new IllegalStateException("Requesting acceleration value beyond plan.");
812                     }
813                 }
814                 else
815                 {
816                     this.cachedAcceleration = Try.assign(() -> plan.getAcceleration(time),
817                             "getAcceleration() could not derive a valid acceleration for the current operationalPlan");
818                 }
819                 this.cachedAccelerationTime = time.si;
820             }
821             return this.cachedAcceleration;
822         }
823     }
824 
825     /**
826      * Get maximum acceleration.
827      * @return maximumAcceleration
828      */
829     public Acceleration getMaximumAcceleration()
830     {
831         return this.maximumAcceleration;
832     }
833 
834     /**
835      * Set maximum deceleration.
836      * @param maximumAcceleration set maximumAcceleration
837      */
838     public void setMaximumAcceleration(final Acceleration maximumAcceleration)
839     {
840         if (maximumAcceleration.le(Acceleration.ZERO))
841         {
842             throw new OtsRuntimeException("Maximum acceleration of GTU " + this.id + " set to value <= 0");
843         }
844         this.maximumAcceleration = maximumAcceleration;
845     }
846 
847     /**
848      * Get maximum deceleration.
849      * @return maximumDeceleration
850      */
851     public Acceleration getMaximumDeceleration()
852     {
853         return this.maximumDeceleration;
854     }
855 
856     /**
857      * Set the maximum deceleration.
858      * @param maximumDeceleration set maximumDeceleration, must be a negative number
859      */
860     public void setMaximumDeceleration(final Acceleration maximumDeceleration)
861     {
862         if (maximumDeceleration.ge(Acceleration.ZERO))
863         {
864             throw new OtsRuntimeException("Cannot set maximum deceleration of GTU " + this.id + " to " + maximumDeceleration
865                     + " (value must be negative)");
866         }
867         this.maximumDeceleration = maximumDeceleration;
868     }
869 
870     @Override
871     public synchronized DirectedPoint2d getLocation()
872     {
873         Duration locationTime = this.simulator.getSimulatorTime();
874         if (null == this.cacheLocationTime || this.cacheLocationTime.si != locationTime.si)
875         {
876             this.cacheLocation = getLocation(locationTime);
877             this.cacheLocationTime = locationTime;
878         }
879         return this.cacheLocation;
880     }
881 
882     /**
883      * Returns the location of the GTU at the given time.
884      * @param time simulation time
885      * @return location of the GTU at the given time
886      */
887     public synchronized DirectedPoint2d getLocation(final Duration time)
888     {
889         try
890         {
891             return this.operationalPlan.get(time).getLocation(time);
892         }
893         catch (OperationalPlanException exception)
894         {
895             return new DirectedPoint2d(0, 0, 0);
896         }
897     }
898 
899     @Override
900     public double signedDistance(final Point2d point)
901     {
902         return this.shape.signedDistance(point);
903     }
904 
905     /**
906      * Return the shape of a dynamic object at time 'time'. Note that the getContour() method without a time returns the
907      * Minkowski sum of all shapes of the spatial object for a validity time window, e.g., a contour that describes all
908      * locations of a GTU for the next time step, i.e., the contour of the GTU belonging to the next operational plan.
909      * @param time simulation time for which we want the shape
910      * @return the shape of the object at time 'time'
911      */
912     @Override
913     public Polygon2d getAbsoluteContour(final Duration time)
914     {
915         try
916         {
917             return new Polygon2d(0.0, OtsShape.toAbsoluteTransform(this.operationalPlan.get(time).getLocation(time))
918                     .transform(getRelativeContour().iterator()));
919         }
920         catch (OperationalPlanException exception)
921         {
922             throw new OtsRuntimeException(exception);
923         }
924     }
925 
926     /**
927      * Return the shape of the GTU for the validity time of the operational plan. Note that this method without a time returns
928      * the Minkowski sum of all shapes of the spatial object for a validity time window, e.g., a contour that describes all
929      * locations of a GTU for the next time step, i.e., the contour of the GTU belonging to the next operational plan.
930      * @return the shape of the object over the validity of the operational plan
931      */
932     @Override
933     public Polygon2d getAbsoluteContour()
934     {
935         try
936         {
937             // TODO: the actual contour of the GTU has to be moved over the path
938             OtsLine2d path = this.operationalPlan.get().getPath();
939             // part of the Gtu length has to be added before the start and after the end of the path.
940             // we assume the reference point is within the contour of the Gtu.
941             double rear = Math.max(0.0, getReference().dx().si - getRear().dx().si);
942             double front = path.getLength() + Math.max(0.0, getFront().dx().si - getReference().dx().si);
943             Point2d p0 = path.getLocationExtended(-rear);
944             Point2d pn = path.getLocationExtended(front);
945             List<Point2d> pList = path.getPointList();
946             pList.add(0, p0);
947             pList.add(pn);
948             OtsLine2d extendedPath = new OtsLine2d(pList);
949             List<Point2d> swath = new ArrayList<>();
950             swath.addAll(extendedPath.offsetLine(getWidth().si / 2.0).getPointList());
951             swath.addAll(extendedPath.offsetLine(-getWidth().si / 2.0).reverse().getPointList());
952             Polygon2d s = new Polygon2d(0.0, swath);
953             return s;
954         }
955         catch (Exception e)
956         {
957             throw new OtsRuntimeException(e);
958         }
959     }
960 
961     @Override
962     public Polygon2d getRelativeContour()
963     {
964         return this.shape.getRelativeContour();
965     }
966 
967     /**
968      * Returns whether the GTU is destroyed.
969      * @return whether the GTU is destroyed
970      */
971     public boolean isDestroyed()
972     {
973         return this.destroyed;
974     }
975 
976     /**
977      * Return perceivable context.
978      * @return the context to which the GTU belongs
979      */
980     public PerceivableContext getPerceivableContext()
981     {
982         return this.perceivableContext;
983     }
984 
985     /**
986      * Adds the provided GTU to this GTU, meaning it moves with this GTU.
987      * @param gtu gtu to enter this GTU
988      * @throws GtuException if the gtu already has a parent
989      */
990     public void addGtu(final Gtu gtu) throws GtuException
991     {
992         this.children.add(gtu);
993         gtu.setParent(this);
994     }
995 
996     /**
997      * Removes the provided GTU from this GTU, meaning it no longer moves with this GTU.
998      * @param gtu gtu to exit this GTU
999      */
1000     public void removeGtu(final Gtu gtu)
1001     {
1002         this.children.remove(gtu);
1003         try
1004         {
1005             gtu.setParent(null);
1006         }
1007         catch (GtuException exception)
1008         {
1009             // cannot happen, setting null is always ok
1010         }
1011     }
1012 
1013     /**
1014      * Set the parent GTU.
1015      * @param gtu parent GTU, may be {@code null}
1016      * @throws GtuException if the gtu already has a parent
1017      */
1018     public void setParent(final Gtu gtu) throws GtuException
1019     {
1020         Throw.when(gtu != null && this.parent != null, GtuException.class, "GTU %s already has a parent.", this);
1021         this.parent = gtu;
1022     }
1023 
1024     /**
1025      * Returns the parent GTU, or {@code null} if this GTU has no parent.
1026      * @return parent GTU, empty if this GTU has no parent
1027      */
1028     public Optional<Gtu> getParent()
1029     {
1030         return Optional.ofNullable(this.parent);
1031     }
1032 
1033     /**
1034      * Returns the children GTU's.
1035      * @return children GTU's
1036      */
1037     public Set<Gtu> getChildren()
1038     {
1039         return new LinkedHashSet<>(this.children); // safe copy
1040     }
1041 
1042     /**
1043      * Get error handler.
1044      * @return errorHandler.
1045      */
1046     protected GtuErrorHandler getErrorHandler()
1047     {
1048         return this.errorHandler;
1049     }
1050 
1051     /**
1052      * Sets the error handler.
1053      * @param errorHandler error handler
1054      */
1055     public void setErrorHandler(final GtuErrorHandler errorHandler)
1056     {
1057         this.errorHandler = errorHandler;
1058     }
1059 
1060     /**
1061      * Returns the align step.
1062      * @return align step, NaN if not present
1063      */
1064     public double getAlignStep()
1065     {
1066         return this.alignStep;
1067     }
1068 
1069     /**
1070      * Set align step, use NaN to not align.
1071      * @param alignStep align step
1072      */
1073     public void setAlignStep(final double alignStep)
1074     {
1075         this.alignStep = alignStep;
1076     }
1077 
1078     /**
1079      * Note that destroying the next move event of the GTU can be dangerous!
1080      * @return nextMoveEvent the next move event of the GTU, e.g. to cancel it from outside.
1081      */
1082     public SimEventInterface<Duration> getNextMoveEvent()
1083     {
1084         return this.nextMoveEvent;
1085     }
1086 
1087     @Override
1088     public int hashCode()
1089     {
1090         return Objects.hash(this.uniqueNumber);
1091     }
1092 
1093     @Override
1094     @SuppressWarnings("checkstyle:needbraces")
1095     public boolean equals(final Object obj)
1096     {
1097         if (this == obj)
1098             return true;
1099         if (obj == null)
1100             return false;
1101         if (getClass() != obj.getClass())
1102             return false;
1103         Gtu other = (Gtu) obj;
1104         return this.uniqueNumber == other.uniqueNumber;
1105     }
1106 
1107     /**
1108      * The event type for pub/sub indicating a move. <br>
1109      * Payload: [String id, DirectedPoint position, Speed speed, Acceleration acceleration, Length odometer]
1110      */
1111     public static final EventType MOVE_EVENT = new EventType("GTU.MOVE",
1112             new MetaData("GTU move", "GTU id, position, speed, acceleration, odometer",
1113                     new ObjectDescriptor[] {new ObjectDescriptor("Id", "GTU Id", String.class),
1114                             new ObjectDescriptor("position", "position", PositionVector.class),
1115                             new ObjectDescriptor("direction", "direction", Direction.class),
1116                             new ObjectDescriptor("speed", "speed", Speed.class),
1117                             new ObjectDescriptor("acceleration", "acceleration", Acceleration.class),
1118                             new ObjectDescriptor("Odometer", "Total distance travelled since incarnation", Length.class)}));
1119 
1120     /**
1121      * The event type for pub/sub indicating destruction of the GTU. <br>
1122      * Payload: [String id, DirectedPoint lastPosition, Length odometer]
1123      */
1124     public static final EventType DESTROY_EVENT = new EventType("GTU.DESTROY",
1125             new MetaData("GTU destroy", "GTU id, final position, final odometer",
1126                     new ObjectDescriptor[] {new ObjectDescriptor("Id", "GTU Id", String.class),
1127                             new ObjectDescriptor("position", "position", PositionVector.class),
1128                             new ObjectDescriptor("direction", "direction", Direction.class),
1129                             new ObjectDescriptor("Odometer", "Total distance travelled since incarnation", Length.class)}));
1130 
1131 }