/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.coverage.io;
import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.Locale;
import java.util.Arrays;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import java.io.IOException;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import org.geotoolkit.image.io.large.LargeRenderedImage;
import org.opengis.geometry.Envelope;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.coverage.grid.RectifiedGrid;
import org.opengis.metadata.spatial.Georectified;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.util.GenericName;
import org.opengis.util.NameFactory;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.grid.GeneralGridEnvelope;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.grid.GridCoverageBuilder;
import org.geotoolkit.image.io.DimensionSlice;
import org.geotoolkit.image.io.ImageMetadataException;
import org.geotoolkit.image.io.NamedImageStore;
import org.geotoolkit.image.io.SampleConversionType;
import org.geotoolkit.image.io.SpatialImageReadParam;
import org.geotoolkit.image.io.SpatialImageReader;
import org.geotoolkit.image.io.XImageIO;
import org.geotoolkit.image.io.metadata.MetadataHelper;
import org.geotoolkit.image.io.metadata.SampleDimension;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.image.io.metadata.SpatialMetadataFormat;
import org.geotoolkit.image.io.mosaic.MosaicImageReader;
import org.geotoolkit.image.io.mosaic.MosaicImageReadParam;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.internal.image.io.CheckedImageInputStream;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.util.collection.XCollections;
import org.apache.sis.util.collection.BackingStoreException;
import org.geotoolkit.referencing.crs.PredefinedCRS;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import static org.geotoolkit.image.io.MultidimensionalImageStore.*;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
import org.geotoolkit.internal.image.io.DimensionAccessor;
import org.apache.sis.geometry.Envelopes;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A {@link GridCoverageReader} implementation which use an {@link ImageReader} for reading
* sample values. This implementation stores the sample values in a {@link RenderedImage},
* and consequently is targeted toward two-dimensional slices of data.
* <p>
* {@code ImageCoverageReader} basically works as a layer which converts <cite>geodetic
* coordinates</cite> (for example the region to read) to <cite>pixel coordinates</cite>
* before to pass them to the wrapped {@code ImageReader}, and conversely: from pixel
* coordinates to geodetic coordinates. The later conversion is called "<cite>grid to CRS</cite>"
* and is determined from the {@link SpatialMetadata} provided by the {@code ImageReader}.
*
* {@section Closing the input stream}
* An {@linkplain ImageInputStream Image Input Stream} may be created automatically from various
* input types like {@linkplain java.io.File} or {@linkplain java.net.URL}. That input stream is
* <strong>not</strong> closed after a read operation, because many consecutive read operations
* may be performed for the same input. To ensure that the automatically generated input stream
* is closed, user shall invoke the {@link #setInput(Object)} method with a {@code null} input,
* or invoke the {@link #reset()} or {@link #dispose()} methods.
* <p>
* Note that input streams explicitly given by the users are never closed. It is caller
* responsibility to close them.
*
* {@section Default metadata value}
* If no {@linkplain CoordinateReferenceSystem Coordinate Reference System} or no
* <cite>grid to CRS</cite> {@linkplain MathTransform Math Transform} can be created
* from the {@link SpatialMetadata}, then the default values listed below are used:
* <p>
* <table border="1" cellspacing="0">
* <tr bgcolor="lightblue"><th>Type</th><th>Default value</th></tr>
* <tr><td> {@link CoordinateReferenceSystem} </td>
* <td> {@link DefaultImageCRS#GRID_2D} </td></tr>
* <tr><td> {@link MathTransform} </td>
* <td> {@link MathTransforms#identity(int)} with the CRS dimension </td></tr>
* </table>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
* @version 3.21
*
* @since 3.09 (derived from 2.2)
* @module
*/
public class ImageCoverageReader extends GridCoverageReader {
/**
* The name of metadata nodes we are interested in. Some implementations of
* {@link ImageReader} may use this information for reading only the metadata
* we are interested in.
*
* @see SpatialMetadataFormat
*/
private static final Set<String> METADATA_NODES;
static {
final Set<String> s = new HashSet<>(25);
// geotk-coverageio
// ├───ImageDescription
s.add("Dimensions"); // │ ├───Dimensions
s.add("Dimension"); // │ │ └───Dimension
s.add("RangeElementDescriptions"); // │ └───RangeElementDescriptions
s.add("RangeElementDescription"); // │ └───RangeElementDescription
s.add("SpatialRepresentation"); // ├───SpatialRepresentation
s.add("RectifiedGridDomain"); // └───RectifiedGridDomain
s.add("Limits"); // ├───Limits
s.add("OffsetVectors"); // ├───OffsetVectors
s.add("OffsetVector"); // │ └───OffsetVector
s.add("CoordinateReferenceSystem"); // └───CoordinateReferenceSystem
s.add("CoordinateSystem"); // ├───CoordinateSystem
s.add("Axes"); // │ └───Axes
s.add("CoordinateSystemAxis"); // │ └───CoordinateSystemAxis
s.add("Datum"); // ├───Datum
s.add("Ellipsoid"); // │ ├───Ellipsoid
s.add("PrimeMeridian"); // │ └───PrimeMeridian
s.add("Conversion"); // └───Conversion
s.add("Parameters"); // └───Parameters
s.add("ParameterValue"); // └───ParameterValue
METADATA_NODES = Collections.unmodifiableSet(s);
}
/**
* The {@link ImageReader} to use for decoding {@link RenderedImage}s. This reader is
* initially {@code null} and lazily created the first time {@link #setInput(Object)}
* is invoked. Once created, it is reused for subsequent inputs if possible.
*/
protected ImageReader imageReader;
/**
* Optional parameter to be given (if non-null) to the image reader
* {@link ImageReader#setInput(Object, boolean, boolean) setInput} method.
*
* If {@code TRUE}, images and metadata may only be read in ascending order from the input
* source. If {@code FALSE}, they may be read in any order. If {@code null}, then this
* parameter is not given to the {@linkplain #imageReader image reader} which is free to
* use a plugin-dependent default (usually {@code false}).
*/
protected Boolean seekForwardOnly;
/**
* Optional parameter to be given (if non-null) to the image reader
* {@link ImageReader#setInput(Object, boolean, boolean) setInput} method.
*
* If {@code TRUE}, metadata may be ignored during reads. If {@code FALSE}, metadata will be
* parsed. If {@code null}, then this parameter is not given to the {@linkplain #imageReader
* image reader} which is free to use a plugin-dependent default (usually {@code false}).
*/
protected Boolean ignoreMetadata;
/**
* The names of coverages, or {@code null} if not yet determined.
* This is created by {@link #getCoverageNames()} when first needed.
*/
private transient List<? extends GenericName> coverageNames;
/**
* The value returned by {@link #getGridGeometry(int)}, computed when first needed.
*/
private transient Map<Integer,GridGeometry2D> gridGeometries;
/**
* The value returned by {@link #getSampleDimensions(int)}, computed when first needed. By
* convention, an empty list means that we already checked for bands and didn't found any
* having a {@code SampleDimension} description, in which case {@link #getSampleDimensions(int)}
* shall returns {@code null}. We use this convention because a coverage having zero bands
* should not be valid.
*/
private transient Map<Integer,List<GridSampleDimension>> sampleDimensions;
/**
* The metadata for the image at index {@link #imageMetadataIndex}, cached for avoiding to
* compute it many time. Note that {@code null} if a valid value - we need to check the image
* index in order to determine if the value is valid.
*
* @see #getImageMetadata(ImageReader, int)
*/
private transient SpatialMetadata imageMetadata;
/**
* The image index of {@link #imageMetadata}, or -1 if not yet computed.
*
* @see #getImageMetadata(ImageReader, int)
*/
private transient int imageMetadataIndex;
/**
* Helper utilities for parsing metadata. Created only when needed.
*/
private transient MetadataHelper helper;
/**
* The grid coverage builder to use for building {@link GridCoverage2D} instances.
*
* @since 3.21
*/
private final GridCoverageBuilder coverageBuilder;
/**
* The name factory to use for building {@link GenericName} instances.
* This factory can be specified at construction time in the {@link Hints} map.
*
* @since 3.20
*/
protected final NameFactory nameFactory;
/**
* Creates a new instance using the default
* {@linkplain GridCoverageFactory grid coverage factory}.
*/
public ImageCoverageReader() {
this(null);
}
/**
* Creates a new instance using the {@linkplain GridCoverageFactory grid coverage factory}
* specified by the given set of hints.
*
* @param hints The hints to use for fetching a {@link GridCoverageFactory},
* or {@code null} for the default hints.
*/
public ImageCoverageReader(final Hints hints) {
coverageBuilder = new GridCoverageBuilder(hints);
nameFactory = FactoryFinder.getNameFactory(hints);
imageMetadataIndex = -1;
}
/**
* Sets the logging level to use for read operations. If the {@linkplain #imageReader image
* reader} implements the {@link org.geotoolkit.util.logging.LogProducer} interface, then it
* is also set to the given level.
*
* @since 3.15
*/
@Override
public void setLogLevel(final Level level) {
super.setLogLevel(level);
copyLevel(imageReader);
}
/**
* {@inheritDoc}
* <p>
* The given locale will also be given to the wrapped {@linkplain #imageReader image reader},
* providing that the image reader supports the locale language. If it doesn't, then the image
* reader locale is set to {@code null}.
*/
@Override
public void setLocale(final Locale locale) {
super.setLocale(locale);
setLocale(imageReader, locale);
helper = null;
}
/**
* Sets the given locale to the given {@link ImageReader}, provided that the image reader
* supports the language of that locale. Otherwise sets the reader locale to {@code null}.
*
* @see ImageReader#setLocale(Locale)
*/
private static void setLocale(final ImageReader reader, final Locale locale) {
if (reader != null) {
reader.setLocale(select(locale, reader.getAvailableLocales()));
}
}
/**
* Sets the input source to the given object. The input is typically a
* {@link java.nio.file.Path}, {@link java.io.File}, {@link java.net.URL} or {@link String} object,
* but other types (especially {@link ImageInputStream}) may be accepted
* as well depending on the {@linkplain #imageReader image reader} implementation.
* <p>
* The given input can also be an {@link ImageReader} instance with its input initialized,
* in which case it is used directly as the {@linkplain #imageReader image reader} wrapped
* by this {@code ImageCoverageReader}.
*
* {@section Implementation note}
* This method ensures that the {@link #imageReader} field is set to a suitable
* {@link ImageReader} instance. This is done by invoking the following methods,
* which can be overridden by subclasses:
* <p>
* <ol>
* <li>If the current {@link #imageReader} is non-null, invoke
* {@link #canReuseImageReader(ImageReaderSpi, Object)} for determining
* if it can be reused for the new input.</li>
* <li>If the current {@code imageReader} was null or if the above method call
* returned {@code false}, invoke {@link #createImageReader(Object)} for creating
* a new {@link ImageReader} instance for the given input.</li>
* </ol>
* <p>
* Then this method {@linkplain ImageReader#setInput(Object, boolean, boolean) sets the input}
* of the {@link #imageReader} instance, if it was not already done by the above method calls.
*/
@Override
public void setInput(final Object input) throws CoverageStoreException {
final ImageReader oldReader = imageReader;
try {
reset();
assert (oldReader == null) || (oldReader.getInput() == null) : oldReader;
if (input != null) {
ImageReader newReader = null;
if (input instanceof ImageReader) {
newReader = (ImageReader) input;
// The old reader will be disposed and the locale will be set below.
} else {
/*
* First, check if the current reader can be reused. If the user
* didn't overridden the canReuseImageReader(...) method, then the
* default implementation is to look at the file extension.
*/
if (oldReader != null) {
final ImageReaderSpi provider = oldReader.getOriginatingProvider();
if (provider != null && canReuseImageReader(provider, input)) {
newReader = oldReader;
}
}
/*
* If we can't reuse the old reader, create a new one. If the user didn't
* overridden the createImageReader(...) method, then the default behavior
* is to get an image reader by the extension.
*/
if (newReader == null) {
newReader = createImageReader(input);
}
/*
* Set the input if it was not already done. In the default implementation,
* this is done by 'createImageReader' but not by 'canReuseImageReader'.
* However the user could have overridden the above-cited methods with a
* different behavior.
*/
if (newReader != input && newReader.getInput() == null) {
Object imageInput = input;
final ImageReaderSpi provider = newReader.getOriginatingProvider();
if (provider != null) {
boolean needStream = false;
for (final Class<?> inputType : provider.getInputTypes()) {
if (inputType.isInstance(imageInput)) {
needStream = false;
break;
}
if (inputType.isAssignableFrom(ImageInputStream.class)) {
needStream = true;
// Do not break - maybe the input type is accepted later.
}
}
if (needStream) {
imageInput = ImageIO.createImageInputStream(input);
assert CheckedImageInputStream.isValid((ImageInputStream) (imageInput =
CheckedImageInputStream.wrap((ImageInputStream) imageInput)));
if (imageInput == null) {
final short messageKey;
final Object argument;
if (IOUtilities.canProcessAsPath(input)) {
messageKey = Errors.Keys.CantReadFile_1;
argument = IOUtilities.filename(input);
} else {
messageKey = Errors.Keys.UnknownType_1;
argument = input.getClass();
}
throw new CoverageStoreException(Errors.getResources(locale).getString(messageKey, argument));
}
}
}
if (seekForwardOnly != null) {
if (ignoreMetadata != null) {
newReader.setInput(imageInput, seekForwardOnly, ignoreMetadata);
} else {
newReader.setInput(imageInput, seekForwardOnly);
}
} else {
newReader.setInput(imageInput);
}
}
}
if (newReader != oldReader) {
if (oldReader != null) {
oldReader.dispose();
}
copyLevel(newReader);
setLocale(newReader, locale);
if (LOGGER.isLoggable(getFineLevel())) {
ImageCoverageStore.logCodecCreation(this, ImageCoverageReader.class,
newReader, newReader.getOriginatingProvider());
}
}
imageReader = newReader;
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(input, e, false), e);
}
super.setInput(input);
}
/**
* Returns {@code true} if the image reader created by the given provider can be reused.
* This method is invoked automatically by {@link #setInput(Object)} for determining if
* the current {@linkplain #imageReader image reader} can be reused for reading the given
* input.
* <p>
* The default implementation checks if the suffix of the given input is one of the
* {@linkplain ImageReaderSpi#getFileSuffixes() file suffixes known to the provider}.
* If the given object has no suffix (for example if it is an instance of
* {@link ImageInputStream}), then this method fallbacks on
* {@link ImageReaderSpi#canDecodeInput(Object)}.
* <p>
* Subclasses can override this method if they want to determine in another way
* whatever the {@linkplain #imageReader image reader} can be reused. Subclasses
* don't need to set the image reader input; this will be done by the caller.
*
* @param provider The provider of the image reader.
* @param input The input to set to the image reader.
* @return {@code true} if the image reader can be reused.
* @throws IOException If an error occurred while determining if the current
* image reader can read the given input.
*/
protected boolean canReuseImageReader(final ImageReaderSpi provider, final Object input) throws IOException {
if (IOUtilities.canProcessAsPath(input)) {
final String[] suffixes = provider.getFileSuffixes();
return (suffixes != null) && ArraysExt.containsIgnoreCase(suffixes,
IOUtilities.extension(input));
} else {
return provider.canDecodeInput(input);
}
}
/**
* Creates an {@link ImageReader} that claim to be able to decode the given input.
* This method is invoked automatically by {@link #setInput(Object)} for creating
* a new {@linkplain #imageReader image reader}.
* <p>
* This method shall {@linkplain ImageReader#setInput(Object, boolean, boolean) set
* the input} of the image reader that it create before returning it.
* <p>
* The default implementation delegates to {@link XImageIO#getReaderBySuffix(Object,
* Boolean, Boolean)}. Subclasses can override this method if they want to create a
* new {@linkplain #imageReader image reader} in another way.
*
* @param input The input source.
* @return An initialized image reader for reading the given input.
* @throws IOException If no suitable image reader has been found, or if an error occurred
* while creating it.
*/
protected ImageReader createImageReader(final Object input) throws IOException {
// No need to check for MosaicImageReader inputs, because XImageIO does this check.
return XImageIO.getReaderBySuffix(input, seekForwardOnly, ignoreMetadata);
}
/**
* Returns the default Java I/O parameters to use for reading an image. This method
* is invoked by the {@link #read(int, GridCoverageReadParam)} method in order to get
* the Java parameter object to use for controlling the reading process.
* <p>
* The default implementation returns {@link ImageReader#getDefaultReadParam()}.
* Subclasses can override this method in order to perform additional parameter settings.
* For example a subclass may want to {@linkplain SpatialImageReadParam#setPaletteName set
* the color palette} according some information unknown to this base class. Note however
* that any
* {@linkplain ImageReadParam#setSourceRegion source region},
* {@linkplain ImageReadParam#setSourceSubsampling source subsampling} and
* {@linkplain ImageReadParam#setSourceBands source bands} settings may be overwritten
* by the {@code read} method, which perform its own computation.
*
* @param index The index of the image to be queried.
* @return A default Java I/O parameters object to use for controlling the reading process.
* @throws IOException If an I/O operation was required and failed.
*
* @see #read(int, GridCoverageReadParam)
*
* @since 3.11
*/
protected ImageReadParam createImageReadParam(int index) throws IOException {
return imageReader.getDefaultReadParam();
}
/**
* {@inheritDoc}
*/
@Override
public List<? extends GenericName> getCoverageNames() throws CoverageStoreException {
if (coverageNames == null) {
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
try {
List<String> imageNames = null;
if (imageReader instanceof NamedImageStore) {
imageNames = ((NamedImageStore) imageReader).getImageNames();
}
if (imageNames != null) {
coverageNames = new NameList(nameFactory, imageNames);
} else {
coverageNames = new NameList(nameFactory, getInputName(), imageReader.getNumImages(true));
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
}
return coverageNames;
}
/**
* Returns a helper object for parsing metadata.
*/
private MetadataHelper getMetadataHelper() {
if (helper == null) {
helper = new MetadataHelper(this);
}
return helper;
}
/**
* Returns the grid geometry for the {@link GridCoverage2D} to be read at the given index.
* The default implementation performs the following:
* <p>
* <ul>
* <li>The {@link GridEnvelope} is determined from the
* {@linkplain SpatialImageReader#getGridEnvelope(int) spatial image reader}
* if possible, or from the image {@linkplain ImageReader#getWidth(int) width}
* and {@linkplain ImageReader#getHeight(int) height} otherwise.</li>
* <li>The {@link CoordinateReferenceSystem} and the "<cite>grid to CRS</cite>" conversion
* are determined from the {@link SpatialMetadata} if any.</li>
* </ul>
*/
@Override
public GridGeometry2D getGridGeometry(final int index) throws CoverageStoreException {
GridGeometry2D gridGeometry = getCached(gridGeometries, index);
if (gridGeometry == null) {
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
/*
* Get the required information from the SpatialMetadata, if any.
* For now we just collect them - they will be processed later.
*/
CoordinateReferenceSystem crs = null;
MathTransform gridToCRS = null;
PixelOrientation pointInPixel = null;
final int width, height;
try {
width = imageReader.getWidth(index);
height = imageReader.getHeight(index);
final SpatialMetadata metadata = getImageMetadata(imageReader, index);
if (metadata != null) {
crs = metadata.getInstanceForType(CoordinateReferenceSystem.class);
if (crs == null || crs == PredefinedCRS.GRID_2D) {
crs = coverageBuilder.getCoordinateReferenceSystem();
}
if (crs == null) {
crs = PredefinedCRS.GRID_2D;
}
if (crs instanceof GridGeometry) { // Some formats (e.g. NetCDF) do that.
gridToCRS = ((GridGeometry) crs).getGridToCRS();
} else {
final RectifiedGrid grid = metadata.getInstanceForType(RectifiedGrid.class);
if (grid != null) {
gridToCRS = getMetadataHelper().getGridToCRS(grid);
}
final Georectified georect = metadata.getInstanceForType(Georectified.class);
if (georect != null) {
pointInPixel = georect.getPointInPixel();
}
}
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
/*
* If any metadata are still null, replace them by their default values. Those default
* values are selected in order to be as neutral as possible: An ImageCRS which is not
* convertible to GeodeticCRS, an identity "grid to CRS" conversion, a PixelOrientation
* equivalent to performing no shift at all in the "grid to CRS" conversion.
*/
if (crs == null) {
crs = PredefinedCRS.GRID_2D;
}
final int dimension = crs.getCoordinateSystem().getDimension();
if (gridToCRS == null) {
gridToCRS = MathTransforms.identity(dimension);
}
if (pointInPixel == null) {
pointInPixel = PixelOrientation.CENTER;
}
/*
* Now build the grid geometry. Note that the grid extent spans shall be set to 1
* for all dimensions other than X and Y, even if the original file has more data,
* since this is a GridGeometry2D requirement.
*/
final int[] lower = new int[dimension];
final int[] upper = new int[dimension];
Arrays.fill(upper, 1);
upper[X_DIMENSION] = width;
upper[Y_DIMENSION] = height;
final GridEnvelope gridExtent = new GeneralGridEnvelope(lower, upper, false);
gridGeometry = new GridGeometry2D(gridExtent, pointInPixel, gridToCRS, crs, null);
Map.Entry<Map<Integer,GridGeometry2D>,GridGeometry2D> entry = setCached(gridGeometry, gridGeometries, index);
gridGeometries = entry.getKey();
gridGeometry = entry.getValue();
}
return gridGeometry;
}
/**
* Gets the element at the given index in the given map if the map is non null and the
* index is valid, or returns {@code null} otherwise. This method is used for fetching
* a value that may or may not have been cached in a previous method call.
*/
private static <T> T getCached(final Map<Integer,T> cache, final int index) {
return (cache != null) ? cache.get(index) : null;
}
/**
* Sets the cached value to the given element. The cache is returned together with the value.
* Both of them may be different than the given arguments. We use {@code Map.Entry} only as a
* lazy way to emulate multi return values.
*/
private static <T> Map.Entry<Map<Integer,T>,T> setCached(T value, Map<Integer,T> cache, final int index) {
if (value != null) {
if (cache == null) {
cache = new HashMap<>();
}
for (final T current : cache.values()) {
if (current.equals(value)) {
value = current;
break;
}
}
cache.put(index, value);
}
return new HashMap.SimpleEntry<>(cache, value);
}
/**
* Return {@code true} if metadata contains Dimension informations from image descriptions else false.
*
* @param metadata current image description.
* @return {@code true} if metadata contains Dimension informations from image descriptions else false.
*/
private boolean hasDimensionMetadata(final IIOMetadata metadata) {
final Node asTree = metadata.getAsTree(GEOTK_FORMAT_NAME);
if (asTree.hasChildNodes()) {
final NodeList nl = asTree.getChildNodes();
final int length = nl.getLength();
for (int i = 0; i < length; i++) {
final Node current = nl.item(i);
if (current.getNodeName().equalsIgnoreCase("ImageDescription")) {
final NodeList idnl = current.getChildNodes();
final int l = idnl.getLength();
for (int j = 0; j < l; j++) {
if (idnl.item(j).getNodeName().equalsIgnoreCase("Dimensions")) return true;
}
}
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public List<GridSampleDimension> getSampleDimensions(final int index) throws CoverageStoreException {
List<GridSampleDimension> sd = getCached(sampleDimensions, index);
if (sd == null) {
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
/*
* Get the required information from the SpatialMetadata, if any.
* Here we just collect them - they will be processed by MetadataHelper.
*/
List<SampleDimension> bands = null;
try {
final SpatialMetadata metadata = getImageMetadata(imageReader, index);
if (metadata != null && hasDimensionMetadata(metadata)) {
DimensionAccessor accessor = new DimensionAccessor(metadata);
sd = accessor.getGridSampleDimensions();
if (sd != null) return sd;
bands = metadata.getListForType(SampleDimension.class);
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
if (isNullOrEmpty(bands)) {
// See the convention documented below.
sd = Collections.emptyList();
} else try {
// MetadataHelper default implementation returns an unmodifiable list.
sd = getMetadataHelper().getGridSampleDimensions(bands);
} catch (ImageMetadataException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
Map.Entry<Map<Integer,List<GridSampleDimension>>,List<GridSampleDimension>> entry =
setCached(sd, sampleDimensions, index);
sampleDimensions = entry.getKey();
sd = entry.getValue();
}
/*
* By convention, an empty list means that we already checked for sample dimensions
* and didn't found any. This is not the same than a coverage having no bands, which
* should not be valid.
*/
if (sd.isEmpty()) {
return null;
}
return sd;
}
/**
* Returns the sample dimensions for each band to be read, as determined from the given
* optional parameters. If parameters are not null, then this method returns only the
* sample dimensions for supplied source bands list and returns them in the order
* inferred from the destination bands list.
*
* @return The bands as a non-empty array, or {@code null}. This method is not allowed to
* return an empty array, because {@link GridCoverageFactory} interprets that as
* "no band" (as opposed to {@code null} which means "unspecified bands").
*/
private GridSampleDimension[] getSampleDimensions(final int index, final int[] srcBands, final int[] dstBands)
throws CoverageStoreException
{
final List<GridSampleDimension> bands = getSampleDimensions(index);
if (bands != null) {
int bandCount = bands.size();
if (bandCount != 0) {
if (srcBands != null && srcBands.length < bandCount) bandCount = srcBands.length;
if (dstBands != null && dstBands.length < bandCount) bandCount = dstBands.length;
final GridSampleDimension[] selectedBands = new GridSampleDimension[bandCount];
/*
* Searches for 'GridSampleDimension' from the given source band index and
* stores their reference at the position given by destination band index.
*/
for (int j=0; j<bandCount; j++) {
final int srcBand = (srcBands != null) ? srcBands[j] : j;
final int dstBand = (dstBands != null) ? dstBands[j] : j;
selectedBands[dstBand] = bands.get(srcBand % bandCount);
}
return selectedBands;
}
}
return null;
}
/**
* Returns {@code true} if the given sample dimensions contain at least one signed range.
*/
private static boolean isRangeSigned(final GridSampleDimension[] bands) {
if (bands != null) {
for (final GridSampleDimension band : bands) {
if (band != null && band.isRangeSigned()) {
return true;
}
}
}
return false;
}
/**
* Returns the metadata associated with the input source as a whole, or {@code null} if none.
* The default implementation delegates to the {@linkplain #imageReader image reader}, wrapping
* the {@link IIOMetadata} in a {@code SpatialMetadata} if necessary.
*
* @return The metadata associated with the input source as a whole, or {@code null}.
* @throws CoverageStoreException if an error occurs reading the information from the input source.
*
* @since 3.14
*/
@Override
public SpatialMetadata getStreamMetadata() throws CoverageStoreException {
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
try {
final IIOMetadata metadata = imageReader.getStreamMetadata();
if (metadata instanceof SpatialMetadata) {
return (SpatialMetadata) metadata;
} else if (metadata != null) {
return new SpatialMetadata(true, imageReader, metadata);
} else {
return null;
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
}
/**
* Returns the metadata associated with the given coverage, or {@code null} if none.
* The default implementation delegates to the {@linkplain #imageReader image reader},
* wrapping the {@link IIOMetadata} in a {@code SpatialMetadata} if necessary.
*
* @param index The index of the coverage to be queried.
* @return The metadata associated with the given coverage, or {@code null}.
* @throws CoverageStoreException if an error occurs reading the information from the input source.
*
* @since 3.14
*/
@Override
public SpatialMetadata getCoverageMetadata(final int index) throws CoverageStoreException {
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
try {
final IIOMetadata metadata = imageReader.getImageMetadata(index);
if (metadata instanceof SpatialMetadata) {
return (SpatialMetadata) metadata;
} else if (metadata != null) {
return new SpatialMetadata(false, imageReader, metadata);
} else {
return null;
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
}
/**
* Gets the spatial metadata from the given image reader, or return {@code null}
* if none were found. This method asks only for the metadata nodes listed in the
* {@link #METADATA_NODES} collection. Note however that most {@link ImageReader}
* implementations will return all metadata anyway.
*
* @param imageReader The image reader from which to get the metadata.
* @param index The index of the image to be queried.
* @return The metadata of the given index, or {@code null} if none.
* @throws IOException If an error occurred while reading the metadata.
*
* @see #getCoverageMetadata(int)
*/
private SpatialMetadata getImageMetadata(final ImageReader imageReader, final int index) throws IOException {
if (imageMetadataIndex != index) {
final IIOMetadata metadata = imageReader.getImageMetadata(index, GEOTK_FORMAT_NAME, METADATA_NODES);
if (metadata == null || metadata instanceof SpatialMetadata) {
imageMetadata = (SpatialMetadata) metadata;
} else {
imageMetadata = new SpatialMetadata(false, imageReader, metadata);
}
imageMetadataIndex = index;
}
return imageMetadata;
}
/**
* Converts geodetic parameters to image parameters, reads the image and wraps it in a
* grid coverage. First, this method creates an initially empty block of image parameters
* by invoking the {@link #createImageReadParam(int)} method. The image parameter
* {@linkplain ImageReadParam#setSourceRegion source region},
* {@linkplain ImageReadParam#setSourceSubsampling source subsampling} and
* {@linkplain ImageReadParam#setSourceBands source bands} are computed from the
* parameter given to this {@code read} method. Then, the following image parameters
* are set (if the image parameter class allows such settings):
* <p>
* <ul>
* <li><code>{@linkplain SpatialImageReadParam#setSampleConversionAllowed
* setSampleConversionAllowed}({@linkplain SampleConversionType#REPLACE_FILL_VALUES
* REPLACE_FILL_VALUES}, true)</code> in order to allow the replacement of
* fill values by {@link Float#NaN NaN}.</li>
*
* <li><code>{@linkplain SpatialImageReadParam#setSampleConversionAllowed
* setSampleConversionAllowed}({@linkplain SampleConversionType#SHIFT_SIGNED_INTEGERS
* SHIFT_SIGNED_INTEGERS}, true)</code> if the sample dimensions declare an unsigned
* range of sample values.</li>
*
* <li><code>{@linkplain MosaicImageReadParam#setSubsamplingChangeAllowed
* setSubsamplingChangeAllowed}(true)</code> in order to allow {@link MosaicImageReader}
* to use a different resolution than the requested one. This is crucial from a
* performance point of view. Since the {@code GridCoverageReader} contract does not
* guarantee that the grid geometry of the returned coverage is the requested geometry,
* we are allowed to do that.</li>
* </ul>
* <p>
* Finally, the image is read and wrapped in a {@link GridCoverage2D} using the
* information provided by {@link #getGridGeometry(int)} and {@link #getSampleDimensions(int)}.
*
* /!\ If {@link org.geotoolkit.coverage.io.GridCoverageReadParam#setDeferred(boolean)} parameter is set to true, the
* returned coverage will rely on the current reader to cache it's data on the fly, so you CANNOT dispose of the current
* reader while your using the resulting coverage.
*/
@Override
public GridCoverage2D read(final int index, final GridCoverageReadParam param)
throws CoverageStoreException, CancellationException
{
final boolean loggingEnabled = isLoggable();
long fullTime = (loggingEnabled) ? System.nanoTime() : 0;
ignoreGridTransforms = !loggingEnabled;
/*
* Parameters check.
*/
abortRequested = false;
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
GridGeometry2D gridGeometry = getGridGeometry(index);
checkAbortState();
final ImageReadParam imageParam;
try {
imageParam = createImageReadParam(index);
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
final int[] srcBands;
final int[] dstBands;
MathTransform2D destToExtractedGrid = null;
if (param != null) {
srcBands = param.getSourceBands();
dstBands = param.getDestinationBands();
if (srcBands != null && dstBands != null && srcBands.length != dstBands.length) {
throw new IllegalArgumentException(Errors.getResources(locale).getString(
Errors.Keys.MismatchedArrayLength_2, "sourceBands", "destinationBands"));
}
/*
* Convert geodetic envelope and resolution to pixel coordinates.
* Store the result of the above conversions in the ImageReadParam object.
*/
destToExtractedGrid = geodeticToPixelCoordinates(gridGeometry, param, imageParam, false);
/*
* Conceptually we could compute right now:
*
* AffineTransform change = new AffineTransform();
* change.translate(sourceRegion.x, sourceRegion.y);
* change.scale(xSubsampling, ySubsampling);
*
* However this implementation will scale only after the image has been read,
* because the MosaicImageReader may have changed the subsampling to more
* efficient values if it was authorized to make such change.
*/
imageParam.setSourceBands(srcBands);
imageParam.setDestinationBands(dstBands);
} else {
srcBands = null;
dstBands = null;
}
/*
* At this point, the standard parameters (source region, source bands) are set.
* The following is Geotk-specific. First, allow MosaicImageReader to use a different
* resolution than the requested one. This is crucial from a performance point of view.
* Since the GridCoverageReader contract does not guarantee that the grid geometry of the
* returned coverage is the requested geometry, we are allowed to do that.
*/
if (imageParam instanceof MosaicImageReadParam) {
// Note: we don't create a new ImageReadParam if it is null
// since we would be reading the image at full resolution anyway.
((MosaicImageReadParam) imageParam).setSubsamplingChangeAllowed(true);
}
/*
* Next, check if we should allow the image reader to add an offset to signed intergers
* in order to make them unsigned. We will allow such offset if the SampleDimensions
* declare unsigned range of sample values.
*/
boolean usePaletteFactory = false;
final GridSampleDimension[] bands = getSampleDimensions(index, srcBands, dstBands);
if (imageParam instanceof SpatialImageReadParam) {
final SpatialImageReadParam sp = (SpatialImageReadParam) imageParam;
if (!isRangeSigned(bands)) {
sp.setSampleConversionAllowed(SampleConversionType.SHIFT_SIGNED_INTEGERS, true);
}
sp.setSampleConversionAllowed(SampleConversionType.REPLACE_FILL_VALUES, true);
/*
* If the image does not have its own color palette, provides a palette factory
* which will create the IndexColorModel (if needed) from the GridSampleDimension.
*/
if (bands != null && imageReader instanceof SpatialImageReader) try {
usePaletteFactory = !((SpatialImageReader) imageReader).hasColors(index);
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
/*
* If there is supplemental dimensions (over the usual 2 dimensions) and the subclass
* implementation did not defined explicitely some dimension slices, then convert the
* envelope bounds in those supplemental dimensions to slices index.
*
* TODO: there is some duplication between this code and the work done in the parent
* class. We need to refactor geodeticToPixelCoordinates(…) in a new helper
* class for making easier to divide the work in smaller parts.
*/
if (param != null && !sp.hasDimensionSlices()) {
final int gridDim = gridGeometry.getDimension();
if (gridDim > 2) { // max(X_DIMENSION, Y_DIMENSION) + 1
final CoordinateReferenceSystem crs = gridGeometry.getCoordinateReferenceSystem();
final int geodeticDim = crs.getCoordinateSystem().getDimension();
if (geodeticDim > 2) {
Envelope envelope = param.getEnvelope();
if (envelope != null && envelope.getDimension() > 2) try {
if (crs instanceof CompoundCRS) {
envelope = CRSUtilities.appendMissingDimensions(envelope, (CompoundCRS) crs);
}
envelope = Envelopes.transform(envelope, crs);
final double[] median = new double[geodeticDim];
for (int i=0; i<geodeticDim; i++) {
median[i] = envelope.getMedian(i);
}
final double[] indices = new double[gridDim];
gridGeometry.getGridToCRS().inverse().transform(median, 0, indices, 0, 1);
final GridEnvelope gridExtent;
if (crs instanceof GridGeometry) {
gridExtent = ((GridGeometry) crs).getExtent();
} else {
// We can not fallback on gridGeometry.getExtent(), because
// GridGeometry2D contract forces all extra dimensions to have
// a span of 1.
gridExtent = null;
}
for (int i=0; i<gridDim; i++) {
if (i != gridGeometry.gridDimensionX && i != gridGeometry.gridDimensionY) {
final double sliceIndex = indices[i];
if (!Double.isNaN(sliceIndex)) {
final DimensionSlice slice = sp.newDimensionSlice();
slice.addDimensionId(i);
slice.setSliceIndex((int) Math.round(
Math.max(gridExtent != null ? gridExtent.getLow (i) : Integer.MIN_VALUE,
Math.min(gridExtent != null ? gridExtent.getHigh(i) : Integer.MAX_VALUE,
sliceIndex))));
}
}
}
} catch (TransformException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
}
}
}
}
final Map<?,?> properties = getProperties(index);
checkAbortState();
/*
* Read the image using the ImageReader.read(...) method. We could have used
* ImageReader.readAsRenderedImage(...) instead in order to give the reader a
* chance to return a tiled image, but experience with some formats suggests
* that it requires to keep the ImageReader with its input stream open.
*/
final String name;
RenderedImage image;
try {
final List<? extends GenericName> names = getCoverageNames();
try {
final GenericName gc = (index < names.size()) ? names.get(index) : null;
name = (gc != null) ? gc.toString() : null;
} catch (BackingStoreException e) {
throw e.unwrapOrRethrow(IOException.class);
}
if (usePaletteFactory) {
SampleDimensionPalette.BANDS.set(bands);
((SpatialImageReadParam) imageParam).setPaletteFactory(SampleDimensionPalette.FACTORY);
}
if (param != null && param.isDeferred()) {
image = new LargeRenderedImage(imageReader.getOriginatingProvider(), imageParam, imageReader.getInput(), index, null, null);
} else {
image = imageReader.read(index, imageParam);
}
} catch (IOException | IllegalArgumentException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
} finally {
if (usePaletteFactory) {
SampleDimensionPalette.BANDS.remove();
}
}
/*
* If the grid geometry changed as a result of subsampling or reading a smaller region,
* update the grid geometry. The (xmin, ymin) values are usually (0,0), but we take
* them in account anyway as a paranoiac safety (a previous version of this code used
* the 'readAsRenderedImage(...)' method, which could have shifted the image).
*/
if (param != null) {
final Rectangle sourceRegion = imageParam.getSourceRegion();
final AffineTransform change = AffineTransform.getTranslateInstance(sourceRegion.x, sourceRegion.y);
change.scale(imageParam.getSourceXSubsampling(), imageParam.getSourceYSubsampling());
final int xmin = image.getMinX();
final int ymin = image.getMinY();
final int xi = gridGeometry.gridDimensionX;
final int yi = gridGeometry.gridDimensionY;
final MathTransform gridToCRS = gridGeometry.getGridToCRS(PixelInCell.CELL_CORNER);
MathTransform newGridToCRS = gridToCRS;
if (!change.isIdentity()) {
final int gridDimension = gridToCRS.getSourceDimensions();
final Matrix matrix = Matrices.createIdentity(gridDimension + 1);
matrix.setElement(xi, xi, change.getScaleX());
matrix.setElement(yi, yi, change.getScaleY());
matrix.setElement(xi, gridDimension, change.getTranslateX() - xmin);
matrix.setElement(yi, gridDimension, change.getTranslateY() - ymin);
newGridToCRS = MathTransforms.concatenate(MathTransforms.linear(matrix), gridToCRS);
}
final GridEnvelope gridExtent = gridGeometry.getExtent();
final int[] low = gridExtent.getLow ().getCoordinateValues();
final int[] high = gridExtent.getHigh().getCoordinateValues();
low[xi] = xmin; high[xi] = xmin + image.getWidth() - 1;
low[yi] = ymin; high[yi] = ymin + image.getHeight() - 1;
if (imageParam instanceof SpatialImageReadParam) {
for (final DimensionSlice slice : ((SpatialImageReadParam) imageParam).getDimensionSlices()) {
for (final Object id : slice.getDimensionIds()) {
if (id instanceof Integer) {
final int dim = (Integer) id;
low[dim] = high[dim] = slice.getSliceIndex();
}
}
}
}
final GridEnvelope newGridRange = new GeneralGridEnvelope(low, high, true);
if (newGridToCRS != gridToCRS || !newGridRange.equals(gridExtent)) {
gridGeometry = new GridGeometry2D(newGridRange, PixelInCell.CELL_CORNER,
newGridToCRS, gridGeometry.getCoordinateReferenceSystem(), null);
}
}
final GridCoverage2D coverage;
final GridCoverageBuilder builder = coverageBuilder;
try {
builder.setName(name);
builder.setRenderedImage(image);
builder.setSampleDimensions(bands);
builder.setGridGeometry(gridGeometry);
builder.setProperties(properties);
coverage = builder.getGridCoverage2D();
} finally {
builder.reset();
}
if (loggingEnabled) {
fullTime = System.nanoTime() - fullTime;
final Level level = getLogLevel(fullTime);
if (LOGGER.isLoggable(level)) {
ImageCoverageStore.logOperation(level, locale, ImageCoverageReader.class, false,
input, index, coverage, null, null, destToExtractedGrid, fullTime);
}
}
return coverage;
}
/**
* Cancels the read operation. The default implementation forward the call to the
* {@linkplain #imageReader image reader}, if any. The content of the coverage
* following the abort will be undefined.
*/
@Override
public void abort() {
super.abort();
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader != null) {
imageReader.abort();
}
}
/**
* Returns an error message for the given exception. If the {@linkplain #input input} is
* known, this method returns "<cite>Can't read 'the name'</cite>" followed by the cause
* message. Otherwise it returns the localized message of the given exception.
*/
@Override
final String formatErrorMessage(final Throwable e) {
return formatErrorMessage(input, e, false);
}
/**
* Closes the input used by the {@link ImageReader}, provided that the stream was not
* given explicitly by the user. The {@link ImageReader} is not disposed, so it can be
* reused for the next image to read.
*
* @throws IOException if an error occurs while closing the input.
*/
private void close() throws IOException {
final Object oldInput = input;
input = null; // Clear now in case the code below fails.
coverageNames = null;
XCollections.clear(gridGeometries);
XCollections.clear(sampleDimensions);
final ImageReader imageReader = this.imageReader; // Protect from changes.
if (imageReader != null) {
if (imageReader.getInput() != oldInput) {
XImageIO.close(imageReader);
} else {
imageReader.setInput(null);
}
}
}
/**
* {@inheritDoc}
*
* @see ImageReader#reset()
*/
@Override
public void reset() throws CoverageStoreException {
try {
close();
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
if (imageReader != null) {
imageReader.reset();
}
helper = null;
imageMetadata = null;
imageMetadataIndex = -1;
super.reset();
}
/**
* Allows any resources held by this reader to be released. The result of calling any other
* method subsequent to a call to this method is undefined.
* <p>
* The default implementation closes the {@linkplain #imageReader image reader} input if
* the later is a stream, then {@linkplain ImageReader#dispose() disposes} that reader.
*
* @see ImageReader#dispose()
*/
@Override
public void dispose() throws CoverageStoreException {
try {
close();
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
if (imageReader != null) {
imageReader.dispose();
imageReader = null;
}
helper = null;
super.dispose();
}
@Override
protected void finalize() throws Throwable {
dispose();
super.finalize();
}
}