001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Management Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * This package contains documentation from OpenGIS specifications.
018: * OpenGIS consortium's work is fully acknowledged here.
019: */
020: package org.geotools.coverage;
021:
022: // J2SE dependencies
023: import java.awt.Color;
024: import java.util.Arrays;
025: import java.io.Serializable;
026:
027: // JAI dependencies
028: import javax.media.jai.operator.PiecewiseDescriptor;
029:
030: // OpenGIS dependencies
031: import org.opengis.referencing.operation.MathTransform;
032: import org.opengis.referencing.operation.MathTransform1D;
033: import org.opengis.referencing.operation.MathTransformFactory;
034: import org.opengis.referencing.operation.TransformException;
035: import org.opengis.util.InternationalString;
036:
037: // Geotools dependencies
038: import org.geotools.referencing.operation.transform.LinearTransform1D;
039: import org.geotools.resources.Utilities;
040: import org.geotools.resources.XMath;
041: import org.geotools.resources.i18n.Errors;
042: import org.geotools.resources.i18n.ErrorKeys;
043: import org.geotools.resources.i18n.Vocabulary;
044: import org.geotools.resources.i18n.VocabularyKeys;
045: import org.geotools.util.SimpleInternationalString;
046: import org.geotools.util.NumberRange;
047:
048: /**
049: * A category delimited by a range of sample values. A categogy may be either
050: * <em>qualitative</em> or <em>quantitative</em>. For exemple, a classified
051: * image may have a qualitative category defining sample value {@code 0}
052: * as water. An other qualitative category may defines sample value {@code 1}
053: * as forest, etc. An other image may define elevation data as sample values
054: * in the range {@code [0..100]}. The later is a <em>quantitative</em>
055: * category, because sample values are related to some measurement in the real
056: * world. For example, elevation data may be related to an altitude in metres
057: * through the following linear relation:
058: *
059: * <var>altitude</var> = <var>sample value</var>×100.
060: *
061: * Some image mixes both qualitative and quantitative categories. For example,
062: * images of Sea Surface Temperature (SST) may have a quantitative category
063: * for temperature with values ranging from –2 to 35°C, and three qualitative
064: * categories for cloud, land and ice.
065: * <p>
066: * All categories must have a human readable name. In addition, quantitative
067: * categories may define a transformation between sample values <var>s</var>
068: * and geophysics values <var>x</var>. This transformation is usually (but
069: * not always) a linear equation of the form:
070: *
071: * <P align="center"><var>x</var><code> = {@linkplain GridSampleDimension#getOffset()
072: * offset} + {@linkplain GridSampleDimension#getScale()
073: * scale}×</code><var>s</var></P>
074: *
075: * More general equation are allowed. For example, <cite>SeaWiFS</cite> images
076: * use a logarithmic transform. General transformations are expressed with a
077: * {@link MathTransform1D} object. In the special case where the transformation
078: * is a linear one (as in the formula above), then a {@code Category} object
079: * may be understood as the interval between two breakpoints in the JAI's
080: * {@linkplain PiecewiseDescriptor piecewise} operation.
081: * <p>
082: * All {@code Category} objects are immutable and thread-safe.
083: *
084: * @since 2.1
085: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/coverage/Category.java $
086: * @version $Id: Category.java 26695 2007-08-23 18:58:56Z desruisseaux $
087: * @author Martin Desruisseaux
088: *
089: * @see GridSampleDimension
090: * @see PiecewiseDescriptor
091: */
092: public class Category implements Serializable {
093: /**
094: * Serial number for interoperability with different versions.
095: */
096: private static final long serialVersionUID = 6215962897884256696L;
097:
098: /**
099: * The 0 value as a byte. Used for {@link #FALSE} categories.
100: */
101: private static final NumberRange BYTE_0;
102: static {
103: final Byte index = new Byte((byte) 0);
104: BYTE_0 = new NumberRange(Byte.class, index, index);
105: }
106:
107: /**
108: * The 1 value as a byte. Used for {@link #TRUE} categories.
109: */
110: private static final NumberRange BYTE_1;
111: static {
112: final Byte index = new Byte((byte) 1);
113: BYTE_1 = new NumberRange(Byte.class, index, index);
114: }
115:
116: /**
117: * A default category for "no data" values. This default qualitative category use
118: * sample value 0, which is mapped to geophysics value {@link Float#NaN} for those who work
119: * with floating point images. The rendering color default to a fully transparent color and
120: * the name is "no data" localized to the requested locale.
121: */
122: public static final Category NODATA = new Category(Vocabulary
123: .formatInternational(VocabularyKeys.NODATA), new Color(0,
124: 0, 0, 0), 0);
125:
126: /**
127: * A default category for the boolean "{@link Boolean#FALSE false}" value. This default
128: * identity category uses sample value 0, the color {@linkplain Color#BLACK black} and
129: * the name "false" localized to the specified locale.
130: */
131: public static final Category FALSE = new Category(Vocabulary
132: .formatInternational(VocabularyKeys.FALSE), Color.BLACK,
133: false);
134:
135: /**
136: * A default category for the boolean "{@link Boolean#TRUE true}" value. This default
137: * identity category uses sample value 1, the color {@linkplain Color#WHITE white}
138: * and the name "true" localized to the specified locale.
139: */
140: public static final Category TRUE = new Category(Vocabulary
141: .formatInternational(VocabularyKeys.TRUE), Color.WHITE,
142: true);
143:
144: /**
145: * The category name.
146: */
147: private final InternationalString name;
148:
149: /**
150: * The minimal sample value (inclusive). This category is made of all values
151: * in the range {@code minimum} to {@code maximum} inclusive.
152: *
153: * If this category is an instance of {@code GeophysicsCategory},
154: * then this field is the minimal geophysics value in this category.
155: * For qualitative categories, the geophysics value is one of {@code NaN} values.
156: */
157: final double minimum;
158:
159: /**
160: * The maximal sample value (inclusive). This category is made of all values
161: * in the range {@code minimum} to {@code maximum} inclusive.
162: *
163: * If this category is an instance of {@code GeophysicsCategory},
164: * then this field is the maximal geophysics value in this category.
165: * For qualitative categories, the geophysics value is one of {@code NaN} values.
166: */
167: final double maximum;
168:
169: /**
170: * The range of values {@code [minimum..maximum]}.
171: * May be computed only when first requested, or may be
172: * user-supplied (which is why it must be serialized).
173: */
174: NumberRange range;
175:
176: /**
177: * The math transform from sample to geophysics values (never {@code null}).
178: *
179: * If this category is an instance of {@code GeophysicsCategory}, then this transform
180: * is the inverse (as computed by {@link MathTransform#inverse()}), except for qualitative
181: * categories. Since {@link #getSampleToGeophysics} returns {@code null} for
182: * qualitative categories, this difference is not visible to the user.
183: *
184: * @see GridSampleDimension#getScale()
185: * @see GridSampleDimension#getOffset()
186: */
187: final MathTransform1D transform;
188:
189: /**
190: * A reference to the {@code GeophysicsCategory}. If this category is already an
191: * instance of {@code GeophysicsCategory}, then {@code inverse} is a reference
192: * to the {@link Category} object that own it.
193: */
194: final Category inverse;
195:
196: /**
197: * Codes ARGB des couleurs de la catégorie. Les couleurs par
198: * défaut seront un gradient allant du noir au blanc opaque.
199: */
200: private final int[] ARGB;
201:
202: /**
203: * Codes ARGB par défaut. On utilise un exemplaire unique
204: * pour toutes les création d'objets {@link Category}.
205: */
206: private static final int[] DEFAULT = { 0xFF000000, 0xFFFFFFFF };
207:
208: /**
209: * A set of default category colors.
210: */
211: private static final Color[] CYCLE = { Color.BLUE, Color.RED,
212: Color.ORANGE, Color.YELLOW, Color.PINK, Color.MAGENTA,
213: Color.GREEN, Color.CYAN, Color.LIGHT_GRAY, Color.GRAY };
214:
215: /**
216: * Constructs a qualitative category for a boolean value.
217: *
218: * @param name The category name as a {@link String} or {@link InternationalString} object.
219: * @param color The category color, or {@code null} for a default color.
220: * @param sample The sample value as a boolean.
221: */
222: public Category(final CharSequence name, final Color color,
223: final boolean sample) {
224: this (name, new Color[] { color }, sample ? BYTE_0 : BYTE_1,
225: LinearTransform1D.IDENTITY);
226: }
227:
228: /**
229: * Constructs a qualitative category for sample value {@code sample}.
230: *
231: * @param name The category name as a {@link String} or {@link InternationalString} object.
232: * @param color The category color, or {@code null} for a default color.
233: * @param sample The sample value as an integer, usually in the range 0 to 255.
234: */
235: public Category(final CharSequence name, final Color color,
236: final int sample) {
237: this (name, toARGB(color, sample), new Integer(sample));
238: assert minimum == sample : minimum;
239: assert maximum == sample : maximum;
240: }
241:
242: /**
243: * Constructs a qualitative category for sample value {@code sample}.
244: *
245: * @param name The category name as a {@link String} or {@link InternationalString} object.
246: * @param color The category color, or {@code null} for a default color.
247: * @param sample The sample value as a double. May be one of {@code NaN} values.
248: */
249: public Category(final CharSequence name, final Color color,
250: final double sample) {
251: this (name, toARGB(color, (int) sample), new Double(sample));
252: assert Double.doubleToRawLongBits(minimum) == Double
253: .doubleToRawLongBits(sample) : minimum;
254: assert Double.doubleToRawLongBits(maximum) == Double
255: .doubleToRawLongBits(sample) : maximum;
256: }
257:
258: /**
259: * Constructs a qualitative category for sample value {@code sample}.
260: */
261: private Category(final CharSequence name, final int[] ARGB,
262: final Number sample) {
263: this (name, ARGB, new NumberRange(sample.getClass(), sample,
264: sample), null);
265: assert Double.isNaN(inverse.minimum) : inverse.minimum;
266: assert Double.isNaN(inverse.maximum) : inverse.maximum;
267: }
268:
269: /**
270: * Constructs a quantitative category for samples in the specified range.
271: *
272: * @param name The category name as a {@link String} or {@link InternationalString} object.
273: * @param color The category color, or {@code null} for a default color.
274: * @param sampleValueRange The range of sample values for this category. Element class
275: * is usually {@link Integer}, but {@link Float} and {@link Double} are
276: * accepted as well.
277: */
278: public Category(final CharSequence name, final Color color,
279: final NumberRange sampleValueRange)
280: throws IllegalArgumentException {
281: this (name, new Color[] { color }, sampleValueRange,
282: (MathTransform1D) null);
283: }
284:
285: /**
286: * Constructs a quantitative category for sample values ranging from {@code lower}
287: * inclusive to {@code upper} exclusive. Sample values are converted into geophysics
288: * values using the following linear equation:
289: *
290: * <center><var>x</var><code> = {@linkplain GridSampleDimension#getOffset()
291: * offset} + {@linkplain GridSampleDimension#getScale()
292: * scale}×</code><var>s</var></center>
293: *
294: * @param name The category name as a {@link String} or {@link InternationalString} object.
295: * @param colors A set of colors for this category. This array may have any length;
296: * colors will be interpolated as needed. An array of length 1 means
297: * that an uniform color should be used for all sample values. An array
298: * of length 0 or a {@code null} array means that some default colors
299: * should be used (usually a gradient from opaque black to opaque white).
300: * @param lower The lower sample value, inclusive.
301: * @param upper The upper sample value, exclusive.
302: * @param scale The {@link GridSampleDimension#getScale() scale} value which is
303: * multiplied to sample values for this category.
304: * @param offset The {@link GridSampleDimension#getOffset() offset} value to add
305: * to sample values for this category.
306: *
307: * @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper}.
308: * @throws IllegalArgumentException if {@code scale} or {@code offset} are not real numbers.
309: */
310: public Category(final CharSequence name, final Color[] colors,
311: final int lower, final int upper, final double scale,
312: final double offset) throws IllegalArgumentException {
313: this (name, colors, new NumberRange(Integer.class, new Integer(
314: lower), true, new Integer(upper), false), scale, offset);
315: }
316:
317: /**
318: * Constructs a quantitative category for sample values in the specified range.
319: * Sample values are converted into geophysics values using the following linear
320: * equation:
321: *
322: * <center><var>x</var><code> = {@linkplain GridSampleDimension#getOffset()
323: * offset} + {@linkplain GridSampleDimension#getScale()
324: * scale}×</code><var>s</var></center>
325: *
326: * @param name The category name as a {@link String} or {@link InternationalString} object.
327: * @param colors A set of colors for this category. This array may have any length;
328: * colors will be interpolated as needed. An array of length 1 means
329: * that an uniform color should be used for all sample values. An array
330: * of length 0 or a {@code null} array means that some default colors
331: * should be used (usually a gradient from opaque black to opaque white).
332: * @param sampleValueRange The range of sample values for this category. Element class
333: * is usually {@link Integer}, but {@link Float} and {@link Double} are
334: * accepted as well.
335: * @param scale The {@link GridSampleDimension#getScale() scale} value which is
336: * multiplied to sample values for this category.
337: * @param offset The {@link GridSampleDimension#getOffset() offset} value to add
338: * to sample values for this category.
339: *
340: * @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper}.
341: * @throws IllegalArgumentException if {@code scale} or {@code offset} are not real numbers.
342: */
343: public Category(final CharSequence name, final Color[] colors,
344: final NumberRange sampleValueRange, final double scale,
345: final double offset) throws IllegalArgumentException {
346: this (name, colors, sampleValueRange, createLinearTransform(
347: scale, offset));
348: try {
349: assert Double.doubleToLongBits(transform.derivative(0)) == Double
350: .doubleToLongBits(scale);
351: assert Double.doubleToLongBits(transform.transform(0)) == Double
352: .doubleToLongBits(offset);
353: } catch (TransformException exception) {
354: throw new AssertionError(exception);
355: }
356: if (Double.isNaN(scale) || Double.isInfinite(scale)) {
357: throw new IllegalArgumentException(Errors.format(
358: ErrorKeys.BAD_COEFFICIENT_$2, "scale", new Double(
359: scale)));
360: }
361: if (Double.isNaN(offset) || Double.isInfinite(offset)) {
362: throw new IllegalArgumentException(Errors.format(
363: ErrorKeys.BAD_COEFFICIENT_$2, "offset", new Double(
364: offset)));
365: }
366: }
367:
368: /**
369: * Constructs a quantitative category mapping samples to geophysics values in the specified
370: * range. Sample values in the {@code sampleValueRange} will be mapped to geophysics
371: * values in the {@code geophysicsValueRange} through a linear equation of the form:
372: *
373: * <center><var>x</var><code> = {@linkplain GridSampleDimension#getOffset()
374: * offset} + {@linkplain GridSampleDimension#getScale()
375: * scale}×</code><var>s</var></center>
376: *
377: * {@code scale} and {@code offset} coefficients are computed from the ranges supplied in
378: * arguments.
379: *
380: * @param name The category name as a {@link String} or {@link InternationalString} object.
381: * @param colors A set of colors for this category. This array may have any length;
382: * colors will be interpolated as needed. An array of length 1 means
383: * that an uniform color should be used for all sample values. An array
384: * of length 0 or a {@code null} array means that some default colors
385: * should be used (usually a gradient from opaque black to opaque white).
386: * @param sampleValueRange The range of sample values for this category. Element class
387: * is usually {@link Integer}, but {@link Float} and {@link Double} are
388: * accepted as well.
389: * @param geophysicsValueRange The range of geophysics values for this category.
390: * Element class is usually {@link Float} or {@link Double}.
391: *
392: * @throws ClassCastException if the range element class is not a {@link Number} subclass.
393: * @throws IllegalArgumentException if the range is invalid.
394: */
395: public Category(final CharSequence name, final Color[] colors,
396: final NumberRange sampleValueRange,
397: final NumberRange geophysicsValueRange)
398: throws IllegalArgumentException {
399: this (name, colors, sampleValueRange, createLinearTransform(
400: sampleValueRange, geophysicsValueRange));
401: inverse.range = geophysicsValueRange;
402: assert range.equals(sampleValueRange);
403: }
404:
405: /**
406: * Constructs a qualitative or quantitative category for samples in the specified range.
407: * Sample values (usually integers) will be converted into geophysics values (usually
408: * floating-point) through the {@code sampleToGeophysics} transform.
409: *
410: * @param name The category name as a {@link String} or {@link InternationalString} object.
411: * @param colors A set of colors for this category. This array may have any length;
412: * colors will be interpolated as needed. An array of length 1 means
413: * that an uniform color should be used for all sample values. An array
414: * of length 0 or a {@code null} array means that some default colors
415: * should be used (usually a gradient from opaque black to opaque white).
416: * @param sampleValueRange The range of sample values for this category. Element class
417: * is usually {@link Integer}, but {@link Float} and {@link Double} are
418: * accepted as well.
419: * @param sampleToGeophysics A transform from sample values to geophysics values,
420: * or {@code null} if this category is not a quantitative one.
421: *
422: * @throws ClassCastException if the range element class is not a {@link Number} subclass.
423: * @throws IllegalArgumentException if the range is invalid.
424: */
425: public Category(final CharSequence name, final Color[] colors,
426: final NumberRange sampleValueRange,
427: final MathTransform1D sampleToGeophysics)
428: throws IllegalArgumentException {
429: this (name, toARGB(colors), sampleValueRange, sampleToGeophysics);
430: }
431:
432: /**
433: * Constructs a category with the specified math transform. This private constructor is
434: * used for both qualitative and quantitative category constructors. It also used by
435: * {@link #recolor} in order to construct a new category similar to this one except for
436: * ARGB codes.
437: */
438: private Category(final CharSequence name, final int[] ARGB,
439: final NumberRange range, MathTransform1D sampleToGeophysics)
440: throws IllegalArgumentException {
441: ensureNonNull("name", name);
442: this .name = SimpleInternationalString.wrap(name);
443: this .ARGB = ARGB;
444: this .range = range;
445: Class type = range.getElementClass();
446: boolean minInc = range.isMinIncluded();
447: boolean maxInc = range.isMaxIncluded();
448: this .minimum = doubleValue(type, range.getMinValue(),
449: minInc ? 0 : +1);
450: this .maximum = doubleValue(type, range.getMaxValue(),
451: maxInc ? 0 : -1);
452: /*
453: * If we are constructing a qualitative category for a single NaN value,
454: * accepts it as a valid one.
455: */
456: if (sampleToGeophysics == null
457: && minInc
458: && maxInc
459: && Double.isNaN(minimum)
460: && Double.doubleToRawLongBits(minimum) == Double
461: .doubleToRawLongBits(maximum)) {
462: inverse = this ;
463: transform = createLinearTransform(0, minimum);
464: return;
465: }
466: /*
467: * Checks the arguments. Use '!' in comparaison in order to reject NaN values,
468: * except for the legal case catched by the "if" block just above.
469: */
470: if (!(minimum <= maximum) || Double.isInfinite(minimum)
471: || Double.isInfinite(maximum)) {
472: throw new IllegalArgumentException(Errors.format(
473: ErrorKeys.BAD_RANGE_$2, range.getMinValue(), range
474: .getMaxValue()));
475: }
476: /*
477: * Now initialize the geophysics category.
478: */
479: TransformException cause = null;
480: try {
481: if (sampleToGeophysics == null) {
482: inverse = new GeophysicsCategory(this , false);
483: transform = createLinearTransform(0, inverse.minimum); // sample to geophysics
484: return;
485: }
486: transform = sampleToGeophysics; // Must be set before GeophysicsCategory construction!
487: if (sampleToGeophysics.isIdentity()) {
488: inverse = this ;
489: } else {
490: inverse = new GeophysicsCategory(this , true);
491: }
492: if (inverse.minimum <= inverse.maximum) {
493: return;
494: }
495: // If we reach this point, geophysics range is NaN. This is an illegal argument.
496: } catch (TransformException exception) {
497: cause = exception;
498: }
499: IllegalArgumentException exception = new IllegalArgumentException(
500: Errors.format(ErrorKeys.BAD_TRANSFORM_$1, Utilities
501: .getShortClassName(sampleToGeophysics)));
502: exception.initCause(cause);
503: throw exception;
504: }
505:
506: /**
507: * Construct a geophysics category. <strong>This constructor should never
508: * be invoked outside {@link GeophysicsCategory} constructor.</strong>
509: *
510: * @param inverse The originating {@link Category}.
511: * @param isQuantitative {@code true} if the originating category is quantitative.
512: * @throws TransformException if a transformation failed.
513: *
514: * @todo The algorithm for finding minimum and maximum values is very simple for
515: * now and will not work if the transformation has local extremas. We would
516: * need some more sophesticated algorithm for the most general cases. Such
517: * a general algorithm would be usefull in {@link GeophysicsCategory#getRange}
518: * as well.
519: */
520: Category(final Category inverse, final boolean isQuantitative)
521: throws TransformException {
522: assert (this instanceof GeophysicsCategory);
523: assert !(inverse instanceof GeophysicsCategory);
524: this .inverse = inverse;
525: this .name = inverse.name;
526: this .ARGB = inverse.ARGB;
527: if (!isQuantitative) {
528: minimum = maximum = XMath.toNaN((int) Math
529: .round((inverse.minimum + inverse.maximum) / 2));
530: transform = createLinearTransform(0, inverse.minimum); // geophysics to sample
531: return;
532: }
533: /*
534: * Compute 'minimum' and 'maximum' (which must be real numbers) using the transformation
535: * from sample to geophysics values. To be strict, we should use some numerical algorithm
536: * for finding a function's minimum and maximum. For linear and logarithmic functions,
537: * minimum and maximum are always at the bounding input values, so we are using a very
538: * simple algorithm for now.
539: */
540: transform = (MathTransform1D) inverse.transform.inverse();
541: final double min = inverse.transform.transform(inverse.minimum);
542: final double max = inverse.transform.transform(inverse.maximum);
543: if (min > max) {
544: minimum = max;
545: maximum = min;
546: } else {
547: minimum = min;
548: maximum = max;
549: }
550: }
551:
552: /**
553: * Returns a linear transform with the supplied scale and offset values.
554: *
555: * @param scale The scale factor. May be 0 for a constant transform.
556: * @param offset The offset value. May be NaN if this method is invoked from a constructor
557: * for initializing {@link #transform} for a qualitative category.
558: */
559: static MathTransform1D createLinearTransform(final double scale,
560: final double offset) {
561: return LinearTransform1D.create(scale, offset);
562: }
563:
564: /**
565: * Create a linear transform mapping values from {@code sampleValueRange}
566: * to {@code geophysicsValueRange}.
567: */
568: private static MathTransform1D createLinearTransform(
569: final NumberRange sampleValueRange,
570: final NumberRange geophysicsValueRange) {
571: final Class sType = sampleValueRange.getElementClass();
572: final Class gType = geophysicsValueRange.getElementClass();
573: /*
574: * First, find the direction of the adjustment to apply to the ranges if we wanted
575: * all values to be inclusives. Then, check if the adjustment is really needed: if
576: * the values of both ranges are inclusive or exclusive, then there is no need for
577: * an adjustment before computing the coefficient of a linear relation.
578: */
579: int sMinInc = sampleValueRange.isMinIncluded() ? 0 : +1;
580: int sMaxInc = sampleValueRange.isMaxIncluded() ? 0 : -1;
581: int gMinInc = geophysicsValueRange.isMinIncluded() ? 0 : +1;
582: int gMaxInc = geophysicsValueRange.isMaxIncluded() ? 0 : -1;
583: if (sMinInc == gMinInc)
584: sMinInc = gMinInc = 0;
585: if (sMaxInc == gMaxInc)
586: sMaxInc = gMaxInc = 0;
587: /*
588: * If the minimal geophysics value is exclusive while the minimal sample value is inclusive,
589: * prepares to substract 1 to the sample value in order to make it exclusive (so that sample
590: * and geophysics values have the same "exclusive" state). Do similar processing on maximal
591: * values as well. Note: the change is usually applied on sample values, but may be applied
592: * on geophysics values instead if sample are floats or geophysics values are integers.
593: */
594: final boolean adjustSamples = (XMath.isInteger(sType) && !XMath
595: .isInteger(gType));
596: if ((adjustSamples ? gMinInc : sMinInc) != 0) {
597: int swap = sMinInc;
598: sMinInc = -gMinInc;
599: gMinInc = -swap;
600: }
601: if ((adjustSamples ? gMaxInc : sMaxInc) != 0) {
602: int swap = sMaxInc;
603: sMaxInc = -gMaxInc;
604: gMaxInc = -swap;
605: }
606: /*
607: * Now, extracts the minimal and maximal values and computes the linear coefficients.
608: */
609: final double minSample = doubleValue(sType, sampleValueRange
610: .getMinValue(), sMinInc);
611: final double maxSample = doubleValue(sType, sampleValueRange
612: .getMaxValue(), sMaxInc);
613: final double minValue = doubleValue(gType, geophysicsValueRange
614: .getMinValue(), gMinInc);
615: final double maxValue = doubleValue(gType, geophysicsValueRange
616: .getMaxValue(), gMaxInc);
617: double scale = (maxValue - minValue) / (maxSample - minSample);
618: if (Double.isNaN(scale) && !Double.isNaN(maxValue - minValue)
619: && !Double.isNaN(maxSample - minSample)) {
620: scale = 1.0;
621: }
622: final double offset = minValue - scale * minSample;
623: return createLinearTransform(scale, offset);
624: }
625:
626: /**
627: * Returns a {@code double} value for the specified number. If {@code direction}
628: * is non-zero, then this method will returns the closest representable number of type
629: * {@code type} before or after the double value.
630: *
631: * @param type The range element class. {@code number} must be
632: * an instance of this class (this will not be checked).
633: * @param number The number to transform to a {@code double} value.
634: * @param direction -1 to return the previous representable number,
635: * +1 to return the next representable number, or
636: * 0 to return the number with no change.
637: */
638: private static double doubleValue(final Class type,
639: final Comparable number, final int direction) {
640: assert (direction >= -1) && (direction <= +1) : direction;
641: return XMath.rool(type, ((Number) number).doubleValue(),
642: direction);
643: }
644:
645: /**
646: * Convert an array of colors to an array of ARGB values.
647: * If {@code colors} is null, then a default array
648: * will be returned.
649: *
650: * @param colors The array of colors to convert (may be null).
651: * @return The colors as ARGB values. Never null.
652: */
653: private static int[] toARGB(final Color[] colors) {
654: final int[] ARGB;
655: if (colors != null && colors.length != 0) {
656: ARGB = new int[colors.length];
657: for (int i = 0; i < ARGB.length; i++) {
658: ARGB[i] = colors[i].getRGB();
659: }
660: } else {
661: ARGB = DEFAULT;
662: }
663: return ARGB;
664: }
665:
666: /**
667: * Returns ARGB values for the specified color. If {@code color}
668: * is null, a default ARGB code will be returned.
669: */
670: private static int[] toARGB(Color color, final int sample) {
671: if (color == null) {
672: color = CYCLE[Math.abs(sample) % CYCLE.length];
673: }
674: return toARGB(new Color[] { color });
675: }
676:
677: /**
678: * Returns the category name.
679: */
680: public InternationalString getName() {
681: return name;
682: }
683:
684: /**
685: * Returns the set of colors for this category.
686: * Change to the returned array will not affect
687: * this category.
688: *
689: * @see GridSampleDimension#getColorModel
690: */
691: public Color[] getColors() {
692: final Color[] colors = new Color[ARGB.length];
693: for (int i = 0; i < colors.length; i++) {
694: colors[i] = new Color(ARGB[i], true);
695: }
696: return colors;
697: }
698:
699: /**
700: * Returns the range of sample values occurring in this category. Sample values can be
701: * transformed into geophysics values using the {@link #getSampleToGeophysics} transform.
702: *
703: * @return The range of sample values.
704: *
705: * @see NumberRange#getMinimum(boolean)
706: * @see NumberRange#getMaximum(boolean)
707: * @see GridSampleDimension#getMinimumValue()
708: * @see GridSampleDimension#getMaximumValue()
709: */
710: public NumberRange getRange() {
711: assert range != null;
712: return range;
713: }
714:
715: /**
716: * Returns a transform from sample values to geophysics values. If this category
717: * is not a quantitative one, then this method returns {@code null}.
718: */
719: public MathTransform1D getSampleToGeophysics() {
720: return isQuantitative() ? transform : null;
721: }
722:
723: /**
724: * Returns {@code true} if this category is quantitative. A quantitative category
725: * has a non-null {@link #getSampleToGeophysics() sampleToGeophysics} transform.
726: *
727: * @return {@code true} if this category is quantitative, or
728: * {@code false} if this category is qualitative.
729: */
730: public boolean isQuantitative() {
731: return !Double.isNaN(inverse.minimum)
732: && !Double.isNaN(inverse.maximum);
733: }
734:
735: /**
736: * Returns a category for the same range of sample values but a different color palette.
737: * The array given in argument may have any length; colors will be interpolated as needed.
738: * An array of length 1 means that an uniform color should be used for all sample values.
739: * An array of length 0 or a {@code null} array means that some default colors should be
740: * used (usually a gradient from opaque black to opaque white).
741: *
742: * @param colors A set of colors for the new category.
743: * @return A category with the new color palette, or {@code this}
744: * if the new colors are identical to the current ones.
745: *
746: * @see org.geotools.coverage.processing.ColorMap#recolor
747: */
748: public Category recolor(final Color[] colors) {
749: // GeophysicsCategory overrides this method in such
750: // a way that the case below should never occurs.
751: assert !(this instanceof GeophysicsCategory) : this ;
752: final int[] newARGB = toARGB(colors);
753: if (Arrays.equals(ARGB, newARGB)) {
754: return this ;
755: }
756: // The range can be null only for GeophysicsCategory cases. Because
757: // the later override this method, the case below should never occurs.
758: assert range != null : this ;
759: final Category newCategory = new Category(name, newARGB, range,
760: getSampleToGeophysics());
761: newCategory.inverse.range = inverse.range; // Share a common instance.
762: return newCategory;
763: }
764:
765: /**
766: * Changes the mapping from sample to geophysics values. This method returns a category with
767: * a "{@linkplain #getSampleToGeophysics sample to geophysics}" transformation set to the
768: * specified one. Other properties like the {@linkplain #getRange sample value range}
769: * and the {@linkplain #getColors colors} are unchanged.
770: * <p>
771: * <strong>Note about geophysics categories:</strong> The above rules are straightforward
772: * when applied on non-geophysics category, but this method can be invoked on geophysics
773: * category (as returned by <code>{@linkplain #geophysics geophysics}(true)</code>) as well.
774: * Since geophysics categories are already the result of some "sample to geophysics"
775: * transformation, invoking this method on those is equivalent to
776: * {@linkplain MathTransformFactory#createConcatenatedTransform concatenate}
777: * this "sample to geophysics" transform with the specified one.
778: *
779: * @param sampleToGeophysics The new {@linkplain #getSampleToGeophysics sample to geophysics}
780: * transform.
781: * @return A category using the specified transform.
782: *
783: * @see #getSampleToGeophysics
784: * @see GridSampleDimension#rescale
785: */
786: public Category rescale(final MathTransform1D sampleToGeophysics) {
787: if (Utilities.equals(sampleToGeophysics, transform)) {
788: return this ;
789: }
790: return new Category(name, ARGB, range, sampleToGeophysics);
791: }
792:
793: /**
794: * If {@code true}, returns the geophysics companion of this category. By definition, a
795: * <cite>geophysics category</cite> is a category with a {@linkplain #getRange range of sample
796: * values} transformed in such a way that the {@link #getSampleToGeophysics sampleToGeophysics}
797: * transform is always the identity transform, or {@code null} if no such transform existed
798: * in the first place. In other words, the range of sample values in a geophysics category maps
799: * directly the "real world" values without the need for any transformation.
800: * <p>
801: * {@code Category} objects live by pair: a <cite>geophysics</cite> one (used for
802: * computation) and a <cite>non-geophysics</cite> one (used for packing data, usually as
803: * integers). The {@code geo} argument specifies which object from the pair is wanted,
804: * regardless if this method is invoked on the geophysics or non-geophysics instance of the
805: * pair. In other words, the result of {@code geophysics(b1).geophysics(b2).geophysics(b3)}
806: * depends only on the value in the last call ({@code b3}).
807: * <p>
808: * Newly constructed categories are non-geophysics (i.e. a {@linkplain #getSampleToGeophysics
809: * sample to geophysics} transform must be applied in order to gets geophysics values).
810: *
811: * @param geo {@code true} to get a category with an identity
812: * {@linkplain #getSampleToGeophysics transform} and a {@linkplain #getRange range of
813: * sample values} matching the geophysics values, or {@code false} to get back the
814: * original category (the one constructed with {@code new Category(...)}).
815: * @return The category. Never {@code null}, but may be {@code this}.
816: *
817: * @see GridSampleDimension#geophysics
818: * @see org.geotools.coverage.grid.GridCoverage2D#geophysics
819: */
820: public Category geophysics(final boolean geo) {
821: return geo ? inverse : this ;
822: }
823:
824: /**
825: * Returns a hash value for this category.
826: * This value need not remain consistent between
827: * different implementations of the same class.
828: */
829: public int hashCode() {
830: return name.hashCode();
831: }
832:
833: /**
834: * Compares the specified object with
835: * this category for equality.
836: */
837: public boolean equals(final Object object) {
838: if (object == this ) {
839: // Slight optimization
840: return true;
841: }
842: if (object != null && object.getClass().equals(getClass())) {
843: final Category that = (Category) object;
844: if (Double.doubleToRawLongBits(minimum) == Double
845: .doubleToRawLongBits(that.minimum)
846: && Double.doubleToRawLongBits(maximum) == Double
847: .doubleToRawLongBits(that.maximum)
848: && Utilities.equals(this .transform, that.transform)
849: && Utilities.equals(this .name, that.name)
850: && Arrays.equals(this .ARGB, that.ARGB)) {
851: // Special test for 'range', since 'GeophysicsCategory'
852: // computes it only when first needed.
853: if (this .range != null && that.range != null) {
854: if (!Utilities.equals(this .range, that.range)) {
855: return false;
856: }
857: if (inverse instanceof GeophysicsCategory) {
858: assert inverse.equals(that.inverse);
859: }
860: return true;
861: }
862: assert (this instanceof GeophysicsCategory);
863: return true;
864: }
865: }
866: return false;
867: }
868:
869: /**
870: * Returns a string representation of this category.
871: * The returned string is implementation dependent.
872: * It is usually provided for debugging purposes.
873: */
874: public String toString() {
875: final StringBuffer buffer = new StringBuffer(Utilities
876: .getShortClassName(this ));
877: buffer.append("(\"");
878: buffer.append(name);
879: buffer.append("\":[");
880: if (Double.isNaN(minimum) && Double.isNaN(maximum)) {
881: buffer.append("NaN(");
882: buffer.append(Math.round(inverse.minimum));
883: buffer.append("...");
884: buffer.append(Math.round(inverse.maximum));
885: buffer.append(')');
886: } else {
887: if (XMath.isInteger(getRange().getElementClass())) {
888: buffer.append(Math.round(minimum));
889: buffer.append("...");
890: buffer.append(Math.round(maximum)); // Inclusive
891: } else {
892: buffer.append(minimum);
893: buffer.append(" ... ");
894: buffer.append(maximum); // Inclusive
895: }
896: }
897: buffer.append("])");
898: return buffer.toString();
899: }
900:
901: /**
902: * Makes sure that an argument is non-null.
903: *
904: * @param name Argument name.
905: * @param object User argument.
906: * @throws IllegalArgumentException if {@code object} is null.
907: */
908: static void ensureNonNull(final String name, final Object object)
909: throws IllegalArgumentException {
910: if (object == null) {
911: throw new IllegalArgumentException(Errors.format(
912: ErrorKeys.NULL_ARGUMENT_$1, name));
913: }
914: }
915: }
|