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