/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.coverage; import java.awt.Color; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import javax.measure.unit.Unit; import javax.media.jai.JAI; import org.geotools.referencing.operation.transform.LinearTransform1D; import org.geotools.resources.ClassChanger; import org.geotools.resources.Classes; import org.geotools.resources.XArray; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.resources.image.ColorUtilities; import org.geotools.util.NumberRange; import org.geotools.util.SimpleInternationalString; import org.geotools.util.Utilities; import org.opengis.coverage.ColorInterpretation; import org.opengis.coverage.PaletteInterpretation; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.SampleDimensionType; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; import org.opengis.util.InternationalString; /** * Describes the data values for a coverage as a list of {@linkplain Category categories}. For * a grid coverage a sample dimension is a band. Sample values in a band may be organized in * categories. This {@code GridSampleDimension} implementation is capable to differenciate * qualitative and quantitative categories. For example an image of sea surface * temperature (SST) could very well defines the following categories: * *
* * In this example, sample values in range {@code [10..210]} defines a quantitative category, * while all others categories are qualitative. The difference between those two kinds of category * is that the {@link Category#getSampleToGeophysics} method returns a non-null transform if and * only if the category is quantitative. ** [0] : no data * [1] : cloud * [2] : land * [10..210] : temperature to be converted into Celsius degrees through a linear equation *
* While this class can be used with arbitrary {@linkplain org.opengis.coverage.Coverage coverage}, * the primary target for this implementation is {@linkplain org.opengis.coverage.grid.GridCoverage * grid coverage} storing their sample values as integers. This explain the "{@code Grid}" prefix * in the class name. * * @since 2.1 * * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class GridSampleDimension implements SampleDimension, Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 6026936545776852758L; /** * A sample dimension wrapping the list of categories {@code CategoryList.inverse}. * This object is constructed and returned by {@link #geophysics}. Constructed when first * needed, but serialized anyway because it may be a user-supplied object. */ private GridSampleDimension inverse; /** * The category list for this sample dimension, or {@code null} if this sample * dimension has no category. This field is read by {@code SampleTranscoder} only. */ final CategoryList categories; /** * {@code true} if all categories in this sample dimension have been already scaled * to geophysics ranges. If {@code true}, then the {@link #getSampleToGeophysics()} * method should returns an identity transform. Note that the opposite do not always hold: * an identity transform doesn't means that all categories are geophysics. For example, * some qualitative categories may map to some values differents than {@code NaN}. *
* Assertions: *
* If {@code sampleToGeophysics} is non-null, then {@code hasQuantitative} * must be true. However, the opposite do not hold in all cases: a * {@code true} value doesn't means that {@code sampleToGeophysics} should be non-null. */ private final boolean hasQuantitative; /** * The {@link Category#getSampleToGeophysics sampleToGeophysics} transform used by every * quantitative {@link Category}, or {@code null}. This field may be null for two reasons: * *
* * @return The sequence of category names for the values contained in this sample dimension, * or {@code null} if there is no category in this sample dimension. * @throws IllegalStateException if a sequence can't be mapped because some category use * negative or non-integer sample values. * * @see #getCategories * @see #getCategory */ public InternationalString[] getCategoryNames() throws IllegalStateException { if (categories == null) { return null; } if (categories.isEmpty()) { return new InternationalString[0]; } InternationalString[] names = null; for (int i=categories.size(); --i>=0;) { final Category category = categories.get(i); final int lower = (int) category.minimum; final int upper = (int) category.maximum; if (lower != category.minimum || lower < 0 || upper != category.maximum || upper < 0) { throw new IllegalStateException(Errors.format(ErrorKeys.NON_INTEGER_CATEGORY)); } if (names == null) { names = new InternationalString[upper+1]; } Arrays.fill(names, lower, upper+1, category.getName()); } return names; } /** * Returns all categories in this sample dimension. Note that a {@link Category} object may * apply to an arbitrary range of sample values. Consequently, the first element in this * collection may not be directly related to the sample value {@code 0}. * * @return The list of categories in this sample dimension, or {@code null} if none. * * @see #getCategoryNames * @see #getCategory */ public List* [0] Background * [1] Water * [2] Forest * [3] Urban *
* * Together with {@link #getScale()} and {@link #getNoDataValues()}, this method provides a * limited way to transform sample values into geophysics values. However, the recommended * way is to use the {@link #getSampleToGeophysics sampleToGeophysics} transform instead, * which is more general and take care of converting automatically "no data" values * into {@code NaN}. * * @return The offset to add to grid values. * @throws IllegalStateException if the transform from sample to geophysics values * is not a linear relation. * * @see #getSampleToGeophysics * @see #rescale */ public double getOffset() throws IllegalStateException { return getCoefficient(0); } /** * Returns the value which is multiplied to grid values for this sample dimension. * This attribute is typically used when the sample dimension represents elevation * data. The transformation equation is: * *offset + scale*sample
* * Together with {@link #getOffset()} and {@link #getNoDataValues()}, this method provides a * limited way to transform sample values into geophysics values. However, the recommended * way is to use the {@link #getSampleToGeophysics sampleToGeophysics} transform instead, * which is more general and take care of converting automatically "no data" values * into {@code NaN}. * * @return The scale to multiply to grid value. * @throws IllegalStateException if the transform from sample to geophysics values * is not a linear relation. * * @see #getSampleToGeophysics * @see #rescale */ public double getScale() { return getCoefficient(1); } /** * Returns a coefficient of the linear transform from sample to geophysics values. * * @param order The coefficient order (0 for the offset, or 1 for the scale factor, * 2 if we were going to implement quadratic relation, 3 for cubic, etc.). * @return The coefficient. * @throws IllegalStateException if the transform from sample to geophysics values * is not a linear relation. */ private double getCoefficient(final int order) throws IllegalStateException { if (!hasQuantitative) { // Default value for "offset" is 0; default value for "scale" is 1. // This is equal to the order if 0 <= order <= 1. return order; } Exception cause = null; if (sampleToGeophysics != null) try { final double value; switch (order) { case 0: value = sampleToGeophysics.transform(0); break; case 1: value = sampleToGeophysics.derivative(Double.NaN); break; default: throw new AssertionError(order); // Should not happen } if (!Double.isNaN(value)) { return value; } } catch (TransformException exception) { cause = exception; } throw new IllegalStateException(Errors.format(ErrorKeys.NON_LINEAR_RELATION), cause); } /** * Returns a transform from sample values to geophysics values. If this sample dimension * has no category, then this method returns {@code null}. If all sample values are * already geophysics values (including {@code NaN} for "no data" values), then this * method returns an identity transform. Otherwise, this method returns a transform expecting * sample values as input and computing geophysics value as output. This transform will take * care of converting all "{@linkplain #getNoDataValues() no data values}" into * {@code NaN} values. *offset + scale*sample
* The sampleToGeophysics.{@linkplain MathTransform1D#inverse() inverse()}
* transform is capable to differenciate {@code NaN} values to get back the original
* sample value.
*
* @return The transform from sample to geophysics values, or {@code null} if this
* sample dimension do not defines any transform (which is not the same that
* defining an identity transform).
*
* @see #getScale
* @see #getOffset
* @see #getNoDataValues
* @see #rescale
*/
public MathTransform1D getSampleToGeophysics() {
if (isGeophysics) {
return LinearTransform1D.IDENTITY;
}
if (!hasQualitative && sampleToGeophysics!=null) {
// If there is only quantitative categories and they all use the same transform,
// then we don't need the indirection level provided by CategoryList.
return sampleToGeophysics;
}
// CategoryList is a MathTransform1D.
return categories;
}
/**
* Returns the {@linkplain org.geotools.coverage.grid.ViewType#GEOPHYSICS geophysics} or
* {@linkplain org.geotools.coverage.grid.ViewType#PACKED packed} view of this sample dimension.
* By definition, a geophysics sample dimension is a sample dimension with a
* {@linkplain #getRange range of sample values} transformed in such a way that the
* {@linkplain #getSampleToGeophysics sample to geophysics} transform is always the
* {@linkplain MathTransform1D#isIdentity identity} transform, or {@code null} if no such
* transform existed in the first place. In other words, the range of sample values in all
* {@linkplain Category categories} maps directly the "real world" values
* without the need for any transformation.
*
* {@code GridSampleDimension} objects live by pair: a
* {@linkplain org.geotools.coverage.grid.ViewType#GEOPHYSICS geophysics} one (used for
* computation) and a {@linkplain org.geotools.coverage.grid.ViewType#PACKED packed} one
* (used for storing data, usually as integers). The {@code geo} argument specifies which
* object from the pair is wanted, regardless if this method is invoked on the geophysics or
* packed instance of the pair.
*
* @param geo {@code true} to get a sample dimension with an identity
* {@linkplain #getSampleToGeophysics transform} and a {@linkplain #getRange range of
* values} matching the {@linkplain org.geotools.coverage.grid.ViewType#GEOPHYSICS
* geophysics} values, or {@code false} to get back the
* {@linkplain org.geotools.coverage.grid.ViewType#PACKED packed} sample dimension.
* @return The sample dimension. Never {@code null}, but may be {@code this}.
*
* @see Category#geophysics
* @see org.geotools.coverage.grid.GridCoverage2D#view
*/
public GridSampleDimension geophysics(final boolean geo) {
if (geo == isGeophysics) {
return this;
}
if (inverse == null) {
if (categories != null) {
inverse = new GridSampleDimension(description, categories.inverse);
inverse.inverse = this;
} else {
/*
* If there is no categories, then there is no real difference between geophysics
* and packed sample dimensions. Both kinds of sample dimensions would be identical
* objects, so we are better to just returns 'this'.
*/
inverse = this;
}
}
return inverse;
}
/**
* Color palette associated with the sample dimension. A color palette can have any number of
* colors. See palette interpretation for meaning of the palette entries. If the grid coverage
* has no color palette, {@code null} will be returned.
*
* @return The color palette associated with the sample dimension.
*
* @see #getPaletteInterpretation
* @see #getColorInterpretation
* @see IndexColorModel
*
* @deprecated No replacement.
*/
public int[][] getPalette() {
final ColorModel color = getColorModel();
if (color instanceof IndexColorModel) {
final IndexColorModel cm = (IndexColorModel) color;
final int[][] colors = new int[cm.getMapSize()][];
for (int i=0; i{@link #getRange}
range. May be {@code null} if this
* sample dimension has no category.
*/
public ColorModel getColorModel() {
// The 'Grid2DSampleDimension' class overrides this method
// with better values for 'band' and 'numBands' constants.
final int band = 0;
final int numBands = 1;
return getColorModel(band, numBands);
}
/**
* Returns a color model for this sample dimension. The default implementation create the
* color model using each category's colors as returned by {@link Category#getColors}. The
* returned color model will typically use {@link DataBuffer#TYPE_FLOAT} if this sample
* dimension is {@linkplain org.geotools.coverage.grid.ViewType#GEOPHYSICS geophysics},
* or an integer data type otherwise.
*
* @param visibleBand The band to be made visible (usually 0). All other bands, if any
* will be ignored.
* @param numBands The number of bands for the color model (usually 1). The returned color
* model will renderer only the {@code visibleBand} and ignore the others, but
* the existence of all {@code numBands} will be at least tolerated. Supplemental
* bands, even invisible, are useful for processing with Java Advanced Imaging.
* @return The requested color model, suitable for {@link java.awt.image.RenderedImage} objects
* with values in the {@link #getRange}
range. May be {@code null} if this
* sample dimension has no category.
*
* @todo This method may be deprecated in a future version. It it strange to use
* only one {@code SampleDimension} object for creating a multi-bands color
* model. Logically, we would expect as many {@code SampleDimension}s as bands.
*/
public ColorModel getColorModel(final int visibleBand, final int numBands) {
if (categories != null) {
if (isGeophysics && hasQualitative) {
// Data likely to have NaN values, which require a floating point type.
return categories.getColorModel(visibleBand, numBands, DataBuffer.TYPE_FLOAT);
}
return categories.getColorModel(visibleBand, numBands);
}
return null;
}
/**
* Returns a color model for this sample dimension. The default implementation create the
* color model using each category's colors as returned by {@link Category#getColors}.
*
* @param visibleBand The band to be made visible (usually 0). All other bands, if any
* will be ignored.
* @param numBands The number of bands for the color model (usually 1). The returned color
* model will renderer only the {@code visibleBand} and ignore the others, but
* the existence of all {@code numBands} will be at least tolerated. Supplemental
* bands, even invisible, are useful for processing with Java Advanced Imaging.
* @param type The data type that has to be used for the sample model.
* @return The requested color model, suitable for {@link java.awt.image.RenderedImage} objects
* with values in the {@link #getRange}
range. May be {@code null} if this
* sample dimension has no category.
*
* @todo This method may be deprecated in a future version. It it strange to use
* only one {@code SampleDimension} object for creating a multi-bands color
* model. Logically, we would expect as many {@code SampleDimension}s as bands.
*/
public ColorModel getColorModel(final int visibleBand, final int numBands, final int type) {
if (categories != null) {
return categories.getColorModel(visibleBand, numBands, type);
}
return null;
}
/**
* Returns a sample dimension using new {@link #getScale scale} and {@link #getOffset offset}
* coefficients. Other properties like the {@linkplain #getRange sample value range},
* {@linkplain #getNoDataValues no data values} and {@linkplain #getColorModel colors}
* are unchanged.
*
* @param scale The value which is multiplied to grid values for the new sample dimension.
* @param offset The value to add to grid values for the new sample dimension.
* @return The scaled sample dimension.
*
* @see #getScale
* @see #getOffset
* @see Category#rescale
*/
public GridSampleDimension rescale(final double scale, final double offset) {
final MathTransform1D sampleToGeophysics = Category.createLinearTransform(scale, offset);
final Category[] categories = (Category[]) getCategories().toArray();
boolean changed = false;
for (int i=0; i