/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.kvp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.resource.Resource;
import org.geoserver.platform.resource.Resources;
import org.geotools.image.palette.InverseColorMapOp;
import org.geotools.util.SoftValueHashMap;
/**
* Allows access to palettes (implemented as {@link IndexColorModel} classes)
*
* @author Andrea Aime - TOPP
* @author Simone Giannecchini - GeoSolutions
*
*/
public class PaletteManager {
private static final Logger LOG = org.geotools.util.logging.Logging.getLogger("PaletteManager");
/**
* The key used in the format options to specify what quantizer to use
*/
public static final String QUANTIZER = "quantizer";
/**
* Safe palette, a 6x6x6 color cube, followed by a 39 elements gray scale,
* and a final transparent element. See the internet safe color palette for
* a reference <a href="http://www.intuitive.com/coolweb/colors.html">
*/
public static final String SAFE = "SAFE";
public static final IndexColorModel safePalette = buildDefaultPalette();
static SoftValueHashMap<String, PaletteCacheEntry> paletteCache = new SoftValueHashMap<String, PaletteCacheEntry>();
static SoftValueHashMap<IndexColorModelKey, InverseColorMapOp> opCache = new SoftValueHashMap<IndexColorModelKey, InverseColorMapOp>();
/**
* TODO: we should probably provide the data directory as a constructor
* parameter here
*/
private PaletteManager() {
}
/**
* Loads a PaletteManager
*
* @param name
*
*/
public static IndexColorModel getPalette(String name) throws Exception {
// check for safe paletteInverter
if ("SAFE".equals(name.toUpperCase())) {
return safePalette;
}
// check for cached one, making sure it's not stale
final PaletteCacheEntry entry = (PaletteCacheEntry) paletteCache.get(name);
if (entry != null) {
if (entry.isStale()) {
paletteCache.remove(name);
} else {
return entry.icm;
}
}
// ok, load it. for the moment we load palettes from .png and .gif
// files, but we may want to extend this ability to other file formats
// (Gimp palettes for example), in this case we'll adopt the classic
// plugin approach using either the Spring context of the SPI
// hum... loading the paletteDir could be done once, but then if the
// users
// adds the paletteInverter dir with a running Geoserver, we won't find it
// anymore...
GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class);
Resource palettes = loader.get("palettes");
Set<String> names = new HashSet<String>();
names.addAll(Arrays.asList(new String[] { name + ".gif", name + ".png", name + ".pal",
name + ".tif" }));
List<Resource> paletteFiles = new ArrayList<Resource>();
for (Resource item : palettes.list()) {
if (names.contains(item.name().toLowerCase())) {
paletteFiles.add(item);
}
}
// scan the files found (we may have multiple files with different
// extensions and return the first paletteInverter you find
for (Resource resource : paletteFiles) {
final String fileName = resource.name();
if (fileName.endsWith("pal")) {
final IndexColorModel icm = new PALFileLoader(resource).getIndexColorModel();
if (icm != null) {
paletteCache.put(name, new PaletteCacheEntry(resource, icm));
return icm;
}
} else {
ImageInputStream iis = ImageIO.createImageInputStream(resource.in());
final Iterator it = ImageIO.getImageReaders(iis);
if (it.hasNext()) {
final ImageReader reader = (ImageReader) it.next();
reader.setInput(iis);
final ColorModel cm = ((ImageTypeSpecifier) reader.getImageTypes(0).next())
.getColorModel();
if (cm instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) cm;
paletteCache.put(name, new PaletteCacheEntry(resource, icm));
return icm;
}
}
}
LOG.warning("Skipping paletteInverter file " + fileName
+ " since color model is not indexed (no 256 colors paletteInverter)");
}
return null;
}
public static InverseColorMapOp getInverseColorMapOp(IndexColorModel icm) {
// check for cached one, making sure it's not stale
IndexColorModelKey key = new IndexColorModelKey(icm);
InverseColorMapOp op = (InverseColorMapOp) opCache.get(key);
if (op != null) {
return op;
} else {
op = new InverseColorMapOp(icm);
opCache.put(key, op);
return op;
}
}
/**
* Builds the internet safe paletteInverter
*/
static IndexColorModel buildDefaultPalette() {
int[] cmap = new int[256];
// Create the standard 6x6x6 color cube (all elements do cycle
// between 00, 33, 66, 99, CC and FF, the decimal difference is 51)
// The color is made of alpha, red, green and blue, in this order, from
// the most significant bit onwards.
int i = 0;
int opaqueAlpha = 255 << 24;
for (int r = 0; r < 256; r += 51) {
for (int g = 0; g < 256; g += 51) {
for (int b = 0; b < 256; b += 51) {
cmap[i] = opaqueAlpha | (r << 16) | (g << 8) | b;
i++;
}
}
}
// The gray scale. Make sure we end up with gray == 255
int grayIncr = 256 / (255 - i);
int gray = 255 - ((255 - i - 1) * grayIncr);
for (; i < 255; i++) {
cmap[i] = opaqueAlpha | (gray << 16) | (gray << 8) | gray;
gray += grayIncr;
}
// setup the transparent color (alpha == 0)
cmap[255] = (255 << 16) | (255 << 8) | 255;
// create the color model
return new IndexColorModel(8, 256, cmap, 0, true, 255,
DataBuffer.TYPE_BYTE);
}
/**
* An entry in the paletteInverter cache. Can determine wheter it's stale or not,
* too
*/
private static class PaletteCacheEntry {
Resource file;
long lastModified;
IndexColorModel icm;
public PaletteCacheEntry(Resource file,
IndexColorModel icm) {
this.file = file;
this.icm = icm;
this.lastModified = file.lastmodified();
}
/**
* Returns true if the backing file does not exist any more, or has been
* modified
*
*
*/
public boolean isStale() {
return !Resources.exists(file) || (file.lastmodified() != lastModified);
}
}
/**
* IndexColorModel has a broken hashcode implementation (inherited from ColorModel
* and not overridden), use a custom key that leverages identity hash code instead
* (a full equals would be expensive, palettes can have 65k entries)
*/
private static class IndexColorModelKey {
IndexColorModel icm;
public IndexColorModelKey(IndexColorModel icm) {
this.icm = icm;
}
@Override
public int hashCode() {
return System.identityHashCode(icm);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
IndexColorModelKey other = (IndexColorModelKey) obj;
return icm == other.icm;
}
}
}