View Javadoc
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 }