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