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