1 package org.opentrafficsim.draw.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.Set;
10
11 import org.djunits.unit.SpeedUnit;
12 import org.djunits.value.vdouble.scalar.Duration;
13 import org.djunits.value.vdouble.scalar.Length;
14 import org.djunits.value.vdouble.scalar.Speed;
15 import org.djunits.value.vdouble.scalar.Time;
16 import org.djutils.exceptions.Throw;
17 import org.djutils.logger.CategoryLogger;
18 import org.opentrafficsim.core.egtf.Converter;
19 import org.opentrafficsim.core.egtf.DataSource;
20 import org.opentrafficsim.core.egtf.DataStream;
21 import org.opentrafficsim.core.egtf.Egtf;
22 import org.opentrafficsim.core.egtf.EgtfEvent;
23 import org.opentrafficsim.core.egtf.EgtfListener;
24 import org.opentrafficsim.core.egtf.Filter;
25 import org.opentrafficsim.core.egtf.Quantity;
26 import org.opentrafficsim.core.egtf.typed.TypedQuantity;
27 import org.opentrafficsim.draw.graphs.GraphPath.Section;
28 import org.opentrafficsim.kpi.interfaces.LaneData;
29 import org.opentrafficsim.kpi.sampling.SamplerData;
30 import org.opentrafficsim.kpi.sampling.Trajectory;
31 import org.opentrafficsim.kpi.sampling.Trajectory.SpaceTimeView;
32 import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
33
34 /**
35 * Class that contains data for contour plots. One data source can be shared between contour plots, in which case the
36 * granularity, path, sampler, update interval, and whether the data is smoothed (EGTF) are equal between the plots.
37 * <p>
38 * By default the source contains traveled time and traveled distance per cell.
39 * <p>
40 * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
41 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
42 * </p>
43 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
44 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
45 * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
46 */
47 public class ContourDataSource
48 {
49
50 // *************************
51 // *** GLOBAL PROPERTIES ***
52 // *************************
53
54 /** Space granularity values. */
55 protected static final double[] DEFAULT_SPACE_GRANULARITIES = {10, 20, 50, 100, 200, 500, 1000};
56
57 /** Index of the initial space granularity. */
58 protected static final int DEFAULT_SPACE_GRANULARITY_INDEX = 3;
59
60 /** Time granularity values. */
61 protected static final double[] DEFAULT_TIME_GRANULARITIES = {1, 2, 5, 10, 20, 30, 60, 120, 300, 600};
62
63 /** Index of the initial time granularity. */
64 protected static final int DEFAULT_TIME_GRANULARITY_INDEX = 3;
65
66 /** Initial lower bound for the time scale. */
67 protected static final Time DEFAULT_LOWER_TIME_BOUND = Time.ZERO;
68
69 /**
70 * Total kernel size relative to sigma and tau. This factor is determined through -log(1 - p) with p ~= 99%. This means that
71 * the cumulative exponential distribution has 99% at 5 times sigma or tau. Note that due to a coordinate change in the
72 * Adaptive Smoothing Method, the actual cumulative distribution is slightly different. Hence, this is just a heuristic.
73 */
74 private static final int KERNEL_FACTOR = 5;
75
76 /** Spatial kernel size. Larger value may be used when using a large granularity. */
77 private static final Length SIGMA = Length.instantiateSI(300);
78
79 /** Temporal kernel size. Larger value may be used when using a large granularity. */
80 private static final Duration TAU = Duration.instantiateSI(30);
81
82 /** Maximum free flow propagation speed. */
83 private static final Speed MAX_C_FREE = new Speed(80.0, SpeedUnit.KM_PER_HOUR);
84
85 /** Factor on speed limit to determine vc, the flip over speed between congestion and free flow. */
86 private static final double VC_FACRTOR = 0.8;
87
88 /** Congestion propagation speed. */
89 private static final Speed C_CONG = new Speed(-18.0, SpeedUnit.KM_PER_HOUR);
90
91 /** Delta v, speed transition region around threshold. */
92 private static final Speed DELTA_V = new Speed(10.0, SpeedUnit.KM_PER_HOUR);
93
94 // *****************************
95 // *** CONTEXTUAL PROPERTIES ***
96 // *****************************
97
98 /** Sampler data. */
99 private final SamplerData<?> samplerData;
100
101 /** Update interval. */
102 private final Duration updateInterval;
103
104 /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */
105 private final Duration delay;
106
107 /** Path. */
108 private final GraphPath<? extends LaneData> path;
109
110 /** Space axis. */
111 final Axis spaceAxis;
112
113 /** Time axis. */
114 final Axis timeAxis;
115
116 /** Registered plots. */
117 private Set<AbstractContourPlot<?>> plots = new LinkedHashSet<>();
118
119 // *****************
120 // *** PLOT DATA ***
121 // *****************
122
123 /** Total distance traveled per cell. */
124 private float[][] distance;
125
126 /** Total time traveled per cell. */
127 private float[][] time;
128
129 /** Data of other types. */
130 private final Map<ContourDataType<?, ?>, float[][]> additionalData = new LinkedHashMap<>();
131
132 // ****************************
133 // *** SMOOTHING PROPERTIES ***
134 // ****************************
135
136 /** Free flow propagation speed. */
137 private Speed cFree;
138
139 /** Flip-over speed between congestion and free flow. */
140 private Speed vc;
141
142 /** Smoothing filter. */
143 private Egtf egtf;
144
145 /** Data stream for speed. */
146 private DataStream<Speed> speedStream;
147
148 /** Data stream for travel time. */
149 private DataStream<Duration> travelTimeStream;
150
151 /** Data stream for travel distance. */
152 private DataStream<Length> travelDistanceStream;
153
154 /** Quantity for travel time. */
155 private final Quantity<Duration, double[][]> travelTimeQuantity = new Quantity<>("travel time", Converter.SI);
156
157 /** Quantity for travel distance. */
158 private final Quantity<Length, double[][]> travelDistanceQuantity = new Quantity<>("travel distance", Converter.SI);
159
160 /** Data streams for any additional data. */
161 private Map<ContourDataType<?, ?>, DataStream<?>> additionalStreams = new LinkedHashMap<>();
162
163 // *****************************
164 // *** CONTINUITY PROPERTIES ***
165 // *****************************
166
167 /** Updater for update times. */
168 private final GraphUpdater<Time> graphUpdater;
169
170 /** Whether any command since or during the last update asks for a complete redo. */
171 private boolean redo = true;
172
173 /** Time up to which to determine data. This is a multiple of the update interval, which is now, or recent on a redo. */
174 private Time toTime;
175
176 /** Number of items that are ready. To return NaN values if not ready, and for operations between consecutive updates. */
177 private int readyItems = -1;
178
179 /** Selected space granularity, to be set and taken on the next update. */
180 private Double desiredSpaceGranularity = null;
181
182 /** Selected time granularity, to be set and taken on the next update. */
183 private Double desiredTimeGranularity = null;
184
185 /** Whether to smooth data. */
186 private boolean smooth = false;
187
188 // ********************
189 // *** CONSTRUCTORS ***
190 // ********************
191
192 /**
193 * Constructor using default granularities.
194 * @param samplerData SamplerData<?>; sampler data
195 * @param path GraphPath<? extends LaneData>; path
196 */
197 public ContourDataSource(final SamplerData<?> samplerData, final GraphPath<? extends LaneData> path)
198 {
199 this(samplerData, Duration.instantiateSI(1.0), path, DEFAULT_SPACE_GRANULARITIES, DEFAULT_SPACE_GRANULARITY_INDEX,
200 DEFAULT_TIME_GRANULARITIES, DEFAULT_TIME_GRANULARITY_INDEX, DEFAULT_LOWER_TIME_BOUND,
201 AbstractPlot.DEFAULT_INITIAL_UPPER_TIME_BOUND);
202 }
203
204 /**
205 * Constructor for non-default input.
206 * @param samplerData SamplerData<?>; sampler data
207 * @param delay Duration; delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories
208 * @param path GraphPath<? extends LaneData>; path
209 * @param spaceGranularity double[]; granularity options for space dimension
210 * @param initSpaceIndex int; initial selected space granularity
211 * @param timeGranularity double[]; granularity options for time dimension
212 * @param initTimeIndex int; initial selected time granularity
213 * @param start Time; start time
214 * @param initialEnd Time; initial end time of plots, will be expanded if simulation time exceeds it
215 */
216 @SuppressWarnings("parameternumber")
217 public ContourDataSource(final SamplerData<?> samplerData, final Duration delay, final GraphPath<? extends LaneData> path,
218 final double[] spaceGranularity, final int initSpaceIndex, final double[] timeGranularity, final int initTimeIndex,
219 final Time start, final Time initialEnd)
220 {
221 this.samplerData = samplerData;
222 this.updateInterval = Duration.instantiateSI(timeGranularity[initTimeIndex]);
223 this.delay = delay;
224 this.path = path;
225 this.spaceAxis = new Axis(0.0, path.getTotalLength().si, spaceGranularity[initSpaceIndex], spaceGranularity);
226 this.timeAxis = new Axis(start.si, initialEnd.si, timeGranularity[initTimeIndex], timeGranularity);
227
228 // get length-weighted mean speed limit from path to determine cFree and Vc for smoothing
229 this.cFree = Speed.min(path.getSpeedLimit(), MAX_C_FREE);
230 this.vc = Speed.min(path.getSpeedLimit().times(VC_FACRTOR), MAX_C_FREE);
231
232 // setup updater to do the actual work in another thread
233 this.graphUpdater = new GraphUpdater<>("Contour Data Source worker", Thread.currentThread(), (t) -> update(t));
234 }
235
236 // ************************************
237 // *** PLOT INTERFACING AND GETTERS ***
238 // ************************************
239
240 /**
241 * Returns the sampler data for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
242 * @return SamplerData<?>; the sampler
243 */
244 public final SamplerData<?> getSamplerData()
245 {
246 return this.samplerData;
247 }
248
249 /**
250 * Returns the update interval for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
251 * @return Duration; update interval
252 */
253 final Duration getUpdateInterval()
254 {
255 return this.updateInterval;
256 }
257
258 /**
259 * Returns the delay for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
260 * @return Duration; delay
261 */
262 final Duration getDelay()
263 {
264 return this.delay;
265 }
266
267 /**
268 * Returns the path for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
269 * @return GraphPath<? extends LaneData>; the path
270 */
271 final GraphPath<? extends LaneData> getPath()
272 {
273 return this.path;
274 }
275
276 /**
277 * Register a contour plot to this data pool. The contour constructor will do this.
278 * @param contourPlot AbstractContourPlot<?>; contour plot
279 */
280 final void registerContourPlot(final AbstractContourPlot<?> contourPlot)
281 {
282 ContourDataType<?, ?> contourDataType = contourPlot.getContourDataType();
283 if (contourDataType != null)
284 {
285 this.additionalData.put(contourDataType, null);
286 }
287 this.plots.add(contourPlot);
288 }
289
290 /**
291 * Returns the bin count.
292 * @param dimension Dimension; space or time
293 * @return int; bin count
294 */
295 final int getBinCount(final Dimension dimension)
296 {
297 return dimension.getAxis(this).getBinCount();
298 }
299
300 /**
301 * Returns the size of a bin. Usually this is equal to the granularity, except for the last which is likely smaller.
302 * @param dimension Dimension; space or time
303 * @param item int; item number (cell number in contour plot)
304 * @return double; the size of a bin
305 */
306 final synchronized double getBinSize(final Dimension dimension, final int item)
307 {
308 int n = dimension.equals(Dimension.DISTANCE) ? getSpaceBin(item) : getTimeBin(item);
309 double[] ticks = dimension.getAxis(this).getTicks();
310 return ticks[n + 1] - ticks[n];
311 }
312
313 /**
314 * Returns the value on the axis of an item.
315 * @param dimension Dimension; space or time
316 * @param item int; item number (cell number in contour plot)
317 * @return double; the value on the axis of this item
318 */
319 final double getAxisValue(final Dimension dimension, final int item)
320 {
321 if (dimension.equals(Dimension.DISTANCE))
322 {
323 return this.spaceAxis.getBinValue(getSpaceBin(item));
324 }
325 return this.timeAxis.getBinValue(getTimeBin(item));
326 }
327
328 /**
329 * Returns the axis bin number of the given value.
330 * @param dimension Dimension; space or time
331 * @param value double; value
332 * @return int; axis bin number of the given value
333 */
334 final int getAxisBin(final Dimension dimension, final double value)
335 {
336 if (dimension.equals(Dimension.DISTANCE))
337 {
338 return this.spaceAxis.getValueBin(value);
339 }
340 return this.timeAxis.getValueBin(value);
341 }
342
343 /**
344 * Returns the available granularities that a linked plot may use.
345 * @param dimension Dimension; space or time
346 * @return double[]; available granularities that a linked plot may use
347 */
348 @SuppressWarnings("synthetic-access")
349 public final double[] getGranularities(final Dimension dimension)
350 {
351 return dimension.getAxis(this).granularities;
352 }
353
354 /**
355 * Returns the selected granularity that a linked plot should use.
356 * @param dimension Dimension; space or time
357 * @return double; granularity that a linked plot should use
358 */
359 @SuppressWarnings("synthetic-access")
360 public final double getGranularity(final Dimension dimension)
361 {
362 return dimension.getAxis(this).granularity;
363 }
364
365 /**
366 * Called by {@code AbstractContourPlot} to update the time. This will invalidate the plot triggering a redraw.
367 * @param updateTime Time; current time
368 */
369 @SuppressWarnings("synthetic-access")
370 final synchronized void increaseTime(final Time updateTime)
371 {
372 if (updateTime.si > this.timeAxis.maxValue)
373 {
374 this.timeAxis.setMaxValue(updateTime.si);
375 for (AbstractContourPlot<?> plot : this.plots)
376 {
377 plot.setUpperDomainBound(updateTime.si);
378 }
379 }
380 if (this.toTime == null || updateTime.si > this.toTime.si) // null at initialization
381 {
382 invalidate(updateTime);
383 }
384 }
385
386 /**
387 * Sets the granularity of the plot. This will invalidate the plot triggering a redraw.
388 * @param dimension Dimension; space or time
389 * @param granularity double; granularity in space or time (SI unit)
390 */
391 public final synchronized void setGranularity(final Dimension dimension, final double granularity)
392 {
393 if (dimension.equals(Dimension.DISTANCE))
394 {
395 this.desiredSpaceGranularity = granularity;
396 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
397 {
398 contourPlot.setSpaceGranularity(granularity);
399 }
400 }
401 else
402 {
403 this.desiredTimeGranularity = granularity;
404 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
405 {
406 contourPlot.setUpdateInterval(Duration.instantiateSI(granularity));
407 contourPlot.setTimeGranularity(granularity);
408 }
409 }
410 invalidate(null);
411 }
412
413 /**
414 * Sets bi-linear interpolation enabled or disabled. This will invalidate the plot triggering a redraw.
415 * @param interpolate boolean; whether to enable interpolation
416 */
417 @SuppressWarnings("synthetic-access")
418 public final void setInterpolate(final boolean interpolate)
419 {
420 if (this.timeAxis.interpolate != interpolate)
421 {
422 synchronized (this)
423 {
424 this.timeAxis.setInterpolate(interpolate);
425 this.spaceAxis.setInterpolate(interpolate);
426 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
427 {
428 contourPlot.setInterpolation(interpolate);
429 }
430 invalidate(null);
431 }
432 }
433 }
434
435 /**
436 * Sets the adaptive smoothing enabled or disabled. This will invalidate the plot triggering a redraw.
437 * @param smooth boolean; whether to smooth the plor
438 */
439 public final void setSmooth(final boolean smooth)
440 {
441 if (this.smooth != smooth)
442 {
443 synchronized (this)
444 {
445 this.smooth = smooth;
446 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
447 {
448 System.out.println("not notifying plot " + contourPlot);
449 // TODO work out what to do with this: contourPlot.setSmoothing(smooth);
450 }
451 invalidate(null);
452 }
453 }
454 }
455
456 // ************************
457 // *** UPDATING METHODS ***
458 // ************************
459
460 /**
461 * Each method that changes a setting such that the plot is no longer valid, should call this method after the setting was
462 * changed. If time is updated (increased), it should be given as input in to this method. The given time <i>should</i> be
463 * {@code null} if the plot is not valid for any other reason. In this case a full redo is initiated.
464 * <p>
465 * Every method calling this method should be {@code synchronized}, at least for the part where the setting is changed and
466 * this method is called. This method will in all cases add an update request to the updater, working in another thread. It
467 * will invoke method {@code update()}. That method utilizes a synchronized block to obtain all synchronization sensitive
468 * data, before starting the actual work.
469 * @param t Time; time up to which to show data
470 */
471 private synchronized void invalidate(final Time t)
472 {
473 if (t != null)
474 {
475 this.toTime = t;
476 }
477 else
478 {
479 this.redo = true;
480 }
481 if (this.toTime != null) // null at initialization
482 {
483 // either a later time was set, or time was null and a redo is required (will be picked up through the redo field)
484 // note that we cannot set {@code null}, hence we set the current to time, which may or may not have just changed
485 this.graphUpdater.offer(this.toTime);
486 }
487 }
488
489 /**
490 * Heart of the data pool. This method is invoked regularly by the "DataPool worker" thread, as scheduled in a queue through
491 * planned updates at an interval, or by user action changing the plot appearance. No two invocations can happen at the same
492 * time, as the "DataPool worker" thread executes this method before the next update request from the queue is considered.
493 * <p>
494 * This method regularly checks conditions that indicate the update should be interrupted as for example a setting has
495 * changed and appearance should change. Whenever a new invalidation causes {@code redo = true}, this method can stop as the
496 * full data needs to be recalculated. This can be set by any change of e.g. granularity or smoothing, during the update.
497 * <p>
498 * During the data recalculation, a later update time may also trigger this method to stop, while the next update will pick
499 * up where this update left off. During the smoothing this method doesn't stop for an increased update time, as that will
500 * leave a gap in the smoothed data. Note that smoothing either smoothes all data (when {@code redo = true}), or only the
501 * last part that falls within the kernel.
502 * @param t Time; time up to which to show data
503 */
504 @SuppressWarnings({"synthetic-access", "methodlength"})
505 private void update(final Time t)
506 {
507 Throw.when(this.plots.isEmpty(), IllegalStateException.class, "ContourDataSource is used, but not by a contour plot!");
508
509 if (t.si < this.toTime.si)
510 {
511 // skip this update as new updates were commanded, while this update was in the queue, and a previous was running
512 return;
513 }
514
515 /**
516 * This method is executed once at a time by the worker Thread. Many properties, such as the data, are maintained by
517 * this method. Other properties, which other methods can change, are read first in a synchronized block, while those
518 * methods are also synchronized.
519 */
520 boolean redo0;
521 boolean smooth0;
522 boolean interpolate0;
523 double timeGranularity;
524 double spaceGranularity;
525 double[] spaceTicks;
526 double[] timeTicks;
527 int fromSpaceIndex = 0;
528 int fromTimeIndex = 0;
529 int toTimeIndex;
530 double tFromEgtf = 0;
531 int nFromEgtf = 0;
532 synchronized (this)
533 {
534 // save local copies so commands given during this execution can change it for the next execution
535 redo0 = this.redo;
536 smooth0 = this.smooth;
537 interpolate0 = this.timeAxis.interpolate;
538 // timeTicks may be longer than the simulation time, so we use the time bin for the required time of data
539 if (this.desiredTimeGranularity != null)
540 {
541 this.timeAxis.setGranularity(this.desiredTimeGranularity);
542 this.desiredTimeGranularity = null;
543 }
544 if (this.desiredSpaceGranularity != null)
545 {
546 this.spaceAxis.setGranularity(this.desiredSpaceGranularity);
547 this.desiredSpaceGranularity = null;
548 }
549 timeGranularity = this.timeAxis.granularity;
550 spaceGranularity = this.spaceAxis.granularity;
551 spaceTicks = this.spaceAxis.getTicks();
552 timeTicks = this.timeAxis.getTicks();
553 if (!redo0)
554 {
555 // remember where we started, readyItems will be updated but we need to know where we started during the update
556 fromSpaceIndex = getSpaceBin(this.readyItems + 1);
557 fromTimeIndex = getTimeBin(this.readyItems + 1);
558 }
559 toTimeIndex = ((int) (t.si / timeGranularity)) - (interpolate0 ? 0 : 1);
560 if (smooth0)
561 {
562 // time of current bin - kernel size, get bin of that time, get time (middle) of that bin
563 tFromEgtf = this.timeAxis.getBinValue(redo0 ? 0 : this.timeAxis.getValueBin(
564 this.timeAxis.getBinValue(fromTimeIndex) - Math.max(TAU.si, timeGranularity / 2) * KERNEL_FACTOR));
565 nFromEgtf = this.timeAxis.getValueBin(tFromEgtf);
566 }
567 // starting execution, so reset redo trigger which any next command may set to true if needed
568 this.redo = false;
569 }
570
571 // reset upon a redo
572 if (redo0)
573 {
574 this.readyItems = -1;
575
576 // init all data arrays
577 int nSpace = spaceTicks.length - 1;
578 int nTime = timeTicks.length - 1;
579 this.distance = new float[nSpace][nTime];
580 this.time = new float[nSpace][nTime];
581 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
582 {
583 this.additionalData.put(contourDataType, new float[nSpace][nTime]);
584 }
585
586 // setup the smoothing filter
587 if (smooth0)
588 {
589 // create the filter
590 this.egtf = new Egtf(C_CONG.si, this.cFree.si, DELTA_V.si, this.vc.si);
591
592 // create data source and its data streams for speed, distance traveled, time traveled, and additional
593 DataSource generic = this.egtf.getDataSource("generic");
594 generic.addStream(TypedQuantity.SPEED, Speed.instantiateSI(1.0), Speed.instantiateSI(1.0));
595 generic.addStreamSI(this.travelTimeQuantity, 1.0, 1.0);
596 generic.addStreamSI(this.travelDistanceQuantity, 1.0, 1.0);
597 this.speedStream = generic.getStream(TypedQuantity.SPEED);
598 this.travelTimeStream = generic.getStream(this.travelTimeQuantity);
599 this.travelDistanceStream = generic.getStream(this.travelDistanceQuantity);
600 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
601 {
602 this.additionalStreams.put(contourDataType, generic.addStreamSI(contourDataType.getQuantity(), 1.0, 1.0));
603 }
604
605 // in principle we use sigma and tau, unless the data is so rough, we need more (granularity / 2).
606 double tau2 = Math.max(TAU.si, timeGranularity / 2);
607 double sigma2 = Math.max(SIGMA.si, spaceGranularity / 2);
608 // for maximum space and time range, increase sigma and tau by KERNEL_FACTOR, beyond which both kernels diminish
609 this.egtf.setGaussKernelSI(sigma2 * KERNEL_FACTOR, tau2 * KERNEL_FACTOR, sigma2, tau2);
610
611 // add listener to provide a filter status update and to possibly stop the filter when the plot is invalidated
612 this.egtf.addListener(new EgtfListener()
613 {
614 /** {@inheritDoc} */
615 @Override
616 public void notifyProgress(final EgtfEvent event)
617 {
618 // check stop (explicit use of property, not locally stored value)
619 if (ContourDataSource.this.redo)
620 {
621 // plots need to be redone
622 event.interrupt(); // stop the EGTF
623 setStatusLabel(" "); // reset status label so no "ASM at 12.6%" remains there
624 return;
625 }
626 String status =
627 event.getProgress() >= 1.0 ? " " : String.format("ASM at %.2f%%", event.getProgress() * 100);
628 setStatusLabel(status);
629 }
630 });
631 }
632 }
633
634 // discard any data from smoothing if we are not smoothing
635 if (!smooth0)
636 {
637 // free for garbage collector to remove the data
638 this.egtf = null;
639 this.speedStream = null;
640 this.travelTimeStream = null;
641 this.travelDistanceStream = null;
642 this.additionalStreams.clear();
643 }
644
645 // ensure capacity
646 for (int i = 0; i < this.distance.length; i++)
647 {
648 this.distance[i] = GraphUtil.ensureCapacity(this.distance[i], toTimeIndex + 1);
649 this.time[i] = GraphUtil.ensureCapacity(this.time[i], toTimeIndex + 1);
650 for (float[][] additional : this.additionalData.values())
651 {
652 additional[i] = GraphUtil.ensureCapacity(additional[i], toTimeIndex + 1);
653 }
654 }
655
656 // loop cells to update data
657 for (int j = fromTimeIndex; j <= toTimeIndex; j++)
658 {
659 Time tFrom = Time.instantiateSI(timeTicks[j]);
660 Time tTo = Time.instantiateSI(timeTicks[j + 1]);
661
662 // we never filter time, time always spans the entire simulation, it will contain tFrom till tTo
663
664 for (int i = fromSpaceIndex; i < spaceTicks.length - 1; i++)
665 {
666 // when interpolating, set the first row and column to NaN so colors representing 0 do not mess up the edges
667 if ((j == 0 || i == 0) && interpolate0)
668 {
669 this.distance[i][j] = Float.NaN;
670 this.time[i][j] = Float.NaN;
671 this.readyItems++;
672 continue;
673 }
674
675 // only first loop with offset, later in time, none of the space was done in the previous update
676 fromSpaceIndex = 0;
677 Length xFrom = Length.instantiateSI(spaceTicks[i]);
678 Length xTo = Length.instantiateSI(Math.min(spaceTicks[i + 1], this.path.getTotalLength().si));
679
680 // init cell data
681 double totalDistance = 0.0;
682 double totalTime = 0.0;
683 Map<ContourDataType<?, ?>, Object> additionalIntermediate = new LinkedHashMap<>();
684 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
685 {
686 additionalIntermediate.put(contourDataType, contourDataType.identity());
687 }
688
689 // aggregate series in cell
690 for (int series = 0; series < this.path.getNumberOfSeries(); series++)
691 {
692 // obtain trajectories
693 List<TrajectoryGroup<?>> trajectories = new ArrayList<>();
694 for (Section<? extends LaneData> section : getPath().getSections())
695 {
696 TrajectoryGroup<?> trajectoryGroup = this.samplerData.getTrajectoryGroup(section.getSource(series));
697 if (null == trajectoryGroup)
698 {
699 CategoryLogger.always().error("trajectoryGroup {} is null", series);
700 }
701 trajectories.add(trajectoryGroup);
702 }
703
704 // filter groups (lanes) that overlap with section i
705 List<TrajectoryGroup<?>> included = new ArrayList<>();
706 List<Length> xStart = new ArrayList<>();
707 List<Length> xEnd = new ArrayList<>();
708 for (int k = 0; k < trajectories.size(); k++)
709 {
710 TrajectoryGroup<?> trajectoryGroup = trajectories.get(k);
711 LaneData lane = trajectoryGroup.getLane();
712 Length startDistance = this.path.getStartDistance(this.path.get(k));
713 if (startDistance.si + this.path.get(k).getLength().si > spaceTicks[i]
714 && startDistance.si < spaceTicks[i + 1])
715 {
716 included.add(trajectoryGroup);
717 double scale = this.path.get(k).getLength().si / lane.getLength().si;
718 // divide by scale, so we go from base length to section length
719 xStart.add(Length.max(xFrom.minus(startDistance).divide(scale), Length.ZERO));
720 xEnd.add(Length.min(xTo.minus(startDistance).divide(scale), trajectoryGroup.getLane().getLength()));
721 }
722 }
723
724 // accumulate distance and time of trajectories
725 for (int k = 0; k < included.size(); k++)
726 {
727 TrajectoryGroup<?> trajectoryGroup = included.get(k);
728 for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories())
729 {
730 // for optimal operations, we first do quick-reject based on time, as by far most trajectories
731 // during the entire time span of simulation will not apply to a particular cell in space-time
732 if (GraphUtil.considerTrajectory(trajectory, tFrom, tTo))
733 {
734 // again for optimal operations, we use a space-time view only (we don't need more)
735 SpaceTimeView spaceTimeView;
736 try
737 {
738 spaceTimeView = trajectory.getSpaceTimeView(xStart.get(k), xEnd.get(k), tFrom, tTo);
739 }
740 catch (IllegalArgumentException exception)
741 {
742 CategoryLogger.always().debug(exception,
743 "Unable to generate space-time view from x = {} to {} and t = {} to {}.",
744 xStart.get(k), xEnd.get(k), tFrom, tTo);
745 continue;
746 }
747 totalDistance += spaceTimeView.getDistance().si;
748 totalTime += spaceTimeView.getTime().si;
749 }
750 }
751 }
752
753 // loop and set any additional data
754 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
755 {
756 addAdditional(additionalIntermediate, contourDataType, included, xStart, xEnd, tFrom, tTo);
757 }
758
759 }
760
761 // scale values to the full size of a cell on a single lane, so the EGTF is interpolating comparable values
762 double norm = spaceGranularity / (xTo.si - xFrom.si) / this.path.getNumberOfSeries();
763 totalDistance *= norm;
764 totalTime *= norm;
765 this.distance[i][j] = (float) totalDistance;
766 this.time[i][j] = (float) totalTime;
767 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
768 {
769 this.additionalData.get(contourDataType)[i][j] =
770 finalizeAdditional(additionalIntermediate, contourDataType);
771 }
772
773 // add data to EGTF (yes it's a copy, but our local data will be overwritten with smoothed data later)
774 if (smooth0)
775 {
776 // center of cell
777 double xDat = (xFrom.si + xTo.si) / 2.0;
778 double tDat = (tFrom.si + tTo.si) / 2.0;
779 // speed data is implicit as totalDistance/totalTime, but the EGTF needs it explicitly
780 this.egtf.addPointDataSI(this.speedStream, xDat, tDat, totalDistance / totalTime);
781 this.egtf.addPointDataSI(this.travelDistanceStream, xDat, tDat, totalDistance);
782 this.egtf.addPointDataSI(this.travelTimeStream, xDat, tDat, totalTime);
783 for (ContourDataType<?, ?> contourDataType : this.additionalStreams.keySet())
784 {
785 ContourDataSource.this.egtf.addPointDataSI(
786 ContourDataSource.this.additionalStreams.get(contourDataType), xDat, tDat,
787 this.additionalData.get(contourDataType)[i][j]);
788 }
789 }
790
791 // check stop (explicit use of properties, not locally stored values)
792 if (this.redo)
793 {
794 // plots need to be redone, or time has increased meaning that a next call may continue further just as well
795 return;
796 }
797
798 // one more item is ready for plotting
799 this.readyItems++;
800 }
801
802 // notify changes for every time slice
803 this.plots.forEach((plot) -> plot.notifyPlotChange());
804 }
805
806 // smooth all data that is as old as our kernel includes (or all data on a redo)
807 if (smooth0)
808 {
809 Set<Quantity<?, ?>> quantities = new LinkedHashSet<>();
810 quantities.add(this.travelDistanceQuantity);
811 quantities.add(this.travelTimeQuantity);
812 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
813 {
814 quantities.add(contourDataType.getQuantity());
815 }
816 Filter filter = this.egtf.filterFastSI(spaceTicks[0] + 0.5 * spaceGranularity, spaceGranularity,
817 spaceTicks[0] + (-1.5 + spaceTicks.length) * spaceGranularity, tFromEgtf, timeGranularity, t.si,
818 quantities.toArray(new Quantity<?, ?>[quantities.size()]));
819 if (filter != null) // null if interrupted
820 {
821 overwriteSmoothed(this.distance, nFromEgtf, filter.getSI(this.travelDistanceQuantity));
822 overwriteSmoothed(this.time, nFromEgtf, filter.getSI(this.travelTimeQuantity));
823 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
824 {
825 overwriteSmoothed(this.additionalData.get(contourDataType), nFromEgtf,
826 filter.getSI(contourDataType.getQuantity()));
827 }
828 this.plots.forEach((plot) -> plot.notifyPlotChange());
829 }
830 }
831 }
832
833 /**
834 * Add additional data to stored intermediate result.
835 * @param additionalIntermediate Map<ContourDataType<?, ?>, Object>; intermediate storage map
836 * @param contourDataType ContourDataType<?, ?>; additional data type
837 * @param included List<TrajectoryGroup<?>>; trajectories
838 * @param xStart List<Length>; start distance per trajectory group
839 * @param xEnd List<Length>; end distance per trajectory group
840 * @param tFrom Time; start time
841 * @param tTo Time; end time
842 * @param <I> intermediate data type
843 */
844 @SuppressWarnings("unchecked")
845 private <I> void addAdditional(final Map<ContourDataType<?, ?>, Object> additionalIntermediate,
846 final ContourDataType<?, ?> contourDataType, final List<TrajectoryGroup<?>> included, final List<Length> xStart,
847 final List<Length> xEnd, final Time tFrom, final Time tTo)
848 {
849 additionalIntermediate.put(contourDataType, ((ContourDataType<?, I>) contourDataType)
850 .processSeries((I) additionalIntermediate.get(contourDataType), included, xStart, xEnd, tFrom, tTo));
851 }
852
853 /**
854 * Stores a finalized result for additional data.
855 * @param additionalIntermediate Map<ContourDataType<?, ?>, Object>; intermediate storage map
856 * @param contourDataType ContourDataType<?, ?>; additional data type
857 * @return float; finalized results for a cell
858 * @param <I> intermediate data type
859 */
860 @SuppressWarnings("unchecked")
861 private <I> float finalizeAdditional(final Map<ContourDataType<?, ?>, Object> additionalIntermediate,
862 final ContourDataType<?, ?> contourDataType)
863 {
864 return ((ContourDataType<?, I>) contourDataType).finalize((I) additionalIntermediate.get(contourDataType)).floatValue();
865 }
866
867 /**
868 * Helper method to fill smoothed data in to raw data.
869 * @param raw float[][]; the raw unsmoothed data
870 * @param rawCol int; column from which onward to fill smoothed data in to the raw data which is used for plotting
871 * @param smoothed double[][]; smoothed data returned by {@code EGTF}
872 */
873 private void overwriteSmoothed(final float[][] raw, final int rawCol, final double[][] smoothed)
874 {
875 for (int i = 0; i < raw.length; i++)
876 {
877 // can't use System.arraycopy due to float vs double
878 for (int j = 0; j < smoothed[i].length; j++)
879 {
880 raw[i][j + rawCol] = (float) smoothed[i][j];
881 }
882 }
883 }
884
885 /**
886 * Helper method used by an {@code EgtfListener} to present the filter progress.
887 * @param status String; progress report
888 */
889 private void setStatusLabel(final String status)
890 {
891 for (AbstractContourPlot<?> plot : ContourDataSource.this.plots)
892 {
893 // TODO what shall we do this this? plot.setStatusLabel(status);
894 }
895 }
896
897 // ******************************
898 // *** DATA RETRIEVAL METHODS ***
899 // ******************************
900
901 /**
902 * Returns the speed of the cell pertaining to plot item.
903 * @param item int; plot item
904 * @return double; speed of the cell, calculated as 'total distance' / 'total space'.
905 */
906 public double getSpeed(final int item)
907 {
908 if (item > this.readyItems)
909 {
910 return Double.NaN;
911 }
912 return getTotalDistance(item) / getTotalTime(item);
913 }
914
915 /**
916 * Returns the total distance traveled in the cell pertaining to plot item.
917 * @param item int; plot item
918 * @return double; total distance traveled in the cell
919 */
920 public double getTotalDistance(final int item)
921 {
922 if (item > this.readyItems)
923 {
924 return Double.NaN;
925 }
926 return this.distance[getSpaceBin(item)][getTimeBin(item)];
927 }
928
929 /**
930 * Returns the total time traveled in the cell pertaining to plot item.
931 * @param item int; plot item
932 * @return double; total time traveled in the cell
933 */
934 public double getTotalTime(final int item)
935 {
936 if (item > this.readyItems)
937 {
938 return Double.NaN;
939 }
940 return this.time[getSpaceBin(item)][getTimeBin(item)];
941 }
942
943 /**
944 * Returns data of the given {@code ContourDataType} for a specific item.
945 * @param item int; plot item
946 * @param contourDataType ContourDataType<?, ?>; contour data type
947 * @return data of the given {@code ContourDataType} for a specific item
948 */
949 public double get(final int item, final ContourDataType<?, ?> contourDataType)
950 {
951 if (item > this.readyItems)
952 {
953 return Double.NaN;
954 }
955 return this.additionalData.get(contourDataType)[getSpaceBin(item)][getTimeBin(item)];
956 }
957
958 /**
959 * Returns the time bin number of the item.
960 * @param item int; item number
961 * @return int; time bin number of the item
962 */
963 private int getTimeBin(final int item)
964 {
965 Throw.when(item < 0 || item >= this.spaceAxis.getBinCount() * this.timeAxis.getBinCount(),
966 IndexOutOfBoundsException.class, "Item out of range");
967 return item / this.spaceAxis.getBinCount();
968 }
969
970 /**
971 * Returns the space bin number of the item.
972 * @param item int; item number
973 * @return int; space bin number of the item
974 */
975 private int getSpaceBin(final int item)
976 {
977 return item % this.spaceAxis.getBinCount();
978 }
979
980 // **********************
981 // *** HELPER CLASSES ***
982 // **********************
983
984 /**
985 * Enum to refer to either the distance or time axis.
986 * <p>
987 * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
988 * <br>
989 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
990 * </p>
991 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
992 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
993 * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
994 */
995 public enum Dimension
996 {
997 /** Distance axis. */
998 DISTANCE
999 {
1000 /** {@inheritDoc} */
1001 @Override
1002 protected Axis getAxis(final ContourDataSource dataPool)
1003 {
1004 return dataPool.spaceAxis;
1005 }
1006 },
1007
1008 /** Time axis. */
1009 TIME
1010 {
1011 /** {@inheritDoc} */
1012 @Override
1013 protected Axis getAxis(final ContourDataSource dataPool)
1014 {
1015 return dataPool.timeAxis;
1016 }
1017 };
1018
1019 /**
1020 * Returns the {@code Axis} object.
1021 * @param dataPool ContourDataSource; data pool
1022 * @return Axis; axis
1023 */
1024 protected abstract Axis getAxis(ContourDataSource dataPool);
1025 }
1026
1027 /**
1028 * Class to store and determine axis information such as granularity, ticks, and range.
1029 * <p>
1030 * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1031 * <br>
1032 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
1033 * </p>
1034 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
1035 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
1036 * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
1037 */
1038 static class Axis
1039 {
1040 /** Minimum value. */
1041 private final double minValue;
1042
1043 /** Maximum value. */
1044 private double maxValue;
1045
1046 /** Selected granularity. */
1047 private double granularity;
1048
1049 /** Possible granularities. */
1050 private final double[] granularities;
1051
1052 /** Whether the data pool is set to interpolate. */
1053 private boolean interpolate = true;
1054
1055 /** Tick values. */
1056 private double[] ticks;
1057
1058 /**
1059 * Constructor.
1060 * @param minValue double; minimum value
1061 * @param maxValue double; maximum value
1062 * @param granularity double; initial granularity
1063 * @param granularities double[]; possible granularities
1064 */
1065 Axis(final double minValue, final double maxValue, final double granularity, final double[] granularities)
1066 {
1067 this.minValue = minValue;
1068 this.maxValue = maxValue;
1069 this.granularity = granularity;
1070 this.granularities = granularities;
1071 }
1072
1073 /**
1074 * Sets the maximum value.
1075 * @param maxValue double; maximum value
1076 */
1077 void setMaxValue(final double maxValue)
1078 {
1079 if (this.maxValue != maxValue)
1080 {
1081 this.maxValue = maxValue;
1082 this.ticks = null;
1083 }
1084 }
1085
1086 /**
1087 * Sets the granularity.
1088 * @param granularity double; granularity
1089 */
1090 void setGranularity(final double granularity)
1091 {
1092 if (this.granularity != granularity)
1093 {
1094 this.granularity = granularity;
1095 this.ticks = null;
1096 }
1097 }
1098
1099 /**
1100 * Returns the ticks, which are calculated if needed.
1101 * @return double[]; ticks
1102 */
1103 double[] getTicks()
1104 {
1105 if (this.ticks == null)
1106 {
1107 int n = getBinCount() + 1;
1108 this.ticks = new double[n];
1109 int di = this.interpolate ? 1 : 0;
1110 for (int i = 0; i < n; i++)
1111 {
1112 if (i == n - 1)
1113 {
1114 this.ticks[i] = Math.min((i - di) * this.granularity, this.maxValue);
1115 }
1116 else
1117 {
1118 this.ticks[i] = (i - di) * this.granularity;
1119 }
1120 }
1121 }
1122 return this.ticks;
1123 }
1124
1125 /**
1126 * Calculates the number of bins.
1127 * @return int; number of bins
1128 */
1129 int getBinCount()
1130 {
1131 return (int) Math.ceil((this.maxValue - this.minValue) / this.granularity) + (this.interpolate ? 1 : 0);
1132 }
1133
1134 /**
1135 * Calculates the center value of a bin.
1136 * @param bin int; bin number
1137 * @return double; center value of the bin
1138 */
1139 double getBinValue(final int bin)
1140 {
1141 return this.minValue + (0.5 + bin - (this.interpolate ? 1 : 0)) * this.granularity;
1142 }
1143
1144 /**
1145 * Looks up the bin number of the value.
1146 * @param value double; value
1147 * @return int; bin number
1148 */
1149 int getValueBin(final double value)
1150 {
1151 getTicks();
1152 if (value > this.ticks[this.ticks.length - 1])
1153 {
1154 return this.ticks.length - 1;
1155 }
1156 int i = 0;
1157 while (i < this.ticks.length - 1 && this.ticks[i + 1] < value + 1e-9)
1158 {
1159 i++;
1160 }
1161 return i;
1162 }
1163
1164 /**
1165 * Sets interpolation, important is it required the data to have an additional row or column.
1166 * @param interpolate boolean; interpolation
1167 */
1168 void setInterpolate(final boolean interpolate)
1169 {
1170 if (this.interpolate != interpolate)
1171 {
1172 this.interpolate = interpolate;
1173 this.ticks = null;
1174 }
1175 }
1176
1177 /**
1178 * Retrieve the interpolate flag.
1179 * @return boolean; true if interpolation is on; false if interpolation is off
1180 */
1181 public boolean isInterpolate()
1182 {
1183 return this.interpolate;
1184 }
1185
1186 /** {@inheritDoc} */
1187 @Override
1188 public String toString()
1189 {
1190 return "Axis [minValue=" + this.minValue + ", maxValue=" + this.maxValue + ", granularity=" + this.granularity
1191 + ", granularities=" + Arrays.toString(this.granularities) + ", interpolate=" + this.interpolate
1192 + ", ticks=" + Arrays.toString(this.ticks) + "]";
1193 }
1194
1195 }
1196
1197 /**
1198 * Interface for data types of which a contour plot can be made. Using this class, the data pool can determine and store
1199 * cell values for a variable set of additional data types (besides total distance, total time and speed).
1200 * <p>
1201 * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1202 * <br>
1203 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
1204 * </p>
1205 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
1206 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
1207 * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
1208 * @param <Z> value type
1209 * @param <I> intermediate data type
1210 */
1211 public interface ContourDataType<Z extends Number, I>
1212 {
1213 /**
1214 * Returns the initial value for intermediate result.
1215 * @return I, initial intermediate value
1216 */
1217 I identity();
1218
1219 /**
1220 * Calculate value from provided trajectories that apply to a single grid cell on a single series (lane).
1221 * @param intermediate I; intermediate value of previous series, starts as the identity
1222 * @param trajectories List<TrajectoryGroup<?>>; trajectories, all groups overlap the requested space-time
1223 * @param xFrom List<Length>; start location of cell on the section
1224 * @param xTo List<Length>; end location of cell on the section.
1225 * @param tFrom Time; start time of cell
1226 * @param tTo Time; end time of cell
1227 * @return I; intermediate value
1228 */
1229 I processSeries(I intermediate, List<TrajectoryGroup<?>> trajectories, List<Length> xFrom, List<Length> xTo, Time tFrom,
1230 Time tTo);
1231
1232 /**
1233 * Returns the final value of the intermediate result after all lanes.
1234 * @param intermediate I; intermediate result after all lanes
1235 * @return Z; final value
1236 */
1237 Z finalize(I intermediate);
1238
1239 /**
1240 * Returns the quantity that is being plotted on the z-axis for the EGTF filter.
1241 * @return Quantity<Z, ?>; quantity that is being plotted on the z-axis for the EGTF filter
1242 */
1243 Quantity<Z, ?> getQuantity();
1244 }
1245
1246 /** {@inheritDoc} */
1247 @Override
1248 public String toString()
1249 {
1250 return "ContourDataSource [samplerData=" + this.samplerData + ", updateInterval=" + this.updateInterval + ", delay="
1251 + this.delay + ", path=" + this.path + ", spaceAxis=" + this.spaceAxis + ", timeAxis=" + this.timeAxis
1252 + ", plots=" + this.plots + ", distance=" + Arrays.toString(this.distance) + ", time="
1253 + Arrays.toString(this.time) + ", additionalData=" + this.additionalData + ", smooth=" + this.smooth
1254 + ", cFree=" + this.cFree + ", vc=" + this.vc + ", egtf=" + this.egtf + ", speedStream=" + this.speedStream
1255 + ", travelTimeStream=" + this.travelTimeStream + ", travelDistanceStream=" + this.travelDistanceStream
1256 + ", travelTimeQuantity=" + this.travelTimeQuantity + ", travelDistanceQuantity=" + this.travelDistanceQuantity
1257 + ", additionalStreams=" + this.additionalStreams + ", graphUpdater=" + this.graphUpdater + ", redo="
1258 + this.redo + ", toTime=" + this.toTime + ", readyItems=" + this.readyItems + ", desiredSpaceGranularity="
1259 + this.desiredSpaceGranularity + ", desiredTimeGranularity=" + this.desiredTimeGranularity + "]";
1260 }
1261
1262 }