View Javadoc
1   package org.opentrafficsim.graphs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.event.ActionEvent;
6   import java.awt.event.ActionListener;
7   import java.io.Serializable;
8   import java.rmi.RemoteException;
9   import java.text.NumberFormat;
10  import java.text.ParseException;
11  import java.util.ArrayList;
12  import java.util.HashSet;
13  import java.util.List;
14  import java.util.Locale;
15  import java.util.Set;
16  
17  import javax.swing.ButtonGroup;
18  import javax.swing.JFrame;
19  import javax.swing.JLabel;
20  import javax.swing.JMenu;
21  import javax.swing.JPopupMenu;
22  import javax.swing.JRadioButtonMenuItem;
23  import javax.swing.SwingConstants;
24  import javax.swing.event.EventListenerList;
25  
26  import org.djunits.unit.LengthUnit;
27  import org.djunits.unit.TimeUnit;
28  import org.djunits.value.StorageType;
29  import org.djunits.value.ValueException;
30  import org.djunits.value.vdouble.scalar.DoubleScalar;
31  import org.djunits.value.vdouble.scalar.Length;
32  import org.djunits.value.vdouble.scalar.Time;
33  import org.djunits.value.vdouble.vector.LengthVector;
34  import org.jfree.chart.ChartPanel;
35  import org.jfree.chart.JFreeChart;
36  import org.jfree.chart.LegendItem;
37  import org.jfree.chart.LegendItemCollection;
38  import org.jfree.chart.axis.NumberAxis;
39  import org.jfree.chart.event.PlotChangeEvent;
40  import org.jfree.chart.plot.XYPlot;
41  import org.jfree.chart.renderer.xy.XYBlockRenderer;
42  import org.jfree.data.DomainOrder;
43  import org.jfree.data.general.DatasetChangeEvent;
44  import org.jfree.data.general.DatasetChangeListener;
45  import org.jfree.data.general.DatasetGroup;
46  import org.jfree.data.xy.XYZDataset;
47  import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
48  import org.opentrafficsim.core.gtu.GTUException;
49  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
50  import org.opentrafficsim.road.network.lane.Lane;
51  import org.opentrafficsim.simulationengine.OTSSimulationException;
52  
53  import nl.tudelft.simulation.event.EventInterface;
54  import nl.tudelft.simulation.event.EventListenerInterface;
55  import nl.tudelft.simulation.event.TimedEvent;
56  
57  /**
58   * Common code for a contour plot. <br>
59   * The data collection code for acceleration assumes constant acceleration during the evaluation period of the GTU.
60   * <p>
61   * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
62   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
63   * <p>
64   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
65   * initial version Jul 16, 2014 <br>
66   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
67   */
68  public abstract class ContourPlot extends JFrame
69          implements ActionListener, XYZDataset, MultipleViewerChart, LaneBasedGTUSampler, EventListenerInterface, Serializable
70  {
71      /** */
72      private static final long serialVersionUID = 20140716L;
73  
74      /** Caption of the graph. */
75      private final String caption;
76  
77      /** Color scale for the graph. */
78      private final ContinuousColorPaintScale paintScale;
79  
80      /** Definition of the X-axis. */
81      @SuppressWarnings("visibilitymodifier")
82      protected final Axis xAxis;
83  
84      /** Definition of the Y-axis. */
85      @SuppressWarnings("visibilitymodifier")
86      protected final Axis yAxis;
87  
88      /** Difference of successive values in the legend. */
89      private final double legendStep;
90  
91      /** Format string used to create the captions in the legend. */
92      private final String legendFormat;
93  
94      /** Time granularity values. */
95      protected static final double[] STANDARDTIMEGRANULARITIES = { 1, 2, 5, 10, 20, 30, 60, 120, 300, 600 };
96  
97      /** Index of the initial time granularity in standardTimeGranularites. */
98      protected static final int STANDARDINITIALTIMEGRANULARITYINDEX = 3;
99  
100     /** Distance granularity values. */
101     protected static final double[] STANDARDDISTANCEGRANULARITIES = { 10, 20, 50, 100, 200, 500, 1000 };
102 
103     /** Index of the initial distance granularity in standardTimeGranularites. */
104     protected static final int STANDARDINITIALDISTANCEGRANULARITYINDEX = 3;
105 
106     /** Initial lower bound for the time scale. */
107     protected static final Time INITIALLOWERTIMEBOUND = new Time(0, TimeUnit.SECOND);
108 
109     /** Initial upper bound for the time scale. */
110     protected static final Time INITIALUPPERTIMEBOUND = new Time(300, TimeUnit.SECOND);
111 
112     /** The series of Lanes that provide the data for this TrajectoryPlot. */
113     private final ArrayList<Lane> path;
114 
115     /** The cumulative lengths of the elements of path. */
116     private final LengthVector cumulativeLengths;
117 
118     /**
119      * Create a new ContourPlot.
120      * @param caption String; text to show above the plotting area
121      * @param xAxis Axis; the X (time) axis
122      * @param path ArrayList&lt;Lane&gt;; the series of Lanes that will provide the data for this TrajectoryPlot
123      * @param redValue Double; contour value that will be rendered in Red
124      * @param yellowValue Double; contour value that will be rendered in Yellow
125      * @param greenValue Double; contour value that will be rendered in Green
126      * @param valueFormat String; format string for the contour values
127      * @param legendFormat String; format string for the captions in the color legend
128      * @param legendStep Double; increment between color legend entries
129      * @throws OTSSimulationException when the scale cannot be generated
130      */
131     public ContourPlot(final String caption, final Axis xAxis, final List<Lane> path, final double redValue,
132             final double yellowValue, final double greenValue, final String valueFormat, final String legendFormat,
133             final double legendStep) throws OTSSimulationException
134     {
135         this.caption = caption;
136         this.path = new ArrayList<Lane>(path); // make a copy
137         double[] endLengths = new double[path.size()];
138         double cumulativeLength = 0;
139         LengthVector lengths = null;
140         for (int i = 0; i < path.size(); i++)
141         {
142             Lane lane = path.get(i);
143             lane.addListener(this, Lane.GTU_ADD_EVENT, true);
144             lane.addListener(this, Lane.GTU_REMOVE_EVENT, true);
145             try
146             {
147                 // register the current GTUs on the lanes (if any) for statistics sampling.
148                 for (LaneBasedGTU gtu : lane.getGtuList())
149                 {
150                     notify(new TimedEvent<OTSSimTimeDouble>(Lane.GTU_ADD_EVENT, lane, new Object[] { gtu.getId(), gtu },
151                             gtu.getSimulator().getSimulatorTime()));
152                 }
153             }
154             catch (RemoteException exception)
155             {
156                 exception.printStackTrace();
157             }
158             cumulativeLength += lane.getLength().getSI();
159             endLengths[i] = cumulativeLength;
160         }
161         try
162         {
163             lengths = new LengthVector(endLengths, LengthUnit.SI, StorageType.DENSE);
164         }
165         catch (ValueException exception)
166         {
167             exception.printStackTrace();
168         }
169         this.cumulativeLengths = lengths;
170         this.xAxis = xAxis;
171         this.yAxis = new Axis(new Length(0, LengthUnit.METER), getCumulativeLength(-1), STANDARDDISTANCEGRANULARITIES,
172                 STANDARDDISTANCEGRANULARITIES[STANDARDINITIALDISTANCEGRANULARITYINDEX], "", "Distance", "%.0fm");
173         this.legendStep = legendStep;
174         this.legendFormat = legendFormat;
175         extendXRange(xAxis.getMaximumValue());
176         double[] boundaries = { redValue, yellowValue, greenValue };
177         final Color[] colorValues = { Color.RED, Color.YELLOW, Color.GREEN };
178         this.paintScale = new ContinuousColorPaintScale(valueFormat, boundaries, colorValues);
179         createChart(this);
180         reGraph();
181     }
182 
183     /** the GTUs that might be of interest to gather statistics about. */
184     private Set<LaneBasedGTU> gtusOfInterest = new HashSet<>();
185 
186     /** {@inheritDoc} */
187     @Override
188     @SuppressWarnings("checkstyle:designforextension")
189     public void notify(final EventInterface event) throws RemoteException
190     {
191         LaneBasedGTU gtu;
192         if (event.getType().equals(Lane.GTU_ADD_EVENT))
193         {
194             Object[] content = (Object[]) event.getContent();
195             gtu = (LaneBasedGTU) content[1];
196             if (!this.gtusOfInterest.contains(gtu))
197             {
198                 this.gtusOfInterest.add(gtu);
199                 gtu.addListener(this, LaneBasedGTU.MOVE_EVENT);
200             }
201         }
202         else if (event.getType().equals(Lane.GTU_REMOVE_EVENT))
203         {
204             Object[] content = (Object[]) event.getContent();
205             gtu = (LaneBasedGTU) content[1];
206             boolean interest = false;
207             for (Lane lane : gtu.getLanes().keySet())
208             {
209                 if (this.path.contains(lane))
210                 {
211                     interest = true;
212                 }
213             }
214             if (!interest)
215             {
216                 this.gtusOfInterest.remove(gtu);
217                 gtu.removeListener(this, LaneBasedGTU.MOVE_EVENT);
218             }
219         }
220         else if (event.getType().equals(LaneBasedGTU.MOVE_EVENT))
221         {
222             Object[] content = (Object[]) event.getContent();
223             Lane lane = (Lane) content[6];
224             gtu = (LaneBasedGTU) event.getSource();
225             addData(gtu, lane);
226         }
227     }
228 
229     /**
230      * Retrieve the cumulative length of the sampled path at the end of a path element.
231      * @param index int; the index of the path element; if -1, the total length of the path is returned
232      * @return Length; the cumulative length at the end of the specified path element
233      */
234     public final Length getCumulativeLength(final int index)
235     {
236         int useIndex = -1 == index ? this.cumulativeLengths.size() - 1 : index;
237         try
238         {
239             return new Length(this.cumulativeLengths.get(useIndex));
240         }
241         catch (ValueException exception)
242         {
243             exception.printStackTrace();
244         }
245         return null; // NOTREACHED
246     }
247 
248     /**
249      * Create a JMenu to let the user set the granularity of the XYBlockChart.
250      * @param menuName String; caption for the new JMenu
251      * @param format String; format string for the values in the items under the new JMenu
252      * @param commandPrefix String; prefix for the actionCommand of the items under the new JMenu
253      * @param values double[]; array of values to be formatted using the format strings to yield the items under the new JMenu
254      * @param currentValue double; the currently selected value (used to put the bullet on the correct item)
255      * @return JMenu with JRadioMenuItems for the values and a bullet on the currentValue item
256      */
257     private JMenu buildMenu(final String menuName, final String format, final String commandPrefix, final double[] values,
258             final double currentValue)
259     {
260         final JMenu result = new JMenu(menuName);
261         // Enlighten me: Do the menu items store a reference to the ButtonGroup so it won't get garbage collected?
262         final ButtonGroup group = new ButtonGroup();
263         for (double value : values)
264         {
265             final JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, value));
266             item.setSelected(value == currentValue);
267             item.setActionCommand(commandPrefix + String.format(Locale.US, " %f", value));
268             item.addActionListener(this);
269             result.add(item);
270             group.add(item);
271         }
272         return result;
273     }
274 
275     /**
276      * Create a XYBlockChart.
277      * @param container JFrame; the JFrame that will be populated with the chart and the status label
278      * @return JFreeChart; the new XYBlockChart
279      */
280     private JFreeChart createChart(final JFrame container)
281     {
282         final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
283         container.add(statusLabel, BorderLayout.SOUTH);
284         final NumberAxis xAxis1 = new NumberAxis("\u2192 " + "time [s]");
285         xAxis1.setLowerMargin(0.0);
286         xAxis1.setUpperMargin(0.0);
287         final NumberAxis yAxis1 = new NumberAxis("\u2192 " + "Distance [m]");
288         yAxis1.setAutoRangeIncludesZero(false);
289         yAxis1.setLowerMargin(0.0);
290         yAxis1.setUpperMargin(0.0);
291         yAxis1.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
292         XYBlockRenderer renderer = new XYBlockRenderer();
293         renderer.setPaintScale(this.paintScale);
294         final XYPlot plot = new XYPlot(this, xAxis1, yAxis1, renderer);
295         final LegendItemCollection legend = new LegendItemCollection();
296         for (int i = 0;; i++)
297         {
298             double value = this.paintScale.getLowerBound() + i * this.legendStep;
299             if (value > this.paintScale.getUpperBound())
300             {
301                 break;
302             }
303             legend.add(new LegendItem(String.format(this.legendFormat, value), this.paintScale.getPaint(value)));
304         }
305         legend.add(new LegendItem("No data", Color.BLACK));
306         plot.setFixedLegendItems(legend);
307         plot.setBackgroundPaint(Color.lightGray);
308         plot.setDomainGridlinePaint(Color.white);
309         plot.setRangeGridlinePaint(Color.white);
310         final JFreeChart chart = new JFreeChart(this.caption, plot);
311         FixCaption.fixCaption(chart);
312         chart.setBackgroundPaint(Color.white);
313         final ChartPanel cp = new ChartPanel(chart);
314         final PointerHandler ph = new PointerHandler()
315         {
316             /** {@inheritDoc} */
317             @Override
318             void updateHint(final double domainValue, final double rangeValue)
319             {
320                 if (Double.isNaN(domainValue))
321                 {
322                     statusLabel.setText(" ");
323                     return;
324                 }
325                 // XYPlot plot = (XYPlot) getChartPanel().getChart().getPlot();
326                 XYZDataset dataset = (XYZDataset) plot.getDataset();
327                 String value = "";
328                 double roundedTime = domainValue;
329                 double roundedDistance = rangeValue;
330                 for (int item = dataset.getItemCount(0); --item >= 0;)
331                 {
332                     double x = dataset.getXValue(0, item);
333                     if (x + ContourPlot.this.xAxis.getCurrentGranularity() / 2 < domainValue
334                             || x - ContourPlot.this.xAxis.getCurrentGranularity() / 2 >= domainValue)
335                     {
336                         continue;
337                     }
338                     double y = dataset.getYValue(0, item);
339                     if (y + ContourPlot.this.yAxis.getCurrentGranularity() / 2 < rangeValue
340                             || y - ContourPlot.this.yAxis.getCurrentGranularity() / 2 >= rangeValue)
341                     {
342                         continue;
343                     }
344                     roundedTime = x;
345                     roundedDistance = y;
346                     double valueUnderMouse = dataset.getZValue(0, item);
347                     // System.out.println("Value under mouse is " + valueUnderMouse);
348                     if (Double.isNaN(valueUnderMouse))
349                     {
350                         break;
351                     }
352                     String format = ((ContinuousColorPaintScale) (((XYBlockRenderer) (plot.getRenderer(0))).getPaintScale()))
353                             .getFormat();
354                     value = String.format(format, valueUnderMouse);
355                 }
356                 statusLabel.setText(String.format("time %.0fs, distance %.0fm, %s", roundedTime, roundedDistance, value));
357             }
358 
359         };
360         cp.addMouseMotionListener(ph);
361         cp.addMouseListener(ph);
362         container.add(cp, BorderLayout.CENTER);
363         cp.setMouseWheelEnabled(true);
364         JPopupMenu popupMenu = cp.getPopupMenu();
365         popupMenu.add(new JPopupMenu.Separator());
366         popupMenu.add(StandAloneChartWindow.createMenuItem(this));
367         popupMenu.insert(buildMenu("Distance granularity", "%.0f m", "setDistanceGranularity", this.yAxis.getGranularities(),
368                 this.yAxis.getCurrentGranularity()), 0);
369         popupMenu.insert(buildMenu("Time granularity", "%.0f s", "setTimeGranularity", this.xAxis.getGranularities(),
370                 this.xAxis.getCurrentGranularity()), 1);
371         return chart;
372     }
373 
374     /** {@inheritDoc} */
375     @Override
376     public final void actionPerformed(final ActionEvent actionEvent)
377     {
378         final String command = actionEvent.getActionCommand();
379         // System.out.println("command is \"" + command + "\"");
380         String[] fields = command.split("[ ]");
381         if (fields.length == 2)
382         {
383             final NumberFormat nf = NumberFormat.getInstance(Locale.US);
384             double value;
385             try
386             {
387                 value = nf.parse(fields[1]).doubleValue();
388             }
389             catch (ParseException e)
390             {
391                 throw new RuntimeException("Bad value: " + fields[1]);
392             }
393             if (fields[0].equalsIgnoreCase("setDistanceGranularity"))
394             {
395                 this.getYAxis().setCurrentGranularity(value);
396                 clearCachedValues();
397             }
398             else if (fields[0].equalsIgnoreCase("setTimeGranularity"))
399             {
400                 this.getXAxis().setCurrentGranularity(value);
401                 clearCachedValues();
402             }
403             else
404             {
405                 throw new RuntimeException("Unknown ActionEvent");
406             }
407             reGraph();
408         }
409         else
410         {
411             throw new RuntimeException("Unknown ActionEvent: " + command);
412         }
413     }
414 
415     /**
416      * Redraw this ContourGraph (after the underlying data, or a granularity setting has been changed).
417      */
418     public final void reGraph()
419     {
420         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
421         {
422             if (dcl instanceof XYPlot)
423             {
424                 final XYPlot plot = (XYPlot) dcl;
425                 plot.notifyListeners(new PlotChangeEvent(plot));
426                 final XYBlockRenderer blockRenderer = (XYBlockRenderer) plot.getRenderer();
427                 blockRenderer.setBlockHeight(this.getYAxis().getCurrentGranularity());
428                 blockRenderer.setBlockWidth(this.getXAxis().getCurrentGranularity());
429                 // configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI());
430             }
431         }
432         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
433     }
434 
435     /**
436      * Notify interested parties of an event affecting this ContourPlot.
437      * @param event DatasetChangedEvent
438      */
439     private void notifyListeners(final DatasetChangeEvent event)
440     {
441         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
442         {
443             dcl.datasetChanged(event);
444         }
445     }
446 
447     /** List of parties interested in changes of this ContourPlot. */
448     private transient EventListenerList listenerList = new EventListenerList();
449 
450     /** {@inheritDoc} */
451     @Override
452     public final int getSeriesCount()
453     {
454         return 1;
455     }
456 
457     /** Cached result of yAxisBins. */
458     private int cachedYAxisBins = -1;
459 
460     /**
461      * Retrieve the number of cells to use along the distance axis.
462      * @return Integer; the number of cells to use along the distance axis
463      */
464     protected final int yAxisBins()
465     {
466         if (this.cachedYAxisBins >= 0)
467         {
468             return this.cachedYAxisBins;
469         }
470         this.cachedYAxisBins = this.getYAxis().getAggregatedBinCount();
471         return this.cachedYAxisBins;
472     }
473 
474     /**
475      * Return the y-axis bin number (the row number) of an item. <br>
476      * Do not rely on the (current) fact that the data is stored column by column!
477      * @param item Integer; the item
478      * @return Integer; the bin number along the y axis of the item
479      */
480     protected final int yAxisBin(final int item)
481     {
482         int maxItem = getItemCount(0);
483         if (item < 0 || item >= maxItem)
484         {
485             throw new RuntimeException("yAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
486         }
487         return item % yAxisBins();
488     }
489 
490     /**
491      * Return the x-axis bin number (the column number) of an item. <br>
492      * Do not rely on the (current) fact that the data is stored column by column!
493      * @param item Integer; the item
494      * @return Integer; the bin number along the x axis of the item
495      */
496     protected final int xAxisBin(final int item)
497     {
498         int maxItem = getItemCount(0);
499         if (item < 0 || item >= maxItem)
500         {
501             throw new RuntimeException("xAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
502         }
503         return item / yAxisBins();
504     }
505 
506     /** Cached result of xAxisBins. */
507     private int cachedXAxisBins = -1;
508 
509     /**
510      * Retrieve the number of cells to use along the time axis.
511      * @return Integer; the number of cells to use along the time axis
512      */
513     protected final int xAxisBins()
514     {
515         if (this.cachedXAxisBins >= 0)
516         {
517             return this.cachedXAxisBins;
518         }
519         this.cachedXAxisBins = this.getXAxis().getAggregatedBinCount();
520         return this.cachedXAxisBins;
521     }
522 
523     /** Cached result of getItemCount. */
524     private int cachedItemCount = -1;
525 
526     /** {@inheritDoc} */
527     @Override
528     public final int getItemCount(final int series)
529     {
530         if (this.cachedItemCount >= 0)
531         {
532             return this.cachedItemCount;
533         }
534         this.cachedItemCount = yAxisBins() * xAxisBins();
535         return this.cachedItemCount;
536     }
537 
538     /** {@inheritDoc} */
539     @Override
540     public final Number getX(final int series, final int item)
541     {
542         return getXValue(series, item);
543     }
544 
545     /** {@inheritDoc} */
546     @Override
547     public final double getXValue(final int series, final int item)
548     {
549         double result = this.getXAxis().getValue(xAxisBin(item));
550         // System.out.println(String.format("XValue(%d, %d) -> %.3f, binCount=%d", series, item, result,
551         // this.yAxisDefinition.getAggregatedBinCount()));
552         return result;
553     }
554 
555     /** {@inheritDoc} */
556     @Override
557     public final Number getY(final int series, final int item)
558     {
559         return getYValue(series, item);
560     }
561 
562     /** {@inheritDoc} */
563     @Override
564     public final double getYValue(final int series, final int item)
565     {
566         return this.getYAxis().getValue(yAxisBin(item));
567     }
568 
569     /** {@inheritDoc} */
570     @Override
571     public final Number getZ(final int series, final int item)
572     {
573         return getZValue(series, item);
574     }
575 
576     /** {@inheritDoc} */
577     @Override
578     public final void addChangeListener(final DatasetChangeListener listener)
579     {
580         this.listenerList.add(DatasetChangeListener.class, listener);
581     }
582 
583     /** {@inheritDoc} */
584     @Override
585     public final void removeChangeListener(final DatasetChangeListener listener)
586     {
587         this.listenerList.remove(DatasetChangeListener.class, listener);
588     }
589 
590     /** {@inheritDoc} */
591     @Override
592     public final DatasetGroup getGroup()
593     {
594         return null;
595     }
596 
597     /** {@inheritDoc} */
598     @Override
599     public void setGroup(final DatasetGroup group)
600     {
601         // ignore
602     }
603 
604     /** {@inheritDoc} */
605     @SuppressWarnings("rawtypes")
606     @Override
607     public final int indexOf(final Comparable seriesKey)
608     {
609         return 0;
610     }
611 
612     /** {@inheritDoc} */
613     @Override
614     public final DomainOrder getDomainOrder()
615     {
616         return DomainOrder.ASCENDING;
617     }
618 
619     /**
620      * Make sure that the results of the most called methods are re-calculated.
621      */
622     private void clearCachedValues()
623     {
624         this.cachedItemCount = -1;
625         this.cachedXAxisBins = -1;
626         this.cachedYAxisBins = -1;
627     }
628 
629     /**
630      * Add data for a GTU on a lane to this graph.
631      * @param gtu the gtu to add the data for
632      * @param lane the lane on which the GTU is registered
633      */
634     protected final void addData(final LaneBasedGTU gtu, final Lane lane)
635     {
636         // System.out.println("addData car: " + car + ", lastEval: " + car.getSimulator().getSimulatorTime()
637         // + " position of rear on lane " + lane + " is " + car.position(lane, car.getRear()));
638         // Convert the position of the car to a position on path.
639         double lengthOffset = 0;
640         int index = this.path.indexOf(lane);
641         if (index >= 0)
642         {
643             if (index > 0)
644             {
645                 try
646                 {
647                     lengthOffset = this.cumulativeLengths.getSI(index - 1);
648                 }
649                 catch (ValueException exception)
650                 {
651                     // error -- silently ignore for now. Graphs should not cause errors.
652                     System.err.println("ContourPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
653                             + exception.getMessage());
654                 }
655             }
656         }
657         else
658         {
659             // error -- silently ignore for now. Graphs should not cause errors.
660             System.err.println("ContouryPlot: GTU " + gtu.getId() + " is not registered on lane " + lane.toString());
661         }
662 
663         try
664         {
665             final Time fromTime = gtu.getOperationalPlan().getStartTime();
666             if (gtu.position(lane, gtu.getRear(), fromTime).getSI() < 0 && lengthOffset > 0)
667             {
668                 return;
669             }
670             final Time toTime = gtu.getOperationalPlan().getEndTime();
671             if (toTime.getSI() > this.getXAxis().getMaximumValue().getSI())
672             {
673                 extendXRange(toTime);
674                 clearCachedValues();
675                 this.getXAxis().adjustMaximumValue(toTime);
676             }
677             if (toTime.le(fromTime)) // degenerate sample???
678             {
679                 return;
680             }
681             // The "relative" values are "counting" distance or time in the minimum bin size unit
682             final double relativeFromDistance = (gtu.position(lane, gtu.getRear(), fromTime).getSI() + lengthOffset)
683                     / this.getYAxis().getGranularities()[0];
684             final double relativeToDistance =
685                     (gtu.position(lane, gtu.getRear(), toTime).getSI() + lengthOffset) / this.getYAxis().getGranularities()[0];
686             double relativeFromTime =
687                     (fromTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
688             final double relativeToTime =
689                     (toTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
690             final int fromTimeBin = (int) Math.floor(relativeFromTime);
691             final int toTimeBin = (int) Math.floor(relativeToTime) + 1;
692             double relativeMeanSpeed = (relativeToDistance - relativeFromDistance) / (relativeToTime - relativeFromTime);
693             // The code for acceleration assumes that acceleration is constant (which is correct for IDM+, but may be
694             // wrong for other car following algorithms).
695             double acceleration = gtu.getAcceleration().getSI();
696             for (int timeBin = fromTimeBin; timeBin < toTimeBin; timeBin++)
697             {
698                 if (timeBin < 0)
699                 {
700                     continue;
701                 }
702                 double binEndTime = timeBin + 1;
703                 if (binEndTime > relativeToTime)
704                 {
705                     binEndTime = relativeToTime;
706                 }
707                 if (binEndTime <= relativeFromTime)
708                 {
709                     continue; // no time spent in this timeBin
710                 }
711                 double binDistanceStart = (gtu
712                         .position(lane, gtu.getRear(),
713                                 new Time(relativeFromTime * this.getXAxis().getGranularities()[0], TimeUnit.SECOND))
714                         .getSI() - this.getYAxis().getMinimumValue().getSI() + lengthOffset)
715                         / this.getYAxis().getGranularities()[0];
716                 double binDistanceEnd = (gtu
717                         .position(lane, gtu.getRear(),
718                                 new Time(binEndTime * this.getXAxis().getGranularities()[0], TimeUnit.SECOND))
719                         .getSI() - this.getYAxis().getMinimumValue().getSI() + lengthOffset)
720                         / this.getYAxis().getGranularities()[0];
721 
722                 // Compute the time in each distanceBin
723                 for (int distanceBin = (int) Math.floor(binDistanceStart); distanceBin <= binDistanceEnd; distanceBin++)
724                 {
725                     double relativeDuration = 1;
726                     if (relativeFromTime > timeBin)
727                     {
728                         relativeDuration -= relativeFromTime - timeBin;
729                     }
730                     if (distanceBin == (int) Math.floor(binDistanceEnd))
731                     {
732                         // This GTU does not move out of this distanceBin before the binEndTime
733                         if (binEndTime < timeBin + 1)
734                         {
735                             relativeDuration -= timeBin + 1 - binEndTime;
736                         }
737                     }
738                     else
739                     {
740                         // This GTU moves out of this distanceBin before the binEndTime
741                         // Interpolate the time when this GTU crosses into the next distanceBin
742                         // Using f.i. Newton-Rhaphson interpolation would yield a slightly more precise result...
743                         double timeToBinBoundary = (distanceBin + 1 - binDistanceStart) / relativeMeanSpeed;
744                         double endTime = relativeFromTime + timeToBinBoundary;
745                         relativeDuration -= timeBin + 1 - endTime;
746                     }
747                     final double duration = relativeDuration * this.getXAxis().getGranularities()[0];
748                     final double distance = duration * relativeMeanSpeed * this.getYAxis().getGranularities()[0];
749                     // System.out.println(String.format(
750                     // "timeBin=%d, distanceBin=%d, duration=%f, distance=%f, timeBinSize=%f, distanceBinSize=%f", timeBin,
751                     // distanceBin, duration, distance, this.getYAxis().getGranularities()[0], this.getXAxis()
752                     // .getGranularities()[0]));
753                     incrementBinData(timeBin, distanceBin, duration, distance, acceleration);
754                     relativeFromTime += relativeDuration;
755                     binDistanceStart = distanceBin + 1;
756                 }
757                 relativeFromTime = timeBin + 1;
758             }
759         }
760         catch (GTUException exception)
761         {
762             // error -- silently ignore for now. Graphs should not cause errors.
763             System.err.println("ContourPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
764                     + exception.getMessage());
765         }
766     }
767 
768     /**
769      * Increase storage for sample data. <br>
770      * This is only implemented for the time axis.
771      * @param newUpperLimit DoubleScalar&lt;?&gt; new upper limit for the X range
772      */
773     public abstract void extendXRange(DoubleScalar<?> newUpperLimit);
774 
775     /**
776      * Increment the data of one bin.
777      * @param timeBin Integer; the rank of the bin on the time-scale
778      * @param distanceBin Integer; the rank of the bin on the distance-scale
779      * @param duration Double; the time spent in this bin
780      * @param distanceCovered Double; the distance covered in this bin
781      * @param acceleration Double; the average acceleration in this bin
782      */
783     public abstract void incrementBinData(int timeBin, int distanceBin, double duration, double distanceCovered,
784             double acceleration);
785 
786     /** {@inheritDoc} */
787     @Override
788     public final double getZValue(final int series, final int item)
789     {
790         final int timeBinGroup = xAxisBin(item);
791         final int distanceBinGroup = yAxisBin(item);
792         // System.out.println(String.format("getZValue(s=%d, i=%d) -> tbg=%d, dbg=%d", series, item, timeBinGroup,
793         // distanceBinGroup));
794         final int timeGroupSize = (int) (this.getXAxis().getCurrentGranularity() / this.getXAxis().getGranularities()[0]);
795         final int firstTimeBin = timeBinGroup * timeGroupSize;
796         final int distanceGroupSize = (int) (this.getYAxis().getCurrentGranularity() / this.getYAxis().getGranularities()[0]);
797         final int firstDistanceBin = distanceBinGroup * distanceGroupSize;
798         final int endTimeBin = Math.min(firstTimeBin + timeGroupSize, this.getXAxis().getBinCount());
799         final int endDistanceBin = Math.min(firstDistanceBin + distanceGroupSize, this.getYAxis().getBinCount());
800         return computeZValue(firstTimeBin, endTimeBin, firstDistanceBin, endDistanceBin);
801     }
802 
803     /**
804      * Combine values in a range of time bins and distance bins to obtain a combined density value of the ranges.
805      * @param firstTimeBin Integer; the first time bin to use
806      * @param endTimeBin Integer; one higher than the last time bin to use
807      * @param firstDistanceBin Integer; the first distance bin to use
808      * @param endDistanceBin Integer; one higher than the last distance bin to use
809      * @return Double; the density value (or Double.NaN if no value can be computed)
810      */
811     public abstract double computeZValue(int firstTimeBin, int endTimeBin, int firstDistanceBin, int endDistanceBin);
812 
813     /**
814      * Get the X axis.
815      * @return Axis
816      */
817     public final Axis getXAxis()
818     {
819         return this.xAxis;
820     }
821 
822     /**
823      * Get the Y axis.
824      * @return Axis
825      */
826     public final Axis getYAxis()
827     {
828         return this.yAxis;
829     }
830 
831     /** {@inheritDoc} */
832     @Override
833     public final JFrame addViewer()
834     {
835         JFrame result = new JFrame(this.caption);
836         JFreeChart newChart = createChart(result);
837         newChart.setTitle((String) null);
838         addChangeListener(newChart.getPlot());
839         reGraph();
840         return result;
841     }
842 
843 }