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