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