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