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