/*
* 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;
import java.util.Set;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import java.awt.Rectangle;
import java.awt.image.ColorModel; // For javadoc
import java.awt.image.DataBuffer;
import java.awt.image.SampleModel; // For javadoc
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel; // For javadoc
import java.io.IOException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.event.IIOReadWarningListener; // For javadoc
import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Locales;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.IndexedResourceBundle;
import org.geotools.image.io.metadata.GeographicMetadata;
import org.geotools.image.io.metadata.Band;
/**
* Base class for readers of geographic images. The default implementation assumes that only one
* {@linkplain ImageTypeSpecifier image type} is supported (as opposed to the arbitrary number
* allowed by the standard {@link ImageReader}). It also provides a default image type built
* automatically from a color palette and a range of valid values.
* <p>
* More specifically, this class provides the following conveniences to implementors:
*
* <ul>
* <li><p>Provides default {@link #getNumImages} and {@link #getNumBands} implementations,
* which return 1. This default behavior matches simple image formats like flat binary
* files or ASCII files. Those methods need to be overrided for more complex image
* formats.</p></li>
*
* <li><p>Provides {@link #checkImageIndex} and {@link #checkBandIndex} convenience methods.
* Those methods are invoked by most implementation of public methods. They perform their
* checks based on the informations provided by the above-cited {@link #getNumImages} and
* {@link #getNumBands} methods.</p></li>
*
* <li><p>Provides default implementations of {@link #getImageTypes} and {@link #getRawImageType},
* which assume that only one {@linkplain ImageTypeSpecifier image type} is supported. The
* default image type is created from the informations provided by {@link #getRawDataType}
* and {@link #getImageMetadata}.</p></li>
*
* <li><p>Provides {@link #getStreamMetadata} and {@link #getImageMetadata} default
* implementations, which return {@code null} as authorized by the specification.
* Note that subclasses should consider returning {@link GeographicMetadata} instances.</p></li>
* </ul>
*
* Images may be flat binary or ASCII files with no meta-data and no color information.
* Their pixel values may be floating point values instead of integers. The default
* implementation assumes floating point values and uses a grayscale color space scaled
* to fit the range of values. Displaying such an image may be very slow. Consequently,
* users who want to display image are encouraged to change data type and color space with
* <a href="http://java.sun.com/products/java-media/jai/">Java Advanced Imaging</a>
* operators after reading.
*
* @since 2.4
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
public abstract class GeographicImageReader extends ImageReader {
/**
* The logger to use for events related to this image reader.
*/
static final Logger LOGGER = Logging.getLogger("org.geotools.image.io");
/**
* Metadata for each images, or {@code null} if not yet created.
*/
private transient GeographicMetadata[] metadata;
/**
* Constructs a new image reader.
*
* @param provider The {@link ImageReaderSpi} that is invoking this constructor,
* or {@code null} if none.
*/
protected GeographicImageReader(final ImageReaderSpi provider) {
super(provider);
availableLocales = Locales.getAvailableLocales();
}
/**
* Sets the input source to use.
*
* @param input The input object to use for future decoding.
* @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.
*/
@Override
public void setInput(final Object input,
final boolean seekForwardOnly,
final boolean ignoreMetadata)
{
metadata = null; // Clears the cache
super.setInput(input, seekForwardOnly, ignoreMetadata);
}
/**
* Returns the resources for formatting error messages.
*/
final IndexedResourceBundle getErrorResources() {
return Errors.getResources(getLocale());
}
/**
* Ensures that the specified image index is inside the expected range.
* The expected range is {@link #minIndex minIndex} inclusive (initially 0)
* to <code>{@link #getNumImages getNumImages}(false)</code> exclusive.
*
* @param imageIndex Index to check for validity.
* @throws IndexOutOfBoundsException if the specified index is outside the expected range.
* @throws IOException If the operation failed because of an I/O error.
*/
protected void checkImageIndex(final int imageIndex)
throws IOException, IndexOutOfBoundsException
{
final int numImages = getNumImages(false);
if (imageIndex < minIndex || (imageIndex >= numImages && numImages >= 0)) {
throw new IndexOutOfBoundsException(indexOutOfBounds(imageIndex, minIndex, numImages));
}
}
/**
* Ensures that the specified band index is inside the expected range. The expected
* range is 0 inclusive to <code>{@link #getNumBands getNumBands}(imageIndex)</code>
* exclusive.
*
* @param imageIndex The image index.
* @param bandIndex Index to check for validity.
* @throws IndexOutOfBoundsException if the specified index is outside the expected range.
* @throws IOException If the operation failed because of an I/O error.
*/
protected void checkBandIndex(final int imageIndex, final int bandIndex)
throws IOException, IndexOutOfBoundsException
{
// Call 'getNumBands' first in order to call 'checkImageIndex'.
final int numBands = getNumBands(imageIndex);
if (bandIndex >= numBands || bandIndex < 0) {
throw new IndexOutOfBoundsException(indexOutOfBounds(bandIndex, 0, numBands));
}
}
/**
* Formats an error message for an index out of bounds.
*
* @param index The index out of bounds.
* @param lower The lower legal value, inclusive.
* @param upper The upper legal value, exclusive.
*/
private String indexOutOfBounds(final int index, final int lower, final int upper) {
return getErrorResources().getString(ErrorKeys.VALUE_OUT_OF_BOUNDS_$3, index, lower, upper-1);
}
/**
* Returns the number of images available from the current input source.
* The default implementation returns 1.
*
* @param allowSearch If true, the number of images will be returned
* even if a search is required.
* @return The number of images, or -1 if {@code allowSearch}
* is false and a search would be required.
*
* @throws IllegalStateException if the input source has not been set.
* @throws IOException if an error occurs reading the information from the input source.
*/
public int getNumImages(final boolean allowSearch) throws IllegalStateException, IOException {
if (input != null) {
return 1;
}
throw new IllegalStateException(getErrorResources().getString(ErrorKeys.NO_IMAGE_INPUT));
}
/**
* Returns the number of bands available for the specified image.
* The default implementation returns 1.
*
* @param imageIndex The image index.
* @throws IOException if an error occurs reading the information from the input source.
*/
public int getNumBands(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return 1;
}
/**
* Returns the number of dimension of the image at the given index.
* The default implementation always returns 2.
*
* @param imageIndex The image index.
* @return The number of dimension for the image at the given index.
* @throws IOException if an error occurs reading the information from the input source.
*
* @since 2.5
*/
public int getDimension(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return 2;
}
/**
* Returns metadata associated with the input source as a whole. Since many raw images
* can't store metadata, the default implementation returns {@code null}.
*
* @throws IOException if an error occurs during reading.
*/
public IIOMetadata getStreamMetadata() throws IOException {
return null;
}
/**
* Returns metadata associated with the given image. Since many raw images
* can't store metadata, the default implementation returns {@code null}.
*
* @param imageIndex The image index.
* @return The metadata, or {@code null} if none.
* @throws IOException if an error occurs during reading.
*/
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return null;
}
/**
* Returns a helper parser for metadata associated with the given image. This implementation
* invokes <code>{@linkplain #getImageMetadata getImageMetadata}(imageIndex)</code>, wraps
* the result in a {@link GeographicMetadata} object if non-null and caches the result.
* <p>
* Note that this method forces {@link #ignoreMetadata} to {@code false} for the time of
* <code>{@linkplain #getImageMetadata getImageMetadata}(imageIndex)</code> execution,
* because some image reader implementations need geographic metadata in order to infer
* a valid {@linkplain ColorModel color model}.
*
* @param imageIndex The image index.
* @return The geographic metadata, or {@code null} if none.
* @throws IOException if an error occurs during reading.
*/
public GeographicMetadata getGeographicMetadata(final int imageIndex) throws IOException {
// Checks if a cached instance is available.
if (metadata != null && imageIndex >= 0 && imageIndex < metadata.length) {
final GeographicMetadata parser = metadata[imageIndex];
if (parser != null) {
return parser;
}
}
// Checks if metadata are availables. If the user set 'ignoreMetadata' to 'true',
// we override his setting since we really need metadata for creating a ColorModel.
final IIOMetadata candidate;
final boolean oldIgnore = ignoreMetadata;
try {
ignoreMetadata = false;
candidate = getImageMetadata(imageIndex);
} finally {
ignoreMetadata = oldIgnore;
}
if (candidate == null) {
return null;
}
// Wraps the IIOMetadata into a GeographicMetadata object,
// if it was not already of the appropriate type.
final GeographicMetadata parser;
if (candidate instanceof GeographicMetadata) {
parser = (GeographicMetadata) candidate;
} else {
parser = new GeographicMetadata(this);
parser.mergeTree(candidate);
}
if (metadata == null) {
metadata = new GeographicMetadata[Math.max(imageIndex+1, 4)];
}
if (imageIndex >= metadata.length) {
metadata = XArray.resize(metadata, Math.max(imageIndex+1, metadata.length*2));
}
metadata[imageIndex] = parser;
return parser;
}
/**
* Returns a collection of {@link ImageTypeSpecifier} containing possible image types to which
* the given image may be decoded. The default implementation returns a singleton containing
* <code>{@link #getRawImageType(int) getRawImageType}(imageIndex)</code>.
*
* @param imageIndex The index of the image to be retrieved.
* @return A set of suggested image types for decoding the current given image.
* @throws IOException If an error occurs reading the format information from the input source.
*/
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
return Collections.singleton(getRawImageType(imageIndex)).iterator();
}
/**
* Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
* which most closely represents the "raw" internal format of the image. The default
* implementation delegates to the following:
*
* <blockquote><code>{@linkplain #getRawImageType(int,ImageReadParam,SampleConverter[])
* getRawImageType}(imageIndex, {@linkplain #getDefaultReadParam()}, null);</code></blockquote>
*
* If this method needs to be overriden, consider overriding the later instead.
*
* @param imageIndex The index of the image to be queried.
* @return The image type (never {@code null}).
* @throws IOException If an error occurs reading the format information from the input source.
*/
@Override
public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
return getRawImageType(imageIndex, getDefaultReadParam(), null);
}
/**
* Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
* which most closely represents the "raw" internal format of the image. The default
* implementation applies the following rules:
*
* <ol>
* <li><p>The {@linkplain Band#getValidRange range of expected values} and the
* {@linkplain Band#getNoDataValues no-data values} are extracted from the
* {@linkplain #getGeographicMetadata geographic metadata}, if any.</p></li>
*
* <li><p>If the given {@code parameters} argument is an instance of {@link GeographicImageReadParam},
* then the user-supplied {@linkplain GeographicImageReadParam#getPaletteName palette name}
* is fetched. Otherwise or if no palette name was explicitly set, then this method default
* to {@value org.geotools.image.io.GeographicImageReadParam#DEFAULT_PALETTE_NAME}. The
* palette name will be used in order to {@linkplain PaletteFactory#getColors read a
* predefined set of colors} (as RGB values) to be given to the
* {@linkplain IndexColorModel index color model}.</p></li>
*
* <li><p>If the {@linkplain #getRawDataType raw data type} is {@link DataBuffer#TYPE_FLOAT
* TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE TYPE_DOUBLE}, then this method builds
* a {@linkplain PaletteFactory#getContinuousPalette continuous palette} suitable for
* the range fetched at step 1. The data are assumed <cite>geophysics</cite> values
* rather than some packed values. Consequently, the {@linkplain SampleConverter sample
* converters} will replace no-data values by {@linkplain Float#NaN NaN} with no other
* changes.</p></li>
*
* <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a unsigned integer type
* like {@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT},
* then this method builds an {@linkplain PaletteFactory#getPalette indexed palette} (i.e. a
* palette backed by an {@linkplain IndexColorModel index color model}) with just the minimal
* {@linkplain IndexColorModel#getMapSize size} needed for containing fully the range and the
* no-data values fetched at step 1. The data are assumed <cite>packed</cite> values rather
* than geophysics values. Consequently, the {@linkplain SampleConverter sample converters}
* will be the {@linkplain SampleConverter#IDENTITY identity converter} except in the
* following cases:
* <ul>
* <li>The {@linkplain Band#getValidRange range of valid values} is outside the range
* allowed by the {@linkplain #getRawDataType raw data type} (e.g. the range of
* valid values contains negative integers). In this case, the sample converter
* will shift the values to a strictly positive range and replace no-data values
* by 0.</li>
* <li>At least one {@linkplain Band#getNoDataValues no-data value} is outside the range
* of values allowed by the {@linkplain #getRawDataType raw data type}. In this case,
* this method will try to only replace the no-data values by 0, without shifting
* the valid values if this shift can be avoided.</li>
* <li>At least one {@linkplain Band#getNoDataValues no-data value} is far away from the
* {@linkplain Band#getValidRange range of valid values} (for example 9999 while
* the range of valid values is [0..255]). The meaning of "far away" is determined
* by the {@link #collapseNoDataValues collapseNoDataValues} method.</li>
* </ul>
* </p></li>
*
* <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a signed integer
* type like {@link DataBuffer#TYPE_SHORT TYPE_SHORT}, then this method builds an
* {@linkplain PaletteFactory#getPalette indexed palette} with the maximal {@linkplain
* IndexColorModel#getMapSize size} supported by the raw data type (note that this is
* memory expensive - typically 256 kilobytes). Negative values will be stored in their
* two's complement binary form in order to fit in the range of positive integers
* supported by the {@linkplain IndexColorModel index color model}.</p></li>
* </ol>
*
* <h3>Overriding this method</h3>
* Subclasses may override this method when a constant color {@linkplain Palette palette} is
* wanted for all images in a series, for example for all <cite>Sea Surface Temperature</cite>
* (SST) from the same provider. A constant color palette facilitates the visual comparaison
* of different images at different time. The example below creates hard-coded objects:
*
* <blockquote><code>
* int minimum = -2000; // </code>minimal expected value<code><br>
* int maximum = +2300; // </code>maximal expected value<code><br>
* int fillValue = -9999; // </code>Value for missing data<code><br>
* String palette = "SST-Nasa";// </code>Named set of RGB colors<code><br>
* converters[0] = {@linkplain SampleConverter#createOffset(double,double)
* SampleConverter.createOffset}(1 - minimum, fillValue);<br>
* return {@linkplain PaletteFactory#getDefault()}.{@linkplain PaletteFactory#getPalettePadValueFirst
* getPalettePadValueFirst}(paletteName, maximum - minimum).{@linkplain Palette#getImageTypeSpecifier
* getImageTypeSpecifier}();
* </code></blockquote>
*
* @param imageIndex
* The index of the image to be queried.
* @param parameters
* The user-supplied parameters, or {@code null}. Note: we recommand to supply
* {@link #getDefaultReadParam} instead of {@code null} since subclasses may
* override the later with default values suitable to a particular format.
* @param converters
* If non-null, an array where to store the converters created by this method.
* Those converters should be used by <code>{@linkplain #read(int,ImageReadParam)
* read}(imageIndex, parameters)</code> implementations for converting the values
* read in the datafile to values acceptable for the underling {@linkplain
* ColorModel color model}.
* @return
* The image type (never {@code null}).
* @throws IOException
* If an error occurs while reading the format information from the input source.
*
* @see #getRawDataType
* @see #collapseNoDataValues
* @see #getDestination(int, ImageReadParam, int, int, SampleConverter[])
*/
protected ImageTypeSpecifier getRawImageType(final int imageIndex,
final ImageReadParam parameters,
final SampleConverter[] converters)
throws IOException
{
/*
* Gets the minimal and maximal values allowed for the target image type.
* Note that this is meanless for floating point types, so the values in
* that case are arbitrary.
*
* The only integer types that are signed are SHORT (not to be confused with
* USHORT) and INT. Other types like BYTE and USHORT are treated as unsigned.
*/
final boolean isFloat;
final long floor, ceil;
final int dataType = getRawDataType(imageIndex);
switch (dataType) {
case DataBuffer.TYPE_UNDEFINED: // Actually we don't really know what to do for this case...
case DataBuffer.TYPE_DOUBLE: // Fall through since we can treat this case as float.
case DataBuffer.TYPE_FLOAT: {
isFloat = true;
floor = Long.MIN_VALUE;
ceil = Long.MAX_VALUE;
break;
}
case DataBuffer.TYPE_INT: {
isFloat = false;
floor = Integer.MIN_VALUE;
ceil = Integer.MAX_VALUE;
break;
}
case DataBuffer.TYPE_SHORT: {
isFloat = false;
floor = Short.MIN_VALUE;
ceil = Short.MAX_VALUE;
break;
}
default: {
isFloat = false;
floor = 0;
ceil = (1L << DataBuffer.getDataTypeSize(dataType)) - 1;
break;
}
}
/*
* Extracts all informations we will need from the user-supplied parameters, if any.
*/
final String paletteName;
final int[] sourceBands;
final int[] targetBands;
final int visibleBand;
if (parameters != null) {
sourceBands = parameters.getSourceBands();
targetBands = parameters.getDestinationBands();
} else {
sourceBands = null;
targetBands = null;
}
if (parameters instanceof GeographicImageReadParam) {
final GeographicImageReadParam geoparam = (GeographicImageReadParam) parameters;
paletteName = geoparam.getNonNullPaletteName();
visibleBand = geoparam.getVisibleBand();
} else {
paletteName = GeographicImageReadParam.DEFAULT_PALETTE_NAME;
visibleBand = 0;
}
final int numBands;
if (sourceBands != null) {
numBands = sourceBands.length;
} else if (targetBands != null) {
numBands = targetBands.length;
} else {
numBands = getNumBands(imageIndex);
}
/*
* Computes a range of values for all bands, as the union in order to make sure that
* we can stores every sample values. Also creates SampleConverters in the process.
* The later is an opportunist action since we gather most of the needed information
* during the loop.
*/
NumberRange allRanges = null;
NumberRange visibleRange = null;
SampleConverter visibleConverter = SampleConverter.IDENTITY;
double maximumFillValue = 0; // Only in the visible band, and must be positive.
final GeographicMetadata metadata = getGeographicMetadata(imageIndex);
if (metadata != null) {
final int numMetadataBands = metadata.getNumBands();
for (int i=0; i<numBands; i++) {
final int sourceBand = (sourceBands != null) ? sourceBands[i] : i;
if (sourceBand < 0 || sourceBand >= numMetadataBands) {
if (numMetadataBands != 1) {
/*
* If the metadata declares excactly one band, we assume that it is aimed
* to applied for all ImageReader band. We need to apply this patch for
* now because of an inconsistency between Metadata and ImageReader in
* NetCDF files: the former associates NetCDF variables to bands, while
* the later associates NetCDF variables to image index.
*
* TODO: We need to fix this inconcistency after we reviewed
* GeographicMetadata.
*/
warningOccurred("getRawImageType", indexOutOfBounds(sourceBand, 0, numMetadataBands));
}
if (numMetadataBands == 0) {
break; // We are sure that next bands will not be better.
}
}
final Band band = metadata.getBand(Math.min(sourceBand, numMetadataBands-1));
final double[] nodataValues = band.getNoDataValues();
final NumberRange range = band.getValidRange();
double minimum, maximum;
if (range != null) {
minimum = range.getMinimum();
maximum = range.getMaximum();
if (!isFloat) {
// If the metadata do not contain any information about the range,
// treat as if we use the maximal range allowed by the data type.
if (minimum == Double.NEGATIVE_INFINITY) minimum = floor;
if (maximum == Double.POSITIVE_INFINITY) maximum = ceil;
}
final double extent = maximum - minimum;
if (extent >= 0 && (isFloat || extent <= (ceil - floor))) {
allRanges = (allRanges != null) ? allRanges.union(range) : range;
} else {
// Use range.getMin/MaxValue() because they may be integers rather than doubles.
warningOccurred("getRawImageType", Errors.format(ErrorKeys.BAD_RANGE_$2,
range.getMinValue(), range.getMaxValue()));
continue;
}
} else {
minimum = Double.NaN;
maximum = Double.NaN;
}
final int targetBand = (targetBands != null) ? targetBands[i] : i;
/*
* For floating point types, replaces no-data values by NaN because the floating
* point numbers are typically used for geophysics data, so the raster is likely
* to be a "geophysics" view for GridCoverage2D. All other values are stored "as
* is" without any offset.
*
* For integer types, if the range of values from the source data file fits into
* the range of values allowed by the destination raster, we will use an identity
* converter. If the only required conversion is a shift from negative to positive
* values, creates an offset converter with no-data values collapsed to 0.
*/
final SampleConverter converter;
if (isFloat) {
converter = SampleConverter.createPadValuesMask(nodataValues);
} else {
final boolean isZeroValid = (minimum <= 0 && maximum >= 0);
boolean collapsePadValues = false;
if (nodataValues != null && nodataValues.length != 0) {
final double[] sorted = nodataValues.clone();
Arrays.sort(sorted);
double minFill = sorted[0];
double maxFill = minFill;
int indexMax = sorted.length;
while (--indexMax!=0 && Double.isNaN(maxFill = sorted[indexMax]));
assert minFill <= maxFill || Double.isNaN(minFill) : maxFill;
if (targetBand == visibleBand && maxFill > maximumFillValue) {
maximumFillValue = maxFill;
}
if (minFill < floor || maxFill > ceil) {
// At least one fill value is outside the range of acceptable values.
collapsePadValues = true;
} else if (minimum >= 0) {
/*
* Arbitrary optimization of memory usage: if there is a "large" empty
* space between the range of valid values and a no-data value, then we
* may (at subclass implementors choice) collapse the no-data values to
* zero in order to avoid wasting the empty space. Note that we do not
* perform this collapse if the valid range contains negative values
* because it would not save any memory. We do not check the no-data
* values between 0 and 'minimum' for the same reason.
*/
int k = Arrays.binarySearch(sorted, maximum);
if (k >= 0) k++; // We want the first element greater than maximum.
else k = ~k; // Really ~ operator, not -
if (k <= indexMax) {
double unusedSpace = Math.max(sorted[k] - maximum - 1, 0);
while (++k <= indexMax) {
final double delta = sorted[k] - sorted[k-1] - 1;
if (delta > 0) {
unusedSpace += delta;
}
}
final int unused = (int) Math.min(Math.round(unusedSpace), Integer.MAX_VALUE);
collapsePadValues = collapseNoDataValues(isZeroValid, sorted, unused);
// We invoked 'collapseNoDataValues' inconditionnaly even if
// 'unused' is zero because the user may decide on the basis
// of other criterions, like 'isZeroValid'.
}
}
}
if (minimum < floor || maximum > ceil) {
// The range of valid values is outside the range allowed by raw data type.
converter = SampleConverter.createOffset(1 - minimum, nodataValues);
} else if (collapsePadValues) {
if (isZeroValid) {
// We need to collapse the no-data values to 0, but it causes a clash
// with the range of valid values. So we also shift the later.
converter = SampleConverter.createOffset(1 - minimum, nodataValues);
} else {
// We need to collapse the no-data values and there is no clash.
converter = SampleConverter.createPadValuesMask(nodataValues);
}
} else {
/*
* Do NOT take 'nodataValues' in account if there is no need to collapse
* them. This is not the converter's job to transform "packed" values to
* "geophysics" values. We just want them to fit in the IndexColorModel,
* and they already fit. So the identity converter is appropriate even
* in presence of pad values.
*/
converter = SampleConverter.IDENTITY;
}
}
if (converters!=null && targetBand>=0 && targetBand<converters.length) {
converters[targetBand] = converter;
}
if (targetBand == visibleBand) {
visibleConverter = converter;
visibleRange = range;
}
}
}
/*
* Creates a color palette suitable for the range of values in the visible band.
* The case for floating points is the simpliest: we should not have any offset,
* at most a replacement of no-data values. In the case of integer values, we
* must make sure that the indexed color map is large enough for containing both
* the highest data value and the highest no-data value.
*/
if (visibleRange == null) {
visibleRange = (allRanges != null) ? allRanges : new NumberRange(floor, ceil);
}
final PaletteFactory factory = PaletteFactory.getDefault();
factory.setWarningLocale(locale);
final Palette palette;
if (isFloat) {
assert visibleConverter.getOffset() == 0 : visibleConverter;
palette = factory.getContinuousPalette(paletteName, (float) visibleRange.getMinimum(),
(float) visibleRange.getMaximum(), dataType, numBands, visibleBand);
} else {
final double offset = visibleConverter.getOffset();
final double minimum = visibleRange.getMinimum();
final double maximum = visibleRange.getMaximum();
long lower, upper;
if (minimum == Double.NEGATIVE_INFINITY) {
lower = floor;
} else {
lower = Math.round(minimum + offset);
if (!visibleRange.isMinIncluded()) {
lower++; // Must be inclusive
}
}
if (maximum == Double.POSITIVE_INFINITY) {
upper = ceil;
} else {
upper = Math.round(maximum + offset);
if (visibleRange.isMaxIncluded()) {
upper++; // Must be exclusive
}
}
final long size = Math.max(upper, Math.round(maximumFillValue) + 1);
/*
* The target lower, upper and size parameters are usually in the range of SHORT
* or USHORT data type. The Palette class will performs the necessary checks and
* throws an exception if those variables are out of range. However, because we
* need to cast to int before passing the parameter values, we restrict them to
* the 'int' range as a safety in order to avoid results that accidently fall in
* the SHORT or USHORT range. Because Integer.MIN_VALUE or MAX_VALUE are out of
* range, it doesn't matter if those values are inaccurate since we will get an
* exception anyway.
*/
palette = factory.getPalette(paletteName,
(int) Math.max(lower, Integer.MIN_VALUE),
(int) Math.min(upper, Integer.MAX_VALUE),
(int) Math.min(size, Integer.MAX_VALUE), numBands, visibleBand);
}
return palette.getImageTypeSpecifier();
}
/**
* Returns the data type which most closely represents the "raw" internal data of the image.
* It should be one of {@link DataBuffer} constants. The default {@code GeographicImageReader}
* implementation works better with the following types:
*
* {@link DataBuffer#TYPE_BYTE TYPE_BYTE},
* {@link DataBuffer#TYPE_SHORT TYPE_SHORT},
* {@link DataBuffer#TYPE_USHORT TYPE_USHORT} and
* {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT}.
*
* The default implementation returns {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT} in every cases.
* <p>
* <h3>Handling of negative integer values</h3>
* If the raw internal data contains negative values but this method still declares a unsigned
* integer type ({@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT}),
* then the values will be translated in order to fit in the range of strictly positive values.
* For example if the raw internal data range from -23000 to +23000, then there is a choice:
*
* <ul>
* <li><p>If this method returns {@link DataBuffer#TYPE_SHORT}, then the data will be
* stored "as is" without transformation. However the {@linkplain IndexColorModel
* index color model} will have the maximal length allowed by 16 bits integers, with
* positive values in the [0 .. {@value java.lang.Short#MAX_VALUE}] range and negative
* values wrapped in the [32768 .. 65535] range in two's complement binary form. The
* results is a color model consuming 256 kilobytes in every cases. The space not used
* by the [-23000 .. +23000] range (in the above example) is lost.</p></li>
*
* <li><p>If this method returns {@link DataBuffer#TYPE_USHORT}, then the data will be
* translated to the smallest strictly positive range that can holds the data
* ([1..46000] for the above example). Value 0 is reserved for missing data. The
* result is a smaller {@linkplain IndexColorModel index color model} than the
* one used by untranslated data.</p></li>
* </ul>
*
* @param imageIndex The index of the image to be queried.
* @return The data type ({@link DataBuffer#TYPE_FLOAT} by default).
* @throws IOException If an error occurs reading the format information from the input source.
*
* @see #getRawImageType(int, ImageReadParam, SampleConverter[])
*/
protected int getRawDataType(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return DataBuffer.TYPE_FLOAT;
}
/**
* Returns {@code true} if the no-data values should be collapsed to 0 in order to save memory.
* This method is invoked automatically by the {@link #getRawImageType(int, ImageReadParam,
* SampleConverter[]) getRawImageType} method when it detected some unused space between the
* {@linkplain Band#getValidRange range of valid values} and at least one
* {@linkplain Band#getNoDataValues no-data value}.
* <p>
* The default implementation returns {@code false} in all cases, thus avoiding arbitrary
* choice. Subclasses can override this method with some arbitrary threashold, as in the
* example below:
*
* <blockquote><pre>
* return unusedSpace >= 1024;
* </pre></blockquote>
*
* @param isZeroValid
* {@code true} if 0 is a valid value. If this method returns {@code true} while
* {@code isZeroValid} is {@code true}, then the {@linkplain SampleConverter sample
* converter} to be returned by {@link #getRawImageType(int, ImageReadParam,
* SampleConverter[]) getRawImageType} will offset all valid values by 1.
* @param nodataValues
* The {@linkplain Arrays#sort(double[]) sorted}
* {@linkplain Band#getNoDataValues no-data values} (never null and never empty).
* @param unusedSpace
* The largest amount of unused space outside the range of valid values.
*/
protected boolean collapseNoDataValues(final boolean isZeroValid,
final double[] nodataValues,
final int unusedSpace)
{
return false;
}
/**
* Returns the buffered image to which decoded pixel data should be written. The image
* is determined by inspecting the supplied parameters if it is non-null, as described
* in the {@linkplain #getDestination(ImageReadParam,Iterator,int,int) super-class method}.
* In the default implementation, the {@linkplain ImageTypeSpecifier image type specifier}
* set is a singleton containing only the {@linkplain #getRawImageType(int,ImageReadParam,
* SampleConverter[]) raw image type}.
* <p>
* Implementations of the {@link #read(int,ImageReadParam)} method should invoke this
* method instead of {@link #getDestination(ImageReadParam,Iterator,int,int)}.
*
* @param imageIndex The index of the image to be retrieved.
* @param parameters The parameter given to the {@code read} method.
* @param width The true width of the image or tile begin decoded.
* @param height The true width of the image or tile being decoded.
* @param converters If non-null, an array where to store the converters required
* for converting decoded pixel data into stored pixel data.
* @return The buffered image to which decoded pixel data should be written.
*
* @throws IOException If an error occurs reading the format information from the input source.
*
* @see #getRawImageType(int, ImageReadParam, SampleConverter[])
*/
protected BufferedImage getDestination(final int imageIndex, final ImageReadParam parameters,
final int width, final int height, final SampleConverter[] converters)
throws IOException
{
final ImageTypeSpecifier type = getRawImageType(imageIndex, parameters, converters);
final Set<ImageTypeSpecifier> spi = Collections.singleton(type);
return getDestination(parameters, spi.iterator(), width, height);
}
/**
* Returns a default parameter object appropriate for this format. The default
* implementation constructs and returns a new {@link GeographicImageReadParam}.
*
* @return An {@code ImageReadParam} object which may be used.
*
* @todo Replace the return type by {@link GeographicImageReadParam} when we will
* be allowed to compile for J2SE 1.5.
*/
@Override
public ImageReadParam getDefaultReadParam() {
return new GeographicImageReadParam(this);
}
/**
* Reads the image indexed by {@code imageIndex} using a default {@link ImageReadParam}.
* This is a convenience method that calls <code>{@linkplain #read(int,ImageReadParam)
* read}(imageIndex, {@linkplain #getDefaultReadParam})</code>.
* <p>
* The default Java implementation passed a {@code null} parameter. This implementation
* passes the default parameter instead in order to improve consistency when a subclass
* overrides {@link #getDefaultReadParam}.
*
* @param imageIndex the index of the image to be retrieved.
* @return the desired portion of the image.
*
* @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.
*/
@Override
public BufferedImage read(final int imageIndex) throws IOException {
return read(imageIndex, getDefaultReadParam());
}
/**
* Flips the source region vertically. This method should be invoked straight after
* {@link #computeRegions computeRegions} when the image to be read will be flipped
* vertically, for example when the {@linkplain java.awt.image.Raster raster} sample
* values are filled in a "{@code for (y=ymax-1; y>=ymin; y--)}" loop instead of
* "{@code for (y=ymin; y<ymax; y++)}".
* <p>
* This method should be invoked as in the example below:
*
* <blockquote><pre>
* computeRegions(param, srcWidth, srcHeight, image, srcRegion, destRegion);
* flipVertically(param, srcHeight, srcRegion);
* </pre></blockquote>
*
* @param param The {@code param} argument given to {@code computeRegions}.
* @param srcHeight The {@code srcHeight} argument given to {@code computeRegions}.
* @param srcRegion The {@code srcRegion} argument given to {@code computeRegions}.
*/
protected static void flipVertically(final ImageReadParam param, final int srcHeight,
final Rectangle srcRegion)
{
final int spaceLeft = srcRegion.y;
srcRegion.y = srcHeight - (srcRegion.y + srcRegion.height);
/*
* After the flip performed by the above line, we still have 'spaceLeft' pixels left for
* a downward translation. We usually don't need to care about if, except if the source
* region is very close to the bottom of the source image, in which case the correction
* computed below may be greater than the space left.
*
* We are done if there is no vertical subsampling. But if there is subsampling, then we
* need an adjustment. The flipping performed above must be computed as if the source
* region had exactly the size needed for reading nothing more than the last line, i.e.
* 'srcRegion.height' must be a multiple of 'sourceYSubsampling' plus 1. The "offset"
* correction is computed below accordingly.
*/
if (param != null) {
int offset = (srcRegion.height - 1) % param.getSourceYSubsampling();
srcRegion.y += offset;
offset -= spaceLeft;
if (offset > 0) {
// Happen only if we are very close to image border and
// the above translation bring us outside the image area.
srcRegion.height -= offset;
}
}
}
/**
* Invoked when a warning occured. The default implementation make the following choice:
* <p>
* <ul>
* <li>If at least one {@linkplain IIOReadWarningListener warning listener}
* has been {@linkplain #addIIOReadWarningListener specified}, then the
* {@link IIOReadWarningListener#warningOccurred warningOccurred} method is
* invoked for each of them and the log record is <strong>not</strong> logged.</li>
*
* <li>Otherwise, the log record is sent to the {@code "org.geotools.image.io"} logger.</li>
* </ul>
*
* Subclasses may override this method if more processing is wanted, or for
* throwing exception if some warnings should be considered as fatal errors.
*/
public void warningOccurred(final LogRecord record) {
if (warningListeners == null) {
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
} else {
processWarningOccurred(IndexedResourceBundle.format(record));
}
}
/**
* Convenience method for logging a warning from the given method.
*/
private void warningOccurred(final String method, final String message) {
final LogRecord record = new LogRecord(Level.WARNING, message);
record.setSourceClassName(GeographicImageReader.class.getName());
record.setSourceMethodName(method);
warningOccurred(record);
}
/**
* To be overriden and made {@code protected} by {@link StreamImageReader} only.
*/
void close() throws IOException {
metadata = null;
}
}