001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2006, GeoTools Project Managment Committee (PMC)
005: * (C) 2006, Geomatys
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.image.io.netcdf;
018:
019: import java.util.Set;
020: import java.util.List;
021: import java.util.Locale;
022: import java.util.HashSet;
023: import java.awt.Rectangle;
024: import java.awt.image.BufferedImage;
025: import java.awt.image.WritableRaster;
026: import java.awt.image.DataBuffer;
027: import java.net.URL;
028: import java.net.URI;
029: import java.io.File;
030: import java.io.IOException;
031: import java.io.FileNotFoundException;
032: import javax.imageio.IIOException;
033: import javax.imageio.ImageReader;
034: import javax.imageio.ImageReadParam;
035: import javax.imageio.metadata.IIOMetadata;
036:
037: import ucar.ma2.Array;
038: import ucar.ma2.Range;
039: import ucar.ma2.DataType;
040: import ucar.ma2.IndexIterator;
041: import ucar.ma2.InvalidRangeException;
042: import ucar.nc2.dataset.AxisType;
043: import ucar.nc2.dataset.CoordinateAxis;
044: import ucar.nc2.dataset.CoordinateSystem;
045: import ucar.nc2.dataset.CoordSysBuilder;
046: import ucar.nc2.dataset.NetcdfDataset;
047: import ucar.nc2.dataset.VariableDS;
048: import ucar.nc2.dataset.VariableEnhanced;
049: import ucar.nc2.dods.DODSNetcdfFile;
050: import ucar.nc2.util.CancelTask;
051: import ucar.nc2.Dimension;
052: import ucar.nc2.Variable;
053: import ucar.nc2.VariableIF;
054:
055: import org.geotools.image.io.FileImageReader;
056: import org.geotools.image.io.SampleConverter;
057: import org.geotools.image.io.metadata.GeographicMetadata;
058: import org.geotools.math.Statistics;
059: import org.geotools.resources.XArray;
060: import org.geotools.resources.i18n.Errors;
061: import org.geotools.resources.i18n.ErrorKeys;
062:
063: /**
064: * Base implementation for NetCDF image reader. Pixels are assumed organized according the COARDS
065: * convention (a precursor of <A HREF="http://www.cfconventions.org/">CF Metadata conventions</A>),
066: * i.e. in (<var>t</var>,<var>z</var>,<var>y</var>,<var>x</var>) order, where <var>x</var> varies
067: * faster. The image is created from the two last dimensions (<var>x</var>,<var>y</var>).
068: * Additional dimensions (if any) are handled as below in the default implementation:
069: * <p>
070: * <ul>
071: * <li>The third dimension (<var>z</var> in the above sequence) is assigned to bands. Users
072: * can change this behavior by invoking the {@link NetcdfReadParam#setBandDimensionTypes}
073: * method.</li>
074: * <li>Additional dimensions like <var>t</var> are ignored; only the first slice is selected.
075: * Users can change this behavior by invoking the {@link NetcdfReadParam#setSliceIndice}
076: * method.</li>
077: * </ul>
078: * <p>
079: * <b>Example:</b><br>
080: * Assuming that:
081: * <ul>
082: * <li>None of the above methods has been invoked</li>
083: * <li>Axis are (<var>t</var>,<var>z</var>,<var>y</var>,<var>x</var>)</li>
084: * </ul>
085: * Then the users can select the <var>z</var> value using {@link ImageReadParam#setSourceBands}.
086: * If no band is selected, then the default selection is the first band (0) only. Note that this
087: * is different than the usual Image I/O default, which is all bands.
088: * <p>
089: * <b>Connection to DODS servers</b>
090: * This image reader accepts {@link File} and {@link URL} inputs. In the later case, if and only
091: * if the URL uses the DODS protocol (as in "{@code dods://opendap.aviso.oceanobs.com/}"), then
092: * this image reader tries to connect to the DODS remote server. Otherwise the URL content is
093: * copied in a temporary file.
094: *
095: * @since 2.4
096: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio-netcdf/src/main/java/org/geotools/image/io/netcdf/NetcdfImageReader.java $
097: * @version $Id: NetcdfImageReader.java 27762 2007-11-05 21:11:12Z desruisseaux $
098: * @author Antoine Hnawia
099: * @author Martin Desruisseaux
100: */
101: public class NetcdfImageReader extends FileImageReader implements
102: CancelTask {
103: /**
104: * The URL protocol for connections to a DODS server. Any URL with this protocol will be
105: * open using {@link DODSNetcdfFile} instead of the ordinary {@link NetcdfDataset}. The
106: * later works only for local {@linkplain File files}, while the former connects to a
107: * remote server.
108: */
109: private static final String DODS_PROTOCOL = "dods";
110:
111: /**
112: * The dimension <strong>relative to the rank</strong> in {@link #variable} to use as image
113: * width. The actual dimension is {@code variable.getRank() - X_DIMENSION}. Is hard-coded
114: * because the loop in the {@code read} method expects this order.
115: */
116: private static final int X_DIMENSION = 1;
117:
118: /**
119: * The dimension <strong>relative to the rank</strong> in {@link #variable} to use as image
120: * height. The actual dimension is {@code variable.getRank() - Y_DIMENSION}. Is hard-coded
121: * because the loop in the {@code read} method expects this order.
122: */
123: private static final int Y_DIMENSION = 2;
124:
125: /**
126: * The default dimension <strong>relative to the rank</strong> in {@link #variable} to use
127: * as image bands. The actual dimension is {@code variable.getRank() - Z_DIMENSION}.
128: * <p>
129: * At the difference of {@link #X_DIMENSION} and {@link #Y_DIMENSION}, this dimension doesn't
130: * need to be hard-coded. User can invoke {@link NetcdfReadParam#setBandDimensionTypes} in
131: * order to provide a different value.
132: */
133: private static final int Z_DIMENSION = 3;
134:
135: /**
136: * The data type to accept in images. Used for automatic detection of which variables
137: * to assign to images.
138: */
139: private static final Set/*<DataType>*/VALID_TYPES = new HashSet(12);
140: static {
141: VALID_TYPES.add(DataType.BOOLEAN);
142: VALID_TYPES.add(DataType.BYTE);
143: VALID_TYPES.add(DataType.SHORT);
144: VALID_TYPES.add(DataType.INT);
145: VALID_TYPES.add(DataType.LONG);
146: VALID_TYPES.add(DataType.FLOAT);
147: VALID_TYPES.add(DataType.DOUBLE);
148: }
149:
150: /**
151: * The NetCDF dataset, or {@code null} if not yet open. The NetCDF file is open by
152: * {@link #ensureOpen} when first needed.
153: */
154: private NetcdfDataset dataset;
155:
156: /**
157: * The name of the {@linkplain Variable variables} to be read in a NetCDF file.
158: * The first name is assigned to image index 0, the second name to image index 1,
159: * <cite>etc.</cite>.
160: */
161: private String[] variableNames;
162:
163: /**
164: * The image index of the current {@linkplain #variable variable}.
165: */
166: private int variableIndex;
167:
168: /**
169: * The data from the NetCDF file. The value for this field is set by {@link #prepareVariable}
170: * when first needed. This is typically (but not necessarly) an instance of {@link VariableDS}.
171: */
172: protected Variable variable;
173:
174: /**
175: * The last error from the NetCDF library.
176: */
177: private String lastError;
178:
179: /**
180: * {@code true} if {@link CoordSysBuilder#addCoordinateSystems} has been invoked
181: * for current file.
182: */
183: private boolean metadataLoaded;
184:
185: /**
186: * The stream metadata. Will be created only when first needed.
187: */
188: private IIOMetadata streamMetadata;
189:
190: /**
191: * The current image metadata. Will be created only when first needed.
192: */
193: private IIOMetadata imageMetadata;
194:
195: /**
196: * Constructs a new NetCDF reader.
197: *
198: * @param spi The service provider.
199: */
200: public NetcdfImageReader(final Spi spi) {
201: super (spi);
202: }
203:
204: /**
205: * Returns the {@linkplain #input input} as an URL to a DODS dataset, or {@code null} if
206: * none. If this method returns a non-null value, then the input should be open using a
207: * {@link DODSNetcdfFile}. Otherwise it should be open using an ordinary {@link NetcdfDataset}.
208: * <p>
209: * Note that we returns the URL as a String, not as a {@link URL} object, in order to avoid
210: * an "unknown protocol" exception.
211: */
212: private String getInputDODS() {
213: String protocol = null;
214: if (input instanceof URL) {
215: final URL url = (URL) input;
216: protocol = url.getProtocol();
217: } else if (input instanceof URI) {
218: final URI url = (URI) input;
219: protocol = url.getScheme();
220: } else if (input instanceof String) {
221: final String url = (String) input;
222: final int s = url.indexOf(':');
223: if (s > 0) {
224: protocol = url.substring(0, s);
225: }
226: }
227: if (protocol == null
228: || !protocol.equalsIgnoreCase(DODS_PROTOCOL)) {
229: return null;
230: }
231: return input.toString();
232: }
233:
234: /**
235: * Returns the names of the variables to be read. The first name is assigned to image
236: * index 0, the second name to image index 1, <cite>etc.</cite>. In other words a call
237: * to <code>{@linkplain #read(int) read}(imageIndex)</code> will read the variable names
238: * {@code variables[imageIndex]} where {@code variables} is the value returned by this
239: * method.
240: * <p>
241: * The sequence of variable to be read can be changed by a call to {@link #setVariables}.
242: *
243: * @return The name of the variables to be read.
244: * @throws IOException if the NetCDF file can not be read.
245: */
246: public String[] getVariables() throws IOException {
247: if (variableNames == null) {
248: ensureFileOpen();
249: }
250: return (String[]) variableNames.clone();
251: }
252:
253: /**
254: * Sets the name of the {@linkplain Variable variables} to be read in a NetCDF file.
255: * The first name is assigned to image index 0, the second name to image index 1,
256: * <cite>etc.</cite>.
257: * <p>
258: * If {@code variableNames} is set to {@code null} (which is the default), then the
259: * variables will be inferred from the content of the NetCDF file.
260: */
261: public void setVariables(final String[] variableNames) {
262: this .variableNames = (variableNames != null) ? (String[]) variableNames
263: .clone()
264: : null;
265: }
266:
267: /**
268: * Returns the number of images available from the current input source.
269: *
270: * @throws IllegalStateException if the input source has not been set.
271: * @throws IOException if an error occurs reading the information from the input source.
272: */
273: //@Override
274: public int getNumImages(final boolean allowSearch)
275: throws IllegalStateException, IOException {
276: ensureFileOpen();
277: // TODO: consider returning the actual number of images in the file.
278: return variableNames.length;
279: }
280:
281: /**
282: * Convenience method returning the first (and only) sample converter in the specified
283: * array, or a default converter if the specified array contains a null element.
284: */
285: private static SampleConverter first(
286: final SampleConverter[] converters) {
287: SampleConverter converter = converters[0];
288: if (converter == null) {
289: converter = SampleConverter.IDENTITY;
290: }
291: return converter;
292: }
293:
294: /**
295: * Returns statistics about the sample values in the specified image. This is for informative
296: * purpose only and may be used when the {@linkplain #getImageMetadata metadata} do not provides
297: * useful information about valid minimum and maximum values. Note that this method requires a
298: * full scan of image data and may be slow.
299: *
300: * @param imageIndex The index of the image to analyze.
301: * @return Statistics on the sample values in the given image.
302: * @throws IOException if an I/O error occured while reading the sample values.
303: */
304: public Statistics getStatistics(final int imageIndex)
305: throws IOException {
306: final double[] fillValues;
307: final GeographicMetadata metadata = getGeographicMetadata(imageIndex);
308: if (metadata != null && metadata.getNumBands() >= 1) {
309: fillValues = metadata.getBand(0).getNoDataValues();
310: // TODO: What should we do with other bands? For now we assume that
311: // every bands have the same fill values.
312: } else {
313: fillValues = null;
314: }
315: final Array array = variable.read();
316: final IndexIterator it = array.getIndexIterator();
317: final Statistics stats = new Statistics();
318: if (fillValues == null || fillValues.length == 0) {
319: while (it.hasNext()) {
320: stats.add(it.getDoubleNext());
321: }
322: } else if (fillValues.length == 1) {
323: final double fillValue = fillValues[0];
324: while (it.hasNext()) {
325: final double value = it.getDoubleNext();
326: if (value != fillValue) {
327: stats.add(value);
328: }
329: }
330: } else {
331: scan: while (it.hasNext()) {
332: final double value = it.getDoubleNext();
333: if (fillValues != null) {
334: for (int i = 0; i < fillValues.length; i++) {
335: if (fillValues[i] == value) {
336: continue scan;
337: }
338: }
339: }
340: stats.add(value);
341: }
342: }
343: return stats;
344: }
345:
346: /**
347: * Returns the image width.
348: */
349: public int getWidth(final int imageIndex) throws IOException {
350: prepareVariable(imageIndex);
351: return variable.getDimension(variable.getRank() - X_DIMENSION)
352: .getLength();
353: }
354:
355: /**
356: * Returns the image height.
357: */
358: public int getHeight(final int imageIndex) throws IOException {
359: prepareVariable(imageIndex);
360: return variable.getDimension(variable.getRank() - Y_DIMENSION)
361: .getLength();
362: }
363:
364: /**
365: * Ensures that metadata are loaded.
366: */
367: private void ensureMetadataLoaded() throws IOException {
368: if (!metadataLoaded) {
369: CoordSysBuilder.addCoordinateSystems(dataset, this );
370: metadataLoaded = true;
371: }
372: }
373:
374: /**
375: * Returns the metadata associated with the input source as a whole.
376: */
377: //@Override
378: public IIOMetadata getStreamMetadata() throws IOException {
379: if (streamMetadata == null && !ignoreMetadata) {
380: ensureFileOpen();
381: ensureMetadataLoaded();
382: streamMetadata = createMetadata(dataset);
383: }
384: return streamMetadata;
385: }
386:
387: /**
388: * Returns the metadata associated with the image at the specified index.
389: */
390: //@Override
391: public IIOMetadata getImageMetadata(final int imageIndex)
392: throws IOException {
393: if (imageMetadata == null && !ignoreMetadata) {
394: prepareVariable(imageIndex);
395: if (variable instanceof VariableDS) {
396: ensureMetadataLoaded();
397: imageMetadata = createMetadata((VariableDS) variable);
398: }
399: }
400: return imageMetadata;
401: }
402:
403: /**
404: * Creates metadata for the specified NetCDF file. This method is invoked automatically
405: * by {@link #getStreamMetadata} when first needed. The default implementation returns an
406: * instance of {@link NetcdfMetadata}. Subclasses can override this method in order to create
407: * a more specific set of metadata.
408: */
409: protected IIOMetadata createMetadata(final NetcdfDataset file)
410: throws IOException {
411: return new NetcdfMetadata(this , file);
412: }
413:
414: /**
415: * Creates metadata for the specified NetCDF variable. This method is invoked automatically
416: * by {@link #getImageMetadata} when first needed. The default implementation returns an
417: * instance of {@link NetcdfMetadata}. Subclasses can override this method in order to create
418: * a more specific set of metadata.
419: */
420: protected IIOMetadata createMetadata(final VariableDS variable)
421: throws IOException {
422: return new NetcdfMetadata(this , variable);
423: }
424:
425: /**
426: * Returns the data type which most closely represents the "raw" internal data of the image.
427: *
428: * @param imageIndex The index of the image to be queried.
429: * @return The data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
430: * @throws IOException If an error occurs reading the format information from the input source.
431: */
432: //@Override
433: protected int getRawDataType(final int imageIndex)
434: throws IOException {
435: prepareVariable(imageIndex);
436: return VariableMetadata.getRawDataType(variable);
437: }
438:
439: /**
440: * Returns the slice to select for the specified axis type.
441: *
442: * @return The axis type.
443: * @return The indice as a value from 0 inclusive to {@link Dimension#getLength} exclusive.
444: * @throws IOException If an error occured while reading the data.
445: */
446: private int getSliceIndice(final ImageReadParam param,
447: final int dimension) throws IOException {
448: if (param instanceof NetcdfReadParam) {
449: final NetcdfReadParam p = (NetcdfReadParam) param;
450: if (p.hasNonDefaultIndices()) {
451: final AxisType type = getAxisType(dimension);
452: if (type != null) {
453: return p.getSliceIndice(type);
454: }
455: }
456: }
457: return NetcdfReadParam.DEFAULT_INDICE;
458: }
459:
460: /**
461: * Returns the axis type at the specified dimension. The {@link #prepareVariable} method
462: * must be invoked prior this method (this is not verified).
463: * <p>
464: * This method is not public because it duplicates (in a different form) the informations
465: * already provided in image metadata. We use it when we want only this specific information
466: * without the the rest of metadata. In many cases this method will not be invoked at all,
467: * thus avoiding the need to load metadata.
468: *
469: * @param dimension The dimension.
470: * @return The axis type, or {@code null} if unknown.
471: * @throws IOException If an error occured while loading the axis informations.
472: */
473: private AxisType getAxisType(final int dimension)
474: throws IOException {
475: if (variable instanceof VariableDS) {
476: ensureMetadataLoaded();
477: final List sys = ((VariableDS) variable)
478: .getCoordinateSystems();
479: if (sys != null) {
480: final int count = sys.size();
481: for (int i = 0; i < count; i++) {
482: final CoordinateSystem cs = (CoordinateSystem) sys
483: .get(i);
484: final List axes = cs.getCoordinateAxes();
485: if (axes != null && axes.size() > dimension) {
486: final CoordinateAxis axis = (CoordinateAxis) axes
487: .get(dimension);
488: if (axis != null) {
489: return axis.getAxisType();
490: }
491: }
492: }
493: }
494: }
495: return null;
496: }
497:
498: /**
499: * Returns {@code true} if the specified variable is a dimension of an other variable.
500: * Such dimensions will be excluded from the list returned by {@link #getVariables}.
501: *
502: * @param candidate The variable to test.
503: * @param variables The list of variables.
504: * @return {@code true} if the specified variable is a dimension of an other variable.
505: */
506: private static boolean isAxis(final VariableIF candidate,
507: final List variables) {
508: final String name = candidate.getName();
509: final int size = variables.size();
510: for (int i = 0; i < size; i++) {
511: final VariableIF var = (VariableIF) variables.get(i);
512: if (var != candidate) {
513: Dimension dim;
514: for (int d = 0; (dim = var.getDimension(d)) != null; d++) {
515: if (dim.getName().equals(name)) {
516: return true;
517: }
518: }
519: }
520: }
521: return false;
522: }
523:
524: /**
525: * Ensures that the NetCDF file is open, but do not load any variable yet.
526: * The {@linkplain #variable} will be read by {@link #prepareVariable} only.
527: */
528: private void ensureFileOpen() throws IOException {
529: if (dataset == null) {
530: /*
531: * Clears the 'abort' flag here (instead of in 'read' method only) because
532: * we pass this ImageReader instance to the NetCDF DataSet as a CancelTask.
533: */
534: lastError = null;
535: clearAbortRequest();
536: final String dodsURL = getInputDODS();
537: if (dodsURL != null) {
538: if (variableNames == null) {
539: final int s = dodsURL.indexOf('?');
540: if (s >= 0) {
541: variableNames = new String[] { dodsURL
542: .substring(s + 1) };
543: }
544: }
545: dataset = new NetcdfDataset(new DODSNetcdfFile(dodsURL,
546: this ), false);
547: } else {
548: final File inputFile = getInputFile();
549: dataset = NetcdfDataset.openDataset(
550: inputFile.getPath(), false, this );
551: if (dataset == null) {
552: throw new FileNotFoundException(Errors
553: .format(ErrorKeys.FILE_DOES_NOT_EXIST_$1,
554: inputFile));
555: }
556: }
557: if (variableNames == null) {
558: /*
559: * Gets a list of every variables found in the NetcdfDataset and copies the names
560: * in a filtered list which exclude every variable that are dimension of an other
561: * variable. For example "longitude" may be a variable found in the NetcdfDataset,
562: * but is declared only because it is needed as a dimension for the "temperature"
563: * variable. The "longitude" variable is usually not of direct interest to the user
564: * (the interresting variable is "temperature"), so we exclude it.
565: */
566: final List variables = dataset.getVariables();
567: final String[] filtered = new String[variables.size()];
568: int count = 0;
569: for (int i = 0; i < filtered.length; i++) {
570: final VariableIF candidate = (VariableIF) variables
571: .get(i);
572: /*
573: * - Images require at least 2 dimensions. They may have more dimensions,
574: * in which case a slice will be taken later.
575: *
576: * - Excludes characters, strings and structures, which can not be easily
577: * mapped to an image type. In addition, 2-dimensional character arrays
578: * are often used for annotations and we don't wan't to confuse them
579: * with images.
580: *
581: * - Excludes axis. They are often already excluded by the first condition
582: * because axis are usually 1-dimensional, but some are 2-dimensional,
583: * e.g. a localization grid.
584: */
585: if (candidate.getRank() >= 2
586: && VALID_TYPES.contains(candidate
587: .getDataType())
588: && !isAxis(candidate, variables)) {
589: filtered[count++] = candidate.getName();
590: }
591: }
592: variableNames = (String[]) XArray.resize(filtered,
593: count);
594: }
595: }
596: }
597:
598: /**
599: * Ensures that data are loaded in the NetCDF {@linkplain #variable}. If data are already
600: * loaded, then this method do nothing.
601: * <p>
602: * This method is invoked automatically before any operation that require the NetCDF
603: * variable, including (but not limited to):
604: * <ul>
605: * <li>{@link #getWidth}</li>
606: * <li>{@link #getHeight}</li>
607: * <li>{@link #getStatistics}</li>
608: * <li>{@link #getImageMetadata}</li>
609: * <li>{@link #getRawDataType}</li>
610: * <li>{@link #read(int,ImageReadParam)}</li>
611: * </ul>
612: *
613: * @param imageIndex The image index.
614: * @return {@code true} if the {@linkplain #variable} changed as a result of this call,
615: * or {@code false} if the current value is already appropriate.
616: * @throws IndexOutOfBoundsException if the specified index is outside the expected range.
617: * @throws IllegalStateException If {@link #input} is not set.
618: * @throws IOException If the operation failed because of an I/O error.
619: */
620: protected boolean prepareVariable(final int imageIndex)
621: throws IOException {
622: checkImageIndex(imageIndex);
623: if (variable == null || variableIndex != imageIndex) {
624: ensureFileOpen();
625: final String name = variableNames[imageIndex];
626: final Variable candidate = findVariable(name);
627: final int rank = candidate.getRank();
628: if (rank < Math.max(X_DIMENSION, Y_DIMENSION)) {
629: throw new IIOException(Errors.format(
630: ErrorKeys.NOT_TWO_DIMENSIONAL_$1, new Integer(
631: rank)));
632: }
633: variable = candidate;
634: variableIndex = imageIndex;
635: imageMetadata = null;
636: return true;
637: }
638: return false;
639: }
640:
641: /**
642: * Returns the variable of the given name. This method is similar to
643: * {@link NetcdfDataset#findVariable(String)} except that the search
644: * is case-insensitive and an exception is thrown if no variable has
645: * been found for the given name.
646: *
647: * @param name The name of the variable to search.
648: * @return The variable for the given name.
649: * @throws IIOException if no variable has been found for the given name.
650: * @throws IOException If the operation failed because of an I/O error.
651: */
652: protected Variable findVariable(final String name)
653: throws IOException {
654: ensureFileOpen();
655: /*
656: * First tries a case-sensitive search. Case matter since the same letter in different
657: * case may represent different variables. For example "t" and "T" are typically "time"
658: * and "temperature" respectively.
659: */
660: Variable candidate = dataset.findVariable(name);
661: if (candidate != null) {
662: return candidate;
663: }
664: /*
665: * We tried a case-sensitive search without success. Now tries a case-insensitive search
666: * before to report a failure.
667: */
668: final List/*<Variable>*/variables = dataset.getVariables();
669: if (variables != null) {
670: for (final java.util.Iterator/*<Variable>*/it = variables
671: .iterator(); it.hasNext();) {
672: candidate = (Variable) it.next();
673: if (candidate != null
674: && name.equalsIgnoreCase(candidate.getName())) {
675: return candidate;
676: }
677: }
678: }
679: throw new IIOException(Errors.format(
680: ErrorKeys.VARIABLE_NOT_FOUND_IN_FILE_$2, name, dataset
681: .getLocation()));
682: }
683:
684: /**
685: * Returns parameters initialized with default values appropriate for this format.
686: *
687: * @return Parameters which may be used to control the decoding process using a set
688: * of default settings.
689: */
690: //@Override
691: public ImageReadParam getDefaultReadParam() {
692: return new NetcdfReadParam(this );
693: }
694:
695: /**
696: * Creates an image from the specified parameters.
697: */
698: public BufferedImage read(final int imageIndex,
699: final ImageReadParam param) throws IOException {
700: clearAbortRequest();
701: prepareVariable(imageIndex);
702: /*
703: * Fetchs the parameters that are not already processed by utility
704: * methods like 'getDestination' or 'computeRegions' (invoked below).
705: */
706: final int strideX, strideY;
707: final int[] srcBands, dstBands;
708: if (param != null) {
709: strideX = param.getSourceXSubsampling();
710: strideY = param.getSourceYSubsampling();
711: srcBands = param.getSourceBands();
712: dstBands = param.getDestinationBands();
713: } else {
714: strideX = 1;
715: strideY = 1;
716: srcBands = null;
717: dstBands = null;
718: }
719: final int rank = variable.getRank();
720: int bandDimension = rank - Z_DIMENSION;
721: if (param instanceof NetcdfReadParam) {
722: final NetcdfReadParam p = (NetcdfReadParam) param;
723: if (variable instanceof VariableEnhanced) {
724: ensureMetadataLoaded(); // Build the CoordinateSystems
725: bandDimension = p
726: .getBandDimension((VariableEnhanced) variable);
727: final int relative = rank - bandDimension;
728: if (relative < 0 || relative == X_DIMENSION
729: || relative == Y_DIMENSION) {
730: throw new IllegalArgumentException(Errors
731: .format(ErrorKeys.BAD_PARAMETER_$2,
732: "bandDimension", new Integer(
733: bandDimension)));
734: }
735: }
736: }
737: /*
738: * Gets the destination image of appropriate size. We create it now
739: * since it is a convenient way to get the number of destination bands.
740: */
741: final int width = variable.getDimension(rank - X_DIMENSION)
742: .getLength();
743: final int height = variable.getDimension(rank - Y_DIMENSION)
744: .getLength();
745: final SampleConverter[] converters = new SampleConverter[1];
746: final BufferedImage image = getDestination(imageIndex, param,
747: width, height, converters);
748: final WritableRaster raster = image.getRaster();
749: final SampleConverter converter = first(converters);
750: /*
751: * Checks the band setting. If the NetCDF file is at least 3D, the
752: * data along the 'z' dimension are considered as different bands.
753: */
754: final boolean hasBands = (bandDimension >= 0 && bandDimension < rank);
755: final int numSrcBands = hasBands ? variable.getDimension(
756: bandDimension).getLength() : 1;
757: final int numDstBands = raster.getNumBands();
758: if (param != null) {
759: // Do not test when 'param == null' since our default 'srcBands'
760: // value is not the same than the one documented in Image I/O.
761: checkReadParamBandSettings(param, numSrcBands, numDstBands);
762: }
763: /*
764: * Computes the source region (in the NetCDF file) and the destination region
765: * (in the buffered image). Copies those informations into UCAR Range structure.
766: */
767: final Rectangle srcRegion = new Rectangle();
768: final Rectangle destRegion = new Rectangle();
769: computeRegions(param, width, height, image, srcRegion,
770: destRegion);
771: flipVertically(param, height, srcRegion);
772: final Range[] ranges = new Range[rank];
773: for (int i = 0; i < ranges.length; i++) {
774: final int first, length, stride;
775: switch (rank - i) {
776: case X_DIMENSION: {
777: first = srcRegion.x;
778: length = srcRegion.width;
779: stride = strideX;
780: break;
781: }
782: case Y_DIMENSION: {
783: first = srcRegion.y;
784: length = srcRegion.height;
785: stride = strideY;
786: break;
787: }
788: default: {
789: if (i == bandDimension) {
790: first = NetcdfReadParam.DEFAULT_INDICE;
791: } else {
792: first = getSliceIndice(param, i);
793: }
794: length = 1;
795: stride = 1;
796: break;
797: }
798: }
799: try {
800: ranges[i] = new Range(first, first + length - 1, stride);
801: } catch (InvalidRangeException e) {
802: throw netcdfFailure(e);
803: }
804: }
805: final List sections = Range.toList(ranges);
806: /*
807: * Reads the requested sub-region only.
808: */
809: processImageStarted(imageIndex);
810: final float toPercent = 100f / numDstBands;
811: final int type = raster.getSampleModel().getDataType();
812: final int xmin = destRegion.x;
813: final int ymin = destRegion.y;
814: final int xmax = destRegion.width + xmin;
815: final int ymax = destRegion.height + ymin;
816: for (int zi = 0; zi < numDstBands; zi++) {
817: final int srcBand = (srcBands == null) ? zi : srcBands[zi];
818: final int dstBand = (dstBands == null) ? zi : dstBands[zi];
819: final Array array;
820: try {
821: if (hasBands) {
822: ranges[bandDimension] = new Range(srcBand, srcBand,
823: 1);
824: // No need to update 'sections' since it wraps directly the 'ranges' array.
825: }
826: array = variable.read(sections);
827: } catch (InvalidRangeException e) {
828: throw netcdfFailure(e);
829: }
830: final IndexIterator it = array.getIndexIterator();
831: for (int y = ymax; --y >= ymin;) {
832: for (int x = xmin; x < xmax; x++) {
833: switch (type) {
834: case DataBuffer.TYPE_DOUBLE: {
835: raster.setSample(x, y, dstBand, converter
836: .convert(it.getDoubleNext()));
837: break;
838: }
839: case DataBuffer.TYPE_FLOAT: {
840: raster.setSample(x, y, dstBand, converter
841: .convert(it.getFloatNext()));
842: break;
843: }
844: default: {
845: raster.setSample(x, y, dstBand, converter
846: .convert(it.getIntNext()));
847: break;
848: }
849: }
850: }
851: }
852: /*
853: * Checks for abort requests after reading. It would be a waste of a potentially
854: * good image (maybe the abort request occured after we just finished the reading)
855: * if we didn't implemented the 'isCancel()' method. But because of the later, which
856: * is checked by the NetCDF library, we can't assume that the image is complete.
857: */
858: if (abortRequested()) {
859: processReadAborted();
860: return image;
861: }
862: /*
863: * Reports progress here, not in the deeper loop, because the costly part is the
864: * call to 'variable.read(...)' which can't report progress. The loop that copy
865: * pixel values is fast, so reporting progress there would be pointless.
866: */
867: processImageProgress(zi * toPercent);
868: }
869: if (lastError != null) {
870: throw new IIOException(lastError);
871: }
872: processImageComplete();
873: return image;
874: }
875:
876: /**
877: * Wraps a generic exception into a {@link IIOException}.
878: */
879: private IIOException netcdfFailure(final Exception e)
880: throws IOException {
881: return new IIOException(Errors.format(ErrorKeys.CANT_READ_$1,
882: dataset.getLocation()), e);
883: }
884:
885: /**
886: * Invoked by the NetCDF library during read operation in order to check if the task has
887: * been canceled. Users should not invoke this method directly.
888: */
889: public boolean isCancel() {
890: return abortRequested();
891: }
892:
893: /**
894: * Invoked by the NetCDF library when an error occured during the read operation.
895: * Users should not invoke this method directly.
896: */
897: public void setError(final String message) {
898: lastError = message;
899: }
900:
901: /**
902: * Closes the NetCDF file.
903: */
904: //@Override
905: protected void close() throws IOException {
906: metadataLoaded = false;
907: streamMetadata = null;
908: imageMetadata = null;
909: lastError = null;
910: variable = null;
911: if (dataset != null) {
912: dataset.close();
913: dataset = null;
914: }
915: super .close();
916: }
917:
918: /**
919: * The service provider for {@link NetcdfImageReader}.
920: *
921: * @version $Id: NetcdfImageReader.java 27762 2007-11-05 21:11:12Z desruisseaux $
922: * @author Antoine Hnawia
923: * @author Martin Desruisseaux
924: */
925: public static class Spi extends FileImageReader.Spi {
926: /**
927: * List of legal names for NetCDF readers.
928: */
929: private static final String[] NAMES = new String[] { "netcdf",
930: "NetCDF" };
931:
932: /**
933: * The mime types for the default {@link NetcdfImageReader} configuration.
934: */
935: private static final String[] MIME_TYPES = new String[] { "image/x-netcdf" };
936:
937: /**
938: * Default list of file's extensions.
939: */
940: private static final String[] SUFFIXES = new String[] { "nc",
941: "NC" };
942:
943: /**
944: * Constructs a default {@code NetcdfImageReader.Spi}. This constructor
945: * provides the following defaults in addition to the defaults defined
946: * in the super-class constructor:
947: *
948: * <ul>
949: * <li>{@link #names} = {@code "NetCDF"}</li>
950: * <li>{@link #MIMETypes} = {@code "image/x-netcdf"}</li>
951: * <li>{@link #pluginClassName} = {@code "org.geotools.image.io.netcdf.NetcdfImageReader"}</li>
952: * <li>{@link #vendorName} = {@code "Geotools"}</li>
953: * <li>{@link #suffixes} = {{@code "nc"}, {@code "NC"}}</li>
954: * </ul>
955: *
956: * For efficienty reasons, the above fields are initialized to shared arrays. Subclasses
957: * can assign new arrays, but should not modify the default array content.
958: */
959: public Spi() {
960: names = NAMES;
961: MIMETypes = MIME_TYPES;
962: suffixes = SUFFIXES;
963: pluginClassName = "org.geotools.image.io.netcdf.NetcdfImageReader";
964: vendorName = "Geotools";
965: version = "2.4";
966: }
967:
968: /**
969: * Returns a description for this provider.
970: *
971: * @todo Localize
972: */
973: public String getDescription(final Locale locale) {
974: return "NetCDF image decoder";
975: }
976:
977: /**
978: * Checks if the specified input seems to be a readeable NetCDF file.
979: * This method is only for indication purpose. Current implementation
980: * conservatively returns {@code false}.
981: *
982: * @todo Implements a more advanced check.
983: */
984: public boolean canDecodeInput(final Object source)
985: throws IOException {
986: return false;
987: }
988:
989: /**
990: * Constructs a NetCDF image reader.
991: */
992: public ImageReader createReaderInstance(final Object extension)
993: throws IOException {
994: return new NetcdfImageReader(this);
995: }
996: }
997: }
|