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