/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2003-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.coverage; import java.util.List; import java.util.Locale; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; 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 java.lang.reflect.UndeclaredThrowableException; import org.opengis.coverage.Coverage; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.CannotEvaluateException; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.coverage.grid.GridGeometry; import org.opengis.coverage.grid.GridCoverage; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.TemporalCRS; 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.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.Interpolator2D; import org.geotools.geometry.GeneralDirectPosition; import org.geotools.geometry.GeneralEnvelope; import org.geotools.image.io.IIOListeners; import org.geotools.image.io.IIOReadProgressAdapter; import org.geotools.util.NumberRange; import org.geotools.util.FrequencySortedSet; import org.geotools.util.logging.Logging; import org.geotools.resources.XArray; import org.geotools.resources.Classes; import org.geotools.resources.CRSUtilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Loggings; import org.geotools.resources.i18n.LoggingKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.referencing.CRS; import org.geotools.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 static org.geotools.referencing.CRS.equalsIgnoreMetadata; /** * Wraps a stack of {@linkplain Coverage coverages} as an extra dimension. For example this class * can wraps an array of {@link org.geotools.coverage.grid.GridCoverage2D} on the same geographic * area, but where each {@code GridCoverage2D} is for a different date. This {@code CoverageStack} * manages the two-dimensional coverages as if the whole set was a three-dimensional coverage. * <p> * Each {@linkplain Element coverage element} in the stack usually covers the same * {@linkplain Coverage#getEnvelope geographic area}, but this is not a requirement. As of GeoTools * 2.5, elements are not required to have the same {@linkplain CoordinateReferenceSystem coordinate * reference system} neither, but they are required to handle the transformation from this coverage * CRS to their CRS. * <p> * Coverage elements are often two-dimensional, but this is not a requirement. This stack will * simply append one dimension to the element CRS. Coverage elements may be themself backed by * instances of {@code CoverateStack}, thus allowing construction of coverages with four or more * dimensions. * <p> * For documentation purpose, the new dimension added by {@code CoverageStack} is called * <var>z</var>. But this is only a naming convention - the new dimension can really be * anything, including time. * <p> * {@code GridCoverage2D} objects tend to be big. In order to keep memory usage raisonable, this * implementation doesn't requires all {@code GridCoverage} objects at once. Instead, it requires * an array of {@link Element} objects, which will load the coverage content only when first * needed. 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 caching mechanism, * caching is the responsability of {@link Element} implementations. Note that this simple * caching mechanism is suffisient if {@code evaluate(...)} methods are invoked with increasing * <var>z</var> values. * <p> * Each coverage element is expected to extends 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. * <p> * {@code CoverageStack} implementation is thread-safe. * * @since 2.1 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class CoverageStack extends AbstractCoverage { /** * For compatibility during cross-version serialization. */ private static final long serialVersionUID = -7100201963376146053L; /** * An element in a {@linkplain CoverageStack coverage stack}. Each element is expected to * extents over a range of <var>z</var> values (the new dimensions appended by the * {@link CoverageStack} container). Implementations should be capable to returns coverage's * {@linkplain #getZRange range of z-values} without loading the coverage's data. If an * expensive loading is required, it should be delayed until the {@link #getCoverage} method * is invoked. If {@code getCoverage} is invoked more than once, caching (if desirable) is * implementor's responsability. * <p> * All methods declares {@link IOException} in their throws cause 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). * * @since 2.1 * @version $Id$ * @author Martin Desruisseaux */ public static interface Element { /** * Returns a name for the coverage. This method should not load a large amount of data, * since it may be invoked soon. This method is invoked just before {@link #getCoverage} * in order to log a "<cite>Loading data...</cite>" message. * * @throws IOException if an I/O operation was required but failed. */ String getName() throws IOException; /** * Returns the minimum and maximum <var>z</var> values for the coverage. * This information is mandatory. This method should not load a large * amount of data, since it may be invoked soon. Note that this method * may be invoked often, so it should be efficient. * * @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. This method should not load a large amount of data, since it * may be invoked soon. * * @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. This method should not load a large amount of data, since it may be * invoked soon. * * @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. This method should not load a large amount of data, since it * may be invoked soon. * * @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 invokes the * {@link IIOListeners#addListenersTo(ImageReader)} method if they use an image reader * for loading data. Caching (if desired) is implementor's responsability. The default * {@link CoverageStack} implementation caches only the last coverages used. * * @param listeners Listeners to register to the {@linkplain ImageReader image I/O reader}, * if such a reader is going to be used. * @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 a require a fully constructed {@link Coverage} object. Subclasses are strongly * encouraged to provides alternative implementation loading only the minimum amount of data * required for each method. * * @since 2.1 * @version $Id$ * @author Martin Desruisseaux */ 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; /** * 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's * envelope. */ public Adapter(final Coverage coverage, final NumberRange range) { this.coverage = coverage; this.range = range; if (getClass() == Adapter.class) { if (coverage == null) { throw new IllegalArgumentException( Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "coverage")); } } } /** * Returns the coverage name. The default implementation delegates to the * {@linkplain #getCoverage underlying coverage} if it is an instance of * {@link AbstractCoverage}. */ public String getName() throws IOException { Object coverage = getCoverage(null); if (coverage instanceof AbstractCoverage) { coverage = ((AbstractCoverage) coverage).getName(); } return coverage.toString(); } /** * 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's envelope. */ public NumberRange getZRange() throws IOException { if (range == null) { final Envelope envelope = getEnvelope(); final int zDimension = envelope.getDimension() - 1; range = new NumberRange(envelope.getMinimum(zDimension), envelope.getMaximum(zDimension)); } return range; } /** * Returns the coverage envelope. The default implementation delegates to the * {@linkplain #getCoverage underlying coverage}. */ 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}. */ 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}. */ 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)}. */ 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 unknow. * 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 with {@link #zDimension} dimensions. will be created only * when first needed. */ private transient GeneralDirectPosition reducedPosition; /** * The dimension of the <var>z</var> ordinate (the last value in coordinate points). * This is always the {@linkplain #getCoordinateReferenceSystem() coordinate reference * system} dimension minus 1. * * @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 * thel again everytime an {@code evaluate(...)} method is invoked. */ private transient byte[] byteBuffer; /** * Sample integer values. Allocated when first needed, in order to avoid allocating * thel again everytime an {@code evaluate(...)} method is invoked. */ private transient int[] intBuffer; /** * Sample float values. Allocated when first needed, in order to avoid allocating * thel again everytime an {@code evaluate(...)} method is invoked. */ private transient float[] floatBuffer; /** * Sample double values. Allocated when first needed, in order to avoid allocating * thel again everytime 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 GeoTools 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: * * <blockquote><pre> * 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(DefaultGeographicCRS.WGS84_3D); * 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, ...); * } * </pre></blockquote> * * This convenience constructor wraps all coverage intos an {@link Adapter Adapter} object. * Users with a significant amount of data are encouraged to uses the constructor expecting * {@link Element Element} objects instead, in order to provides 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)); } /** * 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()])); } /** * 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. * @throws IOException if an I/O operation was required and failed. */ private CoverageStack(final CharSequence name, final CoordinateReferenceSystem crs, final Element[] elements) throws IOException { this(name, getEnvelope(crs, elements), elements); // '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) throws IOException { super(name, envelope.getCoordinateReferenceSystem(), null, null); assert XArray.isSorted(elements, COMPARATOR); this.elements = elements; this.envelope = envelope; zDimension = envelope.getDimension() - 1; boolean sampleDimensionMismatch = false; SampleDimension[] sampleDimensions = null; for (int j=0; j<elements.length; j++) { final Element element = elements[j]; /* * 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 = CRSUtilities.getSubCRS(crs, zDimension, zDimension+1); } /** * Constructs a new coverage using the same elements than the specified coverage stack. */ protected CoverageStack(final CharSequence name, final CoverageStack source) { super(name, 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 (UndeclaredThrowableException exception) { throw rethrow(exception); } /* * 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; int errorCode = ErrorKeys.ILLEGAL_COORDINATE_REFERENCE_SYSTEM; // In case of error if (crs == null) { errorCode = ErrorKeys.MISMATCHED_COORDINATE_REFERENCE_SYSTEM; 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<CoordinateReferenceSystem>(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, LoggingKeys. FOUND_MISMATCHED_CRS_$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(CoverageStack.class); 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(ErrorKeys.UNSUPPORTED_CRS_$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.setToNull(); /* * 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 = CRSUtilities.getSubCRS(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 = CRS.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.getMinimum(); maximum = range.getMaximum(); } 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; } /** * Rethrows the exception in {@link #COMPARATOR} as a {@link RuntimeException}. * It gives an opportunity for implementations of {@link Element} to uses some * checked exception like {@link IOException}. */ private static IOException rethrow(final UndeclaredThrowableException exception) { final Throwable cause = exception.getCause(); if (cause instanceof IOException) { return (IOException) cause; } if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } throw exception; } /** * 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>() { public int compare(final Object entry1, final Object entry2) { try { return Double.compare(zFromObject(entry1), zFromObject(entry2)); } catch (IOException exception) { throw new UndeclaredThrowableException(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 getZ(entry.getZRange()); } /** * Returns the <var>z</var> value in the middle of the specified range. * If the range is null, then this method returns {@link Double#NaN}. */ private static double getZ(final NumberRange range) { if (range != null) { final Number lower = (Number) range.getMinValue(); final Number upper = (Number) 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(); } } return NaN; } /** * 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.getMinimum() && z<=range.getMaximum(); } /** * 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. */ 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. */ 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. First, this method locate 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. Next, this method locate * the pixel under the {@code point} coordinate in the coverage element. The {@code point} * is then set to the pixel center coordinate and to the <var>z</var> value of the selected * coverage element. Consequently, calling any {@code evaluate(...)} method with snapped * coordinates will returns non-interpolated values. * * @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 (UndeclaredThrowableException exception) { throw rethrow(exception); } 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.getGridRange(); 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(ErrorKeys.CANT_EVALUATE_$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 occured while loading image. */ private Coverage load(final Element element) throws IOException { 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 equalsIgnoreMetadata((sourceCRS = coverage.getCoordinateReferenceSystem()), CRSUtilities.getSubCRS(crs, 0, sourceCRS.getCoordinateSystem().getDimension())) : sourceCRS + "\n\n" + crs; 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 occured while loading image. */ private void load(final int index) throws IOException { final Element element = elements[index]; final NumberRange zRange = element.getZRange(); logLoading(VocabularyKeys.LOADING_IMAGE_$1, new String[] {element.getName()}); lower = upper = load(element); lowerZ = upperZ = getZ(zRange); lowerRange = upperRange = zRange; } /** * Loads images for the given elements. * * @throws IOException if an error occured while loading images. */ private void load(final Element lowerElement, final Element upperElement) throws IOException { logLoading(VocabularyKeys.LOADING_IMAGES_$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(lowerRange); this.upperZ = getZ(upperRange); 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 (UndeclaredThrowableException exception) { // TODO: localize throw new CannotEvaluateException("Can't fetch coverage properties.", rethrow(exception)); } 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().contains(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().contains(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.getMaximum(); final double upperStart = upperRange.getMinimum(); if (lowerEnd+lagTolerance >= upperStart) { if (interpolationEnabled) { load(lowerElement, upperElement); } else { if (Math.abs(getZ(upperRange)-z) > Math.abs(z-getZ(lowerRange))) { index--; } load(index); } return true; } if (lowerRange.contains(Z)) { load(index-1); return true; } if (upperRange.contains(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.wrap((TemporalCRS) zCRS).toDate(z); } else { Zp = Z; } throw new OrdinateOutsideCoverageException(Errors.format( ErrorKeys.ZVALUE_OUTSIDE_COVERAGE_$2, getName(), Zp), zDimension, getEnvelope()); } /** * Returns a point with the same number of dimensions than the specified coverage. * The number of dimensions must be {@link #zDimensions} or {@code zDimensions+1}. */ private final DirectPosition reduce(DirectPosition coord, final Coverage coverage) { 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 equalsIgnoreMetadata(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. */ public Object evaluate(final DirectPosition coord) throws 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 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 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 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 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 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 {@code true} if interpolation are enabled in the <var>z</var> value dimension. * Interpolations are enabled by default. */ public boolean isInterpolationEnabled() { return interpolationEnabled; } /** * Enable or disable interpolations in the <var>z</var> value dimension. */ public synchronized void setInterpolationEnabled(final boolean flag) { lower = null; upper = null; lowerZ = POSITIVE_INFINITY; upperZ = NEGATIVE_INFINITY; interpolationEnabled = flag; } /** * Adds an {@link IIOReadWarningListener} to the list of registered warning listeners. */ public void addIIOReadWarningListener(final IIOReadWarningListener listener) { listeners.addIIOReadWarningListener(listener); } /** * Removes an {@link IIOReadWarningListener} from the list of registered warning listeners. */ public void removeIIOReadWarningListener(final IIOReadWarningListener listener) { listeners.removeIIOReadWarningListener(listener); } /** * Adds an {@link IIOReadProgressListener} to the list of registered progress listeners. */ public void addIIOReadProgressListener(final IIOReadProgressListener listener) { listeners.addIIOReadProgressListener(listener); } /** * Removes an {@link IIOReadProgressListener} from the list of registered progress listeners. */ 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.geotools.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(CoverageStack.class); 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 int 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. * * @version $Id$ * @author Martin Desruisseaux */ 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; } } } }