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