/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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.brewer.color;
import java.awt.Color;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Contains ColorBrewer palettes and suitability data.
*
* @author James Macgill
* @author Cory Horner, Refractions Research Inc.
*
* @source $URL$
*/
public class ColorBrewer {
private static final java.util.logging.Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.brewer.color");
public static final PaletteType ALL = new PaletteType(true, true, "ALL");
public static final PaletteType SUITABLE_RANGED = new PaletteType(true, false);
public static final PaletteType SUITABLE_UNIQUE = new PaletteType(false, true);
public static final PaletteType SEQUENTIAL = new PaletteType(true, false, "SEQUENTIAL");
public static final PaletteType DIVERGING = new PaletteType(true, false, "DIVERGING");
public static final PaletteType QUALITATIVE = new PaletteType(false, true, "QUALITATIVE");
String name = null;
String description = null;
Hashtable<String,BrewerPalette> palettes = new Hashtable<String,BrewerPalette>();
/**
* Creates a new instance of ColorBrewer
*/
public ColorBrewer() {
}
/**
* Creates a static instance of ColorBrewer containing all default palettes
*
* @return The ColorBrewer instance with all the default palettes.
* @throws IOException
*/
public static ColorBrewer instance() {
ColorBrewer me = new ColorBrewer();
me.loadPalettes();
return me;
}
/**
* Creates a static instance of ColorBrewer containing a subset of the
* default palettes.
*
* @param type A PaletteType object which will be used to configure the
* returned ColorBrewer.
* @return The ColorBrewer instance with the palette from the parameter.
* @throws IOException
*/
public static ColorBrewer instance(PaletteType type)
throws IOException {
ColorBrewer me = new ColorBrewer();
me.loadPalettes(type);
return me;
}
public void registerPalette(BrewerPalette pal) {
palettes.put(pal.getName(), pal);
}
/**
* Returns true if the palette exists in this ColorBrewer
*
* @param paletteName A String with the name of the palette
* @return A boolean, true if the ColorBrewer has a palette of the name
* given.
*/
public boolean hasPalette(String paletteName) {
return palettes.containsKey(paletteName);
}
public BrewerPalette[] getPalettes() {
Object[] entry = this.palettes.keySet().toArray();
BrewerPalette[] palettes = new BrewerPalette[entry.length];
for (int i = 0; i < entry.length; i++) {
palettes[i] = (BrewerPalette) getPalette(entry[i].toString());
}
return palettes;
}
public BrewerPalette[] getPalettes(PaletteType type) {
return getPalettes(type, -1);
}
public BrewerPalette[] getPalettes(PaletteType type, int numClasses) {
List<BrewerPalette> palettes = new ArrayList<BrewerPalette>();
Object[] entry = this.palettes.keySet().toArray();
for (int i = 0; i < entry.length; i++) {
BrewerPalette pal = (BrewerPalette) getPalette(entry[i].toString());
boolean match = true;
//filter by number of classes
if (numClasses > -1) {
if (pal.getMaxColors() < numClasses) {
match = false;
}
}
if (!pal.getType().isMatch(type)) {
match = false;
}
if (match) {
palettes.add(pal);
}
}
return (BrewerPalette[]) palettes.toArray(new BrewerPalette[palettes.size()]);
}
public BrewerPalette[] getPalettes(PaletteType type, int numClasses, int requiredViewers) {
List<BrewerPalette> palettes = new ArrayList<BrewerPalette>();
Object[] entry = this.palettes.keySet().toArray();
for (int i = 0; i < entry.length; i++) {
BrewerPalette pal = (BrewerPalette) getPalette(entry[i].toString());
boolean match = true;
//filter by number of classes
if (numClasses > -1) {
if (pal.getMaxColors() < numClasses) {
match = false;
}
}
if (!pal.getType().isMatch(type)) {
match = false;
}
int[] suitability = pal.getPaletteSuitability().getSuitability(numClasses);
if (isSet(PaletteSuitability.VIEWER_COLORBLIND, requiredViewers)
&& (suitability[PaletteSuitability.VIEWER_COLORBLIND] != PaletteSuitability.QUALITY_GOOD)) {
match = false;
} else if (isSet(PaletteSuitability.VIEWER_CRT, requiredViewers)
&& (suitability[PaletteSuitability.VIEWER_CRT] != PaletteSuitability.QUALITY_GOOD)) {
match = false;
} else if (isSet(PaletteSuitability.VIEWER_LCD, requiredViewers)
&& (suitability[PaletteSuitability.VIEWER_LCD] != PaletteSuitability.QUALITY_GOOD)) {
match = false;
} else if (isSet(PaletteSuitability.VIEWER_PHOTOCOPY, requiredViewers)
&& (suitability[PaletteSuitability.VIEWER_PHOTOCOPY] != PaletteSuitability.QUALITY_GOOD)) {
match = false;
} else if (isSet(PaletteSuitability.VIEWER_PRINT, requiredViewers)
&& (suitability[PaletteSuitability.VIEWER_PRINT] != PaletteSuitability.QUALITY_GOOD)) {
match = false;
} else if (isSet(PaletteSuitability.VIEWER_PROJECTOR, requiredViewers)
&& (suitability[PaletteSuitability.VIEWER_PROJECTOR] != PaletteSuitability.QUALITY_GOOD)) {
match = false;
}
if (match) {
palettes.add(pal);
}
}
return (BrewerPalette[]) palettes.toArray(new BrewerPalette[palettes.size()]);
}
/**
* Generates a String array with the names of the palettes in the
* ColorBrewer instance.
*
* @return A String array with the names of the palettes in the ColorBrewer
* instance.
*/
public String[] getPaletteNames() {
Object[] keys = palettes.keySet().toArray();
String[] paletteList = new String[keys.length];
for (int i = 0; i < keys.length; i++) {
paletteList[i] = keys[i].toString();
}
return paletteList;
}
/**
* Generates an array of palette names for palettes which have at least x
* classes and at most y classes.
*
* @param minClasses x
* @param maxClasses y
*
* @return A string array of palette names filtered by number of classes.
*/
public String[] getPaletteNames(int minClasses, int maxClasses) {
Object[] keys = palettes.keySet().toArray();
Set<String> paletteSet = new HashSet<String>();
//generate the set of palette names
for (int i = 0; i < keys.length; i++) {
BrewerPalette thisPalette = (BrewerPalette) palettes.get(keys[i]);
int numColors = thisPalette.getMaxColors();
if ((numColors >= minClasses) && (numColors <= maxClasses)) {
paletteSet.add(thisPalette.getName());
}
}
//convert set to string array
String[] paletteList = new String[paletteSet.size()];
Object[] paletteObjList = paletteSet.toArray();
for (int i = 0; i < paletteSet.size(); i++) {
paletteList[i] = (String) paletteObjList[i];
}
return paletteList;
}
public BrewerPalette getPalette(String name) {
return (BrewerPalette) palettes.get(name);
}
/**
* Loads the default ColorBrewer palettes.
*
* @throws IOException
*/
public void loadPalettes() {
loadPalettes(SEQUENTIAL);
loadPalettes(DIVERGING);
loadPalettes(QUALITATIVE);
}
/**
* Loads into the ColorBrewer instance the set of palettes which have the
* PaletteType matching that of the parameter.
*
* @param type The PaletteType for the palettes to load.
* @throws IOException
*/
public void loadPalettes(PaletteType type) {
if (type.equals(ALL)) {
loadPalettes();
return;
} else if (type.equals(SUITABLE_RANGED)) {
loadPalettes(SEQUENTIAL);
loadPalettes(DIVERGING);
return;
} else if (type.equals(SUITABLE_UNIQUE)) {
loadPalettes(QUALITATIVE);
return;
}
if (type.getName() == null) {
return;
}
// force to lower case with US locale for http://jira.codehaus.org/browse/UDIG-1265
String paletteSet = type.getName().toLowerCase(Locale.US);
URL url = getClass().getResource("resources/" + paletteSet + ".xml");
InputStream stream;
try {
stream = url.openStream();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "couldn't open input stream to load palette", e);
return;
}
load(stream, type);
}
/**
* Loads into the ColorBrewer instance the set of palettes matching the
* given parameters.
*
* @param XMLinput
* @param type identifier for palettes. use "new PaletteType();"
*/
public void loadPalettes(InputStream XMLinput, PaletteType type) {
load(XMLinput, type);
}
private void load(InputStream stream, PaletteType type) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(stream);
this.name = fixToString(document.getElementsByTagName("name").item(0).getFirstChild()
.toString());
this.description = fixToString(document.getElementsByTagName("description").item(0)
.getFirstChild().toString());
SampleScheme scheme = new SampleScheme();
NodeList samples = document.getElementsByTagName("sample");
for (int i = 0; i < samples.getLength(); i++) {
Node sample = samples.item(i);
int size = Integer.parseInt(sample.getAttributes().getNamedItem("size")
.getNodeValue());
String values = fixToString(sample.getFirstChild().toString());
int[] list = new int[size];
StringTokenizer tok = new StringTokenizer(values);
for (int j = 0; j < size; j++) {
list[j] = Integer.parseInt(tok.nextToken(","));
}
scheme.setSampleScheme(size, list);
}
NodeList palettes = document.getElementsByTagName("palette");
for (int i = 0; i < palettes.getLength(); i++) {
BrewerPalette pal = new BrewerPalette();
PaletteSuitability suitability = new PaletteSuitability();
NodeList paletteInfo = palettes.item(i).getChildNodes();
for (int j = 0; j < paletteInfo.getLength(); j++) {
Node item = paletteInfo.item(j);
if (item.getNodeName().equals("name")) {
pal.setName(fixToString(item.getFirstChild().toString()));
}
if (item.getNodeName().equals("description")) {
pal.setDescription(fixToString(item.getFirstChild().toString()));
}
if (item.getNodeName().equals("colors")) {
StringTokenizer oTok = new StringTokenizer(fixToString(
item.getFirstChild().toString()));
int numColors = 0;
Color[] colors = new Color[15];
for (int k = 0; k < 15; k++) {
if (!oTok.hasMoreTokens()) {
break;
}
String entry = oTok.nextToken(":");
StringTokenizer iTok = new StringTokenizer(entry);
int r = Integer.parseInt(iTok.nextToken(",").trim());
int g = Integer.parseInt(iTok.nextToken(",").trim());
int b = Integer.parseInt(iTok.nextToken(",").trim());
colors[numColors] = new Color(r, g, b);
numColors++;
}
pal.setColors(colors);
}
if (item.getNodeName().equals("suitability")) {
NodeList schemeSuitability = item.getChildNodes();
for (int k = 0; k < schemeSuitability.getLength(); k++) {
Node palScheme = schemeSuitability.item(k);
if (palScheme.getNodeName().equals("scheme")) {
int paletteSize = Integer.parseInt(palScheme.getAttributes()
.getNamedItem("size")
.getNodeValue());
String values = fixToString(palScheme.getFirstChild().toString());
String[] list = new String[6];
StringTokenizer tok = new StringTokenizer(values);
// obtain all 6 values, which should each be
// G=GOOD, D=DOUBTFUL, B=BAD, or ?=UNKNOWN.
for (int m = 0; m < 6; m++) {
list[m] = tok.nextToken(",");
}
suitability.setSuitability(paletteSize, list);
}
}
}
}
pal.setType(type);
pal.setColorScheme(scheme);
pal.setPaletteSuitability(suitability);
registerPalette(pal); // add the palette
}
} catch (SAXException sxe) {
LOGGER.log(Level.SEVERE, "Error during palette parsing", sxe);
} catch (ParserConfigurationException pce) {
LOGGER.log(Level.SEVERE, "Parser with specified options can't be built", pce);
} catch (IOException ioe) {
LOGGER.log(Level.SEVERE, "i/o error during palette parsing", ioe);
}
}
/**
* Converts "[#text: 1,2,3]" to "1,2,3".
*
* <p>
* This is a brutal hack for fixing the org.w3c.dom API. Under j1.4
* Node.toString() returns "1,2,3", under j1.5 Node.toString() returns
* "[#text: 1,2,3]".
* </p>
*
* @param input A String with the input.
*
* @return A String with the modified input.
*/
private String fixToString(String input) {
if (input.startsWith("[") && input.endsWith("]")) {
input = input.substring(1, input.length() - 1); //remove []
input = input.replaceAll("#text: ", ""); //remove "#text: "
}
return input;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public void reset() {
name = null;
description = null;
palettes = new Hashtable<String,BrewerPalette>();
}
public boolean isSet(int singleValue, int multipleValue) {
return ((singleValue & multipleValue) != 0);
}
}