/* * Geotools 2 - OpenSource mapping toolkit * (C) 2003, Geotools Project Management Committee (PMC) * (C) 2001, Institut de Recherche pour le Développement * * 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; either * version 2.1 of the License, or (at your option) any later version. * * 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. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.cv; // J2SE dependencies import java.awt.Color; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.Serializable; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import javax.media.jai.JAI; import org.geotools.ct.MathTransform1D; import org.geotools.resources.ClassChanger; import org.geotools.resources.RemoteProxy; import org.geotools.resources.Utilities; import org.geotools.resources.XArray; import org.geotools.resources.XMath; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.image.ColorUtilities; import org.geotools.units.Unit; import org.geotools.util.NumberRange; import org.opengis.cs.CS_Unit; import org.opengis.cv.CV_ColorInterpretation; import org.opengis.cv.CV_PaletteInterpretation; import org.opengis.cv.CV_SampleDimension; import org.opengis.cv.CV_SampleDimensionType; import org.opengis.referencing.operation.TransformException; /** * Describes the data values for a coverage. For a grid coverage a sample dimension is a band. * Sample values in a band may be organized in categories. This SampleDimension * 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: * *
 *   [0]       : no data
 *   [1]       : cloud
 *   [2]       : land
 *   [10..210] : temperature to be converted into Celsius degrees through a linear equation
 * 
* * In this example, sample values in range [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. * * @source $URL$ * @version $Id$ * @author OpenGIS * @author Martin Desruisseaux * * @see org.opengis.cv.CV_SampleDimension * * @deprecated Replaced by {@link org.geotools.coverage.GridSampleDimension} * in the org.geotools.coverage package. */ public class SampleDimension implements Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 6026936545776852758L; /** * A sample dimension wrapping the list of categories 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 SampleDimension inverse; /** * The category list for this sample dimension, or null if this sample * dimension has no category. This field is read by SampleTranscoder only. */ final CategoryList categories; /** * true if all categories in this sample dimension have been already scaled * to geophysics ranges. If 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 NaN. *

* Assertions: * */ private final boolean isGeophysics; /** * true if this sample dimension has at least one qualitative category. * An arbitrary number of qualitative categories is allowed, providing their sample * value ranges do not overlap. A sample dimension can have both qualitative and * quantitative categories. */ private final boolean hasQualitative; /** * true if this sample dimension has at least one quantitative category. * An arbitrary number of quantitative categories is allowed, providing their sample * value ranges do not overlap. *

* If sampleToGeophysics is non-null, then hasQuantitative * must be true. However, the opposite do not hold in all cases: a * true value doesn't means that sampleToGeophysics should * be non-null. */ private final boolean hasQuantitative; /** * The {@link Category#getSampleToGeophysics sampleToGeophysics} transform used by every * quantitative {@link Category}, or null. This field may be null for two * reasons: * * * * This field is used by {@link #getOffset} and {@link #getScale}. The * {@link #getSampleToGeophysics} method may also returns directly this * value in some conditions. */ private final MathTransform1D sampleToGeophysics; /** * OpenGIS object returned by {@link #toOpenGIS}. * It may be a hard or a weak reference. */ private transient Object proxy; /** * Construct a sample dimension with no category. */ public SampleDimension() { this((CategoryList) null); } /** * Constructs a sample dimension with a set of qualitative categories only. * This sample dimension will have no unit and a default set of colors. * * @param names Sequence of category names for the values contained in a sample dimension. * This allows for names to be assigned to numerical values. The first entry * in the sequence relates to a cell value of zero. For example: * [0]="Background", [1]="Water", [2]="Forest", [3]="Urban". */ public SampleDimension(final String[] names) { // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999 // ("Relax constraint on placement of this()/super() call in constructors"). this(list(names)); } /** Constructs a list of categories. Used by constructors only. */ private static CategoryList list(final String[] names) { final Color[] colors = new Color[names.length]; final double scale = 255.0/colors.length; for (int i=0; inames. */ public SampleDimension(final String[] names, final Color[] colors) { // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999 // ("Relax constraint on placement of this()/super() call in constructors"). this(list(names, colors)); } /** Constructs a list of categories. Used by constructors only. */ private static CategoryList list(final String[] names, final Color[] colors) { if (names.length != colors.length) { throw new IllegalArgumentException( Errors.format(ErrorKeys.MISMATCHED_ARRAY_LENGTH)); } final Category[] categories = new Category[names.length]; for (int i=0; idouble primitive can be null. This constructor * allows the construction of a SampleDimension without explicit construction of * {@link Category} objects. An heuristic approach is used for dispatching the informations * into a set of {@link Category} objects. However, this constructor still less general and * provides less fine-grain control than the constructor expecting an array of {@link Category}. * * @param description The sample dimension title or description, or null if none. * This is the value to be returned by {@link #getDescription}. * @param type The grid value data type (which indicate the number of bits for the data type), * or null for computing it automatically from the range * [minimum..maximum]. This is the value to be returned by * {@link #getSampleDimensionType}. * @param color The color interpretation, or null for a default value (usually * {@link ColorInterpretation#PALETTE_INDEX PALETTE_INDEX}). This is the value to be * returned by {@link #getColorInterpretation}. * @param palette The color palette associated with the sample dimension, or null * for a default color palette (usually grayscale). If categories is * non-null, then both arrays usually have the same length. However, this constructor * is tolerant on this array length. This is the value to be returned (indirectly) by * {@link #getColorModel}. * @param categories A sequence of category names for the values contained in the sample * dimension, or null if none. This is the values to be returned by * {@link #getCategoryNames}. * @param nodata the values to indicate "no data", or null if none. This is the * values to be returned by {@link #getNoDataValue}. * @param minimum The lower value, inclusive. The [minimum..maximum] range may or * may not includes the nodata values; the range will be adjusted as * needed. If categories was non-null, then minimum is * usually 0. This is the value to be returned by {@link #getMinimumValue}. * @param maximum The upper value, inclusive as well. The * [minimum..maximum] range may or may not includes the nodata * values; the range will be adjusted as needed. If categories was non-null, * then maximum is usually equals to categories.length-1. This * is the value to be returned by {@link #getMaximumValue}. * @param scale The value which is multiplied to grid values, or 1 if none. This is the value * to be returned by {@link #getScale}. * @param offset The value to add to grid values, or 0 if none. This is the value to be * returned by {@link #getOffset}. * @param unit The unit information for this sample dimension, or null if none. * This is the value to be returned by {@link #getUnits}. * * @throws IllegalArgumentException if the range [minimum..maximum] is not valid. */ public SampleDimension(final String description, SampleDimensionType type, ColorInterpretation color, final Color [] palette, final String[] categories, final double[] nodata, double minimum, double maximum, final double scale, final double offset, final Unit unit) { // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999 // ("Relax constraint on placement of this()/super() call in constructors"). this(list(description, type, color, palette, categories, nodata, minimum, maximum, scale, offset, unit)); } /** Constructs a list of categories. Used by constructors only. */ private static CategoryList list(final String description, SampleDimensionType type, ColorInterpretation color, final Color [] palette, final String[] categories, final double[] nodata, double minimum, double maximum, final double scale, final double offset, final Unit unit) { if (Double.isInfinite(minimum) || Double.isInfinite(maximum) || !(minimum < maximum)) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_RANGE_$2, new Double(minimum), new Double(maximum))); } if (Double.isNaN(scale) || Double.isInfinite(scale) || scale==0) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_PARAMETER_$2, "scale", new Double(scale))); } if (Double.isNaN(offset) || Double.isInfinite(offset)) { throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_PARAMETER_$2, "offset", new Double(offset))); } if (type == null) { type = SampleDimensionType.getEnum(minimum, maximum); } if (color == null) { color = ColorInterpretation.PALETTE_INDEX; } final int nameCount = (categories!=null) ? categories.length : 0; final int nodataCount = (nodata !=null) ? nodata.length : 0; final List categoryList = new ArrayList(nameCount + nodataCount + 2); /* * STEP 1 - Add a qualitative category for each 'nodata' value. * NAME: Fetched from 'categories' if available, otherwise default to the value. * COLOR: Fetched from 'palette' if available, otherwise use Category default. */ for (int i=0; i=0 && intValue=0;) { Category category = (Category) categoryList.get(i); if (!category.isQuantitative()) { final NumberRange range = category.getRange(); final Comparable min = range.getMinValue(); final Comparable max = range.getMaxValue(); if (min.compareTo(max) != 0) { final double xmin = ((Number)min).doubleValue(); final double xmax = ((Number)max).doubleValue(); if (!rangeContains(xmin, xmax, nodata)) { final String name = category.getName(null); final Color[] colors = category.getColors(); category = new Category(name, colors, range, scale, offset); categoryList.set(i, category); needQuantitative = false; } } } } } /* * STEP 4 - Create at most one quantitative category for the remaining sample values. * The new category will range from 'minimum' to 'maximum' inclusive, minus * all ranges used by previous categories. If there is no range left, then * no new category will be created. This step will be executed only if the * information provided by the user seem to be incomplete. * * Note that substractions way break a range into many smaller ranges. * The naive algorithm used here try to keep the widest range. */ if (needQuantitative) { boolean minIncluded = true; boolean maxIncluded = true; for (int i=categoryList.size(); --i>=0;) { final NumberRange range = ((Category) categoryList.get(i)).getRange(); final double min = range.getMinimum(); final double max = range.getMaximum(); if (max-minimum < maximum-min) { if (max >= minimum) { // We are loosing some sample values in // the lower range because of nodata values. minimum = max; minIncluded = !range.isMaxIncluded(); } } else { if (min <= maximum) { // We are loosing some sample values in // the upper range because of nodata values. maximum = min; maxIncluded = !range.isMinIncluded(); } } } // If the remaining range is wide enough, add the category. if (maximum-minimum > (minIncluded && maxIncluded ? 0 : 1)) { Number min = type.wrapSample(minimum, false); Number max = type.wrapSample(maximum, false); final Class classe = ClassChanger.getWidestClass(min, max); min = ClassChanger.cast(min, classe); max = ClassChanger.cast(max, classe); final NumberRange range = new NumberRange(classe, min, minIncluded, max, maxIncluded); final Color[] colors = ColorUtilities.subarray(palette, (int)Math.ceil (minimum), (int)Math.floor(maximum)); categoryList.add(new Category(description!=null ? description : "(automatic)", colors, range, scale, offset)); needQuantitative = false; } } /* * STEP 5 - Now, the list of categories should be complete. Construct a * sample dimension appropriate for the type of palette used. */ final Category[] cl = (Category[]) categoryList.toArray(new Category[categoryList.size()]); if (ColorInterpretation.PALETTE_INDEX.equals(color) || ColorInterpretation.GRAY_INDEX.equals(color)) { return list(cl, unit); } throw new UnsupportedOperationException("Not yet implemented"); } /** * Constructs a sample dimension with an arbitrary set of categories, which may be both * quantitative and qualitative. It is possible to specify more than one quantitative * categories, providing that their sample value ranges do not overlap. Quantitative * categories can map sample values to geophysics values using arbitrary relation (not * necessarly linear). * * @param categories The list of categories. * @param units The unit information for this sample dimension. * May be null if no category has units. * This unit apply to values obtained after the * {@link #getSampleToGeophysics sampleToGeophysics} transformation. * @throws IllegalArgumentException if categories contains incompatible * categories. If may be the case for example if two or more categories have * overlapping ranges of sample values. */ public SampleDimension(Category[] categories, Unit units) throws IllegalArgumentException { // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999 // ("Relax constraint on placement of this()/super() call in constructors"). this(list(categories, units)); } /** Construct a list of categories. Used by constructors only. */ private static CategoryList list(final Category[] categories, final Unit units) { if (categories == null) { return null; } CategoryList list = new CategoryList(categories, units); list = (CategoryList) Category.pool.canonicalize(list); if (CategoryList.isScaled(categories, false)) return list; if (CategoryList.isScaled(categories, true )) return list.inverse; throw new IllegalArgumentException(Errors.format(ErrorKeys.MIXED_CATEGORIES)); } /** * Constructs a sample dimension with the specified list of categories. * * @param list The list of categories, or null. */ private SampleDimension(final CategoryList list) { MathTransform1D main = null; boolean isMainValid = true; boolean qualitative = false; if (list != null) { for (int i=list.size(); --i>=0;) { final MathTransform1D candidate = ((Category)list.get(i)).getSampleToGeophysics(); if (candidate == null) { qualitative = true; continue; } if (main != null) { isMainValid &= main.equals(candidate); } main = candidate; } this.isGeophysics = list.isScaled(true); } else { this.isGeophysics = false; } this.categories = list; this.hasQualitative = qualitative; this.hasQuantitative = (main != null); this.sampleToGeophysics = isMainValid ? main : null; } /** * Constructs a new sample dimension with the same categories and * units than the specified sample dimension. * * @param other The other sample dimension, or null. */ protected SampleDimension(final SampleDimension other) { if (other != null) { inverse = other.inverse; categories = other.categories; isGeophysics = other.isGeophysics; hasQualitative = other.hasQualitative; hasQuantitative = other.hasQuantitative; sampleToGeophysics = other.sampleToGeophysics; } else { // 'inverse' will be set when needed. categories = null; isGeophysics = false; hasQualitative = false; hasQuantitative = false; sampleToGeophysics = null; } } /** * Returns a code value indicating grid value data type. * This will also indicate the number of bits for the data type. * * @return a code value indicating grid value data type. */ public SampleDimensionType getSampleDimensionType() { final NumberRange range = getRange(); if (range == null) { return SampleDimensionType.FLOAT; } return SampleDimensionType.getEnum(range); } /** * Get the sample dimension title or description. * This string may be null if no description is present. * * @param locale The locale, or null for the default one. * @return The localized description, or null if none. * If no description was available in the specified locale, * then a default locale is used. * * @see CV_SampleDimension#getDescription() */ public String getDescription(final Locale locale) { return (categories!=null) ? categories.getName(locale) : null; } /** * Returns a sequence of category names for the values contained in this sample dimension. * This allows for names to be assigned to numerical values. The first entry in the sequence * relates to a cell value of zero. For example: * *
     *    [0] Background
     *    [1] Water
     *    [2] Forest
     *    [3] Urban
     *  
* * @param locale The locale for category names, or null for a default locale. * @return The sequence of category names for the values contained in this sample dimension, * or 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 CV_SampleDimension#getCategoryNames() * @see #getCategories * @see #getCategory */ public String[] getCategoryNames(final Locale locale) throws IllegalStateException { if (categories == null) { return null; } if (categories.isEmpty()) { return new String[0]; } String[] names = null; for (int i=categories.size(); --i>=0;) { final Category 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) { final Vocabulary resources = Vocabulary.getResources(locale); throw new IllegalStateException(resources.getString( ErrorKeys.NON_INTEGER_CATEGORY)); } if (names == null) { names = new String[upper+1]; } Arrays.fill(names, lower, upper+1, category.getName(locale)); } 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 0. * * @return The list of categories in this sample dimension, or null if none. * * @see #getCategoryNames * @see #getCategory */ public List getCategories() { return categories; } /** * Returns the category for the specified sample value. If this method can't maps * a category to the specified value, then it returns null. * * @param sample The value (can be one of NaN values). * @return The category for the supplied value, or null if none. * * @see #getCategories * @see #getCategoryNames */ public Category getCategory(final double sample) { return (categories!=null) ? categories.getCategory(sample) : null; } /** * Returns a default category to use for background. A background category is used * when an image is resampled (for * example reprojected in an other coordinate system) and the resampled image do not * fit in a rectangular area. It can also be used in various situation where a raisonable * "no data" category is needed. The default implementation try to returns one * of the {@linkplain #getNoDataValue no data values}. If no suitable category is found, * then a {@linkplain Category#NODATA default} one is returned. * * @return A category to use as background for the "Resample" operation. * Never null. */ public Category getBackground() { return (categories!=null) ? categories.nodata : Category.NODATA; } /** * Returns the values to indicate "no data" for this sample dimension. The default * implementation deduces the "no data" values from the list of categories supplied * at construction time. The rules are: * *
    *
  • If {@link #getSampleToGeophysics} returns null, then * getNoDataValue() returns null as well. * This means that this sample dimension contains no category or contains * only qualitative categories (e.g. a band from a classified image).
  • * *
  • If {@link #getSampleToGeophysics} returns an identity transform, * then getNoDataValue() returns null. * This means that sample value in this sample dimension are already * expressed in geophysics values and that all "no data" values (if any) * have already been converted into NaN values.
  • * *
  • Otherwise, if there is at least one quantitative category, returns the sample values * of all non-quantitative categories. For example if "Temperature" is a quantitative * category and "Land" and "Cloud" are two qualitative categories, then sample values * for "Land" and "Cloud" will be considered as "no data" values. "No data" values * that are already NaN will be ignored.
  • *
* * Together with {@link #getOffset()} and {@link #getScale()}, 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 NaN. * * @return The values to indicate no data values for this sample dimension, * or null if not applicable. * @throws IllegalStateException if some qualitative categories use a range of * non-integer values. * * @see CV_SampleDimension#getNoDataValue() * @see #getSampleToGeophysics */ public double[] getNoDataValue() throws IllegalStateException { if (!hasQuantitative) { return null; } int count = 0; double[] padValues = null; final int size = categories.size(); for (int i=0; i= padValues.length) { padValues = XArray.resize(padValues, count*2); } padValues[count++] = min; /* * The "no data" value has been extracted. Now, check if we have a range * of "no data" values instead of a single one for this category. If we * have a single value, it can be of any type. But if we have a range, * then it must be a range of integers (otherwise we can't expand it). */ if (max != min) { int lower = (int) min; int upper = (int) max; if (lower!=min || upper!=max || !XMath.isInteger(category.getRange().getElementClass())) { throw new IllegalStateException(Errors.format( ErrorKeys.NON_INTEGER_CATEGORY)); } final int requiredLength = count + (upper-lower); if (requiredLength > padValues.length) { padValues = XArray.resize(padValues, requiredLength*2); } while (++lower <= upper) { padValues[count++] = lower; } } } } } if (padValues != null) { padValues = XArray.resize(padValues, count); } return padValues; } /** * Returns the minimum value occurring in this sample dimension. * The default implementation fetch this value from the categories supplied at * construction time. If the minimum value can't be computed, then this method * returns {@link Double#NEGATIVE_INFINITY}. * * @see CV_SampleDimension#getMinimumValue() * @see #getRange */ public double getMinimumValue() { if (categories!=null && !categories.isEmpty()) { final double value = ((Category) categories.get(0)).minimum; if (!Double.isNaN(value)) { return value; } } return Double.NEGATIVE_INFINITY; } /** * Returns the maximum value occurring in this sample dimension. * The default implementation fetch this value from the categories supplied at * construction time. If the maximum value can't be computed, then this method * returns {@link Double#POSITIVE_INFINITY}. * * @see CV_SampleDimension#getMaximumValue() * @see #getRange */ public double getMaximumValue() { if (categories!=null) { for (int i=categories.size(); --i>=0;) { final double value = ((Category) categories.get(i)).maximum; if (!Double.isNaN(value)) { return value; } } } return Double.POSITIVE_INFINITY; } /** * Returns the range of values in this sample dimension. This is the union of the range of * values of every categories, excluding NaN values. A {@link NumberRange} object * gives more informations than {@link #getMinimumValue} and {@link #getMaximumValue} methods * since it contains also the data type (integer, float, etc.) and inclusion/exclusion * informations. * * @return The range of values. May be null if this sample dimension has no * quantitative category. * * @see Category#getRange * @see #getMinimumValue * @see #getMaximumValue * * @task TODO: We should do a better job in CategoryList.getRange() when selecting * the appropriate data type. SampleDimensionType.getEnum(Range) may be of * some help. */ public NumberRange getRange() { return (categories!=null) ? categories.getRange() : null; } /** * Returns true if at least one value of values is * in the range lower inclusive to upper exclusive. */ private static boolean rangeContains(final double lower, final double upper, final double[] values) { if (values != null) { for (int i=0; i=lower && v *
  • If value maps a qualitative category, then the * category name is returned as of {@link Category#getName(Locale)}.
  • * *
  • Otherwise, if value maps a quantitative category, then the value is * transformed into a geophysics value as with the {@link #getSampleToGeophysics() * sampleToGeophysics} transform, the result is formatted as a number and the unit * symbol is appened.
  • * * * @param value The sample value (can be one of NaN values). * @param locale Locale to use for formatting, or null for the default locale. * @return A string representation of the geophysics value, or null if there is * none. * * @task REVISIT: What should we do when the value can't be formatted? * SampleDimension returns null if there is no * category or if an exception is thrown, but CategoryList * returns "Untitled" if the value is an unknow NaN, and try to format * the number anyway in other cases. */ public String getLabel(final double value, final Locale locale) { if (categories != null) { if (isGeophysics) { return categories.format(value, locale); } else try { return categories.inverse.format(categories.transform(value), locale); } catch (TransformException exception) { // Value probably don't match a category. Ignore... } } return null; } /** * Returns the unit information for this sample dimension. * May returns null if this dimension has no units. * This unit apply to values obtained after the {@link #getSampleToGeophysics * sampleToGeophysics} transformation. * * @see CV_SampleDimension#getUnits() * @see #getSampleToGeophysics */ public Unit getUnits() { return (categories!=null) ? categories.geophysics(true).getUnits() : null; } /** * Returns the value to add 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 #getScale()} and {@link #getNoDataValue()}, 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 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 CV_SampleDimension#getOffset() * @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 #getNoDataValue()}, 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 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 CV_SampleDimension#getScale() * @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; } IllegalStateException exception = new IllegalStateException(Errors.format( ErrorKeys.NON_LINEAR_RELATION)); exception.initCause(cause); throw exception; } /** * Returns a transform from sample values to geophysics values. If this sample dimension * has no category, then this method returns null. If all sample values are * already geophysics values (including 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 #getNoDataValue() no data values}" into * NaN values. * The sampleToGeophysics.{@linkplain MathTransform1D#inverse() inverse()} * transform is capable to differenciate NaN values to get back the original * sample value. * * @return The transform from sample to geophysics values, or 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 #getNoDataValue * @see #rescale */ public MathTransform1D getSampleToGeophysics() { if (isGeophysics) { return GeophysicsCategory.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; } /** * If true, returns the geophysics companion 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 * {@link #getSampleToGeophysics sampleToGeophysics} transform is always the identity * transform, or null if no such transform existed in the first place. In * other words, the range of sample values in all category maps directly the "real world" * values without the need for any transformation. *

    * SampleDimension objects live by pair: a geophysics one (used for * computation) and a non-geophysics one (used for packing data, usually as * integers). The geo argument specifies which object from the pair is wanted, * regardless if this method is invoked on the geophysics or non-geophysics instance of the * pair. In other words, the result of geophysics(b1).geophysics(b2).geophysics(b3) * depends only on the value in the last call (b3). * * @param geo true to get a sample dimension with an identity * {@linkplain #getSampleToGeophysics transform} and a {@linkplain #getRange range of * sample values} matching the geophysics values, or false to get back the * original sample dimension. * @return The sample dimension. Never null, but may be this. * * @see Category#geophysics * @see org.geotools.gc.GridCoverage#geophysics */ public SampleDimension geophysics(final boolean geo) { if (geo == isGeophysics) { return this; } if (inverse == null) { if (categories != null) { inverse = new SampleDimension(categories.inverse); inverse.inverse = this; } else { /* * If there is no categories, then there is no real difference between * "geophysics" and "indexed" sample dimensions. Both kinds of sample * dimensions would be identical objects, so we are better to just * returns 'this'. */ inverse = this; } } return inverse; } // NOTE: "getPaletteInterpretation()" is not available in Geotools since // palette are backed by IndexColorModel, which support only RGB. /** * Returns the color interpretation of the sample dimension. * A sample dimension can be an index into a color palette or be a color model * component. If the sample dimension is not assigned a color interpretation * the value is {@link ColorInterpretation#UNDEFINED}. * * @see CV_SampleDimension#getColorInterpretation() */ public ColorInterpretation getColorInterpretation() { // The 'GridSampleDimension' class overrides this method // with better values for 'band' and 'numBands' constants. final int band = 0; final int numBands = 1; return ColorInterpretation.getEnum(getColorModel(band, numBands), band); } /** * Returns a color model for this sample dimension. The default implementation create a color * model with 1 band using each category's colors as returned by {@link Category#getColors}. * The returned color model will typically use data type {@link DataBuffer#TYPE_FLOAT} if this * SampleDimension instance is "geophysics", or an integer data type otherwise. *

    * Note that {@link org.geotools.gc.GridCoverage#getSampleDimensions} returns special * implementations of SampleDimension. In this particular case, the color model * created by this getColorModel() method will have the same number of bands * than the grid coverage's {@link RenderedImage}. * * @return The requested color model, suitable for {@link RenderedImage} objects with values * in the {@link #getRange} range. May be null if this * sample dimension has no category. */ public ColorModel getColorModel() { // The 'GridSampleDimension' 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 data type {@link DataBuffer#TYPE_FLOAT} if this * SampleDimension instance is "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 visibleBand and ignore the others, but * the existence of all 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 RenderedImage} objects with values * in the {@link #getRange} range. May be null if this * sample dimension has no category. * * @task REVISIT: This method may be deprecated in a future version. It it strange to use * only one SampleDimension for creating a multi-bands color * model. Logically, we would expect as many SampleDimensions * as bands. */ public ColorModel getColorModel(final int visibleBand, final int numBands) { if (categories != null) { 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 visibleBand and ignore the others, but * the existence of all 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 RenderedImage} objects with values * in the {@link #getRange} range. May be null if this * sample dimension has no category. * * @task REVISIT: This method may be deprecated in a future version. It it strange to use * only one SampleDimension for creating a multi-bands color * model. Logically, we would expect as many SampleDimensions * 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 #getNoDataValue 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. * * @see #getScale * @see #getOffset * @see Category#rescale */ public SampleDimension rescale(final double scale, final double offset) { final MathTransform1D sampleToGeophysics = Category.createLinearTransform(scale, offset); final Category[] categories = (Category[]) getCategories().toArray(); final Category[] reference = (Category[]) categories.clone(); for (int i=0; iMETA-INF/registryFile.jai file, which is automatically * parsed during JAI initialization. Unfortunatly, it can't access private * classes and we don't want to make our registration classes public. We * can't move our registration classes into a hidden "resources" package * neither because we need package-private access to CategoryList. * For now, we assume that people using the GCS package probably want to work * with {@link org.geotools.gc.GridCoverage}, which make extensive use of JAI. * Peoples just working with {@link org.geotools.cv.Coverage} are stuck with * the overhead. Note that we register the image operation here because the * only operation's argument is of type SampleDimension[]. * Consequently, the image operation may be invoked at any time after class * loading of {@link SampleDimension}. *

    * Additional note: moving the initialization into the * META-INF/registryFile.jai file may not be the best idea neithter, * since peoples using JAI without the GCS module may be stuck with the overhead * of loading GCS classes. */ static { SampleTranscoder.register(JAI.getDefaultInstance()); } ///////////////////////////////////////////////////////////////////////// //////////////// //////////////// //////////////// OPENGIS ADAPTER //////////////// //////////////// //////////////// ///////////////////////////////////////////////////////////////////////// /** * Returns an OpenGIS interface for this sample dimension. This method first * looks in the cache. If no interface was previously cached, then this * method creates a new adapter and caches the result. * * @param adapters The originating {@link Adapters}. * @return The OpenGIS interface. The returned type is a generic {@link Object} * in order to avoid premature class loading of OpenGIS interface. * @throws RemoteException if this object can't be exported. */ final synchronized Object toOpenGIS(final Object adapters) throws RemoteException { if (proxy != null) { if (proxy instanceof Reference) { final Object ref = ((Reference) proxy).get(); if (ref != null) { return ref; } } else { return proxy; } } final Object opengis = new Export(adapters); proxy = new WeakReference(opengis); return opengis; } /** * Wraps a {@link SampleDimension} object for use with OpenGIS. This wrapper is a * good place to check for non-implemented OpenGIS methods (just check for methods * throwing {@link UnsupportedOperationException}). This class is suitable for RMI * use. */ final class Export extends UnicastRemoteObject implements CV_SampleDimension, RemoteProxy { /** * The originating adapter. */ private final Adapters adapters; /** * Constructs a remote object. */ protected Export(final Object adapters) throws RemoteException { super(); // TODO: Fetch the port number from the adapter. this.adapters = (Adapters)adapters; } /** * Returns the underlying implementation. */ public final Serializable getImplementation() throws RemoteException { return SampleDimension.this; } /** * Sample dimension title or description. */ public String getDescription() throws RemoteException { return SampleDimension.this.getDescription(null); } /** * A code value indicating grid value data type. * * @task TODO: We should get this information by inspecting * the image's underlying {@link SampleModel}. */ public CV_SampleDimensionType getSampleDimensionType() throws RemoteException { return adapters.export(SampleDimension.this.getSampleDimensionType()); } /** * Sequence of category names for the values contained in a sample dimension. */ public String[] getCategoryNames() throws RemoteException { return SampleDimension.this.getCategoryNames(null); } /** * Color interpretation of the sample dimension. */ public CV_ColorInterpretation getColorInterpretation() throws RemoteException { return adapters.export(SampleDimension.this.getColorInterpretation()); } /** * Indicates the type of color palette entry for sample dimensions which have a palette. */ public CV_PaletteInterpretation getPaletteInterpretation() throws RemoteException { return new CV_PaletteInterpretation(CV_PaletteInterpretation.CV_RGB); } /** * Color palette associated with the sample dimension. */ public int[][] getPalette() throws RemoteException { final ColorModel model = getColorModel(); if (model instanceof IndexColorModel) { final IndexColorModel index = (IndexColorModel) model; final int[][] palette = new int[index.getMapSize()][]; final boolean hasAlpha = index.hasAlpha(); for (int i=0; i