/*
* 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.util.List;
import java.util.Collections;
import java.util.concurrent.Future;
import java.sql.SQLException;
import java.awt.geom.Dimension2D;
import org.opengis.util.LocalName;
import org.opengis.util.FactoryException;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.CRS;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.grid.GeneralGridGeometry;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.display.shape.DoubleDimension2D;
import org.geotoolkit.util.collection.FrequencySortedSet;
import org.geotoolkit.image.palette.IIOListeners;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.resources.Errors;
/**
* A grid coverage reader for a layer. This class provides a way to read the data using only the
* {@link GridCoverageReader} API, with {@linkplain #getInput() input} of kind {@link Layer}.
* <p>
* The {@link #read read} method actually reads two-dimensional slices selected according
* the spatio-temporal envelope given to the {@link GridCoverageReadParam} argument.
* <p>
* <b>Usage example:</b>
* {@preformat java
* CoverageDatabase db = new CoverageDatabase(...);
* LayerCoverageReader reader = db.createGridCoverageReader("My layer");
* GridCoverageReadParam param = new GridCoverageGridParam();
* param.setEnvelope(...);
* param.setResolution(...);
* GridCoverage coverage = reader.read(0, param);
* }
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @see CoverageDatabase#createGridCoverageReader(String)
* @see CoverageDatabase#readSlice(String, CoverageEnvelope, IIOListeners)
* @see GridCoverageReference#read(CoverageEnvelope, IIOListeners)
*
* @since 3.10
* @module
*/
public class LayerCoverageReader extends GridCoverageReader {
/**
* The coverage database which created this {@code LayerCoverageReader}.
*/
protected final CoverageDatabase database;
/**
* A temporary object used for computing the value to be given to the
* {@link Layer#getCoverageReference(CoverageEnvelope)} method. Subclasses can use
* this field for computation purpose, but its content shall not be presumed stable.
*/
protected final CoverageEnvelope temporaryEnvelope;
/**
* The list of coverage names, computed when first needed.
*/
private List<LocalName> names;
/**
* The most frequently used grid geometry.
* This is computed when first needed.
*/
private transient GeneralGridGeometry gridGeometry;
/**
* The most commonly used sample dimensions, or {@code null} if none.
* This is computed when first needed.
*/
private transient List<GridSampleDimension> sampleDimensions;
/**
* Metadata created when first needed.
*
* @since 3.16
*/
private transient SpatialMetadata streamMetadata, imageMetadata;
/**
* Creates a new reader for the given database. The {@link #setInput(Object)}
* method must be invoked before this reader can be used.
*
* @param database The database to used with this reader.
*/
protected LayerCoverageReader(final CoverageDatabase database) {
this.database = database;
temporaryEnvelope = new CoverageEnvelope(database.database);
}
/**
* Creates a new reader for the given database and initializes its layer to the given value.
*
* @throws CoverageStoreException Declared for compilation raison, but should never happen.
*/
LayerCoverageReader(final CoverageDatabase database, final Future<Layer> layer)
throws CoverageStoreException
{
this(database);
super.setInput(layer);
}
/**
* Returns the object to use for formatting error messages.
*/
private Errors errors() {
return Errors.getResources(getLocale());
}
/**
* Ensures that the input is set.
*
* @throws CoverageStoreException Declared for compilation raison, but should never happen.
*/
private void ensureInputSet() throws CoverageStoreException, IllegalStateException {
if (super.getInput() == null) { // Use 'super' because we don't want to wait for Future.
throw new IllegalStateException(errors().getString(Errors.Keys.NoImageInput));
}
}
/**
* Ensures that the given coverage index is valid. Invoking
* this method implies a call to {@link #ensureInputSet()}.
*
* @param index The coverage index.
* @throws CoverageStoreException Declared for compilation raison, but should never happen.
*/
private void ensureValidIndex(final int index) throws CoverageStoreException, IndexOutOfBoundsException {
ensureInputSet();
if (index != 0) {
throw new IndexOutOfBoundsException(errors().getString(
Errors.Keys.IndexOutOfBounds_1, index));
}
}
/**
* Returns the current layer which is used as input, or {@code null} if none.
*/
@Override
public final Layer getInput() throws CoverageStoreException {
Object input = super.getInput();
if (input instanceof Future<?>) {
input = ((FutureQuery<?>) input).result();
super.setInput(input);
}
return (Layer) input;
}
/**
* Sets a new layer as input. The given input can be either a {@link Layer} instance,
* or the name of a layer as a {@link CharSequence}.
*
* @param input The new input as a {@link Layer} instance or a {@link CharSequence},
* or {@code null} for removing any input previously set.
* @throws IllegalArgumentException If the given input is not of a legal type.
*/
@Override
public void setInput(Object input) throws CoverageStoreException {
if (input != null) {
if (input instanceof CharSequence) {
input = database.getLayer(input.toString());
} else if (!(input instanceof Layer)) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalClass_2,
input.getClass(), Layer.class));
}
}
clearCache();
super.setInput(input);
}
/**
* Returns the layer name.
*/
@Override
public List<LocalName> getCoverageNames() throws CoverageStoreException {
ensureInputSet();
if (names == null) {
names = Collections.singletonList(FactoryFinder.getNameFactory(null)
.createLocalName(null, getInput().getName()));
}
return names;
}
/**
* Returns the most commonly used grid geometry. If no grid geometry can be found
* (for example because the layer doesn't contain any coverage), then this method
* returns {@code null}.
*/
@Override
public GeneralGridGeometry getGridGeometry(final int index) throws CoverageStoreException {
ensureValidIndex(index);
if (gridGeometry == null) {
for (final GeneralGridGeometry geometry : getInput().getGridGeometries()) {
if (geometry != null) {
gridGeometry = geometry;
break;
}
}
}
return gridGeometry;
}
/**
* Returns the most commonly used sample dimensions for each band of the {@link GridCoverage}
* to be read. If sample dimensions are not known, then this method returns {@code null}.
*/
@Override
public List<GridSampleDimension> getSampleDimensions(final int index) throws CoverageStoreException {
ensureValidIndex(index);
if (sampleDimensions == null) {
final FrequencySortedSet<SeriesEntry> series;
try {
series = ((LayerEntry) getInput()).getCountBySeries();
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
final FrequencySortedSet<List<GridSampleDimension>> sd = new FrequencySortedSet<>(true);
final int[] count = series.frequencies();
int i = 0;
for (final SeriesEntry entry : series) {
sd.add(entry.format.sampleDimensions, count[i++]);
}
for (final List<GridSampleDimension> result : sd) {
if (result != null) {
sampleDimensions = result;
break;
}
}
}
return sampleDimensions;
}
/**
* 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.
*
* @since 3.16
*/
@Override
public SpatialMetadata getStreamMetadata() throws CoverageStoreException {
ensureInputSet();
SpatialMetadata metadata = streamMetadata;
if (metadata == null) {
streamMetadata = metadata = GridCoverageLoader.createStreamMetadata(
getInput().getGeographicBoundingBox());
}
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.
*
* @since 3.16
*/
@Override
public SpatialMetadata getCoverageMetadata(final int index) throws CoverageStoreException {
ensureValidIndex(index);
SpatialMetadata metadata = imageMetadata;
if (metadata == null) {
imageMetadata = metadata = GridCoverageLoader.createImageMetadata(getLocale(),
getSampleDimensions(index), getGridGeometry(index));
}
return metadata;
}
/**
* Reads the data and return them as a coverage. The current implementation delegates to
* {@link #readSlice(int, GridCoverageReadParam)}. A future implementation may return a
* three-dimensional coverage.
*
* @param index The image index (usually 0).
* @param param Optional read parameters (including the envelope and resolution to request),
* or {@code null} if none.
* @return The coverage, or {@code null} if none.
* @throws CoverageStoreException If an error occurred while querying the database or reading
* the image.
*/
@Override
public GridCoverage read(final int index, final GridCoverageReadParam param) throws CoverageStoreException {
return readSlice(index, param);
}
/**
* Reads the data of a two-dimensional slice and returns them as a coverage. Note that the
* returned two-dimensional slice is not guaranteed to have exactly the requested envelope.
* Callers may need to check the geometry of the returned envelope and perform an additional
* resampling if needed.
*
* @param index The image index (usually 0).
* @param param Optional read parameters (including the envelope and resolution to request),
* or {@code null} if none.
* @return The coverage, or {@code null} if none.
* @throws CoverageStoreException If an error occurred while querying the database or reading
* the image.
*
* @see CoverageDatabase#readSlice(String, CoverageEnvelope, IIOListeners)
*/
public GridCoverage2D readSlice(final int index, final GridCoverageReadParam param) throws CoverageStoreException {
ensureValidIndex(index);
final Layer layer = getInput();
CoverageEnvelope envelope = null;
if (param != null) {
envelope = temporaryEnvelope;
Dimension2D hr = null;
/*
* Transforms the envelope and the resolution (if any) from the user envelope CRS
* to the database CRS. In the particular case of the resolution, we will transform
* an offset vector in the center of the intersection between the user envelope and
* the layer envelope.
*/
try {
envelope.setEnvelope(param.getEnvelope()); // Null allowed.
double[] resolution = param.getResolution();
if (resolution != null && resolution.length >= 2) {
final CoordinateReferenceSystem crs = param.getCoordinateReferenceSystem();
if (crs != null) {
envelope.intersect(layer.getEnvelope(null, null));
final GeneralDirectPosition center = new GeneralDirectPosition(crs);
for (int i=center.getDimension(); --i>=0;) {
center.setOrdinate(i, envelope.getMedian(i));
}
resolution = org.geotoolkit.referencing.CRS.deltaTransform(CRS.findOperation(crs,
envelope.database.horizontalCRS, null).getMathTransform(), center, resolution);
}
hr = new DoubleDimension2D(resolution[0], resolution[1]);
}
} catch (TransformException e) {
throw new CoverageStoreException(errors().getString(
Errors.Keys.IllegalCoordinateReferenceSystem), e);
} catch (FactoryException e) {
throw new CoverageStoreException(e);
}
envelope.setPreferredResolution(hr);
}
/*
* Now process to the image reading.
*/
final GridCoverageEntry ref = (GridCoverageEntry) layer.getCoverageReference(envelope);
if (ref != null) {
final IIOListeners listeners = null; // TODO
return ref.read(param, listeners);
}
return null;
}
/**
* 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() {
names = null;
gridGeometry = null;
sampleDimensions = 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();
}
}