/* (c) 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.geoserver.wms.style; import java.awt.Color; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.expression.FunctionBuilder; import org.geotools.styling.StyledLayerDescriptor; import org.geotools.styling.builder.FeatureTypeStyleBuilder; import org.geotools.styling.builder.StyledLayerDescriptorBuilder; import org.opengis.filter.FilterFactory; /** * Support class to parse a palette file and turn it into a dynamic style. The palette file syntax can contain rows with: * <ul> * <li>A comment, started with %</li> * <li>A #RRGGBB or 0xRRGGBB color</li> * <li>A #AARRGGBB or 0xAARRGGBB color</li> * * @author Andrea Aime */ public class PaletteParser { static final FilterFactory FF = CommonFactoryFinder.getFilterFactory(); /** * Number of colors in output, between 1 and 254 (excludes before and after color) */ public static final String NUMCOLORS = "NUMCOLORBANDS"; /** * If true a logarithmic progression palette is generated */ public static final String LOGSCALE = "LOGSCALE"; /** * Color after palette */ public static final String COLOR_AFTER = "ABOVEMAXCOLOR"; /** * Color before palette */ public static final String COLOR_BEFORE = "BELOWMINCOLOR"; /** * Range max value */ public static final String RANGE_MAX = "COLORSCALERANGE_MAX"; /** * Range min value */ public static final String RANGE_MIN = "COLORSCALERANGE_MIN"; /** * Opacity */ public static final String OPACITY = "OPACITY"; List<Color> parseColorMap(Reader reader) throws IOException { return new BufferedReader(reader).lines().filter(this::isNotEmpty).map(String::trim) .map(this::toColor).collect(Collectors.toList()); } StyledLayerDescriptor parseStyle(Reader reader) throws IOException { List<Color> colorMap = parseColorMap(reader); StyledLayerDescriptor sld = toDynamicColorMapStyle(colorMap); return sld; } StyledLayerDescriptor toDynamicColorMapStyle(List<Color> colorMap) { StyledLayerDescriptorBuilder sldBuilder = new StyledLayerDescriptorBuilder(); final FeatureTypeStyleBuilder fts = sldBuilder.namedLayer().style().featureTypeStyle(); fts.rule().raster(); FunctionBuilder dcmFunction = new FunctionBuilder(); dcmFunction.name("ras:DynamicColorMap"); dcmFunction.function("parameter").literal("data").end(); dcmFunction.function("parameter").literal("opacity").function("env").literal(OPACITY) .literal(1f).end().end(); FunctionBuilder paramFunction = dcmFunction.function("parameter"); FunctionBuilder cmFunction = paramFunction.literal("colorRamp").function("colormap"); cmFunction.literal(toColorExpressions(colorMap)); cmFunction.function("env").literal(RANGE_MIN).function("bandStats").literal(0) .literal("minimum").end().end(); cmFunction.function("env").literal(RANGE_MAX).function("bandStats").literal(0) .literal("maximum").end().end(); cmFunction.function("env").literal(COLOR_BEFORE).literal("rgba(255,255,255,0)").end(); cmFunction.function("env").literal(COLOR_AFTER).literal("rgba(255,255,255,0)").end(); cmFunction.function("env").literal(LOGSCALE).literal("false").end(); cmFunction.function("env").literal(NUMCOLORS).literal("254").end(); cmFunction.end(); paramFunction.end(); fts.transformation(dcmFunction.build()); StyledLayerDescriptor sld = sldBuilder.build(); return sld; } private String toColorExpressions(List<Color> colorMap) { return colorMap.stream().map(this::toColorSpec).collect(Collectors.joining(";")); } private String toColorSpec(Color c) { if (c.getAlpha() == 255) { return String.format("rgb(%d,%d,%d)", c.getRed(), c.getGreen(), c.getBlue()); } else { return String.format(Locale.ENGLISH, "rgba(%d,%d,%d,%.2f)", c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha() / 255.); } } private boolean isNotEmpty(String s) { if (s == null) { return false; } s = s.trim(); return !s.isEmpty() && !"extend".equals(s) && !s.startsWith("%"); } private Color toColor(String s) { try { final int length = s.length(); if (length == 7 || length == 8) { // #RRGGBB or 0xRRGGBB return Color.decode(s); } else { if (s.startsWith("#")) { s = s.substring(1); } else if (s.startsWith("0x")) { s = s.substring(2); } return new Color(Integer.valueOf(s.substring(2, 4), 16), Integer.valueOf(s.substring(4, 6), 16), Integer.valueOf(s.substring(6, 8), 16), Integer.valueOf(s.substring(0, 2), 16)); } } catch (Exception e) { throw new InvalidColorException(s, e); } } @SuppressWarnings("serial") public static class InvalidColorException extends RuntimeException { public InvalidColorException(String color, Throwable cause) { super("Invalid color '" + color + "', supported syntaxes are #RRGGBB, 0xRRGGBB, #AARRGGBB and 0xAARRGGBB", cause); } } }