blob: 8f20214e490b76dcdc7ccba530b9f56a1c088a03 [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.coverage.grid;
import java.util.List;
import java.util.function.Function;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferDouble;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.RasterFormatException;
import java.awt.image.RenderedImage;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.Cache;
import org.apache.sis.image.DataType;
// Branch-specific imports
import org.apache.sis.internal.jdk9.JDK9;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
/**
* Basic access to grid data values backed by a <var>n</var>-dimensional {@link DataBuffer}.
* Those data can be shown as an untiled {@link RenderedImage}.
* Images are created when {@link #render(GridExtent)} is invoked instead of at construction time.
* This delayed construction makes this class better suited to <var>n</var>-dimensional grids since
* those grids can not be wrapped into a single {@link RenderedImage}.
*
* <div class="note"><b>Comparison with alternatives:</b>
* this class expects all data to reside in-memory and does not support tiling.
* Pixels are stored in a row-major fashion with all bands in a single array <em>or</em> one array per band.
* By contrast, {@link GridCoverage2D} allows more flexibility in data layout and supports tiling with data
* loaded or computed on-the-fly, but is restricted to two-dimensional images (which may be slices in a
* <var>n</var>-dimensional grid).</div>
*
* The number of bands is determined by the number of {@link SampleDimension}s specified at construction time.
* The {@linkplain DataBuffer#getNumBanks() number of banks} is either 1 or the number of bands.
*
* <ul class="verbose">
* <li>If the number of banks is 1, all data are packed in a single array with band indices varying fastest,
* then column indices (<var>x</var>), then row indices (<var>y</var>), then other dimensions.</li>
* <li>If the number of banks is greater than 1, then each band is stored in a separated array.
* In each array, sample values are stored with column indices (<var>x</var>) varying fastest,
* then row indices (<var>y</var>), then other dimensions.
* In the two-dimensional case, this layout is also known as <cite>row-major</cite>.</li>
* </ul>
*
* The number of cells in each dimension is specified by the {@link GridExtent} of the geometry given at
* construction time. By default the {@linkplain GridExtent#getSize(int) extent size} in the two first dimensions
* will define the {@linkplain RenderedImage#getWidth() image width} and {@linkplain RenderedImage#getHeight() height},
* but different dimensions may be used depending on which dimensions are identified as the
* {@linkplain GridExtent#getSubspaceDimensions(int) subspace dimensions}.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.3
* @since 1.1
* @module
*/
public class BufferedGridCoverage extends GridCoverage {
/**
* The sample values, potentially multi-banded. The bands may be stored either in a single bank
* ({@linkplain java.awt.image.PixelInterleavedSampleModel pixel interleaved} image) or in different banks
* ({@linkplain java.awt.image.BandedSampleModel banded} image). This class detects automatically
* which of those two sample models is used when {@link #render(GridExtent)} is invoked.
*
* <p>Sample values in this buffer shall not be {@linkplain java.awt.image.SinglePixelPackedSampleModel packed}.</p>
*/
protected final DataBuffer data;
/**
* Cache of rendered images produced by calls to {@link #render(GridExtent)}.
* Those images are cached because, even if they are cheap to create,
* they may become the source of a chain of operations for statistics,
* {@linkplain org.apache.sis.image.ResampledImage image resampling}, <i>etc.</i>
* Caching the source image preserves not only the {@link RenderedImage} instance created by the
* {@link #render(GridExtent)} method, but also the chain of operations potentially derived from that image.
*
* <h4>Usage</h4>
* Implementation of {@link #render(GridExtent)} method can be like below:
*
* {@preformat java
* &#64;Override
* public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException {
* if (sliceExtent == null) {
* sliceExtent = gridGeometry.getExtent();
* }
* // Do some other verification if needed…
* // … then get or compute the image.
* try {
* return cachedRenderings.computeIfAbsent(sliceExtent, (slice) -> {
* val renderer = new ImageRenderer(this, slice);
* renderer.setData(data);
* return renderer.createImage();
* });
* } catch (IllegalGridGeometryException | MismatchedDimensionException e) {
* throw e;
* } catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) {
* throw new CannotEvaluateException(e.getMessage(), e);
* }
* }
* }
*/
private final Cache<GridExtent,RenderedImage> cachedRenderings;
/**
* Constructs a grid coverage using the specified grid geometry, sample dimensions and data buffer.
* This method stores the given buffer by reference (no copy). The bands in the given buffer can be
* stored either in a single bank (pixel interleaved image) or in different banks (banded image).
* This class detects automatically which of those two sample models is used
* (see class javadoc for more information).
*
* <p>Note that {@link DataBuffer} does not contain any information about image size.
* Consequently {@link #render(GridExtent)} depends on the domain {@link GridExtent},
* which must be accurate. If the extent size does not reflect accurately the image size,
* then the image will not be rendered properly.</p>
*
* @param domain the grid extent, CRS and conversion from cell indices to CRS.
* @param range sample dimensions for each image band.
* @param data the sample values, potentially multi-banded.
* @throws NullPointerException if an argument is {@code null}.
* @throws IllegalArgumentException if the data buffer has an incompatible number of banks.
* @throws IllegalGridGeometryException if the grid extent is larger than the data buffer capacity.
* @throws ArithmeticException if the number of cells is larger than 64 bits integer capacity.
*/
public BufferedGridCoverage(final GridGeometry domain, final List<? extends SampleDimension> range, final DataBuffer data) {
super(domain, range);
this.data = data;
ArgumentChecks.ensureNonNull("data", data);
/*
* The buffer shall either contain all values in a single bank (pixel interleaved sample model)
* or contain as many banks as bands (banded sample model).
*/
final int numBands = range.size();
final int numBanks = data.getNumBanks();
if (numBanks != 1 && numBanks != numBands) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedBandCount_2, numBanks, numBands));
}
/*
* Verify that the buffer has enough elements for all cells in grid extent.
* Note that the buffer may have all elements in a single bank.
*/
final GridExtent extent = domain.getExtent();
final long expectedSize = getSampleCount(extent, numBands);
final long bufferSize = JDK9.multiplyFull(data.getSize(), numBanks);
if (bufferSize < expectedSize) {
final StringBuilder b = new StringBuilder();
for (int i=0; i < extent.getDimension(); i++) {
if (i != 0) b.append(" × ");
b.append(extent.getSize(i));
}
throw new IllegalGridGeometryException(Resources.format(
Resources.Keys.InsufficientBufferCapacity_3, b, numBands, expectedSize - bufferSize));
}
cachedRenderings = new Cache<>();
}
/**
* Constructs a grid coverage using the specified grid geometry, sample dimensions and data type.
* This constructor creates a single-bank {@link DataBuffer} (pixel interleaved sample model)
* with all sample values initialized to zero.
*
* @param grid the grid extent, CRS and conversion from cell indices to CRS.
* @param bands sample dimensions for each image band.
* @param dataType one of {@code DataBuffer.TYPE_*} constants, the native data type used to store the coverage values.
* @throws ArithmeticException if the grid size is too large.
*/
public BufferedGridCoverage(final GridGeometry grid, final List<? extends SampleDimension> bands, final int dataType) {
super(grid, bands);
final int n = Math.toIntExact(getSampleCount(grid.getExtent(), bands.size()));
switch (dataType) {
case DataBuffer.TYPE_BYTE: data = new DataBufferByte (n); break;
case DataBuffer.TYPE_SHORT: data = new DataBufferShort (n); break;
case DataBuffer.TYPE_USHORT: data = new DataBufferUShort(n); break;
case DataBuffer.TYPE_INT: data = new DataBufferInt (n); break;
case DataBuffer.TYPE_FLOAT: data = new DataBufferFloat (n); break;
case DataBuffer.TYPE_DOUBLE: data = new DataBufferDouble(n); break;
default: throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, dataType));
}
cachedRenderings = new Cache<>();
}
/**
* Returns the number of cells in the given extent multiplied by the number of bands.
*
* @param extent the extent for which to get the number of cells.
* @param nbSamples number of bands.
* @return number of cells multiplied by the number of bands.
* @throws ArithmeticException if the number of samples exceeds 64-bits integer capacity.
*/
private static long getSampleCount(final GridExtent extent, long nbSamples) {
for (int i = extent.getDimension(); --i >= 0;) {
nbSamples = Math.multiplyExact(nbSamples, extent.getSize(i));
}
return nbSamples;
}
/**
* Returns the constant identifying the primitive type used for storing sample values.
*/
@Override
final DataType getBandType() {
return DataType.forDataBufferType(data.getDataType());
}
/**
* Creates a new function for computing or interpolating sample values at given locations.
*
* <h4>Multi-threading</h4>
* {@code GridEvaluator}s are not thread-safe. For computing sample values concurrently,
* a new {@link GridEvaluator} instance should be created for each thread.
*/
@Override
public GridEvaluator evaluator() {
return new CellAccessor(this);
}
/**
* Returns a two-dimensional slice of grid data as a rendered image.
* This method returns a view; sample values are not copied.
*
* <p>The default implementation prepares an {@link ImageRenderer},
* then invokes {@link #configure(ImageRenderer)} for allowing subclasses
* to complete the renderer configuration before to create the image.</p>
*
* @return the grid slice as a rendered image.
*/
@Override
public RenderedImage render(GridExtent sliceExtent) {
if (sliceExtent == null) {
sliceExtent = gridGeometry.extent;
}
try {
return cachedRenderings.computeIfAbsent(sliceExtent, (slice) -> {
ImageRenderer renderer = new ImageRenderer(this, slice);
renderer.setData(data);
return renderer.createImage();
});
} catch (IllegalGridGeometryException | MismatchedDimensionException e) {
throw e;
} catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) {
throw new CannotEvaluateException(e.getMessage(), e);
}
}
/**
* Invoked by the default implementation of {@link #render(GridExtent)}
* for completing the renderer configuration before to create an image.
* The default implementation does nothing.
*
* <p>Some example of methods that subclasses may want to use are:</p>
* <ul>
* <li>{@link ImageRenderer#setCategoryColors(Function)}</li>
* <li>{@link ImageRenderer#setVisibleBand(int)}</li>
* </ul>
*
* @param renderer the renderer to configure before to create an image.
*
* @since 1.3
*/
protected void configure(final ImageRenderer renderer) {
}
/**
* Implementation of evaluator returned by {@link #evaluator()}.
*/
private static class CellAccessor extends GridEvaluator {
/**
* A copy of {@link BufferedGridCoverage#data} reference.
*/
private final DataBuffer data;
/**
* The grid lower values. Those values need to be subtracted to each
* grid coordinate before to compute index in {@link #data} buffer.
*/
private final long[] lower;
/**
* Grid size with shifted index. The size of dimension 0 is stored at index 1, the size of dimension 1
* is stored in index 2, <i>etc.</i>. Element 0 contains the number of banks. This layout is convenient
* for computing index in {@link DataBuffer}.
*/
private final long[] sizes;
/**
* {@code true} for banded sample model, or {@code false} for pixel interleaved sample model.
*/
private final boolean banded;
/**
* Creates a new evaluator for the specified coverage.
*/
CellAccessor(final BufferedGridCoverage coverage) {
super(coverage);
data = coverage.data;
final GridExtent extent = coverage.getGridGeometry().getExtent();
lower = new long[extent.getDimension()];
sizes = new long[lower.length + 1];
sizes[0] = data.getNumBanks();
for (int i=0; i<lower.length; i++) {
lower[i] = extent.getLow(i);
sizes[i+1] = extent.getSize(i);
}
banded = sizes[0] > 1;
values = new double[coverage.getSampleDimensions().size()];
}
/**
* Returns a sequence of double values for a given point in the coverage.
* The CRS of the given point may be any coordinate reference system,
* or {@code null} for the same CRS than the coverage.
*/
@Override
public double[] apply(final DirectPosition point) throws CannotEvaluateException {
final int pos;
try {
final FractionalGridCoordinates gc = toGridPosition(point);
int i = lower.length;
long s = sizes[i];
long index = 0;
while (--i >= 0) {
final long low = lower[i];
long p = gc.getCoordinateValue(i);
// (p - low) may overflow, so we must test (p < low) before.
if (p < low || (p -= low) >= s) {
if (isNullIfOutside()) {
return null;
}
throw new PointOutsideCoverageException(
gc.pointOutsideCoverage(getCoverage().gridGeometry.extent), point);
}
/*
* Following should never overflow, otherwise BufferedGridCoverage
* constructor would have failed. Should never be negative neither.
*/
index = (index + p) * (s = sizes[i]);
}
/*
* Failure on next line would not be caused by a point outside coverage bounds.
* So it should not be rethrown as PointOutsideCoverageException.
*/
pos = Math.toIntExact(index);
} catch (ArithmeticException | FactoryException | TransformException ex) {
throw new CannotEvaluateException(ex.getMessage(), ex);
}
final double[] values = this.values;
if (banded) {
for (int i=0; i<values.length; i++) {
values[i] = data.getElemDouble(i, pos);
}
} else {
for (int i=0; i<values.length; i++) {
values[i] = data.getElemDouble(i + pos);
}
}
return values;
}
}
}