blob: bd871ef526de1c3965797b7870ec2bd0f8282f55 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.image;
import java.util.Optional;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
import java.awt.image.RasterFormatException;
import java.nio.Buffer;
import java.nio.IntBuffer;
import java.nio.FloatBuffer;
import java.nio.DoubleBuffer;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;
/**
* Default iterator used when no specialized implementation is available.
* This iterator uses the {@link Raster} API for traversing the pixels in each tile.
* Calls to {@link #next()} move the current position by increasing the following values, in order:
*
* <ol>
* <li>Column index in a single tile (from left to right)</li>
* <li>Row index in a single tile (from top to bottom).</li>
* <li>Then, {@code tileX} index from left to right.</li>
* <li>Then, {@code tileY} index from top to bottom.</li>
* </ol>
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*
* @todo Change iteration order on tiles for using Hilbert iterator.
*/
class DefaultIterator extends WritablePixelIterator {
/**
* Tile coordinate of {@link #currentRaster}.
* The {@code tileY >= tileUpperY} condition is used for detecting when we reached iteration end.
*/
int tileX, tileY;
/**
* Current column index in current raster.
* The {@code x >= lowerX} condition is used for detecting if iteration started.
*/
int x;
/**
* Current row index in current raster.
*/
int y;
/**
* Bounds of the region traversed by the iterator in current raster.
* When iteration reaches the upper coordinates, the iterator needs to move to next tile.
*/
int currentLowerX, currentUpperX, currentUpperY;
/**
* Creates an iterator for the given region in the given raster.
*
* @param input the raster which contains the sample values to read.
* @param output the raster where to write the sample values, or {@code null} for read-only iterator.
* @param subArea the raster region where to perform the iteration, or {@code null} for iterating over all the raster domain.
* @param window size of the window to use in {@link #createWindow(TransferType)} method, or {@code null} if none.
*/
DefaultIterator(final Raster input, final WritableRaster output, final Rectangle subArea, final Dimension window) {
super(input, output, subArea, window);
currentLowerX = lowerX;
currentUpperX = upperX;
currentUpperY = upperY;
x = Math.decrementExact(lowerX); // Set the position before first pixel.
y = lowerY;
}
/**
* Creates an iterator for the given region in the given image.
*
* @param input the image which contains the sample values to read.
* @param output the image where to write the sample values, or {@code null} for read-only iterator.
* @param subArea the image region where to perform the iteration, or {@code null} for iterating over all the image domain.
* @param window size of the window to use in {@link #createWindow(TransferType)} method, or {@code null} if none.
*/
DefaultIterator(final RenderedImage input, final WritableRenderedImage output, final Rectangle subArea, final Dimension window) {
super(input, output, subArea, window);
tileX = Math.decrementExact(tileLowerX);
tileY = tileLowerY;
currentLowerX = lowerX;
currentUpperX = lowerX; // Really 'lower', so the position is the tile before the first tile.
currentUpperY = lowerY;
x = Math.decrementExact(lowerX); // Set the position before first pixel.
y = lowerY;
/*
* We need to ensure that `tileUpperY+1 > tileUpperY` will alway be true because `tileY` may be equal
* to `tileUpperY` when the `if (++tileY >= tileUpperY)` statement is excuted in the `next()` method.
* This is because `tileY` is used as a sentinel value for detecting when we reached iteration end.
*/
if (tileUpperY == Integer.MAX_VALUE) {
throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
}
}
/**
* Restores this iterator to the same state it was after construction.
*/
@Override
public void rewind() {
close(); // Release current writable raster, if any.
if (image == null) {
tileX = 0;
tileY = 0;
currentUpperX = upperX;
currentUpperY = upperY;
} else {
tileX = tileLowerX - 1; // Note: no need for decrementExact(…) because already checked by constructor.
tileY = tileLowerY;
currentUpperX = lowerX; // Really 'lower', so the position is the tile before the first tile.
currentUpperY = lowerY;
}
currentLowerX = lowerX;
x = lowerX - 1; // Set the position before first pixel.
y = lowerY;
}
/**
* Returns the order in which pixels are traversed.
*/
@Override
public Optional<SequenceType> getIterationOrder() {
if (image == null || (tileUpperX - tileLowerX) <=1 && (tileUpperY - tileLowerY) <= 1) {
return Optional.of(SequenceType.LINEAR);
} else {
return Optional.empty(); // Undefined order.
}
}
/**
* Returns the column (x) and row (y) indices of the current pixel.
* This implementation {@link #x} and {@link #tileY} for determining if the iteration is valid.
*
* @return column and row indices of current iterator position.
* @throws IllegalStateException if this method is invoked before the first call to {@link #next()}
* or {@link #moveTo(int, int)}, or after {@code next()} returned {@code false}.
*/
@Override
public Point getPosition() {
final short message;
if (x < lowerX) {
message = Resources.Keys.IterationNotStarted;
} else if (tileY >= tileUpperY) {
message = Resources.Keys.IterationIsFinished;
} else {
return new Point(x,y);
}
throw new IllegalStateException(Resources.format(message));
}
/**
* Moves the pixel iterator to the given column (x) and row (y) indices.
*
* @param px the column index of the pixel to make current.
* @param py the row index of the pixel to make current.
* @throws IndexOutOfBoundsException if the given indices are outside the iteration domain.
*/
@Override
public void moveTo(final int px, final int py) {
if (px < lowerX || px >= upperX || py < lowerY || py >= upperY) {
throw new IndexOutOfBoundsException(Resources.format(Resources.Keys.OutOfIteratorDomain_2, px, py));
}
if (image != null) {
final int tx = Math.floorDiv(px - tileGridXOffset, tileWidth);
final int ty = Math.floorDiv(py - tileGridYOffset, tileHeight);
if (tx != tileX || ty != tileY) {
close(); // Release current writable raster, if any.
tileX = tx;
tileY = ty;
if (fetchTile() > py || currentLowerX > px) { // `fetchTile()` must be before `currentLowerX`.
throw new RasterFormatException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, tileY));
}
}
}
x = px;
y = py;
}
/**
* Moves the iterator to the next pixel. This default implementation moves to the next tile only after
* all pixels in current tiles have been traversed, but subclasses may apply a different strategy.
*
* @return {@code true} if the current pixel is valid, or {@code false} if there is no more pixels.
* @throws IllegalStateException if this iterator already reached end of iteration in a previous call
* to {@code next()}, and {@link #rewind()} or {@link #moveTo(int,int)} have not been invoked.
*/
@Override
public boolean next() {
if (++x >= currentUpperX) {
if (++y >= currentUpperY) { // Strict equality (==) would work, but use >= as a safety.
close(); // Release current writable raster, if any.
if (++tileX >= tileUpperX) { // Strict equality (==) would work, but use >= as a safety.
if (++tileY >= tileUpperY) {
endOfIteration();
return false;
}
tileX = tileLowerX;
}
y = fetchTile();
}
x = currentLowerX;
}
return true;
}
/**
* Fetches from the image a tile for the current {@link #tileX} and {@link #tileY} coordinates.
* All fields prefixed by {@code current} are updated by this method. The caller is responsible
* for updating the {@link #x} and {@link #y} fields.
*
* <p>Note that there is no {@code currentLowerY} field in this {@code DefaultIterator} class.
* Instead, the value that would be have been set to that field is returned by this method.</p>
*
* @return the {@link #y} value of the first row of new tile.
*/
final int fetchTile() {
currentRaster = null;
if (destination != null) {
destRaster = destination.getWritableTile(tileX, tileY);
if (destination == image) {
currentRaster = destRaster;
}
}
if (currentRaster == null) {
currentRaster = image.getTile(tileX, tileY);
}
final int minX = currentRaster.getMinX();
final int minY = currentRaster.getMinY();
currentLowerX = Math.max(lowerX, minX);
currentUpperX = Math.min(upperX, minX + tileWidth);
currentUpperY = Math.min(upperY, minY + tileHeight);
if (currentRaster.getNumBands() != numBands) {
throw new RasterFormatException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, tileY));
}
return Math.max(lowerY, minY);
}
/**
* Invoked when a call to {@link #next()} moved to the end of iteration. This method sets fields to values
* that will allow {@link #moveTo(int,int)} and {@link #next()} to detect that we already finished iteration.
*/
final void endOfIteration() {
/*
* The `tileY` value is used for checking if next() is invoked again, in order to avoid a
* common misuse pattern. In principle `tileY` needs to be compared only to `tileUpperY`,
* but we also compare to `tileLowerY + 1` for handling the empty iterator case.
*/
final boolean error = tileY > Math.max(tileUpperY, tileLowerY + 1);
/*
* Paranoiac safety: keep the x, y and tileX variables before their limits
* in order to avoid overflow in the `if (++foo >= limit)` statements.
*/
x = currentUpperX - 1;
y = currentUpperY - 1;
tileX = tileUpperX - 1;
tileY = tileUpperY; // Sentinel value for detecting following error condition.
if (error) {
throw new IllegalStateException(Resources.format(Resources.Keys.IterationIsFinished));
}
}
/**
* Returns the sample value in the specified band of current pixel, rounded toward zero.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public int getSample(final int band) {
return currentRaster.getSample(x, y, band);
}
/**
* Returns the sample value in the specified band of current pixel as a single-precision floating point number.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public float getSampleFloat(final int band) {
return currentRaster.getSampleFloat(x, y, band);
}
/**
* Returns the sample value in the specified band of current pixel, without precision lost.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public double getSampleDouble(final int band) {
return currentRaster.getSampleDouble(x, y, band);
}
/**
* Writes a sample value in the specified band of current pixel.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void setSample(final int band, final int value) {
destRaster.setSample(x, y, band, value);
}
/**
* Writes a sample value in the specified band of current pixel.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void setSample(final int band, final float value) {
destRaster.setSample(x, y, band, value);
}
/**
* Writes a sample value in the specified band of current pixel.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void setSample(final int band, final double value) {
destRaster.setSample(x, y, band, value);
}
/**
* Returns the sample values of current pixel for all bands.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public int[] getPixel​(int[] dest) {
return currentRaster.getPixel(x, y, dest);
}
/**
* Returns the sample values of current pixel for all bands.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public float[] getPixel​(float[] dest) {
return currentRaster.getPixel(x, y, dest);
}
/**
* Returns the sample values of current pixel for all bands.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public double[] getPixel​(double[] dest) {
return currentRaster.getPixel(x, y, dest);
}
/**
* Sets the sample values of current pixel for all bands.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void setPixel​(int[] dest) {
destRaster.setPixel(x, y, dest);
}
/**
* Sets the sample values of current pixel for all bands.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void setPixel​(float[] dest) {
destRaster.setPixel(x, y, dest);
}
/**
* Sets the sample values of current pixel for all bands.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void setPixel​(double[] dest) {
destRaster.setPixel(x, y, dest);
}
/**
* Returns a moving window over the sample values in a rectangular region starting at iterator position.
*/
@Override
@SuppressWarnings("unchecked")
public <T extends Buffer> Window<T> createWindow(final TransferType<T> type) {
ArgumentChecks.ensureNonNull("type", type);
final int length = numBands * windowWidth * windowHeight;
final int transferLength = length - numBands * Math.min(windowWidth, windowHeight);
// 'transfer' will always have at least one row or one column less than 'data'.
switch (type.dataBufferType) {
case DataBuffer.TYPE_INT: return (Window<T>) new IntWindow (new int [length], new int [transferLength]);
case DataBuffer.TYPE_FLOAT: return (Window<T>) new FloatWindow (new float [length], new float [transferLength]);
case DataBuffer.TYPE_DOUBLE: return (Window<T>) new DoubleWindow(new double[length], new double[transferLength]);
default: throw new AssertionError(type); // Should never happen unless we updated TransferType and forgot to update this method.
}
}
/**
* The base class of all {@link Window} implementations provided by {@link DefaultIterator}.
* This iterator defines a callback method required by {@link DefaultIterator#update(WindowBase, Object)}.
*
* @todo keep trace of last location and use {@code System#arraycopy(…)} for moving the values that we already have.
*/
private abstract static class WindowBase<T extends Buffer> extends Window<T> {
/**
* Creates a new window which will store the sample values in the given buffer.
*/
WindowBase(final T buffer) {
super(buffer);
}
/**
* Returns an array containing all samples for a rectangle of pixels in the given raster, one sample
* per array element. Subclasses shall delegate to one of the {@code Raster#getPixels(…)} methods
* depending on the buffer data type.
*
* @param raster the raster from which to get the pixel values.
* @param subX the X coordinate of the upper-left pixel location.
* @param subY the Y coordinate of the upper-left pixel location.
* @param subWidth width of the pixel rectangle.
* @param subHeight height of the pixel rectangle.
* @param direct {@code true} for storing directly in the final array,
* or {@code false} for using the transfer array.
* @return the array in which sample values have been stored.
*/
abstract Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct);
}
/**
* {@link Window} implementation backed by an array of {@code int[]}.
*/
private final class IntWindow extends WindowBase<IntBuffer> {
/**
* Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
* Those arrays are overwritten when {@link #update()} is invoked.
*/
private final int[] data, transfer;
/**
* Creates a new window which will store the sample values in the given {@code data} array.
*/
IntWindow(final int[] data, final int[] transfer) {
super(IntBuffer.wrap(data).asReadOnlyBuffer());
this.data = data;
this.transfer = transfer;
}
/**
* Performs the transfer between the underlying raster and this window.
*/
@Override
Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
}
/**
* Updates this window with the sample values in the region starting at current iterator position.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void update() {
values.clear();
DefaultIterator.this.update(this, data);
}
}
/**
* {@link Window} implementation backed by an array of {@code float[]}.
*/
private final class FloatWindow extends WindowBase<FloatBuffer> {
/**
* Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
* Those arrays are overwritten when {@link #update()} is invoked.
*/
private final float[] data, transfer;
/**
* Creates a new window which will store the sample values in the given {@code data} array.
*/
FloatWindow(final float[] data, final float[] transfer) {
super(FloatBuffer.wrap(data).asReadOnlyBuffer());
this.data = data;
this.transfer = transfer;
}
/**
* Performs the transfer between the underlying raster and this window.
*/
@Override
Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
}
/**
* Updates this window with the sample values in the region starting at current iterator position.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void update() {
values.clear();
DefaultIterator.this.update(this, data);
}
}
/**
* {@link Window} implementation backed by an array of {@code double[]}.
*/
private final class DoubleWindow extends WindowBase<DoubleBuffer> {
/**
* Sample values in the window ({@code data}) and a temporary array ({@code transfer}).
* Those arrays are overwritten when {@link #update()} is invoked.
*/
private final double[] data, transfer;
/**
* Creates a new window which will store the sample values in the given {@code data} array.
*/
DoubleWindow(final double[] data, final double[] transfer) {
super(DoubleBuffer.wrap(data).asReadOnlyBuffer());
this.data = data;
this.transfer = transfer;
}
/**
* Performs the transfer between the underlying raster and this window.
*/
@Override
Object getPixels(Raster raster, int subX, int subY, int subWidth, int subHeight, boolean direct) {
return raster.getPixels(subX, subY, subWidth, subHeight, direct ? data : transfer);
}
/**
* Updates this window with the sample values in the region starting at current iterator position.
* This method assumes that {@link #next()} or {@link #moveTo(int,int)} has been invoked.
*/
@Override
public void update() {
values.clear();
DefaultIterator.this.update(this, data);
}
}
/**
* Updates the content of given window with the sample values in the region starting at current iterator position.
*
* @param window the window to update.
* @param data the array of primitive type where sample values are stored.
*/
@SuppressWarnings("SuspiciousSystemArraycopy")
final void update(final WindowBase<?> window, final Object data) {
Raster raster = currentRaster;
int subEndX = (raster.getMinX() - x) + raster.getWidth();
int subEndY = (raster.getMinY() - y) + raster.getHeight();
int subWidth = Math.min(windowWidth, subEndX);
int subHeight = Math.min(windowHeight, subEndY);
boolean fullWidth = (subWidth == windowWidth);
if (fullWidth && subHeight == windowHeight) {
/*
* Optimization for the case where the full window is inside current raster.
* This is the vast majority of cases, so we perform this check soon before
* to compute more internal variables.
*/
final Object transfer = window.getPixels(raster, x, y, subWidth, subHeight, true);
if (transfer != data) { // Paranoiac check (arrays should always be same).
System.arraycopy(transfer, 0, data, 0, numBands * subWidth * subHeight);
}
return;
}
/*
* At this point, we determined that the window is overlapping two or more tiles.
* We will need more variables for iterating over the tiles around 'currentRaster'.
*/
int destOffset = 0; // Index in 'window' array where to copy the sample values.
int subX = 0; // Upper-left corner of a sub-window inside the window.
int subY = 0;
int tileSubX = tileX; // The tile where is located the (subX, subY) coordinate.
int tileSubY = tileY;
final int stride = windowWidth * numBands; // Number of samples between two rows in the 'windows' array.
final int rewind = subEndX;
for (;;) {
if (subWidth > 0 && subHeight > 0) {
final Object transfer = window.getPixels(raster, x + subX, y + subY, subWidth, subHeight, false);
if (fullWidth) {
final int fullLength = stride * subHeight;
System.arraycopy(transfer, 0, data, destOffset, fullLength);
destOffset += fullLength;
} else {
final int rowLength = numBands * subWidth;
final int fullLength = rowLength * subHeight;
for (int srcOffset=0; srcOffset < fullLength; srcOffset += rowLength) {
System.arraycopy(transfer, srcOffset, data, destOffset, rowLength);
destOffset += stride;
}
}
}
/*
* At this point, we copied all sample values that we could obtain from the current tile.
* Move to the next tile on current row, or if we reached the end of row move to the next row.
*/
if (subEndX < windowWidth) {
subX = subEndX;
subEndX += tileWidth; // Next tile on the same row.
tileSubX++;
} else {
if (subEndY >= windowHeight) {
return; // Completed last row of tiles.
}
subY = subEndY;
subEndY += tileHeight; // Tile on the next row.
tileSubY++;
tileSubX = tileX;
subEndX = rewind;
subX = 0; // Move x position back to the window left border.
}
raster = image.getTile(tileSubX, tileSubY);
destOffset = (subY * windowWidth + subX) * numBands;
subWidth = Math.min(windowWidth, subEndX) - subX;
subHeight = Math.min(windowHeight, subEndY) - subY;
fullWidth = (subWidth == windowWidth);
}
}
/**
* Releases the tiles acquired by this iterator, if any.
* This method does nothing if the iterator is read-only.
*/
@Override
public final void close() {
if (destination != null && destRaster != null) {
destRaster = null;
destination.releaseWritableTile(tileX, tileY);
}
}
}