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.LinearDensityUnit;
20  import org.djunits.value.vdouble.scalar.DoubleScalar;
21  import org.jfree.chart.ChartFactory;
22  import org.jfree.chart.ChartPanel;
23  import org.jfree.chart.JFreeChart;
24  import org.jfree.chart.StandardChartTheme;
25  import org.jfree.chart.axis.NumberAxis;
26  import org.jfree.chart.axis.ValueAxis;
27  import org.jfree.chart.event.AxisChangeEvent;
28  import org.jfree.chart.plot.PlotOrientation;
29  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
30  import org.jfree.data.DomainOrder;
31  import org.jfree.data.general.DatasetChangeEvent;
32  import org.jfree.data.general.DatasetChangeListener;
33  import org.jfree.data.general.DatasetGroup;
34  import org.jfree.data.xy.XYDataset;
35  import org.opentrafficsim.core.OTS_SCALAR;
36  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
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 FundamentalDiagramLane 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      /** Area to show status information. */
66      private final JLabel statusLabel;
67  
68      /** Sample duration of the detector that generates this Fundamental Diagram. */
69      private final Time.Rel aggregationTime;
70  
71      /** Storage for the Samples. */
72      private ArrayList<Sample> samples = new ArrayList<Sample>();
73  
74      /** Definition of the density axis. */
75      private Axis densityAxis = new Axis(new DoubleScalar.Abs<LinearDensityUnit>(0, PER_KILOMETER),
76          new DoubleScalar.Abs<LinearDensityUnit>(200, PER_KILOMETER), null, 0d, "Density [veh/km]", "Density",
77          "density %.1f veh/km");
78  
79      /**
80       * @return densityAxis
81       */
82      public final Axis getDensityAxis()
83      {
84          return this.densityAxis;
85      }
86  
87      /** Definition of the speed axis. */
88      private Axis speedAxis = new Axis(new Speed.Abs(0, KM_PER_HOUR), new Speed.Abs(120, KM_PER_HOUR), null, 0d,
89          "Speed [km/h]", "Speed", "speed %.0f km/h");
90  
91      /**
92       * @return speedAxis
93       */
94      public final Axis getSpeedAxis()
95      {
96          return this.speedAxis;
97      }
98  
99      /**
100      * @return flowAxis
101      */
102     public final Axis getFlowAxis()
103     {
104         return this.flowAxis;
105     }
106 
107     /** Definition of the flow axis. */
108     private Axis flowAxis = new Axis(new Frequency.Abs(0, PER_HOUR), new Frequency.Abs(3000d, HERTZ), null, 0d,
109         "Flow [veh/h]", "Flow", "flow %.0f veh/h");
110 
111     /** The currently shown X-axis. */
112     private Axis xAxis;
113 
114     /** The currently shown Y-axis. */
115     private Axis yAxis;
116 
117     /** List of parties interested in changes of this ContourPlot. */
118     private transient EventListenerList listenerList = new EventListenerList();
119 
120     /** Not used internally. */
121     private DatasetGroup datasetGroup = null;
122 
123     /** the lane for which data is gathered. */
124     private final Lane lane;
125 
126     /** the simulator to schedule sampling. */
127     private final OTSDEVSSimulatorInterface simulator;
128     
129     /** flow counter. */
130     
131     int flow = 0;
132 
133     /**
134      * Retrieve the format string for the Y axis.
135      * @return format string
136      */
137     public final String getYAxisFormat()
138     {
139         return this.yAxis.getFormat();
140     }
141 
142     /**
143      * Retrieve the format string for the X axis.
144      * @return format string
145      */
146     public final String getXAxisFormat()
147     {
148         return this.xAxis.getFormat();
149     }
150 
151     /**
152      * Graph a Fundamental Diagram.
153      * @param caption String; the caption shown above the graphing area.
154      * @param aggregationTime DoubleScalarRel&lt;TimeUnit&gt;; the aggregation of the detector that generates the data for this
155      *            Fundamental diagram
156      * @param lane Lane; the Lane on which the traffic will be sampled
157      * @param simulator the simulator to schedule the sampling on
158      * @throws NetworkException on network inconsistency
159      * @throws SimRuntimeException in case scheduling of the sampler fails
160      */
161     public FundamentalDiagramLane(final String caption, final Time.Rel aggregationTime, final Lane lane,
162         final OTSDEVSSimulatorInterface simulator) throws NetworkException, SimRuntimeException
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.lane = lane;
171         this.simulator = simulator;
172         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
173         this.chartPanel =
174             ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
175         FixCaption.fixCaption(this.chartPanel);
176         final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer();
177         renderer.setBaseShapesVisible(true);
178 
179         final ChartPanel cp = new ChartPanel(this.chartPanel);
180         PointerHandler ph = new PointerHandler()
181         {
182             /** {@inheritDoc} */
183             @Override
184             void updateHint(final double domainValue, final double rangeValue)
185             {
186                 if (Double.isNaN(domainValue))
187                 {
188                     setStatusText(" ");
189                     return;
190                 }
191                 String s1 = String.format(getXAxisFormat(), domainValue);
192                 String s2 = String.format(getYAxisFormat(), rangeValue);
193                 setStatusText(s1 + ", " + s2);
194             }
195 
196         };
197         cp.addMouseMotionListener(ph);
198         cp.addMouseListener(ph);
199         cp.setMouseWheelEnabled(true);
200         final JMenu subMenu = new JMenu("Set layout");
201         final ButtonGroup group = new ButtonGroup();
202         final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true);
203         addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false);
204         addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false);
205         actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand()));
206         final JPopupMenu popupMenu = cp.getPopupMenu();
207         popupMenu.insert(subMenu, 0);
208         this.add(cp, BorderLayout.CENTER);
209         this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
210         this.add(this.statusLabel, BorderLayout.SOUTH);
211         simulator.scheduleEventRel(this.aggregationTime, this, this, "addData", null);
212         new FlowSensor(lane);
213     }
214 
215     /**
216      * Update the status text.
217      * @param newText String; the new text to show
218      */
219     public final void setStatusText(final String newText)
220     {
221         this.statusLabel.setText(newText);
222     }
223 
224     /**
225      * @return aggregationTime
226      */
227     public final Time.Rel getAggregationTime()
228     {
229         return this.aggregationTime;
230     }
231 
232     /**
233      * Build one JRadioButtonMenuItem for the sub menu of the context menu.
234      * @param subMenu JMenu; the menu to which the new JRadioButtonMenuItem is added
235      * @param group ButtonGroup; the buttonGroup for the new JRadioButtonMenuItem
236      * @param xAxisToSelect Axis; the Axis that will become X-axis when this item is clicked
237      * @param yAxisToSelect Axis; the Axis that will become Y-axis when this item is clicked
238      * @param selected Boolean; if true, the new JRadioButtonMenuItem will be selected; if false, the new JRadioButtonMenuItem
239      *            will <b>not</b> be selected
240      * @return JRatioButtonMenuItem; the newly added item
241      */
242     private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect,
243         final Axis yAxisToSelect, final boolean selected)
244     {
245         final JRadioButtonMenuItem item =
246             new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName());
247         item.setSelected(selected);
248         item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName());
249         item.addActionListener(this);
250         subMenu.add(item);
251         group.add(item);
252         return item;
253     }
254 
255     /**
256      * Add the density and average speed on the lane to this Fundamental Diagram.
257      * @throws SimRuntimeException when scheduling of next sampling time fails
258      */
259     public final void addData() throws SimRuntimeException
260     {
261         // collect (harmonic) mean speed and number of vehicles per meter on the lane
262         double n = this.lane.getGtuList().size();
263         double density = n / this.lane.getLength().si;
264         if (density > 0.0)
265         {
266             double meanSpeed = 0.0;
267             for (LaneBasedGTU gtu : this.lane.getGtuList())
268             {
269                 meanSpeed += 1 / gtu.getVelocity().si;
270             }
271             meanSpeed = n / meanSpeed;
272             this.samples.add(new Sample(meanSpeed, density, this.flow / this.aggregationTime.si));
273             this.flow = 0;
274         }
275         this.simulator.scheduleEventRel(this.aggregationTime, this, this, "addData", null);
276     }
277 
278     /**
279      * Set up a JFreeChart axis.
280      * @param valueAxis ValueAxis; the axis to set up
281      * @param axis Axis; the Axis that provides the data to setup the ValueAxis
282      */
283     private static void configureAxis(final ValueAxis valueAxis, final Axis axis)
284     {
285         valueAxis.setLabel("\u2192 " + axis.getName());
286         valueAxis.setRange(axis.getMinimumValue().getInUnit(), axis.getMaximumValue().getInUnit());
287     }
288 
289     /**
290      * Redraw this TrajectoryGraph (after the underlying data has been changed, or to change axes).
291      */
292     public final void reGraph()
293     {
294         NumberAxis numberAxis = new NumberAxis();
295         configureAxis(numberAxis, this.xAxis);
296         this.chartPanel.getXYPlot().setDomainAxis(numberAxis);
297         this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
298         numberAxis = new NumberAxis();
299         configureAxis(numberAxis, this.yAxis);
300         this.chartPanel.getXYPlot().setRangeAxis(numberAxis);
301         this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
302         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
303     }
304 
305     /**
306      * Notify interested parties of an event affecting this TrajectoryPlot.
307      * @param event DatasetChangedEvent
308      */
309     private void notifyListeners(final DatasetChangeEvent event)
310     {
311         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
312         {
313             dcl.datasetChanged(event);
314         }
315     }
316 
317     /** {@inheritDoc} */
318     @Override
319     public final int getSeriesCount()
320     {
321         return 1;
322     }
323 
324     /** {@inheritDoc} */
325     @Override
326     public final Comparable<Integer> getSeriesKey(final int series)
327     {
328         return series;
329     }
330 
331     /** {@inheritDoc} */
332     @SuppressWarnings("rawtypes")
333     @Override
334     public final int indexOf(final Comparable seriesKey)
335     {
336         if (seriesKey instanceof Integer)
337         {
338             return (Integer) seriesKey;
339         }
340         return -1;
341     }
342 
343     /** {@inheritDoc} */
344     @Override
345     public final void addChangeListener(final DatasetChangeListener listener)
346     {
347         this.listenerList.add(DatasetChangeListener.class, listener);
348     }
349 
350     /** {@inheritDoc} */
351     @Override
352     public final void removeChangeListener(final DatasetChangeListener listener)
353     {
354         this.listenerList.remove(DatasetChangeListener.class, listener);
355     }
356 
357     /** {@inheritDoc} */
358     @Override
359     public final DatasetGroup getGroup()
360     {
361         return this.datasetGroup;
362     }
363 
364     /** {@inheritDoc} */
365     @Override
366     public final void setGroup(final DatasetGroup group)
367     {
368         this.datasetGroup = group;
369     }
370 
371     /** {@inheritDoc} */
372     @Override
373     public final DomainOrder getDomainOrder()
374     {
375         return DomainOrder.ASCENDING;
376     }
377 
378     /** {@inheritDoc} */
379     @Override
380     public final int getItemCount(final int series)
381     {
382         return this.samples.size();
383     }
384 
385     /**
386      * Retrieve a value from the recorded samples.
387      * @param item Integer; the rank number of the sample
388      * @param axis Axis; the axis that determines which quantity to retrieve
389      * @return Double; the requested value, or Double.NaN if the sample does not (yet) exist
390      */
391     private Double getSample(final int item, final Axis axis)
392     {
393         if (item >= this.samples.size())
394         {
395             return Double.NaN;
396         }
397         double result = this.samples.get(item).getValue(axis);
398         return result;
399     }
400 
401     /** {@inheritDoc} */
402     @Override
403     public final Number getX(final int series, final int item)
404     {
405         return getXValue(series, item);
406     }
407 
408     /** {@inheritDoc} */
409     @Override
410     public final double getXValue(final int series, final int item)
411     {
412         return getSample(item, this.xAxis);
413     }
414 
415     /** {@inheritDoc} */
416     @Override
417     public final Number getY(final int series, final int item)
418     {
419         return getYValue(series, item);
420     }
421 
422     /** {@inheritDoc} */
423     @Override
424     public final double getYValue(final int series, final int item)
425     {
426         return getSample(item, this.yAxis);
427     }
428 
429     /** {@inheritDoc} */
430     @Override
431     public final void actionPerformed(final ActionEvent actionEvent)
432     {
433         final String command = actionEvent.getActionCommand();
434         // System.out.println("command is \"" + command + "\"");
435         final String[] fields = command.split("[/]");
436         if (fields.length == 2)
437         {
438             for (String field : fields)
439             {
440                 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
441                 {
442                     if (field == fields[0])
443                     {
444                         this.yAxis = this.densityAxis;
445                     }
446                     else
447                     {
448                         this.xAxis = this.densityAxis;
449                     }
450                 }
451                 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
452                 {
453                     if (field == fields[0])
454                     {
455                         this.yAxis = this.flowAxis;
456                     }
457                     else
458                     {
459                         this.xAxis = this.flowAxis;
460                     }
461                 }
462                 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
463                 {
464                     if (field == fields[0])
465                     {
466                         this.yAxis = this.speedAxis;
467                     }
468                     else
469                     {
470                         this.xAxis = this.speedAxis;
471                     }
472                 }
473                 else
474                 {
475                     throw new Error("Cannot find axis name: " + field);
476                 }
477             }
478             reGraph();
479         }
480         else
481         {
482             throw new Error("Unknown ActionEvent");
483         }
484     }
485 
486     /**
487      * Storage for one sample of data collected with mean speed [m/s] and number of vehicles per km. Flow per second can be calculated
488      * from these two numbers as  
489      * <p>
490      * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
491      * </p>
492      * $LastChangedDate: 2015-07-15 11:18:39 +0200 (Wed, 15 Jul 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
493      * initial versionJul 31, 2014 <br>
494      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
495      */
496     class Sample
497     {
498         /** mean speed observed during this sample [m/s]. */
499         private final double meanSpeed;
500 
501         /** density [veh/m]. */
502         private final double density;
503         
504         /** flow [veh/s]. */
505         private final double flow;
506 
507         /**
508          * @param meanSpeed mean speed observed during this sample [m/s]
509          * @param density density [veh/m]
510          * @param flow [veh/s]
511          */
512         public Sample(final double meanSpeed, final double density, final double flow)
513         {
514             super();
515             this.meanSpeed = meanSpeed;
516             this.density = density;
517             this.flow = flow;
518         }
519 
520         /**
521          * Retrieve a value stored in this Sample.
522          * @param axis Axis; the axis along which the data is requested
523          * @return double; the retrieved value
524          */
525         public double getValue(final Axis axis)
526         {
527             if (axis == getDensityAxis())
528             {
529                 return 1000.0 * this.density; // [veh/km]
530             }
531             else if (axis == getFlowAxis())
532             {
533                 return 3600.0 * this.meanSpeed * this.density; // [veh/h]
534             }
535             else if (axis == getSpeedAxis())
536             {
537                 return 3.6 * this.meanSpeed; // [km / h]
538             }
539             else
540             {
541                 throw new Error("Sample.getValue: Can not identify axis");
542             }
543         }
544     }
545 
546     /** */
547     private class FlowSensor extends AbstractSensor
548     {
549         /** */
550         private static final long serialVersionUID = 1L;
551 
552         /**
553          * @param lane the lane for which to build the flowSensor
554          */
555         public FlowSensor(final Lane lane)
556         {
557             super(lane, lane.getLength().divideBy(2.0), RelativePosition.FRONT, "FLOW", null);
558         }
559 
560         /** {@inheritDoc} */
561         @Override
562         public void trigger(final LaneBasedGTU gtu)
563         {
564             FundamentalDiagramLane.this.flow += 1;
565         }
566         
567     }
568 }