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 nl.tudelft.simulation.dsol.SimRuntimeException;
18  
19  import org.djunits.unit.FrequencyUnit;
20  import org.djunits.unit.LinearDensityUnit;
21  import org.djunits.unit.SpeedUnit;
22  import org.djunits.value.vdouble.scalar.Frequency;
23  import org.djunits.value.vdouble.scalar.LinearDensity;
24  import org.djunits.value.vdouble.scalar.Speed;
25  import org.djunits.value.vdouble.scalar.Time;
26  import org.jfree.chart.ChartFactory;
27  import org.jfree.chart.ChartPanel;
28  import org.jfree.chart.JFreeChart;
29  import org.jfree.chart.StandardChartTheme;
30  import org.jfree.chart.axis.NumberAxis;
31  import org.jfree.chart.axis.ValueAxis;
32  import org.jfree.chart.event.AxisChangeEvent;
33  import org.jfree.chart.plot.PlotOrientation;
34  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
35  import org.jfree.data.DomainOrder;
36  import org.jfree.data.general.DatasetChangeEvent;
37  import org.jfree.data.general.DatasetChangeListener;
38  import org.jfree.data.general.DatasetGroup;
39  import org.jfree.data.xy.XYDataset;
40  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
41  import org.opentrafficsim.core.gtu.RelativePosition;
42  import org.opentrafficsim.core.network.NetworkException;
43  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
44  import org.opentrafficsim.road.network.lane.AbstractSensor;
45  import org.opentrafficsim.road.network.lane.Lane;
46  
47  /**
48   * The Fundamental Diagram Graph; see <a href="http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow"> Wikipedia:
49   * http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow</a>.
50   * <p>
51   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
52   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
53   * <p>
54   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
55   * initial version Jul 31, 2014 <br>
56   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
57   */
58  public class FundamentalDiagramLane extends JFrame implements XYDataset, ActionListener
59  {
60      /** */
61      private static final long serialVersionUID = 20140701L;
62  
63      /** The ChartPanel for this Fundamental Diagram. */
64      private JFreeChart chartPanel;
65  
66      /** Caption for this Fundamental Diagram. */
67      private final String caption;
68  
69      /** Area to show status information. */
70      private final JLabel statusLabel;
71  
72      /** Sample duration of the detector that generates this Fundamental Diagram. */
73      private final Time.Rel aggregationTime;
74  
75      /** Storage for the Samples. */
76      private ArrayList<Sample> samples = new ArrayList<Sample>();
77  
78      /** Definition of the density axis. */
79      private Axis densityAxis = new Axis(new LinearDensity(0, LinearDensityUnit.PER_KILOMETER), new LinearDensity(200,
80          LinearDensityUnit.PER_KILOMETER), null, 0d, "Density [veh/km]", "Density", "density %.1f veh/km");
81  
82      /**
83       * @return densityAxis
84       */
85      public final Axis getDensityAxis()
86      {
87          return this.densityAxis;
88      }
89  
90      /** Definition of the speed axis. */
91      private Axis speedAxis = new Axis(new Speed(0, SpeedUnit.KM_PER_HOUR), new Speed(120, SpeedUnit.KM_PER_HOUR), null,
92          0d, "Speed [km/h]", "Speed", "speed %.0f km/h");
93  
94      /**
95       * @return speedAxis
96       */
97      public final Axis getSpeedAxis()
98      {
99          return this.speedAxis;
100     }
101 
102     /**
103      * @return flowAxis
104      */
105     public final Axis getFlowAxis()
106     {
107         return this.flowAxis;
108     }
109 
110     /** Definition of the flow axis. */
111     private Axis flowAxis = new Axis(new Frequency(0, FrequencyUnit.PER_HOUR),
112         new Frequency(3000d, FrequencyUnit.HERTZ), null, 0d, "Flow [veh/h]", "Flow", "flow %.0f veh/h");
113 
114     /** The currently shown X-axis. */
115     private Axis xAxis;
116 
117     /** The currently shown Y-axis. */
118     private Axis yAxis;
119 
120     /** List of parties interested in changes of this ContourPlot. */
121     private transient EventListenerList listenerList = new EventListenerList();
122 
123     /** Not used internally. */
124     private DatasetGroup datasetGroup = null;
125 
126     /** The lane for which data is gathered. */
127     private final Lane lane;
128 
129     /** The simulator to schedule sampling. */
130     private final OTSDEVSSimulatorInterface simulator;
131 
132     /** Flow counter. */
133 
134     int flow = 0;
135 
136     /**
137      * Retrieve the format string for the Y axis.
138      * @return format string
139      */
140     public final String getYAxisFormat()
141     {
142         return this.yAxis.getFormat();
143     }
144 
145     /**
146      * Retrieve the format string for the X axis.
147      * @return format string
148      */
149     public final String getXAxisFormat()
150     {
151         return this.xAxis.getFormat();
152     }
153 
154     /**
155      * Graph a Fundamental Diagram.
156      * @param caption String; the caption shown above the graphing area.
157      * @param aggregationTime DoubleScalarRel&lt;TimeUnit&gt;; the aggregation of the detector that generates the data for this
158      *            Fundamental diagram
159      * @param lane Lane; the Lane on which the traffic will be sampled
160      * @param simulator the simulator to schedule the sampling on
161      * @throws NetworkException on network inconsistency
162      * @throws SimRuntimeException in case scheduling of the sampler fails
163      */
164     public FundamentalDiagramLane(final String caption, final Time.Rel aggregationTime, final Lane lane,
165         final OTSDEVSSimulatorInterface simulator) throws NetworkException, SimRuntimeException
166     {
167         if (aggregationTime.getSI() <= 0)
168         {
169             throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")");
170         }
171         this.aggregationTime = aggregationTime;
172         this.caption = caption;
173         this.lane = lane;
174         this.simulator = simulator;
175         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
176         this.chartPanel =
177             ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
178         FixCaption.fixCaption(this.chartPanel);
179         final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer();
180         renderer.setBaseShapesVisible(true);
181 
182         final ChartPanel cp = new ChartPanel(this.chartPanel);
183         PointerHandler ph = new PointerHandler()
184         {
185             /** {@inheritDoc} */
186             @Override
187             void updateHint(final double domainValue, final double rangeValue)
188             {
189                 if (Double.isNaN(domainValue))
190                 {
191                     setStatusText(" ");
192                     return;
193                 }
194                 String s1 = String.format(getXAxisFormat(), domainValue);
195                 String s2 = String.format(getYAxisFormat(), rangeValue);
196                 setStatusText(s1 + ", " + s2);
197             }
198 
199         };
200         cp.addMouseMotionListener(ph);
201         cp.addMouseListener(ph);
202         cp.setMouseWheelEnabled(true);
203         final JMenu subMenu = new JMenu("Set layout");
204         final ButtonGroup group = new ButtonGroup();
205         final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true);
206         addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false);
207         addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false);
208         actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand()));
209         final JPopupMenu popupMenu = cp.getPopupMenu();
210         popupMenu.insert(subMenu, 0);
211         this.add(cp, BorderLayout.CENTER);
212         this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
213         this.add(this.statusLabel, BorderLayout.SOUTH);
214         simulator.scheduleEventRel(this.aggregationTime, this, this, "addData", null);
215         new FlowSensor(lane);
216     }
217 
218     /**
219      * Update the status text.
220      * @param newText String; the new text to show
221      */
222     public final void setStatusText(final String newText)
223     {
224         this.statusLabel.setText(newText);
225     }
226 
227     /**
228      * @return aggregationTime
229      */
230     public final Time.Rel getAggregationTime()
231     {
232         return this.aggregationTime;
233     }
234 
235     /**
236      * Build one JRadioButtonMenuItem for the sub menu of the context menu.
237      * @param subMenu JMenu; the menu to which the new JRadioButtonMenuItem is added
238      * @param group ButtonGroup; the buttonGroup for the new JRadioButtonMenuItem
239      * @param xAxisToSelect Axis; the Axis that will become X-axis when this item is clicked
240      * @param yAxisToSelect Axis; the Axis that will become Y-axis when this item is clicked
241      * @param selected Boolean; if true, the new JRadioButtonMenuItem will be selected; if false, the new JRadioButtonMenuItem
242      *            will <b>not</b> be selected
243      * @return JRatioButtonMenuItem; the newly added item
244      */
245     private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect,
246         final Axis yAxisToSelect, final boolean selected)
247     {
248         final JRadioButtonMenuItem item =
249             new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName());
250         item.setSelected(selected);
251         item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName());
252         item.addActionListener(this);
253         subMenu.add(item);
254         group.add(item);
255         return item;
256     }
257 
258     /**
259      * Add the density and average speed on the lane to this Fundamental Diagram.
260      * @throws SimRuntimeException when scheduling of next sampling time fails
261      */
262     public final void addData() throws SimRuntimeException
263     {
264         // collect (harmonic) mean speed and number of vehicles per meter on the lane
265         double n = this.lane.getGtuList().size();
266         double density = n / this.lane.getLength().si;
267         if (density > 0.0)
268         {
269             double meanSpeed = 0.0;
270             for (LaneBasedGTU gtu : this.lane.getGtuList())
271             {
272                 meanSpeed += 1 / gtu.getVelocity().si;
273             }
274             meanSpeed = n / meanSpeed;
275             this.samples.add(new Sample(meanSpeed, density, this.flow / this.aggregationTime.si));
276             this.flow = 0;
277         }
278         this.simulator.scheduleEventRel(this.aggregationTime, this, this, "addData", null);
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         return result;
402     }
403 
404     /** {@inheritDoc} */
405     @Override
406     public final Number getX(final int series, final int item)
407     {
408         return getXValue(series, item);
409     }
410 
411     /** {@inheritDoc} */
412     @Override
413     public final double getXValue(final int series, final int item)
414     {
415         return getSample(item, this.xAxis);
416     }
417 
418     /** {@inheritDoc} */
419     @Override
420     public final Number getY(final int series, final int item)
421     {
422         return getYValue(series, item);
423     }
424 
425     /** {@inheritDoc} */
426     @Override
427     public final double getYValue(final int series, final int item)
428     {
429         return getSample(item, this.yAxis);
430     }
431 
432     /** {@inheritDoc} */
433     @Override
434     public final void actionPerformed(final ActionEvent actionEvent)
435     {
436         final String command = actionEvent.getActionCommand();
437         // System.out.println("command is \"" + command + "\"");
438         final String[] fields = command.split("[/]");
439         if (fields.length == 2)
440         {
441             for (String field : fields)
442             {
443                 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
444                 {
445                     if (field == fields[0])
446                     {
447                         this.yAxis = this.densityAxis;
448                     }
449                     else
450                     {
451                         this.xAxis = this.densityAxis;
452                     }
453                 }
454                 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
455                 {
456                     if (field == fields[0])
457                     {
458                         this.yAxis = this.flowAxis;
459                     }
460                     else
461                     {
462                         this.xAxis = this.flowAxis;
463                     }
464                 }
465                 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
466                 {
467                     if (field == fields[0])
468                     {
469                         this.yAxis = this.speedAxis;
470                     }
471                     else
472                     {
473                         this.xAxis = this.speedAxis;
474                     }
475                 }
476                 else
477                 {
478                     throw new Error("Cannot find axis name: " + field);
479                 }
480             }
481             reGraph();
482         }
483         else
484         {
485             throw new Error("Unknown ActionEvent");
486         }
487     }
488 
489     /**
490      * Storage for one sample of data collected with mean speed [m/s] and number of vehicles per km. Flow per second can be
491      * calculated from these two numbers; currently the flow is provided (but never used).
492      * <p>
493      * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
494      * </p>
495      * $LastChangedDate: 2015-07-15 11:18:39 +0200 (Wed, 15 Jul 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
496      * initial versionJul 31, 2014 <br>
497      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
498      */
499     class Sample
500     {
501         /** Mean speed observed during this sample [m/s]. */
502         private final double meanSpeed;
503 
504         /** Density [veh/m]. */
505         private final double density;
506 
507         /** Flow [veh/s]. */
508         private final double flow;
509 
510         /**
511          * @param meanSpeed mean speed observed during this sample [m/s]
512          * @param density density [veh/m]
513          * @param flow [veh/s]
514          */
515         public Sample(final double meanSpeed, final double density, final double flow)
516         {
517             super();
518             this.meanSpeed = meanSpeed;
519             this.density = density;
520             this.flow = flow;
521         }
522 
523         /**
524          * Retrieve a value stored in this Sample.
525          * @param axis Axis; the axis along which the data is requested
526          * @return double; the retrieved value
527          */
528         public double getValue(final Axis axis)
529         {
530             if (axis == getDensityAxis())
531             {
532                 return 1000.0 * this.density; // [veh/km]
533             }
534             else if (axis == getFlowAxis())
535             {
536                 return 3600.0 * this.meanSpeed * this.density; // [veh/h]
537             }
538             else if (axis == getSpeedAxis())
539             {
540                 return 3.6 * this.meanSpeed; // [km / h]
541             }
542             else
543             {
544                 throw new Error("Sample.getValue: Can not identify axis");
545             }
546         }
547     }
548 
549     /** */
550     private class FlowSensor extends AbstractSensor
551     {
552         /** */
553         private static final long serialVersionUID = 1L;
554 
555         /**
556          * @param lane the lane for which to build the flowSensor
557          */
558         public FlowSensor(final Lane lane)
559         {
560             super(lane, lane.getLength().divideBy(2.0), RelativePosition.FRONT, "FLOW", null);
561         }
562 
563         /** {@inheritDoc} */
564         @Override
565         public void trigger(final LaneBasedGTU gtu)
566         {
567             FundamentalDiagramLane.this.flow += 1;
568         }
569 
570     }
571 }