001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2012-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.test.coverage.image;
019
020import java.awt.Rectangle;
021import java.awt.image.DataBuffer;
022import java.awt.image.Raster;
023import java.awt.image.RenderedImage;
024import java.awt.image.SampleModel;
025
026import static java.lang.Double.doubleToLongBits;
027import static java.lang.Float.floatToIntBits;
028import static java.lang.StrictMath.*;
029import static org.junit.jupiter.api.Assertions.*;
030
031
032/**
033 * A row-major iterator over sample values in a {@link Raster} or {@link RenderedImage}.
034 * For any image (tiled or not), this class iterates first over the <em>bands</em>, then
035 * over the <em>columns</em> and finally over the <em>rows</em>. If the image is tiled,
036 * then this iterator will perform the necessary calls to the {@link RenderedImage#getTile(int, int)}
037 * method for each row in order to perform the iteration as if the image was untiled.
038 *
039 * <p>On creation, this iterator is positioned <em>before</em> the first sample value.
040 * To use this iterator, invoke the {@link #next()} method in a {@code while} loop
041 * as below:</p>
042 *
043 * {@snippet lang="java" :
044 * PixelIterator it = new PixelIterator(image);
045 * while (it.next()) {
046 *     float value = it.getSampleFloat();
047 *     // Do some processing with the value here...
048 * }}
049 *
050 * @see org.opengis.test.Assertions#assertSampleValuesEqual(RenderedImage, RenderedImage, double, String)
051 *
052 * @author  Rémi Marechal (Geomatys)
053 * @author  Martin Desruisseaux (Geomatys)
054 * @version 3.1
055 * @since   3.1
056 */
057@SuppressWarnings("strictfp")   // Because we still target Java 11.
058public strictfp class PixelIterator {
059    /**
060     * The image in which to iterate.
061     */
062    private final RenderedImage image;
063
064    /**
065     * Current raster in which to iterate.
066     */
067    private Raster raster;
068
069    /**
070     * The bands to iterate over, or {@code null} if none.
071     */
072    private final int[] sourceBands;
073
074    /**
075     * The subsampling to apply during the iteration.
076     */
077    private final int xSubsampling, ySubsampling;
078
079    /**
080     * Number of bands to iterate over.
081     */
082    private final int numBands;
083
084    /**
085     * The iteration bounds in the image, in pixel coordinates.
086     * This rectangle may span an arbitrary number of tiles.
087     */
088    private final int minX, maxX, maxY;
089
090    /**
091     * The iteration bounds in the image, in tile coordinates.
092     */
093    private final int minTileX, maxTileX, maxTileY;
094
095    /**
096     * The intersection of the iteration bounds together with the current
097     * {@linkplain #raster} bounds.
098     */
099    private int currentMaxX, currentMaxY;
100
101    /**
102     * Current band and pixel position in the current {@linkplain #raster}.
103     */
104    private int band, x, y;
105
106    /**
107     * Current raster position in the {@linkplain #image}.
108     */
109    private int tileX, tileY;
110
111    /**
112     * Creates an iterator for the whole area of the given raster.
113     *
114     * @param raster The raster for which to create an iterator.
115     */
116    public PixelIterator(final Raster raster) {
117        this(new RasterImage(raster));
118    }
119
120    /**
121     * Creates an iterator for the whole area of the given image.
122     *
123     * @param image The image for which to create an iterator.
124     */
125    public PixelIterator(final RenderedImage image) {
126        this(image, null, 1, 1, null);
127    }
128
129    /**
130     * Creates an iterator for a sub-area of the given raster.
131     *
132     * @param raster        the raster to iterate over.
133     * @param subArea       rectangle which represent raster sub area iteration, or {@code null} if none.
134     * @param xSubsampling  the iteration step when moving to the next pixel.
135     * @param ySubsampling  the iteration step when moving to the next scan line.
136     * @param sourceBands   the source bands, or {@code null} if none.
137     */
138    public PixelIterator(final Raster raster, final Rectangle subArea,
139            final int xSubsampling, final int ySubsampling, final int[] sourceBands)
140    {
141        this(new RasterImage(raster), subArea, xSubsampling, ySubsampling, sourceBands);
142    }
143
144    /**
145     * Creates an iterator for a sub-area of the given image.
146     *
147     * @param image         the image to iterate over.
148     * @param subArea       rectangle which represent image sub area iteration, or {@code null} if none.
149     * @param xSubsampling  the iteration step when moving to the next pixel.
150     * @param ySubsampling  the iteration step when moving to the next scan line.
151     * @param sourceBands   the source bands, or {@code null} if none.
152     */
153    public PixelIterator(final RenderedImage image, final Rectangle subArea,
154            final int xSubsampling, final int ySubsampling, final int[] sourceBands)
155    {
156        this.image        = image;
157        this.numBands     = (sourceBands != null) ? sourceBands.length : image.getSampleModel().getNumBands();
158        this.sourceBands  = sourceBands;
159        this.xSubsampling = xSubsampling;
160        this.ySubsampling = ySubsampling;
161
162        int minX = image.getMinX();
163        int minY = image.getMinY();
164        int maxX = image.getWidth()  + minX;
165        int maxY = image.getHeight() + minY;
166        if (subArea != null) {
167            minX = max(minX, subArea.x);
168            minY = max(minY, subArea.y);
169            maxX = min(maxX, subArea.x + subArea.width);
170            maxY = min(maxY, subArea.y + subArea.height);
171        }
172        this.minX = minX;
173        this.maxX = maxX;
174        this.maxY = maxY;
175
176        final int gridXOffset = image.getTileGridXOffset();
177        final int gridYOffset = image.getTileGridYOffset();
178        final int tileWidth   = image.getTileWidth();
179        final int tileHeight  = image.getTileHeight();
180
181        final int minTileY;
182        minTileX = divide(minX - gridXOffset, tileWidth,  false);
183        minTileY = divide(minY - gridYOffset, tileHeight, false);
184        maxTileX = divide(maxX - gridXOffset, tileWidth,  true);
185        maxTileY = divide(maxY - gridYOffset, tileHeight, true);
186
187        // Initialize attributes to first iteration.
188        x     = minX;
189        y     = minY;
190        band  = -1;
191        tileX = minTileX;
192        tileY = minTileY;
193        updateRaster();
194    }
195
196    /**
197     * Rounds the given numbers, rounding toward floor or ceil depending on the value
198     * of the {@code ceil} argument. This method works for negative numerator too.
199     *
200     * @param  numerator    the value to divide.
201     * @param  denominator  the divisor.
202     * @param  ceil         whether to round toward up.
203     * @return division result rounded toward the specified direction.
204     */
205    private static int divide(final int numerator, final int denominator, final boolean ceil) {
206        assertTrue(denominator > 0, "Require a non-negative denominator.");
207        int div = numerator / denominator;
208        if (ceil) {
209            if (numerator > 0 && (numerator % denominator) != 0) {
210                div++;
211            }
212        } else {
213            if (numerator < 0 && (numerator % denominator) != 0) {
214                div--;
215            }
216        }
217        return div;
218    }
219
220    /**
221     * Updates the {@linkplain #raster} and related fields for the current
222     * {@link #tileX} and {@link #tileY} values.
223     */
224    private void updateRaster() {
225        raster = image.getTile(tileX, tileY);
226        currentMaxX = min(maxX, raster.getMinX() + raster.getWidth());
227        currentMaxY = min(maxY, raster.getMinY() + raster.getHeight());
228    }
229
230    /**
231     * Moves to the next sample values and returns {@code true} if the iteration has more pixels.
232     *
233     * @return {@code true} if the next sample value exist.
234     */
235    public boolean next() {
236        if (++band == numBands) {
237            if ((x += xSubsampling) >= currentMaxX) {
238                int nextTile = tileX + 1;               // Needed only when the iteration stops before the maxX of the last tile in a row.
239                tileX = divide(x - image.getTileGridXOffset(), image.getTileWidth(), false);
240                if (max(nextTile, tileX) >= maxTileX) {
241                    if ((y += ySubsampling) >= currentMaxY) {
242                        nextTile = tileY + 1;           // Needed only when the iteration stops before the maxY of the last row of tiles.
243                        tileY = divide(y - image.getTileGridYOffset(), image.getTileHeight(), false);
244                        if (max(nextTile, tileY) >= maxTileY) {
245                            return false;
246                        }
247                    }
248                    x = minX;
249                    tileX = minTileX;
250                }
251                updateRaster();
252            }
253            band = 0;
254        }
255        return true;
256    }
257
258    /**
259     * Returns the current <var>x</var> coordinate. The coordinate values range from
260     * {@linkplain RenderedImage#getMinX() image X minimum} (inclusive) to that minimum
261     * plus the {@linkplain RenderedImage#getWidth() image width} (exclusive).
262     *
263     * @return the current <var>x</var> coordinate.
264     *
265     * @see RenderedImage#getMinX()
266     * @see RenderedImage#getWidth()
267     */
268    public int getX() {
269        return x;
270    }
271
272    /**
273     * Returns the current <var>y</var> coordinate. The coordinate values range from
274     * {@linkplain RenderedImage#getMinY() image Y minimum} (inclusive) to that minimum
275     * plus the {@linkplain RenderedImage#getHeight() image height} (exclusive).
276     *
277     * @return the current <var>y</var> coordinate.
278     *
279     * @see RenderedImage#getMinY()
280     * @see RenderedImage#getHeight()
281     */
282    public int getY() {
283        return y;
284    }
285
286    /**
287     * Returns the current band index. The index values range from 0 (inclusive) to
288     * the {@linkplain SampleModel#getNumBands() number of bands} (exclusive), or to
289     * the {@code sourceBands} array length (exclusive) if the array given to the
290     * constructor was non-null.
291     *
292     * @return the current band index.
293     *
294     * @see SampleModel#getNumBands()
295     */
296    public int getBand() {
297        return (sourceBands != null) ? sourceBands[band] : band;
298    }
299
300    /**
301     * Returns the type of the sample values, as one of the {@code TYPE_*} constants
302     * defined in the {@link DataBuffer} class.
303     *
304     * @return the type of the sample values.
305     *
306     * @see SampleModel#getDataType()
307     * @see DataBuffer#TYPE_BYTE
308     * @see DataBuffer#TYPE_SHORT
309     * @see DataBuffer#TYPE_USHORT
310     * @see DataBuffer#TYPE_INT
311     * @see DataBuffer#TYPE_FLOAT
312     * @see DataBuffer#TYPE_DOUBLE
313     */
314    public int getDataType() {
315        return image.getSampleModel().getDataType();
316    }
317
318    /**
319     * Returns the sample value at the current position, as an integer.
320     * This method is appropriate for the
321     * {@linkplain DataBuffer#TYPE_BYTE byte},
322     * {@linkplain DataBuffer#TYPE_SHORT short},
323     * {@linkplain DataBuffer#TYPE_USHORT unsigned short} and
324     * {@linkplain DataBuffer#TYPE_INT integer}
325     * {@linkplain #getDataType() datatypes}.
326     *
327     * @return the sample value at the current position.
328     *
329     * @see Raster#getSample(int, int, int)
330     * @see DataBuffer#TYPE_BYTE
331     * @see DataBuffer#TYPE_SHORT
332     * @see DataBuffer#TYPE_USHORT
333     * @see DataBuffer#TYPE_INT
334     */
335    public int getSample() {
336        return raster.getSample(x, y, getBand());
337    }
338
339    /**
340     * Returns the sample value at the current position, as a floating point number.
341     *
342     * @return the sample value at the current position.
343     *
344     * @see Raster#getSampleFloat(int, int, int)
345     * @see DataBuffer#TYPE_FLOAT
346     */
347    public float getSampleFloat() {
348        return raster.getSampleFloat(x, y, getBand());
349    }
350
351    /**
352     * Returns the sample value at the current position, as a double-precision floating point number.
353     *
354     * @return the sample value at the current position.
355     *
356     * @see Raster#getSampleDouble(int, int, int)
357     * @see DataBuffer#TYPE_DOUBLE
358     */
359    public double getSampleDouble() {
360        return raster.getSampleDouble(x, y, getBand());
361    }
362
363    /**
364     * Compares all sample values iterated by this {@code PixelIterator} with the sample values
365     * iterated by the given iterator. If a mismatch is found, then an {@link AssertionError} is
366     * thrown with a detailed error message.
367     *
368     * <p>This method does not verify the image sizes, number of tiles, number of bands, color
369     * model or datatype. Consequently, this method is robust to the following differences:</p>
370     *
371     * <ul>
372     *   <li>Differences in the ({@linkplain RenderedImage#getMinX() x},
373     *       {@linkplain RenderedImage#getMinY() y}) origin;</li>
374     *   <li>Differences in tile layout (images are compared as if they were untiled);</li>
375     *   <li>Differences in the datatype (values are compared using the widest of this iterator
376     *       {@linkplain #getDataType() datatype} and the datatype of the given iterator).</li>
377     * </ul>
378     *
379     * If the images have different sizes, then an <q>Unexpected end of iteration</q>
380     * exception will be thrown when the first iterator reaches the iteration end.
381     *
382     * @param  actual     the iterator that contains the actual values to be compared with the "expected" sample values.
383     * @param  tolerance  the tolerance threshold for floating point comparison. This threshold does not apply to integer types.
384     * @throws AssertionError if a value in this iterator is not equals to a value in the given iterator with the given
385     *         tolerance threshold.
386     */
387    public void assertSampleValuesEqual(final PixelIterator actual, final double tolerance) throws AssertionError {
388        final int dataType = Math.max(getDataType(), actual.getDataType());
389        while (next()) {
390            assertTrue(actual.next(), "Unexpected end of pixel iteration.");
391            switch (dataType) {
392                case DataBuffer.TYPE_DOUBLE: {
393                    final double a = actual.getSampleDouble();
394                    final double e = this.  getSampleDouble();
395                    if (doubleToLongBits(a) == doubleToLongBits(e)) {
396                        continue;                                       // All variants of NaN values are considered equal.
397                    }
398                    if (abs(a-e) <= tolerance) {
399                        continue;                                       // Negative and positive zeros are considered equal.
400                    }
401                    break;
402                }
403                case DataBuffer.TYPE_FLOAT: {
404                    final float a = actual.getSampleFloat();
405                    final float e = this.  getSampleFloat();
406                    if (floatToIntBits(a) == floatToIntBits(e)) {
407                        continue;                                       // All variants of NaN values are considered equal.
408                    }
409                    if (abs(a-e) <= tolerance) {
410                        continue;                                       // Negative and positive zeros are considered equal.
411                    }
412                    break;
413                }
414                default: {
415                    if (actual.getSample() == getSample()) continue;
416                    break;
417                }
418            }
419            /*
420             * Remainder of this block is for formatting the error message.
421             */
422            final Number ev, av;
423            switch (dataType) {
424                case DataBuffer.TYPE_DOUBLE: ev = getSampleDouble(); av = actual.getSampleDouble(); break;
425                case DataBuffer.TYPE_FLOAT:  ev = getSampleFloat();  av = actual.getSampleFloat();  break;
426                default:                     ev = getSample();       av = actual.getSample();       break;
427            }
428            final String lineSeparator = System.getProperty("line.separator", "\n");
429            final StringBuilder buffer = new StringBuilder(1024);
430            buffer.append("Mismatched sample value: expected ").append(ev).append(" but got ").append(av).append(lineSeparator);
431            buffer.append("Pixel coordinate in the complete image: "); position(buffer); buffer.append(lineSeparator);
432            buffer.append("Pixel coordinate in the compared image: "); actual.position(buffer); buffer.append(lineSeparator);
433            actual.completeComparisonFailureMessage(buffer, lineSeparator);
434            fail(buffer.toString());
435        }
436        assertFalse(actual.next(), "Expected end of pixel iteration, but found more values.");
437    }
438
439    /**
440     * Invoked when a sample value mismatch has been found, for allowing {@link PixelIteratorForIO}
441     * to append to the error message the I/O parameters used for the reading or writing process.
442     *
443     * @param buffer         buffer where to write the message.
444     * @param lineSeparator  value of {@link System#lineSeparator()}.
445     */
446    void completeComparisonFailureMessage(final StringBuilder buffer, final String lineSeparator) {
447    }
448
449    /**
450     * Formats the current position of this iterator in the given buffer.
451     *
452     * @param buffer  buffer where to write the position.
453     */
454    private void position(final StringBuilder buffer) {
455        buffer.append('(').append(getX()).append(", ").append(getY()).append(") band ").append(getBand());
456    }
457
458    /**
459     * Returns a string representation of this iterator position for debugging purpose.
460     *
461     * @return a string representation if this iterator position.
462     */
463    @Override
464    public String toString() {
465        final StringBuilder buffer = new StringBuilder(48);
466        position(buffer.append(getClass().getSimpleName()).append('['));
467        return buffer.append(']').toString();
468    }
469}