/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2006-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.image.io; import java.awt.image.Raster; import org.apache.sis.math.MathFunctions; import org.apache.sis.util.Classes; /** * Converts samples from the values stored in the image file to the values stored in the * {@linkplain Raster raster}. Some typical conversions are: * <p> * <ul> * <li>Replace "<cite>nodata</cite>" values (typically a fixed value like 9999 or * {@value java.lang.Short#MAX_VALUE}) by {@link Float#NaN NaN} if the target type is * {@code float} or {@code double}, or 0 if the target type is an integer.</li> * <li>Replace <em>signed</em> integers by <em>unsigned</em> integers, by applying * an offset to the values.</li> * </ul> * <p> * Note that pad values are replaced by 0 in the integer case, not by an arbitrary number, * because 0 is the result of {@code (int) NaN} cast. While not mandatory, this property * make some mathematics faster during conversions between <cite>geophysics</cite> and * <cite>display</cite> views in the coverage module. * <p> * There is no scaling because this class is not for <cite>samples to geophysics values</cite> * conversions (except the replacement of pad values by {@link Double#NaN NaN}). This class is * about the minimal changes needed in order to comply to the constraints of a target * {@linkplain java.awt.image.ColorModel color model}, for example in order to workaround * the fact that {@link java.awt.image.IndexColorModel} does not accept negative numbers. * <p> * {@code SampleConverter}s are typically created and used by the {@link SpatialImageReader} * class and subclasses. They are created (directly or indirectly) by the * {@link SpatialImageReader#getImageType getImageType} method and used by the * {@link SpatialImageReader#read read} method. * * @author Martin Desruisseaux (Geomatys) * @version 3.07 * * @see SampleConversionType * * @since 2.4 * @module */ public abstract class SampleConverter { /** * A sample converter that do not performs any conversion. */ public static final SampleConverter IDENTITY = new Identity(); /** * Constructs a sample converter. */ protected SampleConverter() { } /** * Creates a sample converter that replaces a single pad value by {@link Double#NaN NaN} * for floating point numbers, or {@code 0} for integers. * * @param padValue The pad values to replace by {@link Double#NaN NaN} or {@code 0}. * @return The sample converter. */ public static SampleConverter createPadValueMask(final double padValue) { return Double.isNaN(padValue) ? IDENTITY : new PadValueMask(padValue); } /** * Creates a sample converter that replaces an arbitrary amount of pad values by * {@link Double#NaN NaN} for floating point numbers, or {@code 0} for integers. * * @param padValues The pad values to replace by {@link Double#NaN NaN} or {@code 0}, * or {@code null} if none. * @return The sample converter. */ public static SampleConverter createPadValuesMask(final double[] padValues) { if (padValues != null) { switch (padValues.length) { default: return new PadValuesMask(padValues); case 1: return createPadValueMask(padValues[0]); case 0: break; } } return IDENTITY; } /** * Creates a sample converter that replaces a pad value by {@link Double#NaN NaN} or * {@code 0}, and applies an offset on all other values. This is typically used in * order to shift a range of arbitrary (including negative) integer values to a range * of strictly positive values. The later is more manageable by * {@linkplain java.awt.image.IndexColorModel index color model}. * * @param offset An offset to add to the values to be read, before to store them in the raster. * This is used primarily for transforming <em>signed</em> short into <em>unsigned</em> * short. * @param padValue The pad value to replace. This the value before the offset is applied. * @return The sample converter. */ public static SampleConverter createOffset(final double offset, final double padValue) { return (offset == 0) ? createPadValueMask(padValue) : new Offset(offset, padValue); } /** * Creates a sample converter that replaces an arbitrary amount of pad values by * {@link Double#NaN NaN} or {@code 0}, and applies an offset on all other values. * * @param offset An offset to add to the values to be read, before to store them in the raster. * @param padValues The pad values to replace. They the values before the offset is applied. * @return The sample converter. */ public static SampleConverter createOffset(final double offset, final double[] padValues) { if (offset == 0) { return createPadValuesMask(padValues); } if (padValues != null) { switch (padValues.length) { default: return new MaskAndOffset(offset, padValues); case 1: return createOffset(offset, padValues[0]); case 0: break; } } return createOffset(offset, Double.NaN); } /** * Converts a double-precision value before to store it in the raster. * Subclasses should override this method if some fixed values need to * be converted into {@link Double#NaN} value. * * @param value The value read from the image file. * @return The value to store in the {@linkplain Raster raster}. */ public abstract double convert(double value); /** * Converts a float-precision value before to store it in the raster. * Subclasses should override this method if some fixed values need to * be converted into {@link Float#NaN} value. * * @param value The value read from the image file. * @return The value to store in the {@linkplain Raster raster}. */ public abstract float convert(float value); /** * Converts a float-precision value before to store it in the raster. * * @param value The value read from the image file. * @return The value to store in the {@linkplain Raster raster}. */ public abstract int convert(int value); /** * Converts in-place an array of double-precision values. * * @param values The values to convert. * @param offset Index of the first sample to convert. * @param length Number of samples to convert. * * @since 3.07 */ public void convert(final double[] values, int offset, int length) { length += offset; while (offset < length) { values[offset] = convert(values[offset]); offset++; } } /** * Converts in-place an array of single-precision values. * * @param values The values to convert. * @param offset Index of the first sample to convert. * @param length Number of samples to convert. * * @since 3.07 */ public void convert(final float[] values, int offset, int length) { length += offset; while (offset < length) { values[offset] = convert(values[offset]); offset++; } } /** * Converts in-place an array of integer values. * * @param values The values to convert. * @param offset Index of the first sample to convert. * @param length Number of samples to convert. * * @since 3.07 */ public void convert(final int[] values, int offset, int length) { length += offset; while (offset < length) { values[offset] = convert(values[offset]); offset++; } } /** * Converts in-place an array of short values. * * @param values The values to convert. * @param offset Index of the first sample to convert. * @param length Number of samples to convert. * * @since 3.07 */ public void convert(final short[] values, int offset, int length) { length += offset; while (offset < length) { values[offset] = (short) convert(values[offset]); offset++; } } /** * Converts in-place an array of unsigned short values. * * @param values The values to convert. * @param offset Index of the first sample to convert. * @param length Number of samples to convert. * * @since 3.07 */ public void convertUnsigned(final short[] values, int offset, int length) { length += offset; while (offset < length) { values[offset] = (short) convert(values[offset] & 0xFFFF); offset++; } } /** * Converts in-place an array of unsigned byte values. * * @param values The values to convert. * @param offset Index of the first sample to convert. * @param length Number of samples to convert. * * @since 3.07 */ public void convertUnsigned(final byte[] values, int offset, int length) { length += offset; while (offset < length) { values[offset] = (byte) convert(values[offset] & 0xFF); offset++; } } /** * If this converter applies an offset, returns the offset. Otherwise returns 0. * * @return The offset applied when converting sample values. */ public double getOffset() { return 0; } /** * Returns a string representation of this sample converter. * This is mostly for debugging purpose and may change in any future version. */ @Override public String toString() { return Classes.getShortClassName(this) + "[offset=" + getOffset() + ']'; } /** * A sample converter that does not perform any conversion. */ private static final class Identity extends SampleConverter { @Override public double convert(double value) {return value;} @Override public float convert(float value) {return value;} @Override public int convert(int value) {return value;} @Override public void convert(double[] values, int offset, int length) {} @Override public void convert(float[] values, int offset, int length) {} @Override public void convert(int[] values, int offset, int length) {} @Override public void convert(short[] values, int offset, int length) {} @Override public void convertUnsigned(short[] values, int offset, int length) {} @Override public void convertUnsigned(byte[] values, int offset, int length) {} } /** * A sample converter that replaces a single pad value by {@link Double#NaN NaN} * for floating point numbers, or {@code 0} for integers. */ private static class PadValueMask extends SampleConverter { final double doubleValue; final float floatValue; final int integerValue; PadValueMask(final double padValue) { doubleValue = padValue; floatValue = (float) padValue; final int p = (int) padValue; integerValue = (p == padValue) ? p : 0; } @Override public double convert(final double value) { return (value == doubleValue) ? Double.NaN : value; } @Override public float convert(final float value) { return (value == floatValue) ? Float.NaN : value; } @Override public int convert(final int value) { return (value == integerValue) ? 0 : value; } } /** * A sample converter that replaces a single pad value by 0, * and applies an offset on all other values. */ private static final class Offset extends PadValueMask { private final double doubleOffset; private final float floatOffset; private final int integerOffset; Offset(final double offset, final double padValue) { super(padValue); doubleOffset = offset; floatOffset = (float) offset; integerOffset = (int) Math.round(offset); } @Override public double convert(final double value) { return (value == doubleValue) ? Double.NaN : value + doubleOffset; } @Override public float convert(final float value) { return (value == floatValue) ? Float.NaN : value + floatOffset; } @Override public int convert(final int value) { return (value == integerValue) ? 0 : value + integerOffset; } @Override public double getOffset() { return doubleOffset; } } /** * A sample converter that replaces an arbitrary amount of pad values by * {@link Double#NaN NaN} for floating point numbers, or {@code 0} for integers. */ private static class PadValuesMask extends SampleConverter { private final double[] doubleValues; private final float [] floatValues; private final float [] NaNs; PadValuesMask(final double[] padValues) { doubleValues = new double[padValues.length]; floatValues = new float [padValues.length]; NaNs = new float [padValues.length]; for (int i=0; i<padValues.length; i++) { floatValues[i] = (float) (doubleValues[i] = padValues[i]); NaNs[i] = MathFunctions.toNanFloat(i); } } @Override public double convert(final double value) { for (int i=0; i<doubleValues.length; i++) { if (value == doubleValues[i]) { return NaNs[i]; } } return value; } @Override public float convert(final float value) { for (int i=0; i<floatValues.length; i++) { if (value == floatValues[i]) { return NaNs[i]; } } return value; } // Do not override: we really need the arithmetic on NaN values. @Override public final int convert(final int value) { return (int) convert((double) value); } } /** * A sample converter that replaces many pad values by 0, * and applies an offset on all other values. */ private static final class MaskAndOffset extends PadValuesMask { private final double doubleOffset; private final float floatOffset; MaskAndOffset(final double offset, final double[] padValues) { super(padValues); doubleOffset = offset; floatOffset = (float) offset; } @Override public double convert(final double value) { return super.convert(value) + doubleOffset; } @Override public float convert(final float value) { return super.convert(value) + floatOffset; } @Override public double getOffset() { return doubleOffset; } } }