/* JAI-Ext - OpenSource Java Advanced Image Extensions Library
* http://www.geo-solutions.it/
* Copyright 2014 GeoSolutions
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.geosolutions.jaiext.classifier;
import it.geosolutions.jaiext.piecewise.DefaultPiecewiseTransform1DElement;
import java.awt.Color;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.util.Arrays;
/**
* Utility class for doing useful ColorMap operations
*
* @author Nicola Lagomarsini geosolutions
*
*/
public class ColorMapUtilities {
/**
* Small number for rounding errors.
*/
private static final double EPS = 1E-6;
/**
* Default ARGB code.
*/
static final int[] DEFAULT_ARGB = { 0xFF000000, 0xFFFFFFFF };
/**
* Makes sure that an argument is non-null.
*
* @param name Argument name.
* @param object User argument.
* @throws IllegalArgumentException if {@code object} is null.
*/
static void ensureNonNull(final String name, final Object object)
throws IllegalArgumentException {
if (object == null) {
throw new IllegalArgumentException("Argument: " + name + " is Null");
}
}
/**
* Compare two doubles taking into account NaN
*/
static int compare(final double v1, final double v2) {
if (Double.isNaN(v1) && Double.isNaN(v2)) {
final long bits1 = Double.doubleToRawLongBits(v1);
final long bits2 = Double.doubleToRawLongBits(v2);
if (bits1 < bits2)
return -1;
if (bits1 > bits2)
return +1;
}
return Double.compare(v1, v2);
}
/**
* Check that all the output values for the various {@link DefaultConstantPiecewiseTransformElement} are equal.
*
* @param preservingElements array of {@link DefaultConstantPiecewiseTransformElement}s.
* @return the array of {@link DefaultConstantPiecewiseTransformElement}s if the check is successful.
* @throws IllegalArgumentException in case the check is unsuccessful.
*/
static DefaultPiecewiseTransform1DElement[] checkPreservingElements(
LinearColorMapElement[] preservingElements) {
if (preservingElements != null) {
double outval = Double.NaN;
Color color = null;
for (int i = 0; i < preservingElements.length; i++) {
// the no data element must be a linear transform mapping to a single value
if (!(preservingElements[i] instanceof ConstantColorMapElement))
throw new IllegalArgumentException(
"The element must be a ConstantColorMapElement");
final ConstantColorMapElement nc = (ConstantColorMapElement) preservingElements[i];
if (nc.getColors().length != 1)
throw new IllegalArgumentException("Color size must be 1");
if (i == 0) {
outval = nc.getOutputMaximum();
color = nc.getColors()[0];
} else {
if (compare(outval, nc.getOutputMaximum()) != 0)
throw new IllegalArgumentException("Wrong Color value");
if (!color.equals(nc.getColors()[0]))
throw new IllegalArgumentException("Wrong Color value");
}
}
}
return preservingElements;
}
/**
* Copies {@code colors} into array {@code ARGB} from index {@code lower} inclusive to index {@code upper} exclusive. If {@code upper-lower} is
* not equals to the length of {@code colors} array, then colors will be interpolated.
* <p>
* <b>Note:</b> Profiling shows that this method is a "hot spot". It needs to be fast, which is why the implementation is not as straight-forward
* as it could.
*
* @param colors Colors to copy into the {@code ARGB} array.
* @param ARGB Array of integer to write ARGB values to.
* @param lower Index (inclusive) of the first element of {@code ARGB} to change.
* @param upper Index (exclusive) of the last element of {@code ARGB} to change.
*/
@SuppressWarnings("fallthrough")
public static void expand(final Color[] colors, final int[] ARGB, final int lower,
final int upper) {
/*
* Trivial cases.
*/
switch (colors.length) {
case 1:
Arrays.fill(ARGB, lower, upper, colors[0].getRGB()); // fall through
case 0:
return; // Note: getRGB() is really getARGB()
}
switch (upper - lower) {
case 1:
ARGB[lower] = colors[0].getRGB(); // fall through
case 0:
return; // Note: getRGB() is really getARGB()
}
/*
* Prepares the coefficients for the iteration. The non-final ones will be updated inside the loop.
*/
final double scale = (double) (colors.length - 1) / (double) (upper - 1 - lower);
final int maxBase = colors.length - 2;
double index = 0;
int base = 0;
for (int i = lower;;) {
final int C0 = colors[base + 0].getRGB();
final int C1 = colors[base + 1].getRGB();
final int A0 = (C0 >>> 24) & 0xFF, A1 = ((C1 >>> 24) & 0xFF) - A0;
final int R0 = (C0 >>> 16) & 0xFF, R1 = ((C1 >>> 16) & 0xFF) - R0;
final int G0 = (C0 >>> 8) & 0xFF, G1 = ((C1 >>> 8) & 0xFF) - G0;
final int B0 = (C0) & 0xFF, B1 = ((C1) & 0xFF) - B0;
final int oldBase = base;
do {
final double delta = index - base;
ARGB[i] = (roundByte(A0 + delta * A1) << 24) | (roundByte(R0 + delta * R1) << 16)
| (roundByte(G0 + delta * G1) << 8) | (roundByte(B0 + delta * B1));
if (++i == upper) {
return;
}
index = (i - lower) * scale;
base = Math.min(maxBase, (int) (index + EPS)); // Really want rounding toward 0.
} while (base == oldBase);
}
}
/**
* Rounds a float value and clamp the result between 0 and 255 inclusive.
*/
public static int roundByte(final double value) {
return (int) Math.min(Math.max(Math.round(value), 0), 255);
}
/**
* Returns a bit count for an {@link IndexColorModel} mapping {@code mapSize} colors. It is guaranteed that the following relation is hold:
*
* <center>
*
* <pre>
* (1 << getBitCount(mapSize)) >= mapSize
* </pre>
*
* </center>
*/
public static int getBitCount(final int mapSize) {
int max = mapSize - 1;
if (max <= 1) {
return 1;
}
int count = 0;
do {
count++;
max >>= 1;
} while (max != 0);
assert (1 << count) >= mapSize : mapSize;
assert (1 << (count - 1)) < mapSize : mapSize;
return count;
}
/**
* Returns a suggested type for an {@link IndexColorModel} of {@code mapSize} colors. This method returns {@link DataBuffer#TYPE_BYTE} or
* {@link DataBuffer#TYPE_USHORT}.
*/
public static int getTransferType(final int mapSize) {
return (mapSize <= 256) ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT;
}
}