/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-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.image.io;
import java.awt.Color;
import java.awt.image.IndexColorModel; // For javadoc
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.ParseException;
import javax.imageio.IIOException;
import javax.imageio.spi.ServiceRegistry;
import java.util.*;
import org.geotools.io.DefaultFileFilter;
import org.geotools.io.LineFormat;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.IndexedResourceBundle;
import org.geotools.util.logging.Logging;
import org.geotools.util.CanonicalSet;
/**
* A factory for {@linkplain IndexColorModel index color models} created from RGB values listed
* in files. The palette definition files are text files containing an arbitrary number of lines,
* each line containing RGB components ranging from 0 to 255 inclusive. An optional fourth column
* may be provided for alpha components. Empty lines and lines starting with the {@code '#'}
* character are ignored. Example:
*
* <blockquote><pre>
* # RGB codes for SeaWiFs images
* # (chlorophylle-a concentration)
*
* 033 000 096
* 032 000 097
* 031 000 099
* 030 000 101
* 029 000 102
* 028 000 104
* 026 000 106
* 025 000 107
* <cite>etc...</cite>
* </pre></blockquote>
*
* The number of RGB codes doesn't have to match the target {@linkplain IndexColorModel#getMapSize
* color map size}. RGB codes will be automatically interpolated as needed.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class PaletteFactory {
/**
* The file which contains a list of available color palettes. This file is optional
* and used only in last resort, since scanning a directory content is more reliable.
* If such file exists in the same directory than the one that contains the palettes,
* this file will be used by {@link #getAvailableNames}.
*/
private static final String LIST_FILE = "list.txt";
/**
* The default sub-directory, relative to the {@code PaletteFactory} class directory.
*/
private static final File DEFAULT_DIRECTORY = new File("colors");
/**
* The default palette factory.
*/
private static PaletteFactory defaultFactory;
/**
* The fallback factory, or {@code null} if there is none. The fallback factory
* will be queried if a palette was not found in current factory.
* <p>
* This field should be considered as final. It is modified by {@link #scanForPlugins} only.
*/
private PaletteFactory fallback;
/**
* The class loader from which to load the palette definition files. If {@code null} and
* {@link #loader} is null as well, then loading will occurs from the system current
* working directory.
*/
private final ClassLoader classloader;
/**
* An alternative to {@link #classloader} for loading resources. At most one of
* {@code classloader} and {@code loader} can be non-null. If both are {@code null},
* then loading will occurs from the system current working directory.
*/
private final Class<?> loader;
/**
* The base directory from which to search for palette definition files.
* If {@code null}, then the working directory ({@code "."}) is assumed.
*/
private final File directory;
/**
* The file extension.
*/
private final String extension;
/**
* The charset to use for parsing files, or {@code null} for the current default.
*/
private final Charset charset;
/**
* The locale to use for parsing files, or {@code null} for the current default.
*/
private final Locale locale;
/**
* The locale to use for formatting error messages, or {@code null} for the current default.
* This locale is informative only; there is no garantee that this locale will be really used.
*/
private transient ThreadLocal<Locale> warningLocales;
/**
* The set of palettes already created.
*/
private final CanonicalSet<Palette> palettes = CanonicalSet.newInstance(Palette.class);
/**
* The set of palettes protected from garbage collection. We protect a palette as long as it
* holds a reference to a color model - this is necessary in order to prevent multiple creation
* of the same {@link IndexColorModel}. The references are cleaned by {@link PaletteDisposer}.
*/
final Set<Palette> protectedPalettes = new HashSet<Palette>();
/**
* Gets the default palette factory. This method creates a default instance looking for
* {@code org/geotools/image/io/colors/*.pal} files where {@code '*'} is a palette name.
* Next, this method {@linkplain #scanForPlugins scan for plugins} using the default
* class loader. The result is cached for subsequent calls to this {@code getDefault()}
* method.
*/
public synchronized static PaletteFactory getDefault() {
if (defaultFactory == null) {
defaultFactory = new PaletteFactory();
scanForPlugins(null);
}
return defaultFactory;
}
/**
* Lookups for additional palette factories on the classpath. The palette factories shall
* be declared in {@code META-INF/services/org.geotools.image.io.PaletteFactory} files.
* <p>
* Palette factories found are added to the chain of default factories. The next time that
* a <code>{@linkplain #getDefault()}.getPalette(...)</code> method will be invoked, the
* scanned factories will be tried first. If they can't create a given palette, then the
* Geotools default factory will be tried last.
* <p>
* It is usually not needed to invoke this method directly since it is invoked automatically
* by {@link #getDefault()} when first needed. This method may be useful when a specific class
* loader need to be used, or when the classpath content changed.
*
* @param loader The class loader to use, or {@code null} for the default one.
*
* @since 2.4
*/
public synchronized static void scanForPlugins(final ClassLoader loader) {
final Set<Class<? extends PaletteFactory>> existings = new HashSet<Class<? extends PaletteFactory>>();
for (PaletteFactory p=getDefault(); p!=null; p=p.fallback) {
existings.add(p.getClass());
}
final Iterator<? extends PaletteFactory> it = (loader == null) ?
ServiceRegistry.lookupProviders(PaletteFactory.class) :
ServiceRegistry.lookupProviders(PaletteFactory.class, loader);
while (it.hasNext()) {
/*
* Adds the scanned factory to the chain. There is no public method for doing that
* because PaletteFactory is quasi-immutable except for this method which modifies
* the fallback field. It is okay in this context since we just created the factory
* instance.
*/
final PaletteFactory factory = it.next();
if (existings.add(factory.getClass())) {
PaletteFactory tail = factory;
while (tail.fallback != null) {
tail = tail.fallback;
}
tail.fallback = defaultFactory;
defaultFactory = factory;
}
}
}
/**
* Constructs a default palette factory using this {@linkplain #getClass object class} for
* loading palette definition files. The default directory is {@code "colors"} relative to
* the directory of the subclass extending this class. The character encoding is ISO-8859-1
* and the locale is {@linkplain Locale#US US}.
* <p>
* This constructor is protected because is it merely a convenience for subclasses registering
* themself as a service in the {@code META-INF/services/org.geotools.image.io.PaletteFactory}
* file. Users should invoke {@link #getDefault} instead, which will returns a shared instance
* of this class together with any custom factories found on the class path.
*
* @since 2.5
*/
protected PaletteFactory() {
this.classloader = null;
this.loader = getClass();
this.directory = DEFAULT_DIRECTORY;
this.extension = ".pal";
this.charset = Charset.forName("ISO-8859-1");
this.locale = Locale.US;
}
/**
* Constructs a palette factory using loading palette definition files in a specific directory.
* No {@linkplain ClassLoader class loader} is used for loading the files.
*
* @param directory The base directory for palette definition files relative to current
* directory, or {@code null} for {@code "."}.
* @param extension File name extension, or {@code null} if there is no extension
* to add to filename. If non-null, this extension will be automatically
* appended to filename. It should starts with the {@code '.'} character.
* @param charset The charset to use for parsing files, or {@code null} for the default.
* @param locale The locale to use for parsing files, or {@code null} for the default.
*
* @since 2.5
*/
public PaletteFactory(final File directory,
final String extension,
final Charset charset,
final Locale locale)
{
this.classloader = null;
this.loader = null;
this.directory = directory;
this.extension = startWithDot(extension);
this.charset = charset;
this.locale = locale;
}
/**
* Constructs a palette factory using an optional {@linkplain ClassLoader class loader}
* for loading palette definition files.
*
* @param fallback An optional fallback factory, or {@code null} if there is none. The fallback
* factory will be queried if a palette was not found in the current factory.
* @param loader An optional class loader to use for loading the palette definition files.
* If {@code null}, loading will occurs from the system current working
* directory.
* @param directory The base directory for palette definition files. It may be a Java package
* if a {@code loader} were specified. If {@code null}, then {@code "."} is
* assumed.
* @param extension File name extension, or {@code null} if there is no extension
* to add to filename. If non-null, this extension will be automatically
* appended to filename. It should starts with the {@code '.'} character.
* @param charset The charset to use for parsing files, or {@code null} for the default.
* @param locale The locale to use for parsing files, or {@code null} for the default.
*/
public PaletteFactory(final PaletteFactory fallback,
final ClassLoader loader,
final File directory,
final String extension,
final Charset charset,
final Locale locale)
{
this.fallback = fallback;
this.classloader = loader;
this.loader = null;
this.directory = directory;
this.extension = startWithDot(extension);
this.charset = charset;
this.locale = locale;
}
/**
* Constructs a palette factory using an optional {@linkplain Class class} for loading
* palette definition files. Using a {@linkplain Class class} instead of a {@linkplain
* ClassLoader class loader} can avoid security issue on some platforms (some platforms
* do not allow to load resources from a {@code ClassLoader} because it can load from the
* root package).
*
* @param fallback An optional fallback factory, or {@code null} if there is none. The fallback
* factory will be queried if a palette was not found in the current factory.
* @param loader An optional class to use for loading the palette definition files.
* If {@code null}, loading will occurs from the system current working
* directory.
* @param directory The base directory for palette definition files. It may be a Java package
* if a {@code loader} were specified. If {@code null}, then {@code "."} is
* assumed.
* @param extension File name extension, or {@code null} if there is no extension
* to add to filename. If non-null, this extension will be automatically
* appended to filename. It should starts with the {@code '.'} character.
* @param charset The charset to use for parsing files, or {@code null} for the default.
* @param locale The locale to use for parsing files. or {@code null} for the default.
*
* @since 2.2
*/
public PaletteFactory(final PaletteFactory fallback,
final Class<?> loader,
final File directory,
final String extension,
final Charset charset,
final Locale locale)
{
this.fallback = fallback;
this.classloader = null;
this.loader = loader;
this.directory = directory;
this.extension = startWithDot(extension);
this.charset = charset;
this.locale = locale;
}
/**
* Ensures that the given string starts with a dot.
*/
private static String startWithDot(String extension) {
if (extension != null && !extension.startsWith(".")) {
extension = '.' + extension;
}
return extension;
}
/**
* Sets the locale to use for formatting warning or error messages. This is typically the
* {@linkplain javax.imageio.ImageReader#getLocale image reader locale}. This locale is
* informative only; there is no garantee that this locale will be really used.
* <p>
* This method sets the locale for the current thread only. It is safe to use this palette
* factory concurrently in many threads, each with their own locale.
*
* @param warningLocale The locale for warning or error messages, or {@code null} for the
* default locale
*
* @since 2.4
*/
public synchronized void setWarningLocale(final Locale warningLocale) {
if (warningLocales == null) {
if (warningLocale == null) {
return;
}
warningLocales = warningLocales();
}
// TODO: use 'remove' on warningLocale==null when we will be allowed to compile for J2SE 1.5.
warningLocales.set(warningLocale);
}
/**
* Gets the {@linkplain #warningLocales} from the fallback or create a new one. This
* method invokes itself recursively in order to assign the same {@link ThreadLocal}
* to every factories in the chain.
*/
private synchronized ThreadLocal<Locale> warningLocales() {
if (warningLocales == null) {
warningLocales = (fallback != null) ? fallback.warningLocales() : new ThreadLocal<Locale>();
}
return warningLocales;
}
/**
* Returns the locale set by the last invocation to {@link #setWarningLocale} in the
* current thread.
*
* @since 2.4
*/
public Locale getWarningLocale() {
final ThreadLocal<Locale> warningLocales = this.warningLocales;
// Protected 'warningLocales' from changes so there is no need to synchronize.
if (warningLocales != null) {
return warningLocales.get();
}
return null;
}
/**
* Returns the resources for formatting error messages.
*/
final IndexedResourceBundle getErrorResources() {
return Errors.getResources(getWarningLocale());
}
/**
* Returns an input stream for reading the specified resource. The default
* implementation delegates to the {@link Class#getResourceAsStream(String) Class} or
* {@link ClassLoader#getResourceAsStream(String) ClassLoader} method of the same name,
* according the {@code loader} argument type given to the constructor. Subclasses may
* override this method if a more elaborated mechanism is wanted for fetching resources.
* This is sometime required in the context of applications using particular class loaders.
*
* @param name The name of the resource to load, constructed as {@code directory} + {@code name}
* + {@code extension} where <var>directory</var> and <var>extension</var> were
* specified to the constructor, while {@code name} was given to the
* {@link #getPalette} method.
* @return The input stream, or {@code null} if the resources was not found.
*
* @since 2.3
*/
protected InputStream getResourceAsStream(final String name) {
if (loader != null) {
return loader.getResourceAsStream(name);
}
if (classloader != null) {
return classloader.getResourceAsStream(name);
}
return null;
}
/**
* Returns the list of available palette names. Any item in this list can be specified as
* argument to {@link #getPalette}.
*
* @return The list of available palette name, or {@code null} if this method
* is unable to fetch this information.
*/
public String[] getAvailableNames() {
final Set<String> names = new TreeSet<String>();
PaletteFactory factory = this;
do {
factory.getAvailableNames(names);
factory = factory.fallback;
} while (factory != null);
return names.toArray(new String[names.size()]);
}
/**
* Adds available palette names to the specified collection.
*/
private void getAvailableNames(final Collection<String> names) {
/*
* First, parses the content of every "list.txt" files found on the classpath. Those files
* are optional. But if they are present, we assume that their content are accurate.
*/
String filename = new File(directory, LIST_FILE).getPath();
BufferedReader in = getReader(LIST_FILE, "getAvailableNames");
try {
if (in != null) {
readNames(in, names);
}
if (classloader != null) {
for (final Enumeration<URL> it=classloader.getResources(filename); it.hasMoreElements();) {
final URL url = it.nextElement();
in = getReader(url.openStream());
readNames(in, names);
}
}
} catch (IOException e) {
/*
* Logs a warning but do not stop. The only consequence is that the names list
* will be incomplete. We log the message as if came from getAvailableNames(),
* which is the public method that invoked this one.
*/
Logging.unexpectedException(PaletteFactory.class, "getAvailableNames", e);
}
/*
* After the "list.txt" files, check if the resources can be read as a directory.
* It may happen if the classpath point toward a directory of .class files rather
* than a JAR file.
*/
File dir = (directory != null) ? directory : new File(".");
if (classloader != null) {
dir = toFile(classloader.getResource(dir.getPath()));
if (dir == null) {
// Directory not found.
return;
}
} else if (loader != null) {
dir = toFile(loader.getResource(dir.getPath()));
if (dir == null) {
// Directory not found.
return;
}
}
if (!dir.isDirectory()) {
return;
}
final String[] list = dir.list(new DefaultFileFilter('*' + extension));
final int extLg = extension.length();
for (int i=0; i<list.length; i++) {
filename = list[i];
final int lg = filename.length();
if (lg>extLg && filename.regionMatches(true, lg-extLg, extension, 0, extLg)) {
names.add(filename.substring(0, lg-extLg));
}
}
}
/**
* Copies the content of the specified reader to the specified collection.
* The reader is closed after this operation.
*/
private static void readNames(final BufferedReader in, final Collection<String> names)
throws IOException
{
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.length() != 0 && line.charAt(0) != '#') {
names.add(line);
}
}
in.close();
}
/**
* Transforms an {@link URL} into a {@link File}. If the URL can't be
* interpreted as a file, then this method returns {@code null}.
*/
private static File toFile(final URL url) {
if (url!=null && url.getProtocol().equalsIgnoreCase("file")) {
return new File(url.getPath());
}
return null;
}
/**
* Returns a buffered reader for the specified palette.
*
* @param The palette's name to load. This name doesn't need to contains a path
* or an extension. Path and extension are set according value specified
* at construction time.
* @return A buffered reader to read {@code name}, or {@code null} if the resource is not found.
*/
private LineNumberReader getPaletteReader(String name) {
if (extension!=null && !name.endsWith(extension)) {
name += extension;
}
return getReader(name, "getPalette");
}
/**
* Returns a buffered reader for the specified filename.
*
* @param The filename. Path and extension are set according value specified
* at construction time.
* @return A buffered reader to read {@code name}, or {@code null} if the resource is not found.
*/
private LineNumberReader getReader(final String name, final String caller) {
final File file = new File(directory, name);
final String path = file.getPath().replace(File.separatorChar, '/');
InputStream stream;
try {
stream = getResourceAsStream(path);
if (stream == null) {
if (file.canRead()) try {
stream = new FileInputStream(file);
} catch (FileNotFoundException e) {
/*
* Should not occurs, since we checked for file existence. This is not a fatal
* error however, since this method is allowed to returns null if the resource
* is not available.
*/
Logging.unexpectedException(PaletteFactory.class, caller, e);
return null;
} else {
return null;
}
}
} catch (SecurityException e) {
Logging.recoverableException(PaletteFactory.class, caller, e);
return null;
}
return getReader(stream);
}
/**
* Wraps the specified input stream into a reader.
*/
private LineNumberReader getReader(final InputStream stream) {
return new LineNumberReader((charset != null) ?
new InputStreamReader(stream, charset) : new InputStreamReader(stream));
}
/**
* Reads the colors declared in the specified input stream. Colors must be encoded on 3 or 4
* columns. If 3 columns, it is assumed RGB values. If 4 columns, it is assumed RGBA values.
* Values must be in the 0-255 ranges. Empty lines and lines starting by {@code '#'} are
* ignored.
*
* @param input The stream to read.
* @param name The palette name to read. Used for formatting error message only.
* @return The colors.
* @throws IOException if an I/O error occured.
* @throws IIOException if a syntax error occured.
*/
@SuppressWarnings("fallthrough")
private Color[] getColors(final LineNumberReader input, final String name) throws IOException {
int values[] = null;
final LineFormat reader = (locale!=null) ? new LineFormat(locale) : new LineFormat();
final List<Color> colors = new ArrayList<Color>();
String line; while ((line=input.readLine()) != null) try {
line = line.trim();
if (line.length() == 0) continue;
if (line.charAt(0) == '#') continue;
if (reader.setLine(line) == 0) continue;
values = reader.getValues(values);
int A=255,R,G,B;
switch (values.length) {
case 4: A = byteValue(values[3]); // fall through
case 3: B = byteValue(values[2]);
G = byteValue(values[1]);
R = byteValue(values[0]);
break;
default: {
throw syntaxError(input, name, null);
}
}
final Color color;
try {
color = new Color(R, G, B, A);
} catch (IllegalArgumentException exception) {
/*
* Color constructor checks the RGBA value and throws an IllegalArgumentException
* if they are not in the 0-255 range. Intercept this exception and rethrows as a
* checked IIOException, since we want to notify the user that the palette file is
* badly formatted. (additional note: it is somewhat redundant with byteValue(int)
* work. Lets keep it as a safety).
*/
throw syntaxError(input, name, exception);
}
colors.add(color);
} catch (ParseException exception) {
throw syntaxError(input, name, exception);
}
return colors.toArray(new Color[colors.size()]);
}
/**
* Prepares an exception for the specified cause, which may be {@code null}.
*/
private IIOException syntaxError(final LineNumberReader input, final String name, final Exception cause) {
String message = getErrorResources().getString(
ErrorKeys.BAD_LINE_IN_FILE_$2, name, input.getLineNumber());
if (cause != null) {
message += cause.getLocalizedMessage();
}
return new IIOException(message, cause);
}
/**
* Loads colors from a definition file. If no colors were found in the current palette
* factory and a fallback was specified at construction time, then the fallback will
* be queried.
*
* @param name The palette's name to load. This name doesn't need to contains a path
* or an extension. Path and extension are set according value specified
* at construction time.
* @return The set of colors, or {@code null} if the set was not found.
* @throws IOException if an error occurs during reading.
* @throws IIOException if an error occurs during parsing.
*/
public Color[] getColors(final String name) throws IOException {
final LineNumberReader reader = getPaletteReader(name);
if (reader == null) {
return (fallback != null) ? fallback.getColors(name) : null;
}
final Color[] colors = getColors(reader, name);
reader.close();
return colors;
}
/**
* Ensures that the specified valus is inside the {@code [0..255]} range.
* If the value is outside that range, a {@link ParseException} is thrown.
*/
private int byteValue(final int value) throws ParseException {
if (value>=0 && value<256) {
return value;
}
throw new ParseException(getErrorResources().getString(
ErrorKeys.RGB_OUT_OF_RANGE_$1, value), 0);
}
/**
* Returns the palette of the specified name and size. The palette's name doesn't need
* to contains a directory path or an extension. Path and extension are set according
* values specified at construction time.
*
* @param name The palette's name to load.
* @param size The {@linkplain IndexColorModel index color model} size.
* @return The palette.
*
* @since 2.4
*/
public Palette getPalette(final String name, final int size) {
return getPalette(name, 0, size, size, 1, 0);
}
/**
* Returns a palette with a <cite>pad value</cite> at index 0.
*
* @param name The palette's name to load.
* @param size The {@linkplain IndexColorModel index color model} size.
* @return The palette.
*
* @since 2.4
*/
public Palette getPalettePadValueFirst(final String name, final int size) {
return getPalette(name, 1, size, size, 1, 0);
}
/**
* Returns a palette with <cite>pad value</cite> at the last index.
*
* @param name The palette's name to load.
* @param size The {@linkplain IndexColorModel index color model} size.
* @return The palette.
*
* @since 2.4
*/
public Palette getPalettePadValueLast(final String name, final int size) {
return getPalette(name, 0, size-1, size, 1, 0);
}
/**
* Returns the palette of the specified name and size. The RGB colors will be distributed
* in the range {@code lower} inclusive to {@code upper} exclusive. Remaining pixel values
* (if any) will be left to a black or transparent color by default.
* <p>
* The palette's name doesn't need to contains a directory path or an extension.
* Path and extension are set according values specified at construction time.
*
* @param name The palette's name to load.
* @param lower Index of the first valid element (inclusive) in the
* {@linkplain IndexColorModel index color model} to be created.
* @param upper Index of the last valid element (exclusive) in the
* {@linkplain IndexColorModel index color model} to be created.
* @param size The size of the {@linkplain IndexColorModel index color model} to be created.
* This is the value to be returned by {@link IndexColorModel#getMapSize}.
* @param numBands The number of bands (usually 1).
* @param visibleBand The band to use for color computations (usually 0).
* @return The palette.
*
* @since 2.4
*/
public Palette getPalette(final String name, final int lower, final int upper, final int size,
final int numBands, final int visibleBand)
{
Palette palette = new IndexedPalette(this, name, lower, upper, size, numBands, visibleBand);
palette = palettes.unique(palette);
return palette;
}
/**
* Creates a palette suitable for floating point values.
*
* @param name The palette name.
* @param minimum The minimal sample value expected.
* @param maximum The maximal sample value expected.
* @param dataType The data type as a {@link java.awt.image.DataBuffer#TYPE_FLOAT}
* or {@link java.awt.image.DataBuffer#TYPE_DOUBLE} constant.
* @param numBands The number of bands (usually 1).
* @param visibleBand The band to use for color computations (usually 0).
*
* @since 2.4
*
* @todo Current implementation ignores the name and builds a gray scale in all cases.
* Future version may improve on that.
*/
public Palette getContinuousPalette(final String name, final float minimum, final float maximum,
final int dataType, final int numBands, final int visibleBand)
{
Palette palette = new ContinuousPalette(this, name, minimum, maximum, dataType, numBands, visibleBand);
palette = palettes.unique(palette);
return palette;
}
}