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