View Javadoc
1   package org.opentrafficsim.animation.graphs;
2   
3   import java.awt.Color;
4   import java.util.ArrayList;
5   import java.util.EnumSet;
6   import java.util.List;
7   import java.util.Set;
8   
9   import org.djunits.value.vdouble.scalar.Duration;
10  import org.djutils.event.EventListener;
11  import org.djutils.event.EventType;
12  import org.djutils.exceptions.Throw;
13  import org.djutils.immutablecollections.ImmutableList;
14  import org.jfree.chart.JFreeChart;
15  import org.jfree.chart.LegendItem;
16  import org.jfree.chart.LegendItemCollection;
17  import org.jfree.chart.axis.NumberAxis;
18  import org.jfree.chart.plot.XYPlot;
19  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
20  import org.jfree.data.DomainOrder;
21  import org.jfree.data.xy.XYDataset;
22  import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
23  import org.opentrafficsim.animation.graphs.FundamentalDiagram.FdPaintState;
24  
25  /**
26   * Fundamental diagram from various sources.
27   * <p>
28   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
29   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
30   * </p>
31   * @author Alexander Verbraeck
32   * @author Peter Knoppers
33   * @author Wouter Schakel
34   */
35  public class FundamentalDiagram extends AbstractBoundedPlot<FdPaintState> implements XYDataset
36  {
37  
38      /** Source providing the data. */
39      private final FdDataSource source;
40  
41      /** Fundamental diagram line. */
42      private final FdLine fdLine;
43  
44      /** Quantity on domain axis. */
45      private Quantity domainQuantity;
46  
47      /** Quantity on range axis. */
48      private Quantity rangeQuantity;
49  
50      /** The other, 3rd quantity. */
51      private Quantity otherQuantity;
52  
53      /** Labels of series. */
54      private final List<String> seriesLabels = new ArrayList<>();
55  
56      /** Property for chart listener to provide time info for status label. */
57      private String timeInfo = "";
58  
59      /** Legend to change text color to indicate visibility. */
60      private LegendItemCollection legend;
61  
62      /** Whether each lane is visible or not. */
63      private final List<Boolean> laneVisible = new ArrayList<>();
64  
65      /**
66       * Constructor.
67       * @param caption caption
68       * @param domainQuantity initial quantity on the domain axis
69       * @param rangeQuantity initial quantity on the range axis
70       * @param source source providing the data
71       * @param fdLine fundamental diagram line, may be {@code null}
72       */
73      public FundamentalDiagram(final String caption, final Quantity domainQuantity, final Quantity rangeQuantity,
74              final FdDataSource source, final FdLine fdLine)
75      {
76          super(source.getPlotScheduler(), caption, source.getAggregationPeriod(), source.getDelay());
77          Throw.when(domainQuantity.equals(rangeQuantity), IllegalArgumentException.class,
78                  "Domain and range quantity should not be equal.");
79          this.fdLine = fdLine;
80          this.setDomainQuantity(domainQuantity);
81          this.setRangeQuantity(rangeQuantity);
82          Set<Quantity> quantities = EnumSet.allOf(Quantity.class);
83          quantities.remove(domainQuantity);
84          quantities.remove(rangeQuantity);
85          this.setOtherQuantity(quantities.iterator().next());
86          this.source = source;
87          int d = 0;
88          if (fdLine != null)
89          {
90              d = 1;
91              this.seriesLabels.add(fdLine.getName());
92              this.laneVisible.add(true);
93          }
94          for (int series = 0; series < source.getNumberOfSeries(); series++)
95          {
96              this.seriesLabels.add(series + d, source.getName(series));
97              this.laneVisible.add(true);
98          }
99          setChart(createChart());
100         setLowerDomainBound(0.0);
101         setLowerRangeBound(0.0);
102 
103         // let this diagram be notified by the source
104         source.addPlot(this);
105     }
106 
107     /**
108      * Create a chart.
109      * @return chart
110      */
111     private JFreeChart createChart()
112     {
113         NumberAxis xAxis = new NumberAxis(this.getDomainQuantity().label());
114         NumberAxis yAxis = new NumberAxis(this.getRangeQuantity().label());
115         XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer()
116         {
117             /** Serialization version UID. */
118             private static final long serialVersionUID = 20181022L;
119 
120             @Override
121             public boolean isSeriesVisible(final int series)
122             {
123                 return FundamentalDiagram.this.laneVisible.get(series);
124             }
125         }; // XYDotRenderer doesn't support different markers
126         renderer.setDefaultLinesVisible(false);
127         if (hasLineFD())
128         {
129             int series = getNumberOfSeries();
130             renderer.setSeriesLinesVisible(series, true);
131             renderer.setSeriesPaint(series, Color.BLACK);
132             renderer.setSeriesShapesVisible(series, false);
133         }
134         XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
135         boolean showLegend = true;
136         if (!hasLineFD() && getNumberOfSeries() < 2)
137         {
138             plot.setFixedLegendItems(null);
139             showLegend = false;
140         }
141         else
142         {
143             this.legend = new LegendItemCollection();
144             for (int i = 0; i < getNumberOfSeries(); i++)
145             {
146                 LegendItem li = new LegendItem(this.source.getName(i));
147                 li.setSeriesKey(i); // lane series, not curve series
148                 li.setShape(renderer.lookupLegendShape(i));
149                 li.setFillPaint(renderer.lookupSeriesPaint(i));
150                 this.legend.add(li);
151             }
152             if (hasLineFD())
153             {
154                 LegendItem li = new LegendItem(this.fdLine.getName());
155                 li.setSeriesKey(-1);
156                 this.legend.add(li);
157             }
158             plot.setFixedLegendItems(this.legend);
159             showLegend = true;
160         }
161         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
162     }
163 
164     /**
165      * Returns the possible updates per period values.
166      * @return possible update per period values
167      */
168     public ImmutableList<Integer> getPossibleUpdatesPerPeriod()
169     {
170         return this.source.getUpdatesPerPeriodSetting().values();
171     }
172 
173     /**
174      * Returns the default updates per period.
175      * @return default updates per period
176      */
177     public int getDefaultUpdatesPerPeriod()
178     {
179         return this.source.getUpdatesPerPeriodSetting().getDefaultValue();
180     }
181 
182     /**
183      * Returns the possible aggregation period values.
184      * @return possible aggregation period values
185      */
186     public ImmutableList<Duration> getPossibleAggregationPeriods()
187     {
188         return this.source.getAggregationPeriodSetting().values();
189     }
190 
191     /**
192      * Returns the default aggregation period.
193      * @return default aggregation period
194      */
195     public Duration getDefaultAggregationPeriod()
196     {
197         return this.source.getAggregationPeriodSetting().getDefaultValue();
198     }
199 
200     /**
201      * Add listener to synchronize UI element when another UI element changes a setting, or the setting is changed
202      * programmatically.
203      * @param listener listener
204      * @param eventType event type (i.e. {@code UPDATES_PER_PERIOD} or {@code AGGREGATION_PERIOD})
205      */
206     public void addListener(final EventListener listener, final EventType eventType)
207     {
208         this.source.addListener(listener, eventType);
209     }
210 
211     /**
212      * Returns the number of series.
213      * @return number of series
214      */
215     public int getNumberOfSeries()
216     {
217         return this.source.getNumberOfSeries();
218     }
219 
220     /**
221      * Returns the update interval.
222      * @return update interval
223      */
224     public Duration getUpdateInterval()
225     {
226         return this.source.getUpdateInterval();
227     }
228 
229     /**
230      * Sets the number of updates per period.
231      * @param n number of updates per period
232      */
233     public void setUpdatesPerPeriod(final int n)
234     {
235         this.source.setUpdatesPerPeriod(n);
236         invalidate();
237     }
238 
239     /**
240      * Sets the aggregation period.
241      * @param period aggregation period
242      */
243     public void setAggregationPeriod(final Duration period)
244     {
245         this.source.setAggregationPeriod(period);
246         invalidate();
247     }
248 
249     @Override
250     protected FdPaintState emptyPaintState()
251     {
252         return new FdPaintState(new FdSeries[0], Duration.ZERO);
253     }
254 
255     @Override
256     protected void calculatePaintState(final Duration time)
257     {
258         this.source.calculatePaintStateSafe(time);
259     }
260 
261     @Override
262     protected void setPaintState()
263     {
264         super.setPaintState();
265         getChart().getXYPlot().zoomDomainAxes(0, null, null);
266         getChart().getXYPlot().zoomRangeAxes(0, null, null);
267     }
268 
269     @Override
270     public int getSeriesCount()
271     {
272         return getPaintState().getSeriesCount() + (hasLineFD() ? 1 : 0);
273     }
274 
275     @Override
276     public Comparable<String> getSeriesKey(final int series)
277     {
278         return this.seriesLabels.get(series);
279     }
280 
281     @SuppressWarnings("rawtypes")
282     @Override
283     public int indexOf(final Comparable seriesKey)
284     {
285         int index = this.seriesLabels.indexOf(seriesKey);
286         return index < 0 ? 0 : index;
287     }
288 
289     @Override
290     public DomainOrder getDomainOrder()
291     {
292         return DomainOrder.NONE;
293     }
294 
295     @Override
296     public int getItemCount(final int series)
297     {
298         if (hasLineFD() && series == getSeriesCount() - 1)
299         {
300             return this.fdLine.getValues(this.domainQuantity).length;
301         }
302         return getPaintState().getItemCount(series);
303     }
304 
305     @Override
306     public Number getX(final int series, final int item)
307     {
308         return getXValue(series, item);
309     }
310 
311     @Override
312     public double getXValue(final int series, final int item)
313     {
314         if (hasLineFD() && series == getSeriesCount() - 1)
315         {
316             return this.fdLine.getValues(this.domainQuantity)[item];
317         }
318         return getPaintState().getValue(getDomainQuantity(), series, item);
319     }
320 
321     @Override
322     public Number getY(final int series, final int item)
323     {
324         return getYValue(series, item);
325     }
326 
327     @Override
328     public double getYValue(final int series, final int item)
329     {
330         if (hasLineFD() && series == getSeriesCount() - 1)
331         {
332             return this.fdLine.getValues(this.rangeQuantity)[item];
333         }
334         return getPaintState().getValue(getRangeQuantity(), series, item);
335     }
336 
337     @Override
338     public GraphType getGraphType()
339     {
340         return GraphType.FUNDAMENTAL_DIAGRAM;
341     }
342 
343     @Override
344     public String getStatusLabel(final double domainValue, final double rangeValue)
345     {
346         return getDomainQuantity().format(domainValue) + ", " + getRangeQuantity().format(rangeValue) + ", "
347                 + getOtherQuantity().format(getDomainQuantity().computeOther(getRangeQuantity(), domainValue, rangeValue))
348                 + getTimeInfo();
349     }
350 
351     /**
352      * Retrieve the legend of this FundamentalDiagram.
353      * @return the legend
354      */
355     public LegendItemCollection getLegend()
356     {
357         return this.legend;
358     }
359 
360     /**
361      * Return the list of lane visibility flags.
362      * @return the list of lane visibility flags
363      */
364     public List<Boolean> getLaneVisible()
365     {
366         return this.laneVisible;
367     }
368 
369     /**
370      * Return the domain quantity.
371      * @return the domain quantity
372      */
373     public Quantity getDomainQuantity()
374     {
375         return this.domainQuantity;
376     }
377 
378     /**
379      * Set the domain quantity.
380      * @param domainQuantity the new domain quantity
381      */
382     public void setDomainQuantity(final Quantity domainQuantity)
383     {
384         this.domainQuantity = domainQuantity;
385     }
386 
387     /**
388      * Get the other (non domain; vertical axis) quantity.
389      * @return the quantity for the vertical axis
390      */
391     public Quantity getOtherQuantity()
392     {
393         return this.otherQuantity;
394     }
395 
396     /**
397      * Set the other (non domain; vertical axis) quantity.
398      * @param otherQuantity the quantity for the vertical axis
399      */
400     public void setOtherQuantity(final Quantity otherQuantity)
401     {
402         this.otherQuantity = otherQuantity;
403     }
404 
405     /**
406      * Get the range quantity.
407      * @return the range quantity
408      */
409     public Quantity getRangeQuantity()
410     {
411         return this.rangeQuantity;
412     }
413 
414     /**
415      * Set the range quantity.
416      * @param rangeQuantity the new range quantity
417      */
418     public void setRangeQuantity(final Quantity rangeQuantity)
419     {
420         this.rangeQuantity = rangeQuantity;
421     }
422 
423     /**
424      * Retrieve the time info.
425      * @return the time info
426      */
427     public String getTimeInfo()
428     {
429         return this.timeInfo;
430     }
431 
432     /**
433      * Set the time info.
434      * @param timeInfo the new time info
435      */
436     public void setTimeInfo(final String timeInfo)
437     {
438         this.timeInfo = timeInfo;
439     }
440 
441     /**
442      * Return whether the plot has a fundamental diagram line.
443      * @return whether the plot has a fundamental diagram line
444      */
445     public boolean hasLineFD()
446     {
447         return this.fdLine != null;
448     }
449 
450     @Override
451     public String toString()
452     {
453         return "FundamentalDiagram [domainQuantity=" + this.getDomainQuantity() + ", rangeQuantity=" + this.getRangeQuantity()
454                 + "]";
455     }
456 
457     // ===== Helper classes =====
458 
459     /**
460      * Quantity enum defining density, flow and speed.
461      */
462     public enum Quantity
463     {
464         /** Density. */
465         DENSITY
466         {
467             @Override
468             public String label()
469             {
470                 return "Density [veh/km] \u2192";
471             }
472 
473             @Override
474             public String format(final double value)
475             {
476                 return String.format("%.0f veh/km", value);
477             }
478 
479             @Override
480             public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
481             {
482                 // .......................... speed = flow / density .. flow = density * speed
483                 return pairing.equals(FLOW) ? pairedValue / thisValue : thisValue * pairedValue;
484             }
485         },
486 
487         /** Flow. */
488         FLOW
489         {
490             @Override
491             public String label()
492             {
493                 return "Flow [veh/h] \u2192";
494             }
495 
496             @Override
497             public String format(final double value)
498             {
499                 return String.format("%.0f veh/h", value);
500             }
501 
502             @Override
503             public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
504             {
505                 // speed = flow * density ... density = flow / speed
506                 return pairing.equals(DENSITY) ? thisValue * pairedValue : thisValue / pairedValue;
507             }
508         },
509 
510         /** Speed. */
511         SPEED
512         {
513             @Override
514             public String label()
515             {
516                 return "Speed [km/h] \u2192";
517             }
518 
519             @Override
520             public String format(final double value)
521             {
522                 return String.format("%.1f km/h", value);
523             }
524 
525             @Override
526             public double computeOther(final Quantity pairing, final double thisValue, final double pairedValue)
527             {
528                 // ............................. flow = speed * density .. density = flow / speed
529                 return pairing.equals(DENSITY) ? thisValue * pairedValue : pairedValue / thisValue;
530             }
531         };
532 
533         /**
534          * Returns an axis label of the quantity.
535          * @return axis label of the quantity
536          */
537         public abstract String label();
538 
539         /**
540          * Formats a value for status display.
541          * @param value value
542          * @return formatted string including quantity
543          */
544         public abstract String format(double value);
545 
546         /**
547          * Compute the value of the 3rd quantity.
548          * @param pairing quantity on other axis
549          * @param thisValue value of this quantity
550          * @param pairedValue value of the paired quantity on the other axis
551          * @return value of the 3rd quantity
552          */
553         public abstract double computeOther(Quantity pairing, double thisValue, double pairedValue);
554 
555     }
556 
557     /**
558      * Defines a line plot for a fundamental diagram.
559      */
560     public interface FdLine
561     {
562 
563         /**
564          * Return the values for the given quantity. For two quantities, this should result in a 2D fundamental diagram line.
565          * @param quantity quantity to return value for.
566          * @return values for quantity
567          */
568         double[] getValues(Quantity quantity);
569 
570         /**
571          * Returns the name of the line, as shown in the legend.
572          * @return name of the line, as shown in the legend
573          */
574         String getName();
575 
576     }
577 
578     /**
579      * One data series within a fundamental diagram paint state.
580      * @param q flow
581      * @param v speed
582      * @param k density
583      */
584     public record FdSeries(float[] q, float[] v, float[] k)
585     {
586 
587         /**
588          * Returns one value for a quantity.
589          * @param quantity quantity
590          * @param item item in series
591          * @return one value for a quantity
592          */
593         public double getValue(final Quantity quantity, final int item)
594         {
595             switch (quantity)
596             {
597                 case DENSITY:
598                     return k()[item];
599                 case FLOW:
600                     return q()[item];
601                 case SPEED:
602                     return v()[item];
603                 default:
604                     throw new IllegalArgumentException("Unknown quantity " + quantity);
605             }
606         }
607 
608     }
609 
610     /**
611      * Paint state for fundamental diagram.
612      * @param fdSeries data series
613      * @param getAvailableTime available time
614      */
615     public record FdPaintState(FdSeries[] fdSeries, Duration getAvailableTime) implements PaintState
616     {
617 
618         /**
619          * Returns the number of series.
620          * @return number of series
621          */
622         public int getSeriesCount()
623         {
624             return this.fdSeries().length;
625         }
626 
627         /**
628          * Get number of items for series.
629          * @param series series
630          * @return number of items for series
631          */
632         private int getItemCount(final int series)
633         {
634             return fdSeries()[series].k().length;
635         }
636 
637         /**
638          * Get value for quantity.
639          * @param quantity quantity
640          * @param series series
641          * @param item item in series
642          * @return value for quantity
643          */
644         private double getValue(final Quantity quantity, final int series, final int item)
645         {
646             return fdSeries()[series].getValue(quantity, item);
647         }
648 
649     }
650 
651 }