/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2006-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.image.io.plugin; import java.nio.file.NoSuchFileException; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import java.util.EnumSet; import java.util.HashMap; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.Locale; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.net.URL; import java.net.URI; import java.io.File; import java.io.IOException; import java.io.FileNotFoundException; import java.awt.geom.AffineTransform; import java.net.MalformedURLException; import java.net.URISyntaxException; import javax.imageio.IIOException; import javax.imageio.ImageReader; import javax.imageio.ImageReadParam; import ucar.ma2.Array; import ucar.ma2.Range; import ucar.ma2.IndexIterator; import ucar.ma2.InvalidRangeException; import ucar.nc2.dataset.CoordinateAxis; import ucar.nc2.dataset.CoordinateSystem; import ucar.nc2.dataset.CoordSysBuilder; import ucar.nc2.dataset.CoordSysBuilderIF; import ucar.nc2.dataset.NetcdfDataset; import ucar.nc2.dataset.VariableDS; import ucar.nc2.dataset.Enhancements; import ucar.nc2.ncml.Aggregation; import ucar.nc2.util.CancelTask; import ucar.nc2.Dimension; import ucar.nc2.Variable; import ucar.nc2.VariableIF; import ucar.nc2.NetcdfFile; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.geotoolkit.image.io.Protocol; import org.geotoolkit.image.io.DimensionSlice; import org.geotoolkit.image.io.FileImageReader; import org.geotoolkit.image.io.DimensionIdentification; import org.geotoolkit.image.io.IllegalImageDimensionException; import org.geotoolkit.image.io.MultidimensionalImageStore; import org.geotoolkit.image.io.AggregatedImageStore; import org.geotoolkit.image.io.NamedImageStore; import org.geotoolkit.image.io.SampleConverter; import org.geotoolkit.image.io.SpatialImageReadParam; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.coverage.grid.GeneralGridEnvelope; import org.geotoolkit.image.io.metadata.ReferencingBuilder; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.internal.image.io.DimensionManager; import org.geotoolkit.internal.image.io.GridDomainAccessor; import org.geotoolkit.internal.image.io.NetcdfVariable; import org.geotoolkit.internal.image.io.SupportFiles; import org.geotoolkit.io.wkt.PrjFiles; import org.geotoolkit.referencing.adapters.NetcdfAxis; import org.geotoolkit.referencing.adapters.NetcdfCRS; import org.geotoolkit.referencing.adapters.NetcdfCRSBuilder; import org.geotoolkit.resources.Errors; import org.geotoolkit.lang.Workaround; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.internal.util.UnmodifiableArrayList; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.ISO_FORMAT_NAME; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * Base implementation for NetCDF image readers. Pixels are assumed organized according the COARDS * convention (a precursor of <A HREF="http://www.cfconventions.org/">CF Metadata conventions</A>). * For a 4-D dataset with horizontal, vertical and temporal ordinates, the dimension are typically * (<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>) where <var>x</var> index varies faster. * * {@note NetCDF data files actually declare dimensions in reverse order. For the above example, * the dimensions would be declared as (<var>t</var>,<var>z</var>,<var>y</var>,<var>x</var>). This * <code>NetcdfImageReader</code> plugin reverse the order of dimensions read in the NetCDF file, * in order to get an ordering consistent with the ordering used by other plugins.} * * The image is created from the (<var>x</var>,<var>y</var>) dimensions in the above example. * Additional dimensions (if any) are ignored by default: only the slice at index 0 is read, * which is <var>z</var><sub>0</sub> and <var>t</var><sub>0</sub> in the example above. See * below for selecting slices in other dimensions. * * {@section Specifying the variable to read} * Each variable having at least two dimensions (except the variables used for Coordinate System * axes) is an image. The variables to read can be specified using the methods defined in the * {@link NamedImageStore} interface, as in the example below: * * {@preformat java * imageReader.setImageNames("temperature", "salinity"); * BufferedImage temperature = imageReader.read(0); * BufferedImage salinity = imageReader.read(1); * } * * Alternatively, the variables can be assigned to bands instead than images. This is useful * when two related variables - for example the East-West (<var>U</var>) and North-South * (<var>V</var>) components of wind speed - shall be stored in the same image: * * {@preformat java * imageReader.setBandNames(0, "WindSpeed-U", "WindSpeed-V"); * BufferedImage windSpeed = imageReader.read(0); * // windSpeed is now an image with two bands. * } * * {@section Specifying the slice to read in extra dimensions} * For any dimension greater than 2, the region to read can be specified in two different ways: * <p> * <ul> * <li>The slice to read can be specified by {@link DimensionSlice} objects, which are * associated to the {@link SpatialImageReadParam} object controlling the reading * process. This approach is similar to the WCS 2.0 specification.</li> * <li>The slice to read can be specified as bands or as image index, using the methods * defined in the {@link MultidimensionalImageStore} interface. This approach allows * compatibility with library working only with the Java Image I/O API.</li> * </ul> * * {@section Connection to DODS servers} * This image reader accepts {@link String}, {@link File}, {@link URL} and {@link URI} inputs. * The input can use the DODS protocol (as in "{@code dods://opendap.aviso.oceanobs.com/}"), * in order to connect to the specified DODS remote server. * * {@section Support of related formats} * This implementation uses the <a href="http://www.unidata.ucar.edu/software/netcdf-java/">UCAR * NetCDF library</a> for reading data. Consequently, it can be used for reading other formats * supported by that library. For a list of supported formats, see * <a href="http://www.unidata.ucar.edu/software/netcdf-java/formats/FileTypes.html">file types * and remote access protocols</a> on the NetCDF web site. * * @author Martin Desruisseaux (Geomatys) * @author Antoine Hnawia (IRD) * @author Johann Sorel (Geomatys) * @version 3.20 * * @see org.geotoolkit.referencing.adapters.NetcdfCRS * * @since 3.08 (derived from 2.4) * @module */ public class NetcdfImageReader extends FileImageReader implements MultidimensionalImageStore, NamedImageStore, AggregatedImageStore, CancelTask { /** * The enhancements to enable when opening a NetCDF data set. */ private static final Set<NetcdfDataset.Enhance> ENHANCEMENTS; static { final Set<NetcdfDataset.Enhance> modes = EnumSet.noneOf(NetcdfDataset.Enhance.class); modes.add(NetcdfDataset.Enhance.ScaleMissingDefer); modes.add(NetcdfDataset.Enhance.CoordSystems); modes.add(NetcdfDataset.Enhance.ConvertEnums); ENHANCEMENTS = Collections.unmodifiableSet(modes); } /** * The API to use for selecting dimensions above the two standard (column, row) dimensions. */ private final DimensionManager dimensionManager; /** * The NetCDF dataset, or {@code null} if not yet open. The NetCDF file is open by * {@link #ensureFileOpen()} when first needed. */ private NetcdfDataset dataset; /** * The builder for {@link NetcdfCRS} objects, created when first needed. * <p> * This field is not used directly by this class. * But it is used by {@link NetcdfMetadata#setCoordinateSystem}. */ NetcdfCRSBuilder crsBuilder; /** * The name of the {@linkplain Variable variables} found in the NetCDF file. * The first name is assigned to image index 0, the second name to image index 1, * <i>etc</i>. This list shall be immutable. * <p> * The user can override this list with his own list of variable to read, * by calls to the {@link #setImageNames(String[])} method. */ private List<String> variableNames; /** * The image index of the current {@linkplain #variable variable}. */ private int variableIndex; /** * The name of the current {@linkplain #variable variable}. This is the name specified * to {@link #setImageNames(String[])} or {@link #setBandNames(int, String[])}, which * may be slightly different than {@link Variable#getName()}. */ private String variableName; /** * The data from the NetCDF file for a given image index. The value for this field is set by * {@link #prepareVariable} when first needed, and may be updated when the argument given to * any method expecting a {@code imageIndex} parameter has changed. * <p> * This field is typically (but not necessarily) an instance of {@link VariableDS}. */ protected Variable variable; /** * The last error from the NetCDF library. */ private String lastError; /** * {@code true} if {@link CoordSysBuilderIF#buildCoordinateSystems} has been invoked * for the current dataset. */ private boolean metadataLoaded; /** * The CRS and <cite>grid to CRS</cite> transform provided by GDAL in the {@code spatial_ref_sys} * and {@code GeoTransform} variable attributes. Those attributes are not conform to the CF * conventions and don't seem to be recognized by the UCAR NetCDF library version 4.2.26. * * @see #getGridMapping() */ @Workaround(library="NetCDF", version="4.2.26") private transient Map<String,GDALGridMapping> gridMapping; /** * Constructs a new NetCDF reader. * * @param spi The service provider. */ public NetcdfImageReader(final Spi spi) { super(spi != null ? spi : new Spi()); dimensionManager = new DimensionManager(this); } /** * Returns the dimension assigned to the given API. By default, the Image I/O API is used * as below: * <p> * <ul> * <li>The {@linkplain ImageReadParam#setSourceRegion source region} specifies the region * to read in the 2 first dimensions (typically <var>x</var> and <var>y</var>).</li> * <li>The {@linkplain ImageReadParam#setSourceBands(int[]) source bands} are not used * and should be set to 0.</li> * <li>The {@code imageIndex} specifies the variable to read, where the variable name * is determined by <code>{@linkplain #getImageNames()}.get(imageIndex)</code>.</li> * </ul> * <p> * The above-cited default can be changed by calls to this method. For example, the following * method call allows usage of the otherwise unused bands API for selecting slices in the third * dimension: * * {@preformat java * reader.setImageNames("temperature"); * reader.getDimensionForAPI(API.BANDS).addDimensionId(2); // Zero-based index * } * * The following method call reads the same image than the above example, * but uses the image index for selecting slices in the third dimension: * * {@preformat java * reader.setBandNames(0, "temperature"); * reader.getDimensionForAPI(API.IMAGES).addDimensionId(3); * } * * This NetCDF reader allows usage of * {@link org.geotoolkit.image.io.DimensionSlice.API#BANDS BANDS} and * {@link org.geotoolkit.image.io.DimensionSlice.API#IMAGES IMAGES} API. * The {@code COLUMNS} and {@code ROWS} API can not be assigned to new * dimensions in current implementation. * * @since 3.15 */ @Override public DimensionIdentification getDimensionForAPI(final DimensionSlice.API api) { return dimensionManager.getOrCreate(api); } /** * {@inheritDoc} * * @since 3.15 */ @Override public DimensionSlice.API getAPIForDimension(Object... identifiers) { return dimensionManager.getAPI(identifiers); } /** * {@inheritDoc} * * @since 3.15 */ @Override public Set<DimensionSlice.API> getAPIForDimensions() { return dimensionManager.getAPIs(); } /** * Returns the URIs to the aggregated files, or {@code null} if none. This information applies * mostly to NcML files, which are XML files listing many NetCDF files to be aggregated as if * they were a single dataset. The method returns the individual files that compose such * aggregation. * * @param imageIndex The index of the variable for which to get the aggregated files. * @return The individual files which are aggregated, or {@code null} if none. * @throws IOException If an error occurred while building the list of files. * * @see NetcdfDataset#getAggregation() * * @since 3.16 */ @Override public List<URI> getAggregatedFiles(int imageIndex) throws IOException { clearAbortRequest(); ensureFileOpen(); imageIndex = dimensionManager.replaceImageIndex(imageIndex); String name = dimensionManager.getVariableName(imageIndex); if (name == null) { name = getVariableNames().get(imageIndex); } return getAggregatedFiles(dataset, name, null); } /** * Adds the aggregated files to the given list. This method invokes itself recursively * if an aggregation is a outer aggregation containing inner elements. * * @param dataset The dataset from which to get the aggregation. * @param variable The name of the variable for which to get aggregated elements. * @param addTo The list in which to add the URI, or {@code null} if not yet created. * @return The {@code addTo} list, or a new list if {@code addTo} was null and new elements * were found. * @throws IOException If an error occurred while building the list of files. */ private List<URI> getAggregatedFiles(NetcdfDataset dataset, final String variable, List<URI> addTo) throws IOException { final Aggregation aggregation = dataset.getAggregation(); if (aggregation != null) { final List<Aggregation.Dataset> components = aggregation.getDatasets(); if (components != null) { if (addTo == null) { addTo = new ArrayList<>(components.size()); } for (final Aggregation.Dataset component : components) { if (abortRequested()) { throw new IIOException(errors().getString(Errors.Keys.CanceledOperation)); } if (component != null) { /* * We will process the aggregated file only if it contains the variable * we are looking for. */ final NetcdfFile componentFile = component.acquireFile(this); if (componentFile.findVariable(variable) != null) { final String location = component.getLocation(); if (location == null) { /* * If the component does not contain a link to a file, it may be an * outer aggregation which contain inner aggregations. Explore the * content recursively. */ if (componentFile instanceof NetcdfDataset) { addTo = getAggregatedFiles((NetcdfDataset) componentFile, variable, addTo); } } else { /* * Get the URI, wrapping exception in a MalformedURLException since * in order to get a subclass of IOException. We give the location * as the message a let the more detailled explanation in the cause. * We do that because MalformedURLException does not have an getInput() * method (at the opposite of URISyntaxException). */ final URI url; try { url = new URI(location); } catch (URISyntaxException c) { MalformedURLException e = new MalformedURLException(location); e.initCause(c); throw e; } addTo.add(url); } } componentFile.close(); } } } } return addTo; } /** * Returns the names of the variables to be read. The first name is assigned to the image * at index 0, the second name to the image at index 1, <i>etc</i>. In other words a call * to <code>{@linkplain #read(int) read}(imageIndex)</code> will read the variable named * {@code variables.get(imageIndex)} where {@code variables} is the list returned by this * method. * <p> * The sequence of variables to be read can be changed by a call to * {@link #setImageNames(String[])}. * * @return The names of the variables to be read. * @throws IOException if the NetCDF file can not be read. * * @see NetcdfDataset#getVariables() */ @Override public List<String> getImageNames() throws IOException { List<String> names = dimensionManager.getImageNames(); if (names == null) { ensureFileOpen(); names = getVariableNames(); } return names; } /** * Sets the name of the {@linkplain Variable variables} to be read in a NetCDF file. * The first name is assigned to image index 0, the second name to image index 1, * <i>etc</i>. * <p> * Special cases: * <ul> * <li>If {@code variableNames} is set to {@code null}, then the variables will be inferred * from the content of the NetCDF file. This is the default behavior.</li> * <li>If the {@link org.geotoolkit.image.io.DimensionSlice.API#IMAGES IMAGES} API has been * assigned to a dimension, then at most one variable can be specified.</li> * </ul> * * @param names The set of variables to be assigned to image index, * or {@code null} for all variables declared in the NetCDF file. * @throws IOException if the NetCDF file can not be read. */ @Override public void setImageNames(final String... names) throws IOException { dimensionManager.setImageNames(names); variable = null; // Will force a reload. } /** * Returns the number of images available from the current input source. By default, this * method returns the number of {@linkplain #getImageNames() variables} since each variable * is considered as an image. However if the * {@link org.geotoolkit.image.io.DimensionSlice.API#IMAGES IMAGES} API has been assigned * to a dimension, then this method returns the number of slices in that dimension. * * @throws IllegalStateException if the input source has not been set. * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumImages(final boolean allowSearch) throws IllegalStateException, IOException { if (dimensionManager.usesImageAPI()) { /* * If the user uses the image index for reading slices in the hyper-cube, * returns the number of slices in the selected dimension. */ if (!allowSearch) { // It is necessary to NOT invoke 'prepareVariable(0)' in this case in order // to avoid an infinite loop when 'checkImageIndex(int)' is invoked. return -1; } // Index 0 below is arbitrary - just the most likely one to be wanted. prepareVariable(DimensionManager.DEFAULT_IMAGE_INDEX); final int imageDimension = findDimensionIndex(DimensionSlice.API.IMAGES, variable.getRank()); if (imageDimension >= 0) { return variable.getDimension(imageDimension).getLength(); } } return getImageNames().size(); } /** * Returns the names of the bands for the given image, or {@code null} if none. * By default, this method returns {@code null} for every image index. Non-null * values can be specified with calls to the {@link #setBandNames(int, String[])} * method. * * @param imageIndex Index of the image for which to get the band names. * @return The variable names of the bands for the given image, or {@code null} * if the bands for the given image are unnamed. * @throws IOException if the NetCDF file can not be read. * * @since 3.11 */ @Override public List<String> getBandNames(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return dimensionManager.getBandNames(imageIndex); } /** * Sets the names of the bands for the given image, or {@code null} for removing any naming. * This method is useful for merging different variables as different bands in the image to * be read, typically because each band is a vector component. See the {@link NamedImageStore} * javadoc for an example. * * @param imageIndex Index of the image for which to set the band names. * @param names The variable names of the bands for the given image, * or {@code null} for removing any naming. * @throws IOException if the NetCDF file can not be read. * * @since 3.11 */ @Override public void setBandNames(final int imageIndex, String... names) throws IOException { checkImageIndex(imageIndex); dimensionManager.setBandNames(imageIndex, names); } /** * Returns the number of bands available for the image identified by the given index. The * default implementation returns the value of the first of the following conditions which * is hold: * <p> * <ol> * <li>If the bands at the give image index {@linkplain #setBandNames have been * assigned to variable names}, returns the number of assigned variables.</li> * <li>Otherwise if the bands API has been {@linkplain MultidimensionalImageStore assigned to * a dimension}, return the {@linkplain VariableIF#getDimension(int) dimension} length of * the {@linkplain #variable} identified by the given image index.</li> * <li>Otherwise the {@linkplain FileImageReader#getNumBands(int) default number of bands} * is 1.</li> * </ol> * * @param imageIndex The image index. * @return The number of bands available. * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumBands(final int imageIndex) throws IOException { final int internalIndex = dimensionManager.replaceImageIndex(imageIndex); final List<String> bandNames = dimensionManager.getBandNames(internalIndex); if (bandNames != null) { return bandNames.size(); } prepareVariable(imageIndex); final int bandDimension = findDimensionIndex(DimensionSlice.API.BANDS, variable.getRank()); if (bandDimension >= 0) { return variable.getDimension(bandDimension).getLength(); } return super.getNumBands(imageIndex); } /** * Returns the image width. * * @throws IOException If an error occurred while reading the NetCDF file. * * @see Variable#getDimension(int) */ @Override public int getWidth(final int imageIndex) throws IOException { prepareVariable(imageIndex); return variable.getDimension(variable.getRank() - (X_DIMENSION + 1)).getLength(); } /** * Returns the image height. * * @throws IOException If an error occurred while reading the NetCDF file. * * @see Variable#getDimension(int) */ @Override public int getHeight(final int imageIndex) throws IOException { prepareVariable(imageIndex); return variable.getDimension(variable.getRank() - (Y_DIMENSION + 1)).getLength(); } /** * Returns the grid envelope in the image identified by the given index. * * @see Variable#getDimension(int) * * @since 3.19 */ @Override public GridEnvelope getGridEnvelope(final int imageIndex) throws IOException { prepareVariable(imageIndex); final int rank = variable.getRank(); final int[] lower = new int[rank]; final int[] upper = new int[rank]; for (int i=0; i<rank;) { upper[i] = variable.getDimension(rank - ++i).getLength(); } return new GeneralGridEnvelope(lower, upper, false); } /** * Returns the number of dimensions in the image identified by the given index. * In the case of NetCDF files, this is the {@linkplain VariableIF#getRank() rank} * of the {@linkplain #variable} associated to the given image index. * * @param imageIndex The image index. * @return The number of dimension for the image at the given index. * @throws IOException if an error occurs reading the information from the input source. * * @see Variable#getRank() */ @Override public int getDimension(final int imageIndex) throws IOException { prepareVariable(imageIndex); return variable.getRank(); } /** * Returns the index of the dimension which has been assigned to the given API, or -1 if none. * The {@link #prepareVariable} method shall be invoked prior this method (this is not verified). * <p> * Note that this method returns the index in the NetCDF {@linkplain #variable}, which is the * reverse order of axis order as viewed from this {@code ImageReader}. * * @param api The API for which to get the dimension index in NetCDF variable. * @param rank The number of dimensions (the rank) in the {@linkplain #variable}. * @return The dimension index in the NetCDF variable, or {@code -1} if none. * @throws IOException If an I/O error occurred. */ private int findDimensionIndex(final DimensionSlice.API api, final int rank) throws IOException { final DimensionIdentification dimension = dimensionManager.get(api); if (dimension != null) { /* * The code below uses a custom Iterable in order to invoke the getAxes(...) * method (which may force the loading of metadata) only if really needed. */ int n; try { n = dimension.findDimensionIndex(new Iterable<Map.Entry<?,Integer>>() { @Override public Iterator<Map.Entry<?,Integer>> iterator() { final List<CoordinateAxis> axes; try { axes = getAxes(rank); } catch (IOException e) { // Will be caught in the enclosing method. throw new BackingStoreException(e); } return (axes != null) ? new NetcdfAxesIterator(axes) : Collections.<Map.Entry<?,Integer>>emptySet().iterator(); } }); } catch (BackingStoreException e) { throw e.unwrapOrRethrow(IOException.class); } /* * If we found the dimension, convert the index from this ImageReader axis order * (typically (x,y,z,t)) to the NetCDF axis order (typically (t,z,y,x)). In this * process, we also ensure that the index is not one of the reserved ones. */ if (n >= 0) { switch (n) { case X_DIMENSION: case Y_DIMENSION: { throw new IllegalImageDimensionException(errors().getString(Errors.Keys.IllegalParameterValue_2, "DimensionSlice(" + api.name() + ')', n)); } } return rank - (n+1); } } return -1; } /** * Returns the indices along all dimensions of the slice to read. The default value for * indices that were not explicitly specified is 0. This method returns elements in the * NetCDF order (reverse of the usual order). * <p> * The {@link #prepareVariable} method shall be invoked prior this method (this is not verified). * * @param param The parameters supplied by the user to the {@code read} method. * @param rank The number of dimensions (the rank) in the {@linkplain #variable}. * @return The indices in NetCDF order (reverse of usual order), as values from 0 * inclusive to {@link Dimension#getLength()} exclusive. * @throws IOException If an error occurred while reading the NetCDF file. */ @SuppressWarnings("fallthrough") private int[] getSourceIndices(final ImageReadParam param, final int rank) throws IOException { final int[] indices = new int[rank]; if (param instanceof SpatialImageReadParam) { final SpatialImageReadParam p = (SpatialImageReadParam) param; if (!p.getDimensionSlices().isEmpty()) { final List<CoordinateAxis> axes = getAxes(rank); final Object[] properties = new Object[axes != null ? 3 : 1]; for (int i=0; i<rank; i++) { final CoordinateAxis axis = (axes != null) ? axes.get(i) : null; switch (properties.length) { default: properties[2] = NetcdfAxis.getDirection(axis); case 2: properties[1] = axis.getFullName(); case 1: properties[0] = (rank - 1) - i; case 0: break; } indices[i] = p.getSliceIndex(properties); } } } return indices; } /** * Returns the axes of the first coordinate system having at least the given number of * dimensions. The {@link #prepareVariable} method shall be invoked prior this method * (this is not verified). * <p> * This method is not public because it duplicates (in a different form) the informations * already provided in image metadata. We use it when we want only this specific information * without the rest of metadata. In many cases this method will not be invoked at all, thus * avoiding the need to load metadata. * <p> * This method returns axes in NetCDF order (reverse of the usual order). * * @param rank The number of dimensions (the rank) in the {@linkplain #variable}. * @return The axes in NetCDF order, or {@code null} if no set of axis is applicable. * @throws IOException If an error occurred while reading the NetCDF file. */ private List<CoordinateAxis> getAxes(final int rank) throws IOException { if (variable instanceof Enhancements) { ensureMetadataLoaded(); final List<CoordinateSystem> sys = ((Enhancements) variable).getCoordinateSystems(); if (sys != null) { final int count = sys.size(); for (int i=0; i<count; i++) { final CoordinateSystem cs = sys.get(i); final List<CoordinateAxis> axes = cs.getCoordinateAxes(); if (axes != null && axes.size() >= rank) { return axes; } } } } return null; } /** * Returns the map containing the CRS and <cite>grid to CRS</cite> transform provided by GDAL. * This method is for use by {@link NetcdfMetadata} only. */ final Map<String,GDALGridMapping> getGridMapping() { if (gridMapping == null) { gridMapping = new HashMap<>(); } return gridMapping; } /** * Ensures that metadata are loaded. */ private void ensureMetadataLoaded() throws IOException { if (!metadataLoaded) { CoordSysBuilder.factory(dataset, this).buildCoordinateSystems(dataset); metadataLoaded = true; } } /** * Creates metadata from {@code .tfw} and {@code .prj} files, if they are present. * This is used as a workaround for NetCDF files with insufficient information in * global attributes. If there is no {@code .tfw} file, then this method returns * {@code null}. */ private SpatialMetadata createMetadataFromWorldFiles(final int imageIndex) throws IOException { AffineTransform gridToCRS = null; CoordinateReferenceSystem crs = null; Object input = IOUtilities.changeExtension(this.input, "tfw"); if (input != null) try { gridToCRS = SupportFiles.parseTFW(IOUtilities.open(input), input); } catch (FileNotFoundException | NoSuchFileException e) { // Ignore. TODO: refactor in Apache SIS in order to check if file exists. } input = IOUtilities.changeExtension(this.input, "prj"); if (input != null) try { crs = PrjFiles.read(IOUtilities.open(input), true); } catch (FileNotFoundException | NoSuchFileException e) { // Ignore. TODO: refactor in Apache SIS in order to check if file exists. } if (gridToCRS != null || crs != null) { final SpatialMetadata metadata = new SpatialMetadata(false, this, null); if (gridToCRS != null) { final int width = getWidth (imageIndex); final int height = getHeight(imageIndex); new GridDomainAccessor(metadata).setAll(gridToCRS, new Rectangle(width, height), null, PixelOrientation.UPPER_LEFT); } if (crs != null) { new ReferencingBuilder(metadata).setCoordinateReferenceSystem(crs); } return metadata; } return null; } /** * Creates a new stream or image metadata. This method is invoked automatically when first * needed. * * @see CoordSysBuilder#factory(NetcdfDataset, CancelTask) */ @Override protected SpatialMetadata createMetadata(final int imageIndex) throws IOException { ensureFileOpen(); ensureMetadataLoaded(); /* * For stream metadata, returns a tree built from the global attributes only. */ if (imageIndex < 0) { return new NetcdfMetadata(this, dataset); } else { final SpatialMetadata metadata = createMetadataFromWorldFiles(imageIndex); if (metadata != null) { return metadata; } // TODO: we should try to read other information too. For now we presume // that when we need this workaround, the NetCDF metadata were very poor // anyway. } /* * If the image index is used for navigating through a third dimension, then the * image metadata are the same for all image index. Returns the common metadata. */ final int internalIndex = dimensionManager.replaceImageIndex(imageIndex); if (internalIndex != imageIndex) { return super.getImageMetadata(internalIndex); } /* * For image metadata, returns a tree built from the variable attributes, where * the variable is inferred from the image index. In the special case where the * user assigned many variable to the same image index (where each variable is * handled as a band), we need to build the variables list from the band names. */ final Variable[] variables; final List<String> bandNames = dimensionManager.getBandNames(internalIndex); if (bandNames != null) { variables = new Variable[bandNames.size()]; for (int i=0; i<variables.length; i++) { variables[i] = findVariable(bandNames.get(i)); } } else { prepareVariable(imageIndex); variables = new Variable[] {variable}; } final NetcdfMetadata metadata = new NetcdfMetadata(this, dataset, variables); metadata.workaroundNonStandard(dataset); return metadata; } /** * Returns the data type which most closely represents the "raw" internal data of the image. * In the case of NetCDF files, The raw type is determined from the value returned by the * {@link VariableIF#getDataType()} method on the {@linkplain #variable} identified by the * given index. The NetCDF {@link ucar.ma2.DataType} is then mapped to one of the * {@link DataBuffer} constants. * * @param imageIndex The index of the image to be queried. * @return The data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown. * @throws IOException If an error occurred while reading the NetCDF file. */ @Override protected int getRawDataType(final int imageIndex) throws IOException { prepareVariable(imageIndex); return NetcdfVariable.getRawDataType(variable); } /** * Ensures that the NetCDF file is open, but does not load any variable yet. * The {@linkplain #variable} will be read by {@link #prepareVariable} only. */ private void ensureFileOpen() throws IOException { if (dataset == null) { /* * Clears the 'abort' flag here (instead of in 'read' method only) because * we pass this ImageReader instance to the NetCDF DataSet as a CancelTask. */ lastError = null; clearAbortRequest(); final String inputURL; boolean useCache = true; final Object input = this.input; if (input instanceof NetcdfFile) { if (input instanceof NetcdfDataset) { dataset = (NetcdfDataset) input; } else { dataset = new NetcdfDataset((NetcdfFile) input); } return; } switch (Protocol.getProtocol(input)) { case DODS: { inputURL = input.toString(); final int s = inputURL.indexOf('?'); if (s >= 0) { variableNames = UnmodifiableArrayList.wrap(new String[] {inputURL.substring(s + 1)}); } break; } default: { /* * The NetCDF library accepts URL, so don't create a temporary file for them. * Just convert the URL to a String and use that directly. * TODO add Path to supported list ? */ if (input instanceof String || input instanceof URL || input instanceof URI || input instanceof File) { inputURL = input.toString(); } else { /* * For other types (especially ImageInputStream), we need * to copy the content to a temporary file before to open it. */ inputURL = getInputPath().toString(); useCache = false; } break; } } if (useCache) { dataset = NetcdfDataset.acquireDataset(null, inputURL, ENHANCEMENTS, 0, this, null); } else { dataset = NetcdfDataset.openDataset(inputURL, ENHANCEMENTS, 0, this, null); } if (dataset == null) { throw new FileNotFoundException(errors().getString( Errors.Keys.FileDoesNotExist_1, inputURL)); } } } /** * Returns the name of all variables in the current NetCDF file. The {@link #ensureFileOpen()} * method must have been invoked before this method (this is not verified). */ private List<String> getVariableNames() throws IOException { if (variableNames == null) { /* * Gets a list of every variables found in the NetcdfDataset and copies the names * in a filtered list which exclude every variable that are dimension of an other * variable. For example "longitude" may be a variable found in the NetcdfDataset, * but is declared only because it is needed as a dimension for the "temperature" * variable. The "longitude" variable is usually not of direct interest to the user * (the interesting variable is "temperature"), so we exclude it. */ final List<Variable> variables = dataset.getVariables(); final String[] filtered = new String[variables.size()]; int count = 0; for (int minLength=2; minLength>=1; minLength--) { for (final VariableIF candidate : variables) { if (NetcdfVariable.isCoverage(candidate, variables, minLength)) { /* * - Images require at least 2 dimensions. They may have more dimensions, * in which case a slice will be taken later. * * - Excludes axis. They are often already excluded by the first condition * because axis are usually 1-dimensional, but some are 2-dimensional, * e.g. a localization grid. * * - Excludes characters, strings and structures, which can not be easily * mapped to an image type. In addition, 2-dimensional character arrays * are often used for annotations and we don't want to confuse them * with images. */ filtered[count++] = candidate.getShortName(); } } if (count != 0) break; // If we didn't found any variable with a length of at least 2 along // 2 dimensions, try again but be less strict (require a length of 1). } variableNames = UnmodifiableArrayList.wrap(ArraysExt.resize(filtered, count)); } return variableNames; } /** * Ensures that data are loaded in the NetCDF {@linkplain #variable}. If data are already * loaded, then this method does nothing. * <p> * This method is invoked automatically before any operation requiring the NetCDF * variable, including (but not limited to): * <ul> * <li>{@link #getWidth(int)}</li> * <li>{@link #getHeight(int)}</li> * <li>{@link #createMetadata(int)}</li> * <li>{@link #getRawDataType(int)}</li> * <li>{@link #read(int,ImageReadParam)}</li> * </ul> * * @param imageIndex The image index. * @return {@code true} if the {@linkplain #variable} changed as a result of this call, * or {@code false} if the current value is already appropriate. * @throws IndexOutOfBoundsException if the specified index is outside the expected range. * @throws IllegalStateException If {@link #input} is not set. * @throws IOException If an error occurred while reading the NetCDF file. */ protected boolean prepareVariable(final int imageIndex) throws IOException { final int internalIndex = dimensionManager.replaceImageIndex(imageIndex); if (variable == null || variableIndex != internalIndex) { checkImageIndex(imageIndex); ensureFileOpen(); /* * Get the name of the variable to search for. This is usually the name at the * given index in the 'selectedName' list. However a special case is performed * if the user invoked the setBandNames(...) method (i.e. specified explicitly * which variable to assign to each band). In such case we will load the first * variable (using the first variable is an arbitrary choice, but work well if * the bands are going to be read in sequential order). */ String name = dimensionManager.getVariableName(internalIndex); if (name == null) { name = getVariableNames().get(internalIndex); } /* * Now get the NetCDF variable and initialize this instance fields. */ final Variable candidate = findVariable(name); final int rank = candidate.getRank(); if (rank < 2) { throw new IIOException(errors().getString(Errors.Keys.NotTwoDimensional_1, rank)); } variable = candidate; variableName = name; variableIndex = internalIndex; return true; } return false; } /** * Returns the variable of the given name. This method is similar to * {@link NetcdfDataset#findVariable(String)} except that the search * is case-insensitive and an exception is thrown if no variable has * been found for the given name. * <p> * Subclasses can override this method if they want this {@code NetcdfImageReader} * to use a different variable for the given name. * * @param name The name of the variable to search. * @return The variable for the given name. * @throws IOException If an error occurred while reading the NetCDF file. * * @see NetcdfDataset#findVariable(String) */ protected Variable findVariable(final String name) throws IOException { ensureFileOpen(); /* * First tries a case-sensitive search. Case matter since the same letter in different * case may represent different variables. For example "t" and "T" are typically "time" * and "temperature" respectively. */ Variable candidate = dataset.findVariable(name); if (candidate != null) { return candidate; } /* * We tried a case-sensitive search without success. Now tries a case-insensitive search * before to report a failure. */ @SuppressWarnings("unchecked") final List<Variable> variables = dataset.getVariables(); if (variables != null) { for (final Variable variable : variables) { if (variable!=null && name.equalsIgnoreCase(variable.getFullName())) { return variable; } } for (final Variable variable : variables) { if (variable!=null && name.equalsIgnoreCase(variable.getShortName())) { return variable; } } } throw new IIOException(errors().getString( Errors.Keys.VariableNotFoundInFile_2, name, dataset.getLocation())); } /** * Creates an image from the specified parameters. * * @throws IOException If an error occurred while reading the NetCDF file. */ @Override public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { clearAbortRequest(); prepareVariable(imageIndex); /* * Fetches the parameters that are not already processed by utility * methods like 'getDestination' or 'computeRegions' (invoked below). */ final int strideX, strideY; final int[] srcBands, dstBands; if (param != null) { strideX = param.getSourceXSubsampling(); strideY = param.getSourceYSubsampling(); srcBands = param.getSourceBands(); dstBands = param.getDestinationBands(); } else { strideX = 1; strideY = 1; srcBands = null; dstBands = null; } final int rank = variable.getRank(); final int imageDimension = findDimensionIndex(DimensionSlice.API.IMAGES, rank); final int bandDimension; /* * Gets the number of source bands, in preference order (must be consistent with the * getNumBands(int) method): from the explicit list of band names, from the variable * dimension at the index identified by DimensionIdentification, or 1. Then check that * the number of source and target bands are consistent. */ final int numSrcBands; final List<String> bandNames = dimensionManager.getBandNames(variableIndex); if (bandNames != null) { numSrcBands = bandNames.size(); bandDimension = -1; } else { bandDimension = findDimensionIndex(DimensionSlice.API.BANDS, rank); numSrcBands = (bandDimension >= 0) ? variable.getDimension(bandDimension).getLength() : 1; } final int numDstBands = (dstBands != null) ? dstBands.length : (srcBands != null) ? srcBands.length : numSrcBands; checkReadParamBandSettings(param, numSrcBands, numDstBands); /* * Gets the destination image of appropriate size. */ final int width = variable.getDimension(rank - (X_DIMENSION + 1)).getLength(); final int height = variable.getDimension(rank - (Y_DIMENSION + 1)).getLength(); final SampleConverter[] converters = new SampleConverter[numDstBands]; final BufferedImage image = getDestination(imageIndex, param, width, height, converters); final WritableRaster raster = image.getRaster(); assert raster.getNumBands() == numDstBands : numDstBands; /* * Computes the source region (in the NetCDF file) and the destination region * (in the buffered image). Copies those informations into UCAR Range structure. */ final Rectangle srcRegion = new Rectangle(); final Rectangle destRegion = new Rectangle(); computeRegions(param, width, height, image, srcRegion, destRegion); final int[] dimensionSlices = getSourceIndices(param, rank); final Range[] ranges = new Range[rank]; for (int i=0; i<ranges.length; i++) { final int first, length, stride; switch (rank - i) { case X_DIMENSION + 1: { first = srcRegion.x; length = srcRegion.width; stride = strideX; break; } case Y_DIMENSION + 1: { first = srcRegion.y; length = srcRegion.height; stride = strideY; break; } default: { first = dimensionSlices[i]; // Already in NetCDF order. length = 1; stride = 1; break; } } try { ranges[i] = new Range(first, first+length-1, stride); } catch (InvalidRangeException e) { throw netcdfFailure(e); } } final List<Range> sections = Arrays.asList(ranges); /* * Reads the requested sub-region only. In the usual case, we read only the current * variable. However if the setBandNames(...) method has been invoked, we may have * many different variables to read, one for each band. */ processImageStarted(imageIndex); final float toPercent = 100f / numDstBands; final int type = raster.getSampleModel().getDataType(); final int xmin = destRegion.x; final int ymin = destRegion.y; final int xmax = destRegion.width + xmin; final int ymax = destRegion.height + ymin; for (int zi=0; zi<numDstBands; zi++) { final int srcBand = (srcBands == null) ? zi : srcBands[zi]; final int dstBand = (dstBands == null) ? zi : dstBands[zi]; Variable bandVariable = variable; if (bandNames != null) { final String name = bandNames.get(srcBand); if (!name.equals(variableName)) { bandVariable = findVariable(name); } } final Array array; try { if (bandDimension >= 0) { /* * Update the Section instance with the index of the slice to read. This code * is executed only if the band API is used for one of the variable dimension, * and the bands are not different variables (i.e. bandNames == null). Note * that there is no need to update 'sections' directly since it wraps directly * the 'ranges' array. */ ranges[bandDimension] = new Range(srcBand, srcBand, 1); } if (imageDimension >= 0) { /* * Like above, but for image index. */ ranges[imageDimension] = new Range(imageIndex, imageIndex, 1); } array = bandVariable.read(sections); } catch (InvalidRangeException e) { throw netcdfFailure(e); } SampleConverter converter = converters[zi]; if (converter == null) { converter = SampleConverter.IDENTITY; } final IndexIterator it = array.getIndexIterator(); for (int y=ymin; y<ymax; y++) { // Y_POSITION for (int x=xmin; x<xmax; x++) { // X_POSITION switch (type) { case DataBuffer.TYPE_DOUBLE: { raster.setSample(x, y, dstBand, converter.convert(it.getDoubleNext())); break; } case DataBuffer.TYPE_FLOAT: { raster.setSample(x, y, dstBand, converter.convert(it.getFloatNext())); break; } default: { raster.setSample(x, y, dstBand, converter.convert(it.getIntNext())); break; } } } } /* * Checks for abort requests after reading. It would be a waste of a potentially * good image (maybe the abort request occurred after we just finished the reading) * if we didn't implemented the 'isCancel()' method. But because of the later, which * is checked by the NetCDF library, we can't assume that the image is complete. */ if (abortRequested()) { processReadAborted(); return image; } /* * Reports progress here, not in the deeper loop, because the costly part is the * call to 'variable.read(...)' which can't report progress. The loop that copy * pixel values is fast, so reporting progress there would be pointless. */ processImageProgress(zi * toPercent); } if (lastError != null) { throw new IIOException(lastError); } processImageComplete(); return image; } /** * Creates a raster from the specified parameters. This method is a bit closer to the actual * NetCDF model than the {@link #read(int, ImageReadParam)}, because NetCDF file usually don't * provide color information. * * @throws IOException If an error occurred while reading the NetCDF file. * * @todo Current implementation delegates to {@code read(int, param)}. * Futures versions should do a more efficient work. * * @since 3.20 */ @Override public Raster readRaster(final int imageIndex, final ImageReadParam param) throws IOException { return read(imageIndex, param).getRaster(); } /** * Returns {@code true} since this class supports calls to * {@link #readRaster(int, ImageReadParam)}. * * @since 3.20 */ @Override public boolean canReadRaster() { return true; } /** * Wraps a generic exception into an {@link IIOException}. */ private IIOException netcdfFailure(final Exception e) throws IOException { return new IIOException(errors().getString(Errors.Keys.CantReadFile_1, dataset.getLocation()), e); } /** * Invoked by the NetCDF library during read operation in order to check if the task has * been canceled. Users should not invoke this method directly. * * @return {@code true} if abort has been requested. */ @Override public boolean isCancel() { return abortRequested(); } /** * Invoked by the NetCDF library for reporting progress * * @param message A description of the operation being executed. * @param progress The progress (not necessarily a percentage). */ public void setProgress(String message, int progress) { } /** * Invoked by the NetCDF library when an error occurred during the read operation. * Users should not invoke this method directly. * * @param message An error message to report. */ @Override public void setError(final String message) { lastError = message; } /** * Returns the error resources bundle. */ private Errors errors() { return Errors.getResources(locale); } /** * Closes the NetCDF file. * * @throws IOException If an error occurred while accessing the NetCDF file. */ @Override protected void close() throws IOException { metadataLoaded = false; crsBuilder = null; gridMapping = null; lastError = null; variable = null; variableName = null; variableNames = null; try { if (dataset != null) { dataset.close(); dataset = null; } } finally { super.close(); // Must delete the temporary file only after we closed the dataset. } } /** * Restores the {@code ImageReader} to its initial state. This method removes the input, * the locale, all listeners and any {@link org.geotoolkit.image.io.DimensionSlice.API}. */ @Override public void reset() { super.reset(); dimensionManager.clear(); } /** * The service provider for {@code NetcdfImageReader}. This SPI provides * necessary implementation for creating default {@link NetcdfImageReader}. * <p> * The default constructor initializes the fields to the values listed below. * Users wanting different values should create a subclass of {@code Spi} and * set the desired values in their constructor. * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"><th>Field</th><th>Value</th></tr> * <tr><td> {@link #names}  </td><td> {@code "NetCDF"} </td></tr> * <tr><td> {@link #MIMETypes}  </td><td> {@code "application/netcdf"}, {@code "application/x-netcdf"} </td></tr> * <tr><td> {@link #pluginClassName}  </td><td> {@code "org.geotoolkit.image.io.plugin.NetcdfImageReader"} </td></tr> * <tr><td> {@link #vendorName}  </td><td> {@code "Geotoolkit.org"} </td></tr> * <tr><td> {@link #version}  </td><td> Value of {@link org.geotoolkit.util.Version#GEOTOOLKIT} </td></tr> * <tr><td colspan="2" align="center">See super-class javadoc for remaining fields</td></tr> * </table> * * @author Martin Desruisseaux (Geomatys) * @author Antoine Hnawia (IRD) * @version 3.20 * * @since 3.08 (derived from 2.4) * @module */ public static class Spi extends FileImageReader.Spi { /** * The name of the native format. It has no version number because this is * a "dynamic" format inferred from the actual content of the NetCDF file. */ static final String NATIVE_FORMAT_NAME = "NetCDF"; /** * List of legal names for NetCDF readers. */ static final String[] NAMES = new String[] {"NetCDF", "netcdf"}; /** * The mime types for the default {@link NetcdfImageReader} configuration. */ static final String[] MIME_TYPES = new String[] {"application/netcdf", "application/x-netcdf"}; /** * Default list of file's extensions. */ static final String[] SUFFIXES = new String[] {"nc", "ncml", "cdf", "grib", "grib1", "grib2", "grb", "grb1", "grb2", "grd"}; /** * Constructs a default {@code NetcdfImageReader.Spi}. The fields are initialized as * documented in the <a href="#skip-navbar_top">class javadoc</a>. Subclasses can * modify those values if desired. * <p> * For efficiency reasons, the fields are initialized to shared arrays. * Subclasses can assign new arrays, but should not modify the default array content. */ public Spi() { names = NAMES; MIMETypes = MIME_TYPES; suffixes = SUFFIXES; pluginClassName = "org.geotoolkit.image.io.plugin.NetcdfImageReader"; writerSpiNames = new String[] {"org.geotoolkit.image.io.plugin.NetcdfImageWriter$Spi"}; final int length = inputTypes.length; inputTypes = Arrays.copyOf(inputTypes, length+1); inputTypes[length] = NetcdfFile.class; nativeStreamMetadataFormatName = NATIVE_FORMAT_NAME; nativeImageMetadataFormatName = NATIVE_FORMAT_NAME; addExtraMetadataFormat(GEOTK_FORMAT_NAME, true, true); addExtraMetadataFormat(ISO_FORMAT_NAME, true, false); } /** * Returns a description for this provider. * * @todo Localize */ @Override public String getDescription(final Locale locale) { return "NetCDF image decoder"; } /** * Checks if the specified input seems to be a readable NetCDF file. Current * implementation check if the given source is a filename having one of the * NetCDF {@linkplain #getFileSuffixes() file suffixes}. In particular this * method conservatively returns {@code false} if the given input is a stream * like {@link javax.imageio.stream.ImageInputStream}, because testing the stream * content would require copying it to a temporary file. * * @param source the object (typically a {@link File} or {@link java.nio.file.Path}) to be decoded. * @return {@code true} if it is likely that the given source can be decoded. * @throws IOException If an error occurred while opening the file. */ @Override public boolean canDecodeInput(final Object source) throws IOException { if (IOUtilities.canProcessAsPath(source)) { return ArraysExt.containsIgnoreCase(SUFFIXES, IOUtilities.extension(source)); /* * If a future version wants to use NetcdfFile.canOpen(String), * then please verify that the following issues are resolved: * * - canOpen(String) recognizes NcML files (last time we tried, it didn't read * any XML file, which is one reason why we had to rely on file extension). * * - canOpen(String) doesn't throw an OutOfMemoryError when given non-NetCDF * file (e.g. PNG, TIFF or JPEG files). Last time we tried, some text file * format decoders invoked BufferedReader.readLine(), which can read huge * amount of data when a binary file contains few '\n' or '\r' bytes. * * - canOpen(String) doesn't load large library like VisAD just for testing * a few bytes, or at the very least doesn't throw NoClassDefFoundError * when such optional dependency is not on the classpath. * * - canOpen(String) returns 'false' if the format is not recognized. Last * time we tried, some code paths throw IOException instead, which make * difficult to distinguish unrecognized formats from real I/O errors. */ } return false; } /** * Returns an instance of the {@code NetcdfImageReader} implementation associated * with this service provider. * * @param extension An optional extension object, which may be null. * @param extension An optional extension object, which may be null. * @return An image reader instance. * @throws IOException if the attempt to instantiate the reader fails. */ @Override public ImageReader createReaderInstance(final Object extension) throws IOException { return new NetcdfImageReader(this); } } }