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