View Javadoc
1   package org.opentrafficsim.animation.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.plot.CrosshairState;
19  import org.jfree.chart.plot.PlotOrientation;
20  import org.jfree.chart.plot.PlotRenderingInfo;
21  import org.jfree.chart.plot.XYPlot;
22  import org.jfree.chart.renderer.PaintScale;
23  import org.jfree.chart.renderer.xy.XYBlockRenderer;
24  import org.jfree.chart.renderer.xy.XYItemRendererState;
25  import org.jfree.chart.ui.RectangleAnchor;
26  import org.jfree.chart.ui.Size2D;
27  import org.jfree.data.xy.XYDataset;
28  import org.opentrafficsim.animation.ColorPaintScale;
29  
30  /**
31   * Renderer for blocks that are filled with bi-directionally interpolated colors. It extends a {@code XYBlockRenderer} and
32   * requires a small extension of the underlying dataset ({@code XyInterpolatedDataset}). The interpolation is performed in the
33   * {@code drawItem} method. This class imposes two constraints on the functionality of the super class: i) no BlockAnchor may be
34   * set as this is tightly related to the interpolation, and ii) only paint scales of type {@code ColorPaintScale} can be used,
35   * as the interpolation obtains pixel colors from it.
36   * <p>
37   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
38   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
39   * </p>
40   * @author Alexander Verbraeck
41   * @author Peter Knoppers
42   * @author Wouter Schakel
43   */
44  public class XyInterpolatedBlockRenderer extends XYBlockRenderer
45  {
46  
47      /** Serialization version UID. */
48      private static final long serialVersionUID = 20181008L;
49  
50      /** Whether to use the interpolation. */
51      private boolean interpolate = true;
52  
53      /** Dataset that allows retrieving surrounding value for interpolation. */
54      private final XyInterpolatedDataset xyInterpolatedDataset;
55  
56      /**
57       * Constructor.
58       * @param 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 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.CENTER);
98          }
99          else
100         {
101             super.setBlockAnchor(RectangleAnchor.TOP_LEFT); // reversed y axis
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                 @Override
140                 public int getTransparency()
141                 {
142                     return TRANSLUCENT;
143                 }
144 
145                 @Override
146                 public PaintContext createContext(final ColorModel cm, final Rectangle deviceBounds,
147                         final Rectangle2D userBounds, final AffineTransform xform, final RenderingHints hints)
148                 {
149                     return new PaintContext()
150                     {
151                         @Override
152                         public void dispose()
153                         {
154                             //
155                         }
156 
157                         @Override
158                         public ColorModel getColorModel()
159                         {
160                             return ColorModel.getRGBdefault();
161                         }
162 
163                         @Override
164                         public Raster getRaster(final int x, final int y, final int w, final int h)
165                         {
166                             // a raster can be obtained for any square subset of 1 cell, obtain the offset
167                             double wOffset = x - deviceBounds.getX();
168                             double hOffset = y - deviceBounds.getY();
169 
170                             // initialize a writable raster
171                             WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h);
172 
173                             // loop pixels in data buffer (raster.setPixel(i, j, float[]) doesn't work...)
174                             for (int k = 0; k < raster.getDataBuffer().getSize(); k++)
175                             {
176                                 // coordinate (i, j) is where pixel k is within the bounds
177                                 double i = hOffset + k / w;
178                                 double j = wOffset + k % w;
179 
180                                 // get weights relative to the edges
181                                 double bot = i / deviceBounds.getHeight();
182                                 double top = 1.0 - bot;
183                                 double rig = j / deviceBounds.getWidth();
184                                 double lef = 1.0 - rig;
185 
186                                 // bilinear interpolation of the value
187                                 double z = z00f * lef * bot + z10f * top * lef + z01f * bot * rig + z11f * top * rig;
188 
189                                 // with the interpolated value, obtain a color the simple way
190                                 Color c = (Color) getPaintScale().getPaint(z); // paint scale forced of type ColorPaintScale
191 
192                                 // write
193                                 raster.getDataBuffer().setElem(k, c.getRGB());
194                             }
195                             return raster;
196                         }
197                     };
198                 }
199             };
200 
201         }
202 
203         // use rect to obtain x and y range, accounting for offset (direct information is private in super class)
204         double x = dataset.getXValue(series, item);
205         double y = dataset.getYValue(series, item);
206         Rectangle2D rect =
207                 RectangleAnchor.createRectangle(new Size2D(getBlockWidth(), getBlockHeight()), x, y, getBlockAnchor());
208         double xx0 = domainAxis.valueToJava2D(rect.getMinX(), dataArea, plot.getDomainAxisEdge());
209         double yy0 = rangeAxis.valueToJava2D(rect.getMinY(), dataArea, plot.getRangeAxisEdge());
210         double xx1 = domainAxis.valueToJava2D(rect.getMaxX(), dataArea, plot.getDomainAxisEdge());
211         double yy1 = rangeAxis.valueToJava2D(rect.getMaxY(), dataArea, plot.getRangeAxisEdge());
212 
213         // code below this is equal to the super implementation
214         Rectangle2D block;
215         PlotOrientation orientation = plot.getOrientation();
216         if (orientation.equals(PlotOrientation.HORIZONTAL))
217         {
218             block = new Rectangle2D.Double(Math.min(yy0, yy1), Math.min(xx0, xx1), Math.abs(yy1 - yy0), Math.abs(xx0 - xx1));
219         }
220         else
221         {
222             block = new Rectangle2D.Double(Math.min(xx0, xx1), Math.min(yy0, yy1), Math.abs(xx1 - xx0), Math.abs(yy1 - yy0));
223         }
224         g2.setPaint(p);
225         g2.fill(block);
226         g2.setStroke(new BasicStroke(1.0f));
227         g2.draw(block);
228 
229         /*- skip this code from super, we don't need it for what we use this class for
230         if (isItemLabelVisible(series, item))
231         {
232             drawItemLabel(g2, orientation, dataset, series, item, block.getCenterX(), block.getCenterY(), y < 0.0);
233         }
234 
235         int datasetIndex = plot.indexOf(dataset);
236         double transX = domainAxis.valueToJava2D(x, dataArea, plot.getDomainAxisEdge());
237         double transY = rangeAxis.valueToJava2D(y, dataArea, plot.getRangeAxisEdge());
238         updateCrosshairValues(crosshairState, x, y, datasetIndex, transX, transY, orientation);
239 
240         EntityCollection entities = state.getEntityCollection();
241         if (entities != null)
242         {
243             addEntity(entities, block, dataset, series, item, block.getCenterX(), block.getCenterY());
244         }*/
245     }
246 
247     /**
248      * Returns the value of an adjacent cell.
249      * @param series the series index
250      * @param item item
251      * @param up whether to get the upper cell (can be combined with right)
252      * @param right whether to get the right cell (can be combined with up)
253      * @return value in adjacent cell, or {@code Double.NaN} if no such cell.
254      */
255     private double getAdjacentZ(final int series, final int item, final boolean up, final boolean right)
256     {
257         if (up && (item + 1) % this.xyInterpolatedDataset.getRangeBinCount() == 0)
258         {
259             // we cannot interpolate beyond the range extent
260             return Double.NaN;
261         }
262         int adjacentItem = item + (up ? 1 : 0) + (right ? this.xyInterpolatedDataset.getRangeBinCount() : 0);
263         if (adjacentItem >= this.xyInterpolatedDataset.getItemCount(series))
264         {
265             // we cannot interpolate beyond the domain extent
266             return Double.NaN;
267         }
268         return this.xyInterpolatedDataset.getZValue(series, adjacentItem);
269     }
270 
271     /**
272      * Restores a corner value if it's NaN using surrounding values. If both adjacent corner points are not NaN, the mean of
273      * those is used. If either is not NaN, that value is used. Otherwise the opposite corner point is used (which may be NaN).
274      * 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
275      * in case of interpolation. Coincidentally it can also fill small data gaps visually.
276      * @param value value to fix (if needed)
277      * @param adjacentCorner1 adjacent corner value
278      * @param adjacentCorner2 other adjacent corner value
279      * @param oppositeCorner opposite corner value
280      * @return fixed value (if possible, i.e. not all corners are NaN)
281      */
282     private double fixNaN(final double value, final double adjacentCorner1, final double adjacentCorner2,
283             final double oppositeCorner)
284     {
285         if (!Double.isNaN(value))
286         {
287             return value;
288         }
289         if (Double.isNaN(adjacentCorner1))
290         {
291             if (Double.isNaN(adjacentCorner2))
292             {
293                 return oppositeCorner;
294             }
295             else
296             {
297                 return adjacentCorner2;
298             }
299         }
300         else if (Double.isNaN(adjacentCorner2))
301         {
302             return adjacentCorner1;
303         }
304         return 0.5 * (adjacentCorner1 + adjacentCorner2);
305     }
306 
307     @Override
308     public String toString()
309     {
310         return "XYInterpolatedBlockRenderer [interpolate=" + this.interpolate + ", xyInterpolatedDataset="
311                 + this.xyInterpolatedDataset + "]";
312     }
313 
314 }