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