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 }