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