XYInterpolatedBlockRenderer.java

  1. package org.opentrafficsim.draw.graphs;

  2. import java.awt.BasicStroke;
  3. import java.awt.Color;
  4. import java.awt.Graphics2D;
  5. import java.awt.Paint;
  6. import java.awt.PaintContext;
  7. import java.awt.Rectangle;
  8. import java.awt.RenderingHints;
  9. import java.awt.geom.AffineTransform;
  10. import java.awt.geom.Rectangle2D;
  11. import java.awt.image.ColorModel;
  12. import java.awt.image.Raster;
  13. import java.awt.image.WritableRaster;

  14. import org.djutils.exceptions.Throw;
  15. import org.jfree.chart.axis.ValueAxis;
  16. import org.jfree.chart.entity.EntityCollection;
  17. import org.jfree.chart.plot.CrosshairState;
  18. import org.jfree.chart.plot.PlotOrientation;
  19. import org.jfree.chart.plot.PlotRenderingInfo;
  20. import org.jfree.chart.plot.XYPlot;
  21. import org.jfree.chart.renderer.PaintScale;
  22. import org.jfree.chart.renderer.xy.XYBlockRenderer;
  23. import org.jfree.chart.renderer.xy.XYItemRendererState;
  24. import org.jfree.chart.ui.RectangleAnchor;
  25. import org.jfree.chart.ui.Size2D;
  26. import org.jfree.data.xy.XYDataset;
  27. import org.opentrafficsim.draw.core.ColorPaintScale;

  28. /**
  29.  * Renderer for blocks that are filled with bidirectionally interpolated colors. It extends a {@code XYBlockRenderer} and
  30.  * requires a small extension of the underlying dataset ({@code XYInterpolatedDataset}). The interpolation is performed in the
  31.  * {@code drawItem} method. This class imposes two constraints on the functionality of the super class: i) no BlockAnchor may be
  32.  * set as this is tightly related to the interpolation, and ii) only paint scales of type {@code ColorPaintScale} can be used,
  33.  * as the interpolation obtains pixel colors from it.
  34.  * <p>
  35.  * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  36.  * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
  37.  * <p>
  38.  * @version $Revision$, $LastChangedDate$, by $Author$, initial version 8 okt. 2018 <br>
  39.  * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  40.  * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  41.  * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
  42.  */
  43. public class XYInterpolatedBlockRenderer extends XYBlockRenderer
  44. {

  45.     /** */
  46.     private static final long serialVersionUID = 20181008L;

  47.     /** Whether to use the interpolation. */
  48.     private boolean interpolate = true;

  49.     /** Dataset that allows retrieving surrounding value for interpolation. */
  50.     private final XYInterpolatedDataset xyInterpolatedDataset;

  51.     /**
  52.      * @param xyInterpolatedDataset XYInterpolatedDataset; dataset that allows retrieving surrounding value for interpolation
  53.      */
  54.     public XYInterpolatedBlockRenderer(final XYInterpolatedDataset xyInterpolatedDataset)
  55.     {
  56.         this.xyInterpolatedDataset = xyInterpolatedDataset;
  57.     }

  58.     /**
  59.      * {@inheritDoc} throws UnsupportedOperationException if the paint scale is not of type ColorPaintScale
  60.      */
  61.     @Override
  62.     public void setPaintScale(final PaintScale scale)
  63.     {
  64.         Throw.when(!(scale instanceof ColorPaintScale), UnsupportedOperationException.class,
  65.                 "Class XYInterpolatedBlockRenderer requires a ColorPaintScale.");
  66.         super.setPaintScale(scale);
  67.     }

  68.     /**
  69.      * {@inheritDoc} throws UnsupportedOperationException block anchor is governed based on interpolation
  70.      */
  71.     @Override
  72.     public void setBlockAnchor(final RectangleAnchor anchor)
  73.     {
  74.         throw new UnsupportedOperationException(
  75.                 "Class XYInterpolatedBlockRenderer does not support setting the anchor, it's coupled to interpolation.");
  76.     }

  77.     /**
  78.      * Enables interpolation or not. Interpolation occurs between cell centers. Therefore the painted blocks are shifted right
  79.      * and up. The user of this class must provide an additional row and column of data to fill up the gaps. These values may be
  80.      * NaN.
  81.      * @param interpolate boolean; interpolate or not
  82.      */
  83.     public final void setInterpolate(final boolean interpolate)
  84.     {
  85.         this.interpolate = interpolate;
  86.         if (interpolate)
  87.         {
  88.             super.setBlockAnchor(RectangleAnchor.TOP_LEFT); // reversed y axis
  89.         }
  90.         else
  91.         {
  92.             super.setBlockAnchor(RectangleAnchor.CENTER);
  93.         }
  94.     }

  95.     /**
  96.      * {@inheritDoc} This code is partially based on the parent implementation.
  97.      */
  98.     @Override
  99.     @SuppressWarnings("parameternumber")
  100.     public void drawItem(final Graphics2D g2, final XYItemRendererState state, final Rectangle2D dataArea,
  101.             final PlotRenderingInfo info, final XYPlot plot, final ValueAxis domainAxis, final ValueAxis rangeAxis,
  102.             final XYDataset dataset, final int series, final int item, final CrosshairState crosshairState, final int pass)
  103.     {

  104.         double z00 = this.xyInterpolatedDataset.getZValue(series, item);
  105.         Paint p;

  106.         if (!this.interpolate)
  107.         {
  108.             // regular non interpolated case
  109.             p = getPaintScale().getPaint(z00);
  110.         }
  111.         else
  112.         {
  113.             // obtain data values in surrounding cells (up, right, and up-right)
  114.             double z10 = getAdjacentZ(series, item, true, false);
  115.             double z01 = getAdjacentZ(series, item, false, true);
  116.             double z11 = getAdjacentZ(series, item, true, true);

  117.             // fix NaN values
  118.             double z00f = fixNaN(z00, z01, z10, z11);
  119.             double z10f = fixNaN(z10, z00, z11, z01);
  120.             double z01f = fixNaN(z01, z00, z11, z10);
  121.             double z11f = fixNaN(z11, z10, z01, z00);

  122.             // use these values to derive an interpolated color raster
  123.             p = new Paint()
  124.             {
  125.                 /** {@inheritDoc} */
  126.                 @Override
  127.                 public int getTransparency()
  128.                 {
  129.                     return TRANSLUCENT;
  130.                 }

  131.                 /** {@inheritDoc} */
  132.                 @Override
  133.                 public PaintContext createContext(final ColorModel cm, final Rectangle deviceBounds,
  134.                         final Rectangle2D userBounds, final AffineTransform xform, final RenderingHints hints)
  135.                 {
  136.                     return new PaintContext()
  137.                     {
  138.                         /** {@inheritDoc} */
  139.                         @Override
  140.                         public void dispose()
  141.                         {
  142.                             //
  143.                         }

  144.                         /** {@inheritDoc} */
  145.                         @Override
  146.                         public ColorModel getColorModel()
  147.                         {
  148.                             return ColorModel.getRGBdefault();
  149.                         }

  150.                         /** {@inheritDoc} */
  151.                         @Override
  152.                         public Raster getRaster(final int x, final int y, final int w, final int h)
  153.                         {
  154.                             // a raster can be obtained for any square subset of 1 cell, obtain the offset
  155.                             double wOffset = x - deviceBounds.getX();
  156.                             double hOffset = y - deviceBounds.getY();

  157.                             // initialize a writable raster
  158.                             WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h);

  159.                             // loop pixels in data buffer (raster.setPixel(i, j, float[]) doesn't work...)
  160.                             for (int k = 0; k < raster.getDataBuffer().getSize(); k++)
  161.                             {
  162.                                 // coordinate (i, j) is where pixel k is within the bounds
  163.                                 double i = hOffset + k / w;
  164.                                 double j = wOffset + k % w;

  165.                                 // get weights relative to the edges
  166.                                 double bot = i / deviceBounds.getHeight();
  167.                                 double top = 1.0 - bot;
  168.                                 double rig = j / deviceBounds.getWidth();
  169.                                 double lef = 1.0 - rig;

  170.                                 // bilinear interpolation of the value
  171.                                 double z = z00f * lef * bot + z10f * top * lef + z01f * bot * rig + z11f * top * rig;

  172.                                 // with the interpolated value, obtain a color the simple way
  173.                                 Color c = (Color) getPaintScale().getPaint(z); // paint scale forced of type ColorPaintScale

  174.                                 // write
  175.                                 raster.getDataBuffer().setElem(k, c.getRGB());
  176.                             }
  177.                             return raster;
  178.                         }
  179.                     };
  180.                 }
  181.             };

  182.         }

  183.         // use rect to obtain x and y range, accounting for offset (direct information is private in super class)
  184.         double x = dataset.getXValue(series, item);
  185.         double y = dataset.getYValue(series, item);
  186.         Rectangle2D rect =
  187.                 RectangleAnchor.createRectangle(new Size2D(getBlockWidth(), getBlockHeight()), x, y, getBlockAnchor());
  188.         double xx0 = domainAxis.valueToJava2D(rect.getMinX(), dataArea, plot.getDomainAxisEdge());
  189.         double yy0 = rangeAxis.valueToJava2D(rect.getMinY(), dataArea, plot.getRangeAxisEdge());
  190.         double xx1 = domainAxis.valueToJava2D(rect.getMaxX(), dataArea, plot.getDomainAxisEdge());
  191.         double yy1 = rangeAxis.valueToJava2D(rect.getMaxY(), dataArea, plot.getRangeAxisEdge());

  192.         // code below this is equal to the super implementation
  193.         Rectangle2D block;
  194.         PlotOrientation orientation = plot.getOrientation();
  195.         if (orientation.equals(PlotOrientation.HORIZONTAL))
  196.         {
  197.             block = new Rectangle2D.Double(Math.min(yy0, yy1), Math.min(xx0, xx1), Math.abs(yy1 - yy0), Math.abs(xx0 - xx1));
  198.         }
  199.         else
  200.         {
  201.             block = new Rectangle2D.Double(Math.min(xx0, xx1), Math.min(yy0, yy1), Math.abs(xx1 - xx0), Math.abs(yy1 - yy0));
  202.         }
  203.         g2.setPaint(p);
  204.         g2.fill(block);
  205.         g2.setStroke(new BasicStroke(1.0f));
  206.         g2.draw(block);

  207.         if (isItemLabelVisible(series, item))
  208.         {
  209.             drawItemLabel(g2, orientation, dataset, series, item, block.getCenterX(), block.getCenterY(), y < 0.0);
  210.         }

  211.         int datasetIndex = plot.indexOf(dataset);
  212.         double transX = domainAxis.valueToJava2D(x, dataArea, plot.getDomainAxisEdge());
  213.         double transY = rangeAxis.valueToJava2D(y, dataArea, plot.getRangeAxisEdge());
  214.         updateCrosshairValues(crosshairState, x, y, datasetIndex, transX, transY, orientation);

  215.         EntityCollection entities = state.getEntityCollection();
  216.         if (entities != null)
  217.         {
  218.             addEntity(entities, block, dataset, series, item, block.getCenterX(), block.getCenterY());
  219.         }
  220.     }

  221.     /**
  222.      * Returns the value of an adjacent cell.
  223.      * @param series int; the series index
  224.      * @param item int; item
  225.      * @param up boolean; whether to get the upper cell (can be combined with right)
  226.      * @param right boolean; whether to get the right cell (can be combined with up)
  227.      * @return double; value in adjacent cell, or {@code Double.NaN} if no such cell.
  228.      */
  229.     private double getAdjacentZ(final int series, final int item, final boolean up, final boolean right)
  230.     {
  231.         if (up && (item + 1) % this.xyInterpolatedDataset.getRangeBinCount() == 0)
  232.         {
  233.             // we cannot interpolate beyond the range extent
  234.             return Double.NaN;
  235.         }
  236.         int adjacentItem = item + (up ? 1 : 0) + (right ? this.xyInterpolatedDataset.getRangeBinCount() : 0);
  237.         if (adjacentItem >= this.xyInterpolatedDataset.getItemCount(series))
  238.         {
  239.             // we cannot interpolate beyond the domain extent
  240.             return Double.NaN;
  241.         }
  242.         return this.xyInterpolatedDataset.getZValue(series, adjacentItem);
  243.     }

  244.     /**
  245.      * Restores a corner value if it's NaN using surrounding values. If both adjacent corner points are not NaN, the mean of
  246.      * those is used. If either is not NaN, that value is used. Otherwise the opposite corner point is used (which may be NaN).
  247.      * This method's main purpose is to fill the left side of the first column of cells and the bottom of the first row of cells
  248.      * in case of interpolation. Coincidentally it can also fill small data gaps visually.
  249.      * @param value double; value to fix (if needed)
  250.      * @param adjacentCorner1 double; adjacent corner value
  251.      * @param adjacentCorner2 double; other adjacent corner value
  252.      * @param oppositeCorner double; opposite corner value
  253.      * @return double; fixed value (if possible, i.e. not all corners are NaN)
  254.      */
  255.     private double fixNaN(final double value, final double adjacentCorner1, final double adjacentCorner2,
  256.             final double oppositeCorner)
  257.     {
  258.         if (!Double.isNaN(value))
  259.         {
  260.             return value;
  261.         }
  262.         if (Double.isNaN(adjacentCorner1))
  263.         {
  264.             if (Double.isNaN(adjacentCorner2))
  265.             {
  266.                 return oppositeCorner;
  267.             }
  268.             else
  269.             {
  270.                 return adjacentCorner2;
  271.             }
  272.         }
  273.         else if (Double.isNaN(adjacentCorner2))
  274.         {
  275.             return adjacentCorner1;
  276.         }
  277.         return 0.5 * (adjacentCorner1 + adjacentCorner2);
  278.     }

  279.     /** {@inheritDoc} */
  280.     @Override
  281.     public String toString()
  282.     {
  283.         return "XYInterpolatedBlockRenderer [interpolate=" + this.interpolate + ", xyInterpolatedDataset="
  284.                 + this.xyInterpolatedDataset + "]";
  285.     }

  286. }