/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2007-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.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import javax.imageio.IIOException; import java.io.File; import java.io.IOException; import static java.lang.Double.NaN; import java.net.URL; import java.net.URI; import java.net.URISyntaxException; import java.sql.SQLException; import java.util.List; import java.util.Date; import java.util.Arrays; import java.util.Objects; import java.util.concurrent.CancellationException; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import org.opengis.geometry.Envelope; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.Matrix; import org.geotoolkit.image.palette.IIOListeners; import org.geotoolkit.image.io.mosaic.TileManager; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.coverage.grid.GridCoverage2D; import org.geotoolkit.coverage.grid.GridGeometry2D; import org.geotoolkit.coverage.grid.GeneralGridEnvelope; import org.geotoolkit.coverage.io.GridCoverageReader; import org.geotoolkit.coverage.io.GridCoverageReadParam; import org.geotoolkit.coverage.io.GridCoverageStorePool; import org.geotoolkit.coverage.io.CoverageStoreException; import org.apache.sis.referencing.crs.DefaultTemporalCRS; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.geotoolkit.util.DateRange; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.logging.Logging; import org.geotoolkit.internal.sql.table.DefaultEntry; import org.geotoolkit.internal.sql.table.IllegalRecordException; import org.apache.sis.internal.metadata.AxisDirections; import org.geotoolkit.resources.Errors; /** * Implementation of {@linkplain GridCoverageReference coverage reference}. * This implementation is immutable and thread-safe. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Sam Hiatt * @version 3.15 * * @since 3.10 (derived from Seagis) * @module */ final class GridCoverageEntry extends DefaultEntry implements GridCoverageReference { /** * For cross-version compatibility. */ private static final long serialVersionUID = -5725249398707248625L; /** * The grid geometry as a {@link GridGeometry2D} object. * Will be created only when first needed. */ private transient GridGeometry2D geometry2D; /** * Image start time, inclusive. */ private final long startTime; /** * Image end time, exclusive. */ private final long endTime; /** * If the image is tiled, the tiles. Otherwise {@code null}. */ private final TileManager[] tiles; /** * The value returned by {@link #getCoverage}, cached for reuse. */ private transient Reference<GridCoverage2D> cached; /** * The loader currently in use, or {@code null} if none. Note that more than one loader can be * in use concurrently. The other loaders are {@linkplain GridCoverageLoader#nextInUse chained * to the current loader}. * <p> * Access to this chained list shall be synchronized on {@code this}. */ private transient GridCoverageLoader currentReader; /** * Creates an entry containing coverage information (but not yet the coverage itself). * * @param identifier The identifier of this grid geometry. * @param startTime The coverage start time, or {@code null} if none. * @param endTime The coverage end time, or {@code null} if none. * @param tiles If the image is tiled, the tiles. Otherwise {@code null}. * @param comments Optional remarks, or {@code null} if none. */ protected GridCoverageEntry(final GridCoverageIdentifier identifier, final Date startTime, final Date endTime, final TileManager[] tiles, final String comments) throws SQLException { super(identifier, comments); this.startTime = (startTime != null) ? startTime.getTime() : Long.MIN_VALUE; this. endTime = ( endTime != null) ? endTime.getTime() : Long.MAX_VALUE; if (identifier.geometry.isEmpty() || this.startTime > this.endTime) { throw new IllegalRecordException(Errors.format(Errors.Keys.EmptyEnvelope2d)); } this.tiles = tiles; } /** * Returns the identifier of this {@code GridCoverageReference}. */ @Override public final GridCoverageIdentifier getIdentifier() { return (GridCoverageIdentifier) identifier; } /** * Returns a name for the coverage, for use in graphical user interfaces. */ @Override public String getName() { final GridCoverageIdentifier identifier = getIdentifier(); final StringBuilder buffer = new StringBuilder(identifier.filename); final int index = identifier.imageIndex; if (index != 0) { buffer.append(':').append(index); } return buffer.toString(); } /** * Returns the path to the image file as an object of the given type. */ @Override public <T> T getFile(final Class<T> type) throws IOException { final Object input; final GridCoverageIdentifier identifier = getIdentifier(); if (type.isAssignableFrom(File.class)) { input = identifier.file(); } else { final boolean isURL = type.isAssignableFrom(URL.class); if (isURL || type.isAssignableFrom(URI.class)) try { final URI uri = identifier.uri(); input = isURL ? uri.toURL() : uri; } catch (URISyntaxException e) { throw new IOException(e); } else { throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, type)); } } return type.cast(input); } /** * Returns the source as a {@link File} or an {@link URI}, in this preference order. * This method never returns {@code null}; if the URI can not be created, then an * exception is thrown. */ final Object getInput() throws URISyntaxException { if (tiles != null) { return tiles; } final GridCoverageIdentifier identifier = getIdentifier(); final File file = identifier.file(); if (file.isAbsolute()) { return file; } return identifier.uri(); } /** * Returns the image format. */ @Override public String getImageFormat() { return getIdentifier().series.format.imageFormat; } /** * Returns the native Coordinate Reference System of the coverage. * The returned CRS may be up to 4-dimensional. */ @Override public CoordinateReferenceSystem getCoordinateReferenceSystem(final boolean includeTime) { final GridGeometryEntry geometry = getIdentifier().geometry; return geometry.getSpatioTemporalCRS(includeTime); } /** * Returns the geographic bounding box of the {@linkplain #getEnvelope coverage envelope}. */ @Override public GeographicBoundingBox getGeographicBoundingBox() { final GridGeometryEntry geometry = getIdentifier().geometry; try { return geometry.getGeographicBoundingBox(); } catch (TransformException e) { // Returning 'null' is allowed by the method contract. Logging.recoverableException(null, GridCoverageReference.class, "getGeographicBoundingBox", e); return null; } } /** * Returns the spatio-temporal envelope of the coverage. The CRS of the returned envelope * is the {@linkplain #getCoordinateReferenceSystem(boolean) spatio-temporal CRS} of this * entry, which may vary on a coverage-by-coverage basis. */ @Override public Envelope getEnvelope() { return getGridGeometry().getEnvelope(); } /** * Returns the range of values in the two first dimensions, which are horizontal. */ @Override public Rectangle2D getXYRange() { return getIdentifier().geometry.standardEnvelope.getBounds2D(); } @Override public Number getZCenter() throws IOException { final NumberRange<?> range = getZRange(); if (range != null) { final Number lower = range.getMinValue(); final Number upper = range.getMaxValue(); if (lower != null) { if (upper != null) { return 0.5 * (lower.doubleValue() + upper.doubleValue()); } else { return lower.doubleValue(); } } else if (upper != null) { return upper.doubleValue(); }else{ return NaN; } }else{ return NaN; } } /** * Returns the range of values in the third dimension, which may be vertical or temporal. * This method returns the range in units of the database vertical or temporal CRS, which * may not be the same than the vertical or temporal CRS of the coverage. */ @Override public NumberRange<Double> getZRange() { final GridGeometryEntry geometry = getIdentifier().geometry; double min = geometry.standardMinZ; double max = geometry.standardMaxZ; if (!(min <= max)) { // Use '!' for catching NaN values. min = Double.NEGATIVE_INFINITY; max = Double.POSITIVE_INFINITY; final DefaultTemporalCRS temporalCRS = geometry.getTemporalCRS(); if (temporalCRS != null) { if (startTime != Long.MIN_VALUE) min = temporalCRS.toValue(new Date(startTime)); if ( endTime != Long.MAX_VALUE) max = temporalCRS.toValue(new Date( endTime)); } } return NumberRange.create(min, true, max, false); } /** * Returns the temporal part of the {@linkplain #getEnvelope coverage envelope}. */ @Override public DateRange getTimeRange() { return new DateRange((startTime != Long.MIN_VALUE) ? new Date(startTime) : null, true, (endTime != Long.MAX_VALUE) ? new Date( endTime) : null, false); } /** * Returns the coverage grid geometry. */ @Override @SuppressWarnings("fallthrough") public synchronized GridGeometry2D getGridGeometry() { if (geometry2D == null) { /* * If the grid coverage has a temporal dimension, we need to set the scale and offset * coefficients for it. Those coefficients need to be set on a coverage-by-coverage * basis since they are typically different for each coverage even if they share the * same GridGeometryEntry. */ double min = Double.NEGATIVE_INFINITY; double max = Double.POSITIVE_INFINITY; final GridCoverageIdentifier identifier = getIdentifier(); final GridGeometryEntry geometry = identifier.geometry; final DefaultTemporalCRS temporalCRS = geometry.getTemporalCRS(); if (temporalCRS != null) { if (startTime != Long.MIN_VALUE) min = temporalCRS.toValue(new Date(startTime)); if ( endTime != Long.MAX_VALUE) max = temporalCRS.toValue(new Date( endTime)); } final boolean hasTime = !Double.isInfinite(min) || !Double.isInfinite(max); final CoordinateReferenceSystem crs = geometry.getSpatioTemporalCRS(hasTime); final int dimension = crs.getCoordinateSystem().getDimension(); final Matrix gridToCRS = geometry.getGridToCRS(dimension, identifier.zIndex); if (hasTime) { /* * The code below makes the following assumptions, * which are checked by the assert statements below: * * 1) The temporal dimension is the last dimension. * 2) The temporal dimension is at the same index in * both the grid CRS and the "real world" CRS. */ assert AxisDirections.indexOfColinear(crs.getCoordinateSystem(), temporalCRS.getCoordinateSystem()) == dimension-1 : crs; assert gridToCRS.getElement(dimension-1, dimension-1) != 0 : gridToCRS; gridToCRS.setElement(dimension-1, dimension-1, max - min); gridToCRS.setElement(dimension-1, dimension, min); } /* * At this point, the 'gridToCRS' matrix has been built. * Now, compute the GridEnvelope. */ final Dimension size = geometry.getImageSize(); final int[] lower = new int[dimension]; final int[] upper = new int[dimension]; switch (dimension) { default: Arrays.fill(upper, 2, dimension, 1); // Fall through for every cases. case 2: upper[1] = size.height; case 1: upper[0] = size.width; case 0: break; } geometry2D = new GridGeometry2D(new GeneralGridEnvelope(lower, upper, false), geometry.getPixelInCell(), MathTransforms.linear(gridToCRS), crs, null); } return geometry2D; } /** * {@inheritDoc} */ @Override public GridSampleDimension[] getSampleDimensions() { final List<GridSampleDimension> sd = getIdentifier().series.format.sampleDimensions; if (sd == null) { return null; } final GridSampleDimension[] bands = sd.toArray(new GridSampleDimension[sd.size()]); for (int i=0; i<bands.length; i++) { bands[i] = bands[i].geophysics(true); } return bands; } /** * {@inheritDoc} */ @Override public GridCoverageReader getCoverageReader(final GridCoverageReader recycle) throws CoverageStoreException { final GridCoverageIdentifier identifier = getIdentifier(); final FormatEntry format = identifier.series.format; GridCoverageLoader reader; if (!(recycle instanceof GridCoverageLoader) || !(reader = (GridCoverageLoader) recycle).format.equals(format)) { reader = new GridCoverageLoader(format); } reader.setInput(this); return reader; } /** * Loads the data if needed and returns the coverage. * Note that the coverage is cached by the read method, since the envelope is null. */ @Override public GridCoverage2D getCoverage(final IIOListeners listeners) throws IOException, CancellationException { try { return read((CoverageEnvelope) null, listeners); } catch (CoverageStoreException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { throw (IOException) cause; } throw new IIOException(Errors.format(Errors.Keys.CantReadFile_1, getName()), e); } } /** * Reads the data in the given envelope and returns them as a coverage. * If the given envelope is {@code null}, then the whole coverage is loaded and cached. */ @Override public GridCoverage2D read(final CoverageEnvelope envelope, final IIOListeners listeners) throws CoverageStoreException, CancellationException { GridCoverageReadParam param = null; if (envelope != null) { final Rectangle2D bounds = envelope.getHorizontalRange(); if (!Double.isInfinite(bounds.getWidth()) || !Double.isInfinite(bounds.getHeight())) { param = new GridCoverageReadParam(); param.setEnvelope(bounds, envelope.database.horizontalCRS); } final Dimension2D resolution = envelope.getPreferredResolution(); if (resolution != null) { if (param == null) { param = new GridCoverageReadParam(); param.setCoordinateReferenceSystem(envelope.database.horizontalCRS); } param.setResolution(resolution.getWidth(), resolution.getHeight()); } } GridCoverage2D coverage; if (param != null) { // Do not use cache. coverage = read(param, listeners); } else { /* * This block is synchronized on identifier, which is a totally arbitrary lock. We * use that lock because we need something different than the lock used by abort(). */ synchronized (identifier) { if (cached != null) { coverage = cached.get(); if (coverage != null) { return coverage; } cached = null; } coverage = read(param, listeners); if (coverage != null) { cached = new SoftReference<>(coverage); } } } return coverage; } /** * Reads the data using the given parameters and returns them as a coverage. */ final GridCoverage2D read(final GridCoverageReadParam param, final IIOListeners listeners) throws CoverageStoreException, CancellationException { final GridCoverageIdentifier identifier = getIdentifier(); final GridCoverageStorePool pool = identifier.series.format.getCoverageLoaders(); final GridCoverageLoader reader = (GridCoverageLoader) pool.acquireReader(); /* * Adds the reader to the list of readers currently in use. * This list will be used by 'abort()' if needed. */ synchronized (this) { reader.nextInUse = currentReader; currentReader = reader; } GridCoverage2D coverage; try { reader.setInput(this); coverage = reader.read(0, param); } finally { /* * Removes the reader from the list of readers currently in use. Note that our * reader may not be anymore the head of the chained list, since new readers * could have been added concurrently to that list. */ synchronized (this) { GridCoverageLoader p = currentReader; if (p == reader) { currentReader = reader.nextInUse; } else { while (p.nextInUse != reader) { p = p.nextInUse; // A NullPointerException here would be a bug in our algorithm. } p.nextInUse = reader.nextInUse; } } reader.setInput(null); // Close the image input stream. } pool.release(reader); return coverage; } /** * Aborts all image reading which are in progress. */ @Override public synchronized void abort() { for (GridCoverageLoader reader=currentReader; reader!=null; reader=reader.nextInUse) { reader.abort(); } } /** * Returns {@code true} if the coverage represented by this entry has enough resolution * compared to the requested one. If this method doesn't have sufficient information, * then it conservatively returns {@code true}. * * @param requested The requested resolution in units of the database horizontal CRS. */ final boolean hasEnoughResolution(final Dimension2D requested) { if (requested != null) try { final GridGeometryEntry geometry = getIdentifier().geometry; final Dimension2D resolution = geometry.getStandardResolution(); if (resolution != null) { return resolution.getWidth() <= requested.getWidth() + SpatialRefSysEntry.EPS && resolution.getHeight() <= requested.getHeight() + SpatialRefSysEntry.EPS; } } catch (TransformException e) { Logging.recoverableException(null, GridCoverageEntry.class, "hasEnoughResolution", e); } return true; } /** * If two grid coverages have the same spatio-temporal envelope, return the one having the * coarsest resolution. If this method can not select an entry, it returns {@code null}. */ final GridCoverageEntry selectCoarseResolution(final GridCoverageEntry that) { if (startTime == that.startTime && endTime == that.endTime) { final GridGeometryEntry geom1 = this.getIdentifier().geometry; final GridGeometryEntry geom2 = that.getIdentifier().geometry; if (geom1.sameEnvelope(geom2)) { final Dimension size1 = geom1.getImageSize(); final Dimension size2 = geom2.getImageSize(); if (size1.width <= size2.width && size1.height <= size2.height) return this; if (size1.width >= size2.width && size1.height >= size2.height) return that; } } return null; } /** * Compares two entries on the same criterion than the one used in the SQL {@code "ORDER BY"} * statement of {@link GridCoverageTable}). Entries without date are treated as unordered. */ final boolean equalsAsSQL(final GridCoverageEntry other) { if (startTime == Long.MIN_VALUE && endTime == Long.MAX_VALUE) { return false; } return endTime == other.endTime; } /** * Compares this entry with the given object for equality. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (super.equals(object)) { final GridCoverageEntry that = (GridCoverageEntry) object; if (startTime == that.startTime && endTime == that.endTime) { final GridGeometryEntry geom1 = this.getIdentifier().geometry; final GridGeometryEntry geom2 = that.getIdentifier().geometry; return Objects.equals(geom1, geom2); } } return false; } }