package edu.oregonstate.cartography.grid;
import edu.oregonstate.cartography.gui.bivariate.BivariateColorPoint;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.ArrayList;
import edu.oregonstate.cartography.gui.bivariate.ColorLUTInterface;
import ij.process.ColorSpaceConverter;
/**
* Renderer using a two-dimensional color look-up table.
*
* @author Bernhard Jenny, School of Science - Geospatial Science, RMIT
* University, Melbourne
*/
public final class ColorLUT implements ColorLUTInterface {
/**
* size of look-up table
*/
public static final int LUT_SIZE = 256;
/**
* color reference points
*/
private final ArrayList<BivariateColorPoint> points = new ArrayList<>();
/**
* exponent value for interpolation
*/
private double exponentP = 2;
/**
* Use inverse distance weighting or Gaussian bell curve weighting
*/
private boolean useIDW = false;
/**
* cached look-up values
*/
private int[][] lut = null;
public ColorLUT() {
initPoints();
}
/**
* Assign default color reference points.
*/
protected final void initPoints() {
addPoint(231, 252, 253, 0.5, 0.0);
addPoint(226, 254, 255, 1.0, 0.0);
addPoint(252, 252, 252, 1.0, 0.35);
addPoint(247, 255, 233, 1.0, 0.65);
addPoint(255, 255, 192, 0.95, 0.8);
addPoint(255, 255, 192, 0.9, 0.9);
addPoint(196, 207, 255, 0.7, 0.6);
addPoint(056, 125, 254, 0.7, 0.9);
}
private void addPoint(int r, int g, int b, double x, double y) {
BivariateColorPoint p = new BivariateColorPoint();
p.setColor(r, g, b);
p.setAttribute1(x);
p.setAttribute2(y);
addPoint(p);
}
/**
* Returns a color for a gray scale shaded value and an elevation.
*
* @param shade gray value between 0 and 1. An index along the horizontal
* axis of the look-up table.
* @param relativeElevation relative elevation between 0 and 1. An index
* along the vertical axis of the look-up table.
* @return the RGB color
*/
public final int getColor(double shade, double relativeElevation) {
assert (shade >= 0d && shade <= 1d);
assert (relativeElevation >= 0d && relativeElevation <= 1d);
int lutCol = (int) Math.round((ColorLUT.LUT_SIZE - 1) * shade);
int lutRow = (int) Math.round((ColorLUT.LUT_SIZE - 1) * relativeElevation);
return lut[lutRow][lutCol];
}
/**
* Renders an image with all possible colors. The image can be used to
* display the color look-up table.
*
* @param width Width of the image
* @param height Height of the image
* @return The new image.
*/
@Override
public BufferedImage getDiagramImage(int width, int height) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
int[] imageBuffer = ((DataBufferInt) (img.getRaster().getDataBuffer())).getData();
for (int r = 0; r < height; r++) {
for (int c = 0; c < width; c++) {
double x = c / (width - 1d);
double y = 1d - r / (height - 1d);
int lutCol = (int) Math.round(x * (ColorLUT.LUT_SIZE - 1));
int lutRow = (int) Math.round(y * (ColorLUT.LUT_SIZE - 1));
imageBuffer[r * width + c] = lut[lutRow][lutCol];
}
}
return img;
}
/**
* Updates the cached values of the color look-up table. Needs to be called
* after a point or any attribute that changes the colors in the look-up
* table has been altered.
*/
@Override
public void colorPointsChanged() {
lut = new int[LUT_SIZE][LUT_SIZE];
for (int r = 0; r < LUT_SIZE; r++) {
double y = r / (LUT_SIZE - 1d);
for (int c = 0; c < LUT_SIZE; c++) {
double x = c / (LUT_SIZE - 1d);
lut[r][c] = interpolateColor(x, y);
}
}
}
/**
* Interpolates a color for a particular location in the look-up table from
* all color reference points.
*
* @param h the horizontal coordinate of the location in the look-up table
* between 0 and 1.
* @param v the vertical coordinate of the location in the look-up table
* between 0 and 1.
* @return
*/
@Override
public int interpolateColor(double h, double v) {
final boolean useLAB = true;
ColorSpaceConverter colorConverter = new ColorSpaceConverter();
double wTot = 0;
double weightedSumR = 0;
double weightedSumG = 0;
double weightedSumB = 0;
double weightedSumLabL = 0;
double weightedSumLabA = 0;
double weightedSumLabB = 0;
for (BivariateColorPoint point : points) {
double attr1Point = point.getAttribute1();
double attr2Point = point.getAttribute2();
// distance in the color look-up table.
double d1 = attr1Point - h;
double d2 = attr2Point - v;
double d = Math.sqrt(d1 * d1 + d2 * d2);
// IDW or Gaussian weigh
double w = useIDW ? inverseDistanceWeight(d) : gaussianWeight(d);
if (useLAB) {
// Lab mix
// mixing with Lab tends to create slightly more saturated colors than with RGB
weightedSumLabL += point.getLabL() * w;
weightedSumLabA += point.getLabA() * w;
weightedSumLabB += point.getLabB() * w;
} else {
// RGB mix
weightedSumR += point.getR() * w;
weightedSumG += point.getG() * w;
weightedSumB += point.getB() * w;
}
wTot += w;
}
if (useLAB) {
double L = Math.min(ColorSpaceConverter.LAB_MAX_L, Math.max(ColorSpaceConverter.LAB_MIN_L, weightedSumLabL / wTot));
double a = Math.min(ColorSpaceConverter.LAB_MAX_A, Math.max(ColorSpaceConverter.LAB_MIN_A, weightedSumLabA / wTot));
double b = Math.min(ColorSpaceConverter.LAB_MAX_B, Math.max(ColorSpaceConverter.LAB_MIN_B, weightedSumLabB / wTot));
int[] rgb = colorConverter.LABtoRGB(L, a, b);
return rgb[2] | (rgb[1] << 8) | (rgb[0] << 16) | 0xFF000000;
} else {
int r = (int) Math.min(255, Math.max(0, weightedSumR / wTot));
int g = (int) Math.min(255, Math.max(0, weightedSumG / wTot));
int b = (int) Math.min(255, Math.max(0, weightedSumB / wTot));
return b | (g << 8) | (r << 16) | 0xFF000000;
}
}
/**
* Returns the color reference points.
*
* @return the points
*/
@Override
public ArrayList<BivariateColorPoint> getPoints() {
return points;
}
/**
* Add a color reference point.
*
* @param p color reference point to add.
*/
@Override
public void addPoint(BivariateColorPoint p) {
points.add(p);
colorPointsChanged();
}
/**
* Remove a point from the color reference points. Will not remove the point
* if it is the only point.
*
* @param point point to remove.
*/
@Override
public void removePoint(BivariateColorPoint point) {
if (points.size() > 1) {
points.remove(point);
colorPointsChanged();
}
}
/**
* Gaussian bell curve.
*
* @param d horizontal value for which a vertical Gaussian bell curve value
* is computed.
* @return the Gaussian bell curve value
*/
private double gaussianWeight(double d) {
// empirical scaling of exponent
// this makes it possible to use the same exponentP for IDW and Gaussian curve weighting
double K = exponentP / 10000 * LUT_SIZE * LUT_SIZE;
return Math.exp(-K * d * d);
}
/**
* Inverse distance weight after Shepard.
*
* @param d distance for which a weight is computed.
* @return the weight for d
*/
private double inverseDistanceWeight(double d) {
return 1. / Math.pow(d, exponentP);
}
/**
* Returns the exponent for weight calculations.
*
* @return the exponent
*/
@Override
public double getExponentP() {
return exponentP;
}
/**
* Set the exponent for weight calculations.
*
* @param exponentP the exponent
*/
@Override
public void setExponentP(double exponentP) {
this.exponentP = exponentP;
colorPointsChanged();
}
/**
* Returns true if inverse distance weighting is used. Returns true if a
* Gaussian bell curve is used for weighting.
*
* @return true=IDW, false=Gauss
*/
@Override
public boolean isUseIDW() {
return useIDW;
}
/**
* Set whether inverse distance weighting or a Gaussian bell curve is used
* for weighting.
*
* @param useIDW true=IDW, false=Gauss
*/
@Override
public void setUseIDW(boolean useIDW) {
this.useIDW = useIDW;
colorPointsChanged();
}
/**
* Returns a warning string that can be displayed to the user.
*
* @return a string warning the user about some illegal condition.
*/
@Override
public String getWarning() {
return null;
}
}