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
29
30
31
32
33
34
35
36
37
38
39 public final class Trajectory<G extends GtuData>
40 {
41
42
43 private static final int DEFAULT_CAPACITY = 10;
44
45
46 private int size = 0;
47
48
49
50
51
52 private float[] x = new float[DEFAULT_CAPACITY];
53
54
55 private float[] v = new float[DEFAULT_CAPACITY];
56
57
58 private float[] a = new float[DEFAULT_CAPACITY];
59
60
61 private float[] t = new float[DEFAULT_CAPACITY];
62
63
64 private final String gtuId;
65
66
67 private final String gtuTypeId;
68
69
70 private final Map<FilterDataType<?, ? super G>, Object> filterData = new LinkedHashMap<>();
71
72
73 private final Map<ExtendedDataType<?, ?, ?, ? super G>, Object> extendedData = new LinkedHashMap<>();
74
75
76
77
78
79
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
89
90
91
92
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
112
113
114
115
116
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
125
126
127
128
129
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
162
163
164
165
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
180
181
182 public int size()
183 {
184 return this.size;
185 }
186
187
188
189
190
191 public String getGtuId()
192 {
193 return this.gtuId;
194 }
195
196
197
198
199
200 public String getGtuTypeId()
201 {
202 return this.gtuTypeId;
203 }
204
205
206
207
208
209 public float[] getX()
210 {
211 return Arrays.copyOf(this.x, this.size);
212 }
213
214
215
216
217
218 public float[] getV()
219 {
220 return Arrays.copyOf(this.v, this.size);
221 }
222
223
224
225
226
227 public float[] getA()
228 {
229 return Arrays.copyOf(this.a, this.size);
230 }
231
232
233
234
235
236 public float[] getT()
237 {
238 return Arrays.copyOf(this.t, this.size);
239 }
240
241
242
243
244
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
258
259
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
273
274
275
276 public float getX(final int index)
277 {
278 checkSample(index);
279 return this.x[index];
280 }
281
282
283
284
285
286
287 public float getV(final int index)
288 {
289 checkSample(index);
290 return this.v[index];
291 }
292
293
294
295
296
297
298 public float getA(final int index)
299 {
300 checkSample(index);
301 return this.a[index];
302 }
303
304
305
306
307
308
309 public float getT(final int index)
310 {
311 checkSample(index);
312 return this.t[index];
313 }
314
315
316
317
318
319
320
321
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
332
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
341
342
343 public FloatLengthVector getPosition()
344 {
345 return new FloatLengthVector(getX(), LengthUnit.SI);
346 }
347
348
349
350
351
352 public FloatSpeedVector getSpeed()
353 {
354 return new FloatSpeedVector(getV(), SpeedUnit.SI);
355 }
356
357
358
359
360
361 public FloatAccelerationVector getAcceleration()
362 {
363 return new FloatAccelerationVector(getA(), AccelerationUnit.SI);
364 }
365
366
367
368
369
370 public FloatTimeVector getTime()
371 {
372 return new FloatTimeVector(getT(), TimeUnit.BASE_SECOND);
373 }
374
375
376
377
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
390
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
403
404
405
406 public boolean contains(final FilterDataType<?, ?> filterDataType)
407 {
408 return this.filterData.containsKey(filterDataType);
409 }
410
411
412
413
414
415
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
425
426
427 public Set<FilterDataType<?, ? super G>> getFilterDataTypes()
428 {
429 return this.filterData.keySet();
430 }
431
432
433
434
435
436
437 public boolean contains(final ExtendedDataType<?, ?, ?, ?> extendedDataType)
438 {
439 return this.extendedData.containsKey(extendedDataType);
440 }
441
442
443
444
445
446
447
448
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
460
461
462 public Set<ExtendedDataType<?, ?, ?, ? super G>> getExtendedDataTypes()
463 {
464 return this.extendedData.keySet();
465 }
466
467
468
469
470
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
484
485
486
487
488
489
490
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
529
530
531
532
533
534
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
551
552
553
554
555
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
571
572
573
574
575
576
577
578
579 public Trajectory<G> subSet(final Length startPosition, final Length endPosition, final Time startTime, final Time endTime)
580 {
581
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
598
599
600
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
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
618
619
620
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
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
638
639
640
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
655
656
657
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
672
673
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
682
683
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
692
693
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
702
703
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
712
713
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
722
723
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
732
733
734
735
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)
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
862
863
864
865 private record Boundary(int index, double fraction)
866 {
867
868
869
870
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
894
895
896
897
898
899 private record Boundaries(int from, double fFrom, int to, double fTo)
900 {
901
902
903
904
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);
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
950
951
952
953 public record SpaceTimeView(Length distance, Duration time)
954 {
955
956
957
958
959 public Speed speed()
960 {
961 return this.distance.divide(this.time);
962 }
963 }
964
965 }