blob: d4de192ee516a0d9ca91e573e5bc8075c2a03798 [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.Arrays;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.text.NumberFormat;
import java.text.FieldPosition;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import static java.lang.Math.min;
import static java.lang.Math.addExact;
import static java.lang.Math.subtractExact;
import static java.lang.Math.toIntExact;
import org.opengis.metadata.spatial.DimensionNameType;
import org.opengis.util.NameFactory;
import org.opengis.util.InternationalString;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.MathTransform1D;
import org.apache.sis.image.DataType;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.privy.ImageUtilities;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Debug;
import org.apache.sis.util.iso.DefaultNameFactory;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.coordinate.MismatchedDimensionException;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
/**
* Basic access to grid data values backed by a two-dimensional {@link RenderedImage}.
* While images are two-dimensional, the coverage <em>envelope</em> may have more dimensions.
* In other words the rendered image can be a two-dimensional slice in a <var>n</var>-dimensional space.
* The only restriction is that the {@linkplain GridGeometry#getExtent() grid extent} has a
* {@linkplain GridExtent#getSize(int) size} equals to 1 in all dimensions except two of them.
*
* <h2>Example</h2>
* A remote sensing image may be valid only over some time range
* (the temporal period of the satellite passing over observed area).
* Envelopes for such grid coverage can have three dimensions:
* the two usual ones (horizontal extent along <var>x</var> and <var>y</var>),
* and a third dimension for start time and end time (temporal extent along <var>t</var>).
* This "two-dimensional" grid coverage can have any number of columns along <var>x</var> axis
* and any number of rows along <var>y</var> axis, but only one plan along <var>t</var> axis.
* This single plan can have a lower bound (the start time) and an upper bound (the end time).
*
* <h2>Image size and location</h2>
* The {@linkplain RenderedImage#getWidth() image width} and {@linkplain RenderedImage#getHeight() height}
* must be equal to the {@linkplain GridExtent#getSize(int) grid extent size} in the two dimensions of the slice.
* However, the image origin ({@linkplain RenderedImage#getMinX() minimal x} and {@linkplain RenderedImage#getMinY() y}
* values) does not need to be equal to the {@linkplain GridExtent#getLow(int) grid extent low values};
* a translation will be applied as needed.
*
* <h2>Image bands</h2>
* Each band in an image is represented as a {@link SampleDimension}.
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Alexis Manin (Geomatys)
* @version 1.4
* @since 1.1
*/
public class GridCoverage2D extends GridCoverage {
/**
* A constant for identifying code that relying on having 2 dimensions.
* This is the minimal number of dimension required for this coverage.
*/
static final int BIDIMENSIONAL = 2;
/**
* The sample values stored as a {@code RenderedImage}.
*/
private final RenderedImage data;
/**
* Offsets to apply for converting grid coverage coordinates to image pixel coordinates.
* This is {@link RenderedImage#getMinX()} − <code>{@linkplain GridExtent#getLow(int)
* GridExtent.getLow}({@linkplain #xDimension})</code> for the <var>x</var> offset
* and a similar formula for the <var>y</var> offset.
*/
private final long gridToImageX, gridToImageY;
/**
* Indices of extent dimensions corresponding to image <var>x</var> and <var>y</var> coordinates.
* Typical values are 0 for {@code xDimension} and 1 for {@code yDimension}, but different values
* are allowed.
*/
private final int xDimension, yDimension;
/**
* The two-dimensional components of the coordinate reference system and "grid to CRS" transform.
* This is derived from {@link #gridGeometry} when first needed, retaining only the components at
* dimension indices {@link #xDimension} and {@link #yDimension}. The same {@link AtomicReference}
* instance may be shared with {@link #convertedView} and {@link #packedView}.
*
* @see #getGridGeometry2D()
*/
private final AtomicReference<GridGeometry> gridGeometry2D;
/**
* Creates a new grid coverage for the conversion of specified source coverage.
*
* @param source the coverage containing source values.
* @param range the sample dimensions to assign to the converted grid coverage.
* @param converters conversion from source to converted coverage, one transform per band.
* @param isConverted whether this grid coverage is for converted or packed values.
*/
private GridCoverage2D(final GridCoverage2D source, final List<SampleDimension> range,
final MathTransform1D[] converters, final boolean isConverted)
{
super(source.gridGeometry, range);
final DataType bandType = ConvertedGridCoverage.getBandType(range, isConverted, source);
data = convert(source.data, bandType, converters, Lazy.PROCESSOR);
gridToImageX = source.gridToImageX;
gridToImageY = source.gridToImageY;
xDimension = source.xDimension;
yDimension = source.yDimension;
gridGeometry2D = source.gridGeometry2D;
}
/**
* Creates a new grid coverage for the resampling of specified source coverage.
*
* @param source the coverage containing source values.
* @param domain the grid extent, CRS and conversion from cell indices to CRS.
* @param extent the {@code domain.getExtent()} value.
* @param data the sample values as a {@link RenderedImage}, with one band for each sample dimension.
*/
GridCoverage2D(final GridCoverage source, final GridGeometry domain, final GridExtent extent, RenderedImage data) {
super(source, domain);
final int[] imageAxes = extent.getSubspaceDimensions(BIDIMENSIONAL);
xDimension = imageAxes[0];
yDimension = imageAxes[1];
this.data = data = unwrapIfSameSize(data);
gridToImageX = subtractExact(data.getMinX(), extent.getLow(xDimension));
gridToImageY = subtractExact(data.getMinY(), extent.getLow(yDimension));
gridGeometry2D = new AtomicReference<>();
}
/**
* Constructs a grid coverage using the same domain and range than the given coverage, but different data.
* This constructor can be used when new data have been computed by an image processing operation,
* but each pixel of the result have the same coordinates and the same units of measurement
* than in the source coverage.
*
* @param source the coverage from which to copy grid geometry and sample dimensions.
* @param data the sample values as a {@link RenderedImage}, with one band for each sample dimension.
* @throws IllegalGridGeometryException if the image size is not consistent with the grid geometry.
* @throws IllegalArgumentException if the image number of bands is not the same as the number of sample dimensions.
*
* @since 1.2
*/
public GridCoverage2D(final GridCoverage source, RenderedImage data) {
super(source, source.getGridGeometry());
this.data = data = unwrapIfSameSize(Objects.requireNonNull(data));
final GridExtent extent = gridGeometry.getExtent();
final int[] imageAxes;
if (source instanceof GridCoverage2D) {
final GridCoverage2D gs = (GridCoverage2D) source;
xDimension = gs.xDimension;
yDimension = gs.yDimension;
gridToImageX = gs.gridToImageX;
gridToImageY = gs.gridToImageY;
gridGeometry2D = gs.gridGeometry2D;
imageAxes = new int[] {xDimension, yDimension};
} else {
imageAxes = extent.getSubspaceDimensions(BIDIMENSIONAL);
xDimension = imageAxes[0];
yDimension = imageAxes[1];
gridToImageX = subtractExact(data.getMinX(), extent.getLow(xDimension));
gridToImageY = subtractExact(data.getMinY(), extent.getLow(yDimension));
gridGeometry2D = new AtomicReference<>();
}
verifyImageSize(extent, data, imageAxes);
verifyBandCount(super.getSampleDimensions(), data);
}
/**
* Constructs a grid coverage using the specified domain, range and data. If the given domain does not
* have an extent, then a default {@link GridExtent} will be computed from given image. Otherwise the
* {@linkplain RenderedImage#getWidth() image width} and {@linkplain RenderedImage#getHeight() height}
* must be equal to the {@linkplain GridExtent#getSize(int) grid extent size} in the two dimensions of
* the slice.
*
* <p>The image origin ({@linkplain RenderedImage#getMinX() minimal x} and {@linkplain RenderedImage#getMinY() y}
* values) can be anywhere; it does not need to be the same as the {@linkplain GridExtent#getLow(int) grid extent
* low values}. Translations will be applied automatically when needed.</p>
*
* <p>This constructor throws an {@link IllegalGridGeometryException} if one
* of the following errors is detected in the {@code domain} argument:</p>
* <ul>
* <li>The given domain has less than two dimensions.</li>
* <li>The given domain has more than two dimensions having an
* {@linkplain GridExtent#getSize(int) extent size} greater than 1.</li>
* <li>The extent size along <var>x</var> and <var>y</var> axes is not equal to the image width and height.</li>
* </ul>
*
* @param domain the grid extent (may be absent), CRS and conversion from cell indices.
* If {@code null} a default grid geometry will be created with no CRS and identity conversion.
* @param range sample dimensions for each image band. The size of this list must be equal to the number of bands.
* If {@code null}, default sample dimensions will be created with no transfer function.
* @param data the sample values as a {@link RenderedImage}, with one band for each sample dimension.
* @throws IllegalGridGeometryException if the {@code domain} does not met the above-documented conditions.
* @throws IllegalArgumentException if the image number of bands is not the same as the number of sample dimensions.
* @throws ArithmeticException if the distance between grid location and image location exceeds the {@code long} capacity.
*
* @see GridCoverageBuilder
*/
public GridCoverage2D(GridGeometry domain, final List<? extends SampleDimension> range, RenderedImage data) {
/*
* The complex nesting of method calls below is a workaround
* while waiting for JEP 447: Statements before super(…).
*/
super(domain = addExtentIfAbsent(domain, data = unwrapIfSameSize(data)),
defaultIfAbsent(range, data, ImageUtilities.getNumBands(data)));
this.data = Objects.requireNonNull(data);
/*
* Find indices of the two dimensions of the slice. Those dimensions are usually 0 for x and 1 for y,
* but not necessarily. A two dimensional CRS will be extracted for those dimensions later if needed.
*/
final GridExtent extent = domain.getExtent();
final int[] imageAxes;
try {
imageAxes = extent.getSubspaceDimensions(BIDIMENSIONAL);
} catch (CannotEvaluateException e) {
throw new IllegalGridGeometryException(e.getMessage(), e);
}
xDimension = imageAxes[0];
yDimension = imageAxes[1];
gridToImageX = subtractExact(data.getMinX(), extent.getLow(xDimension));
gridToImageY = subtractExact(data.getMinY(), extent.getLow(yDimension));
verifyImageSize(extent, data, imageAxes);
verifyBandCount(range, data);
gridGeometry2D = new AtomicReference<>();
}
/**
* Returns the wrapped image if the only difference is a translation, or {@code data} otherwise.
*/
private static RenderedImage unwrapIfSameSize(RenderedImage data) {
if (data instanceof ReshapedImage) {
final RenderedImage source = ((ReshapedImage) data).source;
if (source.getWidth() == data.getWidth() && source.getHeight() == data.getHeight()) {
data = source;
}
}
return data;
}
/**
* If the given domain does not have a {@link GridExtent}, creates a new grid geometry
* with an extent computed from the given image. The new grid will start at the same
* location than the image and will have the same size.
*
* @param domain the domain to complete. May be {@code null}.
* @param data user supplied image, or {@code null} if missing.
* @return the potentially completed domain (may be {@code null}).
*/
static GridGeometry addExtentIfAbsent(GridGeometry domain, final RenderedImage data) {
if (data != null) {
domain = addExtentIfAbsent(domain, ImageUtilities.getBounds(data));
}
return domain;
}
/**
* If the given domain does not have a {@link GridExtent}, creates a new grid geometry
* with an extent computed from the given image bounds. The new grid will start at the
* same location as the image and will have the same size.
*
* <p>This method does nothing if the given domain already has an extent;
* it does not verify that the extent is consistent with image size.
* This verification should be done by the caller.</p>
*
* @param domain the domain to complete. May be {@code null}.
* @param bounds image or raster bounds (cannot be {@code null}).
* @return the potentially completed domain (may be {@code null}).
*/
static GridGeometry addExtentIfAbsent(GridGeometry domain, final Rectangle bounds) {
if (domain == null) {
GridExtent extent = new GridExtent(bounds);
domain = new GridGeometry(extent, PixelInCell.CELL_CENTER, null, null);
} else if (!domain.isDefined(GridGeometry.EXTENT)) {
final int dimension = domain.getDimension();
if (dimension >= BIDIMENSIONAL) {
CoordinateReferenceSystem crs = null;
if (domain.isDefined(GridGeometry.CRS)) {
crs = domain.getCoordinateReferenceSystem();
}
final GridExtent extent = createExtent(dimension, bounds, crs);
if (domain.isDefined(GridGeometry.GRID_TO_CRS)) try {
domain = new GridGeometry(domain, extent, null);
} catch (TransformException e) {
throw new IllegalGridGeometryException(e); // Should never happen.
} else {
domain = new GridGeometry(extent, domain.envelope, GridOrientation.HOMOTHETY);
}
}
}
return domain;
}
/**
* Creates a grid extent with the low and high coordinates of the given image bounds.
* The coordinate reference system is used for extracting grid axis names, in particular
* the {@link DimensionNameType#VERTICAL} and {@link DimensionNameType#TIME} dimensions.
* The {@link DimensionNameType#COLUMN} and {@link DimensionNameType#ROW} dimensions can
* not be inferred from CRS analysis; they are added from knowledge that we have an image.
*
* @param dimension number of dimensions.
* @param bounds bounds of the image for which to create a grid extent.
* @param crs coordinate reference system, or {@code null} if none.
*/
private static GridExtent createExtent(final int dimension, final Rectangle bounds, final CoordinateReferenceSystem crs) {
final long[] low = new long[dimension];
final long[] high = new long[dimension];
low [0] = bounds.x;
low [1] = bounds.y;
high[0] = bounds.width + low[0] - 1; // Inclusive.
high[1] = bounds.height + low[1] - 1;
DimensionNameType[] axisTypes = GridExtent.typeFromAxes(crs, dimension);
if (axisTypes == null) {
axisTypes = new DimensionNameType[dimension];
}
if (!ArraysExt.contains(axisTypes, DimensionNameType.COLUMN)) axisTypes[0] = DimensionNameType.COLUMN;
if (!ArraysExt.contains(axisTypes, DimensionNameType.ROW)) axisTypes[1] = DimensionNameType.ROW;
return new GridExtent(axisTypes, low, high, true);
}
/**
* Verifies that the domain is consistent with image size.
* We do not verify image location; it can be anywhere.
*/
private static void verifyImageSize(final GridExtent extent, final RenderedImage data, final int[] imageAxes) {
for (int i=0; i<BIDIMENSIONAL; i++) {
final int imageSize = (i == 0) ? data.getWidth() : data.getHeight();
final long gridSize = extent.getSize(imageAxes[i]);
if (imageSize != gridSize) {
throw new IllegalGridGeometryException(Resources.format(Resources.Keys.MismatchedImageSize_3, i, imageSize, gridSize));
}
}
}
/**
* If the sample dimensions are null, creates default sample dimensions with default names.
* The default names are "gray", "red, green, blue" or "cyan, magenta, yellow" if the color
* model is identified as such, or numbers if the color model is not recognized.
*
* @param range the list of sample dimensions, potentially null.
* @param data the image for which to build sample dimensions, or {@code null}.
* @param numBands the number of bands in the given image, or 0 if none.
* @return the given list of sample dimensions if it was non-null, or a default list otherwise.
*/
static List<? extends SampleDimension> defaultIfAbsent(List<? extends SampleDimension> range,
final RenderedImage data, final int numBands)
{
if (range == null) {
final short[] names;
if (data != null) {
names = ImageUtilities.bandNames(data.getColorModel(), data.getSampleModel());
} else {
names = ArraysExt.EMPTY_SHORT;
}
final SampleDimension[] sd = new SampleDimension[numBands];
final NameFactory factory = DefaultNameFactory.provider();
for (int i=0; i<numBands; i++) {
final InternationalString name;
final short k;
if (i < names.length && (k = names[i]) != 0) {
name = Vocabulary.formatInternational(k);
} else {
name = Vocabulary.formatInternational(Vocabulary.Keys.Band_1, i+1);
}
sd[i] = new SampleDimension(factory.createLocalName(null, name), null, List.of());
}
range = Arrays.asList(sd);
}
return range;
}
/**
* Verifies that the number of bands in the image is equal to the number of sample dimensions.
* The number of bands is fetched from the sample model, which in theory shall never be null.
* However, this class has a little bit of tolerance to missing sample model.
* It may happen when the image is used only as a matrix storage.
*/
private static void verifyBandCount(final List<? extends SampleDimension> range, final RenderedImage data) {
if (range != null) {
final SampleModel sm = data.getSampleModel();
if (sm != null) {
final int nb = sm.getNumBands();
final int ns = range.size();
if (nb != ns) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedBandCount_2, nb, ns));
}
}
}
}
/**
* Returns the constant identifying the primitive type used for storing sample values.
*/
@Override
final DataType getBandType() {
return DataType.forBands(data);
}
/**
* Returns the two-dimensional part of this grid geometry.
* If the {@linkplain #getGridGeometry() complete geometry} is already two-dimensional,
* then this method returns the same geometry. Otherwise it returns a geometry for the two first
* axes having a {@linkplain GridExtent#getSize(int) size} greater than 1 in the grid envelope.
* Note that those axes are guaranteed to appear in the same order as in the complete geometry.
*
* @return the two-dimensional part of the grid geometry.
*
* @see #getGridGeometry()
* @see GridGeometry#selectDimensions(int[])
*/
public GridGeometry getGridGeometry2D() {
GridGeometry g = gridGeometry2D.get();
if (g == null) {
g = gridGeometry.selectDimensions(xDimension, yDimension);
if (!gridGeometry2D.compareAndSet(null, g)) {
GridGeometry other = gridGeometry2D.get();
if (other != null) return other;
}
}
return g;
}
/**
* Creates a grid coverage that contains real values or sample values,
* depending if {@code converted} is {@code true} or {@code false} respectively.
* This method is invoked by the default implementation of {@link #forConvertedValues(boolean)}
* when first needed.
*
* @param converted {@code true} for a coverage containing converted values,
* or {@code false} for a coverage containing packed values.
* @return a coverage containing converted or packed values, depending on {@code converted} argument value.
*/
@Override
protected GridCoverage createConvertedValues(final boolean converted) {
try {
final List<SampleDimension> sources = getSampleDimensions();
final List<SampleDimension> targets = new ArrayList<>(sources.size());
final MathTransform1D[] converters = ConvertedGridCoverage.converters(sources, targets, converted);
return (converters == null) ? this : new GridCoverage2D(this, targets, converters, converted);
} catch (NoninvertibleTransformException e) {
throw new CannotEvaluateException(e.getMessage(), e);
}
}
/**
* Creates a new function for computing or interpolating sample values at given locations.
*
* <h4>Multi-threading</h4>
* {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
* a new {@code Evaluator} instance should be created for each thread.
*
* @since 1.1
*/
@Override
public Evaluator evaluator() {
return new PixelAccessor();
}
/**
* Implementation of evaluator returned by {@link #evaluator()}.
*/
private final class PixelAccessor extends DefaultEvaluator {
/**
* Creates a new evaluator for the enclosing coverage.
*/
PixelAccessor() {
super(GridCoverage2D.this);
}
/**
* 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 as the coverage.
*/
@Override
public double[] apply(final DirectPosition point) throws CannotEvaluateException {
try {
final FractionalGridCoordinates gc = toGridPosition(point);
try {
final int x = toIntExact(addExact(gc.getCoordinateValue(xDimension), gridToImageX));
final int y = toIntExact(addExact(gc.getCoordinateValue(yDimension), gridToImageY));
return evaluate(data, x, y);
} catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) {
if (isNullIfOutside()) {
return null;
}
throw (PointOutsideCoverageException) new PointOutsideCoverageException(
gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex);
}
} catch (PointOutsideCoverageException ex) {
ex.setOffendingLocation(point);
throw ex;
} catch (RuntimeException | FactoryException | TransformException ex) {
throw new CannotEvaluateException(ex.getMessage(), ex);
}
}
}
/**
* Returns a grid data region as a rendered image. The {@code sliceExtent} argument
* specifies the area of interest and may be {@code null} for requesting the whole image.
* The coordinates given by {@link RenderedImage#getMinX()} and {@link RenderedImage#getMinY() getMinY()}
* will be the image location <em>relative to</em> the location specified in {@code sliceExtent}
* {@linkplain GridExtent#getLow(int) low coordinates} (see super-class javadoc for more discussion).
* The {@linkplain RenderedImage#getWidth() image width} and {@linkplain RenderedImage#getHeight() height} will be
* the {@code sliceExtent} {@linkplain GridExtent#getSize(int) sizes} if this method can honor exactly the request,
* but this method is free to return a smaller or larger image if doing so reduce the number of data to create or copy.
* This implementation returns a view as much as possible, without copying sample values.
*
* @param sliceExtent area of interest, or {@code null} for the whole image.
* @return the grid slice as a rendered image. Image location is relative to {@code sliceExtent}.
* @throws MismatchedDimensionException if the given extent does not have the same number of dimensions as this coverage.
* @throws DisjointExtentException if the given extent does not intersect this grid coverage.
* @throws CannotEvaluateException if this method cannot produce the rendered image for another reason.
*
* @see BufferedImage#getSubimage(int, int, int, int)
*/
@Override
@SuppressWarnings("AssertWithSideEffects")
public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException {
final GridExtent extent = gridGeometry.extent;
if (sliceExtent == null) {
if (extent == null || (data.getMinX() == 0 && data.getMinY() == 0)) {
return data;
}
sliceExtent = extent;
} else {
final int expected = gridGeometry.getDimension();
final int dimension = sliceExtent.getDimension();
if (expected != dimension) {
throw new org.opengis.geometry.MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_3, "sliceExtent", expected, dimension));
}
}
if (extent != null) {
final int n = min(sliceExtent.getDimension(), extent.getDimension());
for (int i=0; i<n; i++) {
if (sliceExtent.getHigh(i) < extent.getLow(i) || sliceExtent.getLow(i) > extent.getHigh(i)) {
throw new DisjointExtentException(extent, sliceExtent, i);
}
}
}
try {
/*
* Convert the coordinates from this grid coverage coordinate system to the image coordinate system.
* The coverage coordinates may require 64 bits integers, but after translation the (x,y) coordinates
* should be in 32 bits integers range. Do not cast to 32 bits now however; this will be done later.
*/
final long xmin = addExact(sliceExtent.getLow (xDimension), gridToImageX);
final long ymin = addExact(sliceExtent.getLow (yDimension), gridToImageY);
final long xmax = addExact(sliceExtent.getHigh(xDimension), gridToImageX);
final long ymax = addExact(sliceExtent.getHigh(yDimension), gridToImageY);
/*
* BufferedImage.getSubimage() returns a new image with upper-left coordinate at (0,0),
* which is exactly what this method contract is requesting provided that the requested
* upper-left point is inside the image.
*/
if (data instanceof BufferedImage) {
BufferedImage result = (BufferedImage) data;
/*
* BufferedImage origin should be (0, 0). But for consistency with image API,
* we consider it as variable.
*/
final long ix = result.getMinX();
final long iy = result.getMinY();
if (xmin >= ix && ymin >= iy) {
final int width = result.getWidth();
final int height = result.getHeight();
/*
* Result of `ix + width` requires at most 33 bits for any `ix` value (same for y axis).
* Subtractions by `xmin` and `ymin` never overflow if `ix` and `iy` are zero or positive,
* which should always be the case with BufferedImage. The +1 is applied after subtraction
* instead of on `xmax` and `ymax` for avoiding overflow, since the result of `min(…)`
* uses at most 33 bits.
*/
final int nx = toIntExact(min(xmax, ix + width - 1) - xmin + 1);
final int ny = toIntExact(min(ymax, iy + height - 1) - ymin + 1);
if ((xmin | ymin) != 0 || nx != width || ny != height) {
result = result.getSubimage(toIntExact(xmin), toIntExact(ymin), nx, ny);
}
/*
* Workaround for https://bugs.openjdk.java.net/browse/JDK-8166038
* If BufferedImage cannot be used, fallback on ReshapedImage
* at the cost of returning an image larger than necessary.
* This workaround can be removed on JDK17.
*/
if (org.apache.sis.coverage.privy.TilePlaceholder.PENDING_JDK_FIX) {
if (result.getTileGridXOffset() == ix && result.getTileGridYOffset() == iy) {
return result;
}
}
}
}
/*
* Return the backing image almost as-is (with potentially just a wrapper) for avoiding to copy data.
* As per method contract, we shall set the (x,y) location to the difference between requested region
* and actual region of the returned image. For example if the user requested an image starting at
* (5,5) but the image to return starts at (1,1), then we need to set its location to (-4,-4).
*
* Note: we could do a special case when the result has only one tile and create a BufferedImage
* with Raster.createChild(…), but that would force us to invoke RenderedImage.getTile(…) which
* may force data loading earlier than desired.
*/
final ReshapedImage result = new ReshapedImage(data, xmin, ymin, xmax, ymax);
return result.isIdentity() ? data : result;
} catch (ArithmeticException e) {
throw new CannotEvaluateException(e.getMessage(), e);
}
}
/**
* Appends a "data layout" branch (if it exists) to the tree representation of this coverage.
* That branch will be inserted between "coverage domain" and "sample dimensions" branches.
*
* @param root root of the tree where to add a branch.
* @param vocabulary localized resources for vocabulary.
* @param column the single column where to write texts.
*/
@Debug
@Override
void appendDataLayout(final TreeTable.Node root, final Vocabulary vocabulary, final TableColumn<CharSequence> column) {
final TreeTable.Node branch = root.newChild();
branch.setValue(column, vocabulary.getString(Vocabulary.Keys.ImageLayout));
final NumberFormat nf = NumberFormat.getIntegerInstance(vocabulary.getLocale());
final FieldPosition pos = new FieldPosition(0);
final StringBuffer buffer = new StringBuffer();
write: for (int item=0; ; item++) try {
switch (item) {
case 0: {
vocabulary.appendLabel(Vocabulary.Keys.Origin, buffer);
nf.format(data.getMinX(), buffer.append(' '), pos);
nf.format(data.getMinY(), buffer.append(", "), pos);
break;
}
case 1: {
final int tx = data.getTileWidth();
final int ty = data.getTileHeight();
if (tx == data.getWidth() && ty == data.getHeight()) continue;
vocabulary.appendLabel(Vocabulary.Keys.TileSize, buffer);
nf.format(tx, buffer.append( ' ' ), pos);
nf.format(ty, buffer.append(" × "), pos);
break;
}
case 2: {
final String type = ImageUtilities.getDataTypeName(data.getSampleModel());
if (type == null) continue;
vocabulary.appendLabel(Vocabulary.Keys.DataType, buffer);
buffer.append(' ').append(type);
break;
}
case 3: {
final short t = ImageUtilities.getTransparencyDescription(data.getColorModel());
if (t != 0) {
final String desc = Resources.forLocale(vocabulary.getLocale()).getString(t);
branch.newChild().setValue(column, desc);
}
continue;
}
default: break write;
}
branch.newChild().setValue(column, buffer.toString());
buffer.setLength(0);
} catch (IOException e) {
throw new UncheckedIOException(e); // Should never happen since we are writing to StringBuilder.
}
}
}