001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2005-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
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; either
010: * version 2.1 of the License, or (at your option) any later version.
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.coverage.io;
018:
019: // J2SE dependencies
020: import java.awt.Dimension;
021: import java.awt.image.RenderedImage;
022: import java.io.File;
023: import java.io.IOException;
024: import java.io.PrintWriter;
025: import java.io.StringWriter;
026: import java.net.URL;
027: import java.util.Arrays;
028: import java.util.Iterator;
029: import java.util.Locale;
030: import java.util.MissingResourceException;
031: import java.util.logging.Level;
032: import java.util.logging.Logger;
033: import javax.imageio.ImageIO;
034: import javax.imageio.ImageReadParam;
035: import javax.imageio.ImageReader;
036: import javax.imageio.stream.ImageInputStream;
037:
038: // OpenGIS dependencoes
039: import org.opengis.coverage.grid.GridRange;
040: import org.opengis.coverage.grid.GridCoverage;
041: import org.opengis.geometry.Envelope;
042: import org.opengis.referencing.operation.MathTransform;
043: import org.opengis.referencing.crs.CoordinateReferenceSystem;
044:
045: // Geotools dependencies
046: import org.geotools.factory.Hints;
047: import org.geotools.coverage.FactoryFinder;
048: import org.geotools.coverage.GridSampleDimension;
049: import org.geotools.coverage.grid.GeneralGridRange;
050: import org.geotools.coverage.grid.GridCoverageFactory;
051: import org.geotools.image.io.RawBinaryImageReadParam;
052: import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
053: import org.geotools.io.LineWriter;
054: import org.geotools.io.TableWriter;
055: import org.geotools.util.logging.Logging;
056: import org.geotools.resources.i18n.Errors;
057: import org.geotools.resources.i18n.ErrorKeys;
058: import org.geotools.resources.i18n.Vocabulary;
059: import org.geotools.resources.i18n.VocabularyKeys;
060:
061: /**
062: * Base class for reading {@link GridCoverage} objects. Reading is a two steps process:
063: * The input file must be set first, then the actual reading is performed with the
064: * {@link #getGridCoverage}. Example:
065: *
066: * <blockquote><pre>
067: * AbstractGridCoverageReader reader = ...
068: * reader.{@linkplain #setInput setInput}(new File("MyCoverage.dat"), true);
069: * GridCoverage coverage = reader.{@linkplain #getGridCoverage getGridCoverage}(0);
070: * </pre></blockquote>
071: *
072: * Subclasses needs to implements at least the following methods:
073: *
074: * <ul>
075: * <li>{@link #getCoordinateReferenceSystem}</li>
076: * <li>{@link #getEnvelope}</li>
077: * </ul>
078: *
079: * The default implementation should be able to create acceptable grid
080: * coverage using informations provided by the two above-mentioned methods.
081: * However, other methods may be overriden too in order to get finner control
082: * on the result.
083: *
084: * @since 2.2
085: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/coverage/io/AbstractGridCoverageReader.java $
086: * @version $Id: AbstractGridCoverageReader.java 27862 2007-11-12 19:51:19Z desruisseaux $
087: * @author Martin Desruisseaux
088: */
089: public abstract class AbstractGridCoverageReader implements
090: GridCoverageReader {
091: /**
092: * The logger for the {@link #getGridCoverage} method.
093: */
094: static Logger LOGGER = Logging
095: .getLogger("org.geotools.coverage.io");
096:
097: /**
098: * The format name (e.g. "PNG" or "GeoTIFF"). This format name should
099: * be known to {@link ImageIO#getImageReadersByFormatName(String)},
100: * unless {@link #getImageReaders} is overriden.
101: */
102: private final String formatName;
103:
104: /**
105: * The {@link ImageReader} to use for decoding {@link RenderedImage}s. This reader is
106: * initially {@code null} and lazily created the first time {@link #setInput} is invoked.
107: * Once created, it is reused as much as possible. Invoking {@link #reset} dispose the
108: * reader and set it back to {@code null}.
109: */
110: protected ImageReader reader;
111:
112: /**
113: * The object to use for creating {@linkplain MathTransform math transform} from the
114: * {@linkplain #getGridRange grid range} and the {@linkplain #getEnvelope envelope}.
115: * Subclasses can change its configuration after {@code AbstractGridCoverageReader}
116: * construction in order to change its behavior regarding axis reversal or axis
117: * swapping for example.
118: *
119: * @see #getMathTransform
120: *
121: * @since 2.4
122: */
123: protected final GridToEnvelopeMapper gridToEnvelope = new GridToEnvelopeMapper();
124:
125: /**
126: * The grid coverage factory to use.
127: */
128: private final GridCoverageFactory factory;
129:
130: /**
131: * The input {@link File} or {@link URL}, or {@code null} if input is not set.
132: */
133: private Object input;
134:
135: /**
136: * The input stream, or {@code null} if input is not set or if {@link #reader}
137: * accepted directly {@link #input}.
138: */
139: private ImageInputStream stream;
140:
141: /**
142: * The locale to use for formatting messages, or {@code null} for a default locale.
143: */
144: private Locale locale;
145:
146: /**
147: * Constructs a {@code AbstractGridCoverageReader}. The {@linkplain ImageReader image reader}
148: * will be determined from the file extension, if any.
149: *
150: * @param hints The factory hints to use.
151: *
152: * @since 2.4
153: */
154: public AbstractGridCoverageReader(final Hints hints) {
155: this (hints, null);
156: }
157:
158: /**
159: * Constructs a {@code AbstractGridCoverageReader} for the specified format name. This
160: * format name should be known to {@link ImageIO#getImageReadersByFormatName(String)}.
161: *
162: * @param hints The factory hints to use.
163: * @param formatName The format name of the {@linkplain ImageReader image reader} to use,
164: * or {@code null} for relying on file extension instead.
165: *
166: * @since 2.4
167: */
168: public AbstractGridCoverageReader(final Hints hints,
169: final String formatName) {
170: this .factory = FactoryFinder.getGridCoverageFactory(hints);
171: this .formatName = formatName;
172: }
173:
174: /**
175: * Restores the {@code AbstractGridCoverageReader} to its initial state.
176: *
177: * @throws IOException if an error occurs while disposing resources.
178: */
179: public synchronized void reset() throws IOException {
180: clear();
181: locale = null;
182: if (reader != null) {
183: reader.reset();
184: reader.dispose();
185: reader = null;
186: }
187: }
188:
189: /**
190: * Clears this {@code AbstractGridCoverageReader}. This method is similar to {@link #reset},
191: * except that it doesn't destroy the current {@link ImageReader} and keeps the locale setting.
192: *
193: * @throws IOException if an error occurs while disposing resources.
194: */
195: private void clear() throws IOException {
196: assert Thread.holdsLock(this );
197: input = null;
198: if (reader != null) {
199: reader.setInput(null);
200: }
201: if (stream != null) {
202: stream.close();
203: stream = null;
204: }
205: }
206:
207: /**
208: * Returns a localized string for the specified error key.
209: */
210: final String getString(final int key) {
211: return Errors.getResources(locale).getString(key);
212: }
213:
214: /**
215: * Returns the currently set {@link Locale}, or {@code null} if none has been set.
216: */
217: public Locale getLocale() {
218: return locale;
219: }
220:
221: /**
222: * Sets the current {@linkplain Locale locale} of this coverage reader to the given value.
223: * A value of {@code null} removes any previous setting, and indicates that the reader
224: * should localize as it sees fit.
225: */
226: public synchronized void setLocale(final Locale locale) {
227: this .locale = locale;
228: setReaderLocale(locale);
229: }
230:
231: /**
232: * Set the locale for the {@link ImageReader}.
233: */
234: private void setReaderLocale(final Locale locale) {
235: if (reader != null) {
236: final Locale[] list = reader.getAvailableLocales();
237: for (int i = list.length; --i >= 0;) {
238: if (locale.equals(list[i])) {
239: reader.setLocale(locale);
240: return;
241: }
242: }
243: final String language = getISO3Language(locale);
244: if (language != null) {
245: for (int i = list.length; --i >= 0;) {
246: if (language.equals(getISO3Language(list[i]))) {
247: reader.setLocale(list[i]);
248: return;
249: }
250: }
251: }
252: reader.setLocale(null);
253: }
254: }
255:
256: /**
257: * Returns the ISO language code for the specified locale, or {@code null} if not available.
258: */
259: private static String getISO3Language(final Locale locale) {
260: try {
261: return locale.getISO3Language();
262: } catch (MissingResourceException exception) {
263: return null;
264: }
265: }
266:
267: /**
268: * Returns the specified input as a {@link File} object, or {@code null}.
269: */
270: private static File asFile(final Object input) {
271: if (input instanceof File) {
272: return (File) input;
273: }
274: if (input instanceof URL) {
275: return new File(((URL) input).getPath());
276: }
277: return null;
278: }
279:
280: /**
281: * Returns an {@link Iterator} containing all currently registered {@link ImageReader}s
282: * that claim to be able to decode the image. The default implementation returns
283: * <code>ImageIO.getImageReadersByFormatName({@link #formatName})</code>.
284: *
285: * @param input The input source.
286: */
287: protected Iterator getImageReaders(final Object input) {
288: if (formatName != null) {
289: return ImageIO.getImageReadersByFormatName(formatName);
290: }
291: final File file = asFile(input);
292: if (file != null) {
293: final String filename = file.getName();
294: final int separator = filename.lastIndexOf('.');
295: if (separator >= 0) {
296: final String extension = filename
297: .substring(separator + 1);
298: return ImageIO.getImageReadersBySuffix(extension);
299: }
300: }
301: return ImageIO.getImageReaders(input);
302: }
303:
304: /**
305: * Sets the input source to the given object. The input is usually a
306: * {@link File} or an {@link URL} object. But some other types (e.g.
307: * {@link ImageInputStream}) may be accepted too.
308: * <p>
309: * If this method is invoked for the first time or after a call to
310: * {@link #reset}, then it will queries {@link #getImageReaders} for
311: * a list of {@link ImageReader}s and select the first one that accept
312: * the input.
313: *
314: * @param input The {@link File} or {@link URL} to be read.
315: * @param seekForwardOnly if {@code true}, grid coverages
316: * and metadata may only be read in ascending order from
317: * the input source.
318: * @throws IOException if an I/O operation failed.
319: * @throws IllegalArgumentException if input is not an instance
320: * of one of the classes declared by the {@link ImageReader}
321: * service provider.
322: */
323: public synchronized void setInput(final Object input,
324: final boolean seekForwardOnly) throws IOException {
325: clear();
326: if (input != null) {
327: ImageReader reader = this .reader;
328: boolean reuseLast = (reader != null);
329: for (final Iterator it = getImageReaders(input); it
330: .hasNext();) {
331: if (!reuseLast) {
332: reader = (ImageReader) it.next();
333: setReaderLocale(locale);
334: }
335: reuseLast = false;
336: final Class[] types = reader.getOriginatingProvider()
337: .getInputTypes();
338: if (contains(types, input.getClass())) {
339: reader.setInput(input, seekForwardOnly);
340: this .input = input;
341: this .reader = reader;
342: return;
343: }
344: if (contains(types, ImageInputStream.class)) {
345: assert stream == null;
346: stream = ImageIO.createImageInputStream(input);
347: if (stream != null) {
348: reader.setInput(stream, seekForwardOnly);
349: this .input = input;
350: this .reader = reader;
351: return;
352: }
353: }
354: }
355: throw new IllegalArgumentException(
356: getString(ErrorKeys.NO_IMAGE_READER));
357: }
358: }
359:
360: /**
361: * Checks if the array {@code types} contains {@code type} or a super-class of {@code type}.
362: */
363: private static boolean contains(final Class[] types,
364: final Class type) {
365: for (int i = 0; i < types.length; i++) {
366: if (types[i].isAssignableFrom(type)) {
367: return true;
368: }
369: }
370: return false;
371: }
372:
373: /**
374: * Returns the number of images available from the current input source.
375: * Note that some image formats do not specify how many images are present
376: * in the stream. Thus determining the number of images will require the
377: * entire stream to be scanned and may require memory for buffering.
378: * The {@code allowSearch} parameter may be set to {@code false}
379: * to indicate that an exhaustive search is not desired.
380: *
381: * @param allowSearch If {@code true}, the true number of images will
382: * be returned even if a search is required. If {@code false},
383: * the reader may return -1 without performing the search.
384: * @return The number of images, or -1 if {@code allowSearch} is
385: * {@code false} and a search would be required.
386: * @throws IllegalStateException If the input source has not been set, or if
387: * the input has been specified with {@code seekForwardOnly} set to {@code true}.
388: * @throws IOException If an error occurs reading the information from the input source.
389: */
390: public synchronized int getNumImages(final boolean allowSearch)
391: throws IOException {
392: if (reader == null) {
393: throw new IllegalStateException(
394: getString(ErrorKeys.NO_IMAGE_INPUT));
395: }
396: return reader.getNumImages(allowSearch);
397: }
398:
399: /**
400: * Ensures that the specified image index in inside the expected range. The upper limit
401: * (exclusive) is given by <code>{@link #getNumImages getNumImages}(false)</code>.
402: *
403: * @param imageIndex The index to check for validity.
404: * @return The {@code numImages} value, as an opportunist information.
405: * @throws IndexOutOfBoundsException If the index is invalid.
406: * @throws IOException If an error occurs reading the information from the input source.
407: */
408: final int checkImageIndex(final int imageIndex) throws IOException,
409: IndexOutOfBoundsException {
410: assert Thread.holdsLock(this );
411: if (reader == null) {
412: throw new IllegalStateException(
413: getString(ErrorKeys.NO_IMAGE_INPUT));
414: }
415: final int numImages = getNumImages(false);
416: if (imageIndex < reader.getMinIndex()
417: || (imageIndex >= numImages && numImages >= 0)) {
418: throw new IndexOutOfBoundsException(Errors.getResources(
419: locale).getString(ErrorKeys.ILLEGAL_ARGUMENT_$2,
420: "imageIndex", new Integer(imageIndex)));
421: }
422: return numImages;
423: }
424:
425: /**
426: * Gets the {@link GridCoverage} name at the specified index. The default implementation
427: * returns the input filename, or the "Untitled" string if input is not a {@link File} or
428: * an {@link URL} object.
429: *
430: * @param index The index of the image to be queried.
431: * @return The name for the {@link GridCoverage} at the specified index.
432: * @throws IllegalStateException if the input source has not been set.
433: * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
434: * @throws IOException if an error occurs reading the information from the input source.
435: */
436: public synchronized String getName(final int index)
437: throws IOException {
438: final int numImages = checkImageIndex(index);
439: String name;
440: final File file = asFile(input);
441: if (file != null) {
442: name = file.getName();
443: } else {
444: name = Vocabulary.getResources(locale).getString(
445: VocabularyKeys.UNTITLED);
446: }
447: if (numImages != 1) {
448: name = name + " [" + index + ']';
449: }
450: return name;
451: }
452:
453: /**
454: * Returns the coordinate reference system for the {@link GridCoverage} to be read.
455: *
456: * @param index The index of the image to be queried.
457: * @return The coordinate reference system for the {@link GridCoverage} at the specified index.
458: * @throws IllegalStateException if the input source has not been set.
459: * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
460: * @throws IOException if an error occurs reading the width information from the input source.
461: */
462: public abstract CoordinateReferenceSystem getCoordinateReferenceSystem(
463: int index) throws IOException;
464:
465: /**
466: * Returns the envelope for the {@link GridCoverage} to be read.
467: * The envelope must have the same number of dimensions than the
468: * coordinate reference system.
469: *
470: * @param index The index of the image to be queried.
471: * @return The envelope for the {@link GridCoverage} at the specified index.
472: * @throws IllegalStateException if the input source has not been set.
473: * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
474: * @throws IOException if an error occurs reading the width information from the input source.
475: */
476: public abstract Envelope getEnvelope(int index) throws IOException;
477:
478: /**
479: * Returns the grid range for the {@link GridCoverage} to be read.
480: * The grid range must have the same number of dimensions than the
481: * envelope.
482: *
483: * The default implementation construct a {@link GridRange} object
484: * using information provided by {@link ImageReader#getWidth} and
485: * {@link ImageReader#getHeight}.
486: *
487: * @param index The index of the image to be queried.
488: * @return The grid range for the {@link GridCoverage} at the specified index.
489: * @throws IllegalStateException if the input source has not been set.
490: * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
491: * @throws IOException if an error occurs reading the width information from the input source.
492: */
493: public synchronized GridRange getGridRange(final int index)
494: throws IOException {
495: checkImageIndex(index);
496: final int dimension = getCoordinateReferenceSystem(index)
497: .getCoordinateSystem().getDimension();
498: final int[] lower = new int[dimension];
499: final int[] upper = new int[dimension];
500: Arrays.fill(upper, 1);
501: upper[0] = reader.getWidth(index);
502: upper[1] = reader.getHeight(index);
503: return new GeneralGridRange(lower, upper);
504: }
505:
506: /**
507: * Returns the transform from {@linkplain #getGridRange grid range} to {@linkplain
508: * #getCoordinateReferenceSystem CRS} coordinates. The default implementation uses
509: * the {@link #gridToEnvelope} mapper.
510: *
511: * @since 2.4
512: */
513: public synchronized MathTransform getMathTransform(final int index)
514: throws IOException {
515: gridToEnvelope.setGridRange(getGridRange(index));
516: gridToEnvelope.setEnvelope(getEnvelope(index));
517: return gridToEnvelope.createTransform();
518: }
519:
520: /**
521: * Returns the sample dimensions for each band of the {@link GridCoverage}
522: * to be read. If sample dimensions are not known, then this method returns
523: * {@code null}. The default implementation always returns {@code null}.
524: *
525: * @param index The index of the image to be queried.
526: * @return The category lists for the {@link GridCoverage} at the specified index.
527: * This array's length must be equals to the number of bands in {@link GridCoverage}.
528: * @throws IllegalStateException if the input source has not been set.
529: * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
530: * @throws IOException if an error occurs reading the width information from the input source.
531: */
532: public synchronized GridSampleDimension[] getSampleDimensions(
533: final int index) throws IOException {
534: checkImageIndex(index);
535: return null;
536: }
537:
538: /**
539: * Reads the grid coverage. The default implementation gets the default {@link ImageReadParam}
540: * and checks if it is an instance of {@link RawBinaryImageReadParam}. If it is, this method
541: * then invokes {@link RawBinaryImageReadParam#setStreamImageSize} with informations provided
542: * by {@link #getGridRange}. Finally, a grid coverage is constructed using informations provided
543: * by {@link #getName}, {@link #getCoordinateReferenceSystem} and {@link #getEnvelope}.
544: *
545: * @param index The index of the image to be queried.
546: * @return The {@link GridCoverage} at the specified index.
547: * @throws IllegalStateException if the input source has not been set.
548: * @throws IndexOutOfBoundsException if the supplied index is out of bounds.
549: * @throws IOException if an error occurs reading the width information from the input source.
550: */
551: public synchronized GridCoverage getGridCoverage(final int index)
552: throws IOException {
553: checkImageIndex(index);
554: final ImageReadParam param = reader.getDefaultReadParam();
555: if (param instanceof RawBinaryImageReadParam) {
556: final RawBinaryImageReadParam rawParam = (RawBinaryImageReadParam) param;
557: final GridRange range = getGridRange(index);
558: final Dimension size = new Dimension(range.getLength(0),
559: range.getLength(1));
560: rawParam.setStreamImageSize(size);
561: }
562: final String name = getName(index);
563: final MathTransform gridToCRS = getMathTransform(index);
564: final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(index);
565: final GridSampleDimension[] bands = getSampleDimensions(index);
566: final RenderedImage image = reader.readAsRenderedImage(index,
567: param);
568: if (LOGGER.isLoggable(Level.FINE)) {
569: /*
570: * Log the arguments used for creating the GridCoverage. This is a costly logging:
571: * the string representations for some argument are very long (RenderedImage and
572: * CoordinateReferenceSystem), and string representation for sample dimensions may
573: * use many lines.
574: */
575: final Envelope envelope = getEnvelope(index);
576: final StringWriter buffer = new StringWriter();
577: final LineWriter trimer = new LineWriter(buffer);
578: final TableWriter table = new TableWriter(trimer, 1);
579: final PrintWriter out = new PrintWriter(table);
580: buffer.write("Creating GridCoverage[\"");
581: buffer.write(name);
582: buffer.write("\"] with:");
583: buffer.write(trimer.getLineSeparator());
584: table.setMultiLinesCells(true);
585: final int sdCount = (bands != null) ? bands.length : 0;
586: for (int i = -3; i < sdCount; i++) {
587: String key = "";
588: Object value;
589: switch (i) {
590: case -3:
591: key = "RenderedImage";
592: value = image;
593: break;
594: case -2:
595: key = "CoordinateReferenceSystem";
596: value = crs;
597: break;
598: case -1:
599: key = "Envelope";
600: value = envelope;
601: break;
602: case 0:
603: key = "SampleDimensions"; // fall through
604: default:
605: value = bands[i];
606: break;
607: }
608: out.print(" ");
609: out.print(key);
610: table.nextColumn();
611: out.print('=');
612: table.nextColumn();
613: out.print(value);
614: table.nextLine();
615: }
616: out.flush();
617: LOGGER.fine(buffer.toString());
618: }
619: return factory.create(name, image, crs, gridToCRS, bands, null,
620: null);
621: }
622: }
|