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