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