View Javadoc
1   package org.opentrafficsim.graphs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.event.ActionEvent;
5   import java.awt.event.ActionListener;
6   import java.util.ArrayList;
7   
8   import javax.swing.ButtonGroup;
9   import javax.swing.JFrame;
10  import javax.swing.JLabel;
11  import javax.swing.JMenu;
12  import javax.swing.JPopupMenu;
13  import javax.swing.JRadioButtonMenuItem;
14  import javax.swing.SwingConstants;
15  import javax.swing.event.EventListenerList;
16  
17  import org.djunits.unit.FrequencyUnit;
18  import org.djunits.unit.LinearDensityUnit;
19  import org.djunits.unit.SpeedUnit;
20  import org.djunits.value.vdouble.scalar.Frequency;
21  import org.djunits.value.vdouble.scalar.Length;
22  import org.djunits.value.vdouble.scalar.LinearDensity;
23  import org.djunits.value.vdouble.scalar.Speed;
24  import org.djunits.value.vdouble.scalar.Time;
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.labels.XYItemLabelGenerator;
33  import org.jfree.chart.plot.PlotOrientation;
34  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
35  import org.jfree.data.DomainOrder;
36  import org.jfree.data.general.DatasetChangeEvent;
37  import org.jfree.data.general.DatasetChangeListener;
38  import org.jfree.data.general.DatasetGroup;
39  import org.jfree.data.xy.XYDataset;
40  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
41  import org.opentrafficsim.core.gtu.GTUException;
42  import org.opentrafficsim.core.gtu.GTUType;
43  import org.opentrafficsim.core.gtu.RelativePosition;
44  import org.opentrafficsim.core.network.NetworkException;
45  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
46  import org.opentrafficsim.road.network.lane.AbstractSensor;
47  import org.opentrafficsim.road.network.lane.Lane;
48  
49  /**
50   * The Fundamental Diagram Graph; see <a href="http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow"> Wikipedia:
51   * http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow</a>.
52   * <p>
53   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
54   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
55   * <p>
56   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
57   * initial version Jul 31, 2014 <br>
58   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
59   */
60  public class FundamentalDiagram extends JFrame implements XYDataset, ActionListener
61  {
62      /** */
63      private static final long serialVersionUID = 20140701L;
64  
65      /** The ChartPanel for this Fundamental Diagram. */
66      private JFreeChart chartPanel;
67  
68      /** Caption for this Fundamental Diagram. */
69      private final String caption;
70  
71      /** Position of this Fundamental Diagram sensor. */
72      private final Length.Rel position;
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 Time.Rel aggregationTime;
79  
80      /**
81       * @return aggregationTime
82       */
83      public final Time.Rel getAggregationTime()
84      {
85          return this.aggregationTime;
86      }
87  
88      /** Storage for the Samples. */
89      private ArrayList<Sample> samples = new ArrayList<Sample>();
90  
91      /** Definition of the density axis. */
92      private Axis densityAxis = new Axis(new LinearDensity(0, LinearDensityUnit.PER_KILOMETER), new LinearDensity(200,
93          LinearDensityUnit.PER_KILOMETER), null, 0d, "Density [veh/km]", "Density", "density %.1f veh/km");
94  
95      /**
96       * @return densityAxis
97       */
98      public final Axis getDensityAxis()
99      {
100         return this.densityAxis;
101     }
102 
103     /** Definition of the speed axis. */
104     private Axis speedAxis = new Axis(new Speed(0, SpeedUnit.KM_PER_HOUR), new Speed(180, SpeedUnit.KM_PER_HOUR), null,
105         0d, "Speed [km/h]", "Speed", "speed %.0f km/h");
106 
107     /**
108      * @return speedAxis
109      */
110     public final Axis getSpeedAxis()
111     {
112         return this.speedAxis;
113     }
114 
115     /**
116      * @return flowAxis
117      */
118     public final Axis getFlowAxis()
119     {
120         return this.flowAxis;
121     }
122 
123     /** Definition of the flow axis. */
124     private Axis flowAxis = new Axis(new Frequency(0, FrequencyUnit.PER_HOUR),
125         new Frequency(3000d, FrequencyUnit.HERTZ), null, 0d, "Flow [veh/h]", "Flow", "flow %.0f veh/h");
126 
127     /** The currently shown X-axis. */
128     private Axis xAxis;
129 
130     /** The currently shown Y-axis. */
131     private Axis yAxis;
132 
133     /** List of parties interested in changes of this ContourPlot. */
134     private transient EventListenerList listenerList = new EventListenerList();
135 
136     /** Not used internally. */
137     private DatasetGroup datasetGroup = null;
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 position DoubleScalarRel&lt;LengthUnit&gt;; longitudinal position of the detector on the Lane
164      * @throws NetworkException on network inconsistency
165      */
166     public FundamentalDiagram(final String caption, final Time.Rel aggregationTime, final Lane lane,
167         final Length.Rel position) throws NetworkException
168     {
169         if (aggregationTime.getSI() <= 0)
170         {
171             throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")");
172         }
173         this.aggregationTime = aggregationTime;
174         this.caption = caption;
175         this.position = position;
176         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
177         this.chartPanel =
178             ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
179         FixCaption.fixCaption(this.chartPanel);
180         final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer();
181         renderer.setBaseLinesVisible(true);
182         renderer.setBaseShapesVisible(true);
183         renderer.setBaseItemLabelGenerator(new XYItemLabelGenerator()
184         {
185             @Override
186             public String generateLabel(final XYDataset dataset, final int series, final int item)
187             {
188                 return String.format("%.0fs", item * aggregationTime.getSI());
189             }
190         });
191         renderer.setBaseItemLabelsVisible(true);
192         final ChartPanel cp = new ChartPanel(this.chartPanel);
193         PointerHandler ph = new PointerHandler()
194         {
195             /** {@inheritDoc} */
196             @Override
197             void updateHint(final double domainValue, final double rangeValue)
198             {
199                 if (Double.isNaN(domainValue))
200                 {
201                     setStatusText(" ");
202                     return;
203                 }
204                 String s1 = String.format(getXAxisFormat(), domainValue);
205                 String s2 = String.format(getYAxisFormat(), rangeValue);
206                 setStatusText(s1 + ", " + s2);
207             }
208 
209         };
210         cp.addMouseMotionListener(ph);
211         cp.addMouseListener(ph);
212         cp.setMouseWheelEnabled(true);
213         final JMenu subMenu = new JMenu("Set layout");
214         final ButtonGroup group = new ButtonGroup();
215         final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true);
216         addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false);
217         addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false);
218         actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand()));
219         final JPopupMenu popupMenu = cp.getPopupMenu();
220         popupMenu.insert(subMenu, 0);
221         this.add(cp, BorderLayout.CENTER);
222         this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
223         this.add(this.statusLabel, BorderLayout.SOUTH);
224         new FundamentalDiagramSensor(lane, position, null);
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      * Retrieve the position of the detector.
238      * @return Length.Rel; the position of the detector
239      */
240     public final Length.Rel getPosition()
241     {
242         return this.position;
243     }
244 
245     /**
246      * Build one JRadioButtonMenuItem for the sub menu of the context menu.
247      * @param subMenu JMenu; the menu to which the new JRadioButtonMenuItem is added
248      * @param group ButtonGroup; the buttonGroup for the new JRadioButtonMenuItem
249      * @param xAxisToSelect Axis; the Axis that will become X-axis when this item is clicked
250      * @param yAxisToSelect Axis; the Axis that will become Y-axis when this item is clicked
251      * @param selected Boolean; if true, the new JRadioButtonMenuItem will be selected; if false, the new JRadioButtonMenuItem
252      *            will <b>not</b> be selected
253      * @return JRatioButtonMenuItem; the newly added item
254      */
255     private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect,
256         final Axis yAxisToSelect, final boolean selected)
257     {
258         final JRadioButtonMenuItem item =
259             new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName());
260         item.setSelected(selected);
261         item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName());
262         item.addActionListener(this);
263         subMenu.add(item);
264         group.add(item);
265         return item;
266     }
267 
268     /**
269      * Add the effect of one passing car to this Fundamental Diagram.
270      * @param gtu AbstractLaneBasedGTU; the GTU that passes the detection point
271      * @throws GTUException when the velocity of the GTU cannot be assessed
272      */
273     public final void addData(final LaneBasedGTU gtu) throws GTUException
274     {
275         Time.Abs detectionTime = gtu.getSimulator().getSimulatorTime().getTime();
276         // Figure out the time bin
277         final int timeBin = (int) Math.floor(detectionTime.getSI() / this.aggregationTime.getSI());
278         // Extend storage if needed
279         while (timeBin >= this.samples.size())
280         {
281             this.samples.add(new Sample());
282         }
283         Sample sample = this.samples.get(timeBin);
284         sample.addData(gtu.getVelocity(detectionTime));
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         /*-
408         System.out.println(String.format("getSample(item=%d, axis=%s) returns %f", item, axis.name,
409                 result));
410          */
411         return result;
412     }
413 
414     /** {@inheritDoc} */
415     @Override
416     public final Number getX(final int series, final int item)
417     {
418         return getXValue(series, item);
419     }
420 
421     /** {@inheritDoc} */
422     @Override
423     public final double getXValue(final int series, final int item)
424     {
425         return getSample(item, this.xAxis);
426     }
427 
428     /** {@inheritDoc} */
429     @Override
430     public final Number getY(final int series, final int item)
431     {
432         return getYValue(series, item);
433     }
434 
435     /** {@inheritDoc} */
436     @Override
437     public final double getYValue(final int series, final int item)
438     {
439         return getSample(item, this.yAxis);
440     }
441 
442     /**
443      * Storage for one sample of data collected by a point-detector that accumulates harmonic mean speed and flow.
444      * <p>
445      * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
446      * <p>
447      * See for project information <a href="http://www.simulation.tudelft.nl/"> www.simulation.tudelft.nl</a>.
448      * <p>
449      * The OpenTrafficSim project is distributed under the following BSD-style license:<br>
450      * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
451      * following conditions are met:
452      * <ul>
453      * <li>Redistributions of source code must retain the above copyright notice, this list of conditions and the following
454      * disclaimer.</li>
455      * <li>Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
456      * disclaimer in the documentation and/or other materials provided with the distribution.</li>
457      * <li>Neither the name of Delft University of Technology, nor the names of its contributors may be used to endorse or
458      * promote products derived from this software without specific prior written permission.</li>
459      * </ul>
460      * This software is provided by the copyright holders and contributors "as is" and any express or implied warranties,
461      * including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are
462      * disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental,
463      * special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services;
464      * loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in
465      * contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this
466      * software, even if advised of the possibility of such damage. $LastChangedDate: 2015-07-15 11:18:39 +0200 (Wed, 15 Jul
467      * 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $, initial versionJul 31, 2014 <br>
468      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
469      */
470     class Sample
471     {
472         /** Harmonic mean speed observed during this sample [m/s]. */
473         private double harmonicMeanSpeed;
474 
475         /** Flow observed during this sample [veh/s]. */
476         private double flow;
477 
478         /**
479          * Retrieve a value stored in this Sample.
480          * @param axis Axis; the axis along which the data is requested
481          * @return double; the retrieved value
482          */
483         public double getValue(final Axis axis)
484         {
485             if (axis == getDensityAxis())
486             {
487                 return this.flow * 3600 / getAggregationTime().getSI() / this.harmonicMeanSpeed;
488             }
489             else if (axis == getFlowAxis())
490             {
491                 return this.flow * 3600 / getAggregationTime().getSI();
492             }
493             else if (axis == getSpeedAxis())
494             {
495                 return this.harmonicMeanSpeed * 3600 / 1000;
496             }
497             else
498             {
499                 throw new Error("Sample.getValue: Can not identify axis");
500             }
501         }
502 
503         /**
504          * Add one Car detection to this Sample.
505          * @param speed Speed; the detected speed
506          */
507         public void addData(final Speed speed)
508         {
509             double sumReciprocalSpeeds = 0;
510             if (this.flow > 0)
511             {
512                 sumReciprocalSpeeds = this.flow / this.harmonicMeanSpeed;
513             }
514             this.flow += 1;
515             sumReciprocalSpeeds += 1d / speed.getSI();
516             this.harmonicMeanSpeed = this.flow / sumReciprocalSpeeds;
517         }
518     }
519 
520     /** {@inheritDoc} */
521     @Override
522     public final void actionPerformed(final ActionEvent actionEvent)
523     {
524         final String command = actionEvent.getActionCommand();
525         // System.out.println("command is \"" + command + "\"");
526         final String[] fields = command.split("[/]");
527         if (fields.length == 2)
528         {
529             for (String field : fields)
530             {
531                 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
532                 {
533                     if (field == fields[0])
534                     {
535                         this.yAxis = this.densityAxis;
536                     }
537                     else
538                     {
539                         this.xAxis = this.densityAxis;
540                     }
541                 }
542                 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
543                 {
544                     if (field == fields[0])
545                     {
546                         this.yAxis = this.flowAxis;
547                     }
548                     else
549                     {
550                         this.xAxis = this.flowAxis;
551                     }
552                 }
553                 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
554                 {
555                     if (field == fields[0])
556                     {
557                         this.yAxis = this.speedAxis;
558                     }
559                     else
560                     {
561                         this.xAxis = this.speedAxis;
562                     }
563                 }
564                 else
565                 {
566                     throw new Error("Cannot find axis name: " + field);
567                 }
568             }
569             reGraph();
570         }
571         else
572         {
573             throw new Error("Unknown ActionEvent");
574         }
575     }
576 
577     /**
578      * Internal Sensor class.
579      * <p>
580      * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
581      * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
582      * <p>
583      * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
584      * initial version feb. 2015 <br>
585      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
586      */
587     class FundamentalDiagramSensor extends AbstractSensor
588     {
589         /** */
590         private static final long serialVersionUID = 20150203L;
591 
592         /**
593          * Construct a FundamentalDiagramSensor.
594          * @param lane Lane; the Lane on which the new FundamentalDiagramSensor is to be added
595          * @param longitudinalPosition Length.Rel; longitudinal position on the Lane of the new FundamentalDiagramSensor
596          * @param simulator simulator to allow animation
597          * @throws NetworkException on network inconsistency
598          */
599         public FundamentalDiagramSensor(final Lane lane, final Length.Rel longitudinalPosition,
600             final OTSDEVSSimulatorInterface simulator) throws NetworkException
601         {
602             super(lane, longitudinalPosition, RelativePosition.REFERENCE, "FUNDAMENTAL_DIAGRAM_SENSOR@"
603                 + lane.toString(), simulator);
604             lane.addSensor(this, GTUType.ALL);
605         }
606 
607         /** {@inheritDoc} */
608         @Override
609         public void trigger(final LaneBasedGTU gtu)
610         {
611             try
612             {
613                 addData(gtu);
614             }
615             catch (GTUException exception)
616             {
617                 exception.printStackTrace(); // TODO
618             }
619         }
620 
621         /** {@inheritDoc} */
622         public final String toString()
623         {
624             return "FundamentalDiagramSensor at " + getLongitudinalPosition();
625         }
626 
627     }
628 
629 }