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