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