/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 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.style.function;
import java.awt.Color;
import java.awt.image.*;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.filter.AbstractExpression;
import org.geotoolkit.filter.DefaultLiteral;
import org.geotoolkit.image.classification.Classification;
import static org.geotoolkit.style.StyleConstants.*;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.ExpressionVisitor;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import javax.media.jai.ImageLayout;
import javax.media.jai.NullOpImage;
import javax.media.jai.OpImage;
import org.geotoolkit.image.palette.Palette;
import org.geotoolkit.image.palette.PaletteFactory;
/**
* Jenks ColorMap function.
* Analyse input RenderedImage, compute jenks classes and recolor output image using defined palette.
*
* @author Quentin Boileau (Geomatys).
*/
public class DefaultJenks extends AbstractExpression implements Jenks {
private static final PaletteFactory PALETTE_FACTORY = PaletteFactory.getDefault();
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.style.function");
private Literal classNumber;
private Literal paletteName;
private Literal fallback;
private double[] noData;
private TreeMap<Double, Color> colorMap;
public DefaultJenks() {
}
public DefaultJenks(final Literal classNumber, final Literal paletteName, final Literal fallback, List<Literal> noDataLiteral) {
this.classNumber = (classNumber == null) ? new DefaultLiteral(10) : classNumber;
this.paletteName = (paletteName == null) ? new DefaultLiteral("rainbow") : paletteName;
this.fallback = (fallback == null) ? DEFAULT_FALLBACK : fallback;
colorMap = new TreeMap<Double, Color>();
if (noDataLiteral == null || noDataLiteral.isEmpty()) {
noData = new double[] {Double.NaN};
} else {
noData = new double[noDataLiteral.size()];
for (int i = 0; i < noDataLiteral.size(); i++) {
noData[i] = (Double) noDataLiteral.get(i).getValue();
}
Arrays.sort(noData);
}
}
@Override
public Literal getClassNumber() {
return classNumber;
}
@Override
public Literal getPalette() {
return paletteName;
}
@Override
public double[] getNoData() {
return noData;
}
@Override
public String getName() {
return "Jenks";
}
@Override
public List<Expression> getParameters() {
final List<Expression> params = new ArrayList<Expression>();
params.add(classNumber);
params.add(paletteName);
for (int i = 0; i < noData.length; i++) {
params.add(new DefaultLiteral(noData[i]));
}
return params;
}
@Override
public Literal getFallbackValue() {
return fallback;
}
@Override
public Object evaluate(Object object) {
return evaluate(object, Object.class);
}
@Override
public Object evaluate(Object object, Class context) {
if (object instanceof RenderedImage) {
final RenderedImage image = (RenderedImage) object;
final int dataType = image.getSampleModel().getDataType();
final Raster data = image.getData();
int classes = (Integer) this.classNumber.getValue();
final int numBands = data.getNumBands();
final int width = data.getWidth();
final int height = data.getHeight();
final Set<Double> values = new TreeSet<Double>();
double[] pixel = new double[numBands];
Double key = Double.NaN;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
data.getPixel(x, y, pixel);
//arbitrary only get the value ofthe first band
//TODO add bandIndex input parameter in Jenks function
key = Double.valueOf(pixel[0]);
//bypass noData values
if (Arrays.binarySearch(noData, key) < 0 && !values.contains(key)) {
values.add(key);
}
}
}
//prevent classification errors if requested classes is superior to computable classe number.
final int computableClasses = values.size();
if (classes > computableClasses) {
classes = computableClasses;
LOGGER.log(Level.WARNING, "Not enough distinct data to compute the requested number of class. Jenks will be computed for {0} classes.", classes);
}
final double[] pixelValues = new double[values.size()];
int index = 0;
for (Double val : values) {
pixelValues[index] = val.doubleValue();
index++;
}
//compute classes
final Classification classification = new Classification();
classification.setData(pixelValues);
classification.setClassNumber(classes);
classification.computeJenks(false);
final int[] indexes = classification.getIndex();
//create palette
final List<Color> colors = new ArrayList<Color>();
try {
final Palette palette = PALETTE_FACTORY.getPalette((String)paletteName.getValue(), classes);
final IndexColorModel icm = (IndexColorModel) palette.getColorModel();
for (int i=0; i<classes; i++) {
colors.add(new Color(icm.getRGB(i)));
}
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Palette not found.", ex);
}
colorMap.clear();
colorMap.put(Double.NEGATIVE_INFINITY, new Color(0, 0, 0, 0));
for (int i = 0; i < indexes.length; i++) {
colorMap.put(pixelValues[indexes[i]-1], colors.get(i));
}
for (int i = 0; i < noData.length; i++) {
colorMap.put(noData[i], new Color(0, 0, 0, 0));
}
/*
* HACK byte -> no-data = 255 else no-data = Double.NaN
* TODO find more elegant way to support no-data values.
*/
if (dataType == DataBuffer.TYPE_BYTE) {
colorMap.put(255.0, new Color(0, 0, 0, 0));
}
final ColorModel originColorModel = image.getColorModel();
final ColorModel newColorModel = new CompatibleColorModel(originColorModel.getPixelSize(), new JenksCategorize(colorMap));
/*
* 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).
*/
final ImageLayout layout = new ImageLayout().setColorModel(newColorModel);
return new NullOpImage(image, layout, null, OpImage.OP_COMPUTE_BOUND);
}
return null;
}
@Override
public Object accept(ExpressionVisitor visitor, Object extraData) {
return visitor.visit(this, extraData);
}
@Override
public Map<Double, Color> getColorMap() {
return colorMap;
}
/**
* Internal function used in CompatibleColorModel to recolor input image.
*/
private class JenksCategorize implements Function {
private TreeMap<Double,Color> values = new TreeMap<Double,Color>();
private JenksCategorize(TreeMap<Double, Color> values) {
this.values.putAll(values);//copy
}
@Override
public String getName() {
return "JenksNumber";
}
@Override
public List<Expression> getParameters() {
return null;
}
@Override
public Literal getFallbackValue() {
return DEFAULT_FALLBACK;
}
@Override
public Object evaluate(Object object) {
return evaluate(object, Object.class);
}
@Override
public Object evaluate(final Object object, final Class c) {
if (object instanceof Number) {
Number value = (Number) object;
return values.headMap(value.doubleValue(),false).lastEntry().getValue();
}
return null;
}
@Override
public Object accept(ExpressionVisitor visitor, Object extraData) {
return visitor.visit(this, extraData);
}
}
}