/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2001-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * 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.geotoolkit.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.Objects; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import javax.media.jai.JAI; import javax.measure.Unit; 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; import org.apache.sis.util.ArraysExt; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.iso.Types; import org.apache.sis.util.Classes; import org.apache.sis.util.Numbers; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Vocabulary; import org.apache.sis.referencing.operation.transform.MathTransforms; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; import org.geotoolkit.image.color.ColorUtilities; /** * 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 differentiate * <em>qualitative</em> and <em>quantitative</em> categories. For example an image of sea surface * temperature (SST) could very well defines the following categories: * * {@preformat text * [0] : no data * [1] : cloud * [2] : land * [10..210] : temperature to be converted into Celsius degrees through a linear equation * } * * In this example, sample values in range {@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. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.11 * * @see org.geotoolkit.gui.swing.coverage.SampleDimensionPanel * * @since 1.2 * @module */ public class GridSampleDimension implements SampleDimension, Serializable { /** * Serial number for inter-operability 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(boolean)}. 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 and no units. Note that it may happen in some occasions that this field * is a non-null empty list. This happen if the users specified units of measurement but no * range of values, in which case it was not possible to build {@link Category} instances. */ 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 different 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; /** * Description 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) { // '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) { // '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( Errors.Keys.MismatchedArrayLength_2, "names", "colors")); } 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. * <p> * The table below lists the methods which will return the parameters given to this * constructor, together with their default value (from heuristic rules). * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"> * <th nowrap> Parameter </th> * <th nowrap> Returned by </th> * <th nowrap> Default value </th> * </tr> * <tr> * <td nowrap> {@code description} </td> * <td nowrap> {@link #getDescription()} </td> * <td nowrap> The name of what looks like the "main" category. </td> * </tr> * <tr> * <td nowrap> {@code type} </td> * <td nowrap> {@link #getSampleDimensionType()} </td> * <td nowrap> Computed automatically from the {@code [minimum..maximum]} range. </td> * </tr> * <tr> * <td nowrap> {@code color} </td> * <td nowrap> {@link #getColorInterpretation()} </td> * <td nowrap> Usually {@link ColorInterpretation#PALETTE_INDEX}. </td> * </tr> * <tr> * <td nowrap> {@code palette} </td> * <td nowrap> {@link #getColorModel()} </td> * <td nowrap> Usually grayscale. </td> * </tr> * <tr> * <td nowrap> {@code categories} </td> * <td nowrap> {@link #getCategoryNames()} </td> * <td nowrap> No category. </td> * </tr> * <tr> * <td nowrap> {@code nodata} </td> * <td nowrap> {@link #getNoDataValues()} </td> * <td nowrap> No pad value. </td> * </tr> * <tr> * <td nowrap> {@code minimum} </td> * <td nowrap> {@link #getMinimumValue()} </td> * <td nowrap> </td> * </tr> * <tr> * <td nowrap> {@code maximum} </td> * <td nowrap> {@link #getMaximumValue()} </td> * <td nowrap> </td> * </tr> * <tr> * <td nowrap> {@code scale} </td> * <td nowrap> {@link #getScale()} </td> * <td nowrap> </td> * </tr> * <tr> * <td nowrap> {@code offset} </td> * <td nowrap> {@link #getOffset()} </td> * <td nowrap> </td> * <tr> * <td nowrap> {@code unit} </td> * <td nowrap> {@link #getUnits()} </td> * <td nowrap> </td> * </tr> * </table> * * @param description * The sample dimension title or description. * @param type * The grid value data type (which indicate the number of bits for the data type). * @param color * The color interpretation. * @param palette * The color palette associated with the sample dimension. If {@code categories} is * non-null, then both arrays usually have the same length. However, this constructor * is tolerant about the array length. * @param categories * A sequence of category names for the values contained in the sample dimension. * @param nodata * The values to indicate "no data". The {@code [minimum..maximum]} range may or may * not includes the {@code nodata} values; the range will be adjusted as needed. * @param minimum * The lower sample value, inclusive. If {@code categories} was non-null, * then {@code minimum} is usually 0. * @param maximum * The upper sample value, <strong>inclusive</strong> as well. If {@code categories} * was non-null, then {@code maximum} is often equals to {@code categories.length-1}. * @param scale * The value which is multiplied to grid values, or 1 if none. * @param offset * The value to add to grid values, or 0 if none. * @param unit * The unit information for this sample dimension. * * @throws IllegalArgumentException * if the range {@code [minimum..maximum]} is not valid. */ 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) { // '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. */ 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(Vocabulary.Keys.Untitled); } if (Double.isInfinite(minimum) || Double.isInfinite(maximum) || !(minimum < maximum)) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2, minimum, maximum)); } if (Double.isNaN(scale) || Double.isInfinite(scale) || scale == 0) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalParameterValue_2, "scale", scale)); } if (Double.isNaN(offset) || Double.isInfinite(offset)) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalParameterValue_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<>(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(); } @SuppressWarnings({"unchecked","rawtypes"}) final NumberRange<?> range = new NumberRange(value.getClass(), value, true, value, true); 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 = Numbers.widestClass(min, max); min = Numbers.cast(min, classe); max = Numbers.cast(max, classe); } @SuppressWarnings({"unchecked","rawtypes"}) final NumberRange<?> range = new NumberRange(classe, min, true, max, true); 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 Number min = range.getMinValue(); final Number max = range.getMaxValue(); @SuppressWarnings({"unchecked","rawtypes"}) final int c = ((Comparable) min).compareTo(max); if (c != 0) { final double xmin = min.doubleValue(); final double xmax = 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 subtractions 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.getMinDouble(); final double max = range.getMaxDouble(); 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 = Numbers.widestClass(min, max); min = Numbers.cast(min, classe); max = Numbers.cast(max, classe); @SuppressWarnings({"unchecked","rawtypes"}) 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 necessarily 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, or {@code null} if none. * @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 { // '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(Category[] categories, final Unit<?> units) { if (categories == null || categories.length == 0) { if (units == null) { return null; } if (categories == null) { categories = new Category[0]; } } 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(Errors.Keys.MixedCategories)); } /** * 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} if none. */ 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 = Types.toInternationalString(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.new Name(); } else { this.description = Vocabulary.formatInternational(Vocabulary.Keys.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.sampleToGeophysics; description = other.description; } else { // 'inverse' will be set when needed. categories = null; isGeophysics = false; hasQualitative = false; hasQuantitative = false; sampleToGeophysics = null; description = Vocabulary.formatInternational(Vocabulary.Keys.Untitled); } } /** * Returns the content of the given OpenGIS's sample dimension as a Geotk * implementation of {@code GridSampleDimension}. * * @param sd The sample dimension to wrap into a Geotk implementation. * @return The given sample dimension as a {@code GridSampleDimension} instance. */ public static GridSampleDimension castOrCopy(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. */ @Override 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. */ @Override 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: * * {@preformat text * [0] Background * [1] Water * [2] Forest * [3] Urban * } * * @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 */ @Override 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(Errors.Keys.NonIntegerCategory)); } 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() { 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 reasonable * "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}. */ 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: * <p> * <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> * <p> * 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 */ @Override 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 = Arrays.copyOf(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 || !Numbers.isInteger(category.getRange().getElementType())) { throw new IllegalStateException(Errors.format( Errors.Keys.NonIntegerCategory)); } final int requiredLength = count + (upper-lower); if (requiredLength > padValues.length) { padValues = Arrays.copyOf(padValues, requiredLength*2); } while (++lower <= upper) { padValues[count++] = lower; } } } } } if (padValues != null) { padValues = ArraysExt.resize(padValues, count); } return padValues; } /** * Returns the minimum value occurring in this sample dimension (inclusive). The default * implementation fetches 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() */ @Override public double getMinimumValue() { if (!isNullOrEmpty(categories)) { 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 fetches 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() */ @Override 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 * non-{@code NaN} value. * * @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(Range)} may be of * some help. */ public NumberRange<?> getRange() { return (categories != null) ? categories.getRange() : null; } /** * Returns {@code true} if the {@linkplain #getRange() range} includes negative values. If * this sample dimension does not declare any quantitative category, then this method * returns {@code false}. * <p> * Some color models, especially {@link IndexColorModel}, support only unsigned integer. * For such color models, any {@code SampleDimension} for which this method return * {@code true} will require special handling. * * @return {@code true} if the {@linkplain #getRange() range} includes negative values. * * @since 3.11 */ public boolean isRangeSigned() { return (categories != null) && categories.isRangeSigned(); } /** * 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 appended.</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 unknown 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() */ @Override 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: * * {@preformat math * offset + scale*sample * } * * 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 */ @Override 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: * * {@preformat math * offset + scale*sample * } * * Together with {@link #getOffset()} and {@link #getNoDataValues()}, this method provides a * limited way to transform sample values into geophysics values. However, the recommended * way is to use the {@link #getSampleToGeophysics() sampleToGeophysics} transform instead, * which is more general and take care of converting automatically "no data" values * into {@code NaN}. * * @return The scale to multiply to grid value. * @throws IllegalStateException if the transform from sample to geophysics values * is not a linear relation. * * @see #getSampleToGeophysics * @see #rescale */ @Override 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(Errors.Keys.NonLinearRelation), cause); } /** * Returns the <cite>transfer function</cite> 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 differentiate {@code NaN} values to get back the original * sample value. * * @return The <cite>transfer function</cite> 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 */ @Override public MathTransform1D getSampleToGeophysics() { if (isGeophysics) { return (MathTransform1D) MathTransforms.identity(1); } 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.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics} or * {@linkplain org.geotoolkit.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.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics} one (used for * computation) and a {@linkplain org.geotoolkit.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.geotoolkit.coverage.grid.ViewType#GEOPHYSICS * geophysics} values, or {@code false} to get back the * {@linkplain org.geotoolkit.coverage.grid.ViewType#PACKED packed} sample dimension. * @return The sample dimension. Never {@code null}, but may be {@code this}. * * @see Category#geophysics * @see org.geotoolkit.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. */ @Override @Deprecated 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. * * @deprecated No replacement. */ @Override @Deprecated 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}. * * @deprecated No replacement. */ @Override @Deprecated 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 creates a color * model with 1 band using each category 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.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics}, or * an integer data type otherwise. * <p> * Note that {@link org.geotoolkit.coverage.grid.GridCoverage2D#getSampleDimension(int)} * 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 {@link java.awt.image.RenderedImage}. * * @return The requested color model, suitable for {@link java.awt.image.RenderedImage} objects * with values in the {@link #getRange()} range. May be {@code null} if this * sample dimension has no category. */ 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 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.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics}, * or an integer data type otherwise. * * @param visibleBand The band to be made visible (usually 0). All other bands, if any * will be ignored. * @param numBands The number of bands for the color model (usually 1). The returned color * model will renderer only the {@code visibleBand} and ignore the others, but * the existence of all {@code numBands} will be at least tolerated. Supplemental * bands, even invisible, are useful for processing with Java Advanced Imaging. * @return The requested color model, suitable for {@link java.awt.image.RenderedImage} objects * with values in the {@link #getRange()} range. May be {@code null} if this * sample dimension has no category. * * @todo This method may be deprecated in a future version. It it strange to use * only one {@code SampleDimension} object for creating a multi-bands color * model. Logically, we would expect as many {@code SampleDimension}s as bands. */ public ColorModel getColorModel(final int visibleBand, final int numBands) { if (categories != null) { if (isGeophysics && hasQualitative) { // Data likely to have NaN values, which require a floating point type. return categories.getColorModel(visibleBand, numBands, DataBuffer.TYPE_FLOAT); } return categories.getColorModel(visibleBand, numBands); } return null; } /** * Returns a color model for this sample dimension. The default implementation create the * color model using each category colors as returned by {@link Category#getColors()}. * * @param visibleBand The band to be made visible (usually 0). All other bands, if any * will be ignored. * @param numBands The number of bands for the color model (usually 1). The returned color * model will renderer only the {@code visibleBand} and ignore the others, but * the existence of all {@code numBands} will be at least tolerated. Supplemental * bands, even invisible, are useful for processing with Java Advanced Imaging. * @param type The data type that has to be used for the sample model. * @return The requested color model, suitable for {@link java.awt.image.RenderedImage} objects * with values in the {@link #getRange()} range. May be {@code null} if this * sample dimension has no category. * * @todo This method may be deprecated in a future version. It it strange to use * only one {@code SampleDimension} object for creating a multi-bands color * model. Logically, we would expect as many {@code SampleDimension}s as bands. */ public ColorModel getColorModel(final int visibleBand, final int numBands, final int type) { if (categories != null) { return categories.getColorModel(visibleBand, numBands, type); } return null; } /** * Returns a sample dimension using new {@link #getScale() scale} and {@link #getOffset() offset} * coefficients. Other properties like the {@linkplain #getRange() sample value range}, * {@linkplain #getNoDataValues() no data values} and {@linkplain #getColorModel() colors} * are unchanged. * * @param scale The value which is multiplied to grid values for the new sample dimension. * @param offset The value to add to grid values for the new sample dimension. * @return The scaled sample dimension. * * @see #getScale * @see #getOffset * @see Category#rescale */ public GridSampleDimension rescale(final double scale, final double offset) { final MathTransform1D sampleToGeophysics = Category.createLinearTransform(scale, offset); final Category[] categories = (Category[]) getCategories().toArray(); boolean changed = false; for (int i=0; i<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 Objects.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. Unfortunately, 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.geotoolkit.coverage.grid.GridCoverage2D}, which make extensive * use of JAI. Peoples just working with {@link org.geotoolkit.coverage.Coverage} are * stuck with the overhead. Note that we register the image operation here because * the only operation 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 neither, * since peoples using JAI without the GCS module may be stuck with the overhead * of loading GC classes. */ static { SampleTranscoder.register(JAI.getDefaultInstance()); } }