| /* |
| * 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 |
| * @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; |
| } |
| } |
| } |