View Javadoc
1   package org.opentrafficsim.graphs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.event.ActionEvent;
5   import java.awt.event.ActionListener;
6   import java.io.Serializable;
7   import java.util.ArrayList;
8   
9   import javax.swing.ButtonGroup;
10  import javax.swing.JFrame;
11  import javax.swing.JLabel;
12  import javax.swing.JMenu;
13  import javax.swing.JPopupMenu;
14  import javax.swing.JRadioButtonMenuItem;
15  import javax.swing.SwingConstants;
16  import javax.swing.event.EventListenerList;
17  
18  import org.djunits.unit.FrequencyUnit;
19  import org.djunits.unit.LinearDensityUnit;
20  import org.djunits.unit.SpeedUnit;
21  import org.djunits.value.vdouble.scalar.Duration;
22  import org.djunits.value.vdouble.scalar.Frequency;
23  import org.djunits.value.vdouble.scalar.Length;
24  import org.djunits.value.vdouble.scalar.LinearDensity;
25  import org.djunits.value.vdouble.scalar.Speed;
26  import org.djunits.value.vdouble.scalar.Time;
27  import org.jfree.chart.ChartFactory;
28  import org.jfree.chart.ChartPanel;
29  import org.jfree.chart.JFreeChart;
30  import org.jfree.chart.StandardChartTheme;
31  import org.jfree.chart.axis.NumberAxis;
32  import org.jfree.chart.axis.ValueAxis;
33  import org.jfree.chart.event.AxisChangeEvent;
34  import org.jfree.chart.labels.XYItemLabelGenerator;
35  import org.jfree.chart.plot.PlotOrientation;
36  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
37  import org.jfree.data.DomainOrder;
38  import org.jfree.data.general.DatasetChangeEvent;
39  import org.jfree.data.general.DatasetChangeListener;
40  import org.jfree.data.general.DatasetGroup;
41  import org.jfree.data.xy.XYDataset;
42  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
43  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
44  import org.opentrafficsim.core.gtu.GTUException;
45  import org.opentrafficsim.core.gtu.RelativePosition;
46  import org.opentrafficsim.core.network.NetworkException;
47  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
48  import org.opentrafficsim.road.network.lane.CrossSectionElement;
49  import org.opentrafficsim.road.network.lane.Lane;
50  import org.opentrafficsim.road.network.lane.object.sensor.AbstractSensor;
51  
52  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
53  import nl.tudelft.simulation.language.Throw;
54  
55  /**
56   * The Fundamental Diagram Graph; see <a href="http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow"> Wikipedia:
57   * http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow</a>.
58   * <p>
59   * Copyright (c) 2013-2017 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
60   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
61   * <p>
62   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
63   * initial version Jul 31, 2014 <br>
64   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
65   */
66  public class FundamentalDiagram extends JFrame implements XYDataset, ActionListener, Serializable
67  {
68      /** */
69      private static final long serialVersionUID = 20140701L;
70  
71      /** The ChartPanel for this Fundamental Diagram. */
72      private JFreeChart chartPanel;
73  
74      /** Caption for this Fundamental Diagram. */
75      private final String caption;
76  
77      /** Position of this Fundamental Diagram sensor. */
78      private final Length position;
79  
80      /** Area to show status information. */
81      private final JLabel statusLabel;
82  
83      /** Sample duration of the detector that generates this Fundamental Diagram. */
84      private final Duration aggregationTime;
85  
86      /**
87       * @return aggregationTime
88       */
89      public final Duration getAggregationTime()
90      {
91          return this.aggregationTime;
92      }
93  
94      /** Storage for the Samples. */
95      private ArrayList<Sample> samples = new ArrayList<Sample>();
96  
97      /** Definition of the density axis. */
98      private Axis densityAxis = new Axis(new LinearDensity(0, LinearDensityUnit.PER_KILOMETER),
99              new LinearDensity(200, LinearDensityUnit.PER_KILOMETER), null, 0d, "Density [veh/km]", "Density",
100             "density %.1f veh/km");
101 
102     /**
103      * @return densityAxis
104      */
105     public final Axis getDensityAxis()
106     {
107         return this.densityAxis;
108     }
109 
110     /** Definition of the speed axis. */
111     private Axis speedAxis = new Axis(new Speed(0, SpeedUnit.KM_PER_HOUR), new Speed(180, SpeedUnit.KM_PER_HOUR), null, 0d,
112             "Speed [km/h]", "Speed", "speed %.0f km/h");
113 
114     /**
115      * @return speedAxis
116      */
117     public final Axis getSpeedAxis()
118     {
119         return this.speedAxis;
120     }
121 
122     /**
123      * @return flowAxis
124      */
125     public final Axis getFlowAxis()
126     {
127         return this.flowAxis;
128     }
129 
130     /** Definition of the flow axis. */
131     private Axis flowAxis = new Axis(new Frequency(0, FrequencyUnit.PER_HOUR), new Frequency(3000d, FrequencyUnit.HERTZ), null,
132             0d, "Flow [veh/h]", "Flow", "flow %.0f veh/h");
133 
134     /** The currently shown X-axis. */
135     private Axis xAxis;
136 
137     /** The currently shown Y-axis. */
138     private Axis yAxis;
139 
140     /** List of parties interested in changes of this ContourPlot. */
141     private transient EventListenerList listenerList = new EventListenerList();
142 
143     /** Not used internally. */
144     private DatasetGroup datasetGroup = null;
145 
146     /**
147      * Retrieve the format string for the Y axis.
148      * @return format string
149      */
150     public final String getYAxisFormat()
151     {
152         return this.yAxis.getFormat();
153     }
154 
155     /**
156      * Retrieve the format string for the X axis.
157      * @return format string
158      */
159     public final String getXAxisFormat()
160     {
161         return this.xAxis.getFormat();
162     }
163 
164     /**
165      * Graph a Fundamental Diagram.
166      * @param caption String; the caption shown above the graphing area.
167      * @param aggregationTime DoubleScalarRel&lt;TimeUnit&gt;; the aggregation of the detector that generates the data for this
168      *            Fundamental diagram
169      * @param lane Lane; the Lane on which the traffic will be sampled
170      * @param position DoubleScalarRel&lt;LengthUnit&gt;; longitudinal position of the detector on the Lane
171      * @param simulator the simulator
172      * @throws NetworkException on network inconsistency
173      */
174     public FundamentalDiagram(final String caption, final Duration aggregationTime, final Lane lane, final Length position,
175             final OTSDEVSSimulatorInterface simulator) throws NetworkException
176     {
177         if (aggregationTime.getSI() <= 0)
178         {
179             throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")");
180         }
181         this.aggregationTime = aggregationTime;
182         this.caption = caption;
183         this.position = position;
184         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
185         this.chartPanel =
186                 ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
187         FixCaption.fixCaption(this.chartPanel);
188         final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer();
189         renderer.setBaseLinesVisible(true);
190         renderer.setBaseShapesVisible(true);
191         renderer.setBaseItemLabelGenerator(new XYItemLabelGenerator()
192         {
193             @Override
194             public String generateLabel(final XYDataset dataset, final int series, final int item)
195             {
196                 return String.format("%.0fs", item * aggregationTime.getSI());
197             }
198         });
199         renderer.setBaseItemLabelsVisible(true);
200         final ChartPanel cp = new ChartPanel(this.chartPanel);
201         PointerHandler ph = new PointerHandler()
202         {
203             /** */
204             private static final long serialVersionUID = 20140000L;
205 
206             /** {@inheritDoc} */
207             @Override
208             void updateHint(final double domainValue, final double rangeValue)
209             {
210                 if (Double.isNaN(domainValue))
211                 {
212                     setStatusText(" ");
213                     return;
214                 }
215                 String s1 = String.format(getXAxisFormat(), domainValue);
216                 String s2 = String.format(getYAxisFormat(), rangeValue);
217                 setStatusText(s1 + ", " + s2);
218             }
219 
220         };
221         cp.addMouseMotionListener(ph);
222         cp.addMouseListener(ph);
223         cp.setMouseWheelEnabled(true);
224         final JMenu subMenu = new JMenu("Set layout");
225         final ButtonGroup group = new ButtonGroup();
226         final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true);
227         addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false);
228         addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false);
229         actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand()));
230         final JPopupMenu popupMenu = cp.getPopupMenu();
231         popupMenu.insert(subMenu, 0);
232         this.add(cp, BorderLayout.CENTER);
233         this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
234         this.add(this.statusLabel, BorderLayout.SOUTH);
235         new FundamentalDiagramSensor(lane, position, simulator);
236     }
237 
238     /**
239      * Update the status text.
240      * @param newText String; the new text to show
241      */
242     public final void setStatusText(final String newText)
243     {
244         this.statusLabel.setText(newText);
245     }
246 
247     /**
248      * Retrieve the position of the detector.
249      * @return Length; the position of the detector
250      */
251     public final Length getPosition()
252     {
253         return this.position;
254     }
255 
256     /**
257      * Build one JRadioButtonMenuItem for the sub menu of the context menu.
258      * @param subMenu JMenu; the menu to which the new JRadioButtonMenuItem is added
259      * @param group ButtonGroup; the buttonGroup for the new JRadioButtonMenuItem
260      * @param xAxisToSelect Axis; the Axis that will become X-axis when this item is clicked
261      * @param yAxisToSelect Axis; the Axis that will become Y-axis when this item is clicked
262      * @param selected Boolean; if true, the new JRadioButtonMenuItem will be selected; if false, the new JRadioButtonMenuItem
263      *            will <b>not</b> be selected
264      * @return JRatioButtonMenuItem; the newly added item
265      */
266     private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect,
267             final Axis yAxisToSelect, final boolean selected)
268     {
269         final JRadioButtonMenuItem item =
270                 new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName());
271         item.setSelected(selected);
272         item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName());
273         item.addActionListener(this);
274         subMenu.add(item);
275         group.add(item);
276         return item;
277     }
278 
279     /**
280      * Add the effect of one passing car to this Fundamental Diagram.
281      * @param gtu AbstractLaneBasedGTU; the GTU that passes the detection point
282      * @throws GTUException when the speed of the GTU cannot be assessed
283      */
284     public final void addData(final LaneBasedGTU gtu) throws GTUException
285     {
286         Time detectionTime = gtu.getSimulator().getSimulatorTime().getTime();
287         // Figure out the time bin
288         final int timeBin = (int) Math.floor(detectionTime.getSI() / this.aggregationTime.getSI());
289         // Extend storage if needed
290         while (timeBin >= this.samples.size())
291         {
292             this.samples.add(new Sample());
293         }
294         Sample sample = this.samples.get(timeBin);
295         sample.addData(gtu.getSpeed());
296     }
297 
298     /**
299      * Set up a JFreeChart axis.
300      * @param valueAxis ValueAxis; the axis to set up
301      * @param axis Axis; the Axis that provides the data to setup the ValueAxis
302      */
303     private static void configureAxis(final ValueAxis valueAxis, final Axis axis)
304     {
305         valueAxis.setLabel("\u2192 " + axis.getName());
306         valueAxis.setRange(axis.getMinimumValue().getInUnit(), axis.getMaximumValue().getInUnit());
307     }
308 
309     /**
310      * Redraw this TrajectoryGraph (after the underlying data has been changed, or to change axes).
311      */
312     public final void reGraph()
313     {
314         NumberAxis numberAxis = new NumberAxis();
315         configureAxis(numberAxis, this.xAxis);
316         this.chartPanel.getXYPlot().setDomainAxis(numberAxis);
317         this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
318         numberAxis = new NumberAxis();
319         configureAxis(numberAxis, this.yAxis);
320         this.chartPanel.getXYPlot().setRangeAxis(numberAxis);
321         this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
322         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
323     }
324 
325     /**
326      * Notify interested parties of an event affecting this TrajectoryPlot.
327      * @param event DatasetChangedEvent
328      */
329     private void notifyListeners(final DatasetChangeEvent event)
330     {
331         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
332         {
333             dcl.datasetChanged(event);
334         }
335     }
336 
337     /** {@inheritDoc} */
338     @Override
339     public final int getSeriesCount()
340     {
341         return 1;
342     }
343 
344     /** {@inheritDoc} */
345     @Override
346     public final Comparable<Integer> getSeriesKey(final int series)
347     {
348         return series;
349     }
350 
351     /** {@inheritDoc} */
352     @SuppressWarnings("rawtypes")
353     @Override
354     public final int indexOf(final Comparable seriesKey)
355     {
356         if (seriesKey instanceof Integer)
357         {
358             return (Integer) seriesKey;
359         }
360         return -1;
361     }
362 
363     /** {@inheritDoc} */
364     @Override
365     public final void addChangeListener(final DatasetChangeListener listener)
366     {
367         this.listenerList.add(DatasetChangeListener.class, listener);
368     }
369 
370     /** {@inheritDoc} */
371     @Override
372     public final void removeChangeListener(final DatasetChangeListener listener)
373     {
374         this.listenerList.remove(DatasetChangeListener.class, listener);
375     }
376 
377     /** {@inheritDoc} */
378     @Override
379     public final DatasetGroup getGroup()
380     {
381         return this.datasetGroup;
382     }
383 
384     /** {@inheritDoc} */
385     @Override
386     public final void setGroup(final DatasetGroup group)
387     {
388         this.datasetGroup = group;
389     }
390 
391     /** {@inheritDoc} */
392     @Override
393     public final DomainOrder getDomainOrder()
394     {
395         return DomainOrder.ASCENDING;
396     }
397 
398     /** {@inheritDoc} */
399     @Override
400     public final int getItemCount(final int series)
401     {
402         return this.samples.size();
403     }
404 
405     /**
406      * Retrieve a value from the recorded samples.
407      * @param item Integer; the rank number of the sample
408      * @param axis Axis; the axis that determines which quantity to retrieve
409      * @return Double; the requested value, or Double.NaN if the sample does not (yet) exist
410      */
411     private Double getSample(final int item, final Axis axis)
412     {
413         if (item >= this.samples.size())
414         {
415             return Double.NaN;
416         }
417         double result = this.samples.get(item).getValue(axis);
418         /*-
419         System.out.println(String.format("getSample(item=%d, axis=%s) returns %f", item, axis.name,
420                 result));
421          */
422         return result;
423     }
424 
425     /** {@inheritDoc} */
426     @Override
427     public final Number getX(final int series, final int item)
428     {
429         return getXValue(series, item);
430     }
431 
432     /** {@inheritDoc} */
433     @Override
434     public final double getXValue(final int series, final int item)
435     {
436         return getSample(item, this.xAxis);
437     }
438 
439     /** {@inheritDoc} */
440     @Override
441     public final Number getY(final int series, final int item)
442     {
443         return getYValue(series, item);
444     }
445 
446     /** {@inheritDoc} */
447     @Override
448     public final double getYValue(final int series, final int item)
449     {
450         return getSample(item, this.yAxis);
451     }
452 
453     /** {@inheritDoc} */
454     @Override
455     public final String toString()
456     {
457         return "FundamentalDiagram [caption=" + this.caption + ", aggregationTime=" + this.aggregationTime + ", samples.size="
458                 + this.samples.size() + "]";
459     }
460 
461     /**
462      * Storage for one sample of data collected by a point-detector that accumulates harmonic mean speed and flow.
463      */
464     class Sample implements Serializable
465     {
466         /** */
467         private static final long serialVersionUID = 20140000L;
468 
469         /** Harmonic mean speed observed during this sample [m/s]. */
470         private double harmonicMeanSpeed;
471 
472         /** Flow observed during this sample [veh/s]. */
473         private double flow;
474 
475         /**
476          * Retrieve a value stored in this Sample.
477          * @param axis Axis; the axis along which the data is requested
478          * @return double; the retrieved value
479          */
480         public double getValue(final Axis axis)
481         {
482             if (axis == getDensityAxis())
483             {
484                 return this.flow * 3600 / getAggregationTime().getSI() / this.harmonicMeanSpeed;
485             }
486             else if (axis == getFlowAxis())
487             {
488                 return this.flow * 3600 / getAggregationTime().getSI();
489             }
490             else if (axis == getSpeedAxis())
491             {
492                 return this.harmonicMeanSpeed * 3600 / 1000;
493             }
494             else
495             {
496                 throw new Error("Sample.getValue: Can not identify axis");
497             }
498         }
499 
500         /**
501          * Add one Car detection to this Sample.
502          * @param speed Speed; the detected speed
503          */
504         public void addData(final Speed speed)
505         {
506             double sumReciprocalSpeeds = 0;
507             if (this.flow > 0)
508             {
509                 sumReciprocalSpeeds = this.flow / this.harmonicMeanSpeed;
510             }
511             this.flow += 1;
512             sumReciprocalSpeeds += 1d / speed.getSI();
513             this.harmonicMeanSpeed = this.flow / sumReciprocalSpeeds;
514         }
515 
516         /** {@inheritDoc} */
517         @Override
518         public final String toString()
519         {
520             return "Sample [harmonicMeanSpeed=" + this.harmonicMeanSpeed + ", flow=" + this.flow + "]";
521         }
522     }
523 
524     /** {@inheritDoc} */
525     @SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ")
526     @Override
527     public final void actionPerformed(final ActionEvent actionEvent)
528     {
529         final String command = actionEvent.getActionCommand();
530         // System.out.println("command is \"" + command + "\"");
531         final String[] fields = command.split("[/]");
532         if (fields.length == 2)
533         {
534             for (String field : fields)
535             {
536                 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
537                 {
538                     if (field == fields[0])
539                     {
540                         this.yAxis = this.densityAxis;
541                     }
542                     else
543                     {
544                         this.xAxis = this.densityAxis;
545                     }
546                 }
547                 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
548                 {
549                     if (field == fields[0])
550                     {
551                         this.yAxis = this.flowAxis;
552                     }
553                     else
554                     {
555                         this.xAxis = this.flowAxis;
556                     }
557                 }
558                 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
559                 {
560                     if (field == fields[0])
561                     {
562                         this.yAxis = this.speedAxis;
563                     }
564                     else
565                     {
566                         this.xAxis = this.speedAxis;
567                     }
568                 }
569                 else
570                 {
571                     throw new Error("Cannot find axis name: " + field);
572                 }
573             }
574             reGraph();
575         }
576         else
577         {
578             throw new Error("Unknown ActionEvent");
579         }
580     }
581 
582     /**
583      * Internal Sensor class.
584      * <p>
585      * Copyright (c) 2013-2017 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
586      * <br>
587      * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
588      * <p>
589      * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
590      * initial version feb. 2015 <br>
591      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
592      */
593     class FundamentalDiagramSensor extends AbstractSensor
594     {
595         /** */
596         private static final long serialVersionUID = 20150203L;
597 
598         /**
599          * Construct a FundamentalDiagramSensor.
600          * @param lane Lane; the Lane on which the new FundamentalDiagramSensor is to be added
601          * @param longitudinalPosition Length; longitudinal position on the Lane of the new FundamentalDiagramSensor
602          * @param simulator simulator to allow animation
603          * @throws NetworkException on network inconsistency
604          */
605         FundamentalDiagramSensor(final Lane lane, final Length longitudinalPosition, final OTSDEVSSimulatorInterface simulator)
606                 throws NetworkException
607         {
608             super("FUNDAMENTAL_DIAGRAM_SENSOR@" + lane.toString(), lane, longitudinalPosition, RelativePosition.REFERENCE,
609                     simulator);
610         }
611 
612         /** {@inheritDoc} */
613         @Override
614         public void triggerResponse(final LaneBasedGTU gtu)
615         {
616             try
617             {
618                 addData(gtu);
619             }
620             catch (GTUException exception)
621             {
622                 exception.printStackTrace(); // TODO
623             }
624         }
625 
626         /** {@inheritDoc} */
627         public final String toString()
628         {
629             return "FundamentalDiagramSensor at " + getLongitudinalPosition();
630         }
631 
632         /** {@inheritDoc} */
633         @Override
634         public FundamentalDiagramSensor clone(final CrossSectionElement newCSE, final OTSSimulatorInterface newSimulator,
635                 final boolean animation) throws NetworkException
636         {
637             Throw.when(!(newCSE instanceof Lane), NetworkException.class, "sensors can only be cloned for Lanes");
638             Throw.when(!(newSimulator instanceof OTSDEVSSimulatorInterface), NetworkException.class,
639                     "simulator should be a DEVSSimulator");
640             return new FundamentalDiagramSensor((Lane) newCSE, getLongitudinalPosition(),
641                     (OTSDEVSSimulatorInterface) newSimulator);
642         }
643 
644     }
645 
646 }