0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2003-2006, Geotools Project Management Committee (PMC)
0005: * (C) 2001, Institut de Recherche pour le Développement
0006: *
0007: * This library is free software; you can redistribute it and/or
0008: * modify it under the terms of the GNU Lesser General Public
0009: * License as published by the Free Software Foundation; either
0010: * version 2.1 of the License, or (at your option) any later version.
0011: *
0012: * This library is distributed in the hope that it will be useful,
0013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0015: * Lesser General Public License for more details.
0016: *
0017: * This package contains documentation from OpenGIS specifications.
0018: * OpenGIS consortium's work is fully acknowledged here.
0019: */
0020: package org.geotools.coverage;
0021:
0022: // J2SE dependencies and extensions
0023: import java.awt.Color;
0024: import java.awt.image.ColorModel;
0025: import java.awt.image.DataBuffer; // For javadoc
0026: import java.awt.image.IndexColorModel;
0027: import java.awt.image.RenderedImage;
0028: import java.io.Serializable;
0029: import java.util.ArrayList;
0030: import java.util.Arrays;
0031: import java.util.List;
0032: import java.util.Locale;
0033: import javax.units.Unit;
0034:
0035: // JAI dependencies
0036: import javax.media.jai.JAI;
0037: import javax.media.jai.util.Range;
0038:
0039: // OpenGIS dependencies
0040: import org.geotools.util.SimpleInternationalString;
0041: import org.opengis.coverage.ColorInterpretation;
0042: import org.opengis.coverage.MetadataNameNotFoundException;
0043: import org.opengis.coverage.PaletteInterpretation;
0044: import org.opengis.coverage.SampleDimension;
0045: import org.opengis.coverage.SampleDimensionType;
0046: import org.opengis.referencing.operation.MathTransform1D;
0047: import org.opengis.referencing.operation.TransformException;
0048: import org.opengis.util.InternationalString;
0049:
0050: // Geotools dependencies
0051: import org.geotools.referencing.operation.transform.LinearTransform1D;
0052: import org.geotools.resources.ClassChanger;
0053: import org.geotools.resources.Utilities;
0054: import org.geotools.resources.XArray;
0055: import org.geotools.resources.XMath;
0056: import org.geotools.resources.i18n.Errors;
0057: import org.geotools.resources.i18n.ErrorKeys;
0058: import org.geotools.resources.i18n.Vocabulary;
0059: import org.geotools.resources.i18n.VocabularyKeys;
0060: import org.geotools.resources.image.ColorUtilities;
0061: import org.geotools.util.NumberRange;
0062:
0063: /**
0064: * Describes the data values for a coverage as a list of {@linkplain Category categories}. For
0065: * a grid coverage a sample dimension is a band. Sample values in a band may be organized in
0066: * categories. This {@code GridSampleDimension} implementation is capable to differenciate
0067: * <em>qualitative</em> and <em>quantitative</em> categories. For example an image of sea surface
0068: * temperature (SST) could very well defines the following categories:
0069: *
0070: * <blockquote><pre>
0071: * [0] : no data
0072: * [1] : cloud
0073: * [2] : land
0074: * [10..210] : temperature to be converted into Celsius degrees through a linear equation
0075: * </pre></blockquote>
0076: *
0077: * In this example, sample values in range {@code [10..210]} defines a quantitative category,
0078: * while all others categories are qualitative. The difference between those two kinds of category
0079: * is that the {@link Category#getSampleToGeophysics} method returns a non-null transform if and
0080: * only if the category is quantitative.
0081: * <p>
0082: * While this class can be used with arbitrary {@linkplain org.opengis.coverage.Coverage coverage},
0083: * the primary target for this implementation is {@linkplain org.opengis.coverage.grid.GridCoverage
0084: * grid coverage} storing their sample values as integers. This explain the "{@code Grid}" prefix
0085: * in the class name.
0086: *
0087: * @since 2.1
0088: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/coverage/GridSampleDimension.java $
0089: * @version $Id: GridSampleDimension.java 23398 2006-12-12 05:57:00Z desruisseaux $
0090: * @author Martin Desruisseaux
0091: */
0092: public class GridSampleDimension implements SampleDimension,
0093: Serializable {
0094: /**
0095: * Serial number for interoperability with different versions.
0096: */
0097: private static final long serialVersionUID = 6026936545776852758L;
0098:
0099: /**
0100: * An empty array of metadata names.
0101: */
0102: private static final String[] EMPTY_METADATA = new String[0];
0103:
0104: /**
0105: * A sample dimension wrapping the list of categories {@code CategoryList.inverse}.
0106: * This object is constructed and returned by {@link #geophysics}. Constructed when first
0107: * needed, but serialized anyway because it may be a user-supplied object.
0108: */
0109: private GridSampleDimension inverse;
0110:
0111: /**
0112: * The category list for this sample dimension, or {@code null} if this sample
0113: * dimension has no category. This field is read by {@code SampleTranscoder} only.
0114: */
0115: final CategoryList categories;
0116:
0117: /**
0118: * {@code true} if all categories in this sample dimension have been already scaled
0119: * to geophysics ranges. If {@code true}, then the {@link #getSampleToGeophysics()}
0120: * method should returns an identity transform. Note that the opposite do not always hold:
0121: * an identity transform doesn't means that all categories are geophysics. For example,
0122: * some qualitative categories may map to some values differents than {@code NaN}.
0123: * <p>
0124: * Assertions:
0125: * <ul>
0126: * <li>{@code isGeophysics} == {@code categories.isScaled(true)}.</li>
0127: * <li>{@code isGeophysics} != {@code categories.isScaled(false)}, except
0128: * if {@code categories.geophysics(true) == categories.geophysics(false)}</li>
0129: * </ul>
0130: */
0131: private final boolean isGeophysics;
0132:
0133: /**
0134: * {@code true} if this sample dimension has at least one qualitative category.
0135: * An arbitrary number of qualitative categories is allowed, providing their sample
0136: * value ranges do not overlap. A sample dimension can have both qualitative and
0137: * quantitative categories.
0138: */
0139: private final boolean hasQualitative;
0140:
0141: /**
0142: * {@code true} if this sample dimension has at least one quantitative category.
0143: * An arbitrary number of quantitative categories is allowed, providing their sample
0144: * value ranges do not overlap.
0145: * <p>
0146: * If {@code sampleToGeophysics} is non-null, then {@code hasQuantitative}
0147: * <strong>must</strong> be true. However, the opposite do not hold in all cases: a
0148: * {@code true} value doesn't means that {@code sampleToGeophysics} should
0149: * be non-null.
0150: */
0151: private final boolean hasQuantitative;
0152:
0153: /**
0154: * The {@link Category#getSampleToGeophysics sampleToGeophysics} transform used by every
0155: * quantitative {@link Category}, or {@code null}. This field may be null for two
0156: * reasons:
0157: *
0158: * <ul>
0159: * <li>There is no quantitative category in this sample dimension.</li>
0160: * <li>There is more than one quantitative category, and all of them
0161: * don't use the same {@link Category#getSampleToGeophysics
0162: * sampleToGeophysics} transform.</li>
0163: * </ul>
0164: *
0165: * This field is used by {@link #getOffset} and {@link #getScale}. The
0166: * {@link #getSampleToGeophysics} method may also returns directly this
0167: * value in some conditions.
0168: */
0169: private final MathTransform1D sampleToGeophysics;
0170:
0171: /**
0172: * Decription for this sample dimension. It is particularly important to
0173: * have the possiblity to specify a description for a sample dimension in
0174: * order to be able to perform a band select by using human comprehensible
0175: * descriptions instead of just numbers. As an instance a service like the
0176: * WCS would use this feature in order to perform band subsetting as
0177: * directed from a user request.
0178: */
0179: private final InternationalString description;
0180:
0181: /**
0182: * Constructs a sample dimension with no description and no category.
0183: *
0184: * @deprecated Use {@link #GridSampleDimension(CharSequence)} instead.
0185: */
0186: public GridSampleDimension() {
0187: this (null, (CategoryList) null);
0188: }
0189:
0190: /**
0191: * Constructs a sample dimension with specified name and no category.
0192: *
0193: * @param description
0194: * The sample dimension title or description, or {@code null} if
0195: * none. This is the value to be returned by {@link #getDescription}.
0196: *
0197: * @since 2.3
0198: */
0199: public GridSampleDimension(final CharSequence description) {
0200: this (description, (CategoryList) null);
0201: }
0202:
0203: /**
0204: * Constructs a sample dimension with a set of qualitative categories only.
0205: * This constructor expects only a sequence of category names for the values
0206: * contained in a sample dimension. This allows for names to be assigned to
0207: * numerical values. The first entry in the sequence relates to a cell value
0208: * of zero. For example: [0]="Background", [1]="Water", [2]="Forest",
0209: * [3]="Urban". The created sample dimension will have no unit and a default
0210: * set of colors.
0211: *
0212: * @param description
0213: * The sample dimension title or description, or {@code null} for the default
0214: * (the name of what looks like the "main" category). This is the value to be
0215: * returned by {@link #getDescription}.
0216: * @param categoriesNames
0217: * Sequence of category names for the values contained in a
0218: * sample dimension, as {@link String} or
0219: * {@link InternationalString} objects.
0220: *
0221: * @since 2.3
0222: */
0223: public GridSampleDimension(final CharSequence description,
0224: final CharSequence[] categoriesNames) {
0225: // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999
0226: // ("Relax constraint on placement of this()/super() call in constructors").
0227: this (description, list(categoriesNames));
0228: }
0229:
0230: /**
0231: * Constructs a sample dimension with a set of qualitative categories only.
0232: * This constructor expects only a sequence of category names for the values
0233: * contained in a sample dimension. This allows for names to be assigned to
0234: * numerical values. The first entry in the sequence relates to a cell value
0235: * of zero. For example: [0]="Background", [1]="Water", [2]="Forest",
0236: * [3]="Urban". The created sample dimension will have no unit and a default
0237: * set of colors.
0238: *
0239: * @param categoriesNames
0240: * Sequence of category names for the values contained in a
0241: * sample dimension, as {@link String} or
0242: * {@link InternationalString} objects.
0243: *
0244: * @deprecated Replaced by {@link #GridSampleDimension(CharSequence, CharSequence[])}.
0245: */
0246: public GridSampleDimension(final CharSequence[] categoriesNames) {
0247: this (null, categoriesNames);
0248: }
0249:
0250: /** Constructs a list of categories. Used by constructors only. */
0251: private static CategoryList list(final CharSequence[] names) {
0252: final int length = names.length;
0253: final Color[] colors = new Color[length];
0254: final double scale = 255.0 / length;
0255: for (int i = 0; i < length; i++) {
0256: final int r = (int) Math.round(scale * i);
0257: colors[i] = new Color(r, r, r);
0258: }
0259: return list(names, colors);
0260: }
0261:
0262: /**
0263: * Constructs a sample dimension with a set of qualitative categories and
0264: * colors. This constructor expects a sequence of category names for the
0265: * values contained in a sample dimension. This allows for names to be
0266: * assigned to numerical values. The first entry in the sequence relates to
0267: * a cell value of zero. For example: [0]="Background", [1]="Water",
0268: * [2]="Forest", [3]="Urban". The created sample dimension will have no unit
0269: * and a default set of colors.
0270: *
0271: * @param description
0272: * The sample dimension title or description, or {@code null} for the default
0273: * (the name of what looks like the "main" category). This is the value to be
0274: * returned by {@link #getDescription}.
0275: * @param names
0276: * Sequence of category names for the values contained in a
0277: * sample dimension, as {@link String} or
0278: * {@link InternationalString} objects.
0279: * @param colors
0280: * Color to assign to each category. This array must have the
0281: * same length than {@code names}.
0282: *
0283: * @since 2.3
0284: */
0285: public GridSampleDimension(final CharSequence description,
0286: final CharSequence[] names, final Color[] colors) {
0287: // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999
0288: // ("Relax constraint on placement of this()/super() call in constructors").
0289: this (description, list(names, colors));
0290: }
0291:
0292: /**
0293: * Constructs a sample dimension with a set of qualitative categories and
0294: * colors. This constructor expects a sequence of category names for the
0295: * values contained in a sample dimension. This allows for names to be
0296: * assigned to numerical values. The first entry in the sequence relates to
0297: * a cell value of zero. For example: [0]="Background", [1]="Water",
0298: * [2]="Forest", [3]="Urban". The created sample dimension will have no unit
0299: * and a default set of colors.
0300: *
0301: * @param names
0302: * Sequence of category names for the values contained in a
0303: * sample dimension, as {@link String} or
0304: * {@link InternationalString} objects.
0305: * @param colors
0306: * Color to assign to each category. This array must have the
0307: * same length than {@code names}.
0308: *
0309: * @deprecated Replaced by {@link #GridSampleDimension(CharSequence, CharSequence[], Color[])}.
0310: */
0311: public GridSampleDimension(final CharSequence[] names,
0312: final Color[] colors) {
0313: this (null, names, colors);
0314: }
0315:
0316: /** Constructs a list of categories. Used by constructors only. */
0317: private static CategoryList list(final CharSequence[] names,
0318: final Color[] colors) {
0319: if (names.length != colors.length) {
0320: throw new IllegalArgumentException(Errors
0321: .format(ErrorKeys.MISMATCHED_ARRAY_LENGTH));
0322: }
0323: final int length = names.length;
0324: final Category[] categories = new Category[length];
0325: for (int i = 0; i < length; i++) {
0326: categories[i] = new Category(names[i], colors[i], i);
0327: }
0328: return list(categories, null);
0329: }
0330:
0331: /**
0332: * Constructs a sample dimension with the specified properties. For
0333: * convenience, any argument which is not a {@code double} primitive can be
0334: * {@code null}, and any {@linkplain CharSequence char sequence} can be
0335: * either a {@link String} or {@link InternationalString} object.
0336: * <p>
0337: * This constructor allows the construction of a {@code GridSampleDimension}
0338: * without explicit construction of {@link Category} objects. An heuristic
0339: * approach is used for dispatching the informations into a set of
0340: * {@link Category} objects. However, this constructor still less general
0341: * and provides less fine-grain control than the constructor expecting an
0342: * array of {@link Category} objects.
0343: *
0344: * @param description
0345: * The sample dimension title or description, or {@code null} for the default
0346: * (the name of what looks like the "main" category). This is the value to be
0347: * returned by {@link #getDescription}.
0348: * @param type
0349: * The grid value data type (which indicate the number of bits
0350: * for the data type), or {@code null} for computing it
0351: * automatically from the range {@code [minimum..maximum]}. This
0352: * is the value to be returned by {@link #getSampleDimensionType}.
0353: * @param color
0354: * The color interpretation, or {@code null} for a default value
0355: * (usually
0356: * {@link ColorInterpretation#PALETTE_INDEX PALETTE_INDEX}).
0357: * This is the value to be returned by
0358: * {@link #getColorInterpretation}.
0359: * @param palette
0360: * The color palette associated with the sample dimension, or
0361: * {@code null} for a default color palette (usually grayscale).
0362: * If {@code categories} is non-null, then both arrays usually
0363: * have the same length. However, this constructor is tolerant on
0364: * this array length. This is the value to be returned
0365: * (indirectly) by {@link #getColorModel}.
0366: * @param categories
0367: * A sequence of category names for the values contained in the
0368: * sample dimension, or {@code null} if none. This is the values
0369: * to be returned by {@link #getCategoryNames}.
0370: * @param nodata
0371: * the values to indicate "no data", or {@code null} if none.
0372: * This is the values to be returned by {@link #getNoDataValues}.
0373: * @param minimum
0374: * The lower value, inclusive. The {@code [minimum..maximum]}
0375: * range may or may not includes the {@code nodata} values; the
0376: * range will be adjusted as needed. If {@code categories} was
0377: * non-null, then {@code minimum} is usually 0. This is the value
0378: * to be returned by {@link #getMinimumValue}.
0379: * @param maximum
0380: * The upper value, <strong>inclusive</strong> as well. The
0381: * {@code [minimum..maximum]} range may or may not includes the
0382: * {@code nodata} values; the range will be adjusted as needed.
0383: * If {@code categories} was non-null, then {@code maximum} is
0384: * usually equals to {@code categories.length-1}. This is the
0385: * value to be returned by {@link #getMaximumValue}.
0386: * @param scale
0387: * The value which is multiplied to grid values, or 1 if none.
0388: * This is the value to be returned by {@link #getScale}.
0389: * @param offset
0390: * The value to add to grid values, or 0 if none. This is the
0391: * value to be returned by {@link #getOffset}.
0392: * @param unit
0393: * The unit information for this sample dimension, or
0394: * {@code null} if none. This is the value to be returned by
0395: * {@link #getUnits}.
0396: *
0397: * @throws IllegalArgumentException
0398: * if the range {@code [minimum..maximum]} is not valid.
0399: */
0400: public GridSampleDimension(final CharSequence description,
0401: final SampleDimensionType type,
0402: final ColorInterpretation color, final Color[] palette,
0403: final CharSequence[] categories, final double[] nodata,
0404: final double minimum, final double maximum,
0405: final double scale, final double offset, final Unit unit) {
0406: // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999
0407: // ("Relax constraint on placement of this()/super() call in constructors").
0408: this (description, list(description, type, color, palette,
0409: categories, nodata, minimum, maximum, scale, offset,
0410: unit));
0411: }
0412:
0413: /** Constructs a list of categories. Used by constructors only. */
0414: private static CategoryList list(CharSequence description,
0415: SampleDimensionType type, ColorInterpretation color,
0416: final Color[] palette, final CharSequence[] categories,
0417: final double[] nodata, double minimum, double maximum,
0418: final double scale, final double offset, final Unit unit) {
0419: if (description == null) {
0420: description = Vocabulary
0421: .formatInternational(VocabularyKeys.UNTITLED);
0422: }
0423: if (Double.isInfinite(minimum) || Double.isInfinite(maximum)
0424: || !(minimum < maximum)) {
0425: throw new IllegalArgumentException(Errors.format(
0426: ErrorKeys.BAD_RANGE_$2, new Double(minimum),
0427: new Double(maximum)));
0428: }
0429: if (Double.isNaN(scale) || Double.isInfinite(scale)
0430: || scale == 0) {
0431: throw new IllegalArgumentException(Errors.format(
0432: ErrorKeys.BAD_PARAMETER_$2, "scale", new Double(
0433: scale)));
0434: }
0435: if (Double.isNaN(offset) || Double.isInfinite(offset)) {
0436: throw new IllegalArgumentException(Errors.format(
0437: ErrorKeys.BAD_PARAMETER_$2, "offset", new Double(
0438: offset)));
0439: }
0440: if (type == null) {
0441: type = TypeMap.getSampleDimensionType(minimum, maximum);
0442: }
0443: if (color == null) {
0444: color = ColorInterpretation.PALETTE_INDEX;
0445: }
0446: final int nameCount = (categories != null) ? categories.length
0447: : 0;
0448: final int nodataCount = (nodata != null) ? nodata.length : 0;
0449: final List categoryList = new ArrayList(nameCount + nodataCount
0450: + 2);
0451: /*
0452: * STEP 1 - Add a qualitative category for each 'nodata' value.
0453: * NAME: Fetched from 'categories' if available, otherwise default to the value.
0454: * COLOR: Fetched from 'palette' if available, otherwise use Category default.
0455: */
0456: for (int i = 0; i < nodataCount; i++) {
0457: CharSequence name = null;
0458: final double padValue = nodata[i];
0459: final int intValue = (int) Math.floor(padValue);
0460: if (intValue >= 0 && intValue < nameCount) {
0461: if (intValue == padValue) {
0462: // This category will be added in step 2 below.
0463: continue;
0464: }
0465: name = categories[intValue];
0466: }
0467: final Number value = TypeMap.wrapSample(padValue, type,
0468: false);
0469: if (name == null) {
0470: name = value.toString();
0471: }
0472: final NumberRange range = new NumberRange(value.getClass(),
0473: value, value);
0474: final Color[] colors = ColorUtilities.subarray(palette,
0475: intValue, intValue + 1);
0476: categoryList.add(new Category(name, colors, range,
0477: (MathTransform1D) null));
0478: }
0479: /*
0480: * STEP 2 - Add a qualitative category for each category name.
0481: * RANGE: Fetched from the index (position) in the 'categories' array.
0482: * COLOR: Fetched from 'palette' if available, otherwise use Category default.
0483: */
0484: if (nameCount != 0) {
0485: int lower = 0;
0486: final int length = categories.length;
0487: for (int upper = 1; upper <= length; upper++) {
0488: if (upper != length
0489: && categories[lower].toString().trim()
0490: .equalsIgnoreCase(
0491: categories[upper].toString()
0492: .trim())) {
0493: // If there is a suite of categories with identical name, create only one
0494: // category with range [lower..upper] instead of one new category for each
0495: // sample value.
0496: continue;
0497: }
0498: final CharSequence name = categories[lower];
0499: Number min = TypeMap.wrapSample(lower, type, false);
0500: Number max = TypeMap.wrapSample(upper - 1, type, false);
0501: final Class classe;
0502: if (min.equals(max)) {
0503: min = max;
0504: classe = max.getClass();
0505: } else {
0506: classe = ClassChanger.getWidestClass(min, max);
0507: min = ClassChanger.cast(min, classe);
0508: max = ClassChanger.cast(max, classe);
0509: }
0510: final NumberRange range = new NumberRange(classe, min,
0511: max);
0512: final Color[] colors = ColorUtilities.subarray(palette,
0513: lower, upper);
0514: categoryList.add(new Category(name, colors, range,
0515: (MathTransform1D) null));
0516: lower = upper;
0517: }
0518: }
0519: /*
0520: * STEP 3 - Changes some qualitative categories into quantitative ones. The hard questions
0521: * is: do we want to mark a category as "quantitative"? OpenGIS has no notion of
0522: * "qualitative" versus "quantitative" category. As an heuristic approach, we will
0523: * look for quantitative category if:
0524: *
0525: * - 'scale' and 'offset' do not map to an identity transform. Those
0526: * coefficients can be stored in quantitative category only.
0527: *
0528: * - 'nodata' were specified. If the user wants to declare "nodata" values,
0529: * then we can reasonably assume that he have real values somewhere else.
0530: *
0531: * - Only 1 category were created so far. A classified raster with only one
0532: * category is useless. Consequently, it is probably a numeric raster instead.
0533: */
0534: boolean needQuantitative = false;
0535: if (scale != 1 || offset != 0 || nodataCount != 0
0536: || categoryList.size() <= 1) {
0537: needQuantitative = true;
0538: for (int i = categoryList.size(); --i >= 0;) {
0539: Category category = (Category) categoryList.get(i);
0540: if (!category.isQuantitative()) {
0541: final NumberRange range = category.getRange();
0542: final Comparable min = range.getMinValue();
0543: final Comparable max = range.getMaxValue();
0544: if (min.compareTo(max) != 0) {
0545: final double xmin = ((Number) min)
0546: .doubleValue();
0547: final double xmax = ((Number) max)
0548: .doubleValue();
0549: if (!rangeContains(xmin, xmax, nodata)) {
0550: final InternationalString name = category
0551: .getName();
0552: final Color[] colors = category.getColors();
0553: category = new Category(name, colors,
0554: range, scale, offset);
0555: categoryList.set(i, category);
0556: needQuantitative = false;
0557: }
0558: }
0559: }
0560: }
0561: }
0562: /*
0563: * STEP 4 - Create at most one quantitative category for the remaining sample values.
0564: * The new category will range from 'minimum' to 'maximum' inclusive, minus
0565: * all ranges used by previous categories. If there is no range left, then
0566: * no new category will be created. This step will be executed only if the
0567: * information provided by the user seem to be incomplete.
0568: *
0569: * Note that substractions way break a range into many smaller ranges.
0570: * The naive algorithm used here try to keep the widest range.
0571: */
0572: if (needQuantitative) {
0573: boolean minIncluded = true;
0574: boolean maxIncluded = true;
0575: for (int i = categoryList.size(); --i >= 0;) {
0576: final NumberRange range = ((Category) categoryList
0577: .get(i)).getRange();
0578: final double min = range.getMinimum();
0579: final double max = range.getMaximum();
0580: if (max - minimum < maximum - min) {
0581: if (max >= minimum) {
0582: // We are loosing some sample values in
0583: // the lower range because of nodata values.
0584: minimum = max;
0585: minIncluded = !range.isMaxIncluded();
0586: }
0587: } else {
0588: if (min <= maximum) {
0589: // We are loosing some sample values in
0590: // the upper range because of nodata values.
0591: maximum = min;
0592: maxIncluded = !range.isMinIncluded();
0593: }
0594: }
0595: }
0596: // If the remaining range is wide enough, add the category.
0597: if (maximum - minimum > (minIncluded && maxIncluded ? 0 : 1)) {
0598: Number min = TypeMap.wrapSample(minimum, type, false);
0599: Number max = TypeMap.wrapSample(maximum, type, false);
0600: final Class classe = ClassChanger.getWidestClass(min,
0601: max);
0602: min = ClassChanger.cast(min, classe);
0603: max = ClassChanger.cast(max, classe);
0604: final NumberRange range = new NumberRange(classe, min,
0605: minIncluded, max, maxIncluded);
0606: final Color[] colors = ColorUtilities.subarray(palette,
0607: (int) Math.ceil(minimum), (int) Math
0608: .floor(maximum));
0609: categoryList.add(new Category(description, colors,
0610: range, scale, offset));
0611: needQuantitative = false;
0612: }
0613: }
0614: /*
0615: * STEP 5 - Now, the list of categories should be complete. Construct a
0616: * sample dimension appropriate for the type of palette used.
0617: */
0618: final Category[] cl = (Category[]) categoryList
0619: .toArray(new Category[categoryList.size()]);
0620: if (ColorInterpretation.PALETTE_INDEX.equals(color)
0621: || ColorInterpretation.GRAY_INDEX.equals(color)) {
0622: return list(cl, unit);
0623: }
0624: throw new UnsupportedOperationException("Not yet implemented");
0625: }
0626:
0627: /**
0628: * Constructs a sample dimension with an arbitrary set of categories, which
0629: * may be both quantitative and qualitative. It is possible to specify more
0630: * than one quantitative categories, providing that their sample value
0631: * ranges do not overlap. Quantitative categories can map sample values to
0632: * geophysics values using arbitrary relation (not necessarly linear).
0633: *
0634: * @param description
0635: * The sample dimension title or description, or {@code null} for the default
0636: * (the name of what looks like the "main" category). This is the value to be
0637: * returned by {@link #getDescription}.
0638: * @param categories
0639: * The list of categories.
0640: * @param units
0641: * The unit information for this sample dimension. May be
0642: * {@code null} if no category has units. This unit apply to
0643: * values obtained after the
0644: * {@link #getSampleToGeophysics sampleToGeophysics}
0645: * transformation.
0646: * @throws IllegalArgumentException
0647: * if {@code categories} contains incompatible categories. If
0648: * may be the case for example if two or more categories have
0649: * overlapping ranges of sample values.
0650: *
0651: * @since 2.3
0652: */
0653: public GridSampleDimension(CharSequence description,
0654: Category[] categories, Unit units)
0655: throws IllegalArgumentException {
0656: // TODO: 'list(...)' should be inlined there if only Sun was to fix RFE #4093999
0657: // ("Relax constraint on placement of this()/super() call in constructors").
0658: this (description, list(categories, units));
0659: }
0660:
0661: /**
0662: * Constructs a sample dimension with an arbitrary set of categories, which
0663: * may be both quantitative and qualitative. It is possible to specify more
0664: * than one quantitative categories, providing that their sample value
0665: * ranges do not overlap. Quantitative categories can map sample values to
0666: * geophysics values using arbitrary relation (not necessarly linear).
0667: *
0668: * @param categories
0669: * The list of categories.
0670: * @param units
0671: * The unit information for this sample dimension. May be
0672: * {@code null} if no category has units. This unit apply to
0673: * values obtained after the
0674: * {@link #getSampleToGeophysics sampleToGeophysics}
0675: * transformation.
0676: * @throws IllegalArgumentException
0677: * if {@code categories} contains incompatible categories. If
0678: * may be the case for example if two or more categories have
0679: * overlapping ranges of sample values.
0680: *
0681: * @deprecated Use {@link #GridSampleDimension(CharSequence, Category[], Unit)} instead.
0682: */
0683: public GridSampleDimension(final Category[] categories,
0684: final Unit units) throws IllegalArgumentException {
0685: this (null, categories, units);
0686: }
0687:
0688: /** Construct a list of categories. Used by constructors only. */
0689: private static CategoryList list(final Category[] categories,
0690: final Unit units) {
0691: if (categories == null) {
0692: return null;
0693: }
0694: final CategoryList list = new CategoryList(categories, units);
0695: if (CategoryList.isScaled(categories, false))
0696: return list;
0697: if (CategoryList.isScaled(categories, true))
0698: return list.inverse;
0699: throw new IllegalArgumentException(Errors
0700: .format(ErrorKeys.MIXED_CATEGORIES));
0701: }
0702:
0703: /**
0704: * Constructs a sample dimension with the specified list of categories.
0705: *
0706: * @param description
0707: * The sample dimension title or description, or {@code null} for the default
0708: * (the name of what looks like the "main" category). This is the value to be
0709: * returned by {@link #getDescription}.
0710: * @param list
0711: * The list of categories, or {@code null}.
0712: */
0713: private GridSampleDimension(final CharSequence description,
0714: final CategoryList list) {
0715: /*
0716: * Checks the supplied description to see if it is null. In such a case it
0717: * builds up a new description by using the list of categories supplied.
0718: * This secondo description is much less human readable and it is therefore
0719: * much better if the user provide a meaningful name for this sample
0720: * dimension.
0721: */
0722: if (description != null) {
0723: this .description = SimpleInternationalString
0724: .wrap(description);
0725: } else {
0726: // we need to build one. Let's use the category list in
0727: // order to build the name of the sample dimension
0728: if (list != null) {
0729: this .description = list.getName();
0730: } else {
0731: this .description = Vocabulary
0732: .formatInternational(VocabularyKeys.UNTITLED);
0733: }
0734: }
0735: /*
0736: * Now process to the category examination.
0737: */
0738: MathTransform1D main = null;
0739: boolean isMainValid = true;
0740: boolean qualitative = false;
0741: if (list != null) {
0742: for (int i = list.size(); --i >= 0;) {
0743: final MathTransform1D candidate = ((Category) list
0744: .get(i)).getSampleToGeophysics();
0745: if (candidate == null) {
0746: qualitative = true;
0747: continue;
0748: }
0749: if (main != null) {
0750: isMainValid &= main.equals(candidate);
0751: }
0752: main = candidate;
0753: }
0754: this .isGeophysics = list.isScaled(true);
0755: } else {
0756: this .isGeophysics = false;
0757: }
0758: this .categories = list;
0759: this .hasQualitative = qualitative;
0760: this .hasQuantitative = (main != null);
0761: this .sampleToGeophysics = isMainValid ? main : null;
0762: }
0763:
0764: /**
0765: * Constructs a new sample dimension with the same categories and
0766: * units than the specified sample dimension.
0767: *
0768: * @param other The other sample dimension, or {@code null}.
0769: */
0770: protected GridSampleDimension(final GridSampleDimension other) {
0771: if (other != null) {
0772: inverse = other.inverse;
0773: categories = other.categories;
0774: isGeophysics = other.isGeophysics;
0775: hasQualitative = other.hasQualitative;
0776: hasQuantitative = other.hasQuantitative;
0777: sampleToGeophysics = other.sampleToGeophysics;
0778: description = other.description;
0779: } else {
0780: // 'inverse' will be set when needed.
0781: categories = null;
0782: isGeophysics = false;
0783: hasQualitative = false;
0784: hasQuantitative = false;
0785: sampleToGeophysics = null;
0786: description = Vocabulary
0787: .formatInternational(VocabularyKeys.UNTITLED);
0788: }
0789: }
0790:
0791: /**
0792: * Wrap the specified OpenGIS's sample dimension into a Geotools's
0793: * implementation of {@code GridSampleDimension}.
0794: *
0795: * @param sd
0796: * The sample dimension to wrap into a Geotools implementation.
0797: */
0798: public static GridSampleDimension wrap(final SampleDimension sd) {
0799: if (sd instanceof GridSampleDimension) {
0800: return (GridSampleDimension) sd;
0801: }
0802: final int[][] palette = sd.getPalette();
0803: final Color[] colors;
0804: if (palette != null) {
0805: final int length = palette.length;
0806: colors = new Color[length];
0807: for (int i = 0; i < length; i++) {
0808: // Assuming RGB. It will be checked in the constructor.
0809: final int[] color = palette[i];
0810: colors[i] = new Color(color[0], color[1], color[2]);
0811: }
0812: } else {
0813: colors = null;
0814: }
0815: return new GridSampleDimension(sd.getDescription(), sd
0816: .getSampleDimensionType(), sd.getColorInterpretation(),
0817: colors, sd.getCategoryNames(), sd.getNoDataValues(), sd
0818: .getMinimumValue(), sd.getMaximumValue(), sd
0819: .getScale(), sd.getOffset(), sd.getUnits());
0820: }
0821:
0822: /**
0823: * Returns a code value indicating grid value data type.
0824: * This will also indicate the number of bits for the data type.
0825: *
0826: * @return a code value indicating grid value data type.
0827: */
0828: public SampleDimensionType getSampleDimensionType() {
0829: final NumberRange range = getRange();
0830: if (range == null) {
0831: return SampleDimensionType.REAL_32BITS;
0832: }
0833: return TypeMap.getSampleDimensionType(range);
0834: }
0835:
0836: /**
0837: * Get the sample dimension title or description.
0838: * This string may be {@code null} if no description is present.
0839: */
0840: public InternationalString getDescription() {
0841: return description;
0842: }
0843:
0844: /**
0845: * Returns a sequence of category names for the values contained in this sample dimension.
0846: * This allows for names to be assigned to numerical values. The first entry in the sequence
0847: * relates to a cell value of zero. For example:
0848: *
0849: * <blockquote><pre>
0850: * [0] Background
0851: * [1] Water
0852: * [2] Forest
0853: * [3] Urban
0854: * </pre></blockquote>
0855: *
0856: * @return The sequence of category names for the values contained in this sample dimension,
0857: * or {@code null} if there is no category in this sample dimension.
0858: * @throws IllegalStateException if a sequence can't be mapped because some category use
0859: * negative or non-integer sample values.
0860: *
0861: * @see #getCategories
0862: * @see #getCategory
0863: */
0864: public InternationalString[] getCategoryNames()
0865: throws IllegalStateException {
0866: if (categories == null) {
0867: return null;
0868: }
0869: if (categories.isEmpty()) {
0870: return new InternationalString[0];
0871: }
0872: InternationalString[] names = null;
0873: for (int i = categories.size(); --i >= 0;) {
0874: final Category category = (Category) categories.get(i);
0875: final int lower = (int) category.minimum;
0876: final int upper = (int) category.maximum;
0877: if (lower != category.minimum || lower < 0
0878: || upper != category.maximum || upper < 0) {
0879: throw new IllegalStateException(Errors
0880: .format(ErrorKeys.NON_INTEGER_CATEGORY));
0881: }
0882: if (names == null) {
0883: names = new InternationalString[upper + 1];
0884: }
0885: Arrays.fill(names, lower, upper + 1, category.getName());
0886: }
0887: return names;
0888: }
0889:
0890: /**
0891: * Returns all categories in this sample dimension. Note that a {@link Category} object may
0892: * apply to an arbitrary range of sample values. Consequently, the first element in this
0893: * collection may not be directly related to the sample value {@code 0}.
0894: *
0895: * @return The list of categories in this sample dimension, or {@code null} if none.
0896: *
0897: * @see #getCategoryNames
0898: * @see #getCategory
0899: */
0900: public List getCategories() {
0901: return categories;
0902: }
0903:
0904: /**
0905: * Returns the category for the specified sample value. If this method can't maps
0906: * a category to the specified value, then it returns {@code null}.
0907: *
0908: * @param sample The value (can be one of {@code NaN} values).
0909: * @return The category for the supplied value, or {@code null} if none.
0910: *
0911: * @see #getCategories
0912: * @see #getCategoryNames
0913: */
0914: public Category getCategory(final double sample) {
0915: return (categories != null) ? categories.getCategory(sample)
0916: : null;
0917: }
0918:
0919: /**
0920: * Returns a default category to use for background. A background category is used
0921: * when an image is <A HREF="../gp/package-summary.html#Resample">resampled</A> (for
0922: * example reprojected in an other coordinate system) and the resampled image do not
0923: * fit in a rectangular area. It can also be used in various situation where a raisonable
0924: * "no data" category is needed. The default implementation try to returns one
0925: * of the {@linkplain #getNoDataValues no data values}. If no suitable category is found,
0926: * then a {@linkplain Category#NODATA default} one is returned.
0927: *
0928: * @return A category to use as background for the "Resample" operation. Never {@code null}.
0929: */
0930: public Category getBackground() {
0931: return (categories != null) ? categories.nodata
0932: : Category.NODATA;
0933: }
0934:
0935: /**
0936: * Returns the values to indicate "no data" for this sample dimension. The default
0937: * implementation deduces the "no data" values from the list of categories supplied
0938: * at construction time. The rules are:
0939: *
0940: * <ul>
0941: * <li>If {@link #getSampleToGeophysics} returns {@code null}, then
0942: * {@code getNoDataValues()} returns {@code null} as well.
0943: * This means that this sample dimension contains no category or contains
0944: * only qualitative categories (e.g. a band from a classified image).</li>
0945: *
0946: * <li>If {@link #getSampleToGeophysics} returns an identity transform,
0947: * then {@code getNoDataValues()} returns {@code null}.
0948: * This means that sample value in this sample dimension are already
0949: * expressed in geophysics values and that all "no data" values (if any)
0950: * have already been converted into {@code NaN} values.</li>
0951: *
0952: * <li>Otherwise, if there is at least one quantitative category, returns the sample values
0953: * of all non-quantitative categories. For example if "Temperature" is a quantitative
0954: * category and "Land" and "Cloud" are two qualitative categories, then sample values
0955: * for "Land" and "Cloud" will be considered as "no data" values. "No data" values
0956: * that are already {@code NaN} will be ignored.</li>
0957: * </ul>
0958: *
0959: * Together with {@link #getOffset()} and {@link #getScale()}, this method provides a limited
0960: * way to transform sample values into geophysics values. However, the recommended way is to
0961: * use the {@link #getSampleToGeophysics sampleToGeophysics} transform instead, which is more
0962: * general and take care of converting automatically "no data" values into {@code NaN}.
0963: *
0964: * @return The values to indicate no data values for this sample dimension,
0965: * or {@code null} if not applicable.
0966: * @throws IllegalStateException if some qualitative categories use a range of
0967: * non-integer values.
0968: *
0969: * @see #getSampleToGeophysics
0970: */
0971: public double[] getNoDataValues() throws IllegalStateException {
0972: if (!hasQuantitative) {
0973: return null;
0974: }
0975: int count = 0;
0976: double[] padValues = null;
0977: final int size = categories.size();
0978: for (int i = 0; i < size; i++) {
0979: final Category category = (Category) categories.get(i);
0980: if (!category.isQuantitative()) {
0981: final double min = category.minimum;
0982: final double max = category.maximum;
0983: if (!Double.isNaN(min) || !Double.isNaN(max)) {
0984: if (padValues == null) {
0985: padValues = new double[size - i];
0986: }
0987: if (count >= padValues.length) {
0988: padValues = XArray.resize(padValues, count * 2);
0989: }
0990: padValues[count++] = min;
0991: /*
0992: * The "no data" value has been extracted. Now, check if we have a range
0993: * of "no data" values instead of a single one for this category. If we
0994: * have a single value, it can be of any type. But if we have a range,
0995: * then it must be a range of integers (otherwise we can't expand it).
0996: */
0997: if (max != min) {
0998: int lower = (int) min;
0999: int upper = (int) max;
1000: if (lower != min
1001: || upper != max
1002: || !XMath.isInteger(category.getRange()
1003: .getElementClass())) {
1004: throw new IllegalStateException(
1005: Errors
1006: .format(ErrorKeys.NON_INTEGER_CATEGORY));
1007: }
1008: final int requiredLength = count
1009: + (upper - lower);
1010: if (requiredLength > padValues.length) {
1011: padValues = XArray.resize(padValues,
1012: requiredLength * 2);
1013: }
1014: while (++lower <= upper) {
1015: padValues[count++] = lower;
1016: }
1017: }
1018: }
1019: }
1020: }
1021: if (padValues != null) {
1022: padValues = XArray.resize(padValues, count);
1023: }
1024: return padValues;
1025: }
1026:
1027: /**
1028: * Returns the minimum value occurring in this sample dimension.
1029: * The default implementation fetch this value from the categories supplied at
1030: * construction time. If the minimum value can't be computed, then this method
1031: * returns {@link Double#NEGATIVE_INFINITY}.
1032: *
1033: * @see #getRange
1034: */
1035: public double getMinimumValue() {
1036: if (categories != null && !categories.isEmpty()) {
1037: final double value = ((Category) categories.get(0)).minimum;
1038: if (!Double.isNaN(value)) {
1039: return value;
1040: }
1041: }
1042: return Double.NEGATIVE_INFINITY;
1043: }
1044:
1045: /**
1046: * Returns the maximum value occurring in this sample dimension.
1047: * The default implementation fetch this value from the categories supplied at
1048: * construction time. If the maximum value can't be computed, then this method
1049: * returns {@link Double#POSITIVE_INFINITY}.
1050: *
1051: * @see #getRange
1052: */
1053: public double getMaximumValue() {
1054: if (categories != null) {
1055: for (int i = categories.size(); --i >= 0;) {
1056: final double value = ((Category) categories.get(i)).maximum;
1057: if (!Double.isNaN(value)) {
1058: return value;
1059: }
1060: }
1061: }
1062: return Double.POSITIVE_INFINITY;
1063: }
1064:
1065: /**
1066: * Returns the range of values in this sample dimension. This is the union of the range of
1067: * values of every categories, excluding {@code NaN} values. A {@link NumberRange} object
1068: * gives more informations than {@link #getMinimumValue} and {@link #getMaximumValue} methods
1069: * since it contains also the data type (integer, float, etc.) and inclusion/exclusion
1070: * informations.
1071: *
1072: * @return The range of values. May be {@code null} if this sample dimension has no
1073: * quantitative category.
1074: *
1075: * @see Category#getRange
1076: * @see #getMinimumValue
1077: * @see #getMaximumValue
1078: *
1079: * @todo We should do a better job in {@code CategoryList.getRange()} when selecting
1080: * the appropriate data type. {@link TypeMap#getSampleDimensionType(Range)}
1081: * may be of some help.
1082: */
1083: public NumberRange getRange() {
1084: return (categories != null) ? categories.getRange() : null;
1085: }
1086:
1087: /**
1088: * Returns {@code true} if at least one value of {@code values} is
1089: * in the range {@code lower} inclusive to {@code upper} exclusive.
1090: */
1091: private static boolean rangeContains(final double lower,
1092: final double upper, final double[] values) {
1093: if (values != null) {
1094: final int length = values.length;
1095: for (int i = 0; i < length; i++) {
1096: final double v = values[i];
1097: if (v >= lower && v < upper) {
1098: return true;
1099: }
1100: }
1101: }
1102: return false;
1103: }
1104:
1105: /**
1106: * Returns a string representation of a sample value. This method try to returns
1107: * a representation of the geophysics value; the transformation is automatically
1108: * applied when necessary. More specifically:
1109: *
1110: * <ul>
1111: * <li>If {@code value} maps a qualitative category, then the
1112: * category name is returned as of {@link Category#getName}.</li>
1113: *
1114: * <li>Otherwise, if {@code value} maps a quantitative category, then the value is
1115: * transformed into a geophysics value as with the {@link #getSampleToGeophysics()
1116: * sampleToGeophysics} transform, the result is formatted as a number and the unit
1117: * symbol is appened.</li>
1118: * </ul>
1119: *
1120: * @param value The sample value (can be one of {@code NaN} values).
1121: * @param locale Locale to use for formatting, or {@code null} for the default locale.
1122: * @return A string representation of the geophysics value, or {@code null} if there is
1123: * none.
1124: *
1125: * @todo What should we do when the value can't be formatted?
1126: * {@code GridSampleDimension} returns {@code null} if there is no
1127: * category or if an exception is thrown, but {@code CategoryList}
1128: * returns "Untitled" if the value is an unknow NaN, and try to format
1129: * the number anyway in other cases.
1130: */
1131: public String getLabel(final double value, final Locale locale) {
1132: if (categories != null) {
1133: if (isGeophysics) {
1134: return categories.format(value, locale);
1135: } else
1136: try {
1137: return categories.inverse.format(categories
1138: .transform(value), locale);
1139: } catch (TransformException exception) {
1140: // Value probably don't match a category. Ignore...
1141: }
1142: }
1143: return null;
1144: }
1145:
1146: /**
1147: * Returns the unit information for this sample dimension.
1148: * May returns {@code null} if this dimension has no units.
1149: * This unit apply to values obtained after the {@link #getSampleToGeophysics
1150: * sampleToGeophysics} transformation.
1151: *
1152: * @see #getSampleToGeophysics
1153: */
1154: public Unit getUnits() {
1155: return (categories != null) ? categories.geophysics(true)
1156: .getUnits() : null;
1157: }
1158:
1159: /**
1160: * Returns the value to add to grid values for this sample dimension.
1161: * This attribute is typically used when the sample dimension represents
1162: * elevation data. The transformation equation is:
1163: *
1164: * <blockquote><pre>offset + scale*sample</pre></blockquote>
1165: *
1166: * Together with {@link #getScale()} and {@link #getNoDataValues()}, this method provides a
1167: * limited way to transform sample values into geophysics values. However, the recommended
1168: * way is to use the {@link #getSampleToGeophysics sampleToGeophysics} transform instead,
1169: * which is more general and take care of converting automatically "no data" values
1170: * into {@code NaN}.
1171: *
1172: * @return The offset to add to grid values.
1173: * @throws IllegalStateException if the transform from sample to geophysics values
1174: * is not a linear relation.
1175: *
1176: * @see #getSampleToGeophysics
1177: * @see #rescale
1178: */
1179: public double getOffset() throws IllegalStateException {
1180: return getCoefficient(0);
1181: }
1182:
1183: /**
1184: * Returns the value which is multiplied to grid values for this sample dimension.
1185: * This attribute is typically used when the sample dimension represents elevation
1186: * data. The transformation equation is:
1187: *
1188: * <blockquote><pre>offset + scale*sample</pre></blockquote>
1189: *
1190: * Together with {@link #getOffset()} and {@link #getNoDataValues()}, this method provides a
1191: * limited way to transform sample values into geophysics values. However, the recommended
1192: * way is to use the {@link #getSampleToGeophysics sampleToGeophysics} transform instead,
1193: * which is more general and take care of converting automatically "no data" values
1194: * into {@code NaN}.
1195: *
1196: * @return The scale to multiply to grid value.
1197: * @throws IllegalStateException if the transform from sample to geophysics values
1198: * is not a linear relation.
1199: *
1200: * @see #getSampleToGeophysics
1201: * @see #rescale
1202: */
1203: public double getScale() {
1204: return getCoefficient(1);
1205: }
1206:
1207: /**
1208: * Returns a coefficient of the linear transform from sample to geophysics values.
1209: *
1210: * @param order The coefficient order (0 for the offset, or 1 for the scale factor,
1211: * 2 if we were going to implement quadratic relation, 3 for cubic, etc.).
1212: * @return The coefficient.
1213: * @throws IllegalStateException if the transform from sample to geophysics values
1214: * is not a linear relation.
1215: */
1216: private double getCoefficient(final int order)
1217: throws IllegalStateException {
1218: if (!hasQuantitative) {
1219: // Default value for "offset" is 0; default value for "scale" is 1.
1220: // This is equal to the order if 0 <= order <= 1.
1221: return order;
1222: }
1223: Exception cause = null;
1224: if (sampleToGeophysics != null)
1225: try {
1226: final double value;
1227: switch (order) {
1228: case 0:
1229: value = sampleToGeophysics.transform(0);
1230: break;
1231: case 1:
1232: value = sampleToGeophysics.derivative(Double.NaN);
1233: break;
1234: default:
1235: throw new AssertionError(order); // Should not happen
1236: }
1237: if (!Double.isNaN(value)) {
1238: return value;
1239: }
1240: } catch (TransformException exception) {
1241: cause = exception;
1242: }
1243: IllegalStateException exception = new IllegalStateException(
1244: Errors.format(ErrorKeys.NON_LINEAR_RELATION));
1245: exception.initCause(cause);
1246: throw exception;
1247: }
1248:
1249: /**
1250: * Returns a transform from sample values to geophysics values. If this sample dimension
1251: * has no category, then this method returns {@code null}. If all sample values are
1252: * already geophysics values (including {@code NaN} for "no data" values), then this
1253: * method returns an identity transform. Otherwise, this method returns a transform expecting
1254: * sample values as input and computing geophysics value as output. This transform will take
1255: * care of converting all "{@linkplain #getNoDataValues() no data values}" into
1256: * {@code NaN} values.
1257: * The <code>sampleToGeophysics.{@linkplain MathTransform1D#inverse() inverse()}</code>
1258: * transform is capable to differenciate {@code NaN} values to get back the original
1259: * sample value.
1260: *
1261: * @return The transform from sample to geophysics values, or {@code null} if this
1262: * sample dimension do not defines any transform (which is not the same that
1263: * defining an identity transform).
1264: *
1265: * @see #getScale
1266: * @see #getOffset
1267: * @see #getNoDataValues
1268: * @see #rescale
1269: */
1270: public MathTransform1D getSampleToGeophysics() {
1271: if (isGeophysics) {
1272: return LinearTransform1D.IDENTITY;
1273: }
1274: if (!hasQualitative && sampleToGeophysics != null) {
1275: // If there is only quantitative categories and they all use the same transform,
1276: // then we don't need the indirection level provided by CategoryList.
1277: return sampleToGeophysics;
1278: }
1279: // CategoryList is a MathTransform1D.
1280: return categories;
1281: }
1282:
1283: /**
1284: * If {@code true}, returns the geophysics companion of this sample dimension. By
1285: * definition, a <cite>geophysics sample dimension</cite> is a sample dimension with a
1286: * {@linkplain #getRange range of sample values} transformed in such a way that the
1287: * {@link #getSampleToGeophysics sampleToGeophysics} transform is always the identity
1288: * transform, or {@code null} if no such transform existed in the first place. In
1289: * other words, the range of sample values in all category maps directly the "real world"
1290: * values without the need for any transformation.
1291: * <p>
1292: * {@code GridSampleDimension} objects live by pair: a <cite>geophysics</cite> one
1293: * (used for computation) and a <cite>non-geophysics</cite> one (used for packing data, usually
1294: * as integers). The {@code geo} argument specifies which object from the pair is wanted,
1295: * regardless if this method is invoked on the geophysics or non-geophysics instance of the
1296: * pair. In other words, the result of {@code geophysics(b1).geophysics(b2).geophysics(b3)}
1297: * depends only on the value in the last call ({@code b3}).
1298: *
1299: * @param geo {@code true} to get a sample dimension with an identity
1300: * {@linkplain #getSampleToGeophysics transform} and a {@linkplain #getRange range of
1301: * sample values} matching the geophysics values, or {@code false} to get back the
1302: * original sample dimension.
1303: * @return The sample dimension. Never {@code null}, but may be {@code this}.
1304: *
1305: * @see Category#geophysics
1306: * @see org.geotools.coverage.grid.GridCoverage2D#geophysics
1307: */
1308: public GridSampleDimension geophysics(final boolean geo) {
1309: if (geo == isGeophysics) {
1310: return this ;
1311: }
1312: if (inverse == null) {
1313: if (categories != null) {
1314: inverse = new GridSampleDimension(description,
1315: categories.inverse);
1316: inverse.inverse = this ;
1317: } else {
1318: /*
1319: * If there is no categories, then there is no real difference between
1320: * "geophysics" and "indexed" sample dimensions. Both kinds of sample
1321: * dimensions would be identical objects, so we are better to just
1322: * returns 'this'.
1323: */
1324: inverse = this ;
1325: }
1326: }
1327: return inverse;
1328: }
1329:
1330: /**
1331: * Color palette associated with the sample dimension.
1332: * A color palette can have any number of colors.
1333: * See palette interpretation for meaning of the palette entries.
1334: * If the grid coverage has no color palette, {@code null} will be returned.
1335: *
1336: * @return The color palette associated with the sample dimension.
1337: *
1338: * @see #getPaletteInterpretation
1339: * @see #getColorInterpretation
1340: * @see IndexColorModel
1341: *
1342: * @deprecated No replacement.
1343: */
1344: public int[][] getPalette() {
1345: final ColorModel color = getColorModel();
1346: if (color instanceof IndexColorModel) {
1347: final IndexColorModel cm = (IndexColorModel) color;
1348: final int[][] colors = new int[cm.getMapSize()][];
1349: final int length = colors.length;
1350: for (int i = 0; i < length; i++) {
1351: colors[i] = new int[] { cm.getRed(i), cm.getGreen(i),
1352: cm.getBlue(i) };
1353: }
1354: return colors;
1355: }
1356: return null;
1357: }
1358:
1359: /**
1360: * Indicates the type of color palette entry for sample dimensions which have a
1361: * palette. If a sample dimension has a palette, the color interpretation must
1362: * be {@link ColorInterpretation#GRAY_INDEX GRAY_INDEX}
1363: * or {@link ColorInterpretation#PALETTE_INDEX PALETTE_INDEX}.
1364: * A palette entry type can be Gray, RGB, CMYK or HLS.
1365: *
1366: * @return The type of color palette entry for sample dimensions which have a palette.
1367: *
1368: * @deprecated No replacement.
1369: */
1370: public PaletteInterpretation getPaletteInterpretation() {
1371: return PaletteInterpretation.RGB;
1372: }
1373:
1374: /**
1375: * Returns the color interpretation of the sample dimension.
1376: * A sample dimension can be an index into a color palette or be a color model
1377: * component. If the sample dimension is not assigned a color interpretation
1378: * the value is {@link ColorInterpretation#UNDEFINED}.
1379: *
1380: * @deprecated No replacement.
1381: */
1382: public ColorInterpretation getColorInterpretation() {
1383: // The 'Grid2DSampleDimension' class overrides this method
1384: // with better values for 'band' and 'numBands' constants.
1385: final int band = 0;
1386: final int numBands = 1;
1387: return TypeMap.getColorInterpretation(getColorModel(band,
1388: numBands), band);
1389: }
1390:
1391: /**
1392: * Returns a color model for this sample dimension. The default implementation create a color
1393: * model with 1 band using each category's colors as returned by {@link Category#getColors}.
1394: * The returned color model will typically use data type {@link DataBuffer#TYPE_FLOAT} if this
1395: * {@code GridSampleDimension} instance is "geophysics", or an integer data type otherwise.
1396: * <p>
1397: * Note that {@link org.geotools.coverage.grid.GridCoverage2D#getSampleDimension} returns
1398: * special implementations of {@code GridSampleDimension}. In this particular case,
1399: * the color model created by this {@code getColorModel()} method will have the same number of
1400: * bands than the grid coverage's {@link RenderedImage}.
1401: *
1402: * @return The requested color model, suitable for {@link RenderedImage} objects with values
1403: * in the <code>{@link #getRange}</code> range. May be {@code null} if this
1404: * sample dimension has no category.
1405: */
1406: public ColorModel getColorModel() {
1407: // The 'Grid2DSampleDimension' class overrides this method
1408: // with better values for 'band' and 'numBands' constants.
1409: final int band = 0;
1410: final int numBands = 1;
1411: return getColorModel(band, numBands);
1412: }
1413:
1414: /**
1415: * Returns a color model for this sample dimension. The default implementation create the
1416: * color model using each category's colors as returned by {@link Category#getColors}. The
1417: * returned color model will typically use data type {@link DataBuffer#TYPE_FLOAT} if this
1418: * {@code GridSampleDimension} instance is "geophysics", or an integer data type otherwise.
1419: *
1420: * @param visibleBand The band to be made visible (usually 0). All other bands, if any
1421: * will be ignored.
1422: * @param numBands The number of bands for the color model (usually 1). The returned color
1423: * model will renderer only the {@code visibleBand} and ignore the others, but
1424: * the existence of all {@code numBands} will be at least tolerated. Supplemental
1425: * bands, even invisible, are useful for processing with Java Advanced Imaging.
1426: * @return The requested color model, suitable for {@link RenderedImage} objects with values
1427: * in the <code>{@link #getRange}</code> range. May be {@code null} if this
1428: * sample dimension has no category.
1429: *
1430: * @todo This method may be deprecated in a future version. It it strange to use
1431: * only one {@code SampleDimension} object for creating a multi-bands color
1432: * model. Logically, we would expect as many {@code SampleDimension}s as bands.
1433: */
1434: public ColorModel getColorModel(final int visibleBand,
1435: final int numBands) {
1436: if (categories != null) {
1437: return categories.getColorModel(visibleBand, numBands);
1438: }
1439: return null;
1440: }
1441:
1442: /**
1443: * Returns a color model for this sample dimension. The default implementation create the
1444: * color model using each category's colors as returned by {@link Category#getColors}.
1445: *
1446: * @param visibleBand The band to be made visible (usually 0). All other bands, if any
1447: * will be ignored.
1448: * @param numBands The number of bands for the color model (usually 1). The returned color
1449: * model will renderer only the {@code visibleBand} and ignore the others, but
1450: * the existence of all {@code numBands} will be at least tolerated. Supplemental
1451: * bands, even invisible, are useful for processing with Java Advanced Imaging.
1452: * @param type The data type that has to be used for the sample model
1453: * @return The requested color model, suitable for {@link RenderedImage} objects with values
1454: * in the <code>{@link #getRange}</code> range. May be {@code null} if this
1455: * sample dimension has no category.
1456: *
1457: * @todo This method may be deprecated in a future version. It it strange to use
1458: * only one {@code SampleDimension} object for creating a multi-bands color
1459: * model. Logically, we would expect as many {@code SampleDimension}s as bands.
1460: */
1461: public ColorModel getColorModel(final int visibleBand,
1462: final int numBands, final int type) {
1463: if (categories != null) {
1464: return categories
1465: .getColorModel(visibleBand, numBands, type);
1466: }
1467: return null;
1468: }
1469:
1470: /**
1471: * Returns a sample dimension using new {@link #getScale scale} and {@link #getOffset offset}
1472: * coefficients. Other properties like the {@linkplain #getRange sample value range},
1473: * {@linkplain #getNoDataValues no data values} and {@linkplain #getColorModel colors}
1474: * are unchanged.
1475: *
1476: * @param scale The value which is multiplied to grid values for the new sample dimension.
1477: * @param offset The value to add to grid values for the new sample dimension.
1478: *
1479: * @see #getScale
1480: * @see #getOffset
1481: * @see Category#rescale
1482: */
1483: public GridSampleDimension rescale(final double scale,
1484: final double offset) {
1485: final MathTransform1D sampleToGeophysics = Category
1486: .createLinearTransform(scale, offset);
1487: final Category[] categories = (Category[]) getCategories()
1488: .toArray();
1489: final Category[] reference = (Category[]) categories.clone();
1490: final int length = categories.length;
1491: for (int i = 0; i < length; i++) {
1492: if (categories[i].isQuantitative()) {
1493: categories[i] = categories[i]
1494: .rescale(sampleToGeophysics);
1495: }
1496: categories[i] = categories[i].geophysics(isGeophysics);
1497: }
1498: if (Arrays.equals(categories, reference)) {
1499: return this ;
1500: }
1501: return new GridSampleDimension(description, categories,
1502: getUnits());
1503: }
1504:
1505: /**
1506: * The list of metadata keywords for a sample dimension.
1507: * If no metadata is available, the sequence will be empty.
1508: *
1509: * @return The list of metadata keywords for a sample dimension.
1510: *
1511: * @see #getMetadataValue
1512: * @see javax.media.jai.PropertySource#getPropertyNames
1513: *
1514: * @deprecated Not implemented.
1515: */
1516: public String[] getMetaDataNames() {
1517: return EMPTY_METADATA;
1518: }
1519:
1520: /**
1521: * Retrieve the metadata value for a given metadata name.
1522: *
1523: * @param name Metadata keyword for which to retrieve metadata.
1524: * @return The metadata value for a given metadata name.
1525: * @throws MetadataNameNotFoundException if there is no value for the specified metadata name.
1526: *
1527: * @see #getMetaDataNames
1528: * @see javax.media.jai.PropertySource#getProperty
1529: *
1530: * @deprecated Not implemented.
1531: */
1532: public String getMetadataValue(String name)
1533: throws MetadataNameNotFoundException {
1534: throw new MetadataNameNotFoundException();
1535: }
1536:
1537: /**
1538: * Returns a hash value for this sample dimension.
1539: * This value need not remain consistent between
1540: * different implementations of the same class.
1541: */
1542: public int hashCode() {
1543: return (categories != null) ? categories.hashCode()
1544: : (int) serialVersionUID;
1545: }
1546:
1547: /**
1548: * Compares the specified object with this sample dimension for equality.
1549: */
1550: public boolean equals(final Object object) {
1551: if (object == this ) {
1552: // Slight optimization
1553: return true;
1554: }
1555: if (object instanceof GridSampleDimension) {
1556: final GridSampleDimension that = (GridSampleDimension) object;
1557: return Utilities.equals(this .categories, that.categories);
1558: // Since everything is deduced from CategoryList, two sample dimensions
1559: // should be equal if they have the same list of categories.
1560: }
1561: return false;
1562: }
1563:
1564: /**
1565: * Returns a string representation of this sample dimension.
1566: * This string is for debugging purpose only and may change
1567: * in future version. The default implementation format the
1568: * sample value range, then the list of categories. A "*"
1569: * mark is put in front of what seems the "main" category.
1570: */
1571: public String toString() {
1572: if (categories != null) {
1573: return categories.toString(this );
1574: } else {
1575: return Utilities.getShortClassName(this );
1576: }
1577: }
1578:
1579: /////////////////////////////////////////////////////////////////////////////////
1580: //////// ////////
1581: //////// REGISTRATION OF "SampleTranscode" IMAGE OPERATION ////////
1582: //////// ////////
1583: /////////////////////////////////////////////////////////////////////////////////
1584:
1585: /**
1586: * Register the "SampleTranscode" image operation.
1587: * Registration is done when the class is first loaded.
1588: *
1589: * @todo This static initializer will imply immediate class loading of a lot of
1590: * JAI dependencies. This is a pretty high overhead if JAI is not wanted
1591: * right now. The correct approach is to declare the image operation into
1592: * the {@code META-INF/registryFile.jai} file, which is automatically
1593: * parsed during JAI initialization. Unfortunatly, it can't access private
1594: * classes and we don't want to make our registration classes public. We
1595: * can't move our registration classes into a hidden "resources" package
1596: * neither because we need package-private access to {@code CategoryList}.
1597: * For now, we assume that people using the GC package probably want to work
1598: * with {@link org.geotools.coverage.grid.GridCoverage2D}, which make extensive
1599: * use of JAI. Peoples just working with {@link org.geotools.coverage.Coverage} are
1600: * stuck with the overhead. Note that we register the image operation here because
1601: * the only operation's argument is of type {@code GridSampleDimension[]}.
1602: * Consequently, the image operation may be invoked at any time after class
1603: * loading of {@link GridSampleDimension}.
1604: * <p>
1605: * Additional note: moving the initialization into the
1606: * {@code META-INF/registryFile.jai} file may not be the best idea neithter,
1607: * since peoples using JAI without the GCS module may be stuck with the overhead
1608: * of loading GC classes.
1609: */
1610: static {
1611: SampleTranscoder.register(JAI.getDefaultInstance());
1612: }
1613: }
|