/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-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.coverage.io;
// J2SE dependencies
import java.awt.Dimension;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GeneralGridEnvelope;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.factory.Hints;
import org.geotools.image.io.RawBinaryImageReadParam;
import org.geotools.io.LineWriter;
import org.geotools.io.TableWriter;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.logging.Logging;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.GridRange;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
/**
* Base class for reading {@link GridCoverage} objects. Reading is a two steps process:
* The input file must be set first, then the actual reading is performed with the
* {@link #getGridCoverage}. Example:
*
* <blockquote><pre>
* AbstractGridCoverageReader reader = ...
* reader.{@linkplain #setInput setInput}(new File("MyCoverage.dat"), true);
* GridCoverage coverage = reader.{@linkplain #getGridCoverage getGridCoverage}(0);
* </pre></blockquote>
*
* Subclasses needs to implements at least the following methods:
*
* <ul>
* <li>{@link #getCoordinateReferenceSystem}</li>
* <li>{@link #getEnvelope}</li>
* </ul>
*
* The default implementation should be able to create acceptable grid
* coverage using informations provided by the two above-mentioned methods.
* However, other methods may be overriden too in order to get finner control
* on the result.
*
* @since 2.2
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public abstract class AbstractGridCoverageReader implements GridCoverageReader {
/**
* The logger for the {@link #getGridCoverage} method.
*/
static Logger LOGGER = Logging.getLogger("org.geotools.coverage.io");
/**
* The format name (e.g. "PNG" or "GeoTIFF"). This format name should
* be known to {@link ImageIO#getImageReadersByFormatName(String)},
* unless {@link #getImageReaders} is overriden.
*/
private final String formatName;
/**
* The {@link ImageReader} to use for decoding {@link RenderedImage}s. This reader is
* initially {@code null} and lazily created the first time {@link #setInput} is invoked.
* Once created, it is reused as much as possible. Invoking {@link #reset} dispose the
* reader and set it back to {@code null}.
*/
protected ImageReader reader;
/**
* The object to use for creating {@linkplain MathTransform math transform} from the
* {@linkplain #getGridRange grid range} and the {@linkplain #getEnvelope envelope}.
* Subclasses can change its configuration after {@code AbstractGridCoverageReader}
* construction in order to change its behavior regarding axis reversal or axis
* swapping for example.
*
* @see #getMathTransform
*
* @since 2.4
*/
protected final GridToEnvelopeMapper gridToEnvelope = new GridToEnvelopeMapper();
/**
* The grid coverage factory to use.
*/
private final GridCoverageFactory factory;
/**
* The input {@link File} or {@link URL}, or {@code null} if input is not set.
*/
private Object input;
/**
* The input stream, or {@code null} if input is not set or if {@link #reader}
* accepted directly {@link #input}.
*/
private ImageInputStream stream;
/**
* The locale to use for formatting messages, or {@code null} for a default locale.
*/
private Locale locale;
/**
* Constructs a {@code AbstractGridCoverageReader}. The {@linkplain ImageReader image reader}
* will be determined from the file extension, if any.
*
* @param hints The factory hints to use.
*
* @since 2.4
*/
public AbstractGridCoverageReader(final Hints hints) {
this(hints, null);
}
/**
* Constructs a {@code AbstractGridCoverageReader} for the specified format name. This
* format name should be known to {@link ImageIO#getImageReadersByFormatName(String)}.
*
* @param hints The factory hints to use.
* @param formatName The format name of the {@linkplain ImageReader image reader} to use,
* or {@code null} for relying on file extension instead.
*
* @since 2.4
*/
public AbstractGridCoverageReader(final Hints hints, final String formatName) {
this.factory = CoverageFactoryFinder.getGridCoverageFactory(hints);
this.formatName = formatName;
}
/**
* Restores the {@code AbstractGridCoverageReader} to its initial state.
*
* @throws IOException if an error occurs while disposing resources.
*/
public synchronized void reset() throws IOException {
clear();
locale = null;
if (reader != null) {
reader.reset();
reader.dispose();
reader = null;
}
}
/**
* Clears this {@code AbstractGridCoverageReader}. This method is similar to {@link #reset},
* except that it doesn't destroy the current {@link ImageReader} and keeps the locale setting.
*
* @throws IOException if an error occurs while disposing resources.
*/
private void clear() throws IOException {
assert Thread.holdsLock(this);
input = null;
if (reader != null) {
reader.setInput(null);
}
if (stream != null) {
stream.close();
stream = null;
}
}
/**
* Returns a localized string for the specified error key.
*/
final String getString(final int key) {
return Errors.getResources(locale).getString(key);
}
/**
* Returns the currently set {@link Locale}, or {@code null} if none has been set.
*/
public Locale getLocale() {
return locale;
}
/**
* Sets the current {@linkplain Locale locale} of this coverage reader to the given value.
* A value of {@code null} removes any previous setting, and indicates that the reader
* should localize as it sees fit.
*/
public synchronized void setLocale(final Locale locale) {
this.locale = locale;
setReaderLocale(locale);
}
/**
* Set the locale for the {@link ImageReader}.
*/
private void setReaderLocale(final Locale locale) {
if (reader != null) {
final Locale[] list = reader.getAvailableLocales();
for (int i=list.length; --i>=0;) {
if (locale.equals(list[i])) {
reader.setLocale(locale);
return;
}
}
final String language = getISO3Language(locale);
if (language != null) {
for (int i=list.length; --i>=0;) {
if (language.equals(getISO3Language(list[i]))) {
reader.setLocale(list[i]);
return;
}
}
}
reader.setLocale(null);
}
}
/**
* Returns the ISO language code for the specified locale, or {@code null} if not available.
*/
private static String getISO3Language(final Locale locale) {
try {
return locale.getISO3Language();
} catch (MissingResourceException exception) {
return null;
}
}
/**
* Returns the specified input as a {@link File} object, or {@code null}.
*/
private static File asFile(final Object input) {
if (input instanceof File) {
return (File) input;
}
if (input instanceof URL) {
return new File(((URL) input).getPath());
}
return null;
}
/**
* Returns an {@link Iterator} containing all currently registered {@link ImageReader}s
* that claim to be able to decode the image. The default implementation returns
* <code>ImageIO.getImageReadersByFormatName({@link #formatName})</code>.
*
* @param input The input source.
*/
protected Iterator getImageReaders(final Object input) {
if (formatName != null) {
return ImageIO.getImageReadersByFormatName(formatName);
}
final File file = asFile(input);
if (file != null) {
final String filename = file.getName();
final int separator = filename.lastIndexOf('.');
if (separator >= 0) {
final String extension = filename.substring(separator + 1);
return ImageIO.getImageReadersBySuffix(extension);
}
}
return ImageIO.getImageReaders(input);
}
/**
* Sets the input source to the given object. The input is usually a
* {@link File} or an {@link URL} object. But some other types (e.g.
* {@link ImageInputStream}) may be accepted too.
* <p>
* If this method is invoked for the first time or after a call to
* {@link #reset}, then it will queries {@link #getImageReaders} for
* a list of {@link ImageReader}s and select the first one that accept
* the input.
*
* @param input The {@link File} or {@link URL} to be read.
* @param seekForwardOnly if {@code true}, grid coverages
* and metadata may only be read in ascending order from
* the input source.
* @throws IOException if an I/O operation failed.
* @throws IllegalArgumentException if input is not an instance
* of one of the classes declared by the {@link ImageReader}
* service provider.
*/
public synchronized void setInput(final Object input, final boolean seekForwardOnly)
throws IOException
{
clear();
if (input != null) {
ImageReader reader = this.reader;
boolean reuseLast = (reader != null);
for (final Iterator it=getImageReaders(input); it.hasNext();) {
if (!reuseLast) {
reader = (ImageReader) it.next();
setReaderLocale(locale);
}
reuseLast = false;
final Class[] types = reader.getOriginatingProvider().getInputTypes();
if (contains(types, input.getClass())) {
reader.setInput(input, seekForwardOnly);
this.input = input;
this.reader = reader;
return;
}
if (contains(types, ImageInputStream.class)) {
assert stream == null;
stream = ImageIO.createImageInputStream(input);
if (stream != null) {
reader.setInput(stream, seekForwardOnly);
this.input = input;
this.reader = reader;
return;
}
}
}
throw new IllegalArgumentException(getString(ErrorKeys.NO_IMAGE_READER));
}
}
/**
* Checks if the array {@code types} contains {@code type} or a super-class of {@code type}.
*/
private static boolean contains(final Class[] types, final Class type) {
for (int i=0; i<types.length; i++) {
if (types[i].isAssignableFrom(type)) {
return true;
}
}
return false;
}
/**
* Returns the number of images available from the current input source.
* Note that some image formats do not specify how many images are present
* in the stream. Thus determining the number of images will require the
* entire stream to be scanned and may require memory for buffering.
* The {@code allowSearch} parameter may be set to {@code false}
* to indicate that an exhaustive search is not desired.
*
* @param allowSearch If {@code true}, the true number of images will
* be returned even if a search is required. If {@code false},
* the reader may return -1 without performing the search.
* @return The number of images, or -1 if {@code allowSearch} is
* {@code false} and a search would be required.
* @throws IllegalStateException If the input source has not been set, or if
* the input has been specified with {@code seekForwardOnly} set to {@code true}.
* @throws IOException If an error occurs reading the information from the input source.
*/
public synchronized int getNumImages(final boolean allowSearch) throws IOException {
if (reader == null) {
throw new IllegalStateException(getString(ErrorKeys.NO_IMAGE_INPUT));
}
return reader.getNumImages(allowSearch);
}
/**
* Ensures that the specified image index in inside the expected range. The upper limit
* (exclusive) is given by <code>{@link #getNumImages getNumImages}(false)</code>.
*
* @param imageIndex The index to check for validity.
* @return The {@code numImages} value, as an opportunist information.
* @throws IndexOutOfBoundsException If the index is invalid.
* @throws IOException If an error occurs reading the information from the input source.
*/
final int checkImageIndex(final int imageIndex) throws IOException, IndexOutOfBoundsException {
assert Thread.holdsLock(this);
if (reader == null) {
throw new IllegalStateException(getString(ErrorKeys.NO_IMAGE_INPUT));
}
final int numImages = getNumImages(false);
if (imageIndex<reader.getMinIndex() || (imageIndex>=numImages && numImages>=0)) {
throw new IndexOutOfBoundsException(Errors.getResources(locale).getString(
ErrorKeys.ILLEGAL_ARGUMENT_$2, "imageIndex", imageIndex));
}
return numImages;
}
/**
* Gets the {@link GridCoverage} name at the specified index. The default implementation
* returns the input filename, or the "Untitled" string if input is not a {@link File} or
* an {@link URL} object.
*
* @param index The index of the image to be queried.
* @return The name for the {@link GridCoverage} at the specified index.
* @throws IllegalStateException if the input source has not been set.
* @throws IndexOutOfBoundsException if the supplied index is out of bounds.
* @throws IOException if an error occurs reading the information from the input source.
*/
public synchronized String getName(final int index) throws IOException {
final int numImages = checkImageIndex(index);
String name;
final File file = asFile(input);
if (file != null) {
name = file.getName();
} else {
name = Vocabulary.getResources(locale).getString(VocabularyKeys.UNTITLED);
}
if (numImages != 1) {
name = name + " [" + index + ']';
}
return name;
}
/**
* Returns the coordinate reference system for the {@link GridCoverage} to be read.
*
* @param index The index of the image to be queried.
* @return The coordinate reference system for the {@link GridCoverage} at the specified index.
* @throws IllegalStateException if the input source has not been set.
* @throws IndexOutOfBoundsException if the supplied index is out of bounds.
* @throws IOException if an error occurs reading the width information from the input source.
*/
public abstract CoordinateReferenceSystem getCoordinateReferenceSystem(int index) throws IOException;
/**
* Returns the envelope for the {@link GridCoverage} to be read.
* The envelope must have the same number of dimensions than the
* coordinate reference system.
*
* @param index The index of the image to be queried.
* @return The envelope for the {@link GridCoverage} at the specified index.
* @throws IllegalStateException if the input source has not been set.
* @throws IndexOutOfBoundsException if the supplied index is out of bounds.
* @throws IOException if an error occurs reading the width information from the input source.
*/
public abstract Envelope getEnvelope(int index) throws IOException;
/**
* Returns the grid range for the {@link GridCoverage} to be read.
* The grid range must have the same number of dimensions than the
* envelope.
*
* The default implementation construct a {@link GridRange} object
* using information provided by {@link ImageReader#getWidth} and
* {@link ImageReader#getHeight}.
*
* @param index The index of the image to be queried.
* @return The grid range for the {@link GridCoverage} at the specified index.
* @throws IllegalStateException if the input source has not been set.
* @throws IndexOutOfBoundsException if the supplied index is out of bounds.
* @throws IOException if an error occurs reading the width information from the input source.
*/
public synchronized GridEnvelope getGridRange(final int index) throws IOException {
checkImageIndex(index);
final int dimension = getCoordinateReferenceSystem(index).getCoordinateSystem().getDimension();
final int[] lower = new int[dimension];
final int[] upper = new int[dimension];
Arrays.fill(upper, 1);
upper[0] = reader.getWidth(index);
upper[1] = reader.getHeight(index);
return new GeneralGridEnvelope(lower, upper);
}
/**
* Returns the transform from {@linkplain #getGridRange grid range} to {@linkplain
* #getCoordinateReferenceSystem CRS} coordinates. The default implementation uses
* the {@link #gridToEnvelope} mapper.
*
* @since 2.4
*/
public synchronized MathTransform getMathTransform(final int index) throws IOException {
gridToEnvelope.setGridRange(getGridRange(index));
gridToEnvelope.setEnvelope (getEnvelope (index));
return gridToEnvelope.createTransform();
}
/**
* Returns the sample dimensions for each band of the {@link GridCoverage}
* to be read. If sample dimensions are not known, then this method returns
* {@code null}. The default implementation always returns {@code null}.
*
* @param index The index of the image to be queried.
* @return The category lists for the {@link GridCoverage} at the specified index.
* This array's length must be equals to the number of bands in {@link GridCoverage}.
* @throws IllegalStateException if the input source has not been set.
* @throws IndexOutOfBoundsException if the supplied index is out of bounds.
* @throws IOException if an error occurs reading the width information from the input source.
*/
public synchronized GridSampleDimension[] getSampleDimensions(final int index) throws IOException {
checkImageIndex(index);
return null;
}
/**
* Reads the grid coverage. The default implementation gets the default {@link ImageReadParam}
* and checks if it is an instance of {@link RawBinaryImageReadParam}. If it is, this method
* then invokes {@link RawBinaryImageReadParam#setStreamImageSize} with informations provided
* by {@link #getGridRange}. Finally, a grid coverage is constructed using informations provided
* by {@link #getName}, {@link #getCoordinateReferenceSystem} and {@link #getEnvelope}.
*
* @param index The index of the image to be queried.
* @return The {@link GridCoverage} at the specified index.
* @throws IllegalStateException if the input source has not been set.
* @throws IndexOutOfBoundsException if the supplied index is out of bounds.
* @throws IOException if an error occurs reading the width information from the input source.
*/
public synchronized GridCoverage getGridCoverage(final int index) throws IOException {
checkImageIndex(index);
final ImageReadParam param = reader.getDefaultReadParam();
if (param instanceof RawBinaryImageReadParam) {
final RawBinaryImageReadParam rawParam = (RawBinaryImageReadParam) param;
final GridEnvelope range = getGridRange(index);
final Dimension size = new Dimension(range.getSpan(0), range.getSpan(1));
rawParam.setStreamImageSize(size);
}
final String name = getName(index);
final MathTransform gridToCRS = getMathTransform(index);
final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(index);
final GridSampleDimension[] bands = getSampleDimensions(index);
final RenderedImage image = reader.readAsRenderedImage(index, param);
if (LOGGER.isLoggable(Level.FINE)) {
/*
* Log the arguments used for creating the GridCoverage. This is a costly logging:
* the string representations for some argument are very long (RenderedImage and
* CoordinateReferenceSystem), and string representation for sample dimensions may
* use many lines.
*/
final Envelope envelope = getEnvelope(index);
final StringWriter buffer = new StringWriter( );
final LineWriter trimer = new LineWriter (buffer );
final TableWriter table = new TableWriter (trimer, 1);
final PrintWriter out = new PrintWriter (table );
buffer.write("Creating GridCoverage[\"");
buffer.write(name);
buffer.write("\"] with:");
buffer.write(trimer.getLineSeparator());
table.setMultiLinesCells(true);
final int sdCount = (bands != null) ? bands.length : 0;
for (int i=-3; i<sdCount; i++) {
String key = "";
Object value;
switch (i) {
case -3: key="RenderedImage"; value=image; break;
case -2: key="CoordinateReferenceSystem"; value=crs; break;
case -1: key="Envelope"; value=envelope; break;
case 0: key="SampleDimensions"; // fall through
default: value = bands[i]; break;
}
out.print(" ");
out.print(key ); table.nextColumn();
out.print('=' ); table.nextColumn();
out.print(value ); table.nextLine();
}
out.flush();
LOGGER.fine(buffer.toString());
}
return factory.create(name, image, crs, gridToCRS, bands, null, null);
}
}