View Javadoc
1   package org.opentrafficsim.draw.graphs;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Graphics2D;
6   import java.awt.Paint;
7   import java.awt.PaintContext;
8   import java.awt.Rectangle;
9   import java.awt.RenderingHints;
10  import java.awt.geom.AffineTransform;
11  import java.awt.geom.Rectangle2D;
12  import java.awt.image.ColorModel;
13  import java.awt.image.Raster;
14  import java.awt.image.WritableRaster;
15  
16  import org.djutils.exceptions.Throw;
17  import org.jfree.chart.axis.ValueAxis;
18  import org.jfree.chart.entity.EntityCollection;
19  import org.jfree.chart.plot.CrosshairState;
20  import org.jfree.chart.plot.PlotOrientation;
21  import org.jfree.chart.plot.PlotRenderingInfo;
22  import org.jfree.chart.plot.XYPlot;
23  import org.jfree.chart.renderer.PaintScale;
24  import org.jfree.chart.renderer.xy.XYBlockRenderer;
25  import org.jfree.chart.renderer.xy.XYItemRendererState;
26  import org.jfree.chart.ui.RectangleAnchor;
27  import org.jfree.chart.ui.Size2D;
28  import org.jfree.data.xy.XYDataset;
29  import org.opentrafficsim.draw.core.ColorPaintScale;
30  
31  /**
32   * Renderer for blocks that are filled with bidirectionally interpolated colors. It extends a {@code XYBlockRenderer} and
33   * requires a small extension of the underlying dataset ({@code XYInterpolatedDataset}). The interpolation is performed in the
34   * {@code drawItem} method. This class imposes two constraints on the functionality of the super class: i) no BlockAnchor may be
35   * set as this is tightly related to the interpolation, and ii) only paint scales of type {@code ColorPaintScale} can be used,
36   * as the interpolation obtains pixel colors from it.
37   * <p>
38   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
39   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
40   * <p>
41   * @version $Revision$, $LastChangedDate$, by $Author$, initial version 8 okt. 2018 <br>
42   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
43   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
44   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
45   */
46  public class XYInterpolatedBlockRenderer extends XYBlockRenderer
47  {
48  
49      /** */
50      private static final long serialVersionUID = 20181008L;
51  
52      /** Whether to use the interpolation. */
53      private boolean interpolate = true;
54  
55      /** Dataset that allows retrieving surrounding value for interpolation. */
56      private final XYInterpolatedDataset xyInterpolatedDataset;
57  
58      /**
59       * @param xyInterpolatedDataset XYInterpolatedDataset; dataset that allows retrieving surrounding value for interpolation
60       */
61      public XYInterpolatedBlockRenderer(final XYInterpolatedDataset xyInterpolatedDataset)
62      {
63          super(); // offsets and initial paint scale
64          this.xyInterpolatedDataset = xyInterpolatedDataset;
65      }
66  
67      /**
68       * {@inheritDoc} throws UnsupportedOperationException if the paint scale is not of type ColorPaintScale
69       */
70      @Override
71      public void setPaintScale(final PaintScale scale)
72      {
73          Throw.when(!(scale instanceof ColorPaintScale), UnsupportedOperationException.class,
74                  "Class XYInterpolatedBlockRenderer requires a ColorPaintScale.");
75          super.setPaintScale(scale);
76      }
77  
78      /**
79       * {@inheritDoc} throws UnsupportedOperationException block anchor is governed based on interpolation
80       */
81      @Override
82      public void setBlockAnchor(final RectangleAnchor anchor)
83      {
84          throw new UnsupportedOperationException(
85                  "Class XYInterpolatedBlockRenderer does not support setting the anchor, it's coupled to interpolation.");
86      }
87  
88      /**
89       * Enables interpolation or not. Interpolation occurs between cell centers. Therefore the painted blocks are shifted right
90       * 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
91       * NaN.
92       * @param interpolate boolean; interpolate or not
93       */
94      public final void setInterpolate(final boolean interpolate)
95      {
96          this.interpolate = interpolate;
97          if (interpolate)
98          {
99              super.setBlockAnchor(RectangleAnchor.TOP_LEFT); // reversed y axis
100         }
101         else
102         {
103             super.setBlockAnchor(RectangleAnchor.CENTER);
104         }
105     }
106 
107     /**
108      * {@inheritDoc} This code is partially based on the parent implementation.
109      */
110     @Override
111     @SuppressWarnings("parameternumber")
112     public void drawItem(final Graphics2D g2, final XYItemRendererState state, final Rectangle2D dataArea,
113             final PlotRenderingInfo info, final XYPlot plot, final ValueAxis domainAxis, final ValueAxis rangeAxis,
114             final XYDataset dataset, final int series, final int item, final CrosshairState crosshairState, final int pass)
115     {
116 
117         double z00 = this.xyInterpolatedDataset.getZValue(series, item);
118         Paint p;
119 
120         if (!this.interpolate)
121         {
122             // regular non interpolated case
123             p = getPaintScale().getPaint(z00);
124         }
125         else
126         {
127             // obtain data values in surrounding cells (up, right, and up-right)
128             double z10 = getAdjacentZ(series, item, true, false);
129             double z01 = getAdjacentZ(series, item, false, true);
130             double z11 = getAdjacentZ(series, item, true, true);
131 
132             // fix NaN values
133             double z00f = fixNaN(z00, z01, z10, z11);
134             double z10f = fixNaN(z10, z00, z11, z01);
135             double z01f = fixNaN(z01, z00, z11, z10);
136             double z11f = fixNaN(z11, z10, z01, z00);
137 
138             // use these values to derive an interpolated color raster
139             p = new Paint()
140             {
141                 /** {@inheritDoc} */
142                 @Override
143                 public int getTransparency()
144                 {
145                     return TRANSLUCENT;
146                 }
147 
148                 /** {@inheritDoc} */
149                 @Override
150                 public PaintContext createContext(final ColorModel cm, final Rectangle deviceBounds,
151                         final Rectangle2D userBounds, final AffineTransform xform, final RenderingHints hints)
152                 {
153                     return new PaintContext()
154                     {
155                         /** {@inheritDoc} */
156                         @Override
157                         public void dispose()
158                         {
159                             //
160                         }
161 
162                         /** {@inheritDoc} */
163                         @Override
164                         public ColorModel getColorModel()
165                         {
166                             return ColorModel.getRGBdefault();
167                         }
168 
169                         /** {@inheritDoc} */
170                         @Override
171                         public Raster getRaster(final int x, final int y, final int w, final int h)
172                         {
173                             // a raster can be obtained for any square subset of 1 cell, obtain the offset
174                             double wOffset = x - deviceBounds.getX();
175                             double hOffset = y - deviceBounds.getY();
176 
177                             // initialize a writable raster
178                             WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h);
179 
180                             // loop pixels in data buffer (raster.setPixel(i, j, float[]) doesn't work...)
181                             for (int k = 0; k < raster.getDataBuffer().getSize(); k++)
182                             {
183                                 // coordinate (i, j) is where pixel k is within the bounds
184                                 double i = hOffset + k / w;
185                                 double j = wOffset + k % w;
186 
187                                 // get weights relative to the edges
188                                 double bot = i / deviceBounds.getHeight();
189                                 double top = 1.0 - bot;
190                                 double rig = j / deviceBounds.getWidth();
191                                 double lef = 1.0 - rig;
192 
193                                 // bilinear interpolation of the value
194                                 double z = z00f * lef * bot + z10f * top * lef + z01f * bot * rig + z11f * top * rig;
195 
196                                 // with the interpolated value, obtain a color the simple way
197                                 Color c = (Color) getPaintScale().getPaint(z); // paint scale forced of type ColorPaintScale
198 
199                                 // write
200                                 raster.getDataBuffer().setElem(k, c.getRGB());
201                             }
202                             return raster;
203                         }
204                     };
205                 }
206             };
207 
208         }
209 
210         // use rect to obtain x and y range, accounting for offset (direct information is private in super class)
211         double x = dataset.getXValue(series, item);
212         double y = dataset.getYValue(series, item);
213         Rectangle2D rect =
214                 RectangleAnchor.createRectangle(new Size2D(getBlockWidth(), getBlockHeight()), x, y, getBlockAnchor());
215         double xx0 = domainAxis.valueToJava2D(rect.getMinX(), dataArea, plot.getDomainAxisEdge());
216         double yy0 = rangeAxis.valueToJava2D(rect.getMinY(), dataArea, plot.getRangeAxisEdge());
217         double xx1 = domainAxis.valueToJava2D(rect.getMaxX(), dataArea, plot.getDomainAxisEdge());
218         double yy1 = rangeAxis.valueToJava2D(rect.getMaxY(), dataArea, plot.getRangeAxisEdge());
219 
220         // code below this is equal to the super implementation
221         Rectangle2D block;
222         PlotOrientation orientation = plot.getOrientation();
223         if (orientation.equals(PlotOrientation.HORIZONTAL))
224         {
225             block = new Rectangle2D.Double(Math.min(yy0, yy1), Math.min(xx0, xx1), Math.abs(yy1 - yy0), Math.abs(xx0 - xx1));
226         }
227         else
228         {
229             block = new Rectangle2D.Double(Math.min(xx0, xx1), Math.min(yy0, yy1), Math.abs(xx1 - xx0), Math.abs(yy1 - yy0));
230         }
231         g2.setPaint(p);
232         g2.fill(block);
233         g2.setStroke(new BasicStroke(1.0f));
234         g2.draw(block);
235 
236         if (isItemLabelVisible(series, item))
237         {
238             drawItemLabel(g2, orientation, dataset, series, item, block.getCenterX(), block.getCenterY(), y < 0.0);
239         }
240 
241         int datasetIndex = plot.indexOf(dataset);
242         double transX = domainAxis.valueToJava2D(x, dataArea, plot.getDomainAxisEdge());
243         double transY = rangeAxis.valueToJava2D(y, dataArea, plot.getRangeAxisEdge());
244         updateCrosshairValues(crosshairState, x, y, datasetIndex, transX, transY, orientation);
245 
246         EntityCollection entities = state.getEntityCollection();
247         if (entities != null)
248         {
249             addEntity(entities, block, dataset, series, item, block.getCenterX(), block.getCenterY());
250         }
251     }
252 
253     /**
254      * Returns the value of an adjacent cell.
255      * @param series int; the series index
256      * @param item int; item
257      * @param up boolean; whether to get the upper cell (can be combined with right)
258      * @param right boolean; whether to get the right cell (can be combined with up)
259      * @return double; value in adjacent cell, or {@code Double.NaN} if no such cell.
260      */
261     private double getAdjacentZ(final int series, final int item, final boolean up, final boolean right)
262     {
263         if (up && (item + 1) % this.xyInterpolatedDataset.getRangeBinCount() == 0)
264         {
265             // we cannot interpolate beyond the range extent
266             return Double.NaN;
267         }
268         int adjacentItem = item + (up ? 1 : 0) + (right ? this.xyInterpolatedDataset.getRangeBinCount() : 0);
269         if (adjacentItem >= this.xyInterpolatedDataset.getItemCount(series))
270         {
271             // we cannot interpolate beyond the domain extent
272             return Double.NaN;
273         }
274         return this.xyInterpolatedDataset.getZValue(series, adjacentItem);
275     }
276 
277     /**
278      * Restores a corner value if it's NaN using surrounding values. If both adjacent corner points are not NaN, the mean of
279      * those is used. If either is not NaN, that value is used. Otherwise the opposite corner point is used (which may be NaN).
280      * 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
281      * in case of interpolation. Coincidentally it can also fill small data gaps visually.
282      * @param value double; value to fix (if needed)
283      * @param adjacentCorner1 double; adjacent corner value
284      * @param adjacentCorner2 double; other adjacent corner value
285      * @param oppositeCorner double; opposite corner value
286      * @return double; fixed value (if possible, i.e. not all corners are NaN)
287      */
288     private double fixNaN(final double value, final double adjacentCorner1, final double adjacentCorner2,
289             final double oppositeCorner)
290     {
291         if (!Double.isNaN(value))
292         {
293             return value;
294         }
295         if (Double.isNaN(adjacentCorner1))
296         {
297             if (Double.isNaN(adjacentCorner2))
298             {
299                 return oppositeCorner;
300             }
301             else
302             {
303                 return adjacentCorner2;
304             }
305         }
306         else if (Double.isNaN(adjacentCorner2))
307         {
308             return adjacentCorner1;
309         }
310         return 0.5 * (adjacentCorner1 + adjacentCorner2);
311     }
312 
313     /** {@inheritDoc} */
314     @Override
315     public String toString()
316     {
317         return "XYInterpolatedBlockRenderer [interpolate=" + this.interpolate + ", xyInterpolatedDataset="
318                 + this.xyInterpolatedDataset + "]";
319     }
320 
321 }