/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.mosaic;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.File;
import java.io.IOException;
import java.util.*; // Lot of imports used in this class.
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.imageio.IIOException;
import javax.imageio.IIOParamController;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import org.geotools.io.TableWriter;
import org.geotools.factory.GeoTools;
import org.geotools.image.io.metadata.MetadataMerge;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.FrequencySortedSet;
import org.geotools.util.logging.Logging;
/**
* An image reader built from a mosaic of other image readers. The mosaic is specified as a
* collection of {@link Tile} objects, organized in a {@link TileManager}.
*
* @since 2.5
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
public class MosaicImageReader extends ImageReader {
/**
* {@code true} for disabling operations that may corrupt data values,
* or {@code false} if only the visual effect matter.
*/
private static final boolean PRESERVE_DATA = true;
/**
* Type arguments made of a single {@code int} value. Used with reflections in order to check
* if a method has been overriden (knowing that it is not the case allows some optimizations).
*/
private static final Class<?>[] INTEGER_ARGUMENTS = {
int.class
};
/**
* An unmodifiable view of {@link #readers} keys.
*/
private final Set<ImageReaderSpi> providers;
/**
* The image reader created for each provider. Keys must be the union of
* {@link TileManager#getImageReaderSpis} at every image index and must be computed right at
* {@link #setInput} invocation time.
*/
private final Map<ImageReaderSpi,ImageReader> readers;
/**
* The input given to each image reader. Values are {@linkplain Tile#getInput tile input}
* before they have been wrapped in an {@linkplain ImageInputStream image input stream}.
*
* @see MosaicImageReadParam#readers
*/
private final Map<ImageReader,Object> readerInputs;
/**
* The reader currently under process of reading, or {@code null} if none. Used by
* {@link #abort} only. Changes must be performed inside a {@code synchronized(this)} block.
*/
private transient ImageReader reading;
/**
* The logging level for tiling information during reads. Note that we choose a default
* level slightly higher than the intermediate results logged by {@link RTree}.
*/
private Level level = Level.FINE;
/**
* Constructs an image reader with the default provider.
*/
public MosaicImageReader() {
this(null);
}
/**
* Constructs an image reader with the specified provider.
*
* @param spi The image reader provider, or {@code null} for the default one.
*/
public MosaicImageReader(final ImageReaderSpi spi) {
super(spi != null ? spi : Spi.DEFAULT);
readers = new HashMap<ImageReaderSpi,ImageReader>();
readerInputs = new IdentityHashMap<ImageReader,Object>();
providers = Collections.unmodifiableSet(readers.keySet());
}
/**
* Returns the logging level for tile information during reads.
*
* @return The current logging level.
*/
public Level getLogLevel() {
return level;
}
/**
* Sets the logging level for tile information during reads. The default
* value is {@link Level#FINE}. A {@code null} value restore the default.
*
* @param level The new logging level, or {@code null} for the default.
*/
public void setLogLevel(Level level) {
if (level == null) {
level = Level.FINE;
}
this.level = level;
}
/**
* Returns the tiles manager, making sure that it is set.
*
* @param imageIndex The image index, from 0 inclusive to {@link #getNumImages} exclusive.
* @return The tile manager for image at the given index.
*/
private TileManager getTileManager(final int imageIndex) throws IOException {
if (input instanceof TileManager[]) {
final TileManager[] tiles = (TileManager[]) input;
if (imageIndex < 0 || imageIndex >= tiles.length) {
throw new IndexOutOfBoundsException(Errors.format(
ErrorKeys.INDEX_OUT_OF_BOUNDS_$1, imageIndex));
}
return tiles[imageIndex];
}
throw new IllegalStateException(Errors.format(ErrorKeys.NO_IMAGE_INPUT));
}
/**
* Returns the input, which is a an array of {@linkplain TileManager tile managers}.
* The array length is the {@linkplain #getNumImages number of images}. The element
* at index <var>i</var> is the tile manager to use when reading at image index <var>i</var>.
*/
@Override
public TileManager[] getInput() {
final TileManager[] managers = (TileManager[]) super.getInput();
return (managers != null) ? managers.clone() : null;
}
/**
* Sets the input source, which is expected to be an array of
* {@linkplain TileManager tile managers}. If the given input is a singleton, an array or a
* {@linkplain Collection collection} of {@link Tile} objects, then it will be wrapped in an
* array of {@link TileManager}s.
*
* @param input The input.
* @param seekForwardOnly if {@code true}, images and metadata may only be read in ascending
* order from this input source.
* @param ignoreMetadata if {@code true}, metadata may be ignored during reads.
* @throws IllegalArgumentException if {@code input} is not an instance of one of the
* expected classes, or if the input can not be used because of an I/O error
* (in which case the exception has a {@link IOException} as its
* {@linkplain IllegalArgumentException#getCause cause}).
*/
@Override
public void setInput(Object input, final boolean seekForwardOnly, final boolean ignoreMetadata)
throws IllegalArgumentException
{
final TileManager[] managers;
try {
managers = TileManagerFactory.DEFAULT.createFromObject(input);
} catch (IOException e) {
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
}
final int numImages = (managers != null) ? managers.length : 0;
super.setInput(input=managers, seekForwardOnly, ignoreMetadata);
availableLocales = null; // Will be computed by getAvailableLocales() when first needed.
/*
* For every tile readers, closes the stream and disposes the ones that are not needed
* anymore for the new input. The image readers that may still useful will be recycled.
* We keep their streams open since it is possible that the new input uses the same ones
* (the old streams will be closed later if appears to not be used).
*/
Set<ImageReaderSpi> providers = Collections.emptySet();
try {
switch (numImages) {
case 0: {
// Keep the empty provider set.
break;
}
case 1: {
providers = managers[0].getImageReaderSpis();
break;
}
default: {
providers = new HashSet<ImageReaderSpi>(managers[0].getImageReaderSpis());
for (int i=1; i<numImages; i++) {
providers.addAll(managers[i].getImageReaderSpis());
}
break;
}
}
} catch (IOException e) {
/*
* Failed to get the set of providers. This is not a big issue; the only consequence
* is that we will dispose more readers than necessary, which means that we will need
* to recreate them later. Note that the set of providers may be partially filled.
*/
Logging.unexpectedException(MosaicImageReader.class, "setInput", e);
}
final Iterator<Map.Entry<ImageReaderSpi,ImageReader>> it = readers.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry<ImageReaderSpi,ImageReader> entry = it.next();
if (!providers.contains(entry.getKey())) {
final ImageReader reader = entry.getValue();
if (reader != null) {
/*
* Closes previous streams, if any. It is not a big deal if this operation
* fails, since we will not use anymore the old streams anyway. However it
* is worth to log.
*/
final Object rawInput = readerInputs.remove(reader);
final Object tileInput = reader.getInput();
if (rawInput != tileInput) try {
Tile.close(tileInput);
} catch (IOException exception) {
Logging.unexpectedException(MosaicImageReader.class, "setInput", exception);
}
reader.dispose();
}
it.remove();
}
}
for (final ImageReaderSpi provider : providers) {
if (!readers.containsKey(provider)) {
readers.put(provider, null);
}
}
assert providers.equals(this.providers);
assert readers.values().containsAll(readerInputs.keySet());
}
/**
* Returns the <cite>Service Provider Interfaces</cite> (SPI) of every
* {@linkplain ImageReader image readers} to be used for reading tiles.
* This method returns an empty set if no input has been set.
*
* @return The service providers for tile readers.
*
* @see TileManager#getImageReaderSpis
*/
public Set<ImageReaderSpi> getTileReaderSpis() {
return providers;
}
/**
* Creates a new {@link ImageReader} from the specified provider. This method do not
* check the cache and do not store the result in the cache. It should be invoked by
* {@link #getTileReader} and {@link #getTileReaders} methods only.
* <p>
* It is technically possible to return the same {@link ImageReader} instance from
* different {@link ImageReaderSpi}. It would broke the usual {@code ImageReaderSpi}
* contract for no obvious reason, but technically this class should work correctly
* even in such case.
*
* @param provider The provider. Must be a member of {@link #getTileReaderSpis}.
* @return The image reader for the given provider.
* @throws IOException if the image reader can not be created.
*/
private ImageReader createReaderInstance(final ImageReaderSpi provider) throws IOException {
final ImageReader reader = provider.createReaderInstance();
if (locale != null) {
try {
reader.setLocale(locale);
} catch (IllegalArgumentException e) {
// Invalid locale. Ignore this exception since it will not prevent the image
// reader to work mostly as expected (warning messages may be in a different
// locale, which is not a big deal).
Logging.recoverableException(MosaicImageReader.class, "getTileReader", e);
}
}
return reader;
}
/**
* Returns the image reader for the given provider.
*
* @param provider The provider. Must be a member of {@link #getTileReaderSpis}.
* @return The image reader for the given provider.
* @throws IOException if the image reader can not be created.
*/
final ImageReader getTileReader(final ImageReaderSpi provider) throws IOException {
assert readers.containsKey(provider); // Key should exists even if the value is null.
ImageReader reader = readers.get(provider);
if (reader == null) {
reader = createReaderInstance(provider);
readers.put(provider, reader);
}
return reader;
}
/**
* Returns every readers used for reading tiles. New readers may be created on the fly
* by this method. However failure to create them will be logged rather than trown as
* an exception. In such case the information obtained by the caller may be incomplete
* and the exception may be thrown later when {@link #getTileReader} will be invoked.
*/
final Set<ImageReader> getTileReaders() {
for (final Map.Entry<ImageReaderSpi,ImageReader> entry : readers.entrySet()) {
ImageReader reader = entry.getValue();
if (reader == null) {
final ImageReaderSpi provider = entry.getKey();
try {
reader = createReaderInstance(provider);
} catch (IOException exception) {
Logging.unexpectedException(MosaicImageReader.class, "getTileReaders", exception);
continue;
}
entry.setValue(reader);
}
if (!readerInputs.containsKey(reader)) {
readerInputs.put(reader, null);
}
}
assert readers.values().containsAll(readerInputs.keySet());
return readerInputs.keySet();
}
/**
* Returns a reader for the tiles, or {@code null}. This method tries to returns an instance
* of the most specific reader class. If no suitable instance is found, then it returns
* {@code null}.
* <p>
* This method is typically invoked for fetching an instance of {@code ImageReadParam}. We
* look for the most specific class because it may contains additional parameters that are
* ignored by super-classes. If we fail to find a suitable instance, then the caller shall
* fallback on the {@link ImageReader} default implementation.
*/
final ImageReader getTileReader() {
final Set<ImageReader> readers = getTileReaders();
Class<?> type = Classes.specializedClass(readers);
while (type!=null && ImageReader.class.isAssignableFrom(type)) {
for (final ImageReader candidate : readers) {
if (type.equals(candidate.getClass())) {
return candidate;
}
}
type = type.getSuperclass();
}
return null;
}
/**
* From the given set of tiles, select one tile to use as a prototype.
* This method tries to select the tile which use the most specific reader.
*
* @return The most specific tile, or {@code null} if none.
*/
private Tile getSpecificTile(final Collection<Tile> tiles) {
Tile fallback = null;
final Set<ImageReader> readers = getTileReaders();
Class<?> type = Classes.specializedClass(readers);
while (type!=null && ImageReader.class.isAssignableFrom(type)) {
for (final ImageReader reader : readers) {
if (type.equals(reader.getClass())) {
final ImageReaderSpi provider = reader.getOriginatingProvider(); // May be null
for (final Tile tile : tiles) {
/*
* We give precedence to ImageReaderSpi.equals(ImageReaderSpi) over
* ImageReaderSpi.isOwnReader(ImageReader) because we need consistency
* with the 'readers' HashMap. However the later will be used as a
* fallback if no exact match has been found.
*/
final ImageReaderSpi candidate = tile.getImageReaderSpi(); // Never null
if (candidate.equals(provider)) {
return tile;
}
if (fallback == null && candidate.isOwnReader(reader)) {
fallback = tile;
}
}
}
}
type = type.getSuperclass();
}
return fallback;
}
/**
* Returns an array of locales that may be used to localize warning listeners. The default
* implementations returns the union of the locales supported by this reader and every
* {@linkplain Tile#getImageReader tile readers}.
*
* @return An array of supported locales, or {@code null}.
*/
@Override
public Locale[] getAvailableLocales() {
if (availableLocales == null) {
final Set<Locale> locales = new LinkedHashSet<Locale>();
for (final ImageReader reader : getTileReaders()) {
final Locale[] additional = reader.getAvailableLocales();
if (additional != null) {
for (final Locale locale : additional) {
locales.add(locale);
}
}
}
if (locales.isEmpty()) {
return null;
}
availableLocales = locales.toArray(new Locale[locales.size()]);
}
return availableLocales.clone();
}
/**
* Sets the current locale of this image reader and every
* {@linkplain Tile#getImageReader tile readers}.
*
* @param locale the desired locale, or {@code null}.
* @throws IllegalArgumentException if {@code locale} is non-null but is not
* one of the {@linkplain #getAvailableLocales available locales}.
*/
@Override
public void setLocale(final Locale locale) throws IllegalArgumentException {
super.setLocale(locale); // May thrown an exception.
for (final ImageReader reader : readers.values()) {
try {
reader.setLocale(locale);
} catch (IllegalArgumentException e) {
// Locale not supported by the reader. It may occurs
// if not all readers support the same set of locales.
Logging.recoverableException(MosaicImageReader.class, "setLocale", e);
}
}
}
/**
* Returns the number of images, not including thumbnails.
*
* @throws IOException If an error occurs reading the information from the input source.
*/
public int getNumImages(final boolean allowSearch) throws IOException {
return (input instanceof TileManager[]) ? ((TileManager[]) input).length : 0;
}
/**
* Returns {@code true} if there is more than one tile for the given image index.
*
* @param imageIndex The index of the image to be queried.
* @return {@code true} If there is at least two tiles.
* @throws IOException If an error occurs reading the information from the input source.
*/
@Override
public boolean isImageTiled(final int imageIndex) throws IOException {
return getTileManager(imageIndex).isImageTiled();
}
/**
* Returns the width in pixels of the given image within the input source.
*
* @param imageIndex The index of the image to be queried.
* @return The width of the image.
* @throws IOException If an error occurs reading the information from the input source.
*/
public int getWidth(final int imageIndex) throws IOException {
return getTileManager(imageIndex).getRegion().width;
}
/**
* Returns the height in pixels of the given image within the input source.
*
* @param imageIndex The index of the image to be queried.
* @return The height of the image.
* @throws IOException If an error occurs reading the information from the input source.
*/
public int getHeight(final int imageIndex) throws IOException {
return getTileManager(imageIndex).getRegion().height;
}
/**
* Returns the width of a tile in the given image.
*
* @param imageIndex The index of the image to be queried.
* @return The width of a tile.
* @throws IOException If an error occurs reading the information from the input source.
*/
@Override
public int getTileWidth(final int imageIndex) throws IOException {
return getTileManager(imageIndex).getTileSize().width;
}
/**
* Returns the height of a tile in the given image.
*
* @param imageIndex The index of the image to be queried.
* @return The height of a tile.
* @throws IOException If an error occurs reading the information from the input source.
*/
@Override
public int getTileHeight(final int imageIndex) throws IOException {
return getTileManager(imageIndex).getTileSize().height;
}
/**
* Returns {@code true} if every image reader uses the default implementation for the given
* method. Some methods may avoid costly file seeking when this method returns {@code true}.
* <p>
* This method always returns {@code true} if there is no tiles.
*/
private boolean useDefaultImplementation(final String methodName, final Class<?>[] parameterTypes) {
for (final ImageReader reader : getTileReaders()) {
Class<?> type = reader.getClass();
try {
type = type.getMethod(methodName, parameterTypes).getDeclaringClass();
} catch (NoSuchMethodException e) {
Logging.unexpectedException(MosaicImageReader.class, "useDefaultImplementation", e);
return false; // Conservative value.
}
if (!type.equals(ImageReader.class)) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if there is only one tile in the given collection and if that singleton
* tile encloses fully the given source region. In such case, {@code MosaicImageReader} can
* delegates directly the reading process to the reader used by that tile.
*
* @param tiles The tile collection.
* @param sourceRegion The source region to be read, as computed by {@link #getSourceRegion}.
* @return {@code true} if {@code MosaicImageReader} can delegates the reading process to the
* singleton tile contained in the given collection.
* @throws IOException If an I/O operation was requiered and failed.
*/
private static boolean canDelegate(final Collection<Tile> tiles, final Rectangle sourceRegion)
throws IOException
{
final Iterator<Tile> it = tiles.iterator();
if (it.hasNext()) {
final Tile tile = it.next();
if (!it.hasNext()) {
return tile.getRegion().contains(sourceRegion);
}
}
return false;
}
/**
* Returns {@code true} if the storage format of the given image places no inherent impediment
* on random access to pixels. The default implementation returns {@code true} if the input of
* every tiles is a {@link File} and {@code isRandomAccessEasy} returned {@code true} for all
* tile readers.
*
* @throws IOException If an error occurs reading the information from the input source.
*/
@Override
public boolean isRandomAccessEasy(final int imageIndex) throws IOException {
if (useDefaultImplementation("isRandomAccessEasy", INTEGER_ARGUMENTS)) {
return super.isRandomAccessEasy(imageIndex);
}
for (final Tile tile : getTileManager(imageIndex).getTiles()) {
final Object input = tile.getInput();
if (!(input instanceof File)) {
return false;
}
final ImageReader reader = tile.getImageReader(this, true, true);
if (!reader.isRandomAccessEasy(tile.getImageIndex())) {
return false;
}
}
return true;
}
/**
* Returns the aspect ratio. If all tiles have the same aspect ratio, then that ratio is
* returned. Otherwise the {@linkplain ImageReader#getAspectRatio default value} is returned.
*
* @param imageIndex The index of the image to be queried.
* @throws IOException If an error occurs reading the information from the input source.
*/
@Override
public float getAspectRatio(final int imageIndex) throws IOException {
if (!useDefaultImplementation("getAspectRatio", INTEGER_ARGUMENTS)) {
float ratio = Float.NaN;
for (final Tile tile : getTileManager(imageIndex).getTiles()) {
final ImageReader reader = tile.getImageReader(this, true, true);
final float candidate = reader.getAspectRatio(tile.getImageIndex());
if (candidate == ratio || Float.isNaN(candidate)) {
// Same ratio or unspecified ratio.
continue;
}
if (!Float.isNaN(ratio)) {
// The ratio is different for different tile. Fall back on default.
return super.getAspectRatio(imageIndex);
}
ratio = candidate;
}
if (!Float.isNaN(ratio)) {
return ratio;
}
}
return super.getAspectRatio(imageIndex);
}
/**
* Returns the image type policy from the specified parameter.
* Fallback on the default policy if the parameter to not specify any.
*/
private ImageTypePolicy getImageTypePolicy(final ImageReadParam param) {
if (param instanceof MosaicImageReadParam) {
final ImageTypePolicy policy = ((MosaicImageReadParam) param).getImageTypePolicy();
if (policy != null) {
return policy;
}
}
return getDefaultImageTypePolicy();
}
/**
* Returns the policy for {@link #getImageTypes computing image types}. This is also
* the policy used by {@linkplain #read read} method when none has been explicitly
* {@linkplain MosaicImageReadParam#setImageTypePolicy set in read parameters}.
* <p>
* The default implementation makes the following choice based on the number of
* {@linkplain #getTileReaderSpis reader providers}:
* <ul>
* <li>{@link ImageTypePolicy#SUPPORTED_BY_ALL SUPPORTED_BY_ALL} if two or more</li>
* <li>{@link ImageTypePolicy#SUPPORTED_BY_ONE SUPPORTED_BY_ONE} if exactly one</li>
* <li>{@link ImageTypePolicy#ALWAYS_ARGB ALWAYS_ARGB} if none.</li>
* </ul>
* <p>
* Note that {@link ImageTypePolicy#SUPPORTED_BY_ONE SUPPORTED_BY_ONE} is <strong>not</strong>
* a really safe choice even if there is only one provider, because the image type can also
* depends on {@linkplain Tile#getInput tile input}. However the safest choice in all cases
* ({@link ImageTypePolicy#SUPPORTED_BY_ALL SUPPORTED_BY_ALL}) is costly and often not
* necessary. The current implementation is a compromize between safety and performance.
* <p>
* If Java assertions are enabled, this reader will verify that {@code SUPPORTED_BY_ONE}
* produces the same result than {@code SUPPORTED_BY_ALL}.
* <p>
* Subclasses can override this method if they want a different policy.
*
* @return The default image type policy.
*/
public ImageTypePolicy getDefaultImageTypePolicy() {
switch (providers.size()) {
default: return ImageTypePolicy.SUPPORTED_BY_ALL;
case 1: return ImageTypePolicy.SUPPORTED_BY_ONE;
case 0: return ImageTypePolicy.ALWAYS_ARGB;
}
}
/**
* Returns type image type specifier for policy of pre-defined types.
* More types may be added in future GeoTools versions.
*/
private static ImageTypeSpecifier getPredefinedImageType(final ImageTypePolicy policy) {
final int type;
switch (policy) {
case ALWAYS_ARGB: type = BufferedImage.TYPE_INT_ARGB; break;
default: throw new IllegalArgumentException(policy.toString());
}
return ImageTypeSpecifier.createFromBufferedImageType(type);
}
/**
* Returns an image type which most closely represents the "raw" internal format of the image.
* The default implementation depends on the {@linkplain #getDefaultImageTypePolicy default
* image type policy}:
* <ul>
* <li>For {@link ImageTypePolicy#SUPPORTED_BY_ONE SUPPORTED_BY_ONE}, this method delegates
* directly to the reader of an arbitrary tile (typically the first one).</li>
* <li>For {@link ImageTypePolicy#SUPPORTED_BY_ALL SUPPORTED_BY_ALL}, this method invokes
* {@code getRawImageType} for every tile readers, ommits the types that are not declared
* in <code>{@linkplain ImageReader#getImageTypes getImageTypes}(imageIndex)</code> for
* every tile readers, and returns the most common remainding value. If none is found,
* then some {@linkplain ImageReader#getRawImageType default specifier} is returned.</li>
* </ul>
*
* @param imageIndex The image index, from 0 inclusive to {@link #getNumImages} exclusive.
* @return A raw image type specifier.
* @throws IOException If an error occurs reading the information from the input source.
*/
@Override
public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
ImageTypeSpecifier type;
final ImageTypePolicy policy = getDefaultImageTypePolicy();
switch (policy) {
default: {
type = getPredefinedImageType(policy);
break;
}
case SUPPORTED_BY_ONE: {
final Collection<Tile> tiles = getTileManager(imageIndex).getTiles();
final Tile tile = getSpecificTile(tiles);
if (tile != null) {
type = tile.getImageReader(this, true, true).getRawImageType(imageIndex);
assert type.equals(getRawImageType(tiles)) : incompatibleImageType(tile);
} else {
type = super.getRawImageType(imageIndex);
}
break;
}
case SUPPORTED_BY_ALL: {
final Collection<Tile> tiles = getTileManager(imageIndex).getTiles();
type = getRawImageType(tiles);
if (type == null) {
type = super.getRawImageType(imageIndex);
}
break;
}
}
return type;
}
/**
* Returns an image type which most closely represents the "raw" internal format of the
* given set of tiles. If none is found, returns {@code null}.
* <p>
* If there is more than one supported types, this method will give preference to the type
* having transparency. We do that because we have no garantee that a tile exists for every
* area in an image to be read, and the empty area typically need to remain transparent.
*
* @param tiles The tiles to iterate over.
* @return A raw image type specifier acceptable for all tiles, or {@code null} if none.
* @throws IOException If an error occurs reading the information from the input source.
*/
private ImageTypeSpecifier getRawImageType(final Collection<Tile> tiles) throws IOException {
// Gets the list of every raw image types, with the most frequent type first.
final Set<ImageTypeSpecifier> rawTypes = new FrequencySortedSet<ImageTypeSpecifier>(true);
final Set<ImageTypeSpecifier> allowed = getImageTypes(tiles, rawTypes);
rawTypes.retainAll(allowed);
boolean transparent = true;
do {
Iterator<ImageTypeSpecifier> it = rawTypes.iterator();
while (it.hasNext()) {
final ImageTypeSpecifier type = it.next();
if (!transparent || isTransparent(type)) {
return type;
}
}
// No raw image reader type. Returns the first allowed type even if it is not "raw".
it = allowed.iterator();
while (it.hasNext()) {
final ImageTypeSpecifier type = it.next();
if (!transparent || isTransparent(type)) {
return type;
}
}
// If no type was found and if we were looking for a transparent
// type, searchs again for a type no matter its transparency.
} while ((transparent = !transparent) == false);
return null;
}
/**
* Returns the possible image types to which the given image may be decoded. This method
* invokes <code>{@linkplain ImageReader#getImageTypes getImageTypes}(imageIndex)</code>
* on every tile readers and returns the intersection of all sets (i.e. only the types
* that are supported by every readers).
*
* @param tiles The tiles to iterate over.
* @param rawTypes If non-null, a collection where to store the raw image types.
* No filtering is applied on this collection.
* @return The image type specifiers that are common to all tiles.
* @throws IOException If an error occurs reading the information from the input source.
*/
private Set<ImageTypeSpecifier> getImageTypes(final Collection<Tile> tiles,
final Collection<ImageTypeSpecifier> rawTypes)
throws IOException
{
int pass = 0;
final Map<ImageTypeSpecifier,Integer> types = new LinkedHashMap<ImageTypeSpecifier,Integer>();
for (final Tile tile : tiles) {
final ImageReader reader = tile.getImageReader(this, true, true);
final int imageIndex = tile.getImageIndex();
if (rawTypes != null) {
rawTypes.add(reader.getRawImageType(imageIndex));
}
final Iterator<ImageTypeSpecifier> toAdd = reader.getImageTypes(imageIndex);
while (toAdd.hasNext()) {
final ImageTypeSpecifier type = toAdd.next();
final Integer old = types.put(type, pass);
if (old == null && pass != 0) {
// Just added a type that did not exists in previous tiles, so remove it.
types.remove(type);
}
}
// Remove all previous types not found in this pass.
for (final Iterator<Integer> it=types.values().iterator(); it.hasNext();) {
if (it.next().intValue() != pass) {
it.remove();
}
}
pass++;
}
return types.keySet();
}
/**
* Returns possible image types to which the given image may be decoded. The default
* implementation depends on the {@linkplain #getDefaultImageTypePolicy default image
* type policy}:
* <ul>
* <li>For {@link ImageTypePolicy#SUPPORTED_BY_ONE SUPPORTED_BY_ONE}, this method delegates
* directly to the reader of an arbitrary tile (typically the first one).</li>
* <li>For {@link ImageTypePolicy#SUPPORTED_BY_ALL SUPPORTED_BY_ALL}, this method invokes
* <code>{@linkplain ImageReader#getImageTypes getImageTypes}(imageIndex)</code> on
* every tile readers and returns the intersection of all sets (i.e. only the types
* that are supported by every readers).</li>
* </ul>
*
* @param imageIndex The image index, from 0 inclusive to {@link #getNumImages} exclusive.
* @return The image type specifiers that are common to all tiles.
* @throws IOException If an error occurs reading the information from the input source.
*/
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
Iterator<ImageTypeSpecifier> types;
final ImageTypePolicy policy = getDefaultImageTypePolicy();
switch (policy) {
default: {
types = Collections.singleton(getPredefinedImageType(policy)).iterator();
break;
}
case SUPPORTED_BY_ONE: {
final Collection<Tile> tiles = getTileManager(imageIndex).getTiles();
final Tile tile = getSpecificTile(tiles);
if (tile == null) {
final Collection<ImageTypeSpecifier> t = Collections.emptySet();
return t.iterator();
}
types = tile.getImageReader(this, true, true).getImageTypes(imageIndex);
assert (types = containsAll(getImageTypes(tiles, null), types)) != null : incompatibleImageType(tile);
break;
}
case SUPPORTED_BY_ALL: {
final Collection<Tile> tiles = getTileManager(imageIndex).getTiles();
types = getImageTypes(tiles, null).iterator();
break;
}
}
return types;
}
/**
* Helper method for assertions only.
*/
private static Iterator<ImageTypeSpecifier> containsAll(
final Collection<ImageTypeSpecifier> expected, final Iterator<ImageTypeSpecifier> types)
{
final List<ImageTypeSpecifier> asList = new ArrayList<ImageTypeSpecifier>(expected.size());
while (types.hasNext()) {
asList.add(types.next());
}
return expected.containsAll(asList) ? asList.iterator() : null;
}
/**
* Returns {@code true} if the given type has transparency.
*/
private static boolean isTransparent(final ImageTypeSpecifier type) {
return type.getColorModel().getTransparency() != ColorModel.OPAQUE;
}
/**
* Helper method for assertions only.
*/
private static String incompatibleImageType(final Tile tile) {
return "Image type computed by " + ImageTypePolicy.SUPPORTED_BY_ONE +
" policy using " + tile + " is incompatible with type computed by " +
ImageTypePolicy.SUPPORTED_BY_ALL + " policy.";
}
/**
* Returns default parameters appropriate for this format.
*/
@Override
public MosaicImageReadParam getDefaultReadParam() {
return new MosaicImageReadParam(this);
}
/**
* Returns the metadata associated with the input source as a whole, or {@code null}.
* The default implementation tries to {@linkplain IIOMetadata#mergeTree merge} the
* metadata from every tiles.
*
* @throws IOException if an error occurs during reading.
*/
public IIOMetadata getStreamMetadata() throws IOException {
IIOMetadata metadata = null;
if (input instanceof TileManager[]) {
final Set<ReaderInputPair> done = new HashSet<ReaderInputPair>();
for (final TileManager manager : (TileManager[]) input) {
for (final Tile tile : manager.getTiles()) {
final ImageReader reader = tile.getImageReader(this, true, ignoreMetadata);
final Object input = reader.getInput();
if (done.add(new ReaderInputPair(reader, input))) {
final IIOMetadata candidate = reader.getStreamMetadata();
metadata = MetadataMerge.merge(candidate, metadata);
}
}
}
}
return metadata;
}
/**
* Returns the stream metadata for the given format and nodes, or {@code null}.
* The default implementation tries to {@linkplain IIOMetadata#mergeTree merge}
* the metadata from every tiles.
*
* @throws IOException if an error occurs during reading.
*/
@Override
public IIOMetadata getStreamMetadata(final String formatName, final Set<String> nodeNames)
throws IOException
{
IIOMetadata metadata = null;
if (input instanceof TileManager[]) {
final Set<ReaderInputPair> done = new HashSet<ReaderInputPair>();
for (final TileManager manager : (TileManager[]) input) {
for (final Tile tile : manager.getTiles()) {
final ImageReader reader = tile.getImageReader(this, true, ignoreMetadata);
final Object input = reader.getInput();
if (done.add(new ReaderInputPair(reader, input))) {
final IIOMetadata candidate = reader.getStreamMetadata(formatName, nodeNames);
metadata = MetadataMerge.merge(candidate, metadata);
}
}
}
}
return metadata;
}
/**
* Returns the metadata associated with the given image, or {@code null}. The
* default implementation tries to {@linkplain IIOMetadata#mergeTree merge}
* the metadata from every tiles.
*
* @param imageIndex the index of the image whose metadata is to be retrieved.
* @return The metadata, or {@code null}.
* @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 during reading.
*/
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
IIOMetadata metadata = null;
for (final Tile tile : getTileManager(imageIndex).getTiles()) {
final ImageReader reader = tile.getImageReader(this, true, ignoreMetadata);
final IIOMetadata candidate = reader.getImageMetadata(tile.getImageIndex());
metadata = MetadataMerge.merge(candidate, metadata);
}
return metadata;
}
/**
* Returns the image metadata for the given format and nodes, or {@code null}.
* The default implementation tries to {@linkplain IIOMetadata#mergeTree merge}
* the metadata from every tiles.
*
* @throws IOException if an error occurs during reading.
*/
@Override
public IIOMetadata getImageMetadata(final int imageIndex,
final String formatName, final Set<String> nodeNames) throws IOException
{
IIOMetadata metadata = null;
for (final Tile tile : getTileManager(imageIndex).getTiles()) {
final ImageReader reader = tile.getImageReader(this, true, ignoreMetadata);
final IIOMetadata candidate = reader.getImageMetadata(tile.getImageIndex(), formatName, nodeNames);
metadata = MetadataMerge.merge(candidate, metadata);
}
return metadata;
}
/**
* Reads the image indexed by {@code imageIndex} using a supplied parameters.
* <strong>See {@link MosaicImageReadParam} for a performance recommandation</strong>.
* If the parameters allow subsampling changes, then the subsampling effectively used
* will be written back in the given parameters.
*
* @param imageIndex The index of the image to be retrieved.
* @param param The parameters used to control the reading process, or {@code null}.
* An instance of {@link MosaicImageReadParam} is expected but not required.
* @return The desired portion of the image.
* @throws IOException if an error occurs during reading.
*/
public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
clearAbortRequest();
processImageStarted(imageIndex);
final Dimension subsampling = new Dimension(1,1);
boolean subsamplingChangeAllowed = false;
MosaicImageReadParam mosaicParam = null;
boolean nullForEmptyImage = false;
if (param != null) {
subsampling.width = param.getSourceXSubsampling();
subsampling.height = param.getSourceYSubsampling();
if (param instanceof MosaicImageReadParam) {
mosaicParam = (MosaicImageReadParam) param;
subsamplingChangeAllowed = mosaicParam.isSubsamplingChangeAllowed();
nullForEmptyImage = mosaicParam.getNullForEmptyImage();
}
// Note: we don't extract subsampling offsets because they will be taken in account
// in the 'sourceRegion' to be calculated by ImageReader.computeRegions(...).
}
final int srcWidth = getWidth (imageIndex);
final int srcHeight = getHeight(imageIndex);
final Rectangle sourceRegion = getSourceRegion(param, srcWidth, srcHeight);
final Collection<Tile> tiles = getTileManager(imageIndex)
.getTiles(sourceRegion, subsampling, subsamplingChangeAllowed);
if (nullForEmptyImage && tiles.isEmpty()) {
processImageComplete();
return null;
}
/*
* If the subsampling changed as a result of TileManager.getTiles(...) call,
* stores the new subsampling values in the parameters. Note that the source
* region will need to be computed again, which we will do later.
*/
final int xSubsampling = subsampling.width;
final int ySubsampling = subsampling.height;
if (subsamplingChangeAllowed) {
if (param.getSourceXSubsampling() != xSubsampling ||
param.getSourceYSubsampling() != ySubsampling)
{
final int xOffset = param.getSubsamplingXOffset() % xSubsampling;
final int yOffset = param.getSubsamplingYOffset() % ySubsampling;
param.setSourceSubsampling(xSubsampling, ySubsampling, xOffset, yOffset);
} else {
subsamplingChangeAllowed = false;
}
}
/*
* If there is exactly one image to read, we will left the image reference to null. It will
* be understood later as an indication to delegate directly to the sole image reader as an
* optimization (no search for raw data type). Otherwise, we need to create the destination
* image here. Note that this is the only image ever to be created during a mosaic read,
* unless some underlying ImageReader do not honor our ImageReadParam.setDestination(image)
* setting. In such case, the default behavior is to thrown an exception.
*/
BufferedImage image = null;
final Rectangle destRegion;
final Point destinationOffset;
ImageTypePolicy policy = null;
if (canDelegate(tiles, sourceRegion) && (policy = getImageTypePolicy(param)).canDelegate) {
destRegion = null;
if (subsamplingChangeAllowed) {
sourceRegion.setBounds(getSourceRegion(param, srcWidth, srcHeight));
}
destinationOffset = (param != null) ? param.getDestinationOffset() : new Point();
} else {
if (param != null) {
image = param.getDestination();
}
destRegion = new Rectangle(); // Computed by the following method call.
computeRegions(param, srcWidth, srcHeight, image, sourceRegion, destRegion);
if (image == null) {
/*
* If no image was explicitly specified, creates one using a raw image type
* acceptable for all tiles. An exception will be thrown if no such raw type
* was found. Note that this fallback may be a little bit costly since it may
* imply to open, close and reopen later some streams.
*/
ImageTypeSpecifier imageType = null;
if (param != null) {
imageType = param.getDestinationType();
}
if (imageType == null) {
if (policy == null) {
policy = getImageTypePolicy(param);
}
switch (policy) {
default: {
imageType = getPredefinedImageType(policy);
break;
}
case SUPPORTED_BY_ONE: {
final Tile tile = getSpecificTile(tiles);
if (tile != null) {
imageType = tile.getImageReader(this, true, true).getRawImageType(imageIndex);
assert imageType.equals(getRawImageType(tiles)) : incompatibleImageType(tile);
}
break;
}
case SUPPORTED_BY_ALL: {
imageType = getRawImageType(tiles);
break;
}
}
if (imageType == null) {
/*
* This case occurs if the tiles collection is empty. We want to produce
* a fully transparent (or empty) image in such case. Remember that tiles
* are not required to exist everywhere in the mosaic bounds, so the set
* of tiles in a particular sub-area is allowed to be empty.
*/
imageType = getRawImageType(imageIndex);
}
}
final int width = destRegion.x + destRegion.width;
final int height = destRegion.y + destRegion.height;
image = imageType.createBufferedImage(width, height);
computeRegions(param, srcWidth, srcHeight, image, sourceRegion, destRegion);
}
destinationOffset = destRegion.getLocation();
}
/*
* Gets a MosaicImageReadParam instance to be used for caching Tile parameters. There is
* no need to invokes 'getDefaultReadParam()' since we are interrested only in the cache
* that MosaicImageReadParam provide.
*/
MosaicController controller = null;
if (mosaicParam == null) {
mosaicParam = new MosaicImageReadParam();
} else if (mosaicParam.hasController()) {
final IIOParamController candidate = mosaicParam.getController();
if (candidate instanceof MosaicController) {
controller = (MosaicController) candidate;
}
}
/*
* If logging are enabled, we will format the tiles that we read in a table and logs
* the table as one log record before the actual reading. If there is nothing to log,
* then the table will be left to null. If non-null, the table will be completed in
* the 'do' loop below.
*/
final Logger logger = Logging.getLogger(MosaicImageReader.class);
TableWriter table = null;
if (logger.isLoggable(level)) {
table = new TableWriter(null, TableWriter.SINGLE_VERTICAL_LINE);
table.writeHorizontalSeparator();
table.write("Reader\tTile\tIndex\tSize\tSource\tDestination\tSubsampling");
table.writeHorizontalSeparator();
}
/*
* Now read every tiles... If logging is disabled, then this loop will be executed exactly
* once. If logging is enabled, then this loop will be executed twice where the first pass
* is used only in order to format the table to be logged. In every cases, the last pass is
* the one where the actual reading occur. We do this two pass approach in order to get the
* table logged before loading rather than after. This is more useful in case of exception.
*/
do {
for (final Tile tile : tiles) {
if (abortRequested()) {
processReadAborted();
break;
}
final Rectangle tileRegion = tile.getAbsoluteRegion();
final Rectangle regionToRead = tileRegion.intersection(sourceRegion);
/*
* Computes the location of the region to read relative to the source region
* requested by the user, and make sure that this location is a multiple of
* subsampling (if any). The region to read may become bigger by one pixel
* (in tile units) as a result of this calculation.
*/
int xOffset = (regionToRead.x - sourceRegion.x) % xSubsampling;
int yOffset = (regionToRead.y - sourceRegion.y) % ySubsampling;
if (xOffset != 0) {
regionToRead.x -= xOffset;
regionToRead.width += xOffset;
if (regionToRead.x < tileRegion.x) {
regionToRead.x = tileRegion.x;
if (regionToRead.width > tileRegion.width) {
regionToRead.width = tileRegion.width;
}
}
}
if (yOffset != 0) {
regionToRead.y -= yOffset;
regionToRead.height += yOffset;
if (regionToRead.y < tileRegion.y) {
regionToRead.y = tileRegion.y;
if (regionToRead.height > tileRegion.height) {
regionToRead.height = tileRegion.height;
}
}
}
if (regionToRead.isEmpty()) {
continue;
}
/*
* Now that the offset is a multiple of subsampling, computes the destination offset.
* Then translate the region to read from "this image reader" space to "tile" space.
*/
if (destRegion != null) {
xOffset = (regionToRead.x - sourceRegion.x) / xSubsampling;
yOffset = (regionToRead.y - sourceRegion.y) / ySubsampling;
destinationOffset.x = destRegion.x + xOffset;
destinationOffset.y = destRegion.y + yOffset;
}
assert tileRegion.contains(regionToRead) : regionToRead;
regionToRead.translate(-tileRegion.x, -tileRegion.y);
/*
* Sets the parameters to be given to the tile reader. We don't use any subsampling
* offset because it has already been calculated in the region to read. Note that
* the tile subsampling should be a divisor of image subsampling; this condition must
* have been checked by the tile manager when it selected the tiles to be returned.
*/
subsampling.setSize(tile.getSubsampling());
assert xSubsampling % subsampling.width == 0 : subsampling;
assert ySubsampling % subsampling.height == 0 : subsampling;
/*
* Transform the region to read from "absolute" coordinates to "relative to tile"
* coordinates. We want to round x and y toward negative infinity, which require
* special processing for negative numbers since integer arithmetic round toward
* zero. The xOffset and yOffset values are the remainding of the division which
* will be added to the width and height in order to get (xmax, ymax) unchanged.
*/
xOffset = regionToRead.x % subsampling.width;
yOffset = regionToRead.y % subsampling.height;
regionToRead.x /= subsampling.width;
regionToRead.y /= subsampling.height;
if (xOffset < 0) {
regionToRead.x--;
xOffset = subsampling.width - xOffset;
}
if (yOffset < 0) {
regionToRead.y--;
yOffset = subsampling.height - yOffset;
}
regionToRead.width += xOffset;
regionToRead.height += yOffset;
regionToRead.width /= subsampling.width;
regionToRead.height /= subsampling.height;
subsampling.width = xSubsampling / subsampling.width;
subsampling.height = ySubsampling / subsampling.height;
final int tileIndex = tile.getImageIndex();
if (table != null) {
/*
* We are only logging - we are not going to read in this first pass.
*/
table.write(Tile.toString(tile.getImageReaderSpi()));
table.nextColumn();
table.write(tile.getInputName());
table.nextColumn();
table.write(String.valueOf(tileIndex));
format(table, regionToRead.width, regionToRead.height);
format(table, regionToRead.x, regionToRead.y);
format(table, destinationOffset.x, destinationOffset.y);
format(table, subsampling.width, subsampling.height);
table.nextLine();
continue;
}
final ImageReader reader = tile.getImageReader(this, true, true);
final ImageReadParam tileParam = mosaicParam.getCachedTileParameters(reader);
tileParam.setDestinationType(null);
tileParam.setDestination(image); // Must be after setDestinationType and may be null.
tileParam.setDestinationOffset(destinationOffset);
if (tileParam.canSetSourceRenderSize()) {
tileParam.setSourceRenderSize(null); // TODO.
}
tileParam.setSourceRegion(regionToRead);
tileParam.setSourceSubsampling(subsampling.width, subsampling.height, 0, 0);
if (controller != null) {
controller.configure(tile, tileParam);
}
final BufferedImage output;
synchronized (this) { // Same lock than ImageReader.abort()
reading = reader;
}
try {
output = reader.read(tileIndex, tileParam);
} finally {
synchronized (this) { // Same lock than ImageReader.abort()
reading = null;
}
}
if (image == null) {
image = output;
} else if (output != image) {
/*
* The read operation ignored our destination image. By default we treat that
* as an error since the SampleModel may be incompatible and changing it would
* break the geophysics meaning of pixel values. However if we are interrested
* only in the visual aspect, we can copy the data (slow, consumes memory) and
* let Java2D performs the required color conversions. Note that it should not
* occur anyway if we choose correctly the raw image type in the code above.
*/
if (PRESERVE_DATA) {
throw new IIOException("Incompatible data format."); // TODO: localize
}
final AffineTransform at = AffineTransform.getTranslateInstance(
destinationOffset.x, destinationOffset.y);
final Graphics2D graphics = image.createGraphics();
graphics.drawRenderedImage(output, at);
graphics.dispose();
}
}
/*
* Finished a pass. If it was the reading pass, then we are done. If it was the logging
* pass, then send the log and redo the look a second time for the actual reading.
*/
if (table == null) {
break;
}
table.writeHorizontalSeparator();
final StringBuilder message = new StringBuilder();
message.append('[').append(sourceRegion.x).append(',').append(sourceRegion.y).
append(" - ").append(sourceRegion.x + sourceRegion.width).append(',').
append(sourceRegion.y + sourceRegion.height).append(']');
final String area = message.toString();
message.setLength(0);
message.append(Vocabulary.format(VocabularyKeys.LOADING_$1, area)).
append(System.getProperty("line.separator", "\n")).append(table);
final LogRecord record = new LogRecord(level, message.toString());
record.setSourceClassName(MosaicImageReader.class.getName());
record.setSourceMethodName("read");
record.setLoggerName(logger.getName());
logger.log(record);
table = null;
} while (true);
processImageComplete();
return image;
}
/**
* Reads the tile indicated by the {@code tileX} and {@code tileY} arguments.
*
* @param imageIndex The index of the image to be retrieved.
* @param tileX The column index (starting with 0) of the tile to be retrieved.
* @param tileY The row index (starting with 0) of the tile to be retrieved.
* @return The desired tile.
* @throws IOException if an error occurs during reading.
*/
@Override
public BufferedImage readTile(final int imageIndex, final int tileX, final int tileY)
throws IOException
{
final int width = getTileWidth (imageIndex);
final int height = getTileHeight(imageIndex);
final Rectangle sourceRegion = new Rectangle(tileX*width, tileY*height, width, height);
final ImageReadParam param = getDefaultReadParam();
param.setSourceRegion(sourceRegion);
return read(imageIndex, param);
}
/**
* Formats a (x,y) value pair. A call to {@link TableWriter#nextColumn} is performed first.
*/
private static void format(final TableWriter table, final int x, final int y) {
table.nextColumn();
table.write('(');
table.write(String.valueOf(x));
table.write(',');
table.write(String.valueOf(y));
table.write(')');
}
/**
* Requests that any current read operation be aborted.
*/
@Override
public synchronized void abort() {
super.abort();
if (reading != null) {
reading.abort();
}
}
/**
* Returns the raw input (<strong>not</strong> wrapped in an image input stream) for the
* given reader. This method is invoked by {@link Tile#getImageReader} only.
*/
final Object getRawInput(final ImageReader reader) {
return readerInputs.get(reader);
}
/**
* Sets the raw input (<strong>not</strong> wrapped in an image input stream) for the
* given reader. The input can be set to {@code null}. This method is invoked by
* {@link Tile#getImageReader} only.
*/
final void setRawInput(final ImageReader reader, final Object input) {
readerInputs.put(reader, input);
}
/**
* Closes any image input streams thay may be held by tiles.
* The streams will be opened again when they will be first needed.
*
* @throws IOException if error occured while closing a stream.
*/
public void close() throws IOException {
for (final Map.Entry<ImageReader,Object> entry : readerInputs.entrySet()) {
final ImageReader reader = entry.getKey();
final Object rawInput = entry.getValue();
final Object input = reader.getInput();
entry .setValue(null);
reader.setInput(null);
if (input != rawInput) {
Tile.close(input);
}
}
}
/**
* Allows any resources held by this reader to be released. The default implementation
* closes any image input streams thay may be held by tiles, then disposes every
* {@linkplain Tile#getImageReader tile image readers}.
*/
@Override
public void dispose() {
input = null;
try {
close();
} catch (IOException e) {
Logging.unexpectedException(MosaicImageReader.class, "dispose", e);
}
readerInputs.clear();
for (final ImageReader reader : readers.values()) {
reader.dispose();
}
readers.clear();
super.dispose();
}
/**
* Service provider for {@link MosaicImageReader}.
*
* @since 2.5
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
public static class Spi extends ImageReaderSpi {
/**
* The format names. This array is shared with {@link MosaicImageWriter.Spi}.
*/
static final String[] NAMES = new String[] {
"mosaic"
};
/**
* The input types. This array is shared with {@link MosaicImageWriter.Spi}.
*/
static final Class<?>[] INPUT_TYPES = new Class[] {
TileManager[].class,
TileManager.class,
Tile[].class,
Collection.class
};
/**
* The default instance.
*/
public static final Spi DEFAULT = new Spi();
/**
* Creates a default provider.
*/
public Spi() {
vendorName = "GeoTools";
version = GeoTools.getVersion().toString();
names = NAMES;
inputTypes = INPUT_TYPES;
pluginClassName = "org.geotools.image.io.mosaic.MosaicImageReader";
}
/**
* Returns {@code true} if the image reader can decode the given input. The default
* implementation returns {@code true} if the given object is non-null and an instance
* of an {@linkplain #inputTypes input types}, or {@code false} otherwise.
*
* @throws IOException If an I/O operation was required and failed.
*/
public boolean canDecodeInput(final Object source) throws IOException {
if (source != null) {
final Class<?> type = source.getClass();
for (final Class<?> inputType : inputTypes) {
if (inputType.isAssignableFrom(type)) {
return true;
}
}
}
return false;
}
/**
* Returns a new {@link MosaicImageReader}.
*
* @throws IOException If an I/O operation was required and failed.
*/
public ImageReader createReaderInstance(final Object extension) throws IOException {
return new MosaicImageReader(this);
}
/**
* Returns a brief, human-readable description of this service provider.
*
* @todo Localize.
*/
public String getDescription(final Locale locale) {
return "Mosaic Image Reader";
}
}
}