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.vdouble.scalar.Time;
18  import org.djunits.value.vfloat.vector.FloatAccelerationVector;
19  import org.djunits.value.vfloat.vector.FloatLengthVector;
20  import org.djunits.value.vfloat.vector.FloatSpeedVector;
21  import org.djunits.value.vfloat.vector.FloatTimeVector;
22  import org.djutils.exceptions.Throw;
23  import org.opentrafficsim.kpi.interfaces.GtuData;
24  import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
25  import org.opentrafficsim.kpi.sampling.filter.FilterDataType;
26  
27  /**
28   * Contains position, speed, acceleration and time data of a GTU, over some section. Position is relative to the start of the
29   * lane in the direction of travel.
30   * <p>
31   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
32   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
33   * </p>
34   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
35   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
36   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
37   * @param <G> GTU data type
38   */
39  public final class Trajectory<G extends GtuData>
40  {
41  
42      /** Default array capacity. */
43      private static final int DEFAULT_CAPACITY = 10;
44  
45      /** Effective length of the underlying data (arrays may be longer). */
46      private int size = 0;
47  
48      /**
49       * Position array. Position is relative to the start of the lane in the direction of travel, also when trajectories have
50       * been truncated at a position x &gt; 0.
51       */
52      private float[] x = new float[DEFAULT_CAPACITY];
53  
54      /** Speed array. */
55      private float[] v = new float[DEFAULT_CAPACITY];
56  
57      /** Acceleration array. */
58      private float[] a = new float[DEFAULT_CAPACITY];
59  
60      /** Time array. */
61      private float[] t = new float[DEFAULT_CAPACITY];
62  
63      /** GTU id. */
64      private final String gtuId;
65  
66      /** GTU type id. */
67      private final String gtuTypeId;
68  
69      /** Filter data. */
70      private final Map<FilterDataType<?, ? super G>, Object> filterData = new LinkedHashMap<>();
71  
72      /** Map of extended data types and their values (usually arrays). */
73      private final Map<ExtendedDataType<?, ?, ?, ? super G>, Object> extendedData = new LinkedHashMap<>();
74  
75      /**
76       * Constructor.
77       * @param gtu GTU of this trajectory, only the id is stored.
78       * @param filterData filter data
79       * @param extendedData types of extended data
80       */
81      public Trajectory(final GtuData gtu, final Map<FilterDataType<?, ? super G>, Object> filterData,
82              final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedData)
83      {
84          this(gtu == null ? null : gtu.getId(), gtu == null ? null : gtu.getGtuTypeId(), filterData, extendedData);
85      }
86  
87      /**
88       * Private constructor for creating subsets.
89       * @param gtuId GTU id
90       * @param gtuTypeId GTU type id
91       * @param filterData filter data
92       * @param extendedData types of extended data
93       */
94      private Trajectory(final String gtuId, final String gtuTypeId, final Map<FilterDataType<?, ? super G>, Object> filterData,
95              final Set<ExtendedDataType<?, ?, ?, ? super G>> extendedData)
96      {
97          Throw.whenNull(gtuId, "GTU id may not be null.");
98          Throw.whenNull(gtuTypeId, "GTU type id may not be null.");
99          Throw.whenNull(filterData, "Filter data may not be null.");
100         Throw.whenNull(extendedData, "Extended data may not be null.");
101         this.gtuId = gtuId;
102         this.gtuTypeId = gtuTypeId;
103         this.filterData.putAll(filterData);
104         for (ExtendedDataType<?, ?, ?, ? super G> dataType : extendedData)
105         {
106             this.extendedData.put(dataType, dataType.initializeStorage());
107         }
108     }
109 
110     /**
111      * Adds values of position, speed, acceleration and time.
112      * @param position position is relative to the start of the lane in the direction of the design line, i.e. irrespective of
113      *            the travel direction, also when trajectories have been truncated at a position x &gt; 0
114      * @param speed speed
115      * @param acceleration acceleration
116      * @param time time
117      */
118     public void add(final Length position, final Speed speed, final Acceleration acceleration, final Time time)
119     {
120         add(position, speed, acceleration, time, null);
121     }
122 
123     /**
124      * Adds values of position, speed, acceleration, time and extended data.
125      * @param position position is relative to the start of the lane in the direction of the design line
126      * @param speed speed
127      * @param acceleration acceleration
128      * @param time time
129      * @param gtu gtu to add extended data for
130      */
131     public void add(final Length position, final Speed speed, final Acceleration acceleration, final Time time, final G gtu)
132     {
133         Throw.whenNull(position, "Position may not be null.");
134         Throw.whenNull(speed, "Speed may not be null.");
135         Throw.whenNull(acceleration, "Acceleration may not be null.");
136         Throw.whenNull(time, "Time may not be null.");
137         if (!this.extendedData.isEmpty())
138         {
139             Throw.whenNull(gtu, "GTU may not be null when extended data is part of the trajectory.");
140         }
141         if (this.size == this.x.length)
142         {
143             int cap = this.size + (this.size >> 1);
144             this.x = Arrays.copyOf(this.x, cap);
145             this.v = Arrays.copyOf(this.v, cap);
146             this.a = Arrays.copyOf(this.a, cap);
147             this.t = Arrays.copyOf(this.t, cap);
148         }
149         this.x[this.size] = (float) position.si;
150         this.v[this.size] = (float) speed.si;
151         this.a[this.size] = (float) acceleration.si;
152         this.t[this.size] = (float) time.si;
153         for (ExtendedDataType<?, ?, ?, ? super G> extendedDataType : this.extendedData.keySet())
154         {
155             appendValue(extendedDataType, gtu);
156         }
157         this.size++;
158     }
159 
160     /**
161      * Append value of the extended data type.
162      * @param extendedDataType extended data type
163      * @param gtu gtu
164      * @param <T> extended data value type
165      * @param <S> extended data storage data type
166      */
167     @SuppressWarnings("unchecked")
168     private <T, S> void appendValue(final ExtendedDataType<T, ?, S, ? super G> extendedDataType, final G gtu)
169     {
170         S in = (S) this.extendedData.get(extendedDataType);
171         S out = extendedDataType.setValue(in, this.size, extendedDataType.getValue(gtu));
172         if (in != out)
173         {
174             this.extendedData.put(extendedDataType, out);
175         }
176     }
177 
178     /**
179      * The size of the underlying data.
180      * @return size of the underlying trajectory data
181      */
182     public int size()
183     {
184         return this.size;
185     }
186 
187     /**
188      * Returns the GTU id.
189      * @return GTU id
190      */
191     public String getGtuId()
192     {
193         return this.gtuId;
194     }
195 
196     /**
197      * Returns the GTU type id.
198      * @return GTU type id
199      */
200     public String getGtuTypeId()
201     {
202         return this.gtuTypeId;
203     }
204 
205     /**
206      * Returns the position array.
207      * @return si position values.
208      */
209     public float[] getX()
210     {
211         return Arrays.copyOf(this.x, this.size);
212     }
213 
214     /**
215      * Returns the speed array.
216      * @return si speed values
217      */
218     public float[] getV()
219     {
220         return Arrays.copyOf(this.v, this.size);
221     }
222 
223     /**
224      * Returns the acceleration array.
225      * @return si acceleration values
226      */
227     public float[] getA()
228     {
229         return Arrays.copyOf(this.a, this.size);
230     }
231 
232     /**
233      * Returns the time array.
234      * @return si time values
235      */
236     public float[] getT()
237     {
238         return Arrays.copyOf(this.t, this.size);
239     }
240 
241     /**
242      * Returns the last index with a position smaller than or equal to the given position.
243      * @param position position
244      * @return last index with a position smaller than or equal to the given position
245      */
246     public int binarySearchX(final float position)
247     {
248         if (this.x[0] >= position)
249         {
250             return 0;
251         }
252         int index = Arrays.binarySearch(this.x, 0, this.size, position);
253         return index < 0 ? -index - 2 : index;
254     }
255 
256     /**
257      * Returns the last index with a time smaller than or equal to the given time.
258      * @param time time
259      * @return last index with a time smaller than or equal to the given time
260      */
261     public int binarySearchT(final float time)
262     {
263         if (this.t[0] >= time)
264         {
265             return 0;
266         }
267         int index = Arrays.binarySearch(this.t, 0, this.size, time);
268         return index < 0 ? -index - 2 : index;
269     }
270 
271     /**
272      * Returns {@code x} value of a single sample.
273      * @param index index
274      * @return {@code x} value of a single sample
275      */
276     public float getX(final int index)
277     {
278         checkSample(index);
279         return this.x[index];
280     }
281 
282     /**
283      * Returns {@code v} value of a single sample.
284      * @param index index
285      * @return {@code v} value of a single sample
286      */
287     public float getV(final int index)
288     {
289         checkSample(index);
290         return this.v[index];
291     }
292 
293     /**
294      * Returns {@code a} value of a single sample.
295      * @param index index
296      * @return {@code a} value of a single sample
297      */
298     public float getA(final int index)
299     {
300         checkSample(index);
301         return this.a[index];
302     }
303 
304     /**
305      * Returns {@code t} value of a single sample.
306      * @param index index
307      * @return {@code t} value of a single sample
308      */
309     public float getT(final int index)
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 data type from which to retrieve the data
318      * @param index 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      */
323     @SuppressWarnings("unchecked")
324     public <T, S> T getExtendedData(final ExtendedDataType<T, ?, S, ?> extendedDataType, final int index)
325     {
326         checkSample(index);
327         return extendedDataType.getStorageValue((S) this.extendedData.get(extendedDataType), index);
328     }
329 
330     /**
331      * Throws an exception if the sample index is out of bounds.
332      * @param index sample index
333      */
334     private void checkSample(final int index)
335     {
336         Throw.when(index < 0 || index >= this.size, IndexOutOfBoundsException.class, "Index is out of bounds.");
337     }
338 
339     /**
340      * Returns strongly type position array.
341      * @return strongly typed position array.
342      */
343     public FloatLengthVector getPosition()
344     {
345         return new FloatLengthVector(getX(), LengthUnit.SI);
346     }
347 
348     /**
349      * Returns strongly typed speed array.
350      * @return strongly typed speed array.
351      */
352     public FloatSpeedVector getSpeed()
353     {
354         return new FloatSpeedVector(getV(), SpeedUnit.SI);
355     }
356 
357     /**
358      * Returns strongly typed acceleration array.
359      * @return strongly typed acceleration array.
360      */
361     public FloatAccelerationVector getAcceleration()
362     {
363         return new FloatAccelerationVector(getA(), AccelerationUnit.SI);
364     }
365 
366     /**
367      * Returns strongly typed time array.
368      * @return strongly typed time array.
369      */
370     public FloatTimeVector getTime()
371     {
372         return new FloatTimeVector(getT(), TimeUnit.BASE_SECOND);
373     }
374 
375     /**
376      * Returns the length of the data.
377      * @return total length of this trajectory
378      */
379     public Length getTotalLength()
380     {
381         if (this.size < 2)
382         {
383             return Length.ZERO;
384         }
385         return new Length(this.x[this.size - 1] - this.x[0], LengthUnit.SI);
386     }
387 
388     /**
389      * Returns the total duration span.
390      * @return total duration of this trajectory
391      */
392     public Duration getTotalDuration()
393     {
394         if (this.size < 2)
395         {
396             return Duration.ZERO;
397         }
398         return new Duration(this.t[this.size - 1] - this.t[0], DurationUnit.SI);
399     }
400 
401     /**
402      * Returns whether the filter data is contained.
403      * @param filterDataType filter data type
404      * @return whether the trajectory contains the filter data of give type
405      */
406     public boolean contains(final FilterDataType<?, ?> filterDataType)
407     {
408         return this.filterData.containsKey(filterDataType);
409     }
410 
411     /**
412      * Returns the value of the filter data.
413      * @param filterDataType filter data type
414      * @param <T> class of filter data
415      * @return value of filter data
416      */
417     @SuppressWarnings("unchecked")
418     public <T> T getFilterData(final FilterDataType<T, ?> filterDataType)
419     {
420         return (T) this.filterData.get(filterDataType);
421     }
422 
423     /**
424      * Returns the included filter data types.
425      * @return included filter data types
426      */
427     public Set<FilterDataType<?, ? super G>> getFilterDataTypes()
428     {
429         return this.filterData.keySet();
430     }
431 
432     /**
433      * Returns whether ths extended data type is contained.
434      * @param extendedDataType extended data type
435      * @return whether the trajectory contains the extended data of give type
436      */
437     public boolean contains(final ExtendedDataType<?, ?, ?, ?> extendedDataType)
438     {
439         return this.extendedData.containsKey(extendedDataType);
440     }
441 
442     /**
443      * Returns the output data of the extended data type.
444      * @param extendedDataType extended data type to return
445      * @param <O> output type
446      * @param <S> storage type
447      * @return values of extended data type
448      * @throws SamplingException if the extended data type is not in the trajectory
449      */
450     @SuppressWarnings("unchecked")
451     public <O, S> O getExtendedData(final ExtendedDataType<?, O, S, ?> extendedDataType) throws SamplingException
452     {
453         Throw.when(!this.extendedData.containsKey(extendedDataType), SamplingException.class,
454                 "Extended data type %s is not in the trajectory.", extendedDataType);
455         return extendedDataType.convert((S) this.extendedData.get(extendedDataType), this.size);
456     }
457 
458     /**
459      * Returns the included extended data types.
460      * @return included extended data types
461      */
462     public Set<ExtendedDataType<?, ?, ?, ? super G>> getExtendedDataTypes()
463     {
464         return this.extendedData.keySet();
465     }
466 
467     /**
468      * Returns a space-time view of this trajectory. This is much more efficient than {@code getX()} as no array is copied. The
469      * limitation is that only distance and time (and mean speed) in the space-time view can be obtained.
470      * @return space-time view of this trajectory
471      */
472     public SpaceTimeView getSpaceTimeView()
473     {
474         if (size() < 2)
475         {
476             return new SpaceTimeView(Length.ZERO, Duration.ZERO);
477         }
478         return new SpaceTimeView(Length.instantiateSI(this.x[this.size - 1] - this.x[0]),
479                 Duration.instantiateSI(this.t[this.size - 1] - this.t[0]));
480     }
481 
482     /**
483      * Returns a space-time view of this trajectory as contained within the defined space-time region. This is much more
484      * efficient than {@code subSet()} as no trajectory is copied. The limitation is that only distance and time (and mean
485      * speed) in the space-time view can be obtained.
486      * @param startPosition start position
487      * @param endPosition end position
488      * @param startTime start time
489      * @param endTime end time
490      * @return space-time view of this trajectory
491      */
492     public SpaceTimeView getSpaceTimeView(final Length startPosition, final Length endPosition, final Time startTime,
493             final Time endTime)
494     {
495         if (size() < 2)
496         {
497             return new SpaceTimeView(Length.ZERO, Duration.ZERO);
498         }
499         Boundaries bounds = spaceBoundaries(startPosition, endPosition).intersect(timeBoundaries(startTime, endTime));
500         double xFrom;
501         double tFrom;
502         if (bounds.fFrom > 0.0)
503         {
504             xFrom = this.x[bounds.from] * (1 - bounds.fFrom) + this.x[bounds.from + 1] * bounds.fFrom;
505             tFrom = this.t[bounds.from] * (1 - bounds.fFrom) + this.t[bounds.from + 1] * bounds.fFrom;
506         }
507         else
508         {
509             xFrom = this.x[bounds.from];
510             tFrom = this.t[bounds.from];
511         }
512         double xTo;
513         double tTo;
514         if (bounds.fTo > 0.0)
515         {
516             xTo = this.x[bounds.to] * (1 - bounds.fTo) + this.x[bounds.to + 1] * bounds.fTo;
517             tTo = this.t[bounds.to] * (1 - bounds.fTo) + this.t[bounds.to + 1] * bounds.fTo;
518         }
519         else
520         {
521             xTo = this.x[bounds.to];
522             tTo = this.t[bounds.to];
523         }
524         return new SpaceTimeView(Length.instantiateSI(xTo - xFrom), Duration.instantiateSI(tTo - tFrom));
525     }
526 
527     /**
528      * Copies the trajectory but with a subset of the data. Longitudinal entry is only true if the original trajectory has true,
529      * and the subset is from the start.
530      * @param startPosition start position
531      * @param endPosition end position
532      * @return subset of the trajectory
533      * @throws NullPointerException if an input is null
534      * @throws IllegalArgumentException of minLength is smaller than maxLength
535      */
536     public Trajectory<G> subSet(final Length startPosition, final Length endPosition)
537     {
538         Throw.whenNull(startPosition, "Start position may not be null");
539         Throw.whenNull(endPosition, "End position may not be null");
540         Throw.when(startPosition.gt(endPosition), IllegalArgumentException.class,
541                 "Start position should be smaller than end position in the direction of travel");
542         if (this.size == 0)
543         {
544             return new Trajectory<>(this.gtuId, this.gtuTypeId, this.filterData, this.extendedData.keySet());
545         }
546         return subSet(spaceBoundaries(startPosition, endPosition));
547     }
548 
549     /**
550      * Copies the trajectory but with a subset of the data.
551      * @param startTime start time
552      * @param endTime end time
553      * @return subset of the trajectory
554      * @throws NullPointerException if an input is null
555      * @throws IllegalArgumentException of minTime is smaller than maxTime
556      */
557     public Trajectory<G> subSet(final Time startTime, final Time endTime)
558     {
559         Throw.whenNull(startTime, "Start time may not be null");
560         Throw.whenNull(endTime, "End time may not be null");
561         Throw.when(startTime.gt(endTime), IllegalArgumentException.class, "Start time should be smaller than end time.");
562         if (this.size == 0)
563         {
564             return new Trajectory<>(this.gtuId, this.gtuTypeId, this.filterData, this.extendedData.keySet());
565         }
566         return subSet(timeBoundaries(startTime, endTime));
567     }
568 
569     /**
570      * Copies the trajectory but with a subset of the data that is contained in the given space-time region.
571      * @param startPosition start position
572      * @param endPosition end position
573      * @param startTime start time
574      * @param endTime end time
575      * @return subset of the trajectory
576      * @throws NullPointerException if an input is null
577      * @throws IllegalArgumentException of minLength/Time is smaller than maxLength/Time
578      */
579     public Trajectory<G> subSet(final Length startPosition, final Length endPosition, final Time startTime, final Time 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 Time startTime, final Time 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 Time getTimeAtPosition(final Length position)
676     {
677         return Time.instantiateSI(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.instantiateSI(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.instantiateSI(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 Time time)
706     {
707         return Length.instantiateSI(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 Time time)
716     {
717         return Speed.instantiateSI(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 Time time)
726     {
727         return Acceleration.instantiateSI(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 }