/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2013-2016, 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.renderer.lite.gridcoverage2d;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.geotools.filter.FilterFactoryImpl;
import org.geotools.styling.ColorMap;
import org.geotools.styling.ColorMapEntry;
import org.geotools.styling.ColorMapEntryImpl;
import org.geotools.styling.ColorMapImpl;
import org.geotools.util.ColorConverterFactory;
import org.geotools.util.Converter;
import org.geotools.util.Converters;
import org.geotools.util.SoftValueHashMap;
import org.geotools.util.Utilities;
import org.opengis.filter.FilterFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* A class mainly used to parse an SVG file and create a ColorMap on top of the
* LinearGradientEntry contained on it, or create a ColorMap on top of a ";" separated values list
* of colors such as: rgb(0,0,255);rgb(0,255,0);rgb(255,0,0);... or
* #0000FF;#00FF00;#FF0000 as an instance.
* *
* @author Daniele Romagnoli, GeoSolutions SAS
*
*/
public class GradientColorMapGenerator {
public static final String RGB_INLINEVALUE_MARKER = "rgb(";
public static final String RGBA_INLINEVALUE_MARKER = "rgba(";
public static final String HEX_INLINEVALUE_MARKER = "#";
public static final String HEX2_INLINEVALUE_MARKER = "0x";
private static Color TRANSPARENT = new Color(0, 0, 0, 0);
private Color beforeColor = TRANSPARENT;
private Color afterColor = TRANSPARENT;
private LinearGradientEntry[] entries;
private static SoftValueHashMap<String, GradientColorMapGenerator> cache = new SoftValueHashMap<String, GradientColorMapGenerator>();
private static final Converter COLOR_CONVERTER = new ColorConverterFactory().createConverter(Color.class, String.class, null);
private GradientColorMapGenerator(LinearGradientEntry[] entries) {
this.entries = entries;
}
/**
* Sets the color to be used before the min value. By default it's transparent
* @param color
*/
public void setBeforeColor(Color color) {
this.beforeColor = color;
}
/**
* Sets the color to be used before the min value, as a string, it accepts the same syntax as {@link GradientColorMapGenerator#getColorMapGenerator(String)}
* @param color
*/
public void setBeforeColor(String color) {
this.beforeColor = getColorWithOpacity(color);
}
/**
* Sets the color to be used after the max value. By default it's transparent
* @param color
*/
public void setAfterColor(Color color) {
this.afterColor = color;
}
/**
* Sets the color to be used after the max value, as a string, it accepts the same syntax as {@link GradientColorMapGenerator#getColorMapGenerator(String)}
* @param color
*/
public void setAfterColor(String color) {
this.afterColor = getColorWithOpacity(color);
}
/**
* Generate a {@link ColorMap} object, by updating the ColorMapEntries quantities on top of the min and max values reported here.
* @param min
* @param max
* @return
*/
public ColorMap generateColorMap(double min, double max) {
final int numEntries = entries.length;
final double range = max - min;
boolean intervals = false;
// Preliminar check on intervals vs ramp
for (int i = 0; i < numEntries - 2; i += 2) {
if (Double.compare(entries[i + 1].percentage, entries[i + 2].percentage) == 0) {
intervals = true;
} else {
intervals = false;
}
}
ColorMap colorMap = new ColorMapImpl();
// Adding transparent color entry before the min
final double offset = 0 /* intervals ? 0 : 1E-2 */;
double start = min - offset;
ColorMapEntry startEntry = entries[0].getColorMapEntry(start);
fillColorInEntry(startEntry, beforeColor);
colorMap.addColorMapEntry(startEntry);
if (intervals) {
colorMap.setType(ColorMap.TYPE_INTERVALS);
for (int i = 1; i < numEntries - 1; i += 2) {
colorMap.addColorMapEntry(entries[i].getColorMapEntry(min, range));
}
} else {
colorMap.setType(ColorMap.TYPE_RAMP);
for (int i = 0; i < numEntries - 1; i ++) {
colorMap.addColorMapEntry(entries[i].getColorMapEntry(min, range));
}
}
colorMap.addColorMapEntry(entries[numEntries - 1].getColorMapEntry(max));
// Adding transparent color entry after the max
ColorMapEntry endEntry = entries[numEntries - 1].getColorMapEntry(max + offset);
fillColorInEntry(endEntry, afterColor);
colorMap.addColorMapEntry(endEntry);
return colorMap;
}
private void fillColorInEntry(ColorMapEntry startEntry, Color color) {
if(color == null) {
color = TRANSPARENT;
}
startEntry.setColor(filterFactory.literal(toHexColor(color)));
startEntry.setOpacity(filterFactory.literal(color.getAlpha() / 255.));
}
private static FilterFactory filterFactory = new FilterFactoryImpl();
/**
* A class representing an SVG LinearGradient color entry
*
* A typical SVG linear gradient is structured like this:
*
* <linearGradient id="GPS-Fire-Dust-Blended" gradientUnits="objectBoundingBox" spreadMethod="pad" x1="0%" x2="100%" y1="0%" y2="0%">
* <stop offset="0.00%" stop-color="rgb(25,9,8)" stop-opacity="1.0000"/>
* <stop offset="14.14%" stop-color="rgb(51,43,43)" stop-opacity="1.0000"/>
* ............................
*
* @author Daniele Romagnoli, GeoSolutions SAS
*/
static class LinearGradientEntry {
public LinearGradientEntry(double percentage, Color color, double opacity) {
this.percentage = percentage;
this.opacity = opacity;
this.color = color;
}
private double opacity;
private double percentage;
private Color color;
private ColorMapEntry getColorMapEntry(double value) {
return getColorMapEntry(value, Double.NaN);
}
private ColorMapEntry getColorMapEntry(double min, double range) {
ColorMapEntry entry = new ColorMapEntryImpl();
entry.setOpacity(filterFactory.literal(opacity));
entry.setColor(filterFactory.literal(toHexColor(color)));
entry.setQuantity(filterFactory.literal(min + (Double.isNaN(range) ? 0 : (percentage*range))));
return entry;
}
}
/**
* Get an SVG ColorMap generator for the specified file
* @param file
* @return
* @throws SAXException
* @throws IOException
* @throws ParserConfigurationException
*/
public static GradientColorMapGenerator getColorMapGenerator(final File file) throws SAXException, IOException, ParserConfigurationException {
GradientColorMapGenerator generator = null;
Utilities.ensureNonNull("file", file);
final String identifier = file.getAbsolutePath();
synchronized (cache) {
if (cache.containsKey(identifier)) {
generator = cache.get(identifier);
} else {
// create the granule coverageDescriptor
generator = parseSVG(file);
cache.put(identifier, generator);
}
}
return generator;
}
/**
* Get an SVG ColorMap generator for the specified file
* @param a ";" separated list of colors in the form c1;c2;c3;... where each color can use syntaxes as rgb(r0,g0,b0), rgba(r0,g0,b0,alpha_0_to_1), #RRGGBB or 0xRRGGBB
* @return
* @throws SAXException
* @throws IOException
* @throws ParserConfigurationException
*/
public static GradientColorMapGenerator getColorMapGenerator(String colorValues) throws IOException, ParserConfigurationException {
Utilities.ensureNonNull("colorValues", colorValues);
if (colorValues.startsWith(RGB_INLINEVALUE_MARKER) || colorValues.startsWith(RGBA_INLINEVALUE_MARKER) || colorValues.startsWith(HEX_INLINEVALUE_MARKER) ||
colorValues.startsWith(HEX2_INLINEVALUE_MARKER)) {
String rampType = "ramp";
if (colorValues.contains(":")) {
final int rampTypeIndex = colorValues.indexOf(":");
rampType = colorValues.substring(rampTypeIndex + 1);
colorValues = colorValues.substring(0, rampTypeIndex);
}
String colors[] = colorValues.split(";");
final int numEntries = colors.length;
LinearGradientEntry[] entries = new LinearGradientEntry[numEntries];
final double step = 1d / (numEntries-1);
for (int i=0; i<numEntries; i++) {
final Color color = createColor(colors[i]);
final float opacity = getOpacity(colors[i]);
entries[i] = new LinearGradientEntry(step*i, color, opacity);
}
GradientColorMapGenerator generator = new GradientColorMapGenerator(entries);
return generator;
} else {
throw new IOException("Unable to parse the specified colors: " + colorValues);
}
}
/**
* Parse an SVG xmlFile
* @param xmlFile
* @return
* @throws SAXException
* @throws IOException
* @throws ParserConfigurationException
*/
private static GradientColorMapGenerator parseSVG(final File xmlFile) throws SAXException, IOException, ParserConfigurationException {
Utilities.ensureNonNull("xmlFile", xmlFile);
final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
final Document doc = dBuilder.parse(xmlFile);
doc.getDocumentElement().normalize();
final NodeList nList = doc.getElementsByTagName("stop");
final int numEntries = nList.getLength();
double percentage;
double opacity;
Color color;
// Setup the Gradient Entries
LinearGradientEntry[] gradientEntries = new LinearGradientEntry[numEntries];
for (int i = 0; i < numEntries; i++) {
Node node = nList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
String offset = element.getAttribute("offset");
String stopColor = element.getAttribute("stop-color");
String stopOpacity = element.getAttribute("stop-opacity");
percentage = Double.parseDouble(offset.substring(0, offset.length() -1)) / 100d;
opacity = Double.parseDouble(stopOpacity);
color = createColor (stopColor);
gradientEntries[i] = new LinearGradientEntry(percentage, color, opacity);
}
}
// Setup a GradientColorMapGenerator
GradientColorMapGenerator generator = new GradientColorMapGenerator(gradientEntries);
return generator;
}
/**
* Create a {@link Color} from a color String which may be an SVG color: rgb(R0,G0,B0), rgba(R0,G0,B0,Alpha) or hex color: #RRGGBB
* @param the String color representation
* @return the {@link Color} instance related to that string definition
*/
private static Color createColor(String color) {
if (color.startsWith(RGB_INLINEVALUE_MARKER)) {
String colorString = color.substring(4, color.length()-1);
String rgb[] = colorString.split("\\s*,\\s*");
return new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]));
} else if (color.startsWith(RGBA_INLINEVALUE_MARKER)) {
String colorString = color.substring(5, color.length()-1);
String rgba[] = colorString.split("\\s*,\\s*");
return new Color(Integer.parseInt(rgba[0]), Integer.parseInt(rgba[1]), Integer.parseInt(rgba[2]));
} else if ((color.startsWith("#") && color.length() == 7) || (color.startsWith("0x") && color.length() == 8)) {
// Try to parse it as an HEX code
return hex2Rgb(color);
}
throw new UnsupportedOperationException("Support for the following color ins't currently supported: " + color);
}
private static float getOpacity(String color) {
if (color.startsWith(RGBA_INLINEVALUE_MARKER)) {
String colorString = color.substring(5, color.length()-1);
String rgba[] = colorString.split("\\s*,\\s*");
return Float.parseFloat(rgba[3]);
} else {
return 1f;
}
}
private Color getColorWithOpacity(String color) {
if(color == null) {
return null;
}
Color c = createColor(color);
float opacity = getOpacity(color);
if(opacity < 1) {
c = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (opacity * 255));
}
return c;
}
/**
* Convert an hex color representation to a {@link Color}
* @param colorStr
* @return the {@link Color} instance related to that color HEX string
*/
public static Color hex2Rgb(String colorStr) {
if(colorStr.startsWith("#")) {
return new Color(
Integer.valueOf( colorStr.substring( 1, 3 ), 16 ),
Integer.valueOf( colorStr.substring( 3, 5 ), 16 ),
Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) );
} else {
return new Color(
Integer.valueOf( colorStr.substring( 2, 4 ), 16 ),
Integer.valueOf( colorStr.substring( 4, 6 ), 16 ),
Integer.valueOf( colorStr.substring( 6, 8 ), 16 ) );
}
}
/**
* Return an HEX representation of a Color
* @param color
* @return
*/
private static String toHexColor(final Color color) {
Utilities.ensureNonNull("color", color);
try {
return COLOR_CONVERTER.convert(color, String.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}