ContourPlot.java

  1. package org.opentrafficsim.graphs;

  2. import java.awt.BorderLayout;
  3. import java.awt.Color;
  4. import java.awt.event.ActionEvent;
  5. import java.awt.event.ActionListener;
  6. import java.io.Serializable;
  7. import java.rmi.RemoteException;
  8. import java.text.NumberFormat;
  9. import java.text.ParseException;
  10. import java.util.HashSet;
  11. import java.util.List;
  12. import java.util.Locale;
  13. import java.util.Set;

  14. import javax.swing.ButtonGroup;
  15. import javax.swing.JFrame;
  16. import javax.swing.JLabel;
  17. import javax.swing.JMenu;
  18. import javax.swing.JPopupMenu;
  19. import javax.swing.JRadioButtonMenuItem;
  20. import javax.swing.SwingConstants;

  21. import org.djunits.unit.LengthUnit;
  22. import org.djunits.unit.TimeUnit;
  23. import org.djunits.value.StorageType;
  24. import org.djunits.value.ValueException;
  25. import org.djunits.value.vdouble.scalar.DoubleScalarInterface;
  26. import org.djunits.value.vdouble.scalar.Length;
  27. import org.djunits.value.vdouble.scalar.Time;
  28. import org.djunits.value.vdouble.vector.LengthVector;
  29. import org.jfree.chart.ChartPanel;
  30. import org.jfree.chart.JFreeChart;
  31. import org.jfree.chart.LegendItem;
  32. import org.jfree.chart.LegendItemCollection;
  33. import org.jfree.chart.axis.NumberAxis;
  34. import org.jfree.chart.event.PlotChangeEvent;
  35. import org.jfree.chart.plot.XYPlot;
  36. import org.jfree.chart.renderer.xy.XYBlockRenderer;
  37. import org.jfree.data.DomainOrder;
  38. import org.jfree.data.general.DatasetChangeEvent;
  39. import org.jfree.data.general.DatasetChangeListener;
  40. import org.jfree.data.general.DatasetGroup;
  41. import org.jfree.data.xy.XYZDataset;
  42. import org.opentrafficsim.core.gtu.GTUException;
  43. import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
  44. import org.opentrafficsim.road.network.lane.Lane;
  45. import org.opentrafficsim.simulationengine.OTSSimulationException;

  46. import nl.tudelft.simulation.event.EventInterface;
  47. import nl.tudelft.simulation.event.EventListenerInterface;
  48. import nl.tudelft.simulation.event.TimedEvent;

  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-2018 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 AbstractOTSPlot
  61.         implements ActionListener, XYZDataset, MultipleViewerChart, LaneBasedGTUSampler, EventListenerInterface, Serializable
  62. {
  63.     /** */
  64.     private static final long serialVersionUID = 20140716L;

  65.     /** Color scale for the graph. */
  66.     private final ContinuousColorPaintScale paintScale;

  67.     /** Definition of the X-axis. */
  68.     @SuppressWarnings("visibilitymodifier")
  69.     protected final Axis xAxis;

  70.     /** Definition of the Y-axis. */
  71.     @SuppressWarnings("visibilitymodifier")
  72.     protected final Axis yAxis;

  73.     /** Difference of successive values in the legend. */
  74.     private final double legendStep;

  75.     /** Format string used to create the captions in the legend. */
  76.     private final String legendFormat;

  77.     /** Time granularity values. */
  78.     protected static final double[] STANDARDTIMEGRANULARITIES = { 1, 2, 5, 10, 20, 30, 60, 120, 300, 600 };

  79.     /** Index of the initial time granularity in standardTimeGranularites. */
  80.     protected static final int STANDARDINITIALTIMEGRANULARITYINDEX = 3;

  81.     /** Distance granularity values. */
  82.     protected static final double[] STANDARDDISTANCEGRANULARITIES = { 10, 20, 50, 100, 200, 500, 1000 };

  83.     /** Index of the initial distance granularity in standardTimeGranularites. */
  84.     protected static final int STANDARDINITIALDISTANCEGRANULARITYINDEX = 3;

  85.     /** Initial lower bound for the time scale. */
  86.     protected static final Time INITIALLOWERTIMEBOUND = new Time(0, TimeUnit.BASE);

  87.     /** Initial upper bound for the time scale. */
  88.     protected static final Time INITIALUPPERTIMEBOUND = new Time(300, TimeUnit.BASE);

  89.     /** The cumulative lengths of the elements of path. */
  90.     private final LengthVector cumulativeLengths;

  91.     /**
  92.      * Create a new ContourPlot.
  93.      * @param caption String; text to show above the plotting area
  94.      * @param xAxis Axis; the X (time) axis
  95.      * @param path ArrayList&lt;Lane&gt;; the series of Lanes that will provide the data for this TrajectoryPlot
  96.      * @param redValue Double; contour value that will be rendered in Red
  97.      * @param yellowValue Double; contour value that will be rendered in Yellow
  98.      * @param greenValue Double; contour value that will be rendered in Green
  99.      * @param valueFormat String; format string for the contour values
  100.      * @param legendFormat String; format string for the captions in the color legend
  101.      * @param legendStep Double; increment between color legend entries
  102.      * @throws OTSSimulationException when the scale cannot be generated
  103.      */
  104.     public ContourPlot(final String caption, final Axis xAxis, final List<Lane> path, final double redValue,
  105.             final double yellowValue, final double greenValue, final String valueFormat, final String legendFormat,
  106.             final double legendStep) throws OTSSimulationException
  107.     {
  108.         super(caption, path);
  109.         double[] endLengths = new double[path.size()];
  110.         double cumulativeLength = 0;
  111.         LengthVector lengths = null;
  112.         for (int i = 0; i < path.size(); i++)
  113.         {
  114.             Lane lane = path.get(i);
  115.             lane.addListener(this, Lane.GTU_ADD_EVENT, true);
  116.             lane.addListener(this, Lane.GTU_REMOVE_EVENT, true);
  117.             try
  118.             {
  119.                 // register the current GTUs on the lanes (if any) for statistics sampling.
  120.                 for (LaneBasedGTU gtu : lane.getGtuList())
  121.                 {
  122.                     notify(new TimedEvent<Time>(Lane.GTU_ADD_EVENT, lane, new Object[] { gtu.getId(), gtu },
  123.                             gtu.getSimulator().getSimulatorTime()));
  124.                 }
  125.             }
  126.             catch (RemoteException exception)
  127.             {
  128.                 exception.printStackTrace();
  129.             }
  130.             cumulativeLength += lane.getLength().getSI();
  131.             endLengths[i] = cumulativeLength;
  132.         }
  133.         try
  134.         {
  135.             lengths = new LengthVector(endLengths, LengthUnit.SI, StorageType.DENSE);
  136.         }
  137.         catch (ValueException exception)
  138.         {
  139.             exception.printStackTrace();
  140.         }
  141.         this.cumulativeLengths = lengths;
  142.         this.xAxis = xAxis;
  143.         this.yAxis = new Axis(new Length(0, LengthUnit.METER), getCumulativeLength(-1), STANDARDDISTANCEGRANULARITIES,
  144.                 STANDARDDISTANCEGRANULARITIES[STANDARDINITIALDISTANCEGRANULARITYINDEX], "", "Distance", "%.0fm");
  145.         this.legendStep = legendStep;
  146.         this.legendFormat = legendFormat;
  147.         extendXRange(xAxis.getMaximumValue());
  148.         double[] boundaries = { redValue, yellowValue, greenValue };
  149.         final Color[] colorValues = { Color.RED, Color.YELLOW, Color.GREEN };
  150.         this.paintScale = new ContinuousColorPaintScale(valueFormat, boundaries, colorValues);
  151.         setChart(createChart(this));
  152.         reGraph();
  153.     }

  154.     /** the GTUs that might be of interest to gather statistics about. */
  155.     private Set<LaneBasedGTU> gtusOfInterest = new HashSet<>();

  156.     /** {@inheritDoc} */
  157.     @Override
  158.     @SuppressWarnings("checkstyle:designforextension")
  159.     public void notify(final EventInterface event) throws RemoteException
  160.     {
  161.         if (event.getType().equals(Lane.GTU_ADD_EVENT))
  162.         {
  163.             Object[] content = (Object[]) event.getContent();
  164.             LaneBasedGTU gtu = (LaneBasedGTU) content[1];
  165.             if (!this.gtusOfInterest.contains(gtu))
  166.             {
  167.                 // System.out.println("Adding " + gtu.getId() + " to lane " + event.getSource());
  168.                 this.gtusOfInterest.add(gtu);
  169.                 gtu.addListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
  170.             }
  171.         }
  172.         else if (event.getType().equals(Lane.GTU_REMOVE_EVENT))
  173.         {
  174.             Object[] content = (Object[]) event.getContent();
  175.             LaneBasedGTU gtu = (LaneBasedGTU) content[1];
  176.             // if (getPath().contains(event.getSource()))
  177.             // {
  178.             // //System.out.println("Removing " + gtu.getId() + " from lane " + event.getSource());
  179.             // this.gtusOfInterest.remove(gtu);
  180.             // gtu.removeListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
  181.             // }
  182.             Lane lane = null;
  183.             try
  184.             {
  185.                 lane = gtu.getReferencePosition().getLane();
  186.             }
  187.             catch (GTUException exception)
  188.             {
  189.                 // ignore - lane will be null
  190.             }
  191.             if (lane == null || !getPath().contains(lane))
  192.             {
  193.                 this.gtusOfInterest.remove(gtu);
  194.                 gtu.removeListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
  195.             }
  196.         }
  197.         else if (event.getType().equals(LaneBasedGTU.LANEBASED_MOVE_EVENT))
  198.         {
  199.             Object[] content = (Object[]) event.getContent();
  200.             Lane lane = (Lane) content[6];
  201.             LaneBasedGTU gtu = (LaneBasedGTU) event.getSource();
  202.             // System.out.println("Moving GTU " + gtu.getId());
  203.             addData(gtu, lane);
  204.         }
  205.     }

  206.     /**
  207.      * Retrieve the cumulative length of the sampled path at the end of a path element.
  208.      * @param index int; the index of the path element; if -1, the total length of the path is returned
  209.      * @return Length; the cumulative length at the end of the specified path element
  210.      */
  211.     public final Length getCumulativeLength(final int index)
  212.     {
  213.         int useIndex = -1 == index ? this.cumulativeLengths.size() - 1 : index;
  214.         try
  215.         {
  216.             return new Length(this.cumulativeLengths.get(useIndex));
  217.         }
  218.         catch (ValueException exception)
  219.         {
  220.             exception.printStackTrace();
  221.         }
  222.         return null; // NOTREACHED
  223.     }

  224.     /**
  225.      * Create a JMenu to let the user set the granularity of the XYBlockChart.
  226.      * @param menuName String; caption for the new JMenu
  227.      * @param format String; format string for the values in the items under the new JMenu
  228.      * @param commandPrefix String; prefix for the actionCommand of the items under the new JMenu
  229.      * @param values double[]; array of values to be formatted using the format strings to yield the items under the new JMenu
  230.      * @param currentValue double; the currently selected value (used to put the bullet on the correct item)
  231.      * @return JMenu with JRadioMenuItems for the values and a bullet on the currentValue item
  232.      */
  233.     private JMenu buildMenu(final String menuName, final String format, final String commandPrefix, final double[] values,
  234.             final double currentValue)
  235.     {
  236.         final JMenu result = new JMenu(menuName);
  237.         // Enlighten me: Do the menu items store a reference to the ButtonGroup so it won't get garbage collected?
  238.         final ButtonGroup group = new ButtonGroup();
  239.         for (double value : values)
  240.         {
  241.             final JRadioButtonMenuItem item = new JRadioButtonMenuItem(String.format(format, value));
  242.             item.setSelected(value == currentValue);
  243.             item.setActionCommand(commandPrefix + String.format(Locale.US, " %f", value));
  244.             item.addActionListener(this);
  245.             result.add(item);
  246.             group.add(item);
  247.         }
  248.         return result;
  249.     }

  250.     /** {@inheritDoc} */
  251.     @Override
  252.     protected final JFreeChart createChart(final JFrame container)
  253.     {
  254.         final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
  255.         container.add(statusLabel, BorderLayout.SOUTH);
  256.         final NumberAxis xAxis1 = new NumberAxis("\u2192 " + "time [s]");
  257.         xAxis1.setLowerMargin(0.0);
  258.         xAxis1.setUpperMargin(0.0);
  259.         final NumberAxis yAxis1 = new NumberAxis("\u2192 " + "Distance [m]");
  260.         yAxis1.setAutoRangeIncludesZero(false);
  261.         yAxis1.setLowerMargin(0.0);
  262.         yAxis1.setUpperMargin(0.0);
  263.         yAxis1.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
  264.         XYBlockRenderer renderer = new XYBlockRenderer();
  265.         renderer.setPaintScale(this.paintScale);
  266.         final XYPlot plot = new XYPlot(this, xAxis1, yAxis1, renderer);
  267.         final LegendItemCollection legend = new LegendItemCollection();
  268.         for (int i = 0;; i++)
  269.         {
  270.             double value = this.paintScale.getLowerBound() + i * this.legendStep;
  271.             if (value > this.paintScale.getUpperBound())
  272.             {
  273.                 break;
  274.             }
  275.             legend.add(new LegendItem(String.format(this.legendFormat, value), this.paintScale.getPaint(value)));
  276.         }
  277.         legend.add(new LegendItem("No data", Color.BLACK));
  278.         plot.setFixedLegendItems(legend);
  279.         plot.setBackgroundPaint(Color.lightGray);
  280.         plot.setDomainGridlinePaint(Color.white);
  281.         plot.setRangeGridlinePaint(Color.white);
  282.         final JFreeChart chart = new JFreeChart(getCaption(), plot);
  283.         FixCaption.fixCaption(chart);
  284.         chart.setBackgroundPaint(Color.white);
  285.         final ChartPanel cp = new ChartPanel(chart);
  286.         final PointerHandler ph = new PointerHandler()
  287.         {
  288.             /** {@inheritDoc} */
  289.             @Override
  290.             void updateHint(final double domainValue, final double rangeValue)
  291.             {
  292.                 if (Double.isNaN(domainValue))
  293.                 {
  294.                     statusLabel.setText(" ");
  295.                     return;
  296.                 }
  297.                 // XYPlot plot = (XYPlot) getChartPanel().getChart().getPlot();
  298.                 XYZDataset dataset = (XYZDataset) plot.getDataset();
  299.                 String value = "";
  300.                 double roundedTime = domainValue;
  301.                 double roundedDistance = rangeValue;
  302.                 for (int item = dataset.getItemCount(0); --item >= 0;)
  303.                 {
  304.                     double x = dataset.getXValue(0, item);
  305.                     if (x + ContourPlot.this.xAxis.getCurrentGranularity() / 2 < domainValue
  306.                             || x - ContourPlot.this.xAxis.getCurrentGranularity() / 2 >= domainValue)
  307.                     {
  308.                         continue;
  309.                     }
  310.                     double y = dataset.getYValue(0, item);
  311.                     if (y + ContourPlot.this.yAxis.getCurrentGranularity() / 2 < rangeValue
  312.                             || y - ContourPlot.this.yAxis.getCurrentGranularity() / 2 >= rangeValue)
  313.                     {
  314.                         continue;
  315.                     }
  316.                     roundedTime = x;
  317.                     roundedDistance = y;
  318.                     double valueUnderMouse = dataset.getZValue(0, item);
  319.                     // System.out.println("Value under mouse is " + valueUnderMouse);
  320.                     if (Double.isNaN(valueUnderMouse))
  321.                     {
  322.                         break;
  323.                     }
  324.                     String format = ((ContinuousColorPaintScale) (((XYBlockRenderer) (plot.getRenderer(0))).getPaintScale()))
  325.                             .getFormat();
  326.                     value = String.format(format, valueUnderMouse);
  327.                 }
  328.                 statusLabel.setText(String.format("time %.0fs, distance %.0fm, %s", roundedTime, roundedDistance, value));
  329.             }

  330.         };
  331.         cp.addMouseMotionListener(ph);
  332.         cp.addMouseListener(ph);
  333.         container.add(cp, BorderLayout.CENTER);
  334.         cp.setMouseWheelEnabled(true);
  335.         JPopupMenu popupMenu = cp.getPopupMenu();
  336.         popupMenu.add(new JPopupMenu.Separator());
  337.         popupMenu.add(StandAloneChartWindow.createMenuItem(this));
  338.         popupMenu.insert(buildMenu("Distance granularity", "%.0f m", "setDistanceGranularity", this.yAxis.getGranularities(),
  339.                 this.yAxis.getCurrentGranularity()), 0);
  340.         popupMenu.insert(buildMenu("Time granularity", "%.0f s", "setTimeGranularity", this.xAxis.getGranularities(),
  341.                 this.xAxis.getCurrentGranularity()), 1);
  342.         return chart;
  343.     }

  344.     /** {@inheritDoc} */
  345.     @Override
  346.     public final void actionPerformed(final ActionEvent actionEvent)
  347.     {
  348.         final String command = actionEvent.getActionCommand();
  349.         // System.out.println("command is \"" + command + "\"");
  350.         String[] fields = command.split("[ ]");
  351.         if (fields.length == 2)
  352.         {
  353.             final NumberFormat nf = NumberFormat.getInstance(Locale.US);
  354.             double value;
  355.             try
  356.             {
  357.                 value = nf.parse(fields[1]).doubleValue();
  358.             }
  359.             catch (ParseException e)
  360.             {
  361.                 throw new RuntimeException("Bad value: " + fields[1]);
  362.             }
  363.             if (fields[0].equalsIgnoreCase("setDistanceGranularity"))
  364.             {
  365.                 this.getYAxis().setCurrentGranularity(value);
  366.                 clearCachedValues();
  367.             }
  368.             else if (fields[0].equalsIgnoreCase("setTimeGranularity"))
  369.             {
  370.                 this.getXAxis().setCurrentGranularity(value);
  371.                 clearCachedValues();
  372.             }
  373.             else
  374.             {
  375.                 throw new RuntimeException("Unknown ActionEvent");
  376.             }
  377.             reGraph();
  378.         }
  379.         else
  380.         {
  381.             throw new RuntimeException("Unknown ActionEvent: " + command);
  382.         }
  383.     }

  384.     /**
  385.      * Redraw this ContourGraph (after the underlying data, or a granularity setting has been changed).
  386.      */
  387.     @Override
  388.     public final void reGraph()
  389.     {
  390.         for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class))
  391.         {
  392.             if (dcl instanceof XYPlot)
  393.             {
  394.                 final XYPlot plot = (XYPlot) dcl;
  395.                 final XYBlockRenderer blockRenderer = (XYBlockRenderer) plot.getRenderer();
  396.                 blockRenderer.setBlockHeight(this.getYAxis().getCurrentGranularity());
  397.                 blockRenderer.setBlockWidth(this.getXAxis().getCurrentGranularity());
  398.                 plot.notifyListeners(new PlotChangeEvent(plot));
  399.                 // configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI());
  400.             }
  401.         }
  402.         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
  403.     }

  404.     /** {@inheritDoc} */
  405.     @Override
  406.     public final int getSeriesCount()
  407.     {
  408.         return 1;
  409.     }

  410.     /** Cached result of yAxisBins. */
  411.     private int cachedYAxisBins = -1;

  412.     /**
  413.      * Retrieve the number of cells to use along the distance axis.
  414.      * @return Integer; the number of cells to use along the distance axis
  415.      */
  416.     protected final int yAxisBins()
  417.     {
  418.         if (this.cachedYAxisBins >= 0)
  419.         {
  420.             return this.cachedYAxisBins;
  421.         }
  422.         this.cachedYAxisBins = this.getYAxis().getAggregatedBinCount();
  423.         return this.cachedYAxisBins;
  424.     }

  425.     /**
  426.      * Return the y-axis bin number (the row number) of an item. <br>
  427.      * Do not rely on the (current) fact that the data is stored column by column!
  428.      * @param item Integer; the item
  429.      * @return Integer; the bin number along the y axis of the item
  430.      */
  431.     protected final int yAxisBin(final int item)
  432.     {
  433.         int maxItem = getItemCount(0);
  434.         if (item < 0 || item >= maxItem)
  435.         {
  436.             throw new RuntimeException("yAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
  437.         }
  438.         return item % yAxisBins();
  439.     }

  440.     /**
  441.      * Return the x-axis bin number (the column number) of an item. <br>
  442.      * Do not rely on the (current) fact that the data is stored column by column!
  443.      * @param item Integer; the item
  444.      * @return Integer; the bin number along the x axis of the item
  445.      */
  446.     protected final int xAxisBin(final int item)
  447.     {
  448.         int maxItem = getItemCount(0);
  449.         if (item < 0 || item >= maxItem)
  450.         {
  451.             throw new RuntimeException("xAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
  452.         }
  453.         return item / yAxisBins();
  454.     }

  455.     /** Cached result of xAxisBins. */
  456.     private int cachedXAxisBins = -1;

  457.     /**
  458.      * Retrieve the number of cells to use along the time axis.
  459.      * @return Integer; the number of cells to use along the time axis
  460.      */
  461.     protected final int xAxisBins()
  462.     {
  463.         if (this.cachedXAxisBins >= 0)
  464.         {
  465.             return this.cachedXAxisBins;
  466.         }
  467.         this.cachedXAxisBins = this.getXAxis().getAggregatedBinCount();
  468.         return this.cachedXAxisBins;
  469.     }

  470.     /** Cached result of getItemCount. */
  471.     private int cachedItemCount = -1;

  472.     /** {@inheritDoc} */
  473.     @Override
  474.     public final int getItemCount(final int series)
  475.     {
  476.         if (this.cachedItemCount >= 0)
  477.         {
  478.             return this.cachedItemCount;
  479.         }
  480.         this.cachedItemCount = yAxisBins() * xAxisBins();
  481.         return this.cachedItemCount;
  482.     }

  483.     /** {@inheritDoc} */
  484.     @Override
  485.     public final Number getX(final int series, final int item)
  486.     {
  487.         return getXValue(series, item);
  488.     }

  489.     /** {@inheritDoc} */
  490.     @Override
  491.     public final double getXValue(final int series, final int item)
  492.     {
  493.         double result = this.getXAxis().getValue(xAxisBin(item));
  494.         // System.out.println(String.format("XValue(%d, %d) -> %.3f, binCount=%d", series, item, result,
  495.         // this.yAxisDefinition.getAggregatedBinCount()));
  496.         return result;
  497.     }

  498.     /** {@inheritDoc} */
  499.     @Override
  500.     public final Number getY(final int series, final int item)
  501.     {
  502.         return getYValue(series, item);
  503.     }

  504.     /** {@inheritDoc} */
  505.     @Override
  506.     public final double getYValue(final int series, final int item)
  507.     {
  508.         return this.getYAxis().getValue(yAxisBin(item));
  509.     }

  510.     /** {@inheritDoc} */
  511.     @Override
  512.     public final Number getZ(final int series, final int item)
  513.     {
  514.         return getZValue(series, item);
  515.     }

  516.     /** {@inheritDoc} */
  517.     @Override
  518.     public final DatasetGroup getGroup()
  519.     {
  520.         return null;
  521.     }

  522.     /** {@inheritDoc} */
  523.     @Override
  524.     public void setGroup(final DatasetGroup group)
  525.     {
  526.         // ignore
  527.     }

  528.     /** {@inheritDoc} */
  529.     @SuppressWarnings("rawtypes")
  530.     @Override
  531.     public final int indexOf(final Comparable seriesKey)
  532.     {
  533.         return 0;
  534.     }

  535.     /** {@inheritDoc} */
  536.     @Override
  537.     public final DomainOrder getDomainOrder()
  538.     {
  539.         return DomainOrder.ASCENDING;
  540.     }

  541.     /**
  542.      * Make sure that the results of the most called methods are re-calculated.
  543.      */
  544.     private void clearCachedValues()
  545.     {
  546.         this.cachedItemCount = -1;
  547.         this.cachedXAxisBins = -1;
  548.         this.cachedYAxisBins = -1;
  549.     }

  550.     /**
  551.      * Add data for a GTU on a lane to this graph.
  552.      * @param gtu the gtu to add the data for
  553.      * @param lane the lane on which the GTU is registered
  554.      */
  555.     protected final void addData(final LaneBasedGTU gtu, final Lane lane)
  556.     {
  557.         // System.out.println("addData car: " + car + ", lastEval: " + car.getSimulator().getSimulatorTime()
  558.         // + " position of rear on lane " + lane + " is " + car.position(lane, car.getRear()));
  559.         // Convert the position of the car to a position on path.
  560.         double lengthOffset = 0;
  561.         int index = getPath().indexOf(lane);
  562.         if (index >= 0)
  563.         {
  564.             if (index > 0)
  565.             {
  566.                 try
  567.                 {
  568.                     lengthOffset = this.cumulativeLengths.getSI(index - 1);
  569.                 }
  570.                 catch (ValueException exception)
  571.                 {
  572.                     // error -- silently ignore for now. Graphs should not cause errors.
  573.                     System.err.println("ContourPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
  574.                             + exception.getMessage());
  575.                 }
  576.             }
  577.         }
  578.         else
  579.         {
  580.             // move events can be given for adjacent lanes during lane changes
  581.             return;
  582.             // error -- silently ignore for now. Graphs should not cause errors.
  583.             // System.err.println("ContourPlot: GTU " + gtu.getId() + " is not registered on lane " + lane.toString());
  584.         }

  585.         try
  586.         {
  587.             final Time fromTime = gtu.getOperationalPlan().getStartTime();
  588.             if (gtu.position(lane, gtu.getRear(), fromTime).getSI() < 0 && lengthOffset > 0)
  589.             {
  590.                 return;
  591.             }
  592.             final Time toTime = gtu.getOperationalPlan().getEndTime();
  593.             if (toTime.getSI() > this.getXAxis().getMaximumValue().getSI())
  594.             {
  595.                 extendXRange(toTime);
  596.                 clearCachedValues();
  597.                 this.getXAxis().adjustMaximumValue(toTime);
  598.             }
  599.             if (toTime.le(fromTime)) // degenerate sample???
  600.             {
  601.                 return;
  602.             }
  603.             // The "relative" values are "counting" distance or time in the minimum bin size unit
  604.             final double relativeFromDistance = (gtu.position(lane, gtu.getRear(), fromTime).getSI() + lengthOffset)
  605.                     / this.getYAxis().getGranularities()[0];
  606.             final double relativeToDistance =
  607.                     (gtu.position(lane, gtu.getRear(), toTime).getSI() + lengthOffset) / this.getYAxis().getGranularities()[0];
  608.             double relativeFromTime =
  609.                     (fromTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
  610.             final double relativeToTime =
  611.                     (toTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
  612.             final int fromTimeBin = (int) Math.floor(relativeFromTime);
  613.             final int toTimeBin = (int) Math.floor(relativeToTime) + 1;
  614.             double relativeMeanSpeed = (relativeToDistance - relativeFromDistance) / (relativeToTime - relativeFromTime);
  615.             // The code for acceleration assumes that acceleration is constant (which is correct for IDM+, but may be
  616.             // wrong for other car following algorithms).
  617.             double acceleration = gtu.getAcceleration().getSI();
  618.             for (int timeBin = fromTimeBin; timeBin < toTimeBin; timeBin++)
  619.             {
  620.                 if (timeBin < 0)
  621.                 {
  622.                     continue;
  623.                 }
  624.                 double binEndTime = timeBin + 1;
  625.                 if (binEndTime > relativeToTime)
  626.                 {
  627.                     binEndTime = relativeToTime;
  628.                 }
  629.                 if (binEndTime <= relativeFromTime)
  630.                 {
  631.                     continue; // no time spent in this timeBin
  632.                 }
  633.                 double binDistanceStart = (gtu
  634.                         .position(lane, gtu.getRear(),
  635.                                 new Time(relativeFromTime * this.getXAxis().getGranularities()[0], TimeUnit.BASE))
  636.                         .getSI() - this.getYAxis().getMinimumValue().getSI() + lengthOffset)
  637.                         / this.getYAxis().getGranularities()[0];
  638.                 double binDistanceEnd = (gtu
  639.                         .position(lane, gtu.getRear(),
  640.                                 new Time(binEndTime * this.getXAxis().getGranularities()[0], TimeUnit.BASE))
  641.                         .getSI() - this.getYAxis().getMinimumValue().getSI() + lengthOffset)
  642.                         / this.getYAxis().getGranularities()[0];

  643.                 // Compute the time in each distanceBin
  644.                 for (int distanceBin = (int) Math.floor(binDistanceStart); distanceBin <= binDistanceEnd; distanceBin++)
  645.                 {
  646.                     double relativeDuration = 1;
  647.                     if (relativeFromTime > timeBin)
  648.                     {
  649.                         relativeDuration -= relativeFromTime - timeBin;
  650.                     }
  651.                     if (distanceBin == (int) Math.floor(binDistanceEnd))
  652.                     {
  653.                         // This GTU does not move out of this distanceBin before the binEndTime
  654.                         if (binEndTime < timeBin + 1)
  655.                         {
  656.                             relativeDuration -= timeBin + 1 - binEndTime;
  657.                         }
  658.                     }
  659.                     else
  660.                     {
  661.                         // This GTU moves out of this distanceBin before the binEndTime
  662.                         // Interpolate the time when this GTU crosses into the next distanceBin
  663.                         // Using f.i. Newton-Rhaphson interpolation would yield a slightly more precise result...
  664.                         double timeToBinBoundary = (distanceBin + 1 - binDistanceStart) / relativeMeanSpeed;
  665.                         double endTime = relativeFromTime + timeToBinBoundary;
  666.                         relativeDuration -= timeBin + 1 - endTime;
  667.                     }
  668.                     final double duration = relativeDuration * this.getXAxis().getGranularities()[0];
  669.                     final double distance = duration * relativeMeanSpeed * this.getYAxis().getGranularities()[0];
  670.                     // System.out.println(String.format(
  671.                     // "timeBin=%d, distanceBin=%d, duration=%f, distance=%f, timeBinSize=%f, distanceBinSize=%f", timeBin,
  672.                     // distanceBin, duration, distance, this.getYAxis().getGranularities()[0], this.getXAxis()
  673.                     // .getGranularities()[0]));
  674.                     incrementBinData(timeBin, distanceBin, duration, distance, acceleration);
  675.                     relativeFromTime += relativeDuration;
  676.                     binDistanceStart = distanceBin + 1;
  677.                 }
  678.                 relativeFromTime = timeBin + 1;
  679.             }
  680.         }
  681.         catch (GTUException exception)
  682.         {
  683.             // error -- silently ignore for now. Graphs should not cause errors.
  684.             System.err.println("ContourPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
  685.                     + exception.getMessage());
  686.         }
  687.     }

  688.     /**
  689.      * Increase storage for sample data. <br>
  690.      * This is only implemented for the time axis.
  691.      * @param newUpperLimit DoubleScalar&lt;?&gt; new upper limit for the X range
  692.      */
  693.     public abstract void extendXRange(DoubleScalarInterface newUpperLimit);

  694.     /**
  695.      * Increment the data of one bin.
  696.      * @param timeBin Integer; the rank of the bin on the time-scale
  697.      * @param distanceBin Integer; the rank of the bin on the distance-scale
  698.      * @param duration Double; the time spent in this bin
  699.      * @param distanceCovered Double; the distance covered in this bin
  700.      * @param acceleration Double; the average acceleration in this bin
  701.      */
  702.     public abstract void incrementBinData(int timeBin, int distanceBin, double duration, double distanceCovered,
  703.             double acceleration);

  704.     /** {@inheritDoc} */
  705.     @Override
  706.     public final double getZValue(final int series, final int item)
  707.     {
  708.         final int timeBinGroup = xAxisBin(item);
  709.         final int distanceBinGroup = yAxisBin(item);
  710.         // System.out.println(String.format("getZValue(s=%d, i=%d) -> tbg=%d, dbg=%d", series, item, timeBinGroup,
  711.         // distanceBinGroup));
  712.         final int timeGroupSize = (int) (this.getXAxis().getCurrentGranularity() / this.getXAxis().getGranularities()[0]);
  713.         final int firstTimeBin = timeBinGroup * timeGroupSize;
  714.         final int distanceGroupSize = (int) (this.getYAxis().getCurrentGranularity() / this.getYAxis().getGranularities()[0]);
  715.         final int firstDistanceBin = distanceBinGroup * distanceGroupSize;
  716.         final int endTimeBin = Math.min(firstTimeBin + timeGroupSize, this.getXAxis().getBinCount());
  717.         final int endDistanceBin = Math.min(firstDistanceBin + distanceGroupSize, this.getYAxis().getBinCount());
  718.         return computeZValue(firstTimeBin, endTimeBin, firstDistanceBin, endDistanceBin);
  719.     }

  720.     /**
  721.      * Combine values in a range of time bins and distance bins to obtain a combined density value of the ranges.
  722.      * @param firstTimeBin Integer; the first time bin to use
  723.      * @param endTimeBin Integer; one higher than the last time bin to use
  724.      * @param firstDistanceBin Integer; the first distance bin to use
  725.      * @param endDistanceBin Integer; one higher than the last distance bin to use
  726.      * @return Double; the density value (or Double.NaN if no value can be computed)
  727.      */
  728.     public abstract double computeZValue(int firstTimeBin, int endTimeBin, int firstDistanceBin, int endDistanceBin);

  729.     /**
  730.      * Get the X axis.
  731.      * @return Axis
  732.      */
  733.     public final Axis getXAxis()
  734.     {
  735.         return this.xAxis;
  736.     }

  737.     /**
  738.      * Get the Y axis.
  739.      * @return Axis
  740.      */
  741.     public final Axis getYAxis()
  742.     {
  743.         return this.yAxis;
  744.     }

  745. }