/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geotools.process.raster; import static org.geotools.filter.capability.FunctionNameImpl.parameter; import java.awt.Color; import java.awt.image.IndexColorModel; import java.security.InvalidParameterException; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.resource.Paths; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.Resource.Type; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.FunctionExpressionImpl; import org.geotools.filter.capability.FunctionNameImpl; import org.geotools.renderer.lite.gridcoverage2d.GradientColorMapGenerator; import org.geotools.renderer.lite.gridcoverage2d.SLDColorMapBuilder; import org.geotools.styling.ColorMap; import org.geotools.styling.ColorMapEntry; import org.geotools.styling.SLDTransformer; import org.geotools.styling.StyleFactory; import org.geotools.util.logging.Logging; import org.opengis.filter.FilterFactory; import org.opengis.filter.capability.FunctionName; import it.geosolutions.jaiext.classifier.LinearColorMap; import it.geosolutions.jaiext.piecewise.TransformationException; /** * Filter function to generate a {@link ColorMap} on top of an SVG file contained within the styles data folder * * * @author Daniele Romagnoli, GeoSolutions SAS */ public class FilterFunction_svgColorMap extends FunctionExpressionImpl { static final int LOG_SAMPLING_DEFAULT = 16; /** * Two colors are used for before and after palette, so this leaves a max of 254 colors to play with */ public static final int MAX_PALETTE_COLORS = 254; private static final Logger LOGGER = Logging.getLogger(FilterFunction_svgColorMap.class); public static final Color TRANSPARENT_COLOR = new Color(0,0,0,0); private static StyleFactory SF = CommonFactoryFinder.getStyleFactory(); private static FilterFactory FF = CommonFactoryFinder.getFilterFactory(); public static FunctionName NAME = new FunctionNameImpl("colormap", parameter("colormap", ColorMap.class), parameter("name", String.class), parameter("min", Number.class), parameter("max", Number.class), parameter("beforeColor", String.class, 0, 1), parameter("afterColor", String.class, 0, 1), parameter("logarithmic", Boolean.class, 0, 1), parameter("numcolors", Integer.class, 0, 1)); public FilterFunction_svgColorMap() { super(NAME); } public Object evaluate(Object feature) { String colorMap = getParameters().get(0).evaluate(feature, String.class); double min = getParameters().get(1).evaluate(feature, Double.class).doubleValue(); double max = getParameters().get(2).evaluate(feature, Double.class).doubleValue(); String beforeColor = null; String afterColor = null; boolean logarithmic = false; int expressionCount = getParameters().size(); int numColors = MAX_PALETTE_COLORS; if(expressionCount >= 4) { beforeColor = getParameters().get(3).evaluate(feature, String.class); } if(expressionCount >= 5) { afterColor = getParameters().get(4).evaluate(feature, String.class); } if(expressionCount >= 6) { Boolean log = getParameters().get(5).evaluate(feature, Boolean.class); if(log != null) { logarithmic = log; } } if(expressionCount >= 7) { Integer nc = getParameters().get(6).evaluate(feature, Integer.class); if(nc != null) { numColors = nc; } } return evaluate(colorMap, min, max, beforeColor, afterColor, logarithmic, numColors); } public Object evaluate(String colorMap, final double min, final double max, String beforeColor, String afterColor, boolean logarithmic, int numColors) { if(numColors < 1 || numColors > 254) { throw new InvalidParameterException("Number of colors must be comprised between 1 and 254"); } GradientColorMapGenerator generator = null; Resource xmlFile = null; if (!colorMap.startsWith(GradientColorMapGenerator.RGB_INLINEVALUE_MARKER) && !colorMap.startsWith(GradientColorMapGenerator.RGBA_INLINEVALUE_MARKER) && !colorMap.startsWith(GradientColorMapGenerator.HEX_INLINEVALUE_MARKER)) { GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class); colorMap = colorMap.replace('\\', '/'); String path = Paths.path("styles", "ramps", colorMap + ".svg"); xmlFile = loader.get( path ); if( xmlFile.getType() != Type.RESOURCE ){ throw new IllegalArgumentException( "The specified colorMap do not exist in the styles/ramps folder\n" + "Check that " + path + " exists and is an .svg file"); } } try { if (xmlFile != null) { generator = GradientColorMapGenerator.getColorMapGenerator(xmlFile.file()); } else { generator = GradientColorMapGenerator.getColorMapGenerator(colorMap); } generator.setBeforeColor(beforeColor); generator.setAfterColor(afterColor); ColorMap cm; if(!logarithmic) { cm = generator.generateColorMap(min, max); if(numColors < MAX_PALETTE_COLORS) { cm = sampleColorMap(numColors, min, max, cm, Function.identity(), numColors < MAX_PALETTE_COLORS); } } else { if(min <= 0) { throw new InvalidParameterException("Min range value must be positive in log scale mode"); } double logMin = Math.log(min); double logMax = Math.log(max); ColorMap logcm = generator.generateColorMap(logMin, logMax); int colors = numColors < MAX_PALETTE_COLORS ? numColors : LOG_SAMPLING_DEFAULT; cm = sampleColorMap(colors, logMin, logMax, logcm, Math::exp, numColors < MAX_PALETTE_COLORS); } if(LOGGER.isLoggable(Level.FINE)) { final SLDTransformer tx = new SLDTransformer(); tx.setIndentation(2); String sld = tx.transform(cm); LOGGER.fine("Generated Colormap:\n " + sld); } return cm; } catch (Exception e) { // probably a type error throw new IllegalArgumentException("Filter Function problem for function colormap", e); } } private ColorMap sampleColorMap(int numColors, double min, double max, ColorMap sourceCM, Function<Double, Double> quantityMapper, boolean useIntervals) throws TransformationException { ColorMap cm; LinearColorMap lcm = toLinearColorMap(sourceCM); IndexColorModel icm = lcm.getColorModel(); cm = SF.createColorMap(); cm.addColorMapEntry(entryForValue(min - Math.ulp(min), quantityMapper.apply(min), lcm, icm)); // before color double step = (max - min) / (numColors); // mind, the entry in interval mode defines the color up to that point for(int i = 0; i < (numColors - 1); i++) { double v = min + step * i; double mapValue = v; if(useIntervals) { mapValue = v + step; } cm.addColorMapEntry(entryForValue(v, quantityMapper.apply(mapValue), lcm, icm)); } cm.addColorMapEntry(entryForValue(max - Math.ulp(max), quantityMapper.apply(max), lcm, icm)); // last color if(useIntervals) { cm.setType(ColorMap.TYPE_INTERVALS); cm.addColorMapEntry(entryForValue(max, Double.POSITIVE_INFINITY, lcm, icm)); // after color } else { cm.addColorMapEntry(entryForValue(max, quantityMapper.apply(max), lcm, icm)); // after color } return cm; } private ColorMapEntry entryForValue(double value, double quantity, LinearColorMap lcm, IndexColorModel icm) throws TransformationException { ColorMapEntry entry = SF.createColorMapEntry(); int position = (int) Math.round(lcm.transform(value)); Color c = new Color(icm.getRed(position), icm.getGreen(position), icm.getBlue(position)); entry.setColor(FF.literal(c)); int alpha = icm.getAlpha(position); if(alpha < 255) { entry.setOpacity(FF.literal(alpha / 255.)); } entry.setQuantity(FF.literal(quantity)); return entry; } private LinearColorMap toLinearColorMap(ColorMap cm) { final SLDColorMapBuilder builder = new SLDColorMapBuilder(); final ColorMapEntry[] entries = cm.getColorMapEntries(); builder.setLinearColorMapType(ColorMap.TYPE_RAMP).setNumberColorMapEntries(entries.length); for (int i = 0; i < entries.length; i++) { builder.addColorMapEntry(entries[i]); } LinearColorMap lcm = builder.buildLinearColorMap(); return lcm; } }