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