/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, 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.util.ArrayList;
import java.util.List;
import java.util.MissingResourceException;
import org.geotools.renderer.i18n.ErrorKeys;
import org.geotools.renderer.i18n.Errors;
import org.geotools.renderer.lite.gridcoverage2d.LinearColorMap.LinearColorMapType;
import org.geotools.styling.ColorMap;
import org.geotools.styling.ColorMapEntry;
import org.geotools.styling.RasterSymbolizer;
import org.geotools.util.NumberRange;
import org.opengis.filter.expression.Expression;
/**
* Builder facility for creating a {@link LinearColorMap} using elements from {@link RasterSymbolizer} {@link ColorMapTransform} element. <p> This class is not intended to be thread safe.
* @author Simone Giannecchini, GeoSolutions
*
* @source $URL$
*/
public class SLDColorMapBuilder {
public final static Color defaultColorForValuesToPreserve= new Color(0,0,0,0);
/**Default color to fill gaps.**/
public final static Color defaultGapsColor= new Color(0,0,0,0);
/**List of {@link LinearColorMapElement} we are putting together to create the final {@link LinearColorMap}**/
private final List<LinearColorMapElement> colormapElements = new ArrayList<LinearColorMapElement>();
/**
* {@link LinearColorMapType} *
* @uml.property name="linearColorMapType"
*/
private int linearColorMapType = -1;
/**{@link Color} used to fill gaps.**/
private Color gapsColor;
/**{@link Color} use for the values that we want to preserve through the color map.**/
private Color preservedValuesColor;
/**List of values that we want to preserve through the color map.**/
private final List<Double> preservedValues = new ArrayList<Double>();
/**Number of colors we can distribute to each {@link ColorMapTransformElement}.**/
private int colorsPerColorMapElement;
/**
* Used during {@link LinearColorMap} creation to hold the last color.
*/
private Color lastColorValue;
private String name;
/**
* Default constructor for the {@link SLDColorMapBuilder} class.
*/
public SLDColorMapBuilder() {
this.name="sld-colormap";
}
/**
* Constructor for the {@link SLDColorMapBuilder} class.
* @param name name for the {@link LinearColorMap} we will create at the
* end of this process.
*/
public SLDColorMapBuilder(
final String name) {
ColorMapUtilities.ensureNonNull("name", name);
this.name= name;
}
/**
* Sets the default {@link Color} to use when a value falls outside the range of values for provided color map elements. <p> Note that once the underlying colormap has been built this method will throw an {@link IllegalStateException} if invoked. <p> In case one would want to unset the default color, he should simply call this method with a <code>null</code> value.
* @param the default {@link Color} to use when a value falls outside the provided color map elements.
* @uml.property name="gapsColor"
*/
public void setGapsColor(final Color defaultColor) {
// /////////////////////////////////////////////////////////////////////
//
// Do we already have a liner color map?
//
// /////////////////////////////////////////////////////////////////////
checkIfColorMapCreated();
//set the defult color
this.gapsColor = defaultColor;
}
/**
* Checks whether or not the underlying {@link LinearColorMap} has been already
* created.
*
* @throws IllegalStateException In case the the underlying {@link LinearColorMap} has been already created.
*/
private void checkIfColorMapCreated() throws IllegalStateException {
// /////////////////////////////////////////////////////////////////////
//
// Do we already have a liner color map?
//
// /////////////////////////////////////////////////////////////////////
if(this.colorMap!=null)
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_STATE));
}
/**
* Sets the {@link LinearColorMapType} for this {@link SLDColorMapBuilder} .
* @see LinearColorMapType
* @return this {@link SLDColorMapBuilder} .
* @uml.property name="linearColorMapType"
*/
public SLDColorMapBuilder setLinearColorMapType(int colorMapType) {
// //
//
// Do we already have a liner color map?
//
// //
checkIfColorMapCreated();
////
//
// Color map type cannot be changed once it has been set.
//
/////
if (LinearColorMapType.validateColorMapTye(this.linearColorMapType ))
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_STATE));
////
//
// provided Color map type validation.
//
/////
if (!LinearColorMapType.validateColorMapTye(colorMapType))
throw new IllegalArgumentException(
Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2,
"colorMapType",
Integer.toString(colorMapType)));
///set the linear colormap type
this.linearColorMapType = colorMapType;
return this;
}
/**
* Retrieves the {@link LinearColorMapType} for this {@link SLDColorMapBuilder} . <p> <code>-1</code> is returned in case the {@link LinearColorMapType} is still unspecified.
* @return the {@link LinearColorMapType} for this {@link SLDColorMapBuilder} or <code>-1</code> case the {@link LinearColorMapType} is still unspecified.
* @uml.property name="linearColorMapType"
*/
public int getLinearColorMapType() {
return linearColorMapType;
}
/**
* Add a new {@link ColorMapEntry} to the list of {@link ColorMapEntry} we want to use
* for building a {@link LinearColorMap}.
*
* <p>
*
* @param colorMapEntry
* @return
*/
public SLDColorMapBuilder addColorMapEntry(ColorMapEntry colorMapEntry) {
///////////////////////////////////////////////////////////////////////
//
// INITIAL CHECKS
//
///////////////////////////////////////////////////////////////////////
// Color map type must be already set.
ColorMapUtilities.ensureNonNull("colorMapEntry",colorMapEntry);
// Color map already created.
checkIfColorMapCreated();
// Color map type must be already set.
// Number of domains must be already set.
ColorMapUtilities.ensureNonNull("colorMapEntry",colorMapEntry);
if(this.numberColorMapEntries==-1||linearColorMapType == -1||numberColorMapEntries<colormapElements.size()+1)
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_STATE));
////
//
// Check that we already computed the number of colors for each range
//
/////
init();
///////////////////////////////////////////////////////////////////////
//
// ACTUAL WORK
//
///////////////////////////////////////////////////////////////////////
////
//
// Parse the provided ColorMapEntry
//
/////
// label
String label = colorMapEntry.getLabel();
label = label == null ? "ColorMapEntry"+this.colormapElements.size() : label;
// quantity
final double q = getQuantity(colorMapEntry);
// color and opacity
Color newColorValue = getColor(colorMapEntry);
ColorMapUtilities.ensureNonNull("newColorValue",newColorValue);
final double opacityValue = getOpacity(colorMapEntry);
newColorValue = new Color(newColorValue.getRed(), newColorValue
.getGreen(), newColorValue.getBlue(),
(int) (opacityValue * 255 + 0.5));
////
//
// Create a specific domain for it
//
/////
final boolean firstEntry=this.colormapElements.isEmpty();
if (firstEntry) {
////
//
//if this is the first entry we are adding, we have a pretty special treatment for it
// since it has to cover, depending on the colormap type a big part of the possible values.
//
//
////
switch (linearColorMapType) {
case ColorMap.TYPE_RAMP:
colormapElements.add(LinearColorMapElement.create(label, newColorValue,
NumberRange.create(Double.NEGATIVE_INFINITY, false, q,
false), 0));
break;
case ColorMap.TYPE_VALUES:
colormapElements.add(LinearColorMapElement.create(label, newColorValue,
q, 0));
break;
case ColorMap.TYPE_INTERVALS:
colormapElements.add(LinearColorMapElement.create(label, newColorValue,
NumberRange.create(Double.NEGATIVE_INFINITY, false, q,
false), 0));
break;
default:
//should not happen
throw new IllegalArgumentException(
Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2,
"ColorMapTransform.type",
Double.toString(opacityValue),
Integer.valueOf(linearColorMapType)));
}
} else {
////
//
// this is NOT the first entry we are adding, hence we need to connect it to the previous one.
//
//
////
// Get the previous category
final int newColorMapElementIndex = this.colormapElements.size();
final LinearColorMapElement previous = (LinearColorMapElement) this.colormapElements
.get(newColorMapElementIndex - 1);
// //
//
// Build the new one.
//
// //
switch (linearColorMapType) {
case ColorMap.TYPE_RAMP:
colormapElements.add(LinearColorMapElement.create(label,
new Color[] { lastColorValue, newColorValue },
NumberRange.create(((NumberRange<? extends Number>) previous.getRange())
.getMaximum(), true, q, false),
NumberRange.create((int) previous.getOutputMaximum() ,
colorsPerColorMapElement
+ (int) previous.getOutputMaximum())));
break;
case ColorMap.TYPE_VALUES:
colormapElements.add(LinearColorMapElement.create(label,
newColorValue, q, newColorMapElementIndex));
break;
case ColorMap.TYPE_INTERVALS:
colormapElements.add(LinearColorMapElement.create(label,
newColorValue, NumberRange.create(((NumberRange<? extends Number>) previous
.getRange()).getMaximum(), true, q, false),
newColorMapElementIndex));
break;
default:
throw new IllegalArgumentException(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, "ColorMapTransform.type", Double
.toString(opacityValue), Integer.valueOf(
linearColorMapType)));
}
}
lastColorValue = newColorValue;
return this;
}
/**
* initialization of the basic element for this {@link SLDColorMapBuilder}.
*/
private void init() {
if(numberOfColorMapElements!=-1)
return;
// //
//
// A ColorMapTransform with a single entry makes sense only if we have
// ColorMapTransform type VALUES
//
// //
if (numberColorMapEntries== 1 && linearColorMapType != ColorMap.TYPE_VALUES)
throw new IllegalArgumentException(
Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,"colormap entries","1"));
// /////////////////////////////////////////////////////////////////////
//
// PREPARATION
//
// /////////////////////////////////////////////////////////////////////
numberOfColorMapElements = numberColorMapEntries;
if (linearColorMapType == ColorMap.TYPE_RAMP)
{
// //
//
// Let's distribute the number of colors that we'll use between the
// various color map elements we'll build. We start by checking if we can use
// 256 colors, otherwise we switch to 65536.
//
// Keep into account that some categories are inherently single
// valued, hence we take them out of the count because we don't.
//
// //
double colorsToDistribute=!extendedColors && numberColorMapEntries < 256?256:65536;
//default color
if(gapsColor!=null)
colorsToDistribute--;
//preserved values
if(preservedValuesColor!=null)
colorsToDistribute--;
//compute the number of colors we can use for each color map element
colorsPerColorMapElement = (int) ((colorsToDistribute ) /
// we remove one since the first and last element use 1 color only, hence we want to account only the internal ranges
(numberOfColorMapElements-1));
//now keep into account the last element
numberOfColorMapElements++;
} else
colorsPerColorMapElement = 1;
}
/**
* Retrieves the values to preserve.
*
* @return an array of double which represents the values that need to
* be preserved by the {@link ColorMapTransform} we will create.
*/
public double[] getValuesToPreserve() {
if (this.preservedValues.size() == 0)
return new double[0];
final double retVal[] = new double[this.preservedValues.size()];
int i = 0;
for(Double value:preservedValues)
retVal[i++] = value.doubleValue();
return retVal;
}
/**
* Add a value that we should try to preserve while applying the color map.
*
* <p>
* This means that all the values we add using this method will be mapped to
* the same color which can be set using {@link #setColorForValuesToPreserve(Color)}.
*
* <p>
*
* @return this {@link SLDColorMapBuilder}.
*/
public SLDColorMapBuilder addValueToPreserve(final double value) {
checkIfColorMapCreated();
//add the value to preserve if not already in
assert preservedValues !=null;
preservedValues.add(value);
return this;
}
/**
* Set the color to use for the values we want to preserve.
* @return this {@link SLDColorMapBuilder}.
*/
public SLDColorMapBuilder setColorForValuesToPreserve(final Color color) {
ColorMapUtilities.ensureNonNull("color",color);
checkIfColorMapCreated();
//@todo TODO check number of color map entries
this.preservedValuesColor = color;
return this;
}
/**
* @return
*/
public Color getColorForValuesToPreserve() {
return this.preservedValuesColor;
}
/**
* @param entry
* @return
* @throws NumberFormatException
*/
private static Color getColor(ColorMapEntry entry)
throws NumberFormatException {
ColorMapUtilities.ensureNonNull("ColorMapEntry",entry);
final Expression color = entry.getColor();
ColorMapUtilities.ensureNonNull("color",color);
final String colorString= (String) color.evaluate(null, String.class);
ColorMapUtilities.ensureNonNull("colorString",colorString);
return Color.decode(colorString);
}
/**
* @param entry
* @return
* @throws IllegalArgumentException
* @throws MissingResourceException
*/
private static double getOpacity(ColorMapEntry entry)
throws IllegalArgumentException, MissingResourceException {
ColorMapUtilities.ensureNonNull("ColorMapEntry",entry);
// //
//
// As stated in <a
// href="https://portal.opengeospatial.org/files/?artifact_id=1188">
// OGC Styled-Layer Descriptor Report (OGC 02-070) version
// 1.0.0.</a>:
// "Not all systems can support opacity in colormaps. The default
// opacity is 1.0 (fully opaque)."
//
// //
ColorMapUtilities.ensureNonNull("entry",entry);
Expression opacity = entry.getOpacity();
Double opacityValue = null;
if (opacity != null)
opacityValue = (Double) opacity.evaluate(null, Double.class);
else
return 1.0;
if ((opacityValue.doubleValue() - 1) > 0
|| opacityValue.doubleValue() < 0) {
throw new IllegalArgumentException(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, "Opacity", opacityValue));
}
return opacityValue.doubleValue();
}
/**
* @param entry
* @return
*/
private static double getQuantity(ColorMapEntry entry) {
ColorMapUtilities.ensureNonNull("ColorMapEntry",entry);
Expression quantity = entry.getQuantity();
ColorMapUtilities.ensureNonNull("quantity",quantity);
Double quantityString = (Double) quantity.evaluate(null, Double.class);
ColorMapUtilities.ensureNonNull("quantityString",quantityString);
double q = quantityString.doubleValue();
return q;
}
/**
* @uml.property name="extendedColors"
*/
private boolean extendedColors = false;
/**
* Getter of the property <tt>extendedColors</tt>
* @return Returns the extendedColors.
* @uml.property name="extendedColors"
*/
public boolean getExtendedColors() {
return extendedColors;
}
/**
* Setter of the property <tt>extendedColors</tt> <p> Unless this property is set prior to start working with this {@link SLDColorMapBuilder} we will make use of only 256 colors. If we use extended colors, then we'll be able to use up to 65536 colors. <p> Note that this imposes a limitation on the maximum number of {@link ColorMapEntry} we can use.
* @param extendedColors The extendedColors to set.
* @return
* @uml.property name="extendedColors"
*/
public SLDColorMapBuilder setExtendedColors(boolean extendedColors) {
if(this.numberColorMapEntries!=-1)
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_STATE));
checkIfColorMapCreated();
this.extendedColors = extendedColors;
return this;
}
/**
* @uml.property name="numberColorMapEntries"
*/
private int numberColorMapEntries = -1;
/**
* Getter of the property <tt>numberColorMapEntries</tt>
* @return Returns the numberColorMapEntries.
* @uml.property name="numberColorMapEntries"
*/
public int getNumberColorMapEntries() {
return numberColorMapEntries;
}
/**
* Setter of the property <tt>numberColorMapEntries</tt>
* @param numberColorMapEntries The numberColorMapEntries to set.
* @return
* @uml.property name="numberColorMapEntries"
*/
public SLDColorMapBuilder setNumberColorMapEntries(
final int numberColorMapEntries) {
checkIfColorMapCreated();
if(this.numberColorMapEntries!=-1)
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_STATE));
if(numberColorMapEntries<=0||numberColorMapEntries>(extendedColors?65536:256))
throw new IllegalArgumentException(
Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2,
"numberColorMapEntries",
Integer.toString(numberColorMapEntries)
));
this.numberColorMapEntries = numberColorMapEntries;
return this;
}
/**
* @uml.property name="numberOfColorMapElements"
*/
private int numberOfColorMapElements = -1;
/**This is the target object for this builder.**/
private LinearColorMap colorMap;
/**
* Getter of the property <tt>numberOfColorMapElements</tt>
* @return Returns the numberOfColorMapElements.
* @uml.property name="numberOfColorMapElements"
*/
public int getNumberOfColorMapElements() {
return numberOfColorMapElements;
}
/**
*/
public LinearColorMap buildLinearColorMap() {
if(this.numberColorMapEntries==-1)
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_STATE));
// /////////////////////////////////////////////////////////////////////
//
// Do we already have an object?
//
// /////////////////////////////////////////////////////////////////////
if(this.colorMap!=null)
return this.colorMap;
// /////////////////////////////////////////////////////////////////////
//
// Create the last category
//
// /////////////////////////////////////////////////////////////////////
LinearColorMapElement last = (LinearColorMapElement) this.colormapElements
.get(this.colormapElements.size() - 1);
if (linearColorMapType == ColorMap.TYPE_RAMP) {
// //
//
// Get the previous category
//
// //
final LinearColorMapElement previous = last;
// //
//
// Build the last one
//
// //
last=LinearColorMapElement.create(
"ColorMapEntry"+this.colormapElements.size(), lastColorValue, NumberRange.create(
previous.getRange().getMaximum(),
true, Double.POSITIVE_INFINITY, false),
(int) previous.getOutputMaximum());
this.colormapElements.add(last);
}
// /////////////////////////////////////////////////////////////////////
//
// Create the list of no data classification domain elements. Note that
// all of them
//
// /////////////////////////////////////////////////////////////////////
final LinearColorMapElement preservedValuesElement[] = new LinearColorMapElement[preservedValues
.size()];
final int value=(int) last.getOutputMaximum()+1;
for (int i = 0; i < preservedValuesElement.length; i++) {
preservedValuesElement[i] = LinearColorMapElement
.create(
org.geotools.resources.i18n.Vocabulary.format(
org.geotools.resources.i18n.VocabularyKeys.NODATA)+ Integer.toString(i + 1),
preservedValuesColor,
NumberRange.create( preservedValues.get(i), preservedValues.get(i)),
value
);
}
this.colorMap = new LinearColorMap(name,
(LinearColorMapElement[]) colormapElements.toArray(new LinearColorMapElement[colormapElements.size()]),
preservedValuesElement,
this.gapsColor);
return colorMap;
}
}