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