/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-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.io; import java.util.Arrays; import java.io.Serializable; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import org.opengis.geometry.Envelope; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.geometry.MismatchedReferenceSystemException; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.Envelope2D; import org.geotoolkit.resources.Errors; import org.apache.sis.referencing.cs.AxesConvention; import org.geotoolkit.util.Cloneable; import org.apache.sis.util.Classes; import org.geotoolkit.internal.referencing.CRSUtilities; import org.apache.sis.util.Utilities; import static org.apache.sis.util.ArgumentChecks.ensurePositive; /** * Base class for {@link GridCoverageReadParam} and {@link GridCoverageWriteParam}. This class * defines which part of the source (a stream when reading, or a coverage when writing) shall * be transfered to the destination (a coverage when reading, or a stream when writing). * * {@note This class is conceptually equivalent to the <code>IIOParam</code> class provided * in the standard Java library. The main difference is that <code>GridCoverageStoreParam</code> * works with geodetic coordinates while <code>IIOParam</code> works with pixel coordinates.} * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.15 * * @see javax.imageio.IIOParam * * @since 3.14 (derived from 3.09) * @module */ public abstract class GridCoverageStoreParam implements Serializable { /** * For cross-version compatibility. */ private static final long serialVersionUID = 5654080292972651645L; /** * The coordinate reference system of the envelope and resolution specified in this object, * or {@code null} if unspecified. */ private CoordinateReferenceSystem crs; /** * The region to read from the source, or {@code null} if unspecified. */ private Envelope envelope; /** * The resolution, or {@code null} if unspecified. */ private double[] resolution; /** * The set of source bands to read, or {@code null} for all of them. */ private int[] sourceBands; /** * Creates a new {@code GridCoverageStoreParam} instance. All properties are * initialized to {@code null}. Callers must invoke setter methods in order * to provide information about the way to decode or encode the stream. */ protected GridCoverageStoreParam() { } /** * Creates a new {@code GridCoverageStoreParam} instance initialized to the same * values than the given parameters. * * @param param The parameters to copy, or {@code null} if none. * * @since 3.15 */ protected GridCoverageStoreParam(final GridCoverageStoreParam param) { if (param != null) { crs = param.crs; envelope = param.envelope; resolution = param.resolution; sourceBands = param.sourceBands; } } /** * Resets all parameters to their {@code null} value. */ public void clear() { crs = null; envelope = null; resolution = null; sourceBands = null; } /** * Ensures that the CRS of the given envelope (if non-null) is equals, ignoring metadata, * to the given CRS. */ private static void ensureCompatibleCRS(final Envelope envelope, final CoordinateReferenceSystem crs) throws MismatchedReferenceSystemException { if (crs != null && envelope != null) { final CoordinateReferenceSystem envelopeCRS = envelope.getCoordinateReferenceSystem(); if (envelopeCRS != null && !Utilities.equalsIgnoreMetadata(crs, envelopeCRS)) { throw new MismatchedReferenceSystemException(Errors.format( Errors.Keys.MismatchedCoordinateReferenceSystem)); } } } /** * Ensures that the dimension of the given resolution array (if non-null) * is compatible with the CRS dimension, if any. */ private static void ensureCompatibleDimension(final double[] resolution, final CoordinateReferenceSystem crs) throws MismatchedDimensionException { if (crs != null && resolution != null) { final int dimension = resolution.length; final int expected = crs.getCoordinateSystem().getDimension(); if (dimension != expected) { throw new MismatchedDimensionException(Errors.format( Errors.Keys.MismatchedDimension_2, dimension, expected)); } } } /** * Ensures that the dimension of the given resolution array (if non-null) * is compatible with the dimension of the given envelope. */ private static void ensureCompatibleDimension(final double[] resolution, final Envelope envelope) throws MismatchedDimensionException { if (resolution != null && envelope != null) { final int dimension = resolution.length; final int expected = envelope.getDimension(); if (dimension != expected) { throw new MismatchedDimensionException(Errors.format( Errors.Keys.MismatchedDimension_2, dimension, expected)); } } } /** * Returns the CRS of the {@linkplain #getEnvelope() envelope} and {@link #getResolution() * resolution} parameters, or {@code null} if unspecified. * * @return The CRS of the envelope and resolution parameters, or {@code null}. */ public CoordinateReferenceSystem getCoordinateReferenceSystem() { if (crs == null && envelope != null) { return envelope.getCoordinateReferenceSystem(); } return crs; } /** * Sets the CRS of the {@linkplain #getEnvelope() envelope} and {@linkplain #getResolution() * resolution} parameters. If the envelope parameter is already defined with a different * CRS, then this method throws a {@link MismatchedReferenceSystemException}. * * @param crs The new CRS for the envelope and resolution parameters. * @throws MismatchedReferenceSystemException If the {@linkplain #getEnvelope() envelope} * parameter is already defined with a different CRS. * @throws MismatchedDimensionException If the {@linkplain #getResolution() resolution} * parameter is already defined with a different dimension. */ public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) throws MismatchedReferenceSystemException, MismatchedDimensionException { ensureCompatibleCRS(envelope, crs); ensureCompatibleDimension(resolution, crs); this.crs = crs; } /** * Returns the maximal extent of the region to read from the source, or {@code null} if * unspecified. If the {@linkplain Envelope#getCoordinateReferenceSystem() envelope CRS} * is not equals to the native CRS of the grid coverage to be read or written, then the * envelope will be transformed to the later CRS at reading or writing time. * * @return The region to read from the stream, or {@code null} if unspecified. * * @see javax.imageio.IIOParam#getSourceRegion() */ public Envelope getEnvelope() { Envelope env = envelope; if (env != null) { if (crs != null && env.getCoordinateReferenceSystem() == null) { final GeneralEnvelope ge = new GeneralEnvelope(env); ge.setCoordinateReferenceSystem(crs); env = ge; } else if (env instanceof Cloneable) { env = (Envelope) ((Cloneable) env).clone(); } } return env; } /** * Returns the {@linkplain #getEnvelope() envelope}, ensuring that it is contained * inside the coordinate system domain of validity. This method also ensures that * the returned envelope is not a direct reference to the {@link #envelope} field, * so it is safe for modification. * * @param needsLongitudeShift {@code true} if the grid geometry needs longitude * values in the [0…360]° range instead than the default [-180 … +180]°. */ final GeneralEnvelope getValidEnvelope(final boolean needsLongitudeShift) { final Envelope env = getEnvelope(); if (env == null) { return null; } final GeneralEnvelope ge; if (env instanceof GeneralEnvelope && env != envelope) { ge = (GeneralEnvelope) env; } else { ge = new GeneralEnvelope(env); } if (needsLongitudeShift) { ge.setCoordinateReferenceSystem(CRSUtilities.shiftAxisRange( ge.getCoordinateReferenceSystem(), AxesConvention.POSITIVE_RANGE)); } if (ge.normalize()) { ge.simplify(); } return ge; } /** * Sets the maximal extent of the region to read from the source. The actual envelope of * the destination (the coverage returned by {@link GridCoverageReader}, or the coverage * in the file written by {@link GridCoverageWriter}) may be smaller if the coverage * available in the source does not fill completely the given envelope. * <p> * The envelope can be specified in any {@linkplain Envelope#getCoordinateReferenceSystem() * Coordinate Reference System}, unless the CRS has been restricted by a call to the * {@link #setCoordinateReferenceSystem setCoordinateReferenceSystem} method. * The envelope CRS is honored as below: * * <ul> * <li><p><b>At reading time</b>: {@link GridCoverageReader} may return a coverage in that CRS * if it can do that cheaply (for example if the backing store already contains the same * coverage in different CRS), or return the coverage in its native CRS otherwise, at * implementation choice. Callers should check the CRS of the returned coverage.</p></li> * * <li><p><b>At writing time</b>: {@link GridCoverageWriter} will reproject the coverage * to that CRS, if needed. If the file format does not support that CRS, then an * exception will be thrown.</p></li> * </ul> * * If the envelope is set to {@code null}, then {@code GridCoverageStore} will read * the full coverage extent in its native CRS. * * @param envelope The region to read from the source, or {@code null}. * @throws MismatchedReferenceSystemException If the given CRS is not equal * (ignoring metadata) to the CRS defined by the last call to * {@link #setCoordinateReferenceSystem setCoordinateReferenceSystem}. * @throws MismatchedDimensionException If the dimension of the given envelope is not * equal to the {@linkplain #getResolution() resolution} dimension. * @throws IllegalArgumentException If the given envelope is illegal for an other reason. * * @see javax.imageio.IIOParam#setSourceRegion(Rectangle) */ public void setEnvelope(Envelope envelope) throws IllegalArgumentException { if (envelope != null) { ensureCompatibleCRS(envelope, crs); ensureCompatibleDimension(resolution, envelope); /* * Ensures that the envelope is non-empty. The two dimensions to be read should * have a span greater than zero, while the extra dimension may have a span of 0. * We can not determine easily which dimensions will map to the rows and columns * (we would need to transform the envelope for that purpose, but we don't want * do do that here). As a compromise, we will just check that at least two * dimensions have a non-null span. */ int dimension = 0; for (int i=envelope.getDimension(); --i>=0;) { if (envelope.getSpan(i) > 0) { dimension++; } } if (dimension < 2) { throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyEnvelope2d)); } if (envelope instanceof Cloneable) { envelope = (Envelope) ((Cloneable) envelope).clone(); } } this.envelope = envelope; } /** * Sets the maximal extent of the region to read from the source. This convenience method * performs the same work than {@link #setEnvelope(Envelope)}, except that the envelope is * created from the given {@linkplain Rectangle rectangle} and two-dimensional coordinate * reference system. * * @param bounds The region to read from the source, or {@code null}. * @param crs The two-dimensional coordinate reference system of the region. * @throws MismatchedReferenceSystemException If the given CRS is not equal * (ignoring metadata) to the CRS defined by the last call to * {@link #setCoordinateReferenceSystem setCoordinateReferenceSystem}. * @throws MismatchedDimensionException If dimension of the current * {@linkplain #getResolution() resolution} is different than 2. * * @see javax.imageio.IIOParam#setSourceRegion(Rectangle) * * @since 3.10 */ public void setEnvelope(final Rectangle2D bounds, final CoordinateReferenceSystem crs) throws MismatchedReferenceSystemException, MismatchedDimensionException { setEnvelope(bounds != null ? new Envelope2D(crs, bounds) : null); } /** * Returns the resolution to read from the source, or {@code null} if unspecified. * The resolution shall be specified in the same {@linkplain CoordinateReferenceSystem * Coordinate Reference System} than the {@linkplain #getEnvelope() envelope} CRS. * This implies that the length of the returned array must match the * {@linkplain Envelope#getDimension() envelope dimension}. * * @return The resolution to read from the stream, or {@code null} if unspecified. * * @see javax.imageio.IIOParam#getSourceXSubsampling() * @see javax.imageio.IIOParam#getSourceYSubsampling() */ public double[] getResolution() { return (resolution != null) ? resolution.clone() : null; } /** * Sets the resolution to read from the source. The resolution shall be specified in the * same {@linkplain CoordinateReferenceSystem Coordinate Reference System} than the * {@linkplain #getEnvelope() envelope} CRS. * <p> * The resolution is honored as below: * * <ul> * <li><p><b>At reading time:</b> If the given resolution does not match a resolution that * {@link GridCoverageReader} can read, then {@code GridCoverageReader} while use the * largest {@code resolution} values which are equal or smaller (finer) than the given * arguments. If no available resolutions are equal or finer than the given ones, then * {@code GridCoverageReader} will use the finest resolution available.</p></li> * * <li><p><b>At writing time:</b> {@link GridCoverageWriter} will resample the coverage to * that resolution, if needed. If the file format does not support that resolution, * then an exception will be thrown.</p></li> * </ul> * * If the dimension is set to {@code null}, then {@link GridCoverageStore} will read * the coverage with the best resolution available. * * @param resolution The new resolution to read from the source, or {@code null}. * @throws MismatchedDimensionException If the dimension of the given resolution is not * equal to the {@linkplain #getEnvelope() envelope} dimension. * * @see javax.imageio.IIOParam#setSourceSubsampling(int, int, int, int) */ public void setResolution(double... resolution) throws MismatchedDimensionException { ensureCompatibleDimension(resolution, crs); ensureCompatibleDimension(resolution, envelope); if (resolution != null) { resolution = resolution.clone(); for (final double r : resolution) { // Accept 0 as well, meaning "best resolution available". ensurePositive("resolution", r); } } this.resolution = resolution; } /** * Returns the set of source bands to read, or {@code null} for all of them. * * @return The set of source bands to read, or {@code null} for all of them. * * @see javax.imageio.IIOParam#getSourceBands() * * @since 3.10 */ public int[] getSourceBands() { final int[] bands = sourceBands; return (bands != null) ? bands.clone() : null; } /** * Sets the indices of the source bands to read. A {@code null} value indicates * that all source bands will be read. * <p> * At the time of reading or writing, an {@link IllegalArgumentException} will be thrown by * the reader or writer if a value larger than the largest available source band index has * been specified or if the number of source bands and destination bands to be used differ. * * @param bands The source bands to read, or {@code null}. * @throws IllegalArgumentException If the given array is empty, * or if it contains duplicated or negative values. * * @see javax.imageio.IIOParam#setSourceBands(int[]) * * @since 3.10 */ public void setSourceBands(final int... bands) throws IllegalArgumentException { sourceBands = checkAndClone(bands); } /** * Clones the given array and ensures that all numbers are positive and non-duplicated. * * @param bands The array to check and clone, or {@code null}. * @return A clone of the given array, or {@code null}. * @throws IllegalArgumentException If the given array is empty, * or if it contains duplicated or negative values. */ static int[] checkAndClone(int[] bands) throws IllegalArgumentException { if (bands != null) { if (bands.length == 0) { throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArray)); } bands = bands.clone(); for (int i=0; i<bands.length; i++) { final int band = bands[i]; if (band < 0) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalBandNumber_1, band)); } for (int j=i; --j>=0;) { if (band == bands[j]) { throw new IllegalArgumentException(Errors.format( Errors.Keys.DuplicatedValue_1, band)); } } } } return bands; } /** * Returns a string representation of this object for debugging purpose. * The content of the returned string may change in any future version. */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this)).append('['); String separator = ""; if (envelope != null) { buffer.append("envelope=").append(envelope); separator = ", "; } if (resolution != null) { buffer.append(separator).append("resolution=").append(Arrays.toString(resolution)); separator = ", "; } final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(); if (crs != null) { buffer.append(separator).append("crs=\"").append(crs.getName().getCode()).append('"'); separator = ", "; } if (sourceBands != null) { buffer.append(separator).append("sourceBands=").append(Arrays.toString(sourceBands)); separator = ", "; } toString(buffer, separator); return buffer.append(']').toString(); } /** * Overloaded by the subclasses in order to complete the string built by {@link #toString()}. * * @param buffer The buffer where to write the string. * @param separator The separator to put before to write anything in the buffer, * or {@code null} if none. */ void toString(final StringBuilder buffer, String separator) { } }