/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008 - 2009, 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.style.function;
import java.awt.Color;
import java.awt.image.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.geotoolkit.filter.AbstractExpression;
import org.geotoolkit.filter.DefaultLiteral;
import org.geotoolkit.image.RecolorRenderedImage;
import org.geotoolkit.internal.coverage.CoverageUtilities;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.geotoolkit.image.color.ColorUtilities;
import org.opengis.filter.capability.FunctionName;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.ExpressionVisitor;
import org.opengis.filter.expression.Literal;
import static org.geotoolkit.style.StyleConstants.*;
import org.opengis.feature.Feature;
import static org.opengis.filter.expression.Expression.*;
/**
*
* Implementation of "Interpolation" as a normal function.
* <p>
* This implementation is compatible with the Function
* interface; the parameter list can be used to set the
* threshold values etc...
* <p>
*
* This function expects:
* <ol>
* <li>PropertyName; use "Rasterdata" to indicate this is a colour map
* <li>Literal: lookup value
* <li>Literal: InterpolationPoint : data 1
* <li>Literal: InterpolationPoint : value 1
* <li>Literal: InterpolationPoint : data 2
* <li>Literal: InterpolationPoint : value 2
* <li>Literal: Mode
* <li>Literal: Method
* </ol>
* In reality any expression will do.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class DefaultInterpolate extends AbstractExpression implements Interpolate {
private final Expression lookup;
private final InterpolationPoint[] points;
private final Method method;
private final Mode mode;
private final Literal fallback;
/**
* Make the instance of FunctionName available in
* a consistent spot.
*/
public static final FunctionName NAME = new Name();
/**
* Describe how this function works.
* (should be available via FactoryFinder lookup...)
*/
public static class Name implements FunctionName {
@Override
public int getArgumentCount() {
return -2; // indicating unbounded, 2 minimum
}
@Override
public List<String> getArgumentNames() {
return Arrays.asList(new String[]{
"LookupValue",
"Data 1", "Value 1",
"Data 2", "Value 2",
"linear, cosine or cubic",
"numeric or color"
});
}
@Override
public String getName() {
return "Interpolate";
}
};
public DefaultInterpolate(final Expression ... expressions){
lookup = expressions[0];
final List<InterpolationPoint> points = new ArrayList<InterpolationPoint>();
for(int i=1;i<expressions.length-2;i+=2){
final InterpolationPoint ip = new DefaultInterpolationPoint(
expressions[i].evaluate(null, Number.class), expressions[i+1]);
points.add(ip);
}
this.points = points.toArray(new InterpolationPoint[points.size()]);
final Method me = Method.parse(expressions[expressions.length-2].evaluate(null, String.class));
final Mode mo = Mode.parse(expressions[expressions.length-1].evaluate(null, String.class));
this.method = (me==null) ? Method.COLOR : me;
this.mode = (mo == null) ? Mode.LINEAR : mo;
this.fallback = DEFAULT_FALLBACK;
}
public DefaultInterpolate(final Expression LookUpValue, List<InterpolationPoint> values,
final Method method, final Mode mode,final Literal fallback){
if(values == null ){
values = Collections.emptyList();
}
this.lookup = (LookUpValue == null || LookUpValue == NIL) ? DEFAULT_CATEGORIZE_LOOKUP : LookUpValue;
this.points = values.toArray(new InterpolationPoint[values.size()]);
Arrays.sort(points, new Comparator<InterpolationPoint>(){
@Override
public int compare(InterpolationPoint t1, InterpolationPoint t2) {
final Number v1 = t1.getData();
final Number v2 = t2.getData();
if(v1 instanceof Float && Float.isNaN(v1.floatValue())){
return -1;
}else if(v1 instanceof Double && Double.isNaN(v1.doubleValue())){
return -1;
}else if(v2 instanceof Float && Float.isNaN(v2.floatValue())){
return +1;
}else if(v2 instanceof Double && Double.isNaN(v2.doubleValue())){
return +1;
}
final double diff = v1.doubleValue() - v2.doubleValue();
if(diff < 0){
return -1;
}else if(diff > 0){
return +1;
}else{
return 0;
}
}
});
this.method = (method == null) ? Method.COLOR : method;
this.mode = (mode == null) ? Mode.LINEAR : mode;
this.fallback = (fallback == null) ? DEFAULT_FALLBACK : fallback;
}
@Override
public String getName() {
return "Interpolate";
}
@Override
public List<Expression> getParameters() {
final List<Expression> params = new ArrayList<Expression>();
params.add(lookup);
for(InterpolationPoint ip : points){
params.add(new DefaultLiteral(ip.getData()));
params.add(ip.getValue());
}
params.add(new DefaultLiteral(method.name().toLowerCase()));
params.add(new DefaultLiteral(mode.name().toLowerCase()));
return params;
}
@Override
public Object accept(final ExpressionVisitor visitor, final Object extraData) {
return visitor.visit(this, extraData);
}
@Override
public Object evaluate(final Object object) {
if (object instanceof RenderedImage) {
return evaluateImage((RenderedImage) object);
}
return evaluate(object, Object.class);
}
@Override
public Object evaluate(final Object object, final Class c) {
final Number value;
if(object instanceof Feature){
value = lookup.evaluate(object,Number.class);
}else if(object instanceof Number){
value = (Number)object;
}else{
return fallback.evaluate(object,c);
}
final double dval = value.doubleValue();
InterpolationPoint before = null;
InterpolationPoint after = null;
for(InterpolationPoint ip : points){
final Number ipnum = ip.getData();
final double ipdnum = ipnum.doubleValue();
final double ipval;
if(Double.isNaN(ipdnum)){
ipval = ipdnum;
//if we want exact NaN match use doubleToRawLongBits
if(Double.doubleToLongBits(ipval) == Double.doubleToLongBits(dval)){
before = ip;
break;
}else{
continue;
}
}else{
ipval = ipnum.doubleValue();
}
if(ipval < dval){
before = ip;
}else if(ipval > dval){
after = ip;
break;
}else{
//exact match
return ip.getValue().evaluate(object,c);
}
}
if(before == null && after == null){
//no value associated, surely an NaN value
//return a translucent color
return ObjectConverters.convert( new Color(0,0,0,0) , c);
}else if(before == null){
//only have an over value
return after.getValue().evaluate(object,c);
}else if(after == null){
//only have an under value
return before.getValue().evaluate(object,c);
}else{
//must interpolate
final double d1 = before.getData().doubleValue();
final double d2 = after.getData().doubleValue();
final double pourcent = (dval - d1)/ (d2 - d1);
final Object o1 = before.getValue().evaluate(object,c);
final Object o2 = after.getValue().evaluate(object,c);
if(o1 instanceof Color && o2 instanceof Color){
//datas are not numbers, looks like we deal with colors
final Color c1 = before.getValue().evaluate(object,Color.class);
final Color c2 = after.getValue().evaluate(object,Color.class);
final Color in = interpolate(c1, c2, pourcent);
return ObjectConverters.convert( in , c);
}else{
final Double n1 = before.getValue().evaluate(object,Double.class);
final Double n2 = after.getValue().evaluate(object,Double.class);
return ObjectConverters.convert( (n1 + pourcent*(n2-n1)) , c);
}
}
}
/**
* Recolor image
* @param image
* @return recolored image
*/
private RenderedImage evaluateImage (final RenderedImage image) {
final int visibleBand = CoverageUtilities.getVisibleBand(image);
final ColorModel candidate = image.getColorModel();
//TODO : this should be used when the index color model can not handle signed values
//
//final SampleModel sm = image.getSampleModel();
//final int datatype = sm.getDataType();
//if(datatype == DataBuffer.TYPE_SHORT){
// final ColorModel model = new CompatibleColorModel(16, function);
// final ImageLayout layout = new ImageLayout().setColorModel(model);
// return new NullOpImage(image, layout, null, OpImage.OP_COMPUTE_BOUND);
//}
/*
* Extracts the ARGB codes from the ColorModel and invokes the
* transformColormap(...) method.
*/
final int[] ARGB;
final ColorModel model;
// As index color model cannot manage negative values, we must use our own in this case.
if (points[0].getData().doubleValue() < 0 || candidate==null) {
final int pixelSize;
if(candidate!=null){
pixelSize = candidate.getPixelSize();
}else{
pixelSize = 16;
}
model = new CompatibleColorModel(pixelSize, this);
} else if (candidate instanceof IndexColorModel) {
final IndexColorModel colors = (IndexColorModel) candidate;
final int mapSize = colors.getMapSize();
ARGB = new int[mapSize];
colors.getRGBs(ARGB);
transformColormap(ARGB);
model = ColorUtilities.getIndexColorModel(ARGB, 1, visibleBand, -1);
} else if (candidate instanceof ComponentColorModel) {
final ComponentColorModel colors = (ComponentColorModel) candidate;
final int nbbit = colors.getPixelSize();
final int type = image.getSampleModel().getDataType();
if ((type == DataBuffer.TYPE_BYTE || type == DataBuffer.TYPE_USHORT) && nbbit <= 16) {
final int mapSize = 1 << nbbit;
ARGB = new int[mapSize];
for (int j = 0; j < mapSize; j++) {
int v = j * 255 / mapSize;
int a = 255 << 24;
int r = v << 16;
int g = v << 8;
int b = v << 0;
ARGB[j] = a | r | g | b;
}
transformColormap(ARGB);
model = ColorUtilities.getIndexColorModel(ARGB, 1, visibleBand, -1);
} else {
//we can't handle a index color model when values exceed int max value
model = new CompatibleColorModel(nbbit, this);
}
} else if (candidate instanceof DirectColorModel) {
final DirectColorModel colors = (DirectColorModel) candidate;
final int nbbit = colors.getPixelSize();
final int type = image.getSampleModel().getDataType();
if ((type == DataBuffer.TYPE_BYTE || type == DataBuffer.TYPE_USHORT) && nbbit <= 16) {
final int mapSize = 1 << nbbit;
ARGB = new int[mapSize];
for (int j = 0; j < mapSize; j++) {
int v = j * 255 / mapSize;
int a = 255 << 24;
int r = v << 16;
int g = v << 8;
int b = v << 0;
ARGB[j] = a | r | g | b;
}
transformColormap(ARGB);
model = ColorUtilities.getIndexColorModel(ARGB, 1, visibleBand, -1);
} else {
//we can't handle a index color model when values exceed int max value
model = new CompatibleColorModel(nbbit, this);
}
} else {
model = new CompatibleColorModel(candidate.getPixelSize(), this);
}
/*
* Gives the color model to the image layout and creates a new image using the Null
* operation, which merely propagates its first source along the operation chain
* unmodified (except for the ColorModel given in the layout in this case).
*/
return new RecolorRenderedImage(image, model);
}
private int[] transformColormap(final int[] ARGB) {
final List<InterpolationPoint> points = getInterpolationPoints();
final double[] SE_VALUES = new double[points.size()];
final int[] SE_ARGB = new int[points.size()];
for (int i = 0, n = points.size(); i < n; i++) {
final InterpolationPoint point = points.get(i);
SE_VALUES[i] = point.getData().doubleValue();
final Color evaluate = point.getValue().evaluate(null, Color.class);
SE_ARGB[i] = (evaluate != null) ? evaluate.getRGB() : 0;
}
int lastStep = -1;
int lastColor = -1;
for (int k = 0; k < SE_VALUES.length; k++) {
final double geoValue = SE_VALUES[k];
final int currentColor = SE_ARGB[k];
final int currentStep = (int) geoValue;
//first element, dont interpolate colors
if (k == 0) {
lastColor = currentColor;
lastStep = -1;
}
final int stepInterval = currentStep - lastStep;
final int lastAlpha = (lastColor >>> 24) & 0xFF;
final int lastRed = (lastColor >>> 16) & 0xFF;
final int lastGreen = (lastColor >>> 8) & 0xFF;
final int lastBlue = (lastColor >>> 0) & 0xFF;
final int alphaInterval = ((currentColor >>> 24) & 0xFF) - lastAlpha;
final int redInterval = ((currentColor >>> 16) & 0xFF) - lastRed;
final int greenInterval = ((currentColor >>> 8) & 0xFF) - lastGreen;
final int blueInterval = ((currentColor >>> 0) & 0xFF) - lastBlue;
for (int i = lastStep + 1; (i <= currentStep && i < ARGB.length); i++) {
//calculate interpolated color
final int relativePosition = i - lastStep;
final double pourcent = (double) ((double) relativePosition / (double) stepInterval);
int a = lastAlpha + (int) (pourcent * alphaInterval);
int r = lastRed + (int) (pourcent * redInterval);
int g = lastGreen + (int) (pourcent * greenInterval);
int b = lastBlue + (int) (pourcent * blueInterval);
a <<= 24;
r <<= 16;
g <<= 8;
b <<= 0;
ARGB[i] = a | r | g | b;
}
lastStep = (int) currentStep;
lastColor = currentColor;
//last element, fill the remaining cell with the color
if (k == SE_VALUES.length - 1) {
for (int i = lastStep; i < ARGB.length; i++) {
ARGB[i] = currentColor;
}
}
}
return ARGB;
}
@Override
public Literal getFallbackValue() {
return fallback;
}
@Override
public Expression getLookupValue() {
return lookup;
}
@Override
public List<InterpolationPoint> getInterpolationPoints() {
return UnmodifiableArrayList.wrap(points);
}
@Override
public Mode getMode() {
return mode;
}
@Override
public Method getMethod() {
return method;
}
public static Color interpolate(Color c1, Color c2, double pourcent){
final int argb1 = c1.getRGB();
final int argb2 = c2.getRGB();
final int lastAlpha = (argb1>>>24) & 0xFF;
final int lastRed = (argb1>>>16) & 0xFF;
final int lastGreen = (argb1>>> 8) & 0xFF;
final int lastBlue = (argb1>>> 0) & 0xFF;
final int alphaInterval = ((argb2>>>24) & 0xFF) - lastAlpha;
final int redInterval = ((argb2>>>16) & 0xFF) - lastRed;
final int greenInterval = ((argb2>>> 8) & 0xFF) - lastGreen;
final int blueInterval = ((argb2>>> 0) & 0xFF) - lastBlue;
//calculate interpolated color
int a = lastAlpha + (int)(pourcent*alphaInterval);
int r = lastRed + (int)(pourcent*redInterval);
int g = lastGreen + (int)(pourcent*greenInterval);
int b = lastBlue + (int)(pourcent*blueInterval);
return new Color(r, g, b, a) ;
}
}