View Javadoc
1   package org.opentrafficsim.draw.graphs;
2   
3   import java.awt.Color;
4   import java.time.Period;
5   import java.util.ArrayList;
6   import java.util.EnumSet;
7   import java.util.Iterator;
8   import java.util.LinkedHashMap;
9   import java.util.LinkedHashSet;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.Map.Entry;
13  import java.util.Set;
14  import java.util.SortedSet;
15  import java.util.TreeSet;
16  
17  import org.djunits.value.vdouble.scalar.Duration;
18  import org.djunits.value.vdouble.scalar.Length;
19  import org.djunits.value.vdouble.scalar.Time;
20  import org.djutils.exceptions.Throw;
21  import org.djutils.immutablecollections.ImmutableLinkedHashSet;
22  import org.djutils.immutablecollections.ImmutableSet;
23  import org.jfree.chart.JFreeChart;
24  import org.jfree.chart.LegendItem;
25  import org.jfree.chart.LegendItemCollection;
26  import org.jfree.chart.axis.NumberAxis;
27  import org.jfree.chart.plot.XYPlot;
28  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
29  import org.jfree.data.DomainOrder;
30  import org.jfree.data.xy.XYDataset;
31  import org.opentrafficsim.kpi.interfaces.LaneData;
32  import org.opentrafficsim.kpi.sampling.Sampler;
33  import org.opentrafficsim.kpi.sampling.SpaceTimeRegion;
34  import org.opentrafficsim.kpi.sampling.Trajectory;
35  import org.opentrafficsim.kpi.sampling.Trajectory.SpaceTimeView;
36  import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
37  
38  /**
39   * Fundamental diagram from various sources.
40   * <p>
41   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
42   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
43   * </p>
44   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
45   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
46   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
47   */
48  public class FundamentalDiagram extends AbstractBoundedPlot implements XYDataset
49  {
50  
51      /** Aggregation periods. */
52      public static final double[] DEFAULT_PERIODS = new double[] {5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 900.0};
53  
54      /** Update frequencies (n * 1/period). */
55      public static final int[] DEFAULT_UPDATE_FREQUENCIES = new int[] {1, 2, 3, 5, 10};
56  
57      /** Source providing the data. */
58      private final FdSource source;
59  
60      /** Fundamental diagram line. */
61      private final FdLine fdLine;
62  
63      /** Quantity on domain axis. */
64      private Quantity domainQuantity;
65  
66      /** Quantity on range axis. */
67      private Quantity rangeQuantity;
68  
69      /** The other, 3rd quantity. */
70      private Quantity otherQuantity;
71  
72      /** Labels of series. */
73      private final List<String> seriesLabels = new ArrayList<>();
74  
75      /** Updater for update times. */
76      private final GraphUpdater<Time> graphUpdater;
77  
78      /** Property for chart listener to provide time info for status label. */
79      private String timeInfo = "";
80  
81      /** Legend to change text color to indicate visibility. */
82      private LegendItemCollection legend;
83  
84      /** Whether each lane is visible or not. */
85      private final List<Boolean> laneVisible = new ArrayList<>();
86  
87      /**
88       * Constructor.
89       * @param caption caption
90       * @param domainQuantity initial quantity on the domain axis
91       * @param rangeQuantity initial quantity on the range axis
92       * @param scheduler scheduler.
93       * @param source source providing the data
94       * @param fdLine fundamental diagram line, may be {@code null}
95       */
96      public FundamentalDiagram(final String caption, final Quantity domainQuantity, final Quantity rangeQuantity,
97              final PlotScheduler scheduler, final FdSource source, final FdLine fdLine)
98      {
99          super(scheduler, caption, source.getUpdateInterval(), source.getDelay());
100         Throw.when(domainQuantity.equals(rangeQuantity), IllegalArgumentException.class,
101                 "Domain and range quantity should not be equal.");
102         this.fdLine = fdLine;
103         this.setDomainQuantity(domainQuantity);
104         this.setRangeQuantity(rangeQuantity);
105         Set<Quantity> quantities = EnumSet.allOf(Quantity.class);
106         quantities.remove(domainQuantity);
107         quantities.remove(rangeQuantity);
108         this.setOtherQuantity(quantities.iterator().next());
109         this.source = source;
110         int d = 0;
111         if (fdLine != null)
112         {
113             d = 1;
114             this.seriesLabels.add(fdLine.getName());
115             this.laneVisible.add(true);
116         }
117         for (int series = 0; series < source.getNumberOfSeries(); series++)
118         {
119             this.seriesLabels.add(series + d, source.getName(series));
120             this.laneVisible.add(true);
121         }
122         setChart(createChart());
123         setLowerDomainBound(0.0);
124         setLowerRangeBound(0.0);
125 
126         // setup updater to do the actual work in another thread
127         this.graphUpdater = new GraphUpdater<>("Fundamental diagram worker", Thread.currentThread(), (t) ->
128         {
129             if (this.getSource() != null)
130             {
131                 this.getSource().increaseTime(t);
132                 notifyPlotChange();
133             }
134         });
135 
136         // let this diagram be notified by the source
137         source.addFundamentalDiagram(this);
138     }
139 
140     /**
141      * Create a chart.
142      * @return chart
143      */
144     private JFreeChart createChart()
145     {
146         NumberAxis xAxis = new NumberAxis(this.getDomainQuantity().label());
147         NumberAxis yAxis = new NumberAxis(this.getRangeQuantity().label());
148         XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer()
149         {
150             /** */
151             private static final long serialVersionUID = 20181022L;
152 
153             @SuppressWarnings("synthetic-access")
154             @Override
155             public boolean isSeriesVisible(final int series)
156             {
157                 return FundamentalDiagram.this.laneVisible.get(series);
158             }
159         }; // XYDotRenderer doesn't support different markers
160         renderer.setDefaultLinesVisible(false);
161         if (hasLineFD())
162         {
163             int series = this.getSource().getNumberOfSeries();
164             renderer.setSeriesLinesVisible(series, true);
165             renderer.setSeriesPaint(series, Color.BLACK);
166             renderer.setSeriesShapesVisible(series, false);
167         }
168         XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
169         boolean showLegend = true;
170         if (!hasLineFD() && this.getSource().getNumberOfSeries() < 2)
171         {
172             plot.setFixedLegendItems(null);
173             showLegend = false;
174         }
175         else
176         {
177             this.legend = new LegendItemCollection();
178             for (int i = 0; i < this.getSource().getNumberOfSeries(); i++)
179             {
180                 LegendItem li = new LegendItem(this.getSource().getName(i));
181                 li.setSeriesKey(i); // lane series, not curve series
182                 li.setShape(renderer.lookupLegendShape(i));
183                 li.setFillPaint(renderer.lookupSeriesPaint(i));
184                 this.legend.add(li);
185             }
186             if (hasLineFD())
187             {
188                 LegendItem li = new LegendItem(this.fdLine.getName());
189                 li.setSeriesKey(-1);
190                 this.legend.add(li);
191             }
192             plot.setFixedLegendItems(this.legend);
193             showLegend = true;
194         }
195         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
196     }
197 
198     @Override
199     protected void increaseTime(final Time time)
200     {
201         if (this.graphUpdater != null && time.si >= this.getSource().getAggregationPeriod().si) // null during construction
202         {
203             this.graphUpdater.offer(time);
204         }
205     }
206 
207     @Override
208     public int getSeriesCount()
209     {
210         if (this.getSource() == null)
211         {
212             return 0;
213         }
214         return this.getSource().getNumberOfSeries() + (hasLineFD() ? 1 : 0);
215     }
216 
217     @Override
218     public Comparable<String> getSeriesKey(final int series)
219     {
220         return this.seriesLabels.get(series);
221     }
222 
223     @SuppressWarnings("rawtypes")
224     @Override
225     public int indexOf(final Comparable seriesKey)
226     {
227         int index = this.seriesLabels.indexOf(seriesKey);
228         return index < 0 ? 0 : index;
229     }
230 
231     @Override
232     public DomainOrder getDomainOrder()
233     {
234         return DomainOrder.NONE;
235     }
236 
237     @Override
238     public int getItemCount(final int series)
239     {
240         if (hasLineFD() && series == getSeriesCount() - 1)
241         {
242             return this.fdLine.getValues(this.domainQuantity).length;
243         }
244         return this.getSource().getItemCount(series);
245     }
246 
247     @Override
248     public Number getX(final int series, final int item)
249     {
250         return getXValue(series, item);
251     }
252 
253     @Override
254     public double getXValue(final int series, final int item)
255     {
256         if (hasLineFD() && series == getSeriesCount() - 1)
257         {
258             return this.fdLine.getValues(this.domainQuantity)[item];
259         }
260         return this.getDomainQuantity().getValue(this.getSource(), series, item);
261     }
262 
263     @Override
264     public Number getY(final int series, final int item)
265     {
266         return getYValue(series, item);
267     }
268 
269     @Override
270     public double getYValue(final int series, final int item)
271     {
272         if (hasLineFD() && series == getSeriesCount() - 1)
273         {
274             return this.fdLine.getValues(this.rangeQuantity)[item];
275         }
276         return this.getRangeQuantity().getValue(this.getSource(), series, item);
277     }
278 
279     @Override
280     public GraphType getGraphType()
281     {
282         return GraphType.FUNDAMENTAL_DIAGRAM;
283     }
284 
285     @Override
286     public String getStatusLabel(final double domainValue, final double rangeValue)
287     {
288         return this.getDomainQuantity().format(domainValue) + ", " + this.getRangeQuantity().format(rangeValue) + ", "
289                 + this.getOtherQuantity()
290                         .format(this.getDomainQuantity().computeOther(this.getRangeQuantity(), domainValue, rangeValue))
291                 + this.getTimeInfo();
292     }
293 
294     /**
295      * Quantity enum defining density, flow and speed.
296      * <p>
297      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
298      * <br>
299      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
300      * </p>
301      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
302      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
303      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
304      */
305     public enum Quantity
306     {
307         /** Density. */
308         DENSITY
309         {
310             @Override
311             public String label()
312             {
313                 return "Density [veh/km] \u2192";
314             }
315 
316             @Override
317             public String format(final double value)
318             {
319                 return String.format("%.0f veh/km", value);
320             }
321 
322             @Override
323             public double getValue(final FdSource src, final int series, final int item)
324             {
325                 return 1000 * src.getDensity(series, item);
326             }
327 
328             @Override
329             public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
330             {
331                 // .......................... speed = flow / density .. flow = density * speed
332                 return pairing.equals(FLOW) ? pairedValue / thisValue : thisValue * pairedValue;
333             }
334         },
335 
336         /** Flow. */
337         FLOW
338         {
339             @Override
340             public String label()
341             {
342                 return "Flow [veh/h] \u2192";
343             }
344 
345             @Override
346             public String format(final double value)
347             {
348                 return String.format("%.0f veh/h", value);
349             }
350 
351             @Override
352             public double getValue(final FdSource src, final int series, final int item)
353             {
354                 return 3600 * src.getFlow(series, item);
355             }
356 
357             @Override
358             public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
359             {
360                 // speed = flow * density ... density = flow / speed
361                 return thisValue / pairedValue;
362             }
363         },
364 
365         /** Speed. */
366         SPEED
367         {
368             @Override
369             public String label()
370             {
371                 return "Speed [km/h] \u2192";
372             }
373 
374             @Override
375             public String format(final double value)
376             {
377                 return String.format("%.1f km/h", value);
378             }
379 
380             @Override
381             public double getValue(final FdSource src, final int series, final int item)
382             {
383                 return 3.6 * src.getSpeed(series, item);
384             }
385 
386             @Override
387             public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
388             {
389                 // ............................. flow = speed * density .. density = flow / speed
390                 return pairing.equals(DENSITY) ? thisValue * pairedValue : pairedValue / thisValue;
391             }
392         };
393 
394         /**
395          * Returns an axis label of the quantity.
396          * @return axis label of the quantity
397          */
398         public abstract String label();
399 
400         /**
401          * Formats a value for status display.
402          * @param value value
403          * @return formatted string including quantity
404          */
405         public abstract String format(double value);
406 
407         /**
408          * Get scaled value in presentation unit.
409          * @param src the data source
410          * @param series series number
411          * @param item item number in series
412          * @return scaled value in presentation unit
413          */
414         public abstract double getValue(FdSource src, int series, int item);
415 
416         /**
417          * Compute the value of the 3rd quantity.
418          * @param pairing quantity on other axis
419          * @param thisValue value of this quantity
420          * @param pairedValue value of the paired quantity on the other axis
421          * @return value of the 3rd quantity
422          */
423         public abstract double computeOther(Quantity pairing, double thisValue, double pairedValue);
424 
425     }
426 
427     /**
428      * Data source for a fundamental diagram.
429      * <p>
430      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
431      * <br>
432      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
433      * </p>
434      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
435      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
436      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
437      */
438     public interface FdSource
439     {
440         /**
441          * Returns the possible intervals.
442          * @return possible intervals
443          */
444         default double[] getPossibleAggregationPeriods()
445         {
446             return DEFAULT_PERIODS;
447         }
448 
449         /**
450          * Returns the possible frequencies, as a factor on 1 / 'aggregation interval'.
451          * @return possible frequencies
452          */
453         default int[] getPossibleUpdateFrequencies()
454         {
455             return DEFAULT_UPDATE_FREQUENCIES;
456         }
457 
458         /**
459          * Add fundamental diagram. Used to notify diagrams when data has changed.
460          * @param fundamentalDiagram fundamental diagram
461          */
462         void addFundamentalDiagram(FundamentalDiagram fundamentalDiagram);
463 
464         /**
465          * Clears all connected fundamental diagrams.
466          */
467         void clearFundamentalDiagrams();
468 
469         /**
470          * Returns the diagrams.
471          * @return ImmutableSet&lt;FundamentalDiagram&gt; diagrams
472          */
473         ImmutableSet<FundamentalDiagram> getDiagrams();
474 
475         /**
476          * The update interval.
477          * @return update interval
478          */
479         Duration getUpdateInterval();
480 
481         /**
482          * Changes the update interval.
483          * @param interval update interval
484          * @param time time until which data has to be recalculated
485          */
486         void setUpdateInterval(Duration interval, Time time);
487 
488         /**
489          * The aggregation period.
490          * @return aggregation period
491          */
492         Duration getAggregationPeriod();
493 
494         /**
495          * Changes the aggregation period.
496          * @param period aggregation period
497          */
498         void setAggregationPeriod(Duration period);
499 
500         /**
501          * Recalculates the data after the aggregation or update time was changed.
502          * @param time time up to which recalculation is required
503          */
504         void recalculate(Time time);
505 
506         /**
507          * Return the delay for graph updates so future influencing events have occurred, e.d. GTU move's.
508          * @return graph delay
509          */
510         Duration getDelay();
511 
512         /**
513          * Increase the time span.
514          * @param time time to increase to
515          */
516         void increaseTime(Time time);
517 
518         /**
519          * Returns the number of series (i.e. lanes or 1 for aggregated).
520          * @return number of series
521          */
522         int getNumberOfSeries();
523 
524         /**
525          * Returns a name of the series.
526          * @param series series number
527          * @return name of the series
528          */
529         String getName(int series);
530 
531         /**
532          * Returns the number of items in the series.
533          * @param series series number
534          * @return number of items in the series
535          */
536         int getItemCount(int series);
537 
538         /**
539          * Return the SI flow value of item in series.
540          * @param series series number
541          * @param item item number in the series
542          * @return SI flow value of item in series
543          */
544         double getFlow(int series, int item);
545 
546         /**
547          * Return the SI density value of item in series.
548          * @param series series number
549          * @param item item number in the series
550          * @return SI density value of item in series
551          */
552         double getDensity(int series, int item);
553 
554         /**
555          * Return the SI speed value of item in series.
556          * @param series series number
557          * @param item item number in the series
558          * @return SI speed value of item in series
559          */
560         double getSpeed(int series, int item);
561 
562         /**
563          * Returns whether this source aggregates lanes.
564          * @return whether this source aggregates lanes
565          */
566         boolean isAggregate();
567 
568         /**
569          * Sets the name of the series when aggregated, e.g. for legend. Default is "Aggregate".
570          * @param aggregateName name of the series when aggregated
571          */
572         void setAggregateName(String aggregateName);
573     }
574 
575     /**
576      * Abstract implementation to link to fundamental diagrams.
577      */
578     abstract static class AbstractFdSource implements FdSource
579     {
580 
581         /** Fundamental diagrams. */
582         private Set<FundamentalDiagram> fundamentalDiagrams = new LinkedHashSet<>();
583 
584         @Override
585         public void addFundamentalDiagram(final FundamentalDiagram fundamentalDiagram)
586         {
587             this.fundamentalDiagrams.add(fundamentalDiagram);
588         }
589 
590         @Override
591         public void clearFundamentalDiagrams()
592         {
593             this.fundamentalDiagrams.clear();
594         }
595 
596         @Override
597         public ImmutableSet<FundamentalDiagram> getDiagrams()
598         {
599             return new ImmutableLinkedHashSet<>(this.fundamentalDiagrams);
600         }
601 
602     }
603 
604     /**
605      * Creates a {@code Source} from a sampler and positions.
606      * @param sampler sampler
607      * @param crossSection cross section
608      * @param aggregateLanes whether to aggregate the positions
609      * @param aggregationTime aggregation time (and update time)
610      * @param harmonic harmonic mean
611      * @return source for a fundamental diagram from a sampler and positions
612      * @param <L> LaneData
613      */
614     @SuppressWarnings("methodlength")
615     public static <L extends LaneData<L>> FdSource sourceFromSampler(final Sampler<?, L> sampler,
616             final GraphCrossSection<L> crossSection, final boolean aggregateLanes, final Duration aggregationTime,
617             final boolean harmonic)
618     {
619         return new CrossSectionSamplerFdSource<>(sampler, crossSection, aggregateLanes, aggregationTime, harmonic);
620     }
621 
622     /**
623      * Creates a {@code Source} from a sampler and positions.
624      * @param sampler sampler
625      * @param path cross section
626      * @param aggregateLanes whether to aggregate the positions
627      * @param aggregationTime aggregation time (and update time)
628      * @return source for a fundamental diagram from a sampler and positions
629      * @param <L> LaneData
630      */
631     public static <L extends LaneData<L>> FdSource sourceFromSampler(final Sampler<?, L> sampler, final GraphPath<L> path,
632             final boolean aggregateLanes, final Duration aggregationTime)
633     {
634         return new PathSamplerFdSource<>(sampler, path, aggregateLanes, aggregationTime);
635     }
636 
637     /**
638      * Combines multiple sources in to one source.
639      * @param sources sources coupled to their names for in the legend
640      * @return combined source
641      */
642     public static FdSource combinedSource(final Map<String, FdSource> sources)
643     {
644         return new MultiFdSource(sources);
645     }
646 
647     /**
648      * Fundamental diagram source based on a cross section.
649      * @param <L> lane data type
650      * @param <S> underlying source type
651      */
652     private static class CrossSectionSamplerFdSource<L extends LaneData<L>, S extends GraphCrossSection<L>>
653             extends AbstractSpaceSamplerFdSource<L, S>
654     {
655         /** Harmonic mean. */
656         private final boolean harmonic;
657 
658         /**
659          * Constructor.
660          * @param sampler Sampler<?, ?>; sampler
661          * @param crossSection cross section
662          * @param aggregateLanes whether to aggregate the lanes
663          * @param aggregationPeriod initial aggregation {@link Period}
664          * @param harmonic harmonic mean
665          */
666         CrossSectionSamplerFdSource(final Sampler<?, L> sampler, final S crossSection, final boolean aggregateLanes,
667                 final Duration aggregationPeriod, final boolean harmonic)
668         {
669             super(sampler, crossSection, aggregateLanes, aggregationPeriod);
670             this.harmonic = harmonic;
671         }
672 
673         @Override
674         protected void getMeasurements(final Trajectory<?> trajectory, final Time startTime, final Time endTime,
675                 final Length length, final int series, final double[] measurements)
676         {
677             Length x = getSpace().position(series);
678             if (GraphUtil.considerTrajectory(trajectory, x, x))
679             {
680                 // detailed check
681                 Time t = trajectory.getTimeAtPosition(x);
682                 if (t.si >= startTime.si && t.si < endTime.si)
683                 {
684                     measurements[0] = 1; // first = count
685                     measurements[1] = // second = sum of (inverted) speeds
686                             this.harmonic ? 1.0 / trajectory.getSpeedAtPosition(x).si : trajectory.getSpeedAtPosition(x).si;
687                 }
688             }
689         }
690 
691         @Override
692         protected double getVehicleCount(final double first, final double second)
693         {
694             return first; // is divided by aggregation period by caller
695         }
696 
697         @Override
698         protected double getSpeed(final double first, final double second)
699         {
700             return this.harmonic ? first / second : second / first;
701         }
702 
703         @Override
704         public String toString()
705         {
706             return "CrossSectionSamplerFdSource [harmonic=" + this.harmonic + "]";
707         }
708 
709     }
710 
711     /**
712      * Fundamental diagram source based on a path. Density, speed and flow over the entire path are calculated per lane.
713      * @param <L> lane data type
714      * @param <S> underlying source type
715      */
716     private static class PathSamplerFdSource<L extends LaneData<L>, S extends GraphPath<L>>
717             extends AbstractSpaceSamplerFdSource<L, S>
718     {
719         /**
720          * Constructor.
721          * @param sampler Sampler<?, ?>; sampler
722          * @param path path
723          * @param aggregateLanes whether to aggregate the lanes
724          * @param aggregationPeriod initial aggregation period
725          */
726         PathSamplerFdSource(final Sampler<?, L> sampler, final S path, final boolean aggregateLanes,
727                 final Duration aggregationPeriod)
728         {
729             super(sampler, path, aggregateLanes, aggregationPeriod);
730         }
731 
732         @Override
733         protected void getMeasurements(final Trajectory<?> trajectory, final Time startTime, final Time endTime,
734                 final Length length, final int sereies, final double[] measurements)
735         {
736             SpaceTimeView stv = trajectory.getSpaceTimeView(Length.ZERO, length, startTime, endTime);
737             measurements[0] = stv.distance().si; // first = total traveled distance
738             measurements[1] = stv.time().si; // second = total traveled time
739         }
740 
741         @Override
742         protected double getVehicleCount(final double first, final double second)
743         {
744             return first / getSpace().getTotalLength().si; // is divided by aggregation period by caller
745         }
746 
747         @Override
748         protected double getSpeed(final double first, final double second)
749         {
750             return first / second;
751         }
752 
753         @Override
754         public String toString()
755         {
756             return "PathSamplerFdSource []";
757         }
758 
759     }
760 
761     /**
762      * Abstract class that deals with updating and recalculating the fundamental diagram.
763      * @param <L> lane data type
764      * @param <S> underlying source type
765      */
766     private abstract static class AbstractSpaceSamplerFdSource<L extends LaneData<L>, S extends AbstractGraphSpace<L>>
767             extends AbstractFdSource
768     {
769         /** Period number of last calculated period. */
770         private int periodNumber = -1;
771 
772         /** Update interval. */
773         private Duration updateInterval;
774 
775         /** Aggregation period. */
776         private Duration aggregationPeriod;
777 
778         /** Last update time. */
779         private Time lastUpdateTime;
780 
781         /** Number of series. */
782         private final int nSeries;
783 
784         /** First data. */
785         private double[][] firstMeasurement;
786 
787         /** Second data. */
788         private double[][] secondMeasurement;
789 
790         /** Whether the plot is in a process such that the data is invalid for the current draw of the plot. */
791         private boolean invalid = false;
792 
793         /** The sampler. */
794         private final Sampler<?, L> sampler;
795 
796         /** Space. */
797         private final S space;
798 
799         /** Whether to aggregate the lanes. */
800         private final boolean aggregateLanes;
801 
802         /** Name of the series when aggregated. */
803         private String aggregateName = "Aggregate";
804 
805         /** For each series (lane), the highest trajectory number (n) below which all trajectories were also handled (0:n). */
806         private Map<L, Integer> lastConsecutivelyAssignedTrajectories = new LinkedHashMap<>();
807 
808         /** For each series (lane), a list of handled trajectories above n, excluding n+1. */
809         private Map<L, SortedSet<Integer>> assignedTrajectories = new LinkedHashMap<>();
810 
811         /**
812          * Constructor.
813          * @param sampler Sampler<?, ?>; sampler
814          * @param space space
815          * @param aggregateLanes whether to aggregate the lanes
816          * @param aggregationPeriod initial aggregation period
817          */
818         AbstractSpaceSamplerFdSource(final Sampler<?, L> sampler, final S space, final boolean aggregateLanes,
819                 final Duration aggregationPeriod)
820         {
821             this.sampler = sampler;
822             this.space = space;
823             this.aggregateLanes = aggregateLanes;
824             this.nSeries = aggregateLanes ? 1 : space.getNumberOfSeries();
825             // create and register kpi lane directions
826             for (L laneDirection : space)
827             {
828                 sampler.registerSpaceTimeRegion(new SpaceTimeRegion<>(laneDirection, Length.ZERO, laneDirection.getLength(),
829                         sampler.now(), Time.instantiateSI(Double.MAX_VALUE)));
830 
831                 // info per kpi lane direction
832                 this.lastConsecutivelyAssignedTrajectories.put(laneDirection, -1);
833                 this.assignedTrajectories.put(laneDirection, new TreeSet<>());
834             }
835 
836             this.updateInterval = aggregationPeriod;
837             this.aggregationPeriod = aggregationPeriod;
838             this.firstMeasurement = new double[this.nSeries][10];
839             this.secondMeasurement = new double[this.nSeries][10];
840         }
841 
842         /**
843          * Returns the space.
844          * @return space
845          */
846         protected S getSpace()
847         {
848             return this.space;
849         }
850 
851         @Override
852         public Duration getUpdateInterval()
853         {
854             return this.updateInterval;
855         }
856 
857         @Override
858         public void setUpdateInterval(final Duration interval, final Time time)
859         {
860             if (this.updateInterval != interval)
861             {
862                 this.updateInterval = interval;
863                 recalculate(time);
864             }
865         }
866 
867         @Override
868         public Duration getAggregationPeriod()
869         {
870             return this.aggregationPeriod;
871         }
872 
873         @Override
874         public void setAggregationPeriod(final Duration period)
875         {
876             if (this.aggregationPeriod != period)
877             {
878                 this.aggregationPeriod = period;
879             }
880         }
881 
882         @Override
883         public void recalculate(final Time time)
884         {
885             new Thread(new Runnable()
886             {
887                 @Override
888                 @SuppressWarnings("synthetic-access")
889                 public void run()
890                 {
891                     synchronized (AbstractSpaceSamplerFdSource.this)
892                     {
893                         // an active plot draw will now request data on invalid items
894                         AbstractSpaceSamplerFdSource.this.invalid = true;
895                         AbstractSpaceSamplerFdSource.this.periodNumber = -1;
896                         AbstractSpaceSamplerFdSource.this.updateInterval = getUpdateInterval();
897                         AbstractSpaceSamplerFdSource.this.firstMeasurement =
898                                 new double[AbstractSpaceSamplerFdSource.this.nSeries][10];
899                         AbstractSpaceSamplerFdSource.this.secondMeasurement =
900                                 new double[AbstractSpaceSamplerFdSource.this.nSeries][10];
901                         AbstractSpaceSamplerFdSource.this.lastConsecutivelyAssignedTrajectories.clear();
902                         AbstractSpaceSamplerFdSource.this.assignedTrajectories.clear();
903                         for (L lane : AbstractSpaceSamplerFdSource.this.space)
904                         {
905                             AbstractSpaceSamplerFdSource.this.lastConsecutivelyAssignedTrajectories.put(lane, -1);
906                             AbstractSpaceSamplerFdSource.this.assignedTrajectories.put(lane, new TreeSet<>());
907                         }
908                         AbstractSpaceSamplerFdSource.this.lastUpdateTime = null; // so the increaseTime call is not skipped
909                         while ((AbstractSpaceSamplerFdSource.this.periodNumber + 1) * getUpdateInterval().si
910                                 + AbstractSpaceSamplerFdSource.this.aggregationPeriod.si <= time.si)
911                         {
912                             increaseTime(Time
913                                     .instantiateSI((AbstractSpaceSamplerFdSource.this.periodNumber + 1) * getUpdateInterval().si
914                                             + AbstractSpaceSamplerFdSource.this.aggregationPeriod.si));
915                             // TODO: if multiple plots are coupled to the same source, other plots are not invalidated
916                             // TODO: change of aggregation period / update freq, is not updated in the GUI on other plots
917                             // for (FundamentalDiagram diagram : getDiagrams())
918                             // {
919                             // }
920                         }
921                         AbstractSpaceSamplerFdSource.this.invalid = false;
922                     }
923                 }
924             }, "Fundamental diagram recalculation").start();
925         }
926 
927         @Override
928         public Duration getDelay()
929         {
930             return Duration.instantiateSI(1.0);
931         }
932 
933         @Override
934         public synchronized void increaseTime(final Time time)
935         {
936             if (time.si < this.aggregationPeriod.si)
937             {
938                 // skip periods that fall below 0.0 time
939                 return;
940             }
941 
942             if (this.lastUpdateTime != null && time.le(this.lastUpdateTime))
943             {
944                 // skip updates from different graphs at the same time
945                 return;
946             }
947             this.lastUpdateTime = time;
948 
949             // ensure capacity
950             int nextPeriod = this.periodNumber + 1;
951             if (nextPeriod >= this.firstMeasurement[0].length - 1)
952             {
953                 for (int i = 0; i < this.nSeries; i++)
954                 {
955                     this.firstMeasurement[i] = GraphUtil.ensureCapacity(this.firstMeasurement[i], nextPeriod + 1);
956                     this.secondMeasurement[i] = GraphUtil.ensureCapacity(this.secondMeasurement[i], nextPeriod + 1);
957                 }
958             }
959 
960             // loop positions and trajectories
961             Time startTime = time.minus(this.aggregationPeriod);
962             double first = 0;
963             double second = 0.0;
964             for (int series = 0; series < this.space.getNumberOfSeries(); series++)
965             {
966                 Iterator<L> it = this.space.iterator(series);
967                 while (it.hasNext())
968                 {
969                     L lane = it.next();
970                     if (!this.sampler.getSamplerData().contains(lane))
971                     {
972                         // sampler has not yet started to record on this lane
973                         continue;
974                     }
975                     TrajectoryGroup<?> trajectoryGroup = this.sampler.getSamplerData().getTrajectoryGroup(lane);
976                     int last = this.lastConsecutivelyAssignedTrajectories.get(lane);
977                     SortedSet<Integer> assigned = this.assignedTrajectories.get(lane);
978                     if (!this.aggregateLanes)
979                     {
980                         first = 0.0;
981                         second = 0.0;
982                     }
983 
984                     // Length x = this.crossSection.position(series);
985                     int i = 0;
986                     for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories())
987                     {
988                         // we can skip all assigned trajectories, which are all up to and including 'last' and all in 'assigned'
989                         if (i > last && !assigned.contains(i))
990                         {
991                             // quickly filter
992                             if (GraphUtil.considerTrajectory(trajectory, startTime, time))
993                             {
994                                 double[] measurements = new double[2];
995                                 getMeasurements(trajectory, startTime, time, lane.getLength(), series, measurements);
996                                 first += measurements[0];
997                                 second += measurements[1];
998                             }
999                             if (trajectory.getT(trajectory.size() - 1) < startTime.si - getDelay().si)
1000                             {
1001                                 assigned.add(i);
1002                             }
1003                         }
1004                         i++;
1005                     }
1006                     if (!this.aggregateLanes)
1007                     {
1008                         this.firstMeasurement[series][nextPeriod] = first;
1009                         this.secondMeasurement[series][nextPeriod] = second;
1010                     }
1011 
1012                     // consolidate list of assigned trajectories in 'all up to n' and 'these specific ones beyond n'
1013                     if (!assigned.isEmpty())
1014                     {
1015                         int possibleNextLastAssigned = assigned.first();
1016                         while (possibleNextLastAssigned == last + 1) // consecutive or very first
1017                         {
1018                             last = possibleNextLastAssigned;
1019                             assigned.remove(possibleNextLastAssigned);
1020                             possibleNextLastAssigned = assigned.isEmpty() ? -1 : assigned.first();
1021                         }
1022                         this.lastConsecutivelyAssignedTrajectories.put(lane, last);
1023                     }
1024                 }
1025             }
1026             if (this.aggregateLanes)
1027             {
1028                 // whatever we measured, it was summed and can be normalized per line like this
1029                 this.firstMeasurement[0][nextPeriod] = first / this.space.getNumberOfSeries();
1030                 this.secondMeasurement[0][nextPeriod] = second / this.space.getNumberOfSeries();
1031             }
1032             this.periodNumber = nextPeriod;
1033         }
1034 
1035         @Override
1036         public int getNumberOfSeries()
1037         {
1038             // if there is an active plot draw as the data is being recalculated, data on invalid items is requested
1039             // a call to getSeriesCount() indicates a new draw, and during a recalculation the data is limited but valid
1040             this.invalid = false;
1041             return this.nSeries;
1042         }
1043 
1044         @Override
1045         public void setAggregateName(final String aggregateName)
1046         {
1047             this.aggregateName = aggregateName;
1048         }
1049 
1050         @Override
1051         public String getName(final int series)
1052         {
1053             if (this.aggregateLanes)
1054             {
1055                 return this.aggregateName;
1056             }
1057             return this.space.getName(series);
1058         }
1059 
1060         @Override
1061         public int getItemCount(final int series)
1062         {
1063             return this.periodNumber + 1;
1064         }
1065 
1066         @Override
1067         public final double getFlow(final int series, final int item)
1068         {
1069             if (this.invalid)
1070             {
1071                 return Double.NaN;
1072             }
1073             return getVehicleCount(this.firstMeasurement[series][item], this.secondMeasurement[series][item])
1074                     / this.aggregationPeriod.si;
1075         }
1076 
1077         @Override
1078         public final double getDensity(final int series, final int item)
1079         {
1080             return getFlow(series, item) / getSpeed(series, item);
1081         }
1082 
1083         @Override
1084         public final double getSpeed(final int series, final int item)
1085         {
1086             if (this.invalid)
1087             {
1088                 return Double.NaN;
1089             }
1090             return getSpeed(this.firstMeasurement[series][item], this.secondMeasurement[series][item]);
1091         }
1092 
1093         @Override
1094         public final boolean isAggregate()
1095         {
1096             return this.aggregateLanes;
1097         }
1098 
1099         /**
1100          * Returns the first and the second measurement of a trajectory. For a cross-section this is 1 and the vehicle speed if
1101          * the trajectory crosses the location, and for a path it is the traveled distance and the traveled time. If the
1102          * trajectory didn't cross the cross section or space-time range, both should be 0.
1103          * @param trajectory trajectory
1104          * @param startTime start time of aggregation period
1105          * @param endTime end time of aggregation period
1106          * @param length length of the section (to cut off possible lane overshoot of trajectories)
1107          * @param series series number in the section
1108          * @param measurements array with length 2 to place the first and second measurement in
1109          */
1110         protected abstract void getMeasurements(Trajectory<?> trajectory, Time startTime, Time endTime, Length length,
1111                 int series, double[] measurements);
1112 
1113         /**
1114          * Returns the vehicle count of two related measurement values. For a cross section: vehicle count & sum of speeds (or
1115          * sum of inverted speeds for the harmonic mean). For a path: total traveled distance & total traveled time.
1116          * <p>
1117          * The value will be divided by the aggregation time to calculate flow. Hence, for a cross section the first measurement
1118          * should be returned, while for a path the first measurement divided by the section length should be returned. That
1119          * will end up to equate to {@code q = sum(x)/XT}.
1120          * @param first first measurement value
1121          * @param second second measurement value
1122          * @return flow
1123          */
1124         protected abstract double getVehicleCount(double first, double second);
1125 
1126         /**
1127          * Returns the speed of two related measurement values. For a cross section: vehicle count & sum of speeds (or sum of
1128          * inverted speeds for the harmonic mean). For a path: total traveled distance & total traveled time.
1129          * @param first first measurement value
1130          * @param second second measurement value
1131          * @return speed
1132          */
1133         protected abstract double getSpeed(double first, double second);
1134 
1135     }
1136 
1137     /**
1138      * Class to group multiple sources in plot.
1139      */
1140     // TODO: when sub-sources recalculate responding to a click in the graph, they notify only their coupled plots, which are
1141     // none
1142     private static class MultiFdSource extends AbstractFdSource
1143     {
1144 
1145         /** Sources. */
1146         private FdSource[] sources;
1147 
1148         /** Source names. */
1149         private String[] sourceNames;
1150 
1151         /**
1152          * Constructor.
1153          * @param sources sources
1154          */
1155         MultiFdSource(final Map<String, FdSource> sources)
1156         {
1157             Throw.when(sources == null || sources.size() == 0, IllegalArgumentException.class,
1158                     "At least 1 source is required.");
1159             this.sources = new FdSource[sources.size()];
1160             this.sourceNames = new String[sources.size()];
1161             int index = 0;
1162             for (Entry<String, FdSource> entry : sources.entrySet())
1163             {
1164                 this.sources[index] = entry.getValue();
1165                 this.sourceNames[index] = entry.getKey();
1166                 index++;
1167             }
1168         }
1169 
1170         /**
1171          * Returns from a series number overall, the index of the sub-source and the series index in that source.
1172          * @param series overall series number
1173          * @return index of the sub-source and the series index in that source
1174          */
1175         private int[] getSourceAndSeries(final int series)
1176         {
1177             int source = 0;
1178             int sourceSeries = series;
1179             while (sourceSeries >= this.sources[source].getNumberOfSeries())
1180             {
1181                 sourceSeries -= this.sources[source].getNumberOfSeries();
1182                 source++;
1183             }
1184             return new int[] {source, sourceSeries};
1185         }
1186 
1187         @Override
1188         public Duration getUpdateInterval()
1189         {
1190             return this.sources[0].getUpdateInterval();
1191         }
1192 
1193         @Override
1194         public void setUpdateInterval(final Duration interval, final Time time)
1195         {
1196             for (FdSource source : this.sources)
1197             {
1198                 source.setUpdateInterval(interval, time);
1199             }
1200         }
1201 
1202         @Override
1203         public Duration getAggregationPeriod()
1204         {
1205             return this.sources[0].getAggregationPeriod();
1206         }
1207 
1208         @Override
1209         public void setAggregationPeriod(final Duration period)
1210         {
1211             for (FdSource source : this.sources)
1212             {
1213                 source.setAggregationPeriod(period);
1214             }
1215         }
1216 
1217         @Override
1218         public void recalculate(final Time time)
1219         {
1220             for (FdSource source : this.sources)
1221             {
1222                 source.recalculate(time);
1223             }
1224         }
1225 
1226         @Override
1227         public Duration getDelay()
1228         {
1229             return this.sources[0].getDelay();
1230         }
1231 
1232         @Override
1233         public void increaseTime(final Time time)
1234         {
1235             for (FdSource source : this.sources)
1236             {
1237                 source.increaseTime(time);
1238             }
1239         }
1240 
1241         @Override
1242         public int getNumberOfSeries()
1243         {
1244             int numberOfSeries = 0;
1245             for (FdSource source : this.sources)
1246             {
1247                 numberOfSeries += source.getNumberOfSeries();
1248             }
1249             return numberOfSeries;
1250         }
1251 
1252         @Override
1253         public String getName(final int series)
1254         {
1255             int[] ss = getSourceAndSeries(series);
1256             return this.sourceNames[ss[0]]
1257                     + (this.sources[ss[0]].isAggregate() ? "" : ": " + this.sources[ss[0]].getName(ss[1]));
1258         }
1259 
1260         @Override
1261         public int getItemCount(final int series)
1262         {
1263             int[] ss = getSourceAndSeries(series);
1264             return this.sources[ss[0]].getItemCount(ss[1]);
1265         }
1266 
1267         @Override
1268         public double getFlow(final int series, final int item)
1269         {
1270             int[] ss = getSourceAndSeries(series);
1271             return this.sources[ss[0]].getFlow(ss[1], item);
1272         }
1273 
1274         @Override
1275         public double getDensity(final int series, final int item)
1276         {
1277             int[] ss = getSourceAndSeries(series);
1278             return this.sources[ss[0]].getDensity(ss[1], item);
1279         }
1280 
1281         @Override
1282         public double getSpeed(final int series, final int item)
1283         {
1284             int[] ss = getSourceAndSeries(series);
1285             return this.sources[ss[0]].getSpeed(ss[1], item);
1286         }
1287 
1288         @Override
1289         public boolean isAggregate()
1290         {
1291             return false;
1292         }
1293 
1294         @Override
1295         public void setAggregateName(final String aggregateName)
1296         {
1297             // invalid for this source type
1298         }
1299 
1300     }
1301 
1302     /**
1303      * Defines a line plot for a fundamental diagram.
1304      */
1305     public interface FdLine
1306     {
1307         /**
1308          * Return the values for the given quantity. For two quantities, this should result in a 2D fundamental diagram line.
1309          * @param quantity quantity to return value for.
1310          * @return values for quantity
1311          */
1312         double[] getValues(Quantity quantity);
1313 
1314         /**
1315          * Returns the name of the line, as shown in the legend.
1316          * @return name of the line, as shown in the legend
1317          */
1318         String getName();
1319     }
1320 
1321     @Override
1322     public String toString()
1323     {
1324         return "FundamentalDiagram [source=" + this.getSource() + ", domainQuantity=" + this.getDomainQuantity()
1325                 + ", rangeQuantity=" + this.getRangeQuantity() + ", otherQuantity=" + this.getOtherQuantity()
1326                 + ", seriesLabels=" + this.seriesLabels + ", graphUpdater=" + this.graphUpdater + ", timeInfo="
1327                 + this.getTimeInfo() + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
1328     }
1329 
1330     /**
1331      * Get the data source.
1332      * @return the data source
1333      */
1334     public FdSource getSource()
1335     {
1336         return this.source;
1337     }
1338 
1339     /**
1340      * Retrievee the legend of this FundamentalDiagram.
1341      * @return the legend
1342      */
1343     public LegendItemCollection getLegend()
1344     {
1345         return this.legend;
1346     }
1347 
1348     /**
1349      * Return the list of lane visibility flags.
1350      * @return the list of lane visibility flags
1351      */
1352     public List<Boolean> getLaneVisible()
1353     {
1354         return this.laneVisible;
1355     }
1356 
1357     /**
1358      * Return the domain quantity.
1359      * @return the domain quantity
1360      */
1361     public Quantity getDomainQuantity()
1362     {
1363         return this.domainQuantity;
1364     }
1365 
1366     /**
1367      * Set the domain quantity.
1368      * @param domainQuantity the new domain quantity
1369      */
1370     public void setDomainQuantity(final Quantity domainQuantity)
1371     {
1372         this.domainQuantity = domainQuantity;
1373     }
1374 
1375     /**
1376      * Get the other (non domain; vertical axis) quantity.
1377      * @return the quantity for the vertical axis
1378      */
1379     public Quantity getOtherQuantity()
1380     {
1381         return this.otherQuantity;
1382     }
1383 
1384     /**
1385      * Set the other (non domain; vertical axis) quantity.
1386      * @param otherQuantity the quantity for the vertical axis
1387      */
1388     public void setOtherQuantity(final Quantity otherQuantity)
1389     {
1390         this.otherQuantity = otherQuantity;
1391     }
1392 
1393     /**
1394      * Get the range quantity.
1395      * @return the range quantity
1396      */
1397     public Quantity getRangeQuantity()
1398     {
1399         return this.rangeQuantity;
1400     }
1401 
1402     /**
1403      * Set the range quantity.
1404      * @param rangeQuantity the new range quantity
1405      */
1406     public void setRangeQuantity(final Quantity rangeQuantity)
1407     {
1408         this.rangeQuantity = rangeQuantity;
1409     }
1410 
1411     /**
1412      * Retrieve the time info.
1413      * @return the time info
1414      */
1415     public String getTimeInfo()
1416     {
1417         return this.timeInfo;
1418     }
1419 
1420     /**
1421      * Set the time info.
1422      * @param timeInfo the new time info
1423      */
1424     public void setTimeInfo(final String timeInfo)
1425     {
1426         this.timeInfo = timeInfo;
1427     }
1428 
1429     /**
1430      * Return whether the plot has a fundamental diagram line.
1431      * @return whether the plot has a fundamental diagram line
1432      */
1433     public boolean hasLineFD()
1434     {
1435         return this.fdLine != null;
1436     }
1437 
1438 }