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