/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2017, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.mbstyle.function; import java.awt.Color; import java.util.ArrayList; import java.util.List; import org.geotools.data.Parameter; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.FunctionImpl; import org.geotools.filter.capability.FunctionNameImpl; import org.geotools.text.Text; import org.geotools.util.Converters; import org.opengis.filter.FilterFactory2; import org.opengis.filter.capability.FunctionName; import org.opengis.filter.expression.Expression; /** * Generate an output by interpolating between stops just less than and just * greater than the function input. The domain must be numeric. * * <h2>Parameters:</h2> * * <ol start="0"> * <li>The interpolation input</li> * <li>The base of the interpolation</li> * <li>(...n) The remaining args are interpreted as pairs of stop values (input, output) for the interpolation. There must be an even number.</li> * </ol> * * * @author Jody Garnett (Boundless) */ public class ExponentialFunction extends FunctionImpl { private static final FilterFactory2 ff2 = CommonFactoryFinder.getFilterFactory2(null); public static final FunctionName NAME; static { Parameter<Object> result = new Parameter<Object>("result",Object.class,1,1); Parameter<Object> input = new Parameter<Object>("input",Object.class,1,1); Parameter<Double> base = new Parameter<Double>( "base", Double.class, Text.text("Base"), Text.text("Exponential base of the interpolation curve controlling rate at which function output increases."), true,0,1, 1.0, null ); Parameter<Object> stops= new Parameter<Object>("stops",Object.class,4,-1); NAME = new FunctionNameImpl("Exponential", result, input, base, stops); } private static class Stop { Expression stop; Expression value; public Stop(Expression stop, Expression value ){ this.stop = stop; this.value = value; } @Override public String toString() { return "Stop "+stop+": "+value; } } public ExponentialFunction() { this.functionName = NAME; } @Override public <T> T evaluate(Object object, Class<T> context) { List<Expression> parameters = getParameters(); List<Stop> stops = new ArrayList<>(); Expression input = parameters.get(0); Expression base = parameters.get(1); if (parameters.size() % 2 != 0) { throw new IllegalArgumentException(this.getClass().getSimpleName() + " requires an even number of stop values, but " + (parameters.size() - 2) + " were provided."); } for (int i = 2; (i + 1) < parameters.size(); i = i + 2) { Stop stop = new Stop(parameters.get(i), parameters.get(i + 1)); stops.add(stop); } Double inputValue = input.evaluate(object, Double.class); Double baseValue = base.evaluate(object, Double.class); if (inputValue == null) { return null; } if( stops.size()==1){ // single stop Stop single = stops.get(0); return single.value.evaluate(object, context); } int find = find(object, inputValue, stops); if( find <= 0 ){ // data is below stop range, use min Stop min = stops.get(0); return min.value.evaluate(object, context); } else if (find >= stops.size()){ // data is above the stop range, use max Stop max = stops.get(stops.size()-1); return max.value.evaluate(object, context); } Stop lower = stops.get(find-1); Stop upper = stops.get(find); Object exponential = exponential(object, inputValue, baseValue, lower, upper, context); return Converters.convert(exponential, context); } private <T> Object exponential(Object object, double inputValue, double base, Stop lower, Stop upper, Class<T> context) { if (Color.class.isAssignableFrom(context)) { return colorExponential(object, inputValue, base, lower, upper); } else { return numericExponential(object, inputValue, base, lower, upper); } } private double numericExponential(Object object, double inputValue, double base, Stop lower, Stop upper) { double stop1 = lower.stop.evaluate(object,Double.class); double value1 = lower.value.evaluate(object,Double.class); double stop2 = upper.stop.evaluate(object,Double.class); double value2 = upper.value.evaluate(object,Double.class); // Basic exponential function: // // value_i = scale*(stop_i)^base - offset // // Determine scale and offset based on the upper and lower stops: double scale = (value2-value1)/(Math.pow(stop2, base) - Math.pow(stop1, base)); double offset = value1-scale*Math.pow(stop1, base); return offset + scale*Math.pow(inputValue, base); } /** * Perform exponential interpolation on each of the channels of the color values at each stop. */ private Object colorExponential(Object object, double inputValue, double base, Stop lower, Stop upper) { Color lowerValue = lower.value.evaluate(object, Color.class); Color upperValue = upper.value.evaluate(object, Color.class); Stop redLowerStop = new Stop(lower.stop, ff2.literal(lowerValue.getRed())); Stop redUpperStop = new Stop(upper.stop, ff2.literal(upperValue.getRed())); Stop greenLowerStop = new Stop(lower.stop, ff2.literal(lowerValue.getGreen())); Stop greenUpperStop = new Stop(upper.stop, ff2.literal(upperValue.getGreen())); Stop blueLowerStop = new Stop(lower.stop, ff2.literal(lowerValue.getBlue())); Stop blueUpperStop = new Stop(upper.stop, ff2.literal(upperValue.getBlue())); Stop alphaLowerStop = new Stop(lower.stop, ff2.literal(lowerValue.getAlpha())); Stop alphaUpperStop = new Stop(upper.stop, ff2.literal(upperValue.getAlpha())); double r = numericExponential(object, inputValue, base, redLowerStop, redUpperStop); double g = numericExponential(object, inputValue, base, greenLowerStop, greenUpperStop); double b = numericExponential(object, inputValue, base, blueLowerStop, blueUpperStop); double a = numericExponential(object, inputValue, base, alphaLowerStop, alphaUpperStop); return new Color((int) Math.round(r), (int) Math.round(g), (int) Math.round(b), (int) Math.round(a)); } /** * Find the stop containing the input value. The value returned is the index, in the stops list, * of the higher point of the segment between two stops. * * @return stop index; or 0 if input is below the range of the stops; or * {@code max stop index + 1} if it is above the range */ private int find(Object object, Double input, List<Stop> stops) { int find = stops.size(); for (int i = 0; i < stops.size(); i++) { Double stop = stops.get(i).stop.evaluate(object, Double.class); if (input <= stop) { find = i; break; } } return find; } }