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