/* * 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.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CancellationException; import java.io.IOException; import javax.imageio.ImageReader; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.measure.Unit; import javax.measure.IncommensurableException; import org.w3c.dom.Node; import org.opengis.metadata.Metadata; import org.opengis.metadata.acquisition.AcquisitionInformation; import org.opengis.metadata.content.ImageDescription; import org.opengis.metadata.content.ContentInformation; import org.opengis.metadata.identification.DataIdentification; import org.opengis.metadata.identification.Identification; import org.opengis.metadata.identification.Resolution; import org.opengis.metadata.quality.DataQuality; import org.opengis.metadata.spatial.Georectified; import org.opengis.metadata.spatial.SpatialRepresentation; import org.opengis.metadata.extent.Extent; import org.opengis.referencing.operation.TransformException; import org.opengis.coverage.grid.GridCoverage; import org.opengis.util.GenericName; import org.apache.sis.util.ArraysExt; import org.apache.sis.measure.Units; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.logging.Logging; import org.geotoolkit.measure.Measure; import org.apache.sis.metadata.iso.DefaultMetadata; import org.apache.sis.metadata.iso.extent.DefaultExtent; import org.apache.sis.metadata.iso.identification.DefaultDataIdentification; import org.apache.sis.metadata.iso.identification.DefaultResolution; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.coverage.grid.GeneralGridGeometry; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.internal.referencing.CRSUtilities; import org.geotoolkit.resources.Vocabulary; import static org.geotoolkit.util.collection.XCollections.addIfNonNull; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.ISO_FORMAT_NAME; /** * Base class of {@link GridCoverage} readers. Reading is a two steps process: * <p> * <ul> * <li>The input must be set first using the {@link #setInput(Object)} method.</li> * <li>The actual reading is performed by a call to the * {@link #read(int, GridCoverageReadParam)} method.</li> * </ul> * <p> * Example: * * {@preformat java * GridCoverageReader reader = ... * reader.setInput(new File("MyCoverage.asc")); * GridCoverage coverage = reader.read(0, null); * } * * {@note This class is conceptually equivalent to the <code>ImageReader</code> class provided in * the standard Java library. Implementations of this class are often wrappers around a Java * <code>ImageReader</code>, converting geodetic coordinates to pixel coordinates before to * delegate the reading of pixel values.} * * @author Martin Desruisseaux (IRD, Geomatys) * @author Johann Sorel (Geomatys) * @version 3.20 * * @see ImageReader * * @since 3.09 (derived from 2.4) * @module */ public abstract class GridCoverageReader extends GridCoverageStore implements CoverageReader { /** * The input (typically a {@link java.io.File}, {@link java.net.URL} or {@link String}), * or {@code null} if input is not set. */ Object input; /** * Creates a new instance. */ protected GridCoverageReader() { ignoreGridTransforms = true; } /** * Sets the input source to the given object. The input is typically a * {@link java.io.File} or a {@link String} object. But some other types * (e.g. {@link javax.imageio.stream.ImageInputStream}) may be accepted * as well depending on the implementation. * * {@section How streams are closed} * <ul> * <li>If the given input is an {@linkplain java.io.InputStream input stream}, * {@linkplain javax.imageio.stream.ImageInputStream image input stream} or * a {@linkplain java.io.Reader reader}, then it is caller responsibility to * close the given stream after usage.</li> * <li>If an input stream has been generated automatically by this {@code GridCoverageReader} * from the given input object, then this coverage reader will close the stream when the * {@link #reset()} or {@link #dispose()} method is invoked, or when a new input is set.</li> * </ul> * * @param input The input (typically {@link java.io.File} or {@link String}) to be read. * @throws IllegalArgumentException If the input is not a valid instance for this reader. * @throws CoverageStoreException If the operation failed. * * @see ImageReader#setInput(Object) */ public void setInput(final Object input) throws CoverageStoreException { this.input = input; abortRequested = false; } /** * Returns the input which was set by the last call to {@link #setInput(Object)}, * or {@code null} if none. * * @return The current input, or {@code null} if none. * @throws CoverageStoreException If the operation failed. * * @see ImageReader#getInput() */ public Object getInput() throws CoverageStoreException { return input; } /** * Returns the name of the {@linkplain #input}, or "<cite>Untitled</cite>" if * the input is not a recognized type. This is used for formatting messages only. */ final String getInputName() { final Object input = this.input; if (IOUtilities.canProcessAsPath(input)) { return IOUtilities.filename(input); } else { return Vocabulary.getResources(locale).getString(Vocabulary.Keys.Untitled); } } /** * Returns the list of coverage names available from the current input source. The length * of the returned list is the number of coverages found in the current input source. The * elements in the returned list are the names of each coverage. * <p> * The returned list may be backed by this {@code GridCoverageReader}: it should be used * only as long as this reader and its input source are valid. Iterating over the list * may be costly and the operation performed on the list may throw a * {@link BackingStoreException}. * * @return The names of the coverages. * @throws IllegalStateException If the input source has not been set. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. * * @see ImageReader#getNumImages(boolean) */ public abstract List<? extends GenericName> getCoverageNames() throws CoverageStoreException, CancellationException; /** * Returns the grid geometry for the {@link GridCoverage} to be read at the given index. * * @param index The index of the coverage to be queried. * @return The grid geometry for the {@link GridCoverage} at the specified index. * @throws IllegalStateException If the input source has not been set. * @throws IndexOutOfBoundsException If the supplied index is out of bounds. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. * * @see ImageReader#getWidth(int) * @see ImageReader#getHeight(int) */ public abstract GeneralGridGeometry getGridGeometry(int index) throws CoverageStoreException, CancellationException; /** * Returns the sample dimensions for each band of the {@link GridCoverage} to be read. * If sample dimensions are not known, then this method returns {@code null}. * * @param index The index of the coverage to be queried. * @return The list of sample dimensions for the {@link GridCoverage} at the specified index, * or {@code null} if none. This list length is equals to the number of bands in the * {@link GridCoverage}. * @throws IllegalStateException If the input source has not been set. * @throws IndexOutOfBoundsException If the supplied index is out of bounds. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. */ public abstract List<GridSampleDimension> getSampleDimensions(int index) throws CoverageStoreException, CancellationException; /** * Returns the ranges of valid sample values for each band in this format. * The ranges are always expressed in <cite>geophysics</cite> units. * <p> * The default implementation computes the ranges from the information returned * by {@link #getSampleDimensions(int)}, if any. * * @param index The index of the coverage to be queried. * @return The ranges of values for each band, or {@code null} if none. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. * * @since 3.10 * * @deprecated Not used in practice. Could be a convenience static method working on any * sample dimensions. */ @Deprecated public List<MeasurementRange<?>> getSampleValueRanges(final int index) throws CoverageStoreException, CancellationException { final List<GridSampleDimension> sampleDimensions = getSampleDimensions(index); if (sampleDimensions == null) { return null; } @SuppressWarnings({"unchecked","rawtypes"}) // Generic array creation. final MeasurementRange<?>[] ranges = new MeasurementRange[sampleDimensions.size()]; for (int i=0; i<ranges.length; i++) { GridSampleDimension sd = sampleDimensions.get(i); if (sd != null) { sd = sd.geophysics(true); ranges[i] = MeasurementRange.createBestFit( sd.getMinimumValue(), true, sd.getMaximumValue(), true, sd.getUnits()); } } return Arrays.asList(ranges); } /** * If the given metadata is non-null, supports the ISO-19115 format and contains a * {@link Metadata} user object in the root node, returns that object. Otherwise * creates a new, initially empty, metadata object. */ private static DefaultMetadata createMetadata(final IIOMetadata streamMetadata) throws CoverageStoreException { if (streamMetadata != null) try { if (ArraysExt.contains(streamMetadata.getExtraMetadataFormatNames(), ISO_FORMAT_NAME)) { final Node root = streamMetadata.getAsTree(ISO_FORMAT_NAME); if (root instanceof IIOMetadataNode) { final Object userObject = ((IIOMetadataNode) root).getUserObject(); if (userObject instanceof Metadata) { // Unconditionally copy the metadata, even if the original object was // already an instance of DefaultMetadata, because the original object // may be cached in the ImageReader - so we don't want to modify it. return new DefaultMetadata((Metadata) userObject); } } } } catch (BackingStoreException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { throw new CoverageStoreException(cause); } throw e.unwrapOrRethrow(CoverageStoreException.class); } return new DefaultMetadata(); } /** * Returns the ISO 19115 metadata object associated with the input source as a whole * and each coverages. The default implementation constructs the metadata from the * {@linkplain #getStreamMetadata() stream metadata} and the * {@linkplain #getCoverageMetadata(int) coverage metadata}, * eventually completed by the {@link #getGridGeometry(int)}. * <p> * Since the relationship between Image I/O metadata and ISO 19115 is not always a * "<cite>one-to-one</cite>" relationship, this method works on a best effort basis. * * @return The ISO 19115 metadata (never {@code null}). * @throws CoverageStoreException If an error occurs while reading the information from the input source. * * @see <a href="../../image/io/metadata/SpatialMetadataFormat.html#default-formats">Metadata formats</a> * * @since 3.18 */ public Metadata getMetadata() throws CoverageStoreException { final SpatialMetadata streamMetadata = getStreamMetadata(); final DefaultMetadata metadata = createMetadata(streamMetadata); /* * Extract all information available from the stream metadata, provided that metadata * elements were not already provided by the above call to createMetadata(...). Since * createMetadata(...) typically get its information from the stream metadata as well, * we assume that creating here new objects from stream metadata would be redundant. */ DataIdentification identification = null; if (streamMetadata != null) { final Collection<DataQuality> quality = metadata.getDataQualityInfo(); if (quality.isEmpty()) { addIfNonNull(quality, streamMetadata.getInstanceForType(DataQuality.class)); } final Collection<AcquisitionInformation> acquisition = metadata.getAcquisitionInformation(); if (acquisition.isEmpty()) { addIfNonNull(acquisition, streamMetadata.getInstanceForType(AcquisitionInformation.class)); } /* * Get the existing identification info if any, or create a new one otherwise. * If an identification info is found, remove it from the metadata (it will be * added back at the end of this method, or a copy of it will be added). */ final Iterator<Identification> it = metadata.getIdentificationInfo().iterator(); while (it.hasNext()) { final Identification candidate = it.next(); if (candidate instanceof DataIdentification) { identification = (DataIdentification) candidate; it.remove(); break; } } if (identification == null) { identification = streamMetadata.getInstanceForType(DataIdentification.class); } } /* * Check if we should complete the extents and resolutions. We will do so only * if the vertical/temporal extent, geographic bounding box and resolution are * not already provided in the metadata. If the geographic extent is declared * by an other kind of object than GeographicBoundingBox, we will still add the * bounding box because the existing extent could be only a textual description. */ boolean failed = false; // For logging warning only once. boolean computeExtents = true; // 'false' if extents are already present. boolean computeResolutions = true; // 'false' is resolutions are already present. DefaultExtent extent = null; // The extent to compute, if needed. List<Extent> extents = null; // The extents already provided in the metadata. Set<Resolution> resolutions = null; // The resolutions to compute, if needed. if (identification != null) { computeResolutions = isNullOrEmpty(identification.getSpatialResolutions()); final Collection<? extends Extent> existings = identification.getExtents(); if (!isNullOrEmpty(existings)) { extents = new ArrayList<>(existings); extent = UniqueExtents.getIncomplete(extents); if (extent == null) { // The plugin-provided Metadata instance seems to contain Extents // that are complete enough, so we will not try to complete them. computeExtents = false; extents = null; } } } /* * Check if we should complete the content info and the spatial representation info. * If the plugin-provided metadata declare explicitly such information, we will not * compute them in this method (the plugin information will have precedence). */ final Collection<ContentInformation> contentInfo = metadata.getContentInfo(); final Collection<SpatialRepresentation> spatialInfo = metadata.getSpatialRepresentationInfo(); final boolean computeContent = (contentInfo != null) && contentInfo.isEmpty(); final boolean computeSpatial = (spatialInfo != null) && spatialInfo.isEmpty(); if (computeContent || computeSpatial || computeResolutions || computeExtents) { final List<? extends GenericName> coverageNames = getCoverageNames(); final int numCoverages = coverageNames.size(); for (int i=0; i<numCoverages; i++) { if (computeContent || computeSpatial) { final SpatialMetadata coverageMetadata = getCoverageMetadata(i); if (coverageMetadata != null) { if (computeContent) { final ImageDescription description = coverageMetadata.getInstanceForType(ImageDescription.class); if (description != null) { contentInfo.add(description); } } if (computeSpatial) { final Georectified rectified = coverageMetadata.getInstanceForType(Georectified.class); if (rectified != null) { metadata.getSpatialRepresentationInfo().add(rectified); } } } } if (computeResolutions || computeExtents) { /* * Resolution along the horizontal axes only, ignoring all other axes. For linear units (feet, * kilometres, etc.), we convert the units to metres for compliance with a current limitation * of Apache SIS, which can handle only metres. For angular resolution (typically in degrees), * we perform an APPROXIMATIVE conversion to metres using the nautical mile definition. This * conversion is only valid along the latitudes axis (the number is wrong along the longitude * axis), and more accurate for mid-latitude (the numbers are differents close to equator or * to the poles). */ final GeneralGridGeometry gg = getGridGeometry(i); if (computeResolutions) { final Measure m = CRSUtilities.getHorizontalResolution( gg.getCoordinateReferenceSystem(), gg.getResolution()); if (m != null) { double measureValue = m.doubleValue(); final Unit<?> unit = m.getUnit(); Unit<?> standardUnit = null; double scaleFactor = 1; if (Units.isAngular(unit)) { standardUnit = Units.DEGREE; scaleFactor = (1852*60); // From definition of nautical miles. } else if (Units.isLinear(unit)) { standardUnit = Units.METRE; } if (standardUnit != null) try { measureValue = unit.getConverterToAny(standardUnit).convert(measureValue) * scaleFactor; final DefaultResolution resolution = new DefaultResolution(); resolution.setDistance(measureValue); if (resolutions == null) { resolutions = new LinkedHashSet<>(); } resolutions.add(resolution); } catch (IncommensurableException e) { // In case of failure, do not create a Resolution object. Logging.recoverableException(LOGGER, GridCoverageReader.class, "getMetadata", e); } } } /* * Horizontal, vertical and temporal extents. The horizontal extents is * represented as a geographic bounding box, which may require a reprojection. */ if (computeExtents && gg.isDefined(GeneralGridGeometry.ENVELOPE)) { if (extent == null) { extent = new UniqueExtents(); } try { extent.addElements(gg.getEnvelope()); } catch (TransformException e) { // Not a big deal if we fail. We will just let the identification section unchanged. if (!failed) { failed = true; // Log only once. Logging.recoverableException(LOGGER, GridCoverageReader.class, "getMetadata", e); } } } } } } /* * At this point, we have computed extents and resolutions from every images * in the stream. Now store the result. Note that we unconditionally create * a copy of the identification info, even if the original object was already * an instance of DefaultDataIdentification, because the original object may * be cached in the ImageReader. */ if (extent != null || resolutions != null) { final DefaultDataIdentification copy = new DefaultDataIdentification(identification); if (extent != null) { if (extents != null) { copy.setExtents(extents); } else { copy.getExtents().add(extent); } } if (resolutions != null) { copy.setSpatialResolutions(resolutions); } identification = copy; } if (identification != null) { metadata.getIdentificationInfo().add(identification); } return metadata; } /** * Returns the metadata associated with the input source as a whole, or {@code null} if none. * The default implementation returns {@code null} in every cases. * * @return The metadata associated with the input source as a whole, or {@code null}. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * * @see ImageReader#getStreamMetadata() * * @since 3.14 */ public SpatialMetadata getStreamMetadata() throws CoverageStoreException { return null; } /** * Returns the metadata associated with the given coverage, or {@code null} if none. * The default implementation returns {@code null} in every cases. * * @param index The index of the coverage to be queried. * @return The metadata associated with the given coverage, or {@code null}. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * * @see ImageReader#getImageMetadata(int) * * @since 3.14 */ public SpatialMetadata getCoverageMetadata(final int index) throws CoverageStoreException { return null; } /** * Returns an optional map of properties associated with the coverage at the given index, or * {@code null} if none. The properties are implementation-specific; they are available to * subclasses for any use. The {@code GridCoverageReader} class will simply gives those * properties to the {@link javax.media.jai.PropertySource} object to be created by the * {@link #read read} method, without any processing. * <p> * The default implementation returns {@code null} in every cases. * * @param index The index of the coverage to be queried. * @return The properties, or {@code null} if none. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. */ public Map<?,?> getProperties(int index) throws CoverageStoreException, CancellationException { return null; } /** * Reads the grid coverage. * * @param index The index of the coverage to be queried. * @param param Optional parameters used to control the reading process, or {@code null}. * @return The {@link GridCoverage} at the specified index. * @throws IllegalStateException if the input source has not been set. * @throws IndexOutOfBoundsException if the supplied index is out of bounds. * @throws CoverageStoreException If an error occurs while reading the information from the input source. * @throws CancellationException If {@link #abort()} has been invoked in an other thread during * the execution of this method. * * @see ImageReader#read(int) */ public abstract GridCoverage read(int index, GridCoverageReadParam param) throws CoverageStoreException, CancellationException; /** * Restores the {@code GridCoverageReader} to its initial state. * * @throws CoverageStoreException If an error occurs while restoring to the initial state. * * @see ImageReader#reset() */ @Override public void reset() throws CoverageStoreException { input = null; super.reset(); } /** * Allows any resources held by this reader to be released. The result of calling * any other method subsequent to a call to this method is undefined. * * @throws CoverageStoreException If an error occurs while disposing resources. * * @see ImageReader#dispose() */ @Override public void dispose() throws CoverageStoreException { input = null; super.dispose(); } }