/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.sql;
import java.awt.Dimension;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;
import java.util.Collections;
import java.io.IOException;
import java.net.URISyntaxException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.spi.ImageReaderSpi;
import org.opengis.util.LocalName;
import org.opengis.util.InternationalString;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.geotoolkit.coverage.Category;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.grid.GeneralGridGeometry;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.grid.ViewType;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.io.ImageCoverageReader;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.GridCoverageStorePool;
import org.geotoolkit.image.io.mosaic.MosaicImageReader;
import org.geotoolkit.image.io.SpatialImageReadParam;
import org.geotoolkit.image.io.DimensionSlice;
import org.geotoolkit.image.io.MultidimensionalImageStore;
import org.geotoolkit.image.io.NamedImageStore;
import org.geotoolkit.image.io.SampleConversionType;
import org.geotoolkit.image.io.XImageIO;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.image.io.metadata.SpatialMetadataFormat;
import org.geotoolkit.internal.coverage.TransferFunction;
import org.geotoolkit.internal.image.io.DiscoveryAccessor;
import org.geotoolkit.internal.image.io.DimensionAccessor;
import org.geotoolkit.internal.image.io.GridDomainAccessor;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.resources.Errors;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.ArraysExt;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME;
import static org.geotoolkit.internal.image.io.DimensionAccessor.fixRoundingError;
/**
* An implementation of {@link ImageCoverageReader} when the {@link GridGeometry2D} and the
* {@link GridSampleDimension}s are obtained from the database instead than from the file.
* <p>
* The values given to the {@link #setInput(Object)} method must be instances
* of {@link GridCoverageEntry}. The caller shall {@linkplain #reset() reset}
* or {@linkplain #dispose() dispose} the reader as soon as the reading is
* finished, in order to close the underlying input stream.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.10
* @module
*/
final class GridCoverageLoader extends ImageCoverageReader {
/**
* A description of the image format.
*/
final FormatEntry format;
/**
* The entry for the grid coverage to be read.
*/
private GridCoverageEntry entry;
/**
* For internal usage by {@link GridCoverageEntry} only.
*/
transient GridCoverageLoader nextInUse;
/**
* The coverage names to be returned by {@link #getCoverageNames()}, created when first needed.
* This field <strong>must</strong> be cleared by {@link #clearCache()} when a new input is set.
*
* @since 3.20
*/
private transient List<LocalName> coverageNames;
/**
* Metadata created when first needed. Those fields <strong>must</strong> be cleared
* by {@link #clearCache()} when a new input is set.
*
* @since 3.15
*/
private transient SpatialMetadata streamMetadata, imageMetadata;
/**
* {@code true} if the check for image index shall be temporarily disabled. This happen after
* the {@link #read(int, GridCoverageReadParam) method replaced the user-supplied image index
* (always 0, which is checked by {@link #ensureValidIndex(int}) by the actual image index as
* specified in the database. This field is resets to {@code false} as soon as the reading
* process is finished.
*/
private transient boolean disableIndexCheck;
/**
* Creates a new reader. This constructor sets {@link #ignoreMetadata} to
* {@code true} because the required metadata are provided by the database.
*/
public GridCoverageLoader(final FormatEntry format) {
this.format = format;
seekForwardOnly = Boolean.TRUE;
ignoreMetadata = Boolean.TRUE;
}
/**
* Returns that the given image index is zero.
*/
private void ensureValidIndex(final int index) {
if (index != 0 && !disableIndexCheck) {
throw new IllegalArgumentException(Errors.getResources(getLocale())
.getString(Errors.Keys.IllegalArgument_2, "imageIndex", index));
}
}
/**
* Ensures that the input is set, and returns it for convenience.
*/
private GridCoverageEntry ensureInputSet() {
final GridCoverageEntry entry = this.entry;
if (entry == null) {
throw new IllegalArgumentException(Errors.getResources(getLocale())
.getString(Errors.Keys.NoImageInput));
}
return entry;
}
/**
* Sets the input, which must be a {@link GridCoverageEntry} using the image format
* given at construction time.
* <p>
* If the image reader is an instance of {@link NamedImageStore}, then this method sets
* the name of NetCDF (or similar format) variable to read as the names declared in the
* {@code SampleDimensions} table.
* <p>
* If the image reader is an instance of {@link MultidimensionalImageStore} (typically the
* NetCDF reader), then this method sets the image index API to the temporal dimension. This
* is consistent with the database schema, where the image index is specified together with
* the date range in the {@code GridCoverages} table.
*
* @param The entry to use as the input.
* @param imageIndex the image index to use
*/
@Override
public void setInput(Object input) throws CoverageStoreException {
while (input instanceof GridCoverageDecorator) {
input = ((GridCoverageDecorator) input).reference;
}
if (input == entry) {
return;
}
final GridCoverageEntry e = (GridCoverageEntry) input;
if (e != null) {
assert format.equals(e.getIdentifier().series.format) : e;
try {
input = e.getInput();
} catch (URISyntaxException ex) {
throw new CoverageStoreException(ex);
}
}
clearCache();
super.setInput(input);
entry = e; // Set the field only on success.
/*
* For the NetCDF format, find the names of the variables to read. They are the names
* declared in the SampleDimensions table. Each variable will be assigned to one band.
* There is typically only one variable to read.
*/
if (input != null) {
if (imageReader instanceof NamedImageStore) {
final List<GridSampleDimension> bands = format.sampleDimensions;
if (bands != null) {
final String[] bandNames = new String[bands.size()];
for (int i=0; i<bandNames.length; i++) {
bandNames[i] = bands.get(i).getDescription().toString();
}
final NamedImageStore named = (NamedImageStore) imageReader;
try {
named.setBandNames(0, bandNames);
} catch (IOException ex) {
throw new CoverageStoreException(ex);
}
}
}
if (imageReader instanceof MultidimensionalImageStore) {
((MultidimensionalImageStore) imageReader).getDimensionForAPI(DimensionSlice.API.IMAGES)
.addDimensionId(AxisDirection.FUTURE, AxisDirection.PAST);
}
}
}
/**
* Returns {@code true} if the given provider is suitable for the image format
* expected by the current entry. This implementation returns {@code true} in
* all cases, since we are supposed to recycle the same reader.
*/
@Override
protected boolean canReuseImageReader(final ImageReaderSpi provider, final Object input) throws IOException {
assert (provider instanceof MosaicImageReader.Spi) || // The format name of this provider is "mosaic".
ArraysExt.containsIgnoreCase(provider.getFormatNames(), format.imageFormat) ||
ArraysExt.containsIgnoreCase(provider.getMIMETypes(), format.imageFormat) : format;
return true;
}
/**
* Creates an {@link ImageReader} that claim to be able to decode the given input.
* This method is invoked automatically by {@link #setInput(Object)} for assigning
* a value to the {@link #imageReader} field.
*/
@Override
protected ImageReader createImageReader(final Object input) throws IOException {
if (MosaicImageReader.Spi.DEFAULT.canDecodeInput(input)) {
return MosaicImageReader.Spi.DEFAULT.createReaderInstance();
}
final String imageFormat = format.imageFormat;
final boolean isMIME = imageFormat.indexOf('/') >= 0;
if (isMIME) {
return XImageIO.getReaderByMIMEType(imageFormat, input, seekForwardOnly, ignoreMetadata);
} else {
return XImageIO.getReaderByFormatName(imageFormat, input, seekForwardOnly, ignoreMetadata);
}
}
/**
* Returns the name of the coverages to be read. This implementations
* assumes that there is exactly one coverage per entry.
*/
@Override
public List<LocalName> getCoverageNames() throws CoverageStoreException {
if (coverageNames == null) {
coverageNames = Collections.singletonList(nameFactory.createLocalName(null, ensureInputSet().getName()));
}
return coverageNames;
}
/**
* Returns the grid geometry which is declared in the database.
*/
@Override
public GridGeometry2D getGridGeometry(int index) throws CoverageStoreException {
ensureValidIndex(index);
return ensureInputSet().getGridGeometry();
}
/**
* Returns the sample dimensions for each band of the {@code GridCoverage} to be read.
* This method returns the sample dimensions declared in the database rather then inferring
* them from the image metadata.
* <p>
* If the sample dimensions are not known, then this method returns {@code null}.
*/
@Override
public List<GridSampleDimension> getSampleDimensions(int index) throws CoverageStoreException {
ensureValidIndex(index);
return format.sampleDimensions;
}
/**
* Creates stream metadata for the given geographic bounding box.
*/
static SpatialMetadata createStreamMetadata(final GeographicBoundingBox bbox) {
final SpatialMetadata metadata = new SpatialMetadata(SpatialMetadataFormat.getStreamInstance(GEOTK_FORMAT_NAME));
if (bbox != null) {
final DiscoveryAccessor accessor = new DiscoveryAccessor(metadata) {
@Override protected double nice(final double value) {
return fixRoundingError(value);
}
};
accessor.setGeographicElement(bbox);
}
return metadata;
}
/**
* Returns the metadata associated with the stream as a whole. This method fetches
* the metadata from the database only; it does not attempt to read the image file.
*/
@Override
public SpatialMetadata getStreamMetadata() throws CoverageStoreException {
SpatialMetadata metadata = streamMetadata;
if (metadata == null) {
streamMetadata = metadata = createStreamMetadata(entry.getGeographicBoundingBox());
}
return metadata;
}
/**
* Creates image metadata for the given sample dimensions and grid geometry.
*
* @param locale The locale to use with {@link InternationalString} attributes.
* @param bands The sample dimension, or {@code null} if none.
* @param geometry The grid geometry, or {@code null} if none.
*/
static SpatialMetadata createImageMetadata(final Locale locale,
final List<GridSampleDimension> bands, final GeneralGridGeometry geometry)
{
final SpatialMetadata metadata = new SpatialMetadata(SpatialMetadataFormat.getImageInstance(GEOTK_FORMAT_NAME));
if (bands != null) {
final DimensionAccessor accessor = new DimensionAccessor(metadata);
for (GridSampleDimension band : bands) {
accessor.selectChild(accessor.appendChild());
final InternationalString title = band.getDescription();
if (title != null) {
accessor.setDescriptor(title.toString(locale));
}
/*
* Add the range of geophysics values to the metadata.
*/
band = band.geophysics(true);
accessor.setValueRange(fixRoundingError(band.getMinimumValue()),
fixRoundingError(band.getMaximumValue()));
accessor.setUnits(band.getUnits());
/*
* Add the range of sample values to the metadata. Those values should
* be integers, because the type of the "lower" and "upper" columns in
* the database are integers.
*/
TransferFunction tf = null;
band = band.geophysics(false);
NumberRange<?> range = null;
int[] fillValues = new int[8];
int fillValuesCount = 0;
for (final Category category : band.getCategories()) {
final NumberRange<?> r = category.getRange();
if (category.isQuantitative()) {
range = (range == null) ? r : range.unionAny(r);
tf = new TransferFunction(category, locale);
} else {
final int lower = (int) Math.round(r.getMinDouble(true));
final int upper = (int) Math.round(r.getMaxDouble(true));
for (int i=lower; i<=upper; i++) {
if (fillValuesCount >= fillValues.length) {
fillValues = Arrays.copyOf(fillValues, fillValuesCount*2);
}
fillValues[fillValuesCount++] = i;
}
}
}
accessor.setValidSampleValue(range);
if (fillValuesCount != 0) {
accessor.setFillSampleValues(ArraysExt.resize(fillValues, fillValuesCount));
}
/*
* Add the transfer function.
*/
if (tf != null) {
accessor.setTransfertFunction(fixRoundingError(tf.getScale()),
fixRoundingError(tf.getOffset()), tf.getType());
}
}
}
/*
* Add the "SpatialRepresentation" and "RectifiedGridDomain" nodes.
*/
if (geometry != null) {
final GridDomainAccessor accessor = new GridDomainAccessor(metadata);
accessor.setGridGeometry(geometry, PixelInCell.CELL_CORNER, null);
}
return metadata;
}
/**
* Returns the metadata associated with the given coverage. This method fetches the
* metadata from the database only; it does not attempt to read the image file.
*/
@Override
public SpatialMetadata getCoverageMetadata(final int index) throws CoverageStoreException {
ensureValidIndex(index);
SpatialMetadata metadata = imageMetadata;
if (metadata == null) {
imageMetadata = metadata = createImageMetadata(getLocale(),
format.sampleDimensions, getGridGeometry(index));
}
return metadata;
}
/**
* Returns read parameters with the z-slice initialized, if needed.
*/
@Override
protected ImageReadParam createImageReadParam(final int index) throws IOException {
final ImageReadParam param = super.createImageReadParam(index);
final int zIndex = ensureInputSet().getIdentifier().zIndex;
if (zIndex != 0) {
if (param instanceof SpatialImageReadParam) {
final DimensionSlice slice = ((SpatialImageReadParam) param).newDimensionSlice();
slice.addDimensionId(AxisDirection.UP, AxisDirection.DOWN);
slice.setSliceIndex(zIndex - 1);
}
}
/*
* Sets the palette name as a safety, but this is actually not used by geotk-coverage-sql
* because SampleDimensionPalette.createImageTypeSpecifier() will use the information
* declared in the GridSampleDimensions.
*
* If the sample values are already geophysics, enable the conversion from integer type
* to floating point type in order to allow the image reader to replace fill values by
* NaN during the read process.
*
* Otherwise, if the format is declared "native" (i.e. it should describe precisely
* how the sample values are stored on disk, without offset of negative values), then
* overwrite the image metadata with the database values. This allow correct results
* when the image metadata are incomplete or inaccurate.
*/
if (param instanceof SpatialImageReadParam) {
final SpatialImageReadParam sp = (SpatialImageReadParam) param;
sp.setPaletteName(format.paletteName);
switch (format.viewType) {
case GEOPHYSICS: {
sp.setSampleConversionAllowed(SampleConversionType.STORE_AS_FLOATS, true);
break;
}
case NATIVE: {
sp.setSampleDomains(format.sampleDomains);
break;
}
}
}
return param;
}
/**
* Reads the grid coverage. This method checks that the size of the image is the same as
* the size declared in the database. This check is only used to catch possible errors that
* would otherwise slip into the database or during the copy of the image to the disk.
*/
@Override
public GridCoverage2D read(int index, final GridCoverageReadParam param)
throws CoverageStoreException
{
ensureValidIndex(index);
final GridCoverageIdentifier identifier = ensureInputSet().getIdentifier();
index = identifier.getImageIndex();
final ImageReader imageReader = this.imageReader; // Protect from changes.
try {
final Dimension expectedSize = identifier.geometry.getImageSize();
final int expectedWidth = expectedSize.width;
final int expectedHeight = expectedSize.height;
final int imageWidth = imageReader.getWidth (index);
final int imageHeight = imageReader.getHeight(index);
if (expectedWidth != imageWidth || expectedHeight != imageHeight) {
throw new CoverageStoreException(Errors.getResources(getLocale()).getString(Errors.Keys.MismatchedImageSize_5,
IOUtilities.filename(getInputName()), imageWidth, imageHeight, expectedWidth, expectedHeight));
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
GridCoverage2D coverage;
disableIndexCheck = true;
try {
coverage = super.read(index, param);
} finally {
disableIndexCheck = false;
}
/*
* The GridCoverageReference.read(...) contract requires that we return
* always the geophysics view, when available.
*/
if (coverage != null) {
coverage = coverage.view(ViewType.GEOPHYSICS);
}
return coverage;
}
/**
* Returns the name of the input.
*/
private String getInputName() throws CoverageStoreException {
final Object input = getInput();
if (IOUtilities.canProcessAsPath(input)) {
return IOUtilities.filename(input);
} else {
return entry.toString();
}
}
/**
* 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.
*/
private String formatErrorMessage(final Exception e) throws CoverageStoreException {
final String cause = e.getLocalizedMessage();
String message = Errors.getResources(getLocale()).getString(Errors.Keys.CantReadFile_1, getInputName());
if (cause != null && cause.indexOf(' ') > 0) { // Append only if we have a sentence.
message = message + '\n' + cause;
}
return message;
}
/**
* Clears the cached object. This method needs to be invoked when the input changed,
* in order to force the calculation of new objects for the new input.
*/
private void clearCache() {
entry = null;
coverageNames = null;
streamMetadata = null;
imageMetadata = null;
}
/**
* {@inheritDoc}
*/
@Override
public void reset() throws CoverageStoreException {
clearCache();
super.reset();
}
/**
* {@inheritDoc}
*/
@Override
public void dispose() throws CoverageStoreException {
clearCache();
super.dispose();
}
/**
* The pool of {@link GridCoverageLoader}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.10
*
* @since 3.10
* @module
*/
static final class Pool extends GridCoverageStorePool {
/**
* A description of the image format.
*/
private final FormatEntry format;
/**
* Creates a new {@code Pool} instance. The maximal number of readers is intentionally
* small, given that we are going to create one pool for each format.
*/
Pool(final FormatEntry format) {
super(4);
this.format = format;
}
/**
* Creates a new {@link GridCoverageLoader}.
*/
@Override
protected GridCoverageReader createReader() throws CoverageStoreException {
return new GridCoverageLoader(format);
}
}
}