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