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