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