| /* |
| * 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.Arrays; |
| import java.util.Hashtable; |
| import java.util.Objects; |
| import java.util.function.Function; |
| import java.nio.Buffer; |
| import java.awt.Color; |
| import java.awt.Point; |
| import java.awt.Rectangle; |
| import java.awt.image.ColorModel; |
| import java.awt.image.DataBuffer; |
| import java.awt.image.SampleModel; |
| import java.awt.image.BufferedImage; |
| import java.awt.image.RenderedImage; |
| import java.awt.image.WritableRaster; |
| import java.awt.image.ImagingOpException; |
| import java.awt.image.RasterFormatException; |
| import java.awt.image.Raster; |
| import static java.lang.Math.addExact; |
| import static java.lang.Math.subtractExact; |
| import static java.lang.Math.multiplyExact; |
| import static java.lang.Math.incrementExact; |
| import static java.lang.Math.toIntExact; |
| import org.opengis.util.FactoryException; |
| import org.opengis.referencing.operation.MathTransformFactory; |
| import org.apache.sis.image.DataType; |
| import org.apache.sis.coverage.SubspaceNotSpecifiedException; |
| import org.apache.sis.coverage.MismatchedCoverageRangeException; |
| import org.apache.sis.coverage.SampleDimension; |
| import org.apache.sis.coverage.Category; |
| import org.apache.sis.coverage.privy.ColorModelBuilder; |
| import org.apache.sis.coverage.privy.DeferredProperty; |
| import org.apache.sis.coverage.privy.RasterFactory; |
| import org.apache.sis.coverage.privy.ObservableImage; |
| import org.apache.sis.coverage.privy.TiledImage; |
| import org.apache.sis.coverage.privy.WritableTiledImage; |
| import org.apache.sis.feature.internal.Resources; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.math.Vector; |
| import static org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY; |
| import static org.apache.sis.image.PlanarImage.SAMPLE_DIMENSIONS_KEY; |
| |
| // Specific to the main and geoapi-3.1 branches: |
| import org.opengis.geometry.MismatchedDimensionException; |
| |
| |
| /** |
| * A builder for the rendered image to be returned by {@link GridCoverage#render(GridExtent)}. |
| * This builder does not copy any sample values. Instead, it wraps existing data arrays into |
| * {@link java.awt.image.Raster} objects by computing required information such as |
| * {@linkplain java.awt.image.ComponentSampleModel#getPixelStride() pixel stride}, |
| * {@linkplain java.awt.image.ComponentSampleModel#getScanlineStride() scanline stride} and |
| * {@linkplain java.awt.image.ComponentSampleModel#getBandOffsets() band offsets}. |
| * Different {@code setData(…)} methods are provided for allowing to specify the data arrays |
| * from different objects such as Java2D {@link DataBuffer} or NIO {@link Buffer}. |
| * |
| * <p>All {@code setData(…)} methods assume that the first valid element in each array is the value |
| * located at <code>{@linkplain GridCoverage#getGridGeometry()}.{@linkplain GridGeometry#getExtent() |
| * getExtent()}.{@linkplain GridExtent#getLow(int) getLow()}</code>. This {@code ImageRenderer} class |
| * computes automatically the offsets from that position to the position of the first value included |
| * in the {@code sliceExtent} given to the constructor.</p> |
| * |
| * <h2>Usage example</h2> |
| * {@snippet lang="java" : |
| * class MyResource extends GridCoverage { |
| * @Override |
| * public RenderedImage render(GridExtent sliceExtent) { |
| * ImageRenderer renderer = new ImageRenderer(this, sliceExtent); |
| * try { |
| * renderer.setData(data); |
| * return renderer.createImage(); |
| * } catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) { |
| * throw new CannotEvaluateException("Cannot create an image.", e); |
| * } |
| * } |
| * } |
| * } |
| * |
| * <h2>Limitations</h2> |
| * Current implementation constructs only images made of a single tile. |
| * Support for tiled images will be added in a future version. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.4 |
| * |
| * @see GridCoverage#render(GridExtent) |
| * |
| * @since 1.0 |
| */ |
| public class ImageRenderer { |
| /** |
| * The grid geometry of the {@link GridCoverage} specified at construction time. |
| * Never {@code null}. |
| */ |
| private final GridGeometry geometry; |
| |
| /** |
| * The requested slice, or {@code null} if unspecified. |
| * If unspecified, then the extent to use is the full coverage grid extent. |
| */ |
| private final GridExtent sliceExtent; |
| |
| /** |
| * The dimensions to select in the grid coverage for producing an image. This is an array of length |
| * {@value GridCoverage2D#BIDIMENSIONAL} obtained by {@link GridExtent#getSubspaceDimensions(int)}. |
| * The array content is almost always {0,1}, but this class should work with other dimensions too. |
| * |
| * @see #getXYDimensions() |
| */ |
| private final int[] gridDimensions; |
| |
| /** |
| * The result of {@link #getImageGeometry(int)} if the specified number of dimension 2. |
| * This is cached for avoiding to recompute this geometry if asked many times. |
| * |
| * @see #getImageGeometry(int) |
| */ |
| private GridGeometry imageGeometry; |
| |
| /** |
| * Offset to add to {@link #buffer} offset for reaching the first sample value for the slice to render. |
| * This is zero for a two-dimensional image, but may be greater for cube having more dimensions. |
| * Despite the "Z" letter in the field name, this field actually combines the offset for <em>all</em> |
| * dimensions other than X and Y. |
| */ |
| private final long offsetZ; |
| |
| /** |
| * Location of the first image pixel relative to the grid coverage extent. The (0,0) offset means that the first pixel |
| * in the {@code sliceExtent} (specified at construction time) is the first pixel in the whole {@link GridCoverage}. |
| * |
| * <h4>Implementation note</h4> |
| * If those offsets exceed 32 bits integer capacity, then it may not be possible to build an image |
| * for given {@code sliceExtent} from a single {@link DataBuffer}, because accessing sample values |
| * would exceed the capacity of index in Java arrays. In those cases the image needs to be tiled. |
| */ |
| private final long offsetX, offsetY; |
| |
| /** |
| * Pixel coordinates of the image upper-left corner, as an offset relative to the {@code sliceExtent}. |
| * This is initially zero (unless {@code sliceExtent} is partially outside the grid coverage extent), |
| * but a different value may be used if the given data are tiled. |
| * |
| * @see RenderedImage#getMinX() |
| * @see RenderedImage#getMinY() |
| */ |
| private final int imageX, imageY; |
| |
| /** |
| * Width (number of pixels in a row) of the image to render. |
| * This is usually set to the grid extent along the first dimension |
| * having a {@linkplain GridExtent#getSize(int) size} greater than 1. |
| * |
| * @see RenderedImage#getWidth() |
| */ |
| private final int width; |
| |
| /** |
| * Height (number of pixels in a column) of the image to render. |
| * This is usually set to the grid extent along the second dimension |
| * having a {@linkplain GridExtent#getSize(int) size} greater than 1. |
| * |
| * @see RenderedImage#getHeight() |
| */ |
| private final int height; |
| |
| /** |
| * Number of data elements between two samples for the same band on the same line. |
| * This is the product of {@linkplain GridExtent#getSize(int) grid sizes} of enclosing {@code GridCoverage} |
| * in all dimensions before the dimension of image {@linkplain #width}. This stride does <strong>not</strong> |
| * include the multiplication factor for the number of bands in a <i>pixel interleaved sample model</i> |
| * because whether this factor is needed or not depends on the data {@linkplain #buffer}, |
| * which is not known at construction time. |
| * |
| * @see #strideFactor |
| * @see java.awt.image.ComponentSampleModel#pixelStride |
| */ |
| private final int pixelStride; |
| |
| /** |
| * Number of data elements between a given sample and the corresponding sample in the same column of the next line. |
| * This is the product of {@linkplain GridExtent#getSize(int) grid sizes} of enclosing {@code GridCoverage} in all |
| * dimensions before the dimension of image {@linkplain #height}. This stride does <strong>not</strong> include the |
| * multiplication factor for the number of bands in a <i>pixel interleaved sample model</i> because whether this |
| * factor is needed or not depends on the data {@linkplain #buffer}, which is not known at construction time. |
| * |
| * @see #strideFactor |
| * @see java.awt.image.ComponentSampleModel#scanlineStride |
| */ |
| private final int scanlineStride; |
| |
| /** |
| * Multiplication factor for {@link #pixelStride} and {@link #scanlineStride}. This is the number of data elements |
| * between two samples in the data {@link #buffer}. There is no direct equivalent in {@link java.awt.image} because |
| * <var>pixel stride</var> and <var>scanline stride</var> in {@link SampleModel} are pre-multiplied by this factor, |
| * but we need to keep this information separated in this builder because its value depends on which methods are invoked: |
| * |
| * <ul> |
| * <li>If {@link #setInterleavedPixelOffsets(int, int[])} is invoked, this is the value given to that method.</li> |
| * <li>Otherwise if {@link #setData(DataBuffer)} is invoked and the given buffer has only |
| * {@linkplain DataBuffer#getNumBanks() one bank}, then this is {@link #getNumBands()}.</li> |
| * <li>Otherwise this is 1.</li> |
| * </ul> |
| * |
| * @see #setInterleavedPixelOffsets(int, int[]) |
| */ |
| private int strideFactor; |
| |
| /** |
| * The sample dimensions, to be used for defining the bands. |
| */ |
| private final SampleDimension[] bands; |
| |
| /** |
| * Offset to add to index of sample values in each band in order to reach the value in the {@link DataBuffer} bank. |
| * This is closely related to {@link java.awt.image.ComponentSampleModel#bandOffsets} but not identical, because of |
| * the following differences: |
| * |
| * <ul> |
| * <li>Another offset for {@link #offsetX} and {@link #offsetY} may need to be added |
| * before to give the {@code bandOffsets} to {@link SampleModel} constructor.</li> |
| * <li>If null, a default value is inferred depending on whether the {@link SampleModel} |
| * to construct is banded or interleaved.</li> |
| * </ul> |
| * |
| * @see #setInterleavedPixelOffsets(int, int[]) |
| */ |
| private int[] bandOffsets; |
| |
| /** |
| * Bank indices for each band, or {@code null} for 0, 1, 2, 3…. |
| * If non-null, this array length must be equal to {@link #bands} array length. |
| */ |
| private int[] bankIndices; |
| |
| /** |
| * The band to use for defining pixel colors when the image is displayed on screen. |
| * All other bands, if any, will exist in the raster but be ignored at display time. |
| * |
| * @see #setVisibleBand(int) |
| */ |
| private int visibleBand; |
| |
| /** |
| * The data to render, or {@code null} if not yet specified. |
| * If non-null, {@link DataBuffer#getNumBanks()} must be equal to {@link #bands} array length. |
| */ |
| private DataBuffer buffer; |
| |
| /** |
| * The colors to use for each category. Never {@code null}. |
| * The function may return {@code null}, which means transparent. |
| * The default value is {@link ColorModelBuilder#GRAYSCALE}. |
| * |
| * @see #setCategoryColors(Function) |
| */ |
| private Function<Category,Color[]> colors; |
| |
| /** |
| * The properties to give to the image, or {@code null} if none. |
| * |
| * @see #addProperty(String, Object) |
| */ |
| @SuppressWarnings("UseOfObsoleteCollectionType") |
| private Hashtable<String,Object> properties; |
| |
| /** |
| * The factory to use for {@link org.opengis.referencing.operation.MathTransform} creations, |
| * or {@code null} for a default factory. |
| * |
| * <p>For now this is fixed to {@code null}. But it may become a non-static, non-final field |
| * in a future version if we want to make this property configurable.</p> |
| */ |
| private static final MathTransformFactory mtFactory = null; |
| |
| /** |
| * Creates a new image renderer for the given slice extent. |
| * |
| * @param coverage the source coverage for which to build an image. |
| * @param sliceExtent the domain from which to create an image, or {@code null} for the {@code coverage} extent. |
| * @throws SubspaceNotSpecifiedException if this method cannot infer a two-dimensional slice from {@code sliceExtent}. |
| * @throws DisjointExtentException if the given extent does not intersect the given coverage. |
| * @throws ArithmeticException if a stride calculation overflows the 32 bits integer capacity. |
| */ |
| public ImageRenderer(final GridCoverage coverage, GridExtent sliceExtent) { |
| bands = coverage.getSampleDimensions().toArray(SampleDimension[]::new); |
| geometry = coverage.getGridGeometry(); |
| final GridExtent source = geometry.getExtent(); |
| final int dimension = source.getDimension(); |
| this.sliceExtent = sliceExtent; |
| if (sliceExtent != null) { |
| if (sliceExtent.getDimension() != dimension) { |
| throw new MismatchedDimensionException(Errors.format( |
| Errors.Keys.MismatchedDimension_3, "sliceExtent", dimension, sliceExtent.getDimension())); |
| } |
| } else { |
| sliceExtent = source; |
| } |
| gridDimensions = sliceExtent.getSubspaceDimensions(GridCoverage2D.BIDIMENSIONAL); |
| final int xd = gridDimensions[0]; |
| final int yd = gridDimensions[1]; |
| final long xcov = source.getLow(xd); |
| final long ycov = source.getLow(yd); |
| final long xreq = sliceExtent.getLow(xd); |
| final long yreq = sliceExtent.getLow(yd); |
| final long xmin = Math.max(xreq, xcov); |
| final long ymin = Math.max(yreq, ycov); |
| final long xmax = Math.min(sliceExtent.getHigh(xd), source.getHigh(xd)); |
| final long ymax = Math.min(sliceExtent.getHigh(yd), source.getHigh(yd)); |
| if (xmax < xmin || ymax < ymin) { // max are inclusive. |
| final int d = (xmax < xmin) ? xd : yd; |
| throw new DisjointExtentException(source, sliceExtent, d); |
| } |
| width = incrementExact(toIntExact(xmax - xmin)); |
| height = incrementExact(toIntExact(ymax - ymin)); |
| imageX = toIntExact(subtractExact(xmin, xreq)); |
| imageY = toIntExact(subtractExact(ymin, yreq)); |
| offsetX = subtractExact(xmin, xcov); |
| offsetY = subtractExact(ymin, ycov); |
| /* |
| * At this point, the RenderedImage properties have been computed as if the image was a single tile. |
| * Now compute `SampleModel` properties (the strides). Current version still assumes a single tile, |
| * but it could be changed in the future if we want to add tiling support. The following loop also |
| * computes a "global" offset to add for reachining the beginning of the slice if we are rendering |
| * a slice in a three-dimensional (or more) cube. |
| */ |
| long stride = 1; |
| long pixelStride = 0; |
| long scanlineStride = 0; |
| long offsetZ = 0; |
| for (int i=0; i<dimension; i++) { |
| if (i == xd) { |
| pixelStride = stride; |
| } else if (i == yd) { |
| scanlineStride = stride; |
| } else { |
| final long min = source.getLow(i); |
| final long c = sliceExtent.getLow(i); |
| if (c > min) { |
| offsetZ = addExact(offsetZ, multiplyExact(stride, c - min)); |
| } |
| } |
| stride = multiplyExact(stride, source.getSize(i)); |
| } |
| this.pixelStride = toIntExact(pixelStride); |
| this.scanlineStride = toIntExact(scanlineStride); |
| this.offsetZ = offsetZ; |
| this.colors = ColorModelBuilder.GRAYSCALE; |
| } |
| |
| /** |
| * Returns the number of bands that the image will have. By default, this is the number of |
| * {@linkplain GridCoverage#getSampleDimensions() sample dimensions} in the grid coverage. |
| * |
| * @return the number of bands in the rendered image. |
| */ |
| public final int getNumBands() { |
| return bands.length; |
| } |
| |
| /** |
| * Ensures that the given number is equal to the expected number of bands. |
| * The given number shall be either 1 (case of interleaved sample model) or |
| * {@link #getNumBands()} (case of banded sample model). |
| */ |
| private void ensureExpectedBandCount(final int n, final boolean acceptOne) { |
| if (!(n == 1 & acceptOne)) { |
| final int e = getNumBands(); |
| if (n != e) { |
| throw new MismatchedCoverageRangeException(Resources.format(Resources.Keys.UnexpectedNumberOfBands_2, e, n)); |
| } |
| } |
| } |
| |
| /** |
| * Returns the location of the image upper-left corner together with the image size. The image coordinate system |
| * is relative to the {@code sliceExtent} specified at construction time: the (0,0) pixel coordinates correspond |
| * to the {@code sliceExtent} {@linkplain GridExtent#getLow(int) low coordinates}. Consequently, the rectangle |
| * {@linkplain Rectangle#x <var>x</var>} and {@linkplain Rectangle#y <var>y</var>} coordinates are (0,0) if |
| * the image is located exactly in the area requested by {@code sliceExtent}, or is shifted as below otherwise: |
| * |
| * <blockquote>( <var>x</var>, <var>y</var> ) = |
| * (grid coordinates of actually provided region) − (grid coordinates of requested region)</blockquote> |
| * |
| * @return the rendered image location and size (never null). |
| */ |
| public final Rectangle getBounds() { |
| return new Rectangle(imageX, imageY, width, height); |
| } |
| |
| /** |
| * The dimensions to select in the grid coverage for producing an image. This is the array obtained |
| * by <code>{@link GridExtent#getSubspaceDimensions(int) GridExtent.getSubspaceDimensions(2)}</code>. |
| * The array content is almost always {0,1}, i.e. the 2 first dimensions in a coordinate tuple. |
| * |
| * @return indices of <var>x</var> and <var>y</var> coordinate values in a grid coordinate tuple. |
| * |
| * @since 1.3 |
| */ |
| public final int[] getXYDimensions() { |
| return gridDimensions.clone(); |
| } |
| |
| /** |
| * Computes the conversion from pixel coordinates to CRS, together with the geospatial envelope of the image. |
| * The {@link GridGeometry} returned by this method is derived from the {@linkplain GridCoverage#getGridGeometry() |
| * coverage grid geometry} with the following changes: |
| * |
| * <ul> |
| * <li>The {@linkplain GridGeometry#getDimension() number of grid dimensions} is always 2.</li> |
| * <li>The number of {@linkplain GridGeometry#getCoordinateReferenceSystem() CRS} dimensions |
| * is specified by {@code dimCRS} (usually 2).</li> |
| * <li>The {@linkplain GridGeometry#getEnvelope() envelope} may be a sub-region of the coverage envelope.</li> |
| * <li>The {@linkplain GridGeometry#getExtent() grid extent} is the {@linkplain #getBounds() image bounds}.</li> |
| * <li>The {@linkplain GridGeometry#getGridToCRS grid to CRS} transform is derived from the coverage transform |
| * with a translation for mapping the {@code sliceExtent} {@linkplain GridExtent#getLow(int) low coordinates} |
| * to (0,0) pixel coordinates.</li> |
| * </ul> |
| * |
| * @param dimCRS desired number of dimensions in the CRS. This is usually 2. |
| * @return conversion from pixel coordinates to CRS of the given number of dimensions, |
| * together with image bounds and geospatial envelope if possible. |
| * |
| * @see org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY |
| * |
| * @since 1.1 |
| */ |
| public GridGeometry getImageGeometry(final int dimCRS) { |
| GridGeometry ig = imageGeometry; |
| if (ig == null || dimCRS != GridCoverage2D.BIDIMENSIONAL) { |
| if (imageUseSameGeometry(dimCRS)) { |
| ig = geometry; |
| } else try { |
| ig = new SliceGeometry(geometry, sliceExtent, gridDimensions, mtFactory) |
| .reduce(new GridExtent(imageX, imageY, width, height), dimCRS); |
| } catch (FactoryException e) { |
| throw SliceGeometry.canNotCompute(e); |
| } |
| if (dimCRS == GridCoverage2D.BIDIMENSIONAL) { |
| imageGeometry = ig; |
| } |
| } |
| return ig; |
| } |
| |
| /** |
| * Returns the value associated to the given property. |
| * The properties recognized by current implementation are: |
| * |
| * <ul> |
| * <li>{@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}.</li> |
| * <li>{@value org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY}.</li> |
| * <li>Any property added by calls to {@link #addProperty(String, Object)}.</li> |
| * </ul> |
| * |
| * @param key the property for which to get a value. |
| * @return value associated to the given property, or {@code null} if none. |
| * |
| * @since 1.1 |
| */ |
| public Object getProperty(final String key) { |
| switch (key) { |
| case GRID_GEOMETRY_KEY: return getImageGeometry(GridCoverage2D.BIDIMENSIONAL); |
| case SAMPLE_DIMENSIONS_KEY: return bands.clone(); |
| } |
| return (properties != null) ? properties.get(key) : null; |
| } |
| |
| /** |
| * Adds a value associated to a property. This method can be invoked only once for each {@code key}. |
| * Those properties will be given to the image created by the {@link #createImage()} method. |
| * |
| * @param key key of the property to set. |
| * @param value value to associate to the given key. |
| * @throws IllegalArgumentException if a value is already associated to the given key. |
| * |
| * @since 1.1 |
| */ |
| @SuppressWarnings("UseOfObsoleteCollectionType") |
| public void addProperty(final String key, final Object value) { |
| ArgumentChecks.ensureNonNull("key", key); |
| ArgumentChecks.ensureNonNull("value", value); |
| if (!(GRID_GEOMETRY_KEY.equals(key) || SAMPLE_DIMENSIONS_KEY.equals(key))) { |
| if (properties == null) { |
| properties = new Hashtable<>(); |
| } |
| if (properties.putIfAbsent(key, value) == null) { |
| return; |
| } |
| } |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1, key)); |
| } |
| |
| /** |
| * Returns {@code true} if a {@link #getImageGeometry(int)} request for the given number of CRS dimensions |
| * can return {@link #geometry} directly. This common case avoids the need for more costly computation with |
| * {@link SliceGeometry}. |
| */ |
| private boolean imageUseSameGeometry(final int dimCRS) { |
| final int tgtDim = geometry.getTargetDimension(); |
| ArgumentChecks.ensureBetween("dimCRS", GridCoverage2D.BIDIMENSIONAL, tgtDim, dimCRS); |
| if (tgtDim == dimCRS && geometry.getDimension() == gridDimensions.length) { |
| final GridExtent extent = geometry.extent; |
| if (sliceExtent == null) { |
| return extent == null || extent.startsAtZero(); |
| } else if (sliceExtent.equals(extent, ComparisonMode.IGNORE_METADATA)) { |
| return sliceExtent.startsAtZero(); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Sets the data as a Java2D buffer. The {@linkplain DataBuffer#getNumBanks() number of banks} |
| * in the given buffer must be equal to the {@linkplain #getNumBands() expected number of bands}. |
| * In each bank, the value located at the {@linkplain DataBuffer#getOffsets() bank offset} is the value |
| * located at <code>{@linkplain GridCoverage#getGridGeometry()}.{@linkplain GridGeometry#getExtent() |
| * getExtent()}.{@linkplain GridExtent#getLow(int) getLow()}</code>, as specified in class javadoc. |
| * |
| * @param data the Java2D buffer containing data for all bands. |
| * @throws NullPointerException if {@code data} is null. |
| * @throws MismatchedCoverageRangeException if the given data buffer does not have the expected number of banks. |
| */ |
| public void setData(final DataBuffer data) { |
| ensureExpectedBandCount(data.getNumBanks(), true); |
| buffer = data; |
| } |
| |
| /** |
| * Sets the data as NIO buffers. The number of buffers must be equal to the {@linkplain #getNumBands() expected |
| * number of bands}. All buffers must be {@linkplain Buffer#array() backed by arrays} of the type specified by |
| * the {@code dataType} argument and have the same number of {@linkplain Buffer#remaining() remaining elements}. |
| * This method wraps the underlying arrays of a primitive type into a Java2D buffer; data are not copied. |
| * For each buffer, the grid coverage data (not only the slice data) starts at {@linkplain Buffer#position() |
| * buffer position} and ends at that position + {@linkplain Buffer#remaining() remaining}. |
| * |
| * <p>The data type must be specified in order to distinguish between the signed and unsigned types. |
| * {@link DataType#BYTE} and {@link DataType#USHORT} are unsigned, all other supported types are signed.</p> |
| * |
| * <p><b>Implementation note:</b> the Java2D buffer is set by a call to {@link #setData(DataBuffer)}, |
| * which can be overridden by subclasses if desired.</p> |
| * |
| * @param dataType type of data. |
| * @param data the buffers wrapping arrays of primitive type. |
| * @throws NullPointerException if {@code data} is null or one of {@code data} element is null. |
| * @throws MismatchedCoverageRangeException if the number of specified buffers is not equal to the number of bands. |
| * @throws UnsupportedOperationException if a buffer is not backed by an accessible array or is read-only. |
| * @throws ArrayStoreException if a buffer type is incompatible with {@code dataType}. |
| * @throws RasterFormatException if buffers do not have the same number of remaining values. |
| * @throws ArithmeticException if a buffer position overflows the 32 bits integer capacity. |
| * |
| * @since 1.1 |
| */ |
| public void setData(final DataType dataType, final Buffer... data) { |
| ArgumentChecks.ensureNonNull("dataType", dataType); |
| ensureExpectedBandCount(data.length, true); |
| setData(RasterFactory.wrap(dataType, data)); |
| } |
| |
| /** |
| * Sets the data as vectors. The number of vectors must be equal to the {@linkplain #getNumBands() expected number of bands}. |
| * All vectors must be backed by arrays (indirectly, through {@linkplain Vector#buffer() buffers} backed by arrays) and have |
| * the same {@linkplain Vector#size() size}. |
| * This method wraps the underlying arrays of a primitive type into a Java2D buffer; data are not copied. |
| * |
| * <p><b>Implementation note:</b> the NIO buffers are set by a call to {@link #setData(DataType, Buffer...)}, |
| * which can be overridden by subclasses if desired.</p> |
| * |
| * @param data the vectors wrapping arrays of primitive type. |
| * @throws NullPointerException if {@code data} is null or one of {@code data} element is null. |
| * @throws MismatchedCoverageRangeException if the number of specified vectors is not equal to the number of bands. |
| * @throws UnsupportedOperationException if a vector is not backed by an accessible array or is read-only. |
| * @throws RasterFormatException if vectors do not have the same size. |
| * @throws ArithmeticException if a buffer position overflows the 32 bits integer capacity. |
| */ |
| public void setData(final Vector... data) { |
| ensureExpectedBandCount(data.length, true); |
| final Buffer[] buffers = new Buffer[data.length]; |
| DataType dataType = null; |
| for (int i=0; i<data.length; i++) { |
| final Vector v = data[i]; |
| ArgumentChecks.ensureNonNullElement("data", i, v); |
| final DataType t = DataType.forPrimitiveType(v.getElementType(), v.isUnsigned()); |
| if (dataType == null) { |
| dataType = t; |
| } else if (dataType != t) { |
| throw new RasterFormatException(Resources.format(Resources.Keys.MismatchedDataType)); |
| } |
| buffers[i] = v.buffer().orElseThrow(UnsupportedOperationException::new); |
| } |
| setData(dataType, buffers); |
| } |
| |
| /** |
| * Specifies the offsets to add to sample index in each band in order to reach the sample value in the {@link DataBuffer} bank. |
| * This method should be invoked when the data given to {@code setData(…)} contains only one {@link Vector}, {@link Buffer} or |
| * {@link DataBuffer} bank, and the bands in that unique bank are interleaved. |
| * |
| * <h4>Example</h4> |
| * For an image having three bands named Red (R), Green (G) and Blue (B), if the sample values are stored in a single bank in a |
| * R₀,G₀,B₀, R₁,G₁,B₁, R₂,G₂,B₂, R₃,G₃,B₃, <i>etc.</i> fashion, then this method should be invoked as below: |
| * |
| * {@snippet lang="java" : |
| * setInterleavedPixelOffsets(3, new int[] {0, 1, 2}); |
| * } |
| * |
| * @param pixelStride the number of data elements between each pixel in the data vector or buffer. |
| * @param bandOffsets offsets to add to sample index in each band. This is typically {0, 1, 2, …}. |
| * The length of this array shall be equal to {@link #getNumBands()}. |
| */ |
| public void setInterleavedPixelOffsets(final int pixelStride, final int[] bandOffsets) { |
| ArgumentChecks.ensureStrictlyPositive("pixelStride", pixelStride); |
| ensureExpectedBandCount(bandOffsets.length, false); |
| this.strideFactor = pixelStride; |
| this.bandOffsets = bandOffsets.clone(); |
| } |
| |
| /** |
| * Specifies the band to use for defining pixel colors when the image is displayed on screen. |
| * All other bands, if any, will exist in the raster but be ignored at display time. |
| * The default value is 0, the first (and often only) band. |
| * |
| * <h4>Implementation note</h4> |
| * An {@link java.awt.image.IndexColorModel} will be used for displaying the image. |
| * |
| * @param band the band to use for display purpose. |
| * @throws IllegalArgumentException if the given band is not between 0 (inclusive) |
| * and {@link #getNumBands()} (exclusive). |
| * |
| * @see org.apache.sis.image.Colorizer.Target#getVisibleBand() |
| * |
| * @since 1.2 |
| */ |
| public void setVisibleBand(final int band) { |
| ArgumentChecks.ensureBetween("band", 0, getNumBands() - 1, band); |
| visibleBand = band; |
| } |
| |
| /** |
| * Specifies the colors to apply for each category in a sample dimension. |
| * The given function can return {@code null} for unrecognized categories. |
| * If this method is never invoked, or if a category is unrecognized, |
| * then the default is a grayscale for |
| * {@linkplain Category#isQuantitative() quantitative categories} and |
| * transparent for qualitative categories (typically "no data" values). |
| * |
| * <h4>Example</h4> |
| * The following code specifies a color palette from blue to red with white in the middle. |
| * This is useful for data with a clear 0 (white) in the middle of the range, |
| * with a minimal value equals to the negative of the maximal value. |
| * |
| * {@snippet lang="java" : |
| * setCategoryColors((category) -> category.isQuantitative() ? new Color[] { |
| * Color.BLUE, Color.CYAN, Color.WHITE, Color.YELLOW, Color.RED} : null); |
| * } |
| * |
| * @param colors the colors to use for each category. The {@code colors} argument cannot be null, |
| * but {@code colors.apply(Category)} can return null. |
| * |
| * @since 1.2 |
| */ |
| public void setCategoryColors(final Function<Category,Color[]> colors) { |
| this.colors = Objects.requireNonNull(colors); |
| } |
| |
| /** |
| * Creates a raster with the data specified by the last call to a {@code setData(…)} method. |
| * The raster upper-left corner is located at the position given by {@link #getBounds()}. |
| * The returned raster is often an instance of {@link WritableRaster}, but read-only rasters are also allowed. |
| * |
| * @return the raster, usually (but not necessarily) an instance of {@link WritableRaster}. |
| * @throws IllegalStateException if no {@code setData(…)} method has been invoked before this method call. |
| * @throws RasterFormatException if a call to a {@link Raster} factory method failed. |
| * @throws ArithmeticException if a property of the raster to construct exceeds the capacity of 32 bits integers. |
| * |
| * @since 1.1 |
| */ |
| public Raster createRaster() { |
| if (buffer == null) { |
| throw new IllegalStateException(Resources.format(Resources.Keys.UnspecifiedRasterData)); |
| } |
| final boolean isInterleaved = (buffer.getNumBanks() == 1); |
| if (bandOffsets == null) { |
| strideFactor = isInterleaved ? getNumBands() : 1; |
| } |
| final int ls = multiplyExact(scanlineStride, strideFactor); // Real scanline stride. |
| final int ps = pixelStride * strideFactor; // Cannot fail if above operation did not fail. |
| /* |
| * Number of data elements from the first element of the bank to the first sample of the band. |
| * This is usually 0 for all bands, unless the upper-left corner (minX, minY) is not (0,0). |
| */ |
| final int[] offsets = new int[getNumBands()]; |
| Arrays.fill(offsets, toIntExact(addExact(addExact( |
| multiplyExact(offsetX, ps), |
| multiplyExact(offsetY, ls)), |
| offsetZ))); |
| /* |
| * Add the offset specified by the user (if any), or the default offset. The default is 0, 1, 2… |
| * for interleaved sample model (all bands in one bank) and 0, 0, 0… for banded sample model. |
| */ |
| if (bandOffsets != null || isInterleaved) { |
| for (int i=0; i<offsets.length; i++) { |
| offsets[i] = addExact(offsets[i], (bandOffsets != null) ? bandOffsets[i] : i); |
| } |
| } |
| final Point location = new Point(imageX, imageY); |
| return RasterFactory.createRaster(buffer, width, height, ps, ls, bankIndices, offsets, location); |
| } |
| |
| /** |
| * Creates an image with the data specified by the last call to a {@code setData(…)} method. |
| * The image upper-left corner is located at the position given by {@link #getBounds()}. |
| * The two-dimensional {@linkplain #getImageGeometry(int) image geometry} is stored as |
| * a property associated to the {@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key. |
| * The sample dimensions are stored as a property associated to the |
| * {@value org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY} key. |
| * |
| * <p>The default implementation returns an instance of {@link java.awt.image.WritableRenderedImage} |
| * if the {@link #createRaster()} return value is an instance of {@link WritableRaster}, |
| * or a read-only {@link RenderedImage} otherwise.</p> |
| * |
| * @return the image. |
| * @throws IllegalStateException if no {@code setData(…)} method has been invoked before this method call. |
| * @throws RasterFormatException if a call to a {@link Raster} factory method failed. |
| * @throws ArithmeticException if a property of the image to construct exceeds the capacity of 32 bits integers. |
| * |
| * @since 1.1 |
| */ |
| @SuppressWarnings("UseOfObsoleteCollectionType") |
| public RenderedImage createImage() { |
| final Raster raster = createRaster(); |
| final var colorizer = new ColorModelBuilder(colors, null, false); |
| final ColorModel colors; |
| final SampleModel sm = raster.getSampleModel(); |
| if (colorizer.initialize(sm, bands[visibleBand]) || colorizer.initialize(sm, visibleBand)) { |
| colors = colorizer.createColorModel(buffer.getDataType(), bands.length, visibleBand); |
| } else { |
| colors = ColorModelBuilder.NULL_COLOR_MODEL; |
| } |
| SliceGeometry supplier = null; |
| if (imageGeometry == null) { |
| if (imageUseSameGeometry(GridCoverage2D.BIDIMENSIONAL)) { |
| imageGeometry = geometry; |
| } else { |
| supplier = new SliceGeometry(geometry, sliceExtent, gridDimensions, mtFactory); |
| } |
| } |
| final WritableRaster wr = (raster instanceof WritableRaster) ? (WritableRaster) raster : null; |
| if (wr != null && colors != null && (imageX | imageY) == 0) { |
| return new Untiled(colors, wr, properties, imageGeometry, supplier, bands); |
| } |
| if (properties == null) { |
| properties = new Hashtable<>(); |
| } |
| properties.putIfAbsent(GRID_GEOMETRY_KEY, (supplier != null) ? new DeferredProperty(supplier) : imageGeometry); |
| properties.putIfAbsent(SAMPLE_DIMENSIONS_KEY, bands); |
| if (wr != null) { |
| return new WritableTiledImage(properties, colors, width, height, 0, 0, wr); |
| } else { |
| return new TiledImage(properties, colors, width, height, 0, 0, raster); |
| } |
| } |
| |
| /** |
| * A {@link BufferedImage} which will compute the {@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} |
| * property when first needed. We use this class even when the property value is known in advance because it |
| * has the desired side-effect of not letting {@link #getSubimage(int, int, int, int)} inherit that property. |
| * The use of a {@link BufferedImage} subclass is desired because Java2D rendering pipeline has optimizations |
| * in the form {@code if (image instanceof BufferedImage)}. |
| */ |
| private static final class Untiled extends ObservableImage { |
| /** |
| * The value associated to the {@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key, |
| * or {@code null} if not yet computed. |
| */ |
| private GridGeometry geometry; |
| |
| /** |
| * The object to use for computing {@link #geometry}, or {@code null} if not needed. |
| * This field is cleared after {@link #geometry} has been computed. |
| */ |
| private SliceGeometry supplier; |
| |
| /** |
| * The value associated to the {@value org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY} key. |
| */ |
| private final SampleDimension[] bands; |
| |
| /** |
| * Creates a new buffered image wrapping the given raster. |
| */ |
| @SuppressWarnings("UseOfObsoleteCollectionType") |
| Untiled(final ColorModel colors, final WritableRaster raster, final Hashtable<?,?> properties, |
| final GridGeometry geometry, final SliceGeometry supplier, final SampleDimension[] bands) |
| { |
| super(colors, raster, false, properties); |
| this.geometry = geometry; |
| this.supplier = supplier; |
| this.bands = bands; |
| } |
| |
| /** |
| * Returns the names of properties that this image can provide. |
| */ |
| @Override |
| public String[] getPropertyNames() { |
| return ArraysExt.concatenate(super.getPropertyNames(), |
| new String[] {GRID_GEOMETRY_KEY, SAMPLE_DIMENSIONS_KEY}); |
| } |
| |
| /** |
| * Returns the property associated to the given key. |
| * If the key is {@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}, |
| * then the {@link GridGeometry} will be computed when first needed. |
| * |
| * @throws ImagingOpException if the property value cannot be computed. |
| */ |
| @Override |
| public Object getProperty(final String key) { |
| switch (key) { |
| default: return super.getProperty(key); |
| case SAMPLE_DIMENSIONS_KEY: return bands.clone(); |
| case GRID_GEOMETRY_KEY: { |
| synchronized (this) { |
| if (geometry == null) { |
| final SliceGeometry s = supplier; |
| if (s != null) { |
| supplier = null; // Let GC do its work. |
| geometry = s.apply(this); |
| } |
| } |
| return geometry; |
| } |
| } |
| } |
| } |
| } |
| } |