/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2003-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; import java.util.List; import java.util.Locale; import java.util.Arrays; import java.util.Comparator; import java.util.Collection; import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; import java.io.IOException; import java.io.ObjectInputStream; import javax.imageio.ImageReader; import javax.imageio.event.IIOReadWarningListener; import javax.imageio.event.IIOReadProgressListener; import javax.media.jai.InterpolationNearest; import org.opengis.coverage.Coverage; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.coverage.grid.GridGeometry; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.CannotEvaluateException; import org.opengis.coverage.PointOutsideCoverageException; import org.opengis.util.FactoryException; import org.opengis.referencing.crs.TemporalCRS; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.CoordinateOperationFactory; import org.opengis.referencing.operation.OperationNotFoundException; import org.opengis.geometry.MismatchedReferenceSystemException; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.apache.sis.util.ArraysExt; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.Classes; import org.geotoolkit.util.collection.FrequencySortedSet; import org.apache.sis.util.collection.BackingStoreException; import org.geotoolkit.coverage.grid.GridCoverage2D; import org.geotoolkit.coverage.grid.Interpolator2D; import org.apache.sis.geometry.GeneralDirectPosition; import org.apache.sis.geometry.GeneralEnvelope; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Loggings; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.referencing.CRS; import org.apache.sis.referencing.crs.DefaultTemporalCRS; import static java.lang.Double.NaN; import static java.lang.Double.isNaN; import static java.lang.Double.POSITIVE_INFINITY; import static java.lang.Double.NEGATIVE_INFINITY; import org.geotoolkit.image.palette.IIOListeners; import org.geotoolkit.image.palette.IIOReadProgressAdapter; import org.apache.sis.geometry.Envelopes; import static org.apache.sis.util.Utilities.equalsIgnoreMetadata; import static org.geotoolkit.internal.InternalUtilities.debugEquals; /** * Creates a (<var>n</var>+1) dimensional coverage from a collection of <var>n</var> dimensional * coverages. For example given a list of {@code Coverage2D} at elevations <var>z</var><sub>0</sub>, * <var>z</var><sub>1</sub> … <var>z</var><sub>max</sub>, this class creates a new * 3-dimensional coverages where the envelope is: * <p> * <ul> * <li>the union of all coverage envelopes in the two first dimensions, * <li>[<var>z</var><sub>0</sub> … <var>z</var><sub>max</sub>] in the third dimension.</li> * </ul> * <p> * Collections of {@code CoverageStack}s can be given recursively to other {@code CoverageStack} * for creating new coverages with higher dimensionality. * <p> * <b>NOTE:</b> For documentation purpose, the new dimension handled by {@code CoverageStack} * is named <var>z</var>. But this is only a naming convention - the new dimension can really * be anything, including time. * * {@section Construction} * {@code GridCoverage2D} objects tend to be big. In order to keep memory usage reasonable, this * implementation doesn't requires all {@code GridCoverage2D} objects at once. Instead, it requires * an array of {@link Element} objects, which will load the coverage content only when first * needed. * <p> * Each element usually have the same {@linkplain Coverage#getEnvelope() envelope}, * but this is not a requirement. Elements are not required to share the same * {@linkplain Coverage#getCoordinateReferenceSystem() Coordinate Reference System} neither, * but they are required to handle the transformation from this coverage CRS to their CRS. * * {@section Usage} * Each {@linkplain Element coverage element} is expected to extend over a range of <var>z</var> * values. If an {@code evaluate(...)} method is invoked with a <var>z</var> value not falling in * the middle of a coverage element, a linear interpolation is applied. * * {@section Caching} * This {@code CoverageStack} implementation remember the last coverage elements used; * it will not trig new data loading as long as consecutive calls to {@code evaluate(...)} * methods require the same coverage elements. Apart from this very simple mechanism, * caching is the responsibility of {@link Element} implementations. Note that this simple * caching mechanism is sufficient if {@code evaluate(...)} methods are invoked with increasing * <var>z</var> values. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Johann Sorel (Geomatys) * @version 3.16 * * @since 2.1 * @module */ public class CoverageStack extends AbstractCoverage { /** * For compatibility during cross-version serialization. */ private static final long serialVersionUID = -7100201963376146053L; /** * Reference to a single <var>n</var> dimensional coverage in a (<var>n</var>+1) dimensional * {@linkplain CoverageStack coverage stack}. Each element is expected to extents over a range * of <var>z</var> values in the dimension handled by the {@link CoverageStack} container. * <p> * {@code Element} implementations shall be able to provide their {@linkplain #getZRange() * range of z-values} without loading the coverage data. If an expensive loading is required, * it should be delayed until the {@link #getCoverage(IIOListeners)} method is invoked. If * {@code getCoverage} is invoked more than once, caching (if desirable) is implementor's * responsibility. * <p> * All methods declare {@link IOException} in their {@code throws} clause in case I/O operations * are required. Subclasses of {@code IOException} include {@link javax.imageio.IIOException} * for image I/O operations, or {@link java.rmi.RemoteException} for remote method invocations * (which may be useful for large images database backed by a distant server). * * @author Martin Desruisseaux (IRD) * @version 3.00 * * @since 2.1 * @module */ public interface Element { /** * Returns a name for the coverage. This information is mandatory. * * {@section Efficiency} * This method shall not load a large amount of data, since it may be invoked soon. * It is invoked just before {@link #getCoverage(IIOListeners)} in order to log a * "<cite>Loading data...</cite>" message. * * @return The coverage name. * @throws IOException if an I/O operation was required but failed. */ String getName() throws IOException; /** * Returns the <var>z</var> center for the coverage. * This information is mandatory. * * The Z center may not necessarily be at the center of the Z range. * It is common to have coverage with non linear values for additional * dimensions like time or elevation. * * @return The Z range center * @throws IOException if an I/O operation was required but failed. */ Number getZCenter() throws IOException; /** * Returns the minimum and maximum <var>z</var> values for the coverage. * This information is mandatory. * * {@section Performance note} * This method shall not load a large amount of data, since it may be invoked soon. * Note that this method may also be invoked often. * * @return The minimal and maximal values of coverage data. * @throws IOException if an I/O operation was required but failed. */ NumberRange<?> getZRange() throws IOException; /** * Returns the coverage envelope, or {@code null} if this information is too expensive to * compute. The envelope may or may not contains an extra dimension for the * {@linkplain #getZRange range of z values}, since the {@link CoverageStack} class is * tolerant in this regard. * * {@section Performance note} * This method shall not load a large amount of data, since it may be invoked soon. * * @return The coverage envelope, or {@code null} if this information is not available * or is too costly to compute. * @throws IOException if an I/O operation was required but failed. */ Envelope getEnvelope() throws IOException; /** * The coverage grid geometry, or {@code null} if this information do not applies or is too * expensive to compute. * * {@section Performance note} * This method shall not load a large amount of data, since it may be invoked soon. * * @return The grid geometry, or {@code null} if this information is not available, * not applicable or is too costly to compute. * @throws IOException if an I/O operation was required but failed. */ GridGeometry getGridGeometry() throws IOException; /** * The sample dimension for the coverage, or {@code null} if this information is too * expensive to compute. * * {@section Performance note} * This method shall not load a large amount of data, since it may be invoked soon. * * @return The sample dimensions, or {@code null} if this information is not available * or is too costly to compute. * @throws IOException if an I/O operation was required but failed. */ SampleDimension[] getSampleDimensions() throws IOException; /** * Returns the coverage, loading the data if needed. Implementations should invoke the * {@link IIOListeners#addListenersTo(ImageReader)} method if they use an image reader * for loading data. * * {@section Performance note} * The default {@link CoverageStack} implementation caches only the last coverages used. * Consequently, more sophisticated coverage caching (if desired) is implementor * responsibility. * * @param listeners Listeners to register to the {@linkplain ImageReader image I/O reader}, * if such a reader is going to be used. * @return The coverage (never {@code null}). * @throws IOException if a data loading was required but failed. */ Coverage getCoverage(IIOListeners listeners) throws IOException; } /** * A convenience adapter class for wrapping a pre-loaded {@link Coverage} into an * {@link Element} object. This adapter provides basic implementation for all methods, * but they require a fully constructed {@link Coverage} object. * <p> * Subclasses are strongly encouraged to provides alternative implementation loading * only the minimum amount of data required for each method. * * @author Martin Desruisseaux (IRD) * @since 2.1 * * @version 3.00 * @module */ public static class Adapter implements Element { /** * The wrapped coverage, or {@code null} if not yet loaded. * If null, the loading must be performed by the {@link #getCoverage} method. */ protected Coverage coverage; /** * Minimum and maximum <var>z</var> values for this element, or {@code null} if not yet * determined. If {@code null}, the range must be computed by the {@link #getZRange} method. */ protected NumberRange<?> range; /** * Center <var>z</var> value for this element, or {@code null} if not yet * determined. If {@code null}, the center must will computed as the center of the Z range. */ protected Number center; /** * Constructs a new adapter for the specified coverage and <var>z</var> values. * * @param coverage The coverage to wrap. Can be {@code null} only if this constructor * is invoked from a sub-class constructor. * @param range The minimum and maximum <var>z</var> values for this element, or * {@code null} to infers it from the last dimension in the coverage * envelope. */ public Adapter(final Coverage coverage, final NumberRange<?> range) { this(coverage,range,null); } /** * Constructs a new adapter for the specified coverage and <var>z</var> values. * * @param coverage The coverage to wrap. Can be {@code null} only if this constructor * is invoked from a sub-class constructor. * @param range The minimum and maximum <var>z</var> values for this element, or * {@code null} to infers it from the last dimension in the coverage * envelope. * @param center The center of the Z range or {@code null} for range middle. */ public Adapter(final Coverage coverage, final NumberRange<?> range, Number center) { this.coverage = coverage; this.range = range; this.center = center; if (getClass() == Adapter.class) { if (coverage == null) { throw new IllegalArgumentException( Errors.format(Errors.Keys.NullArgument_1, "coverage")); } } } /** * Returns the coverage name. The default implementation delegates to the * {@linkplain #getCoverage underlying coverage} if it is an instance of * {@link AbstractCoverage}. */ @Override public String getName() throws IOException { Object coverage = getCoverage(null); if (coverage instanceof AbstractCoverage) { coverage = ((AbstractCoverage) coverage).getName(); } return coverage.toString(); } @Override public Number getZCenter() throws IOException { if(center==null){ final NumberRange<?> range = getZRange(); if (range != null) { final Number lower = range.getMinValue(); final Number upper = range.getMaxValue(); if (lower != null) { if (upper != null) { center = 0.5 * (lower.doubleValue() + upper.doubleValue()); } else { center = lower.doubleValue(); } } else if (upper != null) { center = upper.doubleValue(); }else{ center = NaN; } }else{ center = NaN; } } return center; } /** * Returns the minimum and maximum <var>z</var> values for the coverage. If the range was * not explicitly specified to the constructor, then the default implementation infers it * from the last dimension in the coverage envelope. */ @Override public NumberRange<?> getZRange() throws IOException { if (range == null) { final Envelope envelope = getEnvelope(); final int zDimension = envelope.getDimension() - 1; range = NumberRange.create(envelope.getMinimum(zDimension), true, envelope.getMaximum(zDimension), true); } return range; } /** * Returns the coverage envelope. The default implementation delegates to the * {@linkplain #getCoverage underlying coverage}. */ @Override public Envelope getEnvelope() throws IOException { return getCoverage(null).getEnvelope(); } /** * Returns the coverage grid geometry. The default implementation delegates to the * {@linkplain #getCoverage underlying coverage} if it is an instance of * {@link GridCoverage}. */ @Override public GridGeometry getGridGeometry() throws IOException { final Coverage coverage = getCoverage(null); return (coverage instanceof GridCoverage) ? ((GridCoverage) coverage).getGridGeometry() : null; } /** * Returns the sample dimension for the coverage. The default implementation * delegates to the {@linkplain #getCoverage underlying coverage}. */ @Override public SampleDimension[] getSampleDimensions() throws IOException { final Coverage coverage = getCoverage(null); final SampleDimension[] sd = new SampleDimension[coverage.getNumSampleDimensions()]; for (int i=0; i<sd.length; i++) { sd[i] = coverage.getSampleDimension(i); } return sd; } /** * Returns the coverage. Implementors can overrides this method if they want to load * {@link #coverage} only when first needed. However, they are strongly encouraged to * override all other methods as well in order to load the minimum amount of data, * since all default implementations invoke {@code getCoverage(null)}. */ @Override public Coverage getCoverage(final IIOListeners listeners) throws IOException { return coverage; } } /** * Coverage elements in this stack. Elements may be shared by more than one * instances of {@code CoverageStack}. */ private final Element[] elements; /** * The sample dimensions for this coverage, or {@code null} if unknown. */ private final SampleDimension[] sampleDimensions; /** * The number of sample dimensions for this coverage, or 0 is unknown. * Note: this attribute may be different than zero even if {@link #sampleDimensions} is null. */ private final int numSampleDimensions; /** * The envelope for this coverage. This is the union of all elements envelopes. * * @see #getEnvelope */ private final GeneralEnvelope envelope; /** * A direct position having one less dimension than this coverage dimensions. * will be created only when first needed. */ private transient GeneralDirectPosition reducedPosition; /** * The dimension of the <var>z</var> ordinate. This is typically the * {@linkplain #getCoordinateReferenceSystem() Coordinate Reference System} dimension minus 1. * <p> * This field is named <cite>z dimension</cite> by convention only. The dimension doesn't * need to be the vertical axis. It can be a temporal or any other kind of dimension. * * @since 2.3 */ public final int zDimension; /** * The coordinate reference system for the {@linkplain #zDimension z dimension}, * or {@code null} if unknown. */ private final CoordinateReferenceSystem zCRS; /** * {@code true} if interpolations are allowed. */ private boolean interpolationEnabled = true; /** * Maximal interval between the upper z-value of a coverage and the lower z-value of the next * one. If a greater difference is found, we will consider that there is a hole in the data * and {@code evaluate(...)} methods will returns NaN for <var>z</var> values in this hole. */ private final double lagTolerance = 0; /** * List of objects to inform when image loading are trigged. */ private final IIOListeners listeners = new IIOListeners(); /** * Internal listener for logging image loading. */ private transient Listeners readListener; /** * Coverage with a minimum z-value lower than or equals to the requested <var>z</var> value. * If possible, this class will tries to select a coverage with a middle value (not just the * minimum value) lower than the requested <var>z</var> value. */ private transient Coverage lower; /** * Coverage with a maximum z-value higher than or equals to the requested <var>z</var> value. * If possible, this class will tries to select a coverage with a middle value (not just the * maximum value) higher than the requested <var>z</var> value. */ private transient Coverage upper; /** * <var>Z</var> values in the middle of {@link #lower} and {@link #upper} envelope. */ private transient double lowerZ = POSITIVE_INFINITY, upperZ = NEGATIVE_INFINITY; /** * Range for {@link #lower} and {@link #upper}. */ private transient NumberRange<?> lowerRange, upperRange; /** * Sample byte values. Allocated when first needed, in order to avoid allocating * them again every time an {@code evaluate(...)} method is invoked. */ private transient byte[] byteBuffer; /** * Sample integer values. Allocated when first needed, in order to avoid allocating * them again every time an {@code evaluate(...)} method is invoked. */ private transient int[] intBuffer; /** * Sample float values. Allocated when first needed, in order to avoid allocating * them again every time an {@code evaluate(...)} method is invoked. */ private transient float[] floatBuffer; /** * Sample double values. Allocated when first needed, in order to avoid allocating * them again every time an {@code evaluate(...)} method is invoked. */ private transient double[] doubleBuffer; /** * Initializes fields after deserialization. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); lowerZ = POSITIVE_INFINITY; upperZ = NEGATIVE_INFINITY; } /** * Constructs a new coverage stack with all the supplied elements. Every coverages * <strong>must</strong> specify their <var>z</var> range in the last dimension of * their {@linkplain Coverage#getEnvelope envelope}, and at least one of those envelopes * shall be {@linkplain Envelope#getCoordinateReferenceSystem associated with a CRS} * (the later is always the case with Geotk implementations of {@link Coverage}). * The example below constructs two dimensional grid coverages (to be given as the * {@code coverages} argument) for the same geographic area, but at different elevations: * * {@preformat java * GridCoverageFactory factory = ...; * CoordinateReferenceSystem crs2D = ...; // Yours horizontal CRS. * TemporalCRS timeCRS = ...; // Yours CRS for time measurement. * CoordinateReferenceSystem crs3D = new CompoundCRS(crs3D, timeCRS); * * List<Coverage> coverages = new ArrayList<Coverage>(); * GeneralEnvelope envelope = new GeneralEnvelope(AbstractCRS.castOrCopy(CommonCRS.WGS84.geographic3D()).forConvention(AxesConvention.RIGHT_HANDED)); * envelope.setRange(0, westLongitudeBound, eastLongitudeBound); * envelope.setRange(1, southLatitudeBound, northLatitudeBound); * for (int i=0; i<...; i++) { * envelope.setRange(2, minElevation, maxElevation); * coverages.add(factory.create(..., crs, envelope, ...)); * } * } * * This convenience constructor wraps all coverage into {@link Adapter Adapter} objects. * Users with a significant amount of data are encouraged to use the constructor expecting * {@link Element Element} objects instead, in order to provide their own implementation * loading data only when needed. * * @param name The name for this coverage. * @param coverages All {@link Coverage} elements for this stack. * @throws IOException if an I/O operation was required and failed. */ public CoverageStack(final CharSequence name, final Collection<? extends Coverage> coverages) throws IOException { this(name, (CoordinateReferenceSystem) null, toElements(coverages), null); } /** * Same as {@link #CoverageStack(CharSequence, java.util.Collection)} with specified zDimension. * @param name The name for this coverage. * @param coverages All {@link Coverage} elements for this stack. * @param zDimension Dimension index in CRS where Z varies. If null, use the last dimension * @throws IOException if an I/O operation was required and failed. */ public CoverageStack(final CharSequence name, final Collection<? extends Coverage> coverages, final Integer zDimension) throws IOException { this(name, (CoordinateReferenceSystem) null, toElements(coverages), zDimension); } /** * Workaround for RFE #4093999 ("Relax constraint on placement of this()/super() * call in constructors"). */ private static Element[] toElements(final Collection<? extends Coverage> coverages) { final Element[] elements = new Element[coverages.size()]; int count = 0; for (final Coverage coverage : coverages) { elements[count++] = new Adapter(coverage, null); } return elements; } /** * Constructs a new coverage stack with all the supplied elements. Each element can specify * its <var>z</var> range either in the last dimension of its {@linkplain Element#getEnvelope * envelope}, or as a separated {@linkplain Element#getZRange range}. * <p> * If {@code crs} is {@code null}, this constructor will try to infer the CRS from the * {@linkplain Element#getEnvelope element envelope} but at least one of those must be * associated with a full CRS (including the <var>z</var> dimension). * * @param name The name for this coverage. * @param crs The coordinate reference system for this coverage, or {@code null}. * @param elements All coverage {@link Element Element}s for this stack. * @throws IOException if an I/O operation was required and failed. */ public CoverageStack(final CharSequence name, final CoordinateReferenceSystem crs, final Collection<? extends Element> elements) throws IOException { this(name, crs, elements.toArray(new Element[elements.size()]), null); } /** * Constructs a new coverage stack with all the supplied elements. Each element can specify * its <var>z</var> range either in the last dimension of its {@linkplain Element#getEnvelope * envelope}, or as a separated {@linkplain Element#getZRange range}. * <p> * If {@code crs} is {@code null}, this constructor will try to infer the CRS from the * {@linkplain Element#getEnvelope element envelope} but at least one of those must be * associated with a full CRS (including the <var>z</var> dimension). * * @param name The name for this coverage. * @param crs The coordinate reference system for this coverage, or {@code null}. * @param elements All coverage {@link Element Element}s for this stack. * @param zDimension Dimension index in CRS where Z varies. If null, use the last dimension * @throws IOException if an I/O operation was required and failed. */ public CoverageStack(final CharSequence name, final CoordinateReferenceSystem crs, final Collection<? extends Element> elements, final Integer zDimension) throws IOException { this(name, crs, elements.toArray(new Element[elements.size()]), zDimension); } /** * Constructs a new coverage stack with all the supplied elements. The {@code elements} * array will be modified, so it should never be a direct reference to a user argument. * This constructor should stay private for that reason. * * @param name The name for this coverage. * @param crs The coordinate reference system for this coverage. * @param elements All coverage {@link Element Element}s for this stack. * @param zDimension Dimension index in CRS where Z varies. If null, use the last dimension * @throws IOException if an I/O operation was required and failed. */ private CoverageStack(final CharSequence name, final CoordinateReferenceSystem crs, final Element[] elements, final Integer zDimension) throws IOException { this(name, getEnvelope(crs, elements), elements, zDimension); // 'elements' must be after 'getEnvelope' } /** * Workaround for RFE #4093999 ("Relax constraint on placement of this()/super() * call in constructors"). */ private CoverageStack(final CharSequence name, final GeneralEnvelope envelope, final Element[] elements, final Integer zDimension) throws IOException { super(name, envelope.getCoordinateReferenceSystem(), null, null); assert ArraysExt.isSorted(elements, COMPARATOR, false); this.elements = elements; this.envelope = envelope; this.zDimension = zDimension != null ? zDimension : envelope.getDimension() - 1; boolean sampleDimensionMismatch = false; SampleDimension[] sampleDimensions = null; for (final Element element : elements) { /* * Ensures that all coverages uses the same number of sample dimension. * To be strict, we should ensure that all sample dimensions are identical. * However, this is not needed for proper working of this class, so we will * ensure this condition only in 'getSampleDimension' method. */ final SampleDimension[] candidate = element.getSampleDimensions(); if (candidate != null) { if (sampleDimensions == null) { sampleDimensions = candidate; } else { if (sampleDimensions.length != candidate.length) { throw new IllegalArgumentException( // TODO: localize "Inconsistent number of sample dimensions."); } if (!Arrays.equals(sampleDimensions, candidate)) { sampleDimensionMismatch = true; } } } } this.numSampleDimensions = (sampleDimensions != null) ? sampleDimensions.length : 0; this.sampleDimensions = sampleDimensionMismatch ? null : sampleDimensions; zCRS = CRS.getOrCreateSubCRS(crs, this.zDimension, this.zDimension+1); } /** * Constructs a new coverage using the same elements than the specified coverage stack. * * @param name The coverage name. * @param source The stack to copy. */ protected CoverageStack(final CharSequence name, final CoverageStack source) { super(name, source); // TODO: Put synchronization before the call to super(...) if Sun fixes RFE #4093999 // ("Relax constraint on placement of this()/super() call in constructors"). synchronized (source) { elements = source.elements; sampleDimensions = source.sampleDimensions; numSampleDimensions = source.numSampleDimensions; envelope = source.envelope; zDimension = source.zDimension; zCRS = source.zCRS; interpolationEnabled = source.interpolationEnabled; } } /** * Returns the envelope for the given elements. If {@code crs} is {@code null}, * then the most frequently used CRS will be selected. * <p> * The {@code elements} array will be modified, so it should never be a direct reference * to a user argument. This method should stay private for that reason. * * @param crs The coordinate reference system for the coverage, or {@code null}. * @param elements All coverage {@link Element Element}s for the coverage stack. * @return The envelope in the CRS to be used for the coverage stack (never {@code null}). * @throws IOException if an I/O operation was required and failed. */ @SuppressWarnings("fallthrough") private static GeneralEnvelope getEnvelope(CoordinateReferenceSystem crs, final Element[] elements) throws IOException { try { Arrays.sort(elements, COMPARATOR); } catch (BackingStoreException exception) { throw exception.unwrapOrRethrow(IOException.class); } /* * If no CRS was specified, selects the most frequently used one. The loop below memorizes * the envelopes in order to avoid asking them a second time in the loop after. */ Envelope[] envelopes = null; int zDimension = 0; short errorCode = Errors.Keys.IllegalCoordinateReferenceSystem; // In case of error if (crs == null) { errorCode = Errors.Keys.MismatchedCoordinateReferenceSystem; FrequencySortedSet<CoordinateReferenceSystem> frequency = null; for (int i=0; i<elements.length; i++) { final Envelope envelope = elements[i].getEnvelope(); if (envelope != null) { if (envelopes == null) { envelopes = new Envelope[elements.length]; } envelopes[i] = envelope; final CoordinateReferenceSystem candidate = envelope.getCoordinateReferenceSystem(); if (candidate != null) { if (frequency == null) { frequency = new FrequencySortedSet<>(true); } frequency.add(candidate); } final int dimension = envelope.getDimension(); if (dimension > zDimension) { zDimension = dimension - 1; } } } /* * At this point, all CRS have been added in the frequency set. Now inspect the result. * If there is more than one CRS, logs a warning and selects the most frequently used. */ if (frequency != null) { final int size = frequency.size(); switch (size) { default: { final int[] f = frequency.frequencies(); final LogRecord record = Loggings.format(Level.WARNING, Loggings.Keys. FoundMismatchedCRS_4, size, elements.length, f[0], f[size-1]); record.setSourceClassName(CoverageStack.class.getName()); record.setSourceMethodName("<init>"); // This is the public method invoked. final Logger logger = Logging.getLogger("org.geotoolkit.coverage"); record.setLoggerName(logger.getName()); logger.log(record); // Fall through } case 1: { crs = frequency.first(); // Fall through } case 0: break; } } } /* * Now we should know the CRS. Discarts the old 'zDimension', which was only a fallback. * If we don't know the CRS, keep the 'zDimension' fallback which should be inferred from * the envelope with the greatest amount of dimensions. */ if (crs != null) { zDimension = crs.getCoordinateSystem().getDimension() - 1; } if (zDimension <= 0) { throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedCrs_1, (crs == null) ? "null" : crs.getName().getCode())); } /* * Prepares the envelope, to be computed in the loop later and returned by this method. * Ordinates are initialized to NaN. They will stay NaN if none of the supplied elements * can provide an envelope. */ final GeneralEnvelope envelope; if (crs != null) { envelope = new GeneralEnvelope(crs); } else { envelope = new GeneralEnvelope(zDimension + 1); } envelope.setToNaN(); /* * Computes a CRS without the z dimension (which is assumed to be the last one, as * specified in the javadoc). A coordinate operation is cached during the loop for * transforming envelopes, if needed. */ final CoordinateReferenceSystem reducedCRS = CRS.getOrCreateSubCRS(crs, 0, zDimension); CoordinateOperation operation = null; for (int j=0; j<elements.length; j++) { final Element element = elements[j]; Envelope candidate = (envelopes != null) ? envelopes[j] : element.getEnvelope(); if (candidate == null) { continue; } /* * Computes an envelope for all coverage elements. If a coordinate reference system * information is bundled with the envelope, it will be used in order to reproject * the envelope on the fly (if needed). Otherwise, CRS are assumed the same than the * one specified at construction time. */ final CoordinateReferenceSystem sourceCRS = candidate.getCoordinateReferenceSystem(); if (sourceCRS != null && !equalsIgnoreMetadata(sourceCRS, crs) && !equalsIgnoreMetadata(sourceCRS, reducedCRS)) { // A transformation is required. Reuse the previous operation if possible. if (operation==null || !equalsIgnoreMetadata(sourceCRS, operation.getSourceCRS())) { CoordinateOperationFactory factory = CRS.getCoordinateOperationFactory(true); try { try { // Try a transformation to the full target CRS including z dimension. operation = factory.createOperation(sourceCRS, crs); } catch (OperationNotFoundException e) { // Try a transformation to the target CRS without z dimension. assert !equalsIgnoreMetadata(reducedCRS, crs) : reducedCRS; operation = factory.createOperation(sourceCRS, reducedCRS); } } catch (FactoryException e) { throw new MismatchedReferenceSystemException(Errors.format(errorCode, e)); } } try { candidate = Envelopes.transform(operation, candidate); } catch (TransformException exception) { throw new MismatchedReferenceSystemException(Errors.format(errorCode, exception)); } } /* * Increase the envelope in order to contains 'candidate'. * The range of z-values will be included in the envelope. */ final int dim = candidate.getDimension(); for (int i=0; i<=zDimension; i++) { double min = envelope.getMinimum(i); double max = envelope.getMaximum(i); final double minimum, maximum; if (i < dim) { minimum = candidate.getMinimum(i); maximum = candidate.getMaximum(i); } else if (i == zDimension) { final NumberRange<?> range = element.getZRange(); minimum = range.getMinDouble(); maximum = range.getMaxDouble(); } else { minimum = NEGATIVE_INFINITY; maximum = POSITIVE_INFINITY; } boolean changed = false; if (Double.isNaN(min) || minimum < min) {min = minimum; changed = true;} if (Double.isNaN(max) || maximum > max) {max = maximum; changed = true;} if (changed) { envelope.setRange(i, min, max); } } } return envelope; } /** * A comparator for {@link Element} sorting and binary search. This comparator uses the * middle <var>z</var> value as criterion. It must accepts {@link Double} objects as well * as {@link Element}, because binary search will mix those two kinds of object. */ private static final Comparator<Object> COMPARATOR = new Comparator<Object>() { @Override public int compare(final Object entry1, final Object entry2) { try { return Double.compare(zFromObject(entry1), zFromObject(entry2)); } catch (IOException exception) { throw new BackingStoreException(exception); // Will be catch and rethrown as IOException // by all methods using this comparator. } } }; /** * Returns the <var>z</var> value of the specified object. The specified * object may be a {@link Double} or an {@link Element} instance. * * @param object The object to sort. * @return The z-value of the specified object. * @throws IOException if an I/O operation was required but failed. * @throws ClassCastException if {@code object} is not an instance of {@link Double} * or {@link Element}. */ private static double zFromObject(final Object object) throws IOException, ClassCastException { if (object instanceof Number) { return ((Number) object).doubleValue(); } return getZ((Element) object); } /** * Returns the middle <var>z</var> value. If the element has no <var>z</var> value * (for example if the <var>z</var> value is the time and the coverage is constant * over the time), then this method returns {@link Double#NaN}. */ private static double getZ(final Element entry) throws IOException { return entry.getZCenter().doubleValue(); } /** * Returns {@code true} if the specified z-value is inside the specified range. */ private static boolean contains(final NumberRange<?> range, final double z) { return z >= range.getMinDouble() && z <= range.getMaxDouble(); } /** * Used only by GridCoverageStack sub class to rebuild the grid geometry. * @return Element[] stack elements */ Element[] getElements() { return elements; } /** * Returns the bounding box for the coverage domain in coordinate system coordinates. */ @Override public Envelope getEnvelope() { return envelope.clone(); } /** * Returns the number of sample dimension in this coverage. */ @Override public int getNumSampleDimensions() { if (numSampleDimensions != 0) { return numSampleDimensions; } else { // TODO: provides a localized message. throw new IllegalStateException("Sample dimensions are undetermined."); } } /** * Retrieve sample dimension information for the coverage. * For a grid coverage, a sample dimension is a band. The sample dimension information * include such things as description, data type of the value (bit, byte, integer...), * the no data values, minimum and maximum values and a color table if one is associated * with the dimension. */ @Override public SampleDimension getSampleDimension(final int index) { if (sampleDimensions != null) { return sampleDimensions[index]; } else { // TODO: provides a localized message. throw new IllegalStateException("Sample dimensions are undetermined."); } } /** * Snaps the specified coordinate point to the coordinate of the nearest voxel available in * this coverage. Invoking any {@code evaluate(...)} method with snapped coordinates will * return non-interpolated values. * <p> * <ul> * <li>First, this method locates the {@linkplain Element coverage element} at or near * the last ordinate value (the <var>z</var> value). If no coverage is available at * the specified <var>z</var> value, then the nearest one is selected.</li> * <li>Next, this method locates the pixel under the {@code point} coordinate in the * coverage element.</li> * <li>The {@code point} is then set to the pixel center coordinate and to the * <var>z</var> value of the selected coverage element.</li> * </ul> * * @param point The point to snap. * @throws IOException if an I/O operation was required but failed. */ public void snap(final DirectPosition point) throws IOException { // No synchronization needed. double z = point.getOrdinate(zDimension); int index; try { index = Arrays.binarySearch(elements, Double.valueOf(z), COMPARATOR); } catch (BackingStoreException exception) { throw exception.unwrapOrRethrow(IOException.class); } if (index < 0) { /* * There is no exact match for the z value. * Snap it to the closest coverage element. */ index = ~index; if (index == elements.length) { if (index == 0) { return; // No elements in this coverage } z = getZ(elements[--index]); } else if (index == 0) { z = getZ(elements[index]); } else { final double lowerZ = getZ(elements[index-1]); final double upperZ = getZ(elements[index ]); assert !(z<=lowerZ || z>=upperZ) : z; // Use !(...) in order to accept NaN values. if (isNaN(upperZ) || z-lowerZ < upperZ-z) { index--; z = lowerZ; } else { z = upperZ; } } point.setOrdinate(zDimension, z); } /* * Now that we know the coverage element, * snap the spatial coordinate point. */ final Element element = elements[index]; final GridGeometry geometry = element.getGridGeometry(); if (geometry != null) { final GridEnvelope range = geometry.getExtent(); final MathTransform transform = geometry.getGridToCRS(); final int dimension = transform.getSourceDimensions(); DirectPosition position = new GeneralDirectPosition(dimension); for (int i=dimension; --i>=0;) { // Copy only the first dimensions (may not be up to crs.dimension) position.setOrdinate(i, point.getOrdinate(i)); } try { position = transform.inverse().transform(position, position); for (int i=dimension; --i>=0;) { position.setOrdinate(i, Math.max(range.getLow(i), Math.min(range.getHigh(i), (int)Math.rint(position.getOrdinate(i))))); } position = transform.transform(position, position); for (int i=Math.min(dimension, zDimension); --i>=0;) { // Do not touch the z-value, copy the other ordinates. point.setOrdinate(i, position.getOrdinate(i)); } } catch (TransformException exception) { throw new CannotEvaluateException(cannotEvaluate(point), exception); } } } /** * Returns a message for exception. * * @todo provides a better formatting of the point coordinate. */ private static String cannotEvaluate(final DirectPosition point) { return Errors.format(Errors.Keys.CantEvaluateForCoordinate_1, point); } /** * Loads a single coverage for the specified element. All {@code evaluate(...)} methods * ultimately loads their coverages through this method. It provides a single place where * to add post-loading processing, if needed. * * @param element The coverage to load. * @return The loaded coverage. * @throws IOException if an error occurred while loading image. */ private Coverage load(final Element element) throws IOException { assert Thread.holdsLock(this); Coverage coverage = element.getCoverage(listeners); if (coverage instanceof GridCoverage2D) { final GridCoverage2D coverage2D = (GridCoverage2D) coverage; if (interpolationEnabled) { if (coverage2D.getInterpolation() instanceof InterpolationNearest) { coverage = Interpolator2D.create(coverage2D); } } } /* * CRS assertions (for debugging purpose). */ final CoordinateReferenceSystem sourceCRS; assert debugEquals((sourceCRS = coverage.getCoordinateReferenceSystem()), CRS.getOrCreateSubCRS(crs, 0, sourceCRS.getCoordinateSystem().getDimension())) : sourceCRS; assert coverage.getNumSampleDimensions() == numSampleDimensions : coverage; return coverage; } /** * Loads a single image at the given index. * * @param index Index in {@link #elements} for the image to load. * @throws IOException if an error occurred while loading image. */ private void load(final int index) throws IOException { final Element element = elements[index]; final NumberRange<?> zRange = element.getZRange(); logLoading(Vocabulary.Keys.LoadingImage_1, new String[] {element.getName()}); lower = upper = load(element); lowerZ = upperZ = getZ(element); lowerRange = upperRange = zRange; } /** * Loads images for the given elements. * * @throws IOException if an error occurred while loading images. */ private void load(final Element lowerElement, final Element upperElement) throws IOException { logLoading(Vocabulary.Keys.LoadingImages_2, new String[] { lowerElement.getName(), upperElement.getName() }); final NumberRange<?> lowerRange = lowerElement.getZRange(); final NumberRange<?> upperRange = upperElement.getZRange(); final Coverage lower = load(lowerElement); final Coverage upper = load(upperElement); this.lower = lower; // Set only when BOTH images are OK. this.upper = upper; this.lowerZ = getZ(lowerElement); this.upperZ = getZ(upperElement); this.lowerRange = lowerRange; this.upperRange = upperRange; } /** * Loads coverages required for a linear interpolation at the specified <var>z</var> value. * The loaded coverages will be stored in {@link #lower} and {@link #upper} fields. It is * possible that the same coverage is given to those two fields, if this method determine * that no interpolation is necessary. * * @param z The z value. * @return {@code true} if data were found. * @throws PointOutsideCoverageException if the <var>z</var> value is outside the allowed range. * @throws CannotEvaluateException if the operation failed for some other reason. */ private boolean seek(final double z) throws CannotEvaluateException { assert Thread.holdsLock(this); /* * Check if currently loaded coverages * are valid for the requested z value. */ if ((z>=lowerZ && z<=upperZ) || (isNaN(z) && isNaN(lowerZ) && isNaN(upperZ))) { return true; } /* * Currently loaded coverages are not valid for the requested z value. * Search for the coverage to use as upper bounds ({@link #upper}). */ final Number Z = Double.valueOf(z); int index; try { index = Arrays.binarySearch(elements, Z, COMPARATOR); } catch (BackingStoreException exception) { // TODO: localize throw new CannotEvaluateException("Can't fetch coverage properties.", exception.unwrapOrRethrow(IOException.class)); } try { if (index >= 0) { /* * An exact match has been found. * Load only this coverage and exit. */ load(index); return true; } index = ~index; // Insertion point (note: ~ is NOT the minus sign). if (index == elements.length) { if (--index >= 0) { // Does this stack has at least 1 coverage? /* * The requested z is after the last coverage's central z. * Maybe it is not after the last coverage's upper z. Check... */ if (elements[index].getZRange().containsAny(Z)) { load(index); return true; } } // fall through the exception at this method's end. } else if (index == 0) { /* * The requested z is before the first coverage's central z. * Maybe it is not before the first coverage's lower z. Check... */ if (elements[index].getZRange().containsAny(Z)) { load(index); return true; } // fall through the exception at this method's end. } else { /* * An interpolation between two coverages seems possible. * Checks if there is not a z lag between both. */ final Element lowerElement = elements[index-1]; final Element upperElement = elements[index ]; final NumberRange<?> lowerRange = lowerElement.getZRange(); final NumberRange<?> upperRange = upperElement.getZRange(); final double lowerEnd = lowerRange.getMaxDouble(); final double upperStart = upperRange.getMinDouble(); if (lowerEnd + lagTolerance >= upperStart) { if (interpolationEnabled) { load(lowerElement, upperElement); } else { if (Math.abs(getZ(upperElement)-z) > Math.abs(z-getZ(lowerElement))) { index--; } load(index); } return true; } if (lowerRange.containsAny(Z)) { load(index-1); return true; } if (upperRange.containsAny(Z)) { load(index); return true; } return false; // Missing data. } } catch (IOException exception) { String message = exception.getLocalizedMessage(); if (message == null) { message = Classes.getShortClassName(exception); } throw new CannotEvaluateException(message, exception); } final Object Zp; if (zCRS instanceof TemporalCRS) { Zp = DefaultTemporalCRS.castOrCopy((TemporalCRS) zCRS).toDate(z); } else { Zp = Z; } throw new OrdinateOutsideCoverageException(Errors.format( Errors.Keys.ZvalueOutsideCoverage_2, getName(), Zp), zDimension, getEnvelope()); } /** * Returns a point with the same number of dimensions than the specified coverage. * The number of dimensions must be equals to the dimension of this coverage, or * the dimension of this coverage minus one. */ private DirectPosition reduce(DirectPosition coord, final Coverage coverage) { assert Thread.holdsLock(this); final CoordinateReferenceSystem targetCRS = coverage.getCoordinateReferenceSystem(); final int dimension = targetCRS.getCoordinateSystem().getDimension(); if (dimension == zDimension) { if (reducedPosition == null) { reducedPosition = new GeneralDirectPosition(zDimension); } for (int i=0; i<dimension; i++) { reducedPosition.ordinates[i] = coord.getOrdinate(i); } coord = reducedPosition; } else { assert debugEquals(crs, targetCRS) : targetCRS; } return coord; } /** * Returns a sequence of values for a given point in the coverage. The default implementation * delegates to the {@link #evaluate(DirectPosition, double[])} method. * * @param coord The coordinate point where to evaluate. * @return The value at the specified point. * @throws PointOutsideCoverageException if {@code coord} is outside coverage. * @throws CannotEvaluateException if the computation failed for some other reason. */ @Override public Object evaluate(final DirectPosition coord) throws PointOutsideCoverageException, CannotEvaluateException { return evaluate(coord, (double[]) null); } /** * Returns a sequence of boolean values for a given point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null} to create a new array. * @return The {@code dest} array, or a newly created array if {@code dest} was null. * @throws PointOutsideCoverageException if {@code coord} is outside coverage. * @throws CannotEvaluateException if the computation failed for some other reason. */ @Override public synchronized boolean[] evaluate(final DirectPosition coord, boolean[] dest) throws PointOutsideCoverageException, CannotEvaluateException { final double z = coord.getOrdinate(zDimension); if (!seek(z)) { // Missing data if (dest == null) { dest = new boolean[numSampleDimensions]; } else { Arrays.fill(dest, 0, numSampleDimensions, false); } return dest; } if (lower == upper) { return lower.evaluate(reduce(coord, lower), dest); } assert !(z<lowerZ || z>upperZ) : z; // Uses !(...) in order to accepts NaN. final Coverage coverage = (z >= 0.5*(lowerZ + upperZ)) ? upper : lower; return coverage.evaluate(reduce(coord, coverage), dest); } /** * Returns a sequence of byte values for a given point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null} to create a new array. * @return The {@code dest} array, or a newly created array if {@code dest} was null. * @throws PointOutsideCoverageException if {@code coord} is outside coverage. * @throws CannotEvaluateException if the computation failed for some other reason. */ @Override public synchronized byte[] evaluate(final DirectPosition coord, byte[] dest) throws PointOutsideCoverageException, CannotEvaluateException { final double z = coord.getOrdinate(zDimension); if (!seek(z)) { // Missing data if (dest == null) { dest = new byte[numSampleDimensions]; } else { Arrays.fill(dest, 0, numSampleDimensions, (byte) 0); } return dest; } if (lower == upper) { return lower.evaluate(reduce(coord, lower), dest); } byteBuffer = upper.evaluate(reduce(coord, upper), byteBuffer); dest = lower.evaluate(reduce(coord, lower), dest); assert !(z<lowerZ || z>upperZ) : z; // Uses !(...) in order to accepts NaN. final double ratio = (z - lowerZ) / (upperZ - lowerZ); for (int i=0; i<byteBuffer.length; i++) { dest[i] = (byte) Math.round(dest[i] + ratio*(byteBuffer[i] - dest[i])); } return dest; } /** * Returns a sequence of integer values for a given point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null} to create a new array. * @return The {@code dest} array, or a newly created array if {@code dest} was null. * @throws PointOutsideCoverageException if {@code coord} is outside coverage. * @throws CannotEvaluateException if the computation failed for some other reason. */ @Override public synchronized int[] evaluate(final DirectPosition coord, int[] dest) throws PointOutsideCoverageException, CannotEvaluateException { final double z = coord.getOrdinate(zDimension); if (!seek(z)) { // Missing data if (dest == null) { dest = new int[numSampleDimensions]; } else { Arrays.fill(dest, 0, numSampleDimensions, 0); } return dest; } if (lower == upper) { return lower.evaluate(reduce(coord, lower), dest); } intBuffer = upper.evaluate(reduce(coord, upper), intBuffer); dest = lower.evaluate(reduce(coord, lower), dest); assert !(z<lowerZ || z>upperZ) : z; // Uses !(...) in order to accepts NaN. final double ratio = (z - lowerZ) / (upperZ - lowerZ); for (int i=0; i<intBuffer.length; i++) { dest[i] = (int) Math.round(dest[i] + ratio*(intBuffer[i] - dest[i])); } return dest; } /** * Returns a sequence of float values for a given point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null} to create a new array. * @return The {@code dest} array, or a newly created array if {@code dest} was null. * @throws PointOutsideCoverageException if {@code coord} is outside coverage. * @throws CannotEvaluateException if the computation failed for some other reason. */ @Override public synchronized float[] evaluate(final DirectPosition coord, float[] dest) throws PointOutsideCoverageException, CannotEvaluateException { final double z = coord.getOrdinate(zDimension); if (!seek(z)) { // Missing data if (dest == null) { dest = new float[numSampleDimensions]; } Arrays.fill(dest, 0, numSampleDimensions, Float.NaN); return dest; } if (lower == upper) { return lower.evaluate(reduce(coord, lower), dest); } floatBuffer = upper.evaluate(reduce(coord, upper), floatBuffer); dest = lower.evaluate(reduce(coord, lower), dest); assert !(z<lowerZ || z>upperZ) : z; // Uses !(...) in order to accepts NaN. final double ratio = (z - lowerZ) / (upperZ - lowerZ); for (int i=0; i<floatBuffer.length; i++) { final float lower = dest[i]; final float upper = floatBuffer[i]; float value = (float) (lower + ratio*(upper - lower)); if (Float.isNaN(value)) { if (!Float.isNaN(lower)) { assert Float.isNaN(upper) : upper; if (contains(lowerRange, z)) { value = lower; } } else if (!Float.isNaN(upper)) { assert Float.isNaN(lower) : lower; if (contains(upperRange, z)) { value = upper; } } } dest[i] = value; } return dest; } /** * Returns a sequence of double values for a given point in the coverage. * * @param coord The coordinate point where to evaluate. * @param dest An array in which to store values, or {@code null} to create a new array. * @return The {@code dest} array, or a newly created array if {@code dest} was null. * @throws PointOutsideCoverageException if {@code coord} is outside coverage. * @throws CannotEvaluateException if the computation failed for some other reason. */ @Override public synchronized double[] evaluate(final DirectPosition coord, double[] dest) throws PointOutsideCoverageException, CannotEvaluateException { final double z = coord.getOrdinate(zDimension); if (!seek(z)) { // Missing data if (dest == null) { dest = new double[numSampleDimensions]; } Arrays.fill(dest, 0, numSampleDimensions, NaN); return dest; } if (lower == upper) { return lower.evaluate(reduce(coord, lower), dest); } doubleBuffer = upper.evaluate(reduce(coord, upper), doubleBuffer); dest = lower.evaluate(reduce(coord, lower), dest); assert !(z<lowerZ || z>upperZ) : z; // Uses !(...) in order to accepts NaN. final double ratio = (z - lowerZ) / (upperZ - lowerZ); for (int i=0; i<doubleBuffer.length; i++) { final double lower = dest[i]; final double upper = doubleBuffer[i]; double value = lower + ratio*(upper - lower); if (isNaN(value)) { if (!isNaN(lower)) { assert isNaN(upper) : upper; if (contains(lowerRange, z)) { value = lower; } } else if (!isNaN(upper)) { assert isNaN(lower) : lower; if (contains(upperRange, z)) { value = upper; } } } dest[i] = value; } return dest; } /** * Returns the coverages to be used for the specified <var>z</var> value. Special cases: * <p> * <ul> * <li>If there is no coverage available for the specified <var>z</var> value, returns * an {@linkplain Collections#EMPTY_LIST empty list}.</li> * <li>If there is only one coverage available, or if the specified <var>z</var> value * falls exactly in the middle of the {@linkplain Element#getZRange range value} * (i.e. no interpolation are needed), or if {@linkplain #setInterpolationEnabled * interpolations are disabled}, then this method returns a * {@linkplain Collections#singletonList singleton}.</li> * <li>Otherwise, this method returns a list containing at least 2 coverages, one before * and one after the specified <var>z</var> value.</li> * </ul> * * @param z The z value for the coverages to be returned. * @return The coverages for the specified values. May contains 0, 1 or 2 elements. * * @since 2.3 */ public synchronized List<Coverage> coveragesAt(final double z) { if (!seek(z)) { return Collections.emptyList(); } if (lower == upper) { return Collections.singletonList(lower); } return Arrays.asList(new Coverage[] {lower, upper}); } /** * Returns the crs dimension index from where the <var>z</var> varies. * This information is mandatory. * * @return z dimension index */ protected int getZDimension() { return zDimension; } /** * Return the number of elements. * @return element size */ public int getStackSize() { return elements.length; } /** * Return coverage at specified index. Index must be between 0 and {@link #getStackSize()} - 1. * @param index Index in {@link #elements} for the image to load. * @return Coverage in specified index. */ public synchronized Coverage coverageAtIndex(int index) { try { load(index); return lower; } catch (IOException exception) { String message = exception.getLocalizedMessage(); if (message == null) { message = Classes.getShortClassName(exception); } throw new CannotEvaluateException(message, exception); } } /** * Returns {@code true} if interpolation are enabled in the <var>z</var> value dimension. * Interpolations are enabled by default. * * @return {@code true} if interpolation are enabled in the <var>z</var> value dimension. */ public synchronized boolean isInterpolationEnabled() { return interpolationEnabled; } /** * Enable or disable interpolations in the <var>z</var> value dimension. * * @param enabled {@code true} if interpolation should be enabled in the <var>z</var> value dimension. */ public synchronized void setInterpolationEnabled(final boolean enabled) { lower = null; upper = null; lowerZ = POSITIVE_INFINITY; upperZ = NEGATIVE_INFINITY; interpolationEnabled = enabled; } /** * Adds an {@link IIOReadWarningListener} to the list of registered warning listeners. * * @param listener The listener to add. */ public void addIIOReadWarningListener(final IIOReadWarningListener listener) { listeners.addIIOReadWarningListener(listener); } /** * Removes an {@link IIOReadWarningListener} from the list of registered warning listeners. * * @param listener The listener to remove. */ public void removeIIOReadWarningListener(final IIOReadWarningListener listener) { listeners.removeIIOReadWarningListener(listener); } /** * Adds an {@link IIOReadProgressListener} to the list of registered progress listeners. * * @param listener The listener to add. */ public void addIIOReadProgressListener(final IIOReadProgressListener listener) { listeners.addIIOReadProgressListener(listener); } /** * Removes an {@link IIOReadProgressListener} from the list of registered progress listeners. * * @param listener The listener to remove. */ public void removeIIOReadProgressListener(final IIOReadProgressListener listener) { listeners.removeIIOReadProgressListener(listener); } /** * Invoked automatically when an image is about to be loaded. The default implementation * logs the message in the {@code "org.geotoolkit.coverage"} logger. Subclasses can override * this method if they wants a different logging. * * @param record The log record. The message contains information about the images to load. */ protected void logLoading(final LogRecord record) { final Logger logger = Logging.getLogger("org.geotoolkit.coverage"); record.setLoggerName(logger.getName()); logger.log(record); } /** * Prepares a log record about an image to be loaded, and put the log record in a stack. * The record will be effectively logged only when image loading really beging. */ private void logLoading(final short key, final Object[] parameters) { final Locale locale = null; final LogRecord record = Vocabulary.getResources(locale).getLogRecord(Level.INFO, key); record.setSourceClassName(CoverageStack.class.getName()); record.setSourceMethodName("evaluate"); record.setParameters(parameters); if (readListener == null) { readListener = new Listeners(); addIIOReadProgressListener(readListener); } readListener.record = record; } /** * A listener for monitoring image loading. The purpose for this listener is to * log a message when an image is about to be loaded. * * @author Martin Desruisseaux (IRD) * @version 3.00 * * @since 2.1 * @module */ private final class Listeners extends IIOReadProgressAdapter { /** * The record to log. */ public LogRecord record; /** * Reports that an image read operation is beginning. */ @Override public void imageStarted(ImageReader source, int imageIndex) { if (record != null) { logLoading(record); source.removeIIOReadProgressListener(this); record = null; } } } }