/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.image.io.netcdf; import java.util.Set; import java.util.List; import java.util.Locale; import java.util.HashSet; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.awt.image.DataBuffer; import java.net.URL; import java.net.URI; import java.io.File; import java.io.IOException; import java.io.FileNotFoundException; import javax.imageio.IIOException; import javax.imageio.ImageReader; import javax.imageio.ImageReadParam; import javax.imageio.metadata.IIOMetadata; import ucar.ma2.Array; import ucar.ma2.Range; import ucar.ma2.DataType; import ucar.ma2.IndexIterator; import ucar.ma2.InvalidRangeException; import ucar.nc2.dataset.AxisType; import ucar.nc2.dataset.CoordinateAxis; import ucar.nc2.dataset.CoordinateSystem; import ucar.nc2.dataset.CoordSysBuilder; import ucar.nc2.dataset.NetcdfDataset; import ucar.nc2.dataset.VariableDS; import ucar.nc2.dataset.VariableEnhanced; import ucar.nc2.dods.DODSNetcdfFile; import ucar.nc2.util.CancelTask; import ucar.nc2.Dimension; import ucar.nc2.Variable; import ucar.nc2.VariableIF; import org.geotools.image.io.FileImageReader; import org.geotools.image.io.SampleConverter; import org.geotools.image.io.metadata.GeographicMetadata; import org.geotools.math.Statistics; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Base implementation for NetCDF image reader. Pixels are assumed organized according the COARDS * convention (a precursor of <A HREF="http://www.cfconventions.org/">CF Metadata conventions</A>), * i.e. in (<var>t</var>,<var>z</var>,<var>y</var>,<var>x</var>) order, where <var>x</var> varies * faster. The image is created from the two last dimensions (<var>x</var>,<var>y</var>). * Additional dimensions (if any) are handled as below in the default implementation: * <p> * <ul> * <li>The third dimension (<var>z</var> in the above sequence) is assigned to bands. Users * can change this behavior by invoking the {@link NetcdfReadParam#setBandDimensionTypes} * method.</li> * <li>Additional dimensions like <var>t</var> are ignored; only the first slice is selected. * Users can change this behavior by invoking the {@link NetcdfReadParam#setSliceIndice} * method.</li> * </ul> * <p> * <b>Example:</b><br> * Assuming that: * <ul> * <li>None of the above methods has been invoked</li> * <li>Axis are (<var>t</var>,<var>z</var>,<var>y</var>,<var>x</var>)</li> * </ul> * Then the users can select the <var>z</var> value using {@link ImageReadParam#setSourceBands}. * If no band is selected, then the default selection is the first band (0) only. Note that this * is different than the usual Image I/O default, which is all bands. * <p> * <b>Connection to DODS servers</b> * This image reader accepts {@link File} and {@link URL} inputs. In the later case, if and only * if the URL uses the DODS protocol (as in "{@code dods://opendap.aviso.oceanobs.com/}"), then * this image reader tries to connect to the DODS remote server. Otherwise the URL content is * copied in a temporary file. * * @since 2.4 * @source $URL$ * @version $Id$ * @author Antoine Hnawia * @author Martin Desruisseaux */ public class NetcdfImageReader extends FileImageReader implements CancelTask { /** * The URL protocol for connections to a DODS server. Any URL with this protocol will be * open using {@link DODSNetcdfFile} instead of the ordinary {@link NetcdfDataset}. The * later works only for local {@linkplain File files}, while the former connects to a * remote server. */ private static final String DODS_PROTOCOL = "dods"; /** * The dimension <strong>relative to the rank</strong> in {@link #variable} to use as image * width. The actual dimension is {@code variable.getRank() - X_DIMENSION}. Is hard-coded * because the loop in the {@code read} method expects this order. */ private static final int X_DIMENSION = 1; /** * The dimension <strong>relative to the rank</strong> in {@link #variable} to use as image * height. The actual dimension is {@code variable.getRank() - Y_DIMENSION}. Is hard-coded * because the loop in the {@code read} method expects this order. */ private static final int Y_DIMENSION = 2; /** * The default dimension <strong>relative to the rank</strong> in {@link #variable} to use * as image bands. The actual dimension is {@code variable.getRank() - Z_DIMENSION}. * <p> * At the difference of {@link #X_DIMENSION} and {@link #Y_DIMENSION}, this dimension doesn't * need to be hard-coded. User can invoke {@link NetcdfReadParam#setBandDimensionTypes} in * order to provide a different value. */ private static final int Z_DIMENSION = 3; /** * The data type to accept in images. Used for automatic detection of which variables * to assign to images. */ private static final Set<DataType> VALID_TYPES = new HashSet<DataType>(12); static { VALID_TYPES.add(DataType.BOOLEAN); VALID_TYPES.add(DataType.BYTE); VALID_TYPES.add(DataType.SHORT); VALID_TYPES.add(DataType.INT); VALID_TYPES.add(DataType.LONG); VALID_TYPES.add(DataType.FLOAT); VALID_TYPES.add(DataType.DOUBLE); } /** * The NetCDF dataset, or {@code null} if not yet open. The NetCDF file is open by * {@link #ensureOpen} when first needed. */ private NetcdfDataset dataset; /** * 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, * <cite>etc.</cite>. */ private String[] variableNames; /** * The image index of the current {@linkplain #variable variable}. */ private int variableIndex; /** * The data from the NetCDF file. The value for this field is set by {@link #prepareVariable} * when first needed. This is typically (but not necessarly) an instance of {@link VariableDS}. */ protected Variable variable; /** * The last error from the NetCDF library. */ private String lastError; /** * {@code true} if {@link CoordSysBuilder#addCoordinateSystems} has been invoked * for current file. */ private boolean metadataLoaded; /** * The stream metadata. Will be created only when first needed. */ private IIOMetadata streamMetadata; /** * The current image metadata. Will be created only when first needed. */ private IIOMetadata imageMetadata; /** * Constructs a new NetCDF reader. * * @param spi The service provider. */ public NetcdfImageReader(final Spi spi) { super(spi); } /** * Returns the {@linkplain #input input} as an URL to a DODS dataset, or {@code null} if * none. If this method returns a non-null value, then the input should be open using a * {@link DODSNetcdfFile}. Otherwise it should be open using an ordinary {@link NetcdfDataset}. * <p> * Note that we returns the URL as a String, not as a {@link URL} object, in order to avoid * an "unknown protocol" exception. */ private String getInputDODS() { String protocol = null; if (input instanceof URL) { final URL url = (URL) input; protocol = url.getProtocol(); } else if (input instanceof URI) { final URI url = (URI) input; protocol = url.getScheme(); } else if (input instanceof String) { final String url = (String) input; final int s = url.indexOf(':'); if (s > 0) { protocol = url.substring(0, s); } } if (protocol == null || !protocol.equalsIgnoreCase(DODS_PROTOCOL)) { return null; } return input.toString(); } /** * Returns the names of the variables to be read. The first name is assigned to image * index 0, the second name to image index 1, <cite>etc.</cite>. In other words a call * to <code>{@linkplain #read(int) read}(imageIndex)</code> will read the variable names * {@code variables[imageIndex]} where {@code variables} is the value returned by this * method. * <p> * The sequence of variable to be read can be changed by a call to {@link #setVariables}. * * @return The name of the variables to be read. * @throws IOException if the NetCDF file can not be read. */ public String[] getVariables() throws IOException { if (variableNames == null) { ensureFileOpen(); } return variableNames.clone(); } /** * 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, * <cite>etc.</cite>. * <p> * If {@code variableNames} is set to {@code null} (which is the default), then the * variables will be inferred from the content of the NetCDF file. * * @param variableNames The set of variables to be assigned to image index. */ public void setVariables(final String[] variableNames) { this.variableNames = (variableNames != null) ? variableNames.clone() : null; } /** * Returns the number of images available from the current input source. * * @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 { ensureFileOpen(); // TODO: consider returning the actual number of images in the file. return variableNames.length; } /** * Returns the number of bands available for the specified image. * * @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 { prepareVariable(imageIndex); final int rank = variable.getRank(); final int bandDimension = rank - Z_DIMENSION; if (bandDimension >= 0 && bandDimension < rank) { return variable.getDimension(bandDimension).getLength(); } return super.getNumBands(imageIndex); } /** * Returns the number of dimension of the image at the given 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. * * @since 2.5 */ @Override public int getDimension(final int imageIndex) throws IOException { prepareVariable(imageIndex); return variable.getRank(); } /** * Convenience method returning the first (and only) sample converter in the specified * array, or a default converter if the specified array contains a null element. */ private static SampleConverter first(final SampleConverter[] converters) { SampleConverter converter = converters[0]; if (converter == null) { converter = SampleConverter.IDENTITY; } return converter; } /** * Returns statistics about the sample values in the specified image. This is for informative * purpose only and may be used when the {@linkplain #getImageMetadata metadata} do not provides * useful information about valid minimum and maximum values. Note that this method requires a * full scan of image data and may be slow. * * @param imageIndex The index of the image to analyze. * @return Statistics on the sample values in the given image. * @throws IOException if an I/O error occured while reading the sample values. */ public Statistics getStatistics(final int imageIndex) throws IOException { final double[] fillValues; final GeographicMetadata metadata = getGeographicMetadata(imageIndex); if (metadata != null && metadata.getNumBands() >= 1) { fillValues = metadata.getBand(0).getNoDataValues(); // TODO: What should we do with other bands? For now we assume that // every bands have the same fill values. } else { fillValues = null; } final Array array = variable.read(); final IndexIterator it = array.getIndexIterator(); final Statistics stats = new Statistics(); if (fillValues == null || fillValues.length == 0) { while (it.hasNext()) { stats.add(it.getDoubleNext()); } } else if (fillValues.length == 1) { final double fillValue = fillValues[0]; while (it.hasNext()) { final double value = it.getDoubleNext(); if (value != fillValue) { stats.add(value); } } } else { scan: while (it.hasNext()) { final double value = it.getDoubleNext(); if (fillValues != null) { for (int i=0; i<fillValues.length; i++) { if (fillValues[i] == value) { continue scan; } } } stats.add(value); } } return stats; } /** * Returns the image width. * * @throws IOException If an error occured while reading the NetCDF file. */ public int getWidth(final int imageIndex) throws IOException { prepareVariable(imageIndex); return variable.getDimension(variable.getRank() - X_DIMENSION).getLength(); } /** * Returns the image height. * * @throws IOException If an error occured while reading the NetCDF file. */ public int getHeight(final int imageIndex) throws IOException { prepareVariable(imageIndex); return variable.getDimension(variable.getRank() - Y_DIMENSION).getLength(); } /** * Ensures that metadata are loaded. */ private void ensureMetadataLoaded() throws IOException { if (!metadataLoaded) { CoordSysBuilder.addCoordinateSystems(dataset, this); metadataLoaded = true; } } /** * Returns the metadata associated with the input source as a whole. * * @throws IOException If an error occured while reading the NetCDF file. */ @Override public IIOMetadata getStreamMetadata() throws IOException { if (streamMetadata == null && !ignoreMetadata) { ensureFileOpen(); ensureMetadataLoaded(); streamMetadata = createMetadata(dataset); } return streamMetadata; } /** * Returns the metadata associated with the image at the specified index. * * @throws IOException If an error occured while reading the NetCDF file. */ @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { if (imageMetadata == null && !ignoreMetadata) { prepareVariable(imageIndex); if (variable instanceof VariableDS) { ensureMetadataLoaded(); imageMetadata = createMetadata((VariableDS) variable); } } return imageMetadata; } /** * Creates metadata for the specified NetCDF file. This method is invoked automatically * by {@link #getStreamMetadata} when first needed. The default implementation returns an * instance of {@link NetcdfMetadata}. Subclasses can override this method in order to create * a more specific set of metadata. * * @param file The NetCDF dataset. * @return The metadata for the given dataset. * @throws IOException If an error occured while reading the NetCDF file. */ protected IIOMetadata createMetadata(final NetcdfDataset file) throws IOException { return new NetcdfMetadata(this, file); } /** * Creates metadata for the specified NetCDF variable. This method is invoked automatically * by {@link #getImageMetadata} when first needed. The default implementation returns an * instance of {@link NetcdfMetadata}. Subclasses can override this method in order to create * a more specific set of metadata. * * @param variable The NetCDF variable. * @return The metadata for the given variable. * @throws IOException If an error occured while reading the NetCDF file. */ protected IIOMetadata createMetadata(final VariableDS variable) throws IOException { return new NetcdfMetadata(this, variable); } /** * Returns the data type which most closely represents the "raw" internal data of the image. * * @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 occured while reading the NetCDF file. */ @Override protected int getRawDataType(final int imageIndex) throws IOException { prepareVariable(imageIndex); return VariableMetadata.getRawDataType(variable); } /** * Returns the slice to select for the specified axis type. * * @return The axis type. * @return The indice as a value from 0 inclusive to {@link Dimension#getLength} exclusive. * @throws IOException If an error occured while reading the NetCDF file. */ private int getSliceIndice(final ImageReadParam param, final int dimension) throws IOException { if (param instanceof NetcdfReadParam) { final NetcdfReadParam p = (NetcdfReadParam) param; if (p.hasNonDefaultIndices()) { final AxisType type = getAxisType(dimension); if (type != null) { return p.getSliceIndice(type); } } } return NetcdfReadParam.DEFAULT_INDICE; } /** * Returns the axis type at the specified dimension. The {@link #prepareVariable} method * must 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 the rest of metadata. In many cases this method will not be invoked at all, * thus avoiding the need to load metadata. * * @param dimension The dimension. * @return The axis type, or {@code null} if unknown. * @throws IOException If an error occured while reading the NetCDF file. */ private AxisType getAxisType(final int dimension) throws IOException { if (variable instanceof VariableDS) { ensureMetadataLoaded(); final List sys = ((VariableDS) variable).getCoordinateSystems(); if (sys != null) { final int count = sys.size(); for (int i=0; i<count; i++) { final CoordinateSystem cs = (CoordinateSystem) sys.get(i); final List axes = cs.getCoordinateAxes(); if (axes != null && axes.size() > dimension) { final CoordinateAxis axis = (CoordinateAxis) axes.get(dimension); if (axis != null) { return axis.getAxisType(); } } } } } return null; } /** * Returns {@code true} if the specified variable is a dimension of an other variable. * Such dimensions will be excluded from the list returned by {@link #getVariables}. * * @param candidate The variable to test. * @param variables The list of variables. * @return {@code true} if the specified variable is a dimension of an other variable. */ private static boolean isAxis(final VariableIF candidate, final List variables) { final String name = candidate.getName(); final int size = variables.size(); for (int i=0; i<size; i++) { final VariableIF var = (VariableIF) variables.get(i); if (var != candidate) { Dimension dim; for (int d=0; (dim=var.getDimension(d)) != null; d++) { if (dim.getName().equals(name)) { return true; } } } } return false; } /** * Ensures that the NetCDF file is open, but do 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 dodsURL = getInputDODS(); if (dodsURL != null) { if (variableNames == null) { final int s = dodsURL.indexOf('?'); if (s >= 0) { variableNames = new String[] { dodsURL.substring(s + 1) }; } } dataset = new NetcdfDataset(new DODSNetcdfFile(dodsURL, this), false); } else { final File inputFile = getInputFile(); dataset = NetcdfDataset.openDataset(inputFile.getPath(), false, this); if (dataset == null) { throw new FileNotFoundException(Errors.format( ErrorKeys.FILE_DOES_NOT_EXIST_$1, inputFile)); } } 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 interresting variable is "temperature"), so we exclude it. */ final List variables = dataset.getVariables(); final String[] filtered = new String[variables.size()]; int count = 0; for (int i=0; i<filtered.length; i++) { final VariableIF candidate = (VariableIF) variables.get(i); /* * - Images require at least 2 dimensions. They may have more dimensions, * in which case a slice will be taken later. * * - 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 wan't to confuse them * with images. * * - 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. */ if (candidate.getRank()>=2 && VALID_TYPES.contains(candidate.getDataType()) && !isAxis(candidate, variables)) { filtered[count++] = candidate.getName(); } } variableNames = XArray.resize(filtered, count); } } } /** * Ensures that data are loaded in the NetCDF {@linkplain #variable}. If data are already * loaded, then this method do nothing. * <p> * This method is invoked automatically before any operation that require the NetCDF * variable, including (but not limited to): * <ul> * <li>{@link #getWidth}</li> * <li>{@link #getHeight}</li> * <li>{@link #getStatistics}</li> * <li>{@link #getImageMetadata}</li> * <li>{@link #getRawDataType}</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 occured while reading the NetCDF file. */ protected boolean prepareVariable(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (variable == null || variableIndex != imageIndex) { ensureFileOpen(); final String name = variableNames[imageIndex]; final Variable candidate = findVariable(name); final int rank = candidate.getRank(); if (rank < Math.max(X_DIMENSION, Y_DIMENSION)) { throw new IIOException(Errors.format(ErrorKeys.NOT_TWO_DIMENSIONAL_$1, rank)); } variable = candidate; variableIndex = imageIndex; imageMetadata = null; 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. * * @param name The name of the variable to search. * @return The variable for the given name. * @throws IIOException if no variable has been found for the given name. * @throws IOException If an error occured while reading the NetCDF file. */ 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. */ final List<Variable> variables = dataset.getVariables(); if (variables != null) { for (final Variable variable : variables) { if (variable!=null && name.equalsIgnoreCase(variable.getName())) { return variable; } } } throw new IIOException(Errors.format( ErrorKeys.VARIABLE_NOT_FOUND_IN_FILE_$2, name, dataset.getLocation())); } /** * Returns parameters initialized with default values appropriate for this format. * * @return Parameters which may be used to control the decoding process using a set * of default settings. */ @Override public ImageReadParam getDefaultReadParam() { return new NetcdfReadParam(this); } /** * Creates an image from the specified parameters. * * @throws IOException If an error occured while reading the NetCDF file. */ public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { clearAbortRequest(); prepareVariable(imageIndex); /* * Fetchs 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(); int bandDimension = rank - Z_DIMENSION; if (false && param instanceof NetcdfReadParam) { final NetcdfReadParam p = (NetcdfReadParam) param; if (p.isBandDimensionSet() && variable instanceof VariableEnhanced) { ensureMetadataLoaded(); // Build the CoordinateSystems bandDimension = p.getBandDimension((VariableEnhanced) variable); final int relative = rank - bandDimension; if (relative < 0 || relative == X_DIMENSION || relative == Y_DIMENSION) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_PARAMETER_$2, "bandDimension", bandDimension)); } } } /* * Gets the destination image of appropriate size. We create it now * since it is a convenient way to get the number of destination bands. */ final int width = variable.getDimension(rank - X_DIMENSION).getLength(); final int height = variable.getDimension(rank - Y_DIMENSION).getLength(); final SampleConverter[] converters = new SampleConverter[1]; final BufferedImage image = getDestination(imageIndex, param, width, height, converters); final WritableRaster raster = image.getRaster(); final SampleConverter converter = first(converters); /* * Checks the band setting. If the NetCDF file is at least 3D, the * data along the 'z' dimension are considered as different bands. */ final boolean hasBands = (bandDimension >= 0 && bandDimension < rank); final int numSrcBands = hasBands ? variable.getDimension(bandDimension).getLength() : 1; final int numDstBands = raster.getNumBands(); if (param != null) { // Do not test when 'param == null' since our default 'srcBands' // value is not the same than the one documented in Image I/O. checkReadParamBandSettings(param, numSrcBands, 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); flipVertically(param, height, srcRegion); 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: { first = srcRegion.x; length = srcRegion.width; stride = strideX; break; } case Y_DIMENSION: { first = srcRegion.y; length = srcRegion.height; stride = strideY; break; } default: { if (i == bandDimension) { first = NetcdfReadParam.DEFAULT_INDICE; } else { first = getSliceIndice(param, i); } 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 = Range.toList(ranges); /* * Reads the requested sub-region only. */ 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]; final Array array; try { if (hasBands) { ranges[bandDimension] = new Range(srcBand, srcBand, 1); // No need to update 'sections' since it wraps directly the 'ranges' array. } array = variable.read(sections); } catch (InvalidRangeException e) { throw netcdfFailure(e); } final IndexIterator it = array.getIndexIterator(); for (int y=ymax; --y>=ymin;) { for (int x=xmin; x<xmax; x++) { 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 occured 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; } /** * Wraps a generic exception into a {@link IIOException}. */ private IIOException netcdfFailure(final Exception e) throws IOException { return new IIOException(Errors.format(ErrorKeys.CANT_READ_$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. */ public boolean isCancel() { return abortRequested(); } /** * Invoked by the NetCDF library when an error occured during the read operation. * Users should not invoke this method directly. * * @param message An error message to report. */ public void setError(final String message) { lastError = message; } /** * Closes the NetCDF file. * * @throws IOException If an error occured while accessing the NetCDF file. */ @Override protected void close() throws IOException { metadataLoaded = false; streamMetadata = null; imageMetadata = null; lastError = null; variable = null; if (dataset != null) { dataset.close(); dataset = null; } super.close(); } /** * The service provider for {@link NetcdfImageReader}. * * @version $Id$ * @author Antoine Hnawia * @author Martin Desruisseaux */ public static class Spi extends FileImageReader.Spi { /** * List of legal names for NetCDF readers. */ private static final String[] NAMES = new String[] {"netcdf", "NetCDF"}; /** * The mime types for the default {@link NetcdfImageReader} configuration. */ private static final String[] MIME_TYPES = new String[] {"image/x-netcdf"}; /** * Default list of file's extensions. */ private static final String[] SUFFIXES = new String[] {"nc", "NC"}; /** * Constructs a default {@code NetcdfImageReader.Spi}. This constructor * provides the following defaults in addition to the defaults defined * in the super-class constructor: * * <ul> * <li>{@link #names} = {@code "NetCDF"}</li> * <li>{@link #MIMETypes} = {@code "image/x-netcdf"}</li> * <li>{@link #pluginClassName} = {@code "org.geotools.image.io.netcdf.NetcdfImageReader"}</li> * <li>{@link #vendorName} = {@code "Geotools"}</li> * <li>{@link #suffixes} = {{@code "nc"}, {@code "NC"}}</li> * </ul> * * For efficienty reasons, the above 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.geotools.image.io.netcdf.NetcdfImageReader"; vendorName = "Geotools"; version = "2.4"; } /** * Returns a description for this provider. * * @todo Localize */ public String getDescription(final Locale locale) { return "NetCDF image decoder"; } /** * Checks if the specified input seems to be a readeable NetCDF file. * This method is only for indication purpose. Current implementation * conservatively returns {@code false}. * * @throws IOException If an error occured while reading the NetCDF file. * @todo Implements a more advanced check. */ public boolean canDecodeInput(final Object source) throws IOException { return false; } /** * Constructs a NetCDF image reader. * * @throws IOException If an error occured while reading the NetCDF file. */ public ImageReader createReaderInstance(final Object extension) throws IOException { return new NetcdfImageReader(this); } } }