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