1 package org.opentrafficsim.animation.graphs;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.LinkedHashMap;
6 import java.util.LinkedHashSet;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Optional;
10 import java.util.Set;
11
12 import org.djunits.unit.SpeedUnit;
13 import org.djunits.value.vdouble.scalar.Duration;
14 import org.djunits.value.vdouble.scalar.Frequency;
15 import org.djunits.value.vdouble.scalar.Length;
16 import org.djunits.value.vdouble.scalar.LinearDensity;
17 import org.djunits.value.vdouble.scalar.Speed;
18 import org.djutils.event.EventType;
19 import org.djutils.exceptions.Throw;
20 import org.djutils.math.means.ArithmeticMean;
21 import org.djutils.metadata.MetaData;
22 import org.djutils.metadata.ObjectDescriptor;
23 import org.opentrafficsim.animation.egtf.Converter;
24 import org.opentrafficsim.animation.egtf.DataSource;
25 import org.opentrafficsim.animation.egtf.DataStream;
26 import org.opentrafficsim.animation.egtf.Egtf;
27 import org.opentrafficsim.animation.egtf.Filter;
28 import org.opentrafficsim.animation.egtf.Quantity;
29 import org.opentrafficsim.animation.egtf.typed.TypedQuantity;
30 import org.opentrafficsim.animation.graphs.AbstractContourPlot.ContourPaintState;
31 import org.opentrafficsim.animation.graphs.GraphPath.Section;
32 import org.opentrafficsim.base.logger.Logger;
33 import org.opentrafficsim.kpi.interfaces.LaneData;
34 import org.opentrafficsim.kpi.sampling.SamplerData;
35 import org.opentrafficsim.kpi.sampling.Trajectory;
36 import org.opentrafficsim.kpi.sampling.Trajectory.SpaceTimeView;
37 import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 public class ContourDataSource extends PlotDelegate<ContourPaintState, AbstractContourPlot<?>>
53 {
54
55
56
57
58
59
60 public static final EventType GRANULARITY = new EventType("GRANULARITY",
61 new MetaData("Granularity", "Granularity changed.", new ObjectDescriptor("Axis", "Axis", Dimension.class),
62 new ObjectDescriptor("Granularity", "Granularity", Double.class)));
63
64
65 public static final EventType INTERPOLATE = new EventType("INTERPOLATE", new MetaData("Interpolate", "Interpolate changed.",
66 new ObjectDescriptor("Interpolate", "Interpolate", Boolean.class)));
67
68
69 public static final EventType SMOOTH = new EventType("SMOOTH",
70 new MetaData("Smooth", "Smooth changed.", new ObjectDescriptor("Smooth", "Smooth", Boolean.class)));
71
72
73
74
75
76
77 protected static final PlotSetting<Length> SPACE_GRANULARITIES =
78 PlotSetting.of(new double[] {10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0}, Length::ofSI, 3);
79
80
81 protected static final PlotSetting<Duration> TIME_GRANULARITIES =
82 PlotSetting.of(new double[] {1.0, 2.0, 5.0, 10.0, 20.0, 30.0, 60.0, 120.0, 300.0, 600.0}, Duration::ofSI, 3);
83
84
85 protected static final Duration DEFAULT_LOWER_TIME_BOUND = Duration.ZERO;
86
87
88
89
90
91
92 private static final int KERNEL_FACTOR = 5;
93
94
95 private static final Speed MAX_C_FREE = new Speed(80.0, SpeedUnit.KM_PER_HOUR);
96
97
98 private static final double VC_FACRTOR = 0.8;
99
100
101 private static final Speed C_CONG = new Speed(-18.0, SpeedUnit.KM_PER_HOUR);
102
103
104 private static final Speed DELTA_V = new Speed(10.0, SpeedUnit.KM_PER_HOUR);
105
106
107
108
109
110
111 private final SamplerData<?> samplerData;
112
113
114 private final GraphPath<? extends LaneData<?>> path;
115
116
117 private final Axis spaceAxis;
118
119
120 private final Axis timeAxis;
121
122
123 private final Set<ContourDataType<?>> dataTypes = new LinkedHashSet<>();
124
125
126
127
128
129
130 private float[][] distance;
131
132
133 private float[][] time;
134
135
136 private final Map<ContourAdditionalDataType<?, ?>, float[][]> additionalData = new LinkedHashMap<>();
137
138
139
140
141
142
143 private Speed cFree;
144
145
146 private Speed vc;
147
148
149 private Egtf egtf;
150
151
152 private DataStream<Speed> speedStream;
153
154
155 private DataStream<Duration> travelTimeStream;
156
157
158 private DataStream<Length> travelDistanceStream;
159
160
161 private final Quantity<Duration, double[][]> travelTimeQuantity = new Quantity<>("travel time", Converter.SI);
162
163
164 private final Quantity<Length, double[][]> travelDistanceQuantity = new Quantity<>("travel distance", Converter.SI);
165
166
167 private Map<ContourAdditionalDataType<?, ?>, DataStream<?>> additionalStreams = new LinkedHashMap<>();
168
169
170
171
172
173
174 private double toTime = 0.0;
175
176
177 private int readyItems = -1;
178
179
180 private boolean smooth = false;
181
182
183
184
185
186
187
188
189
190
191
192 public ContourDataSource(final SamplerData<?> samplerData, final GraphPath<? extends LaneData<?>> path,
193 final PlotScheduler plotScheduler)
194 {
195 this(samplerData, Duration.ofSI(1.0), path, plotScheduler, SPACE_GRANULARITIES, TIME_GRANULARITIES,
196 DEFAULT_LOWER_TIME_BOUND, AbstractPlot.DEFAULT_INITIAL_UPPER_TIME_BOUND);
197 }
198
199
200
201
202
203
204
205
206
207
208
209
210 @SuppressWarnings("parameternumber")
211 public ContourDataSource(final SamplerData<?> samplerData, final Duration delay,
212 final GraphPath<? extends LaneData<?>> path, final PlotScheduler plotScheduler,
213 final PlotSetting<Length> spaceGranularities, final PlotSetting<Duration> timeGranularities, final Duration start,
214 final Duration initialEnd)
215 {
216 super(Duration.ofSI(timeGranularities.defaultValueIndex()), delay, plotScheduler);
217 this.samplerData = samplerData;
218 this.path = path;
219 this.spaceAxis = new Axis(0.0, path.getTotalLength().si, spaceGranularities.getDefaultValue().si,
220 spaceGranularities.values().stream().mapToDouble((len) -> len.si).toArray());
221 this.timeAxis = new Axis(start.si, initialEnd.si, timeGranularities.getDefaultValue().si,
222 timeGranularities.values().stream().mapToDouble((len) -> len.si).toArray());
223
224
225 this.cFree = Speed.min(path.getSpeedLimit(), MAX_C_FREE);
226 this.vc = Speed.min(path.getSpeedLimit().times(VC_FACRTOR), MAX_C_FREE);
227 }
228
229 @Override
230 public String toString()
231 {
232 return "ContourDataSource []";
233 }
234
235
236
237
238
239
240
241
242
243 GraphPath<? extends LaneData<?>> getPath()
244 {
245 return this.path;
246 }
247
248 @Override
249 public void addPlot(final AbstractContourPlot<?> contourPlot)
250 {
251 ContourDataType<?> contourDataType = contourPlot.getContourDataType();
252 if (contourDataType instanceof ContourAdditionalDataType<?, ?> type)
253 {
254 this.additionalData.put(type, null);
255 }
256 this.dataTypes.add(contourDataType);
257 super.addPlot(contourPlot);
258 }
259
260
261
262
263
264
265 public double[] getGranularities(final Dimension dimension)
266 {
267 return dimension.getAxis(this).granularities;
268 }
269
270
271
272
273
274
275 public double getGranularity(final Dimension dimension)
276 {
277 return dimension.getAxis(this).granularity;
278 }
279
280
281
282
283
284
285 public void setGranularity(final Dimension dimension, final double granularity)
286 {
287 synchronized (this)
288 {
289 dimension.getAxis(this).setGranularity(granularity);
290 if (dimension.equals(Dimension.TIME))
291 {
292 getPlots().forEach((p) -> p.offerUpdateInterval(Duration.ofSI(granularity)));
293 }
294 invalidateTimeSpan();
295 }
296 fireEvent(GRANULARITY, new Object[] {dimension, granularity});
297 }
298
299
300
301
302
303 public void setInterpolate(final boolean interpolate)
304 {
305 boolean did = false;
306 synchronized (this)
307 {
308 if (this.timeAxis.interpolate != interpolate)
309 {
310 did = true;
311 this.timeAxis.setInterpolate(interpolate);
312 this.spaceAxis.setInterpolate(interpolate);
313 }
314 invalidateTimeSpan();
315 }
316 if (did)
317 {
318 fireEvent(INTERPOLATE, interpolate);
319 }
320 }
321
322
323
324
325
326 public void setSmooth(final boolean smooth)
327 {
328 boolean did = false;
329 synchronized (this)
330 {
331 if (this.smooth != smooth)
332 {
333 did = true;
334 this.smooth = smooth;
335 }
336 invalidateTimeSpan();
337 }
338 if (did)
339 {
340 fireEvent(SMOOTH, smooth);
341 }
342 }
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358 @Override
359 protected void calculatePaintStateUnsafe(final Duration t)
360 {
361 Throw.when(getPlots().isEmpty(), IllegalStateException.class, "ContourDataSource is used, but not by a contour plot!");
362
363
364 UpdateContext uc = snapshotAndPrepare(t);
365
366
367 resetDataOnRedo(uc);
368
369
370 configureFilter(uc);
371
372
373 ensureTimeCapacity(uc.toTimeIndex());
374
375
376 for (int j = uc.fromTimeIndex(); j <= uc.toTimeIndex(); j++)
377 {
378 if (!processTimeSlice(j, uc))
379 {
380 return;
381 }
382 }
383
384
385 applySmoothingIfNeeded(uc);
386 }
387
388
389
390
391
392
393
394 private UpdateContext snapshotAndPrepare(final Duration now)
395 {
396
397
398
399
400
401 boolean redo;
402 double timeGranularity;
403 double spaceGranularity;
404 boolean smooth0;
405 boolean interpolate0;
406 double timeKernelSize;
407 double spaceKernelSize;
408 double[] spaceTicks;
409 double[] timeTicks;
410 int fromSpaceIndex = 0;
411 int fromTimeIndex = 0;
412 int toTimeIndex;
413 double tFromEgtf = 0;
414 int skipTime = 0;
415 int tSliceFromEgtf = 0;
416 double snappedToTime;
417 synchronized (this)
418 {
419 timeGranularity = this.timeAxis.granularity;
420 spaceGranularity = this.spaceAxis.granularity;
421
422 redo = getAndResetInvalidTimeSpan();
423
424 this.toTime = (double) (timeGranularity * ((int) (now.si / timeGranularity)));
425 snappedToTime = this.toTime;
426 if (snappedToTime > this.timeAxis.maxValue)
427 {
428 this.timeAxis.setMaxValue(snappedToTime);
429 }
430
431
432 smooth0 = this.smooth && snappedToTime > timeGranularity;
433 interpolate0 = this.timeAxis.interpolate;
434
435 timeKernelSize = timeGranularity * 2 * KERNEL_FACTOR;
436 spaceKernelSize = spaceGranularity * 2 * KERNEL_FACTOR;
437 spaceTicks = Arrays.copyOf(this.spaceAxis.getTicks(), this.spaceAxis.getTicks().length);
438 timeTicks = Arrays.copyOf(this.timeAxis.getTicks(), this.timeAxis.getTicks().length);
439 if (!redo)
440 {
441
442 fromSpaceIndex = (this.readyItems + 1) % this.spaceAxis.getSliceCount();
443 fromTimeIndex = (this.readyItems + 1) / this.spaceAxis.getSliceCount();
444 }
445 toTimeIndex = ((int) (snappedToTime / timeGranularity)) - (interpolate0 ? 0 : 1);
446 if (smooth0)
447 {
448
449 tFromEgtf = this.timeAxis.getSliceValue(redo ? 0
450 : this.timeAxis.getValueSlice(this.timeAxis.getSliceValue(fromTimeIndex) - 2 * timeKernelSize));
451 tSliceFromEgtf = this.timeAxis.getValueSlice(tFromEgtf);
452
453
454
455
456
457
458
459
460 double tFromEgtf2 = this.timeAxis.getSliceValue(
461 redo ? 0 : this.timeAxis.getValueSlice(this.timeAxis.getSliceValue(fromTimeIndex) - timeKernelSize));
462 int nFromEgtf2 = this.timeAxis.getValueSlice(tFromEgtf2);
463 skipTime = nFromEgtf2 - tSliceFromEgtf;
464 }
465
466 if (redo)
467 {
468 this.readyItems = -1;
469 }
470 }
471 return new UpdateContext(redo, timeGranularity, spaceGranularity, smooth0, interpolate0, timeKernelSize,
472 spaceKernelSize, spaceTicks, timeTicks, fromSpaceIndex, fromTimeIndex, toTimeIndex, tFromEgtf, tSliceFromEgtf,
473 skipTime, snappedToTime);
474 }
475
476
477
478
479
480 private void resetDataOnRedo(final UpdateContext uc)
481 {
482 if (!uc.redo())
483 {
484 return;
485 }
486 int nSpace = uc.spaceTicks().length - 1;
487 int nTime = uc.timeTicks().length - 1;
488 this.distance = new float[nSpace][nTime];
489 this.time = new float[nSpace][nTime];
490 for (ContourAdditionalDataType<?, ?> type : this.additionalData.keySet())
491 {
492 this.additionalData.put(type, new float[nSpace][nTime]);
493 }
494 this.egtf = null;
495 }
496
497
498
499
500
501 private void configureFilter(final UpdateContext uc)
502 {
503 if (uc.smooth() && this.egtf == null)
504 {
505 setupFilter(uc.timeGranularity(), uc.timeKernelSize(), uc.spaceKernelSize());
506 }
507 else if (!uc.smooth())
508 {
509
510 this.egtf = null;
511 this.speedStream = null;
512 this.travelTimeStream = null;
513 this.travelDistanceStream = null;
514 this.additionalStreams.clear();
515 }
516 }
517
518
519
520
521
522
523
524 private void setupFilter(final double timeGranularity, final double timeKernelSize, final double spaceKernelSize)
525 {
526
527 this.egtf = new Egtf(C_CONG.si, this.cFree.si, DELTA_V.si, this.vc.si);
528
529
530 DataSource generic = this.egtf.getDataSource("generic");
531 generic.addStream(TypedQuantity.SPEED, Speed.ofSI(1.0), Speed.ofSI(1.0));
532 generic.addStreamSI(this.travelTimeQuantity, 1.0, 1.0);
533 generic.addStreamSI(this.travelDistanceQuantity, 1.0, 1.0);
534 this.speedStream = generic.getStream(TypedQuantity.SPEED);
535 this.travelTimeStream = generic.getStream(this.travelTimeQuantity);
536 this.travelDistanceStream = generic.getStream(this.travelDistanceQuantity);
537 for (ContourAdditionalDataType<?, ?> contourDataType : this.additionalData.keySet())
538 {
539 this.additionalStreams.put(contourDataType, generic.addStreamSI(contourDataType.getQuantity(), 1.0, 1.0));
540 }
541
542
543 this.egtf.setKernelSI(spaceKernelSize / KERNEL_FACTOR, timeKernelSize / KERNEL_FACTOR, spaceKernelSize,
544 timeGranularity);
545
546
547 this.egtf.addListener((event) ->
548 {
549
550 if (isInvalidTimeSpan())
551 {
552
553 Logger.ots().debug("Interrupting EGTF");
554 event.interrupt();
555 }
556 });
557 }
558
559
560
561
562
563 private void ensureTimeCapacity(final int toTimeIndex)
564 {
565 for (int i = 0; i < this.distance.length; i++)
566 {
567 this.distance[i] = GraphUtil.ensureCapacity(this.distance[i], toTimeIndex + 1);
568 this.time[i] = GraphUtil.ensureCapacity(this.time[i], toTimeIndex + 1);
569 for (float[][] add : this.additionalData.values())
570 {
571 add[i] = GraphUtil.ensureCapacity(add[i], toTimeIndex + 1);
572 }
573 }
574 }
575
576
577
578
579
580
581
582 private boolean processTimeSlice(final int j, final UpdateContext uc)
583 {
584 final Duration tFrom = Duration.ofSI(uc.timeTicks()[j]);
585 final Duration tTo = Duration.ofSI(uc.timeTicks()[j + 1]);
586
587 int fromSpaceIndex = uc.fromSpaceIndex();
588
589 for (int i = fromSpaceIndex; i < uc.spaceTicks().length - 1; i++)
590 {
591 if (handleInterpolationEdges(i, j, uc.interpolate()))
592 {
593 this.readyItems++;
594 if (isInvalidTimeSpan())
595 {
596 return false;
597 }
598 continue;
599 }
600
601
602 fromSpaceIndex = 0;
603
604
605 Length xFrom = Length.ofSI(uc.spaceTicks()[i]);
606 Length xTo = Length.ofSI(Math.min(uc.spaceTicks()[i + 1], this.path.getTotalLength().si));
607 CellWindow window = new CellWindow(i, j, xFrom, xTo, tFrom, tTo);
608
609
610 CellTotals totals = aggregateCell(window);
611
612
613 this.distance[i][j] = (float) totals.distance();
614 this.time[i][j] = (float) totals.time();
615 for (ContourAdditionalDataType<?, ?> type : this.additionalData.keySet())
616 {
617 this.additionalData.get(type)[i][j] = finalizeAdditional(totals.additional(), type);
618 }
619
620 feedFilterIfNeeded(window, totals, uc);
621
622 if (isInvalidTimeSpan())
623 {
624 return false;
625 }
626 this.readyItems++;
627 }
628
629
630 offerPaintState(uc);
631
632 return true;
633 }
634
635
636
637
638
639
640
641
642 private boolean handleInterpolationEdges(final int i, final int j, final boolean interpolate)
643 {
644 if ((j == 0 || i == 0) && interpolate)
645 {
646 this.distance[i][j] = Float.NaN;
647 this.time[i][j] = Float.NaN;
648 for (ContourAdditionalDataType<?, ?> type : this.additionalData.keySet())
649 {
650 this.additionalData.get(type)[i][j] = Float.NaN;
651 }
652 return true;
653 }
654 return false;
655 }
656
657
658
659
660
661
662 private CellTotals aggregateCell(final CellWindow cell)
663 {
664 double totalDistance = 0.0;
665 double totalTime = 0.0;
666
667 Map<ContourAdditionalDataType<?, ?>, Object> additionalIntermediate = new LinkedHashMap<>();
668 for (ContourAdditionalDataType<?, ?> type : this.additionalData.keySet())
669 {
670 additionalIntermediate.put(type, type.identity());
671 }
672
673 int nSeries = this.path.getNumberOfSeries();
674 for (int series = 0; series < nSeries; series++)
675 {
676
677 List<TrajectoryGroup<?>> groups = groupsForSeries(series);
678
679
680 List<TrajectoryGroup<?>> included = new ArrayList<>();
681 List<Length> xStart = new ArrayList<>();
682 List<Length> xEnd = new ArrayList<>();
683 includedGroupsForCell(groups, cell, included, xStart, xEnd);
684
685
686 DistTime distTime = accumulateDistanceAndTime(cell, included, xStart, xEnd);
687 totalDistance += distTime.distance();
688 totalTime += distTime.time();
689 for (ContourAdditionalDataType<?, ?> type : this.additionalData.keySet())
690 {
691 addAdditional(additionalIntermediate, type, included, xStart, xEnd, cell.tFrom(), cell.tTo());
692 }
693 }
694
695
696 double length = cell.xTo().si - cell.xFrom().si;
697 double norm = this.spaceAxis.granularity / length / nSeries;
698 totalDistance *= norm;
699 totalTime *= norm;
700
701 return new CellTotals(totalDistance, totalTime, additionalIntermediate);
702 }
703
704
705
706
707
708
709 private List<TrajectoryGroup<?>> groupsForSeries(final int series)
710 {
711 List<TrajectoryGroup<?>> groups = new ArrayList<>();
712 for (Section<? extends LaneData<?>> section : getPath().getSections())
713 {
714 TrajectoryGroup<?> group = this.samplerData.getTrajectoryGroup(section.getSource(series)).orElse(null);
715 if (group == null)
716 {
717 Logger.ots().error("trajectoryGroup {} is null", series);
718 }
719 groups.add(group);
720 }
721 return groups;
722 }
723
724
725
726
727
728
729
730
731
732 private void includedGroupsForCell(final List<TrajectoryGroup<?>> trajectories, final CellWindow cell,
733 final List<TrajectoryGroup<?>> included, final List<Length> xStart, final List<Length> xEnd)
734 {
735 for (int k = 0; k < trajectories.size(); k++)
736 {
737 TrajectoryGroup<?> tg = trajectories.get(k);
738 LaneData<?> lane = tg.getLane();
739 Length startDistance = this.path.getStartDistance(this.path.get(k));
740 double secStart = startDistance.si;
741 double secEnd = secStart + this.path.get(k).length().si;
742
743 if (secEnd > cell.xFrom().si && secStart < cell.xTo().si)
744 {
745 included.add(tg);
746 double scale = this.path.get(k).length().si / lane.getLength().si;
747 xStart.add(Length.max(cell.xFrom().minus(startDistance).divide(scale), Length.ZERO));
748 xEnd.add(Length.min(cell.xTo().minus(startDistance).divide(scale), tg.getLane().getLength()));
749 }
750 }
751 }
752
753
754
755
756
757
758
759
760
761 private DistTime accumulateDistanceAndTime(final CellWindow cell, final List<TrajectoryGroup<?>> included,
762 final List<Length> xStart, final List<Length> xEnd)
763 {
764 double dist = 0.0;
765 double tim = 0.0;
766 for (int k = 0; k < included.size(); k++)
767 {
768 TrajectoryGroup<?> tg = included.get(k);
769 for (Trajectory<?> tr : tg.getTrajectories())
770 {
771 if (!GraphUtil.considerTrajectory(tr, cell.tFrom(), cell.tTo()))
772 {
773 continue;
774 }
775 try
776 {
777 SpaceTimeView v = tr.getSpaceTimeView(xStart.get(k), xEnd.get(k), cell.tFrom(), cell.tTo());
778 dist += v.distance().si;
779 tim += v.time().si;
780 }
781 catch (IllegalArgumentException ex)
782 {
783 Logger.ots().debug(ex, "Unable to generate space-time view x={}..{}, t={}..{}", xStart.get(k), xEnd.get(k),
784 cell.tFrom(), cell.tTo());
785 }
786 }
787 }
788 return new DistTime(dist, tim);
789 }
790
791
792
793
794
795
796
797
798
799
800
801
802 @SuppressWarnings("unchecked")
803 private <I> void addAdditional(final Map<ContourAdditionalDataType<?, ?>, Object> additionalIntermediate,
804 final ContourAdditionalDataType<?, ?> contourDataType, final List<TrajectoryGroup<?>> included,
805 final List<Length> xStart, final List<Length> xEnd, final Duration tFrom, final Duration tTo)
806 {
807 additionalIntermediate.put(contourDataType, ((ContourAdditionalDataType<?, I>) contourDataType)
808 .processSeries((I) additionalIntermediate.get(contourDataType), included, xStart, xEnd, tFrom, tTo));
809 }
810
811
812
813
814
815
816
817
818 @SuppressWarnings("unchecked")
819 private <I> float finalizeAdditional(final Map<ContourAdditionalDataType<?, ?>, Object> additionalIntermediate,
820 final ContourAdditionalDataType<?, ?> contourDataType)
821 {
822 return ((ContourAdditionalDataType<?, I>) contourDataType).finalize((I) additionalIntermediate.get(contourDataType))
823 .floatValue();
824 }
825
826
827
828
829
830
831
832 private void feedFilterIfNeeded(final CellWindow cell, final CellTotals totals, final UpdateContext uc)
833 {
834 if (!uc.smooth())
835 {
836 return;
837 }
838 double xDat = (cell.xFrom().si + cell.xTo().si) / 2.0;
839 double tDat = (cell.tFrom().si + cell.tTo().si) / 2.0;
840
841 if (this.path.isCircular())
842 {
843 double pathLength = this.path.getTotalLength().si;
844 if (xDat < uc.spaceKernelSize())
845 {
846 setDataInEgtf(pathLength + xDat, tDat, totals.distance(), totals.time(), cell.i(), cell.j());
847 }
848 if (xDat > pathLength - uc.spaceKernelSize())
849 {
850 setDataInEgtf(xDat - pathLength, tDat, totals.distance(), totals.time(), cell.i(), cell.j());
851 }
852 }
853 setDataInEgtf(xDat, tDat, totals.distance(), totals.time(), cell.i(), cell.j());
854 }
855
856
857
858
859
860
861
862
863
864
865 private void setDataInEgtf(final double xDat, final double tDat, final double totalDistance, final double totalTime,
866 final int i, final int j)
867 {
868
869 this.egtf.addPointDataSI(this.speedStream, xDat, tDat, totalDistance / totalTime);
870 this.egtf.addPointDataSI(this.travelDistanceStream, xDat, tDat, totalDistance);
871 this.egtf.addPointDataSI(this.travelTimeStream, xDat, tDat, totalTime);
872 for (ContourAdditionalDataType<?, ?> contourDataType : this.additionalStreams.keySet())
873 {
874 this.egtf.addPointDataSI(this.additionalStreams.get(contourDataType), xDat, tDat,
875 this.additionalData.get(contourDataType)[i][j]);
876 }
877 }
878
879
880
881
882
883 private void applySmoothingIfNeeded(final UpdateContext uc)
884 {
885 if (!uc.smooth())
886 {
887 return;
888 }
889
890
891 Set<Quantity<?, ?>> quantities = new LinkedHashSet<>();
892 quantities.add(this.travelDistanceQuantity);
893 quantities.add(this.travelTimeQuantity);
894 this.additionalData.keySet().forEach((type) -> quantities.add(type.getQuantity()));
895
896
897 int skipSpace = this.path.isCircular() ? (int) Math.ceil(uc.spaceKernelSize() / uc.spaceGranularity()) : 0;
898
899
900 double tTo = uc.snappedToTime();
901 if (tTo <= uc.tSliceFromEgtf())
902 {
903 return;
904 }
905 Optional<Filter> filter = this.egtf.filterFastSI(uc.spaceTicks()[0] + (0.5 - skipSpace) * uc.spaceGranularity(),
906 uc.spaceGranularity(), uc.spaceTicks()[0] + (-1.5 + uc.spaceTicks().length + skipSpace) * uc.spaceGranularity(),
907 uc.tSliceFromEgtf(), uc.timeGranularity(), tTo, quantities.toArray(new Quantity<?, ?>[0]));
908 if (filter.isEmpty())
909 {
910 return;
911 }
912
913
914 overwriteSmoothed(this.distance, uc.nFromEgtf(), filter.get().getSI(this.travelDistanceQuantity), uc.skipTime(),
915 skipSpace);
916 overwriteSmoothed(this.time, uc.nFromEgtf(), filter.get().getSI(this.travelTimeQuantity), uc.skipTime(), skipSpace);
917 for (ContourAdditionalDataType<?, ?> type : this.additionalData.keySet())
918 {
919 overwriteSmoothed(this.additionalData.get(type), uc.nFromEgtf(), filter.get().getSI(type.getQuantity()),
920 uc.skipTime(), skipSpace);
921 }
922
923
924 offerPaintState(uc);
925 }
926
927
928
929
930
931 private void offerPaintState(final UpdateContext uc)
932 {
933 final int nTimeSlices = uc.timeTicks().length - 1;
934 final int nSpaceSlices = uc.spaceTicks().length - 1;
935 final int n = nTimeSlices * nSpaceSlices;
936 final Map<ContourDataType<?>, float[]> dataMap = new LinkedHashMap<>();
937 final double area = uc.timeGranularity() * uc.spaceGranularity();
938 final int limit = Math.min(this.readyItems + 1, n);
939
940 for (ContourDataType<?> dataType : this.dataTypes)
941 {
942 float[] data = new float[n];
943 int i = 0;
944 if (dataType instanceof ContourEdieDataType edieType)
945 {
946 OUTER: for (int timeSlice = 0; timeSlice < nTimeSlices; timeSlice++)
947 {
948 for (int spaceSlice = 0; spaceSlice < nSpaceSlices; spaceSlice++, i++)
949 {
950 if (i == limit)
951 {
952 break OUTER;
953 }
954 else
955 {
956 data[i] = (float) edieType.calculate(this.time[spaceSlice][timeSlice],
957 this.distance[spaceSlice][timeSlice], area);
958 }
959 }
960 }
961 }
962 else
963 {
964 ContourAdditionalDataType<?, ?> additionalType = (ContourAdditionalDataType<?, ?>) dataType;
965 float[][] matrix = this.additionalData.get(dataType);
966 float scale = (float) (additionalType.normalize() ? area : 1.0);
967 OUTER: for (int timeSlice = 0; timeSlice < nTimeSlices; timeSlice++)
968 {
969 for (int spaceSlice = 0; spaceSlice < nSpaceSlices; spaceSlice++, i++)
970 {
971 if (i == limit)
972 {
973 break OUTER;
974 }
975 else
976 {
977 data[i] = matrix[i % nSpaceSlices][i / nSpaceSlices] / scale;
978 }
979 }
980 }
981 }
982 if (i < n - 1)
983 {
984 Arrays.fill(data, i, n, Float.NaN);
985 }
986 dataMap.put(dataType, data);
987 }
988
989 for (AbstractContourPlot<?> plot : getPlots())
990 {
991 plot.offerPaintState(new ContourPaintState(dataMap.get(plot.getContourDataType()), uc.spaceGranularity(),
992 uc.timeGranularity(), nSpaceSlices, uc.interpolate(), Duration.ofSI(uc.snappedToTime())));
993 }
994 }
995
996
997
998
999
1000
1001
1002
1003
1004 private void overwriteSmoothed(final float[][] raw, final int rawCol, final double[][] smoothed, final int skipTime,
1005 final int skipSpace)
1006 {
1007 for (int i = 0; i < raw.length; i++)
1008 {
1009 int ii = i + skipSpace;
1010
1011 for (int j = skipTime; j < smoothed[ii].length; j++)
1012 {
1013 raw[i][j + rawCol] = (float) smoothed[ii][j];
1014 }
1015 }
1016 }
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041 private record UpdateContext(boolean redo, double timeGranularity, double spaceGranularity, boolean smooth,
1042 boolean interpolate, double timeKernelSize, double spaceKernelSize, double[] spaceTicks, double[] timeTicks,
1043 int fromSpaceIndex, int fromTimeIndex, int toTimeIndex, double tSliceFromEgtf, int nFromEgtf, int skipTime,
1044 double snappedToTime)
1045 {
1046 }
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057 private record CellWindow(int i, int j, Length xFrom, Length xTo, Duration tFrom, Duration tTo)
1058 {
1059 }
1060
1061
1062
1063
1064
1065
1066
1067 private record CellTotals(double distance, double time, Map<ContourAdditionalDataType<?, ?>, Object> additional)
1068 {
1069 }
1070
1071
1072
1073
1074
1075
1076 private record DistTime(double distance, double time)
1077 {
1078 }
1079
1080
1081
1082
1083 public enum Dimension
1084 {
1085
1086 DISTANCE
1087 {
1088 @Override
1089 protected Axis getAxis(final ContourDataSource dataPool)
1090 {
1091 return dataPool.spaceAxis;
1092 }
1093 },
1094
1095
1096 TIME
1097 {
1098 @Override
1099 protected Axis getAxis(final ContourDataSource dataPool)
1100 {
1101 return dataPool.timeAxis;
1102 }
1103 };
1104
1105
1106
1107
1108
1109
1110 protected abstract Axis getAxis(ContourDataSource dataPool);
1111 }
1112
1113
1114
1115
1116 static class Axis
1117 {
1118
1119 private final double minValue;
1120
1121
1122 private double maxValue;
1123
1124
1125 private double granularity;
1126
1127
1128 private final double[] granularities;
1129
1130
1131 private boolean interpolate = true;
1132
1133
1134 private double[] ticks;
1135
1136
1137
1138
1139
1140
1141
1142
1143 Axis(final double minValue, final double maxValue, final double granularity, final double[] granularities)
1144 {
1145 this.minValue = minValue;
1146 this.maxValue = maxValue;
1147 this.granularity = granularity;
1148 this.granularities = granularities;
1149 }
1150
1151
1152
1153
1154
1155 void setMaxValue(final double maxValue)
1156 {
1157 if (this.maxValue != maxValue)
1158 {
1159 this.maxValue = maxValue;
1160 this.ticks = null;
1161 }
1162 }
1163
1164
1165
1166
1167
1168 void setGranularity(final double granularity)
1169 {
1170 if (this.granularity != granularity)
1171 {
1172 this.granularity = granularity;
1173 this.ticks = null;
1174 }
1175 }
1176
1177
1178
1179
1180
1181 double[] getTicks()
1182 {
1183 if (this.ticks == null)
1184 {
1185 int n = getSliceCount() + 1;
1186 this.ticks = new double[n];
1187 int di = this.interpolate ? 1 : 0;
1188 for (int i = 0; i < n; i++)
1189 {
1190 if (i == n - 1)
1191 {
1192 this.ticks[i] = Math.min((i - di) * this.granularity, this.maxValue);
1193 }
1194 else
1195 {
1196 this.ticks[i] = (i - di) * this.granularity;
1197 }
1198 }
1199 }
1200 return this.ticks;
1201 }
1202
1203
1204
1205
1206
1207 int getSliceCount()
1208 {
1209 return (int) Math.ceil((this.maxValue - this.minValue) / this.granularity) + (this.interpolate ? 1 : 0);
1210 }
1211
1212
1213
1214
1215
1216
1217 double getSliceValue(final int slice)
1218 {
1219 return this.minValue + (0.5 + slice - (this.interpolate ? 1 : 0)) * this.granularity;
1220 }
1221
1222
1223
1224
1225
1226
1227 int getValueSlice(final double value)
1228 {
1229 getTicks();
1230 if (value > this.ticks[this.ticks.length - 1])
1231 {
1232 return this.ticks.length - 1;
1233 }
1234 int i = 0;
1235 while (i < this.ticks.length - 1 && this.ticks[i + 1] < value + 1e-9)
1236 {
1237 i++;
1238 }
1239 return i;
1240 }
1241
1242
1243
1244
1245
1246 void setInterpolate(final boolean interpolate)
1247 {
1248 if (this.interpolate != interpolate)
1249 {
1250 this.interpolate = interpolate;
1251 this.ticks = null;
1252 }
1253 }
1254
1255
1256
1257
1258
1259 public boolean isInterpolate()
1260 {
1261 return this.interpolate;
1262 }
1263
1264 @Override
1265 public String toString()
1266 {
1267 return "Axis [minValue=" + this.minValue + ", maxValue=" + this.maxValue + ", granularity=" + this.granularity
1268 + ", granularities=" + Arrays.toString(this.granularities) + ", interpolate=" + this.interpolate
1269 + ", ticks=" + Arrays.toString(this.ticks) + "]";
1270 }
1271
1272 }
1273
1274
1275
1276
1277
1278 public sealed interface ContourDataType<Z extends Number> permits ContourEdieDataType, ContourAdditionalDataType
1279 {
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289 static void weighted(final float[] values, final float[] weightDimension, final ArithmeticMean<Double, Double> mean)
1290 {
1291 for (int i = 0; i < values.length - 1; i++)
1292 {
1293 mean.add((double) values[i], (double) (weightDimension[i + 1] - weightDimension[i]));
1294 }
1295 }
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305 static void weightedNaN(final float[] values, final float[] weightDimension, final ArithmeticMean<Double, Double> mean)
1306 {
1307 for (int i = 0; i < values.length - 1; i++)
1308 {
1309 if (!Float.isNaN(values[i]))
1310 {
1311 mean.add((double) values[i], (double) (weightDimension[i + 1] - weightDimension[i]));
1312 }
1313 }
1314 }
1315
1316 }
1317
1318
1319
1320
1321
1322
1323 public non-sealed interface ContourEdieDataType<Z extends Number> extends ContourDataType<Z>
1324 {
1325
1326
1327 ContourEdieDataType<Frequency> FLOW = new ContourEdieDataType<>()
1328 {
1329 @Override
1330 public double calculate(final double totalTime, final double totalDistance, final double area)
1331 {
1332 return totalDistance / area;
1333 }
1334 };
1335
1336
1337 ContourEdieDataType<LinearDensity> DENSITY = new ContourEdieDataType<>()
1338 {
1339 @Override
1340 public double calculate(final double totalTime, final double totalDistance, final double area)
1341 {
1342 return totalTime / area;
1343 }
1344 };
1345
1346
1347 ContourEdieDataType<Speed> SPEED = new ContourEdieDataType<>()
1348 {
1349 @Override
1350 public double calculate(final double totalTime, final double totalDistance, final double area)
1351 {
1352 return totalDistance / totalTime;
1353 }
1354 };
1355
1356
1357
1358
1359
1360
1361
1362
1363 double calculate(double totalTime, double totalDistance, double area);
1364
1365 }
1366
1367
1368
1369
1370
1371
1372
1373 public non-sealed interface ContourAdditionalDataType<Z extends Number, I> extends ContourDataType<Z>
1374 {
1375
1376
1377
1378
1379
1380 I identity();
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392 I processSeries(I intermediate, List<TrajectoryGroup<?>> trajectories, List<Length> xFrom, List<Length> xTo,
1393 Duration tFrom, Duration tTo);
1394
1395
1396
1397
1398
1399
1400 Z finalize(I intermediate);
1401
1402
1403
1404
1405
1406 Quantity<Z, ?> getQuantity();
1407
1408
1409
1410
1411
1412 boolean normalize();
1413
1414 }
1415
1416 }