/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008 - 2010, 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.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DirectColorModel; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import javax.media.jai.ImageLayout; import javax.media.jai.NullOpImage; import javax.media.jai.OpImage; import org.geotoolkit.filter.AbstractExpression; import org.geotoolkit.filter.DefaultLiteral; import org.geotoolkit.internal.coverage.CoverageUtilities; import org.geotoolkit.style.StyleConstants; 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 "Categorize" 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 color map * <li>Literal: lookup value * <li>Literal: value 0 * <li>Literal: threshold 1 * <li>Literal: value 1 * <li>Literal: threshold 2 * <li>Literal: value 2 * <li>Literal: (Optional) succeeding or preceding * </ol> * In reality any expression will do. * @author Jody Garnett * @author Johann Sorel (Geomatys) * @module */ public class DefaultCategorize extends AbstractExpression implements Categorize { private static final Object NEG_INF = StyleConstants.CATEGORIZE_LESS_INFINITY.getValue(); private static final Comparator<Expression> COMPARATOR = new Comparator<Expression>() { @Override public int compare(Expression exp1, Expression exp2) { if(exp1 instanceof Literal && ((Literal)exp1).getValue().equals(NEG_INF)){ //categorize less is always first return -1; }else if(exp2 instanceof Literal && ((Literal)exp2).getValue().equals(NEG_INF)){ //categorize less is always first return +1; }else{ final double d1 = exp1.evaluate(null, Double.class); final double d2 = exp2.evaluate(null, Double.class); //put NaN at the end if(Double.isNaN(d1)) return +1; if(Double.isNaN(d2)) return -1; final double diff = d1-d2; if(diff < 0){ return -1; }else if(diff > 0){ return +1; }else{ return 0; } } } }; private final Expression lookup; private final TreeMap<Expression,Expression> values = new TreeMap<>(COMPARATOR); private final ThreshholdsBelongTo belongTo; 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", "Value", "Threshold 1", "Value 1", "Threshold 2", "Value 2", "succeeding or preceding" }); } @Override public String getName() { return "Categorize"; } }; public DefaultCategorize(final Expression ... expressions){ lookup = expressions[0]; this.values.put(CATEGORIZE_LESS_INFINITY, expressions[1]); if(expressions.length%2 == 0){ for(int i=2;i<expressions.length;i+=2){ this.values.put(expressions[i], expressions[i+1]); } this.belongTo = ThreshholdsBelongTo.SUCCEEDING; }else{ for(int i=2;i<expressions.length-1;i+=2){ this.values.put(expressions[i], expressions[i+1]); } final ThreshholdsBelongTo to = ThreshholdsBelongTo.parse(expressions[expressions.length-1].evaluate(null, String.class)); this.belongTo = (to==null) ? ThreshholdsBelongTo.SUCCEEDING : to; } this.fallback = DEFAULT_FALLBACK; if(this.values.keySet().iterator().next() != CATEGORIZE_LESS_INFINITY){ throw new IllegalArgumentException("Values must hold at least one key : CATEGORIZE_LESS_INFINITY"); } } /** * * @param LookUpValue * @param values map with threadholds keys. * @param belongs * @param fallback */ public DefaultCategorize(final Expression LookUpValue, final Map<Expression,Expression> values, final ThreshholdsBelongTo belongs, final Literal fallback){ if(values == null || values.isEmpty()){ throw new IllegalArgumentException("Values can't be empty"); } this.lookup = (LookUpValue == null || LookUpValue == NIL) ? DEFAULT_CATEGORIZE_LOOKUP : LookUpValue; this.values.putAll(values); this.belongTo = (belongs == null) ? ThreshholdsBelongTo.SUCCEEDING :belongs; this.fallback = (fallback == null) ? DEFAULT_FALLBACK : fallback; if(this.values.keySet().iterator().next() != CATEGORIZE_LESS_INFINITY){ throw new IllegalArgumentException("Values must hold at least one key : CATEGORIZE_LESS_INFINITY"); } } /** * {@inheritDoc } */ @Override public Expression getLookupValue(){ return lookup; } /** * {@inheritDoc } */ @Override public Map<Expression,Expression> getThresholds() { return Collections.unmodifiableMap(values); } /** * {@inheritDoc } */ @Override public ThreshholdsBelongTo getBelongTo(){ return belongTo; } /** * {@inheritDoc } */ @Override public String getName() { return NAME.getName(); } /** * {@inheritDoc } */ @Override public List<Expression> getParameters() { final List<Expression> params = new ArrayList<Expression>(); params.add(lookup); int i=0; for(Entry<Expression,Expression> entry : values.entrySet()){ if(i==0){ params.add(entry.getValue()); }else{ params.add(entry.getKey()); params.add(entry.getValue()); } i++; } params.add(new DefaultLiteral(belongTo.name().toLowerCase())); return params; } /** * {@inheritDoc } */ @Override public Object accept(final ExpressionVisitor visitor, final Object extraData) { return visitor.visit(this, extraData); } /** * {@inheritDoc } */ @Override public Object evaluate(final Object object) { return evaluate(object, Object.class); } @Override public Object evaluate(final Object object, final Class c) { final Object candidate; final Double value; if(object instanceof Feature){ candidate = (Feature)object; value = lookup.evaluate(candidate,Double.class); final Expression exp = new DefaultLiteral<>(value); final boolean b = this.belongTo == ThreshholdsBelongTo.SUCCEEDING; final Expression closest = values.headMap(exp,!b).lastEntry().getValue(); return closest.evaluate(candidate,c); } else if (object instanceof RenderedImage) { return evaluateImage((RenderedImage) object); }else if(object instanceof Number){ candidate = null; value = ((Number)object).doubleValue(); }else if(fallback!=null){ return fallback.evaluate(object,c); }else{ return null; } final Expression exp = new DefaultLiteral<>(value); final boolean b = this.belongTo == ThreshholdsBelongTo.SUCCEEDING; final Expression closest = values.headMap(exp,!b).lastEntry().getValue(); return closest.evaluate(candidate,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; 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) { 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){ 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). */ final ImageLayout layout = new ImageLayout().setColorModel(model); return new NullOpImage(image, layout, null, OpImage.OP_COMPUTE_BOUND); } /** * * @param ARGB array of <code>int</code> * @return an array of <code>int</code> */ private int[] transformColormap(final int[] ARGB) { final Map<Expression,Expression> categorizes = getThresholds(); final List<Expression> keys = new ArrayList<>(categorizes.keySet()); final double[] SE_VALUES = new double[keys.size()]; final int[] SE_ARGB = new int[keys.size()]; final Set<Map.Entry<Expression,Expression>> entries = categorizes.entrySet(); int l=0; for(Map.Entry<Expression,Expression> entry : entries){ if(l==0){ SE_VALUES[0] = Double.NEGATIVE_INFINITY; SE_ARGB[0] = entry.getValue().evaluate(null, Color.class).getRGB(); }else{ // CATEGORIZE LESS INFINITY CASE try { SE_VALUES[l] = entry.getKey().evaluate(null, Double.class); } catch (Exception e) { SE_VALUES[l] = Double.NEGATIVE_INFINITY; } SE_ARGB[l] = entry.getValue().evaluate(null, Color.class).getRGB(); } l++; } int step = 0; for(int k=0;k<SE_VALUES.length-1;k++){ final double geoValue = SE_VALUES[k+1]; int color = SE_ARGB[k]; int sampleValue = (int)(Double.isNaN(geoValue)?Integer.MAX_VALUE:geoValue); for(int i=step ; (i<sampleValue && i<ARGB.length) ; i++){ ARGB[i] = color; } step = (int) sampleValue; if(step < 0) step = 0; //we are on the last element, fill the remaining cell with the color if(k == SE_VALUES.length-2){ color = SE_ARGB[k+1]; for(int i=step ; i<ARGB.length ; i++){ ARGB[i] = color; } } } return ARGB; } /** * {@inheritDoc } */ @Override public Literal getFallbackValue() { return fallback; } }