Package org.geotools.coverage

Source Code of org.geotools.coverage.GridSampleDimension

/*
*    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
* <em>qualitative</em> and <em>quantitative</em> categories. For example an image of sea surface
* temperature (SST) could very well defines the following categories:
*
* <blockquote><pre>
*   [0]       : no data
*   [1]       : cloud
*   [2]       : land
*   [10..210] : temperature to be converted into Celsius degrees through a linear equation
* </pre></blockquote>
*
* 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.
* <p>
* 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}.
     * <p>
     * Assertions:
     *  <ul>
     *    <li>{@code isGeophysics} == {@code categories.isGeophysics(true)}.</li>
     *    <li>{@code isGeophysics} != {@code categories.isGeophysics(false)}, except
     *        if {@code categories.geophysics(true) == categories.geophysics(false)}</li>
     * </ul>
     */
    private final boolean isGeophysics;

    /**
     * {@code 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;

    /**
     * {@code 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.
     * <p>
     * If {@code sampleToGeophysics} is non-null, then {@code hasQuantitative}
     * <strong>must</strong> 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:
     *
     * <ul>
     *   <li>There is no quantitative category in this sample dimension.</li>
     *   <li>There is more than one quantitative category, and all of them don't use the same
     *       {@link Category#getSampleToGeophysics sampleToGeophysics} transform.</li>
     * </ul>
     *
     * 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;

    /**
     * Decription for this sample dimension. Typically used as a way to perform a band select by
     * using human comprehensible descriptions instead of just numbers. Web Coverage Service (WCS)
     * can use this feature in order to perform band subsetting as directed from a user request.
     */
    private final InternationalString description;

    /**
     * Constructs a sample dimension with specified name and no category.
     *
     * @param description
     *            The sample dimension title or description, or {@code null} if
     *            none. This is the value to be returned by {@link #getDescription}.
     *
     * @since 2.3
     */
    public GridSampleDimension(final CharSequence description) {
        this(description, (CategoryList) null);
    }

    /**
     * Constructs a sample dimension with a set of qualitative categories only. This constructor
     * expects only a 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". The created sample dimension will have no unit and a default set of colors.
     *
     * @param description
     *            The sample dimension title or description, or {@code null} for the default
     *            (the name of what looks like the "main" category). This is the value to be
     *            returned by {@link #getDescription}.
     * @param categoriesNames
     *            Sequence of category names for the values contained in a sample dimension,
     *            as {@link String} or {@link InternationalString} objects.
     *
     * @since 2.3
     */
    public GridSampleDimension(final CharSequence description,
                               final CharSequence[] categoriesNames)
    {
        // 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(description, list(categoriesNames));
    }

    /** Constructs a list of categories. Used by constructors only. */
    private static CategoryList list(final CharSequence[] names) {
        final int length = names.length;
        final Color[] colors = new Color[length];
        final double scale = 255.0 / length;
        for (int i=0; i<length; i++) {
            final int r = (int) Math.round(scale * i);
            colors[i] = new Color(r, r, r);
        }
        return list(names, colors);
    }

    /**
     * Constructs a sample dimension with a set of qualitative categories and colors. This
     * constructor expects a 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". The created sample dimension will have no unit and a default
     * set of colors.
     *
     * @param description
     *            The sample dimension title or description, or {@code null} for the default
     *            (the name of what looks like the "main" category). This is the value to be
     *            returned by {@link #getDescription}.
     * @param names
     *            Sequence of category names for the values contained in a sample dimension,
     *            as {@link String} or {@link InternationalString} objects.
     * @param colors
     *            Color to assign to each category. This array must have the
     *            same length than {@code names}.
     *
     * @since 2.3
     */
    public GridSampleDimension(final CharSequence description,
                               final CharSequence[] 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(description, list(names, colors));
    }

    /** Constructs a list of categories. Used by constructors only. */
    private static CategoryList list(final CharSequence[] names, final Color[] colors) {
        if (names.length != colors.length) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.MISMATCHED_ARRAY_LENGTH));
        }
        final int length = names.length;
        final Category[] categories = new Category[length];
        for (int i=0; i<length; i++) {
            categories[i] = new Category(names[i], colors[i], i);
        }
        return list(categories, null);
    }

    /**
     * Constructs a sample dimension with the specified properties. For convenience,
     * any argument which is not a {@code double} primitive can be {@code null}, and
     * any {@linkplain CharSequence char sequence} can be either a {@link String}
     * or {@link InternationalString} object.
     * <p>
     * This constructor allows the construction of a {@code GridSampleDimension} 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} objects.
     *
     * @param description
     *            The sample dimension title or description, or {@code null} for the default
     *            (the name of what looks like the "main" category). 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 {@code null} for computing it automatically from the range
     *            {@code [minimum..maximum]}. This is the value to be returned by
     *            {@link #getSampleDimensionType}.
     * @param color
     *            The color interpretation, or {@code 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 {@code null} for a
     *            default color palette (usually grayscale). If {@code 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 {@code null} if none. This is the values
     *            to be returned by {@link #getCategoryNames}.
     * @param nodata
     *            the values to indicate "no data", or {@code null} if none.
     *            This is the values to be returned by {@link #getNoDataValues}.
     * @param minimum
     *            The lower value, inclusive. The {@code [minimum..maximum]} range may or may not
     *            includes the {@code nodata} values; the range will be adjusted as needed. If
     *            {@code categories} was non-null, then {@code minimum} is usually 0. This is the
     *            value to be returned by {@link #getMinimumValue}.
     * @param maximum
     *            The upper value, <strong>inclusive</strong> as well. The {@code [minimum..maximum]}
     *            range may or may not includes the {@code nodata} values; the range will be adjusted
     *            as needed. If {@code categories} was non-null, then {@code maximum} is usually
     *            equals to {@code 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 {@code null} if none.
     *            This is the value to be returned by {@link #getUnits}.
     *
     * @throws IllegalArgumentException
     *             if the range {@code [minimum..maximum]} is not valid.
     */
    @SuppressWarnings("deprecation")
  public GridSampleDimension(final CharSequence  description,
                               final SampleDimensionType  type,
                               final ColorInterpretation color,
                               final Color[]           palette,
                               final CharSequence[] categories,
                               final double[]           nodata,
                               final double            minimum,
                               final 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(description, list(description, type, color, palette, categories, nodata,
                               minimum, maximum, scale, offset, unit));
    }

    /** Constructs a list of categories. Used by constructors only. */
    @SuppressWarnings("deprecation")
  private static CategoryList list(CharSequence  description,
                                     SampleDimensionType  type,
                                     ColorInterpretation color,
                               final Color[]           palette,
                               final CharSequence[] categories,
                               final double[]           nodata,
                                     double            minimum,
                                     double            maximum,
                               final double              scale,
                               final double             offset,
                               final Unit<?>              unit)
    {
        if (description == null) {
            description = Vocabulary.formatInternational(VocabularyKeys.UNTITLED);
        }
        if (Double.isInfinite(minimum) || Double.isInfinite(maximum) || !(minimum < maximum)) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_RANGE_$2, minimum, maximum));
        }
        if (Double.isNaN(scale) || Double.isInfinite(scale) || scale == 0) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_PARAMETER_$2, "scale", scale));
        }
        if (Double.isNaN(offset) || Double.isInfinite(offset)) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_PARAMETER_$2, "offset", offset));
        }
        if (type == null) {
            type = TypeMap.getSampleDimensionType(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<Category> categoryList = new ArrayList<Category>(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<nodataCount; i++) {
            CharSequence name = null;
            final double padValue = nodata[i];
            final int intValue = (int) Math.floor(padValue);
            if (intValue >= 0 && intValue < nameCount) {
                if (intValue == padValue) {
                    // This category will be added in step 2 below.
                    continue;
                }
                name = categories[intValue];
            }
            final Number value = TypeMap.wrapSample(padValue, type, false);
            if (name == null) {
                name = value.toString();
            }
            final NumberRange<?> range = new NumberRange(value.getClass(), value, value);
            final Color[] colors = ColorUtilities.subarray(palette, intValue, intValue + 1);
            categoryList.add(new Category(name, colors, range, (MathTransform1D) null));
        }
        /*
         * STEP 2 - Add a qualitative category for each category name.
         *   RANGE: Fetched from the index (position) in the 'categories' array.
         *   COLOR: Fetched from 'palette' if available, otherwise use Category default.
         */
        if (nameCount != 0) {
            int lower = 0;
            final int length = categories.length;
            for (int upper=1; upper<=length; upper++) {
                if (upper != length) {
                    final String nameLower = categories[lower].toString().trim();
                    final String nameUpper = categories[upper].toString().trim();
                    if (nameLower.equalsIgnoreCase(nameUpper)) {
                        /*
                         * If there is a suite of categories with identical name,  create only one
                         * category with range [lower..upper] instead of one new category for each
                         * sample value.
                         */
                        continue;
                    }
                }
                final CharSequence name = categories[lower];
                Number min = TypeMap.wrapSample(lower,   type, false);
                Number max = TypeMap.wrapSample(upper-1, type, false);
                final Class<? extends Number> classe;
                if (min.equals(max)) {
                    min = max;
                    classe = max.getClass();
                } else {
                    classe = ClassChanger.getWidestClass(min, max);
                    min    = ClassChanger.cast(min, classe);
                    max    = ClassChanger.cast(max, classe);
                }
                final NumberRange<?> range = new NumberRange(classe, min, max);
                final Color[] colors = ColorUtilities.subarray(palette, lower, upper);
                categoryList.add(new Category(name, colors, range, (MathTransform1D) null));
                lower = upper;
            }
        }
        /*
         * STEP 3 - Changes some qualitative categories into quantitative ones.  The hard questions
         *          is: do we want to mark a category as "quantitative"?   OpenGIS has no notion of
         *          "qualitative" versus "quantitative" category. As an heuristic approach, we will
         *          look for quantitative category if:
         *
         *          - 'scale' and 'offset' do not map to an identity transform. Those
         *            coefficients can be stored in quantitative category only.
         *
         *          - 'nodata' were specified. If the user wants to declare "nodata" values,
         *            then we can reasonably assume that he have real values somewhere else.
         *
         *          - Only 1 category were created so far. A classified raster with only one
         *            category is useless. Consequently, it is probably a numeric raster instead.
         */
        boolean needQuantitative = false;
        if (scale != 1 || offset != 0 || nodataCount != 0 || categoryList.size() <= 1) {
            needQuantitative = true;
            for (int i = categoryList.size(); --i >= 0;) {
                Category category = categoryList.get(i);
                if (!category.isQuantitative()) {
                    final NumberRange range = category.getRange();
                    final Comparable  min   = range.getMinValue();
                    final Comparable  max   = range.getMaxValue();
                    @SuppressWarnings("unchecked")
                    final int c = min.compareTo(max);
                    if (c != 0) {
                        final double xmin = ((Number) min).doubleValue();
                        final double xmax = ((Number) max).doubleValue();
                        if (!rangeContains(xmin, xmax, nodata)) {
                            final InternationalString name = category.getName();
                            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 = 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 = TypeMap.wrapSample(minimum, type, false);
                Number max = TypeMap.wrapSample(maximum, type, false);
                final Class<? extends Number> 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, 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 = 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 description
     *            The sample dimension title or description, or {@code null} for the default
     *            (the name of what looks like the "main" category). This is the value to be
     *            returned by {@link #getDescription}.
     * @param categories
     *            The list of categories.
     * @param units
     *            The unit information for this sample dimension. May be {@code null} if
     *            no category has units. This unit apply to values obtained after the
     *            {@link #getSampleToGeophysics sampleToGeophysics} transformation.
     * @throws IllegalArgumentException
     *             if {@code categories} contains incompatible categories. If
     *             may be the case for example if two or more categories have
     *             overlapping ranges of sample values.
     *
     * @since 2.3
     */
    public GridSampleDimension(CharSequence description, 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(description, list(categories, units));
    }

    /** Constructs a list of categories. Used by constructors only. */
    private static CategoryList list(final Category[] categories, final Unit<?> units) {
        if (categories == null || categories.length == 0) {
            return null;
        }
        final CategoryList list = new CategoryList(categories, units);
        if (CategoryList.isGeophysics(categories, false)) return list;
        if (CategoryList.isGeophysics(categories, true )) return list.inverse;
        throw new IllegalArgumentException(Errors.format(ErrorKeys.MIXED_CATEGORIES));
    }

    /**
     * Constructs a sample dimension with the specified list of categories.
     *
     * @param description
     *            The sample dimension title or description, or {@code null} for the default
     *            (the name of what looks like the "main" category). This is the value to be
     *            returned by {@link #getDescription}.
     * @param list
     *            The list of categories, or {@code null}.
     */
    private GridSampleDimension(final CharSequence description, final CategoryList list) {
        /*
         * Checks the supplied description to see if it is null. In such a case it builds up a new
         * description by using the list of categories supplied. This second description may be less
         * human readable and it is therefore better if the user provide a meaningful name for this
         * sample dimension.
         */
        if (description != null) {
            this.description = SimpleInternationalString.wrap(description);
        } else {
            // we need to build one. Let's use the category list in
            // order to build the name of the sample dimension
            if (list != null) {
                this.description = list.getName();
            } else {
                this.description = Vocabulary.formatInternational(VocabularyKeys.UNTITLED);
            }
        }
        /*
         * Now process to the category examination.
         */
        MathTransform1D main = null;
        boolean isMainValid = true;
        boolean qualitative = false;
        if (list != null) {
            for (int i=list.size(); --i >= 0;) {
                final MathTransform1D candidate = list.get(i).getSampleToGeophysics();
                if (candidate == null) {
                    qualitative = true;
                    continue;
                }
                if (main != null) {
                    isMainValid &= main.equals(candidate);
                }
                main = candidate;
            }
            this.isGeophysics = list.isGeophysics(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 {@code null}.
     */
    protected GridSampleDimension(final GridSampleDimension other) {
        if (other != null) {
            inverse            = other.inverse;
            categories         = other.categories;
            isGeophysics       = other.isGeophysics;
            hasQualitative     = other.hasQualitative;
            hasQuantitative    = other.hasQuantitative;
            sampleToGeophysics = other.getSampleToGeophysics();
            description        = other.getDescription();
        } else {
            // 'inverse' will be set when needed.
            categories         = null;
            isGeophysics       = false;
            hasQualitative     = false;
            hasQuantitative    = false;
            sampleToGeophysics = null;
            description        = Vocabulary.formatInternational(VocabularyKeys.UNTITLED);
        }
    }

    /**
     * Wraps the specified OpenGIS's sample dimension into a Geotools's
     * implementation of {@code GridSampleDimension}.
     *
     * @param sd The sample dimension to wrap into a Geotools implementation.
     * @return The given sample dimension as a {@code GridSampleDimension} instance.
     */
    public static GridSampleDimension wrap(final SampleDimension sd) {
        if (sd instanceof GridSampleDimension) {
            return (GridSampleDimension) sd;
        }
        final int[][] palette = sd.getPalette();
        final Color[] colors;
        if (palette != null) {
            final int length = palette.length;
            colors = new Color[length];
            for (int i = 0; i < length; i++) {
                // Assuming RGB. It will be checked in the constructor.
                final int[] color = palette[i];
                colors[i] = new Color(color[0], color[1], color[2]);
            }
        } else {
            colors = null;
        }
        return new GridSampleDimension(
                sd.getDescription(),
                sd.getSampleDimensionType(),
                sd.getColorInterpretation(), colors,
                sd.getCategoryNames(),
                sd.getNoDataValues(),
                sd.getMinimumValue(),
                sd.getMaximumValue(),
                sd.getScale(),
                sd.getOffset(),
                sd.getUnits());
    }

    /**
     * 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.
     */
    @SuppressWarnings("unchecked")
  public SampleDimensionType getSampleDimensionType() {
        final NumberRange range = getRange();
        if (range == null) {
            return SampleDimensionType.REAL_32BITS;
        }
        return TypeMap.getSampleDimensionType(range);
    }

    /**
     * Gets the sample dimension title or description.
     * This string may be {@code null} if no description is present.
     *
     * @return The title or description of this sample dimension.
     */
    public InternationalString getDescription() {
        return description;
    }

    /**
     * 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:
     *
     *  <blockquote><pre>
     *    [0] Background
     *    [1] Water
     *    [2] Forest
     *    [3] Urban
     *  </pre></blockquote>
     *
     * @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<Category> getCategories() {
        if (categories == null) {
            return Collections.emptyList();
        } else {
            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 {@code null}.
     *
     * @param  sample The value (can be one of {@code NaN} values).
     * @return The category for the supplied value, or {@code 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 <A HREF="../gp/package-summary.html#Resample">resampled</A> (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 #getNoDataValues 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 {@code null}.
     * @deprecated check for no data values instead
     */
    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:
     *
     * <ul>
     *   <li>If {@link #getSampleToGeophysics} returns {@code null}, then {@code getNoDataValues()}
     *       returns {@code 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).</li>
     *
     *   <li>If {@link #getSampleToGeophysics} returns an identity transform, then
     *       {@code getNoDataValues()} returns {@code 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 {@code NaN} values.</li>
     *
     *   <li>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 {@code NaN} will be ignored.</li>
     * </ul>
     *
     * 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 {@code NaN}.
     *
     * @return The values to indicate no data values for this sample dimension,
     *         or {@code null} if not applicable.
     * @throws IllegalStateException if some qualitative categories use a range of
     *         non-integer values.
     *
     * @see #getSampleToGeophysics
     */
    public double[] getNoDataValues() throws IllegalStateException {
        if (!hasQuantitative) {
            return null;
        }
        int count = 0;
        double[] padValues = null;
        final int size = categories.size();
        for (int i=0; i<size; i++) {
            final Category category = categories.get(i);
            if (!category.isQuantitative()) {
                final double min = category.minimum;
                final double max = category.maximum;
                if (!Double.isNaN(min) || !Double.isNaN(max)) {
                    if (padValues == null) {
                        padValues = new double[size-i];
                    }
                    if (count >= 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 ||
                            !Classes.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 (inclusive). 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 #getRange
     */
    public double getMinimumValue() {
        if (categories != null && !categories.isEmpty()) {
            final double value = categories.get(0).minimum;
            if (!Double.isNaN(value)) {
                return value;
            }
        }
        return Double.NEGATIVE_INFINITY;
    }

    /**
     * Returns the maximum value occurring in this sample dimension (inclusive). 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 #getRange
     */
    public double getMaximumValue() {
        if (categories != null) {
            for (int i=categories.size(); --i>=0;) {
                final double value = 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 {@code 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 {@code null} if this sample dimension has no
     *         quantitative category.
     *
     * @see Category#getRange
     * @see #getMinimumValue
     * @see #getMaximumValue
     *
     * @todo We should do a better job in {@code CategoryList.getRange()} when selecting the
     *       appropriate data type. {@link TypeMap#getSampleDimensionType} may be of some help.
     */
    public NumberRange<? extends Number> getRange() {
        return (categories != null) ? categories.getRange() : null;
    }

    /**
     * Returns {@code true} if at least one value of {@code values} is
     * in the range {@code lower} inclusive to {@code upper} exclusive.
     */
    private static boolean rangeContains(final double   lower,
                                         final double   upper,
                                         final double[] values)
    {
        if (values != null) {
            for (int i=0; i<values.length; i++) {
                final double v = values[i];
                if (v>=lower && v<upper) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns a string representation of a sample value. This method try to returns
     * a representation of the geophysics value; the transformation is automatically
     * applied when necessary. More specifically:
     *
     * <ul>
     *   <li>If {@code value} maps a qualitative category, then the
     *       category name is returned as of {@link Category#getName}.</li>
     *
     *   <li>Otherwise, if {@code 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.</li>
     * </ul>
     *
     * @param  value  The sample value (can be one of {@code NaN} values).
     * @param  locale Locale to use for formatting, or {@code null} for the default locale.
     * @return A string representation of the geophysics value, or {@code null} if there is none.
     *
     * @todo What should we do when the value can't be formatted? {@code GridSampleDimension}
     *       returns {@code null} if there is no category or if an exception is thrown, but
     *       {@code 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 {@code null}
     * if this dimension has no units. This unit apply to values obtained after the
     * {@link #getSampleToGeophysics sampleToGeophysics} transformation.
     *
     * @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:
     *
     * <blockquote><pre>offset + scale*sample</pre></blockquote>
     *
     * 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:
     *
     * <blockquote><pre>offset + scale*sample</pre></blockquote>
     *
     * 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.
     * <p>
     * The <code>sampleToGeophysics.{@linkplain MathTransform1D#inverse() inverse()}</code>
     * 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 <cite>geophysics sample dimension</cite> 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 "<cite>real world</cite>" values
     * without the need for any transformation.
     * <p>
     * {@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<colors.length; i++) {
                colors[i] = new int[] {cm.getRed(i), cm.getGreen(i), cm.getBlue(i)};
            }
            return colors;
        }
        return null;
    }

    /**
     * Indicates the type of color palette entry for sample dimensions which have a
     * palette. If a sample dimension has a palette, the color interpretation must
     * be {@link ColorInterpretation#GRAY_INDEX GRAY_INDEX}
     * or {@link ColorInterpretation#PALETTE_INDEX PALETTE_INDEX}.
     * A palette entry type can be Gray, RGB, CMYK or HLS.
     *
     * @return The type of color palette entry for sample dimensions which have a palette.
     *
     */
    public PaletteInterpretation getPaletteInterpretation() {
        return PaletteInterpretation.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}.
     *
     */
    public ColorInterpretation getColorInterpretation() {
        // The 'Grid2DSampleDimension' class overrides this method
        // with better values for 'band' and 'numBands' constants.
        final int band     = 0;
        final int numBands = 1;
        return TypeMap.getColorInterpretation(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 {@link DataBuffer#TYPE_FLOAT} if this sample
     * dimension is {@linkplain org.geotools.coverage.grid.ViewType#GEOPHYSICS geophysics}, or
     * an integer data type otherwise.
     * <p>
     * Note that {@link org.geotools.coverage.grid.GridCoverage2D#getSampleDimension} returns
     * special implementations of {@code GridSampleDimension}. In this particular case,
     * the color model created by this {@code getColorModel()} method will have the same number of
     * bands than the grid coverage's {@link java.awt.image.RenderedImage}.
     *
     * @return The requested color model, suitable for {@link java.awt.image.RenderedImage} objects
     *         with values in the <code>{@link #getRange}</code> 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 <code>{@link #getRange}</code> 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 <code>{@link #getRange}</code> 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<categories.length; i++) {
            Category category = categories[i];
            if (category.isQuantitative()) {
                category = category.rescale(sampleToGeophysics);
            }
            category = category.geophysics(isGeophysics);
            if (!categories[i].equals(category)) {
                categories[i] = category;
                changed = true;
            }
        }
        return changed ? new GridSampleDimension(description, categories, getUnits()) : this;
    }

    /**
     * Returns a hash value for this sample dimension.
     * This value need not remain consistent between
     * different implementations of the same class.
     */
    @Override
    public int hashCode() {
        return (categories!=null) ? categories.hashCode() : (int) serialVersionUID;
    }

    /**
     * Compares the specified object with this sample dimension for equality.
     *
     * @param object The object to compare with.
     * @return {@code true} if the given object is equals to this sample dimension.
     */
    @Override
    public boolean equals(final Object object) {
        if (object == this) {
            // Slight optimization
            return true;
        }
        if (object instanceof GridSampleDimension) {
            final GridSampleDimension that = (GridSampleDimension) object;
            return Utilities.equals(this.categories, that.categories);
            // Since everything is deduced from CategoryList, two sample dimensions
            // should be equal if they have the same list of categories.
        }
        return false;
    }

    /**
     * Returns a string representation of this sample dimension.
     * This string is for debugging purpose only and may change
     * in future version. The default implementation format the
     * sample value range, then the list of categories. A "*"
     * mark is put in front of what seems the "main" category.
     */
    @Override
    public String toString() {
        if (categories != null) {
            return categories.toString(this, description);
        } else {
            return Classes.getShortClassName(this) + "[\"" + description + "\"]";
        }
    }

    /////////////////////////////////////////////////////////////////////////////////
    ////////                                                                 ////////
    ////////        REGISTRATION OF "SampleTranscode" IMAGE OPERATION        ////////
    ////////                                                                 ////////
    /////////////////////////////////////////////////////////////////////////////////

    /**
     * Register the "SampleTranscode" image operation.
     * Registration is done when the class is first loaded.
     *
     * @todo This static initializer will imply immediate class loading of a lot of
     *       JAI dependencies.  This is a pretty high overhead if JAI is not wanted
     *       right now. The correct approach is to declare the image operation into
     *       the {@code META-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 {@code CategoryList}.
     *       For now, we assume that people using the GC package probably want to work
     *       with {@link org.geotools.coverage.grid.GridCoverage2D}, which make extensive
     *       use of JAI. Peoples just working with {@link org.geotools.coverage.Coverage} are
     *       stuck with the overhead. Note that we register the image operation here because
     *       the only operation's argument is of type {@code GridSampleDimension[]}.
     *       Consequently, the image operation may be invoked at any time after class
     *       loading of {@link GridSampleDimension}.
     *       <p>
     *       Additional note: moving the initialization into the
     *       {@code 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 GC classes.
     */
    static {
        SampleTranscoder.register(JAI.getDefaultInstance());
    }
}
TOP

Related Classes of org.geotools.coverage.GridSampleDimension

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.