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