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.rmi.RemoteException;
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.jfree.chart.ChartFactory;
19  import org.jfree.chart.ChartPanel;
20  import org.jfree.chart.JFreeChart;
21  import org.jfree.chart.StandardChartTheme;
22  import org.jfree.chart.axis.NumberAxis;
23  import org.jfree.chart.axis.ValueAxis;
24  import org.jfree.chart.event.AxisChangeEvent;
25  import org.jfree.chart.labels.XYItemLabelGenerator;
26  import org.jfree.chart.plot.PlotOrientation;
27  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
28  import org.jfree.data.DomainOrder;
29  import org.jfree.data.general.DatasetChangeEvent;
30  import org.jfree.data.general.DatasetChangeListener;
31  import org.jfree.data.general.DatasetGroup;
32  import org.jfree.data.xy.XYDataset;
33  import org.opentrafficsim.core.gtu.RelativePosition;
34  import org.opentrafficsim.core.gtu.lane.LaneBasedGTU;
35  import org.opentrafficsim.core.network.NetworkException;
36  import org.opentrafficsim.core.network.lane.AbstractSensor;
37  import org.opentrafficsim.core.network.lane.Lane;
38  import org.opentrafficsim.core.unit.FrequencyUnit;
39  import org.opentrafficsim.core.unit.LengthUnit;
40  import org.opentrafficsim.core.unit.LinearDensityUnit;
41  import org.opentrafficsim.core.unit.SpeedUnit;
42  import org.opentrafficsim.core.unit.TimeUnit;
43  import org.opentrafficsim.core.value.vdouble.scalar.DoubleScalar;
44  
45  /**
46   * The Fundamental Diagram Graph; see <a href="http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow">
47   * Wikipedia: http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow</a>.
48   * <p>
49   * Copyright (c) 2013-2014 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
50   * reserved. <br>
51   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
52   * <p>
53   * @version Jul 31, 2014 <br>
54   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
55   */
56  public class FundamentalDiagram extends JFrame implements XYDataset, ActionListener
57  {
58      /** */
59      private static final long serialVersionUID = 20140701L;
60  
61      /** The ChartPanel for this Fundamental Diagram. */
62      private JFreeChart chartPanel;
63  
64      /** Caption for this Fundamental Diagram. */
65      private final String caption;
66  
67      /** Position of this Fundamental Diagram sensor. */
68      private final DoubleScalar.Rel<LengthUnit> position;
69  
70      /** Area to show status information. */
71      private final JLabel statusLabel;
72  
73      /** Sample duration of the detector that generates this Fundamental Diagram. */
74      private final DoubleScalar.Rel<TimeUnit> aggregationTime;
75  
76      /**
77       * @return aggregationTime
78       */
79      public final DoubleScalar.Rel<TimeUnit> getAggregationTime()
80      {
81          return this.aggregationTime;
82      }
83  
84      /** Storage for the Samples. */
85      private ArrayList<Sample> samples = new ArrayList<Sample>();
86  
87      /** Definition of the density axis. */
88      private Axis densityAxis = new Axis(new DoubleScalar.Abs<LinearDensityUnit>(0, LinearDensityUnit.PER_KILOMETER),
89              new DoubleScalar.Abs<LinearDensityUnit>(200, LinearDensityUnit.PER_KILOMETER), null, 0d,
90              "Density [veh/km]", "Density", "density %.1f veh/km");
91  
92      /**
93       * @return densityAxis
94       */
95      public final Axis getDensityAxis()
96      {
97          return this.densityAxis;
98      }
99  
100     /** Definition of the speed axis. */
101     private Axis speedAxis = new Axis(new DoubleScalar.Abs<SpeedUnit>(0, SpeedUnit.KM_PER_HOUR),
102             new DoubleScalar.Abs<SpeedUnit>(180, SpeedUnit.KM_PER_HOUR), null, 0d, "Speed [km/h]", "Speed",
103             "speed %.0f km/h");
104 
105     /**
106      * @return speedAxis
107      */
108     public final Axis getSpeedAxis()
109     {
110         return this.speedAxis;
111     }
112 
113     /**
114      * @return flowAxis
115      */
116     public final Axis getFlowAxis()
117     {
118         return this.flowAxis;
119     }
120 
121     /** Definition of the flow axis. */
122     private Axis flowAxis = new Axis(new DoubleScalar.Abs<FrequencyUnit>(0, FrequencyUnit.PER_HOUR),
123             new DoubleScalar.Abs<FrequencyUnit>(3000d, FrequencyUnit.HERTZ), null, 0d, "Flow [veh/h]", "Flow",
124             "flow %.0f veh/h");
125 
126     /** The currently shown X-axis. */
127     private Axis xAxis;
128 
129     /** The currently shown Y-axis. */
130     private Axis yAxis;
131 
132     /** List of parties interested in changes of this ContourPlot. */
133     private transient EventListenerList listenerList = new EventListenerList();
134 
135     /** Not used internally. */
136     private DatasetGroup datasetGroup = null;
137 
138     /**
139      * Retrieve the format string for the Y axis.
140      * @return format string
141      */
142     public final String getYAxisFormat()
143     {
144         return this.yAxis.getFormat();
145     }
146 
147     /**
148      * Retrieve the format string for the X axis.
149      * @return format string
150      */
151     public final String getXAxisFormat()
152     {
153         return this.xAxis.getFormat();
154     }
155 
156     /**
157      * Graph a Fundamental Diagram.
158      * @param caption String; the caption shown above the graphing area.
159      * @param aggregationTime DoubleScalarRel&lt;TimeUnit&gt;; the aggregation of the detector that generates the data
160      *            for this Fundamental diagram
161      * @param lane Lane; the Lane on which the traffic will be sampled
162      * @param position DoubleScalarRel&lt;LengthUnit&gt;; longitudinal position of the detector on the Lane
163      * @throws NetworkException on network inconsistency
164      */
165     public FundamentalDiagram(final String caption, final DoubleScalar.Rel<TimeUnit> aggregationTime, final Lane lane,
166             final DoubleScalar.Rel<LengthUnit> position) throws NetworkException
167     {
168         if (aggregationTime.getSI() <= 0)
169         {
170             throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")");
171         }
172         this.aggregationTime = aggregationTime;
173         this.caption = caption;
174         this.position = position;
175         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
176         this.chartPanel =
177                 ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false,
178                         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);
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 DoubleScalar.Rel&lt;LengthUnit&gt;; the position of the detector
239      */
240     public final DoubleScalar.Rel<LengthUnit> 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
252      *            JRadioButtonMenuItem 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&lt;?&gt;; the GTU that passes the detection point
271      */
272     public final void addData(final LaneBasedGTU<?> gtu)
273     {
274         try
275         {
276             DoubleScalar.Abs<TimeUnit> detectionTime = gtu.getSimulator().getSimulatorTime().get();
277             // Figure out the time bin
278             final int timeBin = (int) Math.floor(detectionTime.getSI() / this.aggregationTime.getSI());
279             // Extend storage if needed
280             while (timeBin >= this.samples.size())
281             {
282                 this.samples.add(new Sample());
283             }
284             Sample sample = this.samples.get(timeBin);
285             sample.addData(gtu.getLongitudinalVelocity(detectionTime));
286         }
287         catch (RemoteException exception)
288         {
289             exception.printStackTrace();
290         }
291     }
292 
293     /**
294      * Set up a JFreeChart axis.
295      * @param valueAxis ValueAxis; the axis to set up
296      * @param axis Axis; the Axis that provides the data to setup the ValueAxis
297      */
298     private static void configureAxis(final ValueAxis valueAxis, final Axis axis)
299     {
300         valueAxis.setLabel("\u2192 " + axis.getName());
301         valueAxis.setRange(axis.getMinimumValue().getInUnit(), axis.getMaximumValue().getInUnit());
302     }
303 
304     /**
305      * Redraw this TrajectoryGraph (after the underlying data has been changed, or to change axes).
306      */
307     public final void reGraph()
308     {
309         NumberAxis numberAxis = new NumberAxis();
310         configureAxis(numberAxis, this.xAxis);
311         this.chartPanel.getXYPlot().setDomainAxis(numberAxis);
312         this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
313         numberAxis = new NumberAxis();
314         configureAxis(numberAxis, this.yAxis);
315         this.chartPanel.getXYPlot().setRangeAxis(numberAxis);
316         this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis));
317         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
318     }
319 
320     /**
321      * Notify interested parties of an event affecting this TrajectoryPlot.
322      * @param event DatasetChangedEvent
323      */
324     private void notifyListeners(final DatasetChangeEvent event)
325     {
326         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
327         {
328             dcl.datasetChanged(event);
329         }
330     }
331 
332     /** {@inheritDoc} */
333     @Override
334     public final int getSeriesCount()
335     {
336         return 1;
337     }
338 
339     /** {@inheritDoc} */
340     @Override
341     public final Comparable<Integer> getSeriesKey(final int series)
342     {
343         return series;
344     }
345 
346     /** {@inheritDoc} */
347     @SuppressWarnings("rawtypes")
348     @Override
349     public final int indexOf(final Comparable seriesKey)
350     {
351         if (seriesKey instanceof Integer)
352         {
353             return (Integer) seriesKey;
354         }
355         return -1;
356     }
357 
358     /** {@inheritDoc} */
359     @Override
360     public final void addChangeListener(final DatasetChangeListener listener)
361     {
362         this.listenerList.add(DatasetChangeListener.class, listener);
363     }
364 
365     /** {@inheritDoc} */
366     @Override
367     public final void removeChangeListener(final DatasetChangeListener listener)
368     {
369         this.listenerList.remove(DatasetChangeListener.class, listener);
370     }
371 
372     /** {@inheritDoc} */
373     @Override
374     public final DatasetGroup getGroup()
375     {
376         return this.datasetGroup;
377     }
378 
379     /** {@inheritDoc} */
380     @Override
381     public final void setGroup(final DatasetGroup group)
382     {
383         this.datasetGroup = group;
384     }
385 
386     /** {@inheritDoc} */
387     @Override
388     public final DomainOrder getDomainOrder()
389     {
390         return DomainOrder.ASCENDING;
391     }
392 
393     /** {@inheritDoc} */
394     @Override
395     public final int getItemCount(final int series)
396     {
397         return this.samples.size();
398     }
399 
400     /**
401      * Retrieve a value from the recorded samples.
402      * @param item Integer; the rank number of the sample
403      * @param axis Axis; the axis that determines which quantity to retrieve
404      * @return Double; the requested value, or Double.NaN if the sample does not (yet) exist
405      */
406     private Double getSample(final int item, final Axis axis)
407     {
408         if (item >= this.samples.size())
409         {
410             return Double.NaN;
411         }
412         double result = this.samples.get(item).getValue(axis);
413         /*-
414         System.out.println(String.format("getSample(item=%d, axis=%s) returns %f", item, axis.name,
415                 result));
416          */
417         return result;
418     }
419 
420     /** {@inheritDoc} */
421     @Override
422     public final Number getX(final int series, final int item)
423     {
424         return getXValue(series, item);
425     }
426 
427     /** {@inheritDoc} */
428     @Override
429     public final double getXValue(final int series, final int item)
430     {
431         return getSample(item, this.xAxis);
432     }
433 
434     /** {@inheritDoc} */
435     @Override
436     public final Number getY(final int series, final int item)
437     {
438         return getYValue(series, item);
439     }
440 
441     /** {@inheritDoc} */
442     @Override
443     public final double getYValue(final int series, final int item)
444     {
445         return getSample(item, this.yAxis);
446     }
447 
448     /**
449      * Storage for one sample of data collected by a point-detector that accumulates harmonic mean speed and flow.
450      * <p>
451      * Copyright (c) 2013-2014 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
452      * reserved.
453      * <p>
454      * See for project information <a href="http://www.simulation.tudelft.nl/"> www.simulation.tudelft.nl</a>.
455      * <p>
456      * The OpenTrafficSim project is distributed under the following BSD-style license:<br>
457      * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
458      * following conditions are met:
459      * <ul>
460      * <li>Redistributions of source code must retain the above copyright notice, this list of conditions and the
461      * following disclaimer.</li>
462      * <li>Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
463      * following disclaimer in the documentation and/or other materials provided with the distribution.</li>
464      * <li>Neither the name of Delft University of Technology, nor the names of its contributors may be used to endorse
465      * or promote products derived from this software without specific prior written permission.</li>
466      * </ul>
467      * This software is provided by the copyright holders and contributors "as is" and any express or implied
468      * warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular
469      * purpose are disclaimed. In no event shall the copyright holder or contributors be liable for any direct,
470      * indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of
471      * substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any
472      * theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising
473      * in any way out of the use of this software, even if advised of the possibility of such damage.
474      * @version Jul 31, 2014 <br>
475      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
476      */
477     class Sample
478     {
479         /** Harmonic mean speed observed during this sample [m/s]. */
480         private double harmonicMeanSpeed;
481 
482         /** Flow observed during this sample [veh/s]. */
483         private double flow;
484 
485         /**
486          * Retrieve a value stored in this Sample.
487          * @param axis Axis; the axis along which the data is requested
488          * @return double; the retrieved value
489          */
490         public double getValue(final Axis axis)
491         {
492             if (axis == getDensityAxis())
493             {
494                 return this.flow * 3600 / getAggregationTime().getSI() / this.harmonicMeanSpeed;
495             }
496             else if (axis == getFlowAxis())
497             {
498                 return this.flow * 3600 / getAggregationTime().getSI();
499             }
500             else if (axis == getSpeedAxis())
501             {
502                 return this.harmonicMeanSpeed * 3600 / 1000;
503             }
504             else
505             {
506                 throw new Error("Sample.getValue: Can not identify axis");
507             }
508         }
509 
510         /**
511          * Add one Car detection to this Sample.
512          * @param speed DoubleScalar.Rel&lt;SpeedUnit&gt;; the detected speed
513          */
514         public void addData(final DoubleScalar.Abs<SpeedUnit> speed)
515         {
516             double sumReciprocalSpeeds = 0;
517             if (this.flow > 0)
518             {
519                 sumReciprocalSpeeds = this.flow / this.harmonicMeanSpeed;
520             }
521             this.flow += 1;
522             sumReciprocalSpeeds += 1d / speed.getSI();
523             this.harmonicMeanSpeed = this.flow / sumReciprocalSpeeds;
524         }
525     }
526 
527     /** {@inheritDoc} */
528     @Override
529     public final void actionPerformed(final ActionEvent actionEvent)
530     {
531         final String command = actionEvent.getActionCommand();
532         // System.out.println("command is \"" + command + "\"");
533         final String[] fields = command.split("[/]");
534         if (fields.length == 2)
535         {
536             for (String field : fields)
537             {
538                 if (field.equalsIgnoreCase(this.densityAxis.getShortName()))
539                 {
540                     if (field == fields[0])
541                     {
542                         this.yAxis = this.densityAxis;
543                     }
544                     else
545                     {
546                         this.xAxis = this.densityAxis;
547                     }
548                 }
549                 else if (field.equalsIgnoreCase(this.flowAxis.getShortName()))
550                 {
551                     if (field == fields[0])
552                     {
553                         this.yAxis = this.flowAxis;
554                     }
555                     else
556                     {
557                         this.xAxis = this.flowAxis;
558                     }
559                 }
560                 else if (field.equalsIgnoreCase(this.speedAxis.getShortName()))
561                 {
562                     if (field == fields[0])
563                     {
564                         this.yAxis = this.speedAxis;
565                     }
566                     else
567                     {
568                         this.xAxis = this.speedAxis;
569                     }
570                 }
571                 else
572                 {
573                     throw new Error("Cannot find axis name: " + field);
574                 }
575             }
576             reGraph();
577         }
578         else
579         {
580             throw new Error("Unknown ActionEvent");
581         }
582     }
583 
584     /**
585      * Internal Sensor class.
586      * <p>
587      * Copyright (c) 2013-2014 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
588      * reserved. <br>
589      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
590      * <p>
591      * @version 3 feb. 2015 <br>
592      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
593      */
594     class FundamentalDiagramSensor extends AbstractSensor
595     {
596         /** */
597         private static final long serialVersionUID = 20150203L;
598 
599         /**
600          * Construct a FundamentalDiagramSensor.
601          * @param lane Lane; the Lane on which the new FundamentalDiagramSensor is to be added
602          * @param longitudinalPosition DoubleScalar.Abs&lt;LengthUnit&gt;; longitudinal position on the Lane of the new
603          *            FundamentalDiagramSensor
604          * @throws NetworkException on network inconsistency
605          */
606         public FundamentalDiagramSensor(final Lane lane, final DoubleScalar.Rel<LengthUnit> longitudinalPosition)
607                 throws NetworkException
608         {
609             super(lane, longitudinalPosition, RelativePosition.REFERENCE);
610             lane.addSensor(this);
611         }
612 
613         /** {@inheritDoc} */
614         @Override
615         public void trigger(final LaneBasedGTU<?> gtu) throws RemoteException
616         {
617             addData(gtu);
618         }
619 
620         /** {@inheritDoc} */
621         public final String toString()
622         {
623             return "FundamentalDiagramSensor at " + getLongitudinalPosition();
624         }
625 
626     }
627 
628 }