View Javadoc
1   package org.opentrafficsim.kpi.sampling;
2   
3   import java.util.Arrays;
4   import java.util.LinkedHashMap;
5   import java.util.Map;
6   import java.util.Set;
7   
8   import org.djunits.unit.AccelerationUnit;
9   import org.djunits.unit.DurationUnit;
10  import org.djunits.unit.LengthUnit;
11  import org.djunits.unit.SpeedUnit;
12  import org.djunits.unit.TimeUnit;
13  import org.djunits.value.ValueRuntimeException;
14  import org.djunits.value.vdouble.scalar.Acceleration;
15  import org.djunits.value.vdouble.scalar.Duration;
16  import org.djunits.value.vdouble.scalar.Length;
17  import org.djunits.value.vdouble.scalar.Speed;
18  import org.djunits.value.vdouble.scalar.Time;
19  import org.djunits.value.vfloat.vector.FloatAccelerationVector;
20  import org.djunits.value.vfloat.vector.FloatLengthVector;
21  import org.djunits.value.vfloat.vector.FloatSpeedVector;
22  import org.djunits.value.vfloat.vector.FloatTimeVector;
23  import org.djutils.exceptions.Throw;
24  import org.opentrafficsim.kpi.interfaces.GtuData;
25  import org.opentrafficsim.kpi.interfaces.LaneData;
26  import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
27  import org.opentrafficsim.kpi.sampling.meta.FilterDataType;
28  
29  /**
30   * Contains position, speed, acceleration and time data of a GTU, over some section. Position is relative to the start of the
31   * lane in the direction of travel, also when trajectories have been truncated at a position x > 0. Note that this regards
32   * internal data and output. Input position always refers to the design line of the lane. This class internally flips input
33   * positions and boundaries.
34   * <p>
35   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
36   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
37   * </p>
38   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
39   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
40   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
41   * @param <G> gtu data type
42   */
43  public final class Trajectory<G extends GtuData>
44  {
45  
46      /** Default array capacity. */
47      private static final int DEFAULT_CAPACITY = 10;
48  
49      /** Effective length of the underlying data (arrays may be longer). */
50      private int size = 0;
51  
52      /**
53       * Position array. Position is relative to the start of the lane in the direction of travel, also when trajectories have
54       * been truncated at a position x &gt; 0.
55       */
56      private float[] x = new float[DEFAULT_CAPACITY];
57  
58      /** Speed array. */
59      private float[] v = new float[DEFAULT_CAPACITY];
60  
61      /** Acceleration array. */
62      private float[] a = new float[DEFAULT_CAPACITY];
63  
64      /** Time array. */
65      private float[] t = new float[DEFAULT_CAPACITY];
66  
67      /** GTU id. */
68      private final String gtuId;
69  
70      /** Meta data. */
71      private final Map<FilterDataType<?, ? super G>, Object> filterData = new LinkedHashMap<>();
72  
73      /** Map of array data types and their values. */
74      private final Map<ExtendedDataType<?, ?, ?, ? super G>, Object> extendedData = new LinkedHashMap<>();
75  
76      /** Lane of travel. */
77      private final LaneData<?> lane;
78  
79      /**
80       * @param gtu GtuData; GTU of this trajectory, only the id is stored.
81       * @param filterData Map&lt;FilterDataType&lt;?, ? super G&gt;, Object&gt;; filter data
82       * @param extendedData Set&lt;ExtendedDataType&lt;?, ? ,? , ? super G&gt;&gt;; types of extended data
83       * @param lane LaneData&lt;?&gt;; lane of travel
84       */
85      public Trajectory(final GtuData gtu, final Map<FilterDataType<?, ? super G>, Object> filterData,
86              final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedData, final LaneData<?> lane)
87      {
88          this(gtu == null ? null : gtu.getId(), filterData, extendedData, lane);
89      }
90  
91      /**
92       * Private constructor for creating subsets.
93       * @param gtuId String; GTU id
94       * @param filterData Map&lt;FilterDataType&lt;?, ? super G&gt;, Object&gt;; filter data
95       * @param extendedData Set&lt;ExtendedDataType&lt;?, ?, ?, ? super G&gt;&gt;; types of extended data
96       * @param lane LaneData&lt;?&gt;; lane of travel
97       */
98      private Trajectory(final String gtuId, final Map<FilterDataType<?, ? super G>, Object> filterData,
99              final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedData, final LaneData<?> lane)
100     {
101         Throw.whenNull(gtuId, "GTU may not be null.");
102         Throw.whenNull(filterData, "Filter data may not be null.");
103         Throw.whenNull(extendedData, "Extended data may not be null.");
104         Throw.whenNull(lane, "Lane direction may not be null.");
105         this.gtuId = gtuId;
106         this.filterData.putAll(filterData);
107         for (ExtendedDataType<?, ?, ?, ? super G> dataType : extendedData)
108         {
109             this.extendedData.put(dataType, dataType.initializeStorage());
110         }
111         this.lane = lane;
112     }
113 
114     /**
115      * Adds values of position, speed, acceleration and time.
116      * @param position Length; position is relative to the start of the lane in the direction of the design line, i.e.
117      *            irrespective of the travel direction, also when trajectories have been truncated at a position x &gt; 0
118      * @param speed Speed; speed
119      * @param acceleration Acceleration; acceleration
120      * @param time Time; time
121      */
122     public void add(final Length position, final Speed speed, final Acceleration acceleration, final Time time)
123     {
124         add(position, speed, acceleration, time, null);
125     }
126 
127     /**
128      * Adds values of position, speed, acceleration and time.
129      * @param position Length; position is relative to the start of the lane in the direction of the design line, i.e.
130      *            irrespective of the travel direction, also when trajectories have been truncated at a position x &gt; 0
131      * @param speed Speed; speed
132      * @param acceleration Acceleration; acceleration
133      * @param time Time; time
134      * @param gtu G; gtu to add extended data for
135      */
136     public void add(final Length position, final Speed speed, final Acceleration acceleration, final Time time, final G gtu)
137     {
138         Throw.whenNull(position, "Position may not be null.");
139         Throw.whenNull(speed, "Speed may not be null.");
140         Throw.whenNull(acceleration, "Acceleration may not be null.");
141         Throw.whenNull(time, "Time may not be null.");
142         if (!this.extendedData.isEmpty())
143         {
144             Throw.whenNull(gtu, "GTU may not be null.");
145         }
146         if (this.size == this.x.length)
147         {
148             int cap = this.size + (this.size >> 1);
149             this.x = Arrays.copyOf(this.x, cap);
150             this.v = Arrays.copyOf(this.v, cap);
151             this.a = Arrays.copyOf(this.a, cap);
152             this.t = Arrays.copyOf(this.t, cap);
153         }
154         this.x[this.size] = (float) position.si;
155         this.v[this.size] = (float) speed.si;
156         this.a[this.size] = (float) acceleration.si;
157         this.t[this.size] = (float) time.si;
158         for (ExtendedDataType<?, ?, ?, ? super G> extendedDataType : this.extendedData.keySet())
159         {
160             appendValue(extendedDataType, gtu);
161         }
162         this.size++;
163     }
164 
165     /**
166      * Append value of the extended data type.
167      * @param extendedDataType ExtendedDataType&lt;T, ?, S, ? super G&gt;; extended data type
168      * @param gtu G; gtu
169      * @param <T> extended data value type
170      * @param <S> extended data storage data type
171      */
172     @SuppressWarnings("unchecked")
173     private <T, S> void appendValue(final ExtendedDataType<T, ?, S, ? super G> extendedDataType, final G gtu)
174     {
175         S in = (S) this.extendedData.get(extendedDataType);
176         S out = extendedDataType.setValue(in, this.size, extendedDataType.getValue(gtu));
177         if (in != out)
178         {
179             this.extendedData.put(extendedDataType, out);
180         }
181     }
182 
183     /**
184      * The size of the underlying data.
185      * @return size of the underlying trajectory data
186      */
187     public int size()
188     {
189         return this.size;
190     }
191 
192     /**
193      * Returns the id.
194      * @return GTU id
195      */
196     public String getGtuId()
197     {
198         return this.gtuId;
199     }
200 
201     /**
202      * Returns the position array.
203      * @return si position values.
204      */
205     public float[] getX()
206     {
207         return Arrays.copyOf(this.x, this.size);
208     }
209 
210     /**
211      * Returns the speed array.
212      * @return si speed values
213      */
214     public float[] getV()
215     {
216         return Arrays.copyOf(this.v, this.size);
217     }
218 
219     /**
220      * Returns the acceleration array.
221      * @return si acceleration values
222      */
223     public float[] getA()
224     {
225         return Arrays.copyOf(this.a, this.size);
226     }
227 
228     /**
229      * Returns the time array.
230      * @return si time values
231      */
232     public float[] getT()
233     {
234         return Arrays.copyOf(this.t, this.size);
235     }
236 
237     /**
238      * Returns the last index with a position smaller than or equal to the given position.
239      * @param position float; position
240      * @return int; last index with a position smaller than or equal to the given position
241      */
242     public int binarySearchX(final float position)
243     {
244         if (this.x[0] >= position)
245         {
246             return 0;
247         }
248         int index = Arrays.binarySearch(this.x, 0, this.size, position);
249         return index < 0 ? -index - 2 : index;
250     }
251 
252     /**
253      * Returns the last index with a time smaller than or equal to the given time.
254      * @param time float; time
255      * @return int; last index with a time smaller than or equal to the given time
256      */
257     public int binarySearchT(final float time)
258     {
259         if (this.t[0] >= time)
260         {
261             return 0;
262         }
263         int index = Arrays.binarySearch(this.t, 0, this.size, time);
264         return index < 0 ? -index - 2 : index;
265     }
266 
267     /**
268      * Returns {@code x} value of a single sample.
269      * @param index int; index
270      * @return {@code x} value of a single sample
271      * @throws SamplingException if the index is out of bounds
272      */
273     public float getX(final int index) throws SamplingException
274     {
275         checkSample(index);
276         return this.x[index];
277     }
278 
279     /**
280      * Returns {@code v} value of a single sample.
281      * @param index int; index
282      * @return {@code v} value of a single sample
283      * @throws SamplingException if the index is out of bounds
284      */
285     public float getV(final int index) throws SamplingException
286     {
287         checkSample(index);
288         return this.v[index];
289     }
290 
291     /**
292      * Returns {@code a} value of a single sample.
293      * @param index int; index
294      * @return {@code a} value of a single sample
295      * @throws SamplingException if the index is out of bounds
296      */
297     public float getA(final int index) throws SamplingException
298     {
299         checkSample(index);
300         return this.a[index];
301     }
302 
303     /**
304      * Returns {@code t} value of a single sample.
305      * @param index int; index
306      * @return {@code t} value of a single sample
307      * @throws SamplingException if the index is out of bounds
308      */
309     public float getT(final int index) throws SamplingException
310     {
311         checkSample(index);
312         return this.t[index];
313     }
314 
315     /**
316      * Returns extended data type value of a single sample.
317      * @param extendedDataType ExtendedDataType&lt;T,?,S,?&gt;; data type from which to retrieve the data
318      * @param index int; index for which to retrieve the data
319      * @param <T> scalar type of extended data type
320      * @param <S> storage type of extended data type
321      * @return extended data type value of a single sample
322      * @throws SamplingException if the index is out of bounds
323      */
324     @SuppressWarnings("unchecked")
325     public <T, S> T getExtendedData(final ExtendedDataType<T, ?, S, ?> extendedDataType, final int index)
326             throws SamplingException
327     {
328         checkSample(index);
329         return extendedDataType.getStorageValue((S) this.extendedData.get(extendedDataType), index);
330     }
331 
332     /**
333      * Throws an exception if the sample index is out of bounds.
334      * @param index int; sample index
335      * @throws SamplingException if the sample index is out of bounds
336      */
337     private void checkSample(final int index) throws SamplingException
338     {
339         Throw.when(index < 0 || index >= this.size, SamplingException.class, "Index is out of bounds.");
340     }
341 
342     /**
343      * Returns strongly type position array.
344      * @return strongly typed position array.
345      */
346     public FloatLengthVector getPosition()
347     {
348         try
349         {
350             return new FloatLengthVector(getX(), LengthUnit.SI);
351         }
352         catch (ValueRuntimeException exception)
353         {
354             // should not happen, inputs are not null
355             throw new RuntimeException("Could not return trajectory data.", exception);
356         }
357     }
358 
359     /**
360      * Returns strongly typed speed array.
361      * @return strongly typed speed array.
362      */
363     public FloatSpeedVector getSpeed()
364     {
365         try
366         {
367             return new FloatSpeedVector(getV(), SpeedUnit.SI);
368         }
369         catch (ValueRuntimeException exception)
370         {
371             // should not happen, inputs are not null
372             throw new RuntimeException("Could not return trajectory data.", exception);
373         }
374     }
375 
376     /**
377      * Returns strongly typed acceleration array.
378      * @return strongly typed acceleration array.
379      */
380     public FloatAccelerationVector getAcceleration()
381     {
382         try
383         {
384             return new FloatAccelerationVector(getA(), AccelerationUnit.SI);
385         }
386         catch (ValueRuntimeException exception)
387         {
388             // should not happen, inputs are not null
389             throw new RuntimeException("Could not return trajectory data.", exception);
390         }
391     }
392 
393     /**
394      * Returns strongly typed time array.
395      * @return strongly typed time array.
396      */
397     public FloatTimeVector getTime()
398     {
399         try
400         {
401             return new FloatTimeVector(getT(), TimeUnit.BASE_SECOND);
402         }
403         catch (ValueRuntimeException exception)
404         {
405             // should not happen, inputs are not null
406             throw new RuntimeException("Could not return trajectory data.", exception);
407         }
408     }
409 
410     /**
411      * Returns the length of the data.
412      * @return total length of this trajectory
413      * @throws IllegalStateException if trajectory is empty
414      */
415     public Length getTotalLength()
416     {
417         // TODO do not allow empty trajectory
418         // Throw.when(this.size == 0, IllegalStateException.class, "Empty trajectory does not have a length.");
419         if (this.size == 0)
420         {
421             return Length.ZERO;
422         }
423         return new Length(this.x[this.size - 1] - this.x[0], LengthUnit.SI);
424     }
425 
426     /**
427      * Returns the total duration span.
428      * @return total duration of this trajectory
429      * @throws IllegalStateException if trajectory is empty
430      */
431     public Duration getTotalDuration()
432     {
433         // TODO do not allow empty trajectory
434         // Throw.when(this.size == 0, IllegalStateException.class, "Empty trajectory does not have a duration.");
435         if (this.size == 0)
436         {
437             return Duration.ZERO;
438         }
439         return new Duration(this.t[this.size - 1] - this.t[0], DurationUnit.SI);
440     }
441 
442     /**
443      * Returns whether the filter data is contained.
444      * @param filterDataType MetaDataType&lt;?, ?&gt;; filter data type
445      * @return whether the trajectory contains the filter data of give type
446      */
447     public boolean contains(final FilterDataType<?, ?> filterDataType)
448     {
449         return this.filterData.containsKey(filterDataType);
450     }
451 
452     /**
453      * Returns the value of the filter data.
454      * @param filterDataType MetaDataType&lt;T, ?&gt;; filter data type
455      * @param <T> class of filter data
456      * @return value of filter data
457      */
458     @SuppressWarnings("unchecked")
459     public <T> T getFilterData(final FilterDataType<T, ?> filterDataType)
460     {
461         return (T) this.filterData.get(filterDataType);
462     }
463 
464     /**
465      * Returns the included filter data types.
466      * @return included filter data types
467      */
468     public Set<FilterDataType<?, ? super G>> getFilterDataTypes()
469     {
470         return this.filterData.keySet();
471     }
472 
473     /**
474      * Returns whether ths extended data type is contained.
475      * @param extendedDataType ExtendedDataType&lt;?,?,?,?&gt;; extended data type
476      * @return whether the trajectory contains the extended data of give type
477      */
478     public boolean contains(final ExtendedDataType<?, ?, ?, ?> extendedDataType)
479     {
480         return this.extendedData.containsKey(extendedDataType);
481     }
482 
483     /**
484      * Returns the output data of the extended data type.
485      * @param extendedDataType ExtendedDataType&lt;?,O,S,?&gt;; extended data type to return
486      * @param <O> output type
487      * @param <S> storage type
488      * @return values of extended data type
489      * @throws SamplingException if the extended data type is not in the trajectory
490      */
491     @SuppressWarnings("unchecked")
492     public <O, S> O getExtendedData(final ExtendedDataType<?, O, S, ?> extendedDataType) throws SamplingException
493     {
494         Throw.when(!this.extendedData.containsKey(extendedDataType), SamplingException.class,
495                 "Extended data type %s is not in the trajectory.", extendedDataType);
496         return extendedDataType.convert((S) this.extendedData.get(extendedDataType), this.size);
497     }
498 
499     /**
500      * Returns the included extended data types.
501      * @return included extended data types
502      */
503     public Set<ExtendedDataType<?, ?, ?, ? super G>> getExtendedDataTypes()
504     {
505         return this.extendedData.keySet();
506     }
507 
508     /**
509      * Returns a space-time view of this trajectory. This is much more efficient than {@code subSet()} as no trajectory is
510      * copied. The limitation is that only distance and time (and mean speed) in the space-time view can be obtained.
511      * @param startPosition Length; start position
512      * @param endPosition Length; end position
513      * @param startTime Time; start time
514      * @param endTime Time; end time
515      * @return space-time view of this trajectory
516      */
517     public SpaceTimeView getSpaceTimeView(final Length startPosition, final Length endPosition, final Time startTime,
518             final Time endTime)
519     {
520         if (size() == 0)
521         {
522             return new SpaceTimeView(Length.ZERO, Duration.ZERO);
523         }
524         Boundaries bounds = spaceBoundaries(startPosition, endPosition).intersect(timeBoundaries(startTime, endTime));
525         double xFrom;
526         double tFrom;
527         if (bounds.fFrom > 0.0)
528         {
529             xFrom = this.x[bounds.from] * (1 - bounds.fFrom) + this.x[bounds.from + 1] * bounds.fFrom;
530             tFrom = this.t[bounds.from] * (1 - bounds.fFrom) + this.t[bounds.from + 1] * bounds.fFrom;
531         }
532         else
533         {
534             xFrom = this.x[bounds.from];
535             tFrom = this.t[bounds.from];
536         }
537         double xTo;
538         double tTo;
539         if (bounds.fTo > 0.0)
540         {
541             xTo = this.x[bounds.to] * (1 - bounds.fTo) + this.x[bounds.to + 1] * bounds.fTo;
542             tTo = this.t[bounds.to] * (1 - bounds.fTo) + this.t[bounds.to + 1] * bounds.fTo;
543         }
544         else
545         {
546             xTo = this.x[bounds.to];
547             tTo = this.t[bounds.to];
548         }
549         return new SpaceTimeView(Length.instantiateSI(xTo - xFrom), Duration.instantiateSI(tTo - tFrom));
550     }
551 
552     /**
553      * Copies the trajectory but with a subset of the data. Longitudinal entry is only true if the original trajectory has true,
554      * and the subset is from the start.
555      * @param startPosition Length; start position
556      * @param endPosition Length; end position
557      * @return subset of the trajectory
558      * @throws NullPointerException if an input is null
559      * @throws IllegalArgumentException of minLength is smaller than maxLength
560      */
561     public Trajectory<G> subSet(final Length startPosition, final Length endPosition)
562     {
563         Throw.whenNull(startPosition, "Start position may not be null");
564         Throw.whenNull(endPosition, "End position may not be null");
565         Throw.when(startPosition.gt(endPosition), IllegalArgumentException.class,
566                 "Start position should be smaller than end position in the direction of travel");
567         if (this.size == 0)
568         {
569             return new Trajectory<>(this.gtuId, this.filterData, this.extendedData.keySet(), this.lane);
570         }
571         return subSet(spaceBoundaries(startPosition, endPosition));
572     }
573 
574     /**
575      * Copies the trajectory but with a subset of the data.
576      * @param startTime Time; start time
577      * @param endTime Time; end time
578      * @return subset of the trajectory
579      * @throws NullPointerException if an input is null
580      * @throws IllegalArgumentException of minTime is smaller than maxTime
581      */
582     public Trajectory<G> subSet(final Time startTime, final Time endTime)
583     {
584         Throw.whenNull(startTime, "Start time may not be null");
585         Throw.whenNull(endTime, "End time may not be null");
586         Throw.when(startTime.gt(endTime), IllegalArgumentException.class, "Start time should be smaller than end time.");
587         if (this.size == 0)
588         {
589             return new Trajectory<>(this.gtuId, this.filterData, this.extendedData.keySet(), this.lane);
590         }
591         return subSet(timeBoundaries(startTime, endTime));
592     }
593 
594     /**
595      * Copies the trajectory but with a subset of the data.
596      * @param startPosition Length; start position
597      * @param endPosition Length; end position
598      * @param startTime Time; start time
599      * @param endTime Time; end time
600      * @return subset of the trajectory
601      * @throws NullPointerException if an input is null
602      * @throws IllegalArgumentException of minLength/Time is smaller than maxLength/Time
603      */
604     public Trajectory<G> subSet(final Length startPosition, final Length endPosition, final Time startTime, final Time endTime)
605     {
606         // could use this.subSet(minLength, maxLength).subSet(minTime, maxTime), but that copies twice
607         Throw.whenNull(startPosition, "Start position may not be null");
608         Throw.whenNull(endPosition, "End position may not be null");
609         Throw.when(startPosition.gt(endPosition), IllegalArgumentException.class,
610                 "Start position should be smaller than end position in the direction of travel");
611         Throw.whenNull(startTime, "Start time may not be null");
612         Throw.whenNull(endTime, "End time may not be null");
613         Throw.when(startTime.gt(endTime), IllegalArgumentException.class, "Start time should be smaller than end time.");
614         if (this.size == 0)
615         {
616             return new Trajectory<>(this.gtuId, this.filterData, this.extendedData.keySet(), this.lane);
617         }
618         return subSet(spaceBoundaries(startPosition, endPosition).intersect(timeBoundaries(startTime, endTime)));
619     }
620 
621     /**
622      * Determine spatial boundaries.
623      * @param startPosition Length; start position
624      * @param endPosition Length; end position
625      * @return spatial boundaries
626      */
627     private Boundaries spaceBoundaries(final Length startPosition, final Length endPosition)
628     {
629         if (startPosition.si > this.x[this.size - 1] || endPosition.si < this.x[0])
630         {
631             return new Boundaries(0, 0.0, 0, 0.0);
632         }
633         // to float needed as x is in floats and due to precision fTo > 1 may become true
634         float startPos = (float) startPosition.si;
635         float endPos = (float) endPosition.si;
636         Boundary from = getBoundaryAtPosition(startPos, false);
637         Boundary to = getBoundaryAtPosition(endPos, true);
638         return new Boundaries(from.index, from.fraction, to.index, to.fraction);
639     }
640 
641     /**
642      * Determine temporal boundaries.
643      * @param startTime Time; start time
644      * @param endTime Time; end time
645      * @return spatial boundaries
646      */
647     private Boundaries timeBoundaries(final Time startTime, final Time endTime)
648     {
649         if (startTime.si > this.t[this.size - 1] || endTime.si < this.t[0])
650         {
651             return new Boundaries(0, 0.0, 0, 0.0);
652         }
653         // to float needed as x is in floats and due to precision fTo > 1 may become true
654         float startTim = (float) startTime.si;
655         float endTim = (float) endTime.si;
656         Boundary from = getBoundaryAtTime(startTim, false);
657         Boundary to = getBoundaryAtTime(endTim, true);
658         return new Boundaries(from.index, from.fraction, to.index, to.fraction);
659     }
660 
661     /**
662      * Returns the boundary at the given position.
663      * @param position float; position
664      * @param end boolean; whether the end of a range is searched
665      * @return Boundary; boundary at the given position
666      */
667     private Boundary getBoundaryAtPosition(final float position, final boolean end)
668     {
669         int index = binarySearchX(position);
670         double fraction = 0;
671         if (end ? index < this.size - 1 : this.x[index] < position)
672         {
673             fraction = (position - this.x[index]) / (this.x[index + 1] - this.x[index]);
674         }
675         return new Boundary(index, fraction);
676     }
677 
678     /**
679      * Returns the boundary at the given time.
680      * @param time float; time
681      * @param end boolean; whether the end of a range is searched
682      * @return Boundary; boundary at the given time
683      */
684     private Boundary getBoundaryAtTime(final float time, final boolean end)
685     {
686         int index = binarySearchT(time);
687         double fraction = 0;
688         if (end ? index < this.size - 1 : this.t[index] < time)
689         {
690             fraction = (time - this.t[index]) / (this.t[index + 1] - this.t[index]);
691         }
692         return new Boundary(index, fraction);
693     }
694 
695     /**
696      * Returns an interpolated time at the given position.
697      * @param position Length; position
698      * @return Time; interpolated time at the given position
699      */
700     public Time getTimeAtPosition(final Length position)
701     {
702         return Time.instantiateSI(getBoundaryAtPosition((float) position.si, false).getValue(this.t));
703     }
704 
705     /**
706      * Returns an interpolated speed at the given position.
707      * @param position Length; position
708      * @return Speed; interpolated speed at the given position
709      */
710     public Speed getSpeedAtPosition(final Length position)
711     {
712         return Speed.instantiateSI(getBoundaryAtPosition((float) position.si, false).getValue(this.v));
713     }
714 
715     /**
716      * Returns an interpolated acceleration at the given position.
717      * @param position Length; position
718      * @return Acceleration; interpolated acceleration at the given position
719      */
720     public Acceleration getAccelerationAtPosition(final Length position)
721     {
722         return Acceleration.instantiateSI(getBoundaryAtPosition((float) position.si, false).getValue(this.a));
723     }
724 
725     /**
726      * Returns an interpolated position at the given time.
727      * @param time Time; time
728      * @return Length; interpolated position at the given time
729      */
730     public Length getPositionAtTime(final Time time)
731     {
732         return Length.instantiateSI(getBoundaryAtTime((float) time.si, false).getValue(this.x));
733     }
734 
735     /**
736      * Returns an interpolated speed at the given time.
737      * @param time Time; time
738      * @return Speed; interpolated speed at the given time
739      */
740     public Speed getSpeedAtTime(final Time time)
741     {
742         return Speed.instantiateSI(getBoundaryAtTime((float) time.si, false).getValue(this.v));
743     }
744 
745     /**
746      * Returns an interpolated acceleration at the given time.
747      * @param time Time; time
748      * @return Acceleration; interpolated acceleration at the given time
749      */
750     public Acceleration getAccelerationAtTime(final Time time)
751     {
752         return Acceleration.instantiateSI(getBoundaryAtTime((float) time.si, false).getValue(this.a));
753     }
754 
755     /**
756      * Copies the trajectory but with a subset of the data. Data is taken from position (from + fFrom) to (to + fTo).
757      * @param bounds Boundaries; boundaries
758      * @param <T> type of underlying extended data value
759      * @param <S> storage type
760      * @return subset of the trajectory
761      */
762     @SuppressWarnings("unchecked")
763     private <T, S> Trajectory<G> subSet(final Boundaries bounds)
764     {
765         Trajectory<G> out = new Trajectory<>(this.gtuId, this.filterData, this.extendedData.keySet(), this.lane);
766         if (bounds.from < bounds.to) // otherwise empty, no data in the subset
767         {
768             int nBefore = bounds.fFrom < 1.0 ? 1 : 0;
769             int nAfter = bounds.fTo > 0.0 ? 1 : 0;
770             int n = bounds.to - bounds.from + nBefore + nAfter;
771             out.x = new float[n];
772             out.v = new float[n];
773             out.a = new float[n];
774             out.t = new float[n];
775             System.arraycopy(this.x, bounds.from + 1, out.x, nBefore, bounds.to - bounds.from);
776             System.arraycopy(this.v, bounds.from + 1, out.v, nBefore, bounds.to - bounds.from);
777             System.arraycopy(this.a, bounds.from + 1, out.a, nBefore, bounds.to - bounds.from);
778             System.arraycopy(this.t, bounds.from + 1, out.t, nBefore, bounds.to - bounds.from);
779             if (nBefore == 1)
780             {
781                 out.x[0] = (float) (this.x[bounds.from] * (1 - bounds.fFrom) + this.x[bounds.from + 1] * bounds.fFrom);
782                 out.v[0] = (float) (this.v[bounds.from] * (1 - bounds.fFrom) + this.v[bounds.from + 1] * bounds.fFrom);
783                 out.a[0] = (float) (this.a[bounds.from] * (1 - bounds.fFrom) + this.a[bounds.from + 1] * bounds.fFrom);
784                 out.t[0] = (float) (this.t[bounds.from] * (1 - bounds.fFrom) + this.t[bounds.from + 1] * bounds.fFrom);
785             }
786             if (nAfter == 1)
787             {
788                 out.x[n - 1] = (float) (this.x[bounds.to] * (1 - bounds.fTo) + this.x[bounds.to + 1] * bounds.fTo);
789                 out.v[n - 1] = (float) (this.v[bounds.to] * (1 - bounds.fTo) + this.v[bounds.to + 1] * bounds.fTo);
790                 out.a[n - 1] = (float) (this.a[bounds.to] * (1 - bounds.fTo) + this.a[bounds.to + 1] * bounds.fTo);
791                 out.t[n - 1] = (float) (this.t[bounds.to] * (1 - bounds.fTo) + this.t[bounds.to + 1] * bounds.fTo);
792             }
793             out.size = n;
794             for (ExtendedDataType<?, ?, ?, ? super G> extendedDataType : this.extendedData.keySet())
795             {
796                 int j = 0;
797                 ExtendedDataType<T, ?, S, G> edt = (ExtendedDataType<T, ?, S, G>) extendedDataType;
798                 S fromList = (S) this.extendedData.get(extendedDataType);
799                 S toList = edt.initializeStorage();
800                 try
801                 {
802                     if (nBefore == 1)
803                     {
804                         toList = edt.setValue(toList, j,
805                                 ((ExtendedDataType<T, ?, ?, G>) extendedDataType).interpolate(
806                                         edt.getStorageValue(fromList, bounds.from),
807                                         edt.getStorageValue(fromList, bounds.from + 1), bounds.fFrom));
808                         j++;
809                     }
810                     for (int i = bounds.from + 1; i <= bounds.to; i++)
811                     {
812                         toList = edt.setValue(toList, j, edt.getStorageValue(fromList, i));
813                         j++;
814                     }
815                     if (nAfter == 1)
816                     {
817                         toList = edt.setValue(toList, j,
818                                 ((ExtendedDataType<T, ?, ?, G>) extendedDataType).interpolate(
819                                         edt.getStorageValue(fromList, bounds.to), edt.getStorageValue(fromList, bounds.to + 1),
820                                         bounds.fTo));
821                     }
822                 }
823                 catch (SamplingException se)
824                 {
825                     // should not happen as bounds are determined internally
826                     throw new RuntimeException("Error while obtaining subset of trajectory.", se);
827                 }
828                 out.extendedData.put(extendedDataType, toList);
829             }
830         }
831         return out;
832     }
833 
834     /** {@inheritDoc} */
835     @Override
836     public int hashCode()
837     {
838         final int prime = 31;
839         int result = 1;
840         result = prime * result + ((this.gtuId == null) ? 0 : this.gtuId.hashCode());
841         result = prime * result + this.size;
842         if (this.size > 0)
843         {
844             result = prime * result + Float.floatToIntBits(this.t[0]);
845         }
846         return result;
847     }
848 
849     /** {@inheritDoc} */
850     @Override
851     public boolean equals(final Object obj)
852     {
853         if (this == obj)
854         {
855             return true;
856         }
857         if (obj == null)
858         {
859             return false;
860         }
861         if (getClass() != obj.getClass())
862         {
863             return false;
864         }
865         Trajectory<?> other = (Trajectory<?>) obj;
866         if (this.size != other.size)
867         {
868             return false;
869         }
870         if (this.gtuId == null)
871         {
872             if (other.gtuId != null)
873             {
874                 return false;
875             }
876         }
877         else if (!this.gtuId.equals(other.gtuId))
878         {
879             return false;
880         }
881         if (this.size > 0)
882         {
883             if (this.t[0] != other.t[0])
884             {
885                 return false;
886             }
887         }
888         return true;
889     }
890 
891     /** {@inheritDoc} */
892     @Override
893     public String toString()
894     {
895         if (this.size > 0)
896         {
897             return "Trajectory [size=" + this.size + ", x={" + this.x[0] + "..." + this.x[this.size - 1] + "}, t={" + this.t[0]
898                     + "..." + this.t[this.size - 1] + "}, filterData=" + this.filterData + ", gtuId=" + this.gtuId + "]";
899         }
900         return "Trajectory [size=" + this.size + ", x={}, t={}, filterData=" + this.filterData + ", gtuId=" + this.gtuId + "]";
901     }
902 
903     /**
904      * Spatial or temporal boundary as a fractional position in the array.
905      * <p>
906      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
907      * <br>
908      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
909      * </p>
910      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
911      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
912      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
913      */
914     public class Boundary
915     {
916         /** Rounded-down index. */
917         @SuppressWarnings("checkstyle:visibilitymodifier")
918         public final int index;
919 
920         /** Fraction. */
921         @SuppressWarnings("checkstyle:visibilitymodifier")
922         public final double fraction;
923 
924         /**
925          * @param index int; rounded down index
926          * @param fraction double; fraction
927          */
928         Boundary(final int index, final double fraction)
929         {
930             this.index = index;
931             this.fraction = fraction;
932         }
933 
934         /** {@inheritDoc} */
935         @Override
936         public final String toString()
937         {
938             return "Boundary [index=" + this.index + ", fraction=" + this.fraction + "]";
939         }
940 
941         /**
942          * Returns the value at the boundary in the array.
943          * @param array float[]; float[] array
944          * @return double; value at the boundary in the array
945          */
946         public double getValue(final float[] array)
947         {
948             if (this.fraction == 0.0)
949             {
950                 return array[this.index];
951             }
952             if (this.fraction == 1.0)
953             {
954                 return array[this.index + 1];
955             }
956             return (1 - this.fraction) * array[this.index] + this.fraction * array[this.index + 1];
957         }
958     }
959 
960     /**
961      * Spatial or temporal range as a fractional positions in the array.
962      * <p>
963      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
964      * <br>
965      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
966      * </p>
967      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
968      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
969      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
970      */
971     private class Boundaries
972     {
973         /** Rounded-down from-index. */
974         @SuppressWarnings("checkstyle:visibilitymodifier")
975         public final int from;
976 
977         /** Fraction of to-index. */
978         @SuppressWarnings("checkstyle:visibilitymodifier")
979         public final double fFrom;
980 
981         /** Rounded-down to-index. */
982         @SuppressWarnings("checkstyle:visibilitymodifier")
983         public final int to;
984 
985         /** Fraction of to-index. */
986         @SuppressWarnings("checkstyle:visibilitymodifier")
987         public final double fTo;
988 
989         /**
990          * @param from int; from index, rounded down
991          * @param fFrom double; from index, fraction
992          * @param to int; to index, rounded down
993          * @param fTo double; to index, fraction
994          */
995         Boundaries(final int from, final double fFrom, final int to, final double fTo)
996         {
997             Throw.when(from < 0 || from > Trajectory.this.size() - 1, IllegalArgumentException.class,
998                     "Argument from (%d) is out of bounds.", from);
999             Throw.when(fFrom < 0 || fFrom > 1, IllegalArgumentException.class, "Argument fFrom (%f) is out of bounds.", fFrom);
1000             Throw.when(from == Trajectory.this.size() && fFrom > 0, IllegalArgumentException.class,
1001                     "Arguments from (%d) and fFrom (%f) are out of bounds.", from, fFrom);
1002             Throw.when(to < 0 || to >= Trajectory.this.size(), IllegalArgumentException.class,
1003                     "Argument to (%d) is out of bounds.", to);
1004             Throw.when(fTo < 0 || fTo > 1, IllegalArgumentException.class, "Argument fTo (%f) is out of bounds.", fTo);
1005             Throw.when(to == Trajectory.this.size() && fTo > 0, IllegalArgumentException.class,
1006                     "Arguments to (%d) and fTo (%f) are out of bounds.", to, fTo);
1007             this.from = from;
1008             this.fFrom = fFrom;
1009             this.to = to;
1010             this.fTo = fTo;
1011         }
1012 
1013         /**
1014          * Returns the intersect of both boundaries.
1015          * @param boundaries Boundaries; boundaries
1016          * @return intersect of both boundaries
1017          */
1018         public Boundaries intersect(final Boundaries boundaries)
1019         {
1020             if (this.to < boundaries.from || boundaries.to < this.from
1021                     || this.to == boundaries.from && this.fTo < boundaries.fFrom
1022                     || boundaries.to == this.from && boundaries.fTo < this.fFrom)
1023             {
1024                 return new Boundaries(0, 0.0, 0, 0.0); // no overlap
1025             }
1026             int newFrom;
1027             double newFFrom;
1028             if (this.from > boundaries.from || this.from == boundaries.from && this.fFrom > boundaries.fFrom)
1029             {
1030                 newFrom = this.from;
1031                 newFFrom = this.fFrom;
1032             }
1033             else
1034             {
1035                 newFrom = boundaries.from;
1036                 newFFrom = boundaries.fFrom;
1037             }
1038             int newTo;
1039             double newFTo;
1040             if (this.to < boundaries.to || this.to == boundaries.to && this.fTo < boundaries.fTo)
1041             {
1042                 newTo = this.to;
1043                 newFTo = this.fTo;
1044             }
1045             else
1046             {
1047                 newTo = boundaries.to;
1048                 newFTo = boundaries.fTo;
1049             }
1050             return new Boundaries(newFrom, newFFrom, newTo, newFTo);
1051         }
1052 
1053         /** {@inheritDoc} */
1054         @Override
1055         public final String toString()
1056         {
1057             return "Boundaries [from=" + this.from + ", fFrom=" + this.fFrom + ", to=" + this.to + ", fTo=" + this.fTo + "]";
1058         }
1059 
1060     }
1061 
1062     /**
1063      * Space-time view of a trajectory. This supplies distance and time (and mean speed) in a space-time box.
1064      * <p>
1065      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1066      * <br>
1067      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
1068      * </p>
1069      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
1070      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
1071      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
1072      */
1073     public static final class SpaceTimeView
1074     {
1075 
1076         /** Distance. */
1077         private final Length distance;
1078 
1079         /** Time. */
1080         private final Duration time;
1081 
1082         /**
1083          * Constructor.
1084          * @param distance Length; distance
1085          * @param time Duration; time
1086          */
1087         private SpaceTimeView(final Length distance, final Duration time)
1088         {
1089             this.distance = distance;
1090             this.time = time;
1091         }
1092 
1093         /**
1094          * Returns the distance.
1095          * @return Length; distance
1096          */
1097         public Length getDistance()
1098         {
1099             return this.distance;
1100         }
1101 
1102         /**
1103          * Returns the time.
1104          * @return Duration; time
1105          */
1106         public Duration getTime()
1107         {
1108             return this.time;
1109         }
1110 
1111         /** {@inheritDoc} */
1112         @Override
1113         public String toString()
1114         {
1115             return "SpaceTimeView [distance=" + this.distance + ", time=" + this.time + "]";
1116         }
1117     }
1118 
1119 }