/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.coverage;
import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.SampleModel;
import org.opengis.util.InternationalString;
import org.opengis.coverage.ColorInterpretation;
import org.opengis.coverage.SampleDimensionType;
import static org.opengis.coverage.SampleDimensionType.*;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.image.ColorUtilities;
import org.geotools.util.AbstractInternationalString;
import org.geotools.util.Range;
import org.geotools.util.SimpleInternationalString;
import org.geotools.util.NumberRange;
/**
* Utility methods for choosing a {@linkplain SampleModel sample model} or a
* {@linkplain ColorModel color model} on the basis of a range of values.
* This class provides also some methods for mapping {@link SampleDimensionType}
* to {@link DataBuffer} types.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public final class TypeMap {
/**
* The mapping of {@link SampleDimensionType} to {@link DataBuffer} types.
*/
private static final TypeMap[] MAP = new TypeMap[SampleDimensionType.values().length];
static {
final Map<Number,Number> pool = new HashMap<Number,Number>(32);
final Float M1 = -Float .MAX_VALUE;
final Float P1 = Float .MAX_VALUE;
final Double M2 = -Double.MAX_VALUE;
final Double P2 = Double.MAX_VALUE;
// The constructor will register automatically those objects in the above array.
new TypeMap( UNSIGNED_1BIT, DataBuffer.TYPE_BYTE, (byte) 1, false, false, pool);
new TypeMap( UNSIGNED_2BITS, DataBuffer.TYPE_BYTE, (byte) 2, false, false, pool);
new TypeMap( UNSIGNED_4BITS, DataBuffer.TYPE_BYTE, (byte) 4, false, false, pool);
new TypeMap( UNSIGNED_8BITS, DataBuffer.TYPE_BYTE, (byte) 8, false, false, pool);
new TypeMap( SIGNED_8BITS, DataBuffer.TYPE_BYTE, (byte) 8, true, false, pool);
new TypeMap(UNSIGNED_16BITS, DataBuffer.TYPE_USHORT, (byte)16, false, false, pool);
new TypeMap( SIGNED_16BITS, DataBuffer.TYPE_SHORT, (byte)16, true, false, pool);
new TypeMap(UNSIGNED_32BITS, DataBuffer.TYPE_INT, (byte)32, false, false, pool);
new TypeMap( SIGNED_32BITS, DataBuffer.TYPE_INT, (byte)32, true, false, pool);
new TypeMap( REAL_32BITS, DataBuffer.TYPE_FLOAT, (byte)32, true, true, M1, P1, pool);
new TypeMap( REAL_64BITS, DataBuffer.TYPE_DOUBLE, (byte)64, true, true, M2, P2, pool);
};
/**
* One of {@link SampleDimensionType} code list.
*/
private final SampleDimensionType code;
/**
* The {@link DataBuffer} type. Must be one of the following constants:
* {@link DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT},
* {@link DataBuffer#TYPE_SHORT}, {@link DataBuffer#TYPE_INT},
* {@link DataBuffer#TYPE_FLOAT}, {@link DataBuffer#TYPE_DOUBLE}.
*/
private final int type;
/**
* The size in bits. The value range from 1 to 64. This is different than
* {@link DataBuffer#getDataTypeSize}, which have values ranging from 8 to 64.
*/
private final byte size;
/**
* {@code true} for signed sample type.
*/
private final boolean signed;
/**
* {@code true} for floating-point data type.
*/
private final boolean real;
/**
* The full range of sample values.
*/
private final NumberRange<? extends Number> range;
/**
* The range of positive sample values (excluding 0). This range is non-null only for unsigned
* type. A range excluding 0 is sometime usefull when the 0 value is reserved for a "no data"
* category.
*/
private final NumberRange<? extends Number> positiveRange;
/**
* The name as an international string.
*/
private final InternationalString name = new AbstractInternationalString() {
public String toString(final Locale locale) {
return Vocabulary.getResources(locale).getString(VocabularyKeys.DATA_TYPE_$2,
Integer.valueOf(real ? 2 : signed ? 1 : 0), size);
}
};
/**
* Constructs a new mapping with the specified value.
*/
private TypeMap(final SampleDimensionType code,
final int type, final byte size,
final boolean signed, final boolean real,
final Map<Number,Number> pool)
{
this(code, type, size, signed, real, null, null, pool);
}
/**
* Constructs a new mapping with the specified value.
*/
private TypeMap(final SampleDimensionType code,
final int type, final byte size,
final boolean signed, final boolean real,
Number lower, Number upper, final Map<Number,Number> pool)
{
Number one = null;
if (lower == null) {
final long max = (1L << (signed ? size-1 : size)) - 1;
final long min = signed ? ~max : 0; // Tild (~), not minus sign (-).
if (max <= Byte.MAX_VALUE) {
lower = Byte.valueOf((byte) min);
upper = Byte.valueOf((byte) max);
one = Byte.valueOf((byte) 1);
} else if (max <= Short.MAX_VALUE) {
lower = Short.valueOf((short) min);
upper = Short.valueOf((short) max);
one = Short.valueOf((short) 1);
} else if (max <= Integer.MAX_VALUE) {
lower = Integer.valueOf((int) min);
upper = Integer.valueOf((int) max);
one = Integer.valueOf(1);
} else {
lower = Long.valueOf(min);
upper = Long.valueOf(max);
one = Long.valueOf(1L);
}
lower = unique(pool, lower);
upper = unique(pool, upper);
one = unique(pool, one);
assert lower.longValue() == min;
assert upper.longValue() == max;
}
assert ((Comparable) lower).compareTo(upper) < 0 : upper;
final Class<? extends Number> c = upper.getClass();
this.code = code;
this.type = type;
this.size = size;
this.signed = signed;
this.real = real;
this.range = new NumberRange(c, lower, upper);
this.positiveRange = signed ? null : new NumberRange(c, one, upper);
final int ordinal = code.ordinal();
assert MAP[ordinal] == null : code;
MAP[ordinal] = this;
assert code.equals(getSampleDimensionType(range)) : code;
}
/**
* Returns a single instance of the specified number.
*/
private static Number unique(final Map<Number,Number> pool, final Number n) {
final Number candidate = pool.put(n, n);
if (candidate == null) {
return n;
}
pool.put(candidate, candidate);
return candidate;
}
/**
* Returns the smallest sample dimension type capable to hold the specified range of values.
*
* @param range The range of values.
* @return The smallest sample dimension type for the specified range.
*/
public static SampleDimensionType getSampleDimensionType(final Range<?> range) {
final Class<?> type = range.getElementClass();
if (Double.class.isAssignableFrom(type)) {
return REAL_64BITS;
}
if (Float.class.isAssignableFrom(type)) {
return REAL_32BITS;
}
long min = ((Number) range.getMinValue()).longValue();
long max = ((Number) range.getMaxValue()).longValue();
if (!range.isMinIncluded()) min++;
if (!range.isMaxIncluded()) max--;
return getSampleDimensionType(min, max);
}
/**
* Returns the smallest sample dimension type capable to hold the specified range of values.
* An heuristic approach is used for non-integer values.
*
* @param min The lower value, inclusive.
* @param max The upper value, <strong>inclusive</strong> as well.
* @return The smallest sample dimension type for the specified range.
*/
public static SampleDimensionType getSampleDimensionType(double min, double max) {
final long lgMin = (long) min;
if (lgMin == min) {
final long lgMax = (long) max;
if (lgMax == max) {
return getSampleDimensionType(lgMin, lgMax);
}
}
min = Math.abs(min);
max = Math.abs(max);
if (Math.min(min,max) >= Float.MIN_VALUE && Math.max(min,max) <= Float.MAX_VALUE) {
return REAL_32BITS;
}
return REAL_64BITS;
}
/**
* Returns the smallest sample dimension type capable to hold the specified range of values.
*
* @param min The lower value, inclusive.
* @param max The upper value, <strong>inclusive</strong> as well.
* @return The smallest sample dimension type for the specified range.
*/
public static SampleDimensionType getSampleDimensionType(final long min, final long max) {
if (min >= 0) {
if (max < (1L << 1)) return UNSIGNED_1BIT;
if (max < (1L << 2)) return UNSIGNED_2BITS;
if (max < (1L << 4)) return UNSIGNED_4BITS;
if (max < (1L << 8)) return UNSIGNED_8BITS;
if (max < (1L << 16)) return UNSIGNED_16BITS;
if (max < (1L << 32)) return UNSIGNED_32BITS;
} else {
if (min>=Byte .MIN_VALUE && max<=Byte .MAX_VALUE) return SIGNED_8BITS;
if (min>=Short .MIN_VALUE && max<=Short .MAX_VALUE) return SIGNED_16BITS;
if (min>=Integer.MIN_VALUE && max<=Integer.MAX_VALUE) return SIGNED_32BITS;
}
return REAL_32BITS;
}
/**
* Returns the sample dimension type for the specified sample model and band number. If
* the sample model use an undefined data type, then this method returns {@code null}.
*
* @param model The sample model.
* @param band The band to query.
* @return The sample dimension type for the specified sample model and band number.
* @throws IllegalArgumentException if the band number is not in the valid range.
*/
@SuppressWarnings("fallthrough")
public static SampleDimensionType getSampleDimensionType(final SampleModel model, final int band)
throws IllegalArgumentException
{
if (band<0 || band>=model.getNumBands()) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_BAND_NUMBER_$1, band));
}
boolean signed = true;
switch (model.getDataType()) {
case DataBuffer.TYPE_DOUBLE: return REAL_64BITS;
case DataBuffer.TYPE_FLOAT: return REAL_32BITS;
case DataBuffer.TYPE_USHORT: // Fall through
case DataBuffer.TYPE_BYTE: signed=false; // Fall through
case DataBuffer.TYPE_INT:
case DataBuffer.TYPE_SHORT: {
switch (model.getSampleSize(band)) {
case 1: return UNSIGNED_1BIT;
case 2: return UNSIGNED_2BITS;
case 4: return UNSIGNED_4BITS;
case 8: return signed ? SIGNED_8BITS : UNSIGNED_8BITS;
case 16: return signed ? SIGNED_16BITS : UNSIGNED_16BITS;
case 32: return signed ? SIGNED_32BITS : UNSIGNED_32BITS;
}
}
}
return null;
}
/**
* Returns the sample dimension type name as an international string. For example, the localized
* name for {@link SampleDimensionType#UNSIGNED_16BITS} is "<cite>16 bits unsigned integer</cite>"
* in English and "<cite>Entier non-signé sur 16 bits</cite>" in French.
*/
public static InternationalString getName(final SampleDimensionType type) {
final int ordinal = type.ordinal();
if (ordinal>=0 && ordinal<MAP.length) {
return MAP[ordinal].name;
}
return new SimpleInternationalString(type.name());
}
/**
* Returns the {@link DataBuffer} type. This is one of the following constants:
* {@link DataBuffer#TYPE_BYTE TYPE_BYTE},
* {@link DataBuffer#TYPE_USHORT TYPE_USHORT},
* {@link DataBuffer#TYPE_SHORT TYPE_SHORT},
* {@link DataBuffer#TYPE_INT TYPE_INT},
* {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT},
* {@link DataBuffer#TYPE_DOUBLE TYPE_DOUBLE} or
* {@link DataBuffer#TYPE_UNDEFINED} if the type is unrecognized.
*/
public static int getDataBufferType(final SampleDimensionType type) {
if (type != null) {
final int ordinal = type.ordinal();
if (ordinal>=0 && ordinal<MAP.length) {
return MAP[ordinal].type;
}
}
return DataBuffer.TYPE_UNDEFINED;
}
/**
* Returns the size in bits. The value range from 1 to 64. This is similar, but
* different than {@link DataBuffer#getDataTypeSize}, which have values ranging
* from 8 to 64.
*/
public static int getSize(final SampleDimensionType type) {
return map(type).size;
}
/**
* Returns {@code true} for signed sample type.
*/
public static boolean isSigned(final SampleDimensionType type) {
return map(type).signed;
}
/**
* Returns {@code true} for floating-point data type.
*/
public static boolean isFloatingPoint(final SampleDimensionType type) {
return map(type).real;
}
/**
* Returns the full range of sample values for the specified dimension type.
*/
public static NumberRange<? extends Number> getRange(final SampleDimensionType type) {
if (type != null) {
final int ordinal = type.ordinal();
if (ordinal>=0 && ordinal<MAP.length) {
return MAP[ordinal].range;
}
}
return null;
}
/**
* Returns the range of positive sample values (excluding 0). This range is non-null only for
* unsigned type. A range excluding 0 is sometime usefull when the 0 value is reserved for a
* "no data" category.
*/
public static NumberRange<? extends Number> getPositiveRange(final SampleDimensionType type) {
if (type != null) {
final int ordinal = type.ordinal();
if (ordinal>=0 && ordinal<MAP.length) {
return MAP[ordinal].positiveRange;
}
}
return null;
}
/**
* Returns the mapper for the specified sample dimension type. If no map is found for the
* specified sample dimension type, then an exception is thrown.
*/
private static TypeMap map(final SampleDimensionType type) throws IllegalArgumentException {
if (type != null) {
final int ordinal = type.ordinal();
if (ordinal>=0 && ordinal<MAP.length) {
final TypeMap map = MAP[ordinal];
if (map != null) {
return map;
}
}
}
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "type", type));
}
/**
* Wraps the specified value into a number of the specified data type. If the
* value can't fit in the specified type, then a wider type is choosen unless
* {@code allowWidening} is {@code false}.
*
* @param value The value to wrap in a {@link Number} object.
* @param type A constant from the {@link SampleDimensionType} code list.
* @param allowWidening {@code true} if this method is allowed to returns
* a wider type than the usual one for the specified {@code type}.
* @return The value as a {@link Number}.
* @throws IllegalArgumentException if {@code type} is not a recognized constant.
* @throws IllegalArgumentException if {@code allowWidening} is {@code false}
* and the specified {@code value} can't fit in the specified sample type.
*/
@SuppressWarnings("fallthrough")
public static Number wrapSample(final double value,
final SampleDimensionType type,
final boolean allowWidening)
throws IllegalArgumentException
{
/*
* Note about 'ordinal' computation: We would like to switch on SampleDimensionType
* ordinal values. But the compiler requires constant values, and doesn't recognize
* SampleDimensionType ordinal as such. As a workaround, we use the sample size (in
* bits) with the following convention: negative value if signed, and offset by 16
* bits if floating point numbers.
*/
final TypeMap map = map(type);
int ordinal = map.size;
if (map.real) {
ordinal <<= 16;
} else if (map.signed) {
ordinal = -ordinal;
}
switch (ordinal) {
case 1: // Fall through
case 2: // Fall through
case 4: // Fall through
case -8: {
final byte candidate = (byte) value;
if (candidate == value) {
return new Byte(candidate);
}
if (!allowWidening) break;
// Fall through
}
case 8: // Fall through
case -16: {
final short candidate = (short) value;
if (candidate == value) {
return Short.valueOf(candidate);
}
if (!allowWidening) break;
// Fall through
}
case 16: // Fall through
case -32: {
final int candidate = (int) value;
if (candidate == value) {
return Integer.valueOf(candidate);
}
if (!allowWidening) break;
// Fall through
}
case 32: {
final long candidate = (long) value;
if (candidate == value) {
return Long.valueOf(candidate);
}
if (!allowWidening) break;
// Fall through
}
case (32 << 16): {
if (!allowWidening || Math.abs(value) <= Float.MAX_VALUE) {
return Float.valueOf((float) value);
}
// Fall through
}
case (64 << 16): {
return Double.valueOf(value);
}
default: {
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "type", type));
}
}
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "value", value));
}
/**
* Returns the color interpretation code for the specified color model and band number.
*
* @param model The color model.
* @param band The band to query.
* @return The code for the specified color model and band number.
* @throws IllegalArgumentException if the band number is not in the valid range.
*/
@SuppressWarnings("deprecation")
public static ColorInterpretation getColorInterpretation(final ColorModel model, final int band)
throws IllegalArgumentException
{
if (band<0 || band>=ColorUtilities.getNumBands(model)) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_BAND_NUMBER_$1, band));
}
if (model instanceof IndexColorModel) {
return ColorInterpretation.PALETTE_INDEX;
}
switch (model.getColorSpace().getType()) {
case ColorSpace.TYPE_GRAY: {
switch (band) {
case 0: return ColorInterpretation.GRAY_INDEX;
default: return ColorInterpretation.UNDEFINED;
}
}
case ColorSpace.TYPE_RGB: {
switch (band) {
case 0: return ColorInterpretation.RED_BAND;
case 1: return ColorInterpretation.GREEN_BAND;
case 2: return ColorInterpretation.BLUE_BAND;
case 3: return ColorInterpretation.ALPHA_BAND;
default: return ColorInterpretation.UNDEFINED;
}
}
case ColorSpace.TYPE_HSV: {
switch (band) {
case 0: return ColorInterpretation.HUE_BAND;
case 1: return ColorInterpretation.SATURATION_BAND;
case 2: return ColorInterpretation.LIGHTNESS_BAND;
default: return ColorInterpretation.UNDEFINED;
}
}
case ColorSpace.TYPE_CMY:
case ColorSpace.TYPE_CMYK: {
switch (band) {
case 0: return ColorInterpretation.CYAN_BAND;
case 1: return ColorInterpretation.MAGENTA_BAND;
case 2: return ColorInterpretation.YELLOW_BAND;
case 3: return ColorInterpretation.BLACK_BAND;
default: return ColorInterpretation.UNDEFINED;
}
}
default: return ColorInterpretation.UNDEFINED;
}
}
}