/* * 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.image.io; import java.awt.image.*; import java.awt.Dimension; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.io.IOException; import java.io.FileNotFoundException; import javax.imageio.IIOException; import javax.imageio.ImageTypeSpecifier; import javax.swing.JFrame; import org.geotools.resources.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * A set of RGB colors created by a {@linkplain PaletteFactory palette factory} from * a {@linkplain #name}. A palette can creates a {@linkplain ColorModel color model} * (often {@linkplain IndexColorModel indexed}) or an {@linkplain ImageTypeSpecifier * image type specifier} from the RGB colors. The color model is retained by the palette * as a {@linkplain WeakReference weak reference} (<strong>not</strong> as a {@linkplain * java.lang.ref.SoftReference soft reference}) because it may consume up to 256 kilobytes. * The purpose of the weak reference is to share existing instances in order to reduce * memory usage; the purpose is not to provide caching. * * @since 2.4 * @source $URL$ * @version $Id$ * @author Antoine Hnawia * @author Martin Desruisseaux (IRD) */ public abstract class Palette { /** * The originating factory. */ final PaletteFactory factory; /** * The name of this palette. */ protected final String name; /** * The number of bands in the {@linkplain ColorModel color model}. * The value is 1 in the vast majority of cases. */ protected final int numBands; /** * The band to display, in the range 0 inclusive to {@link #numBands} exclusive. * This is used when an image contains more than one band but only one band can * be used for computing the colors to display. For example {@link IndexColorModel} * works on only one band. */ protected final int visibleBand; /** * The sample model to be given to {@link ImageTypeSpecifier}. */ private transient SampleModel samples; /** * A weak reference to the color model. This color model may consume a significant * amount of memory (up to 256 kb). Consequently, we will prefer {@link WeakReference} * over {@link java.lang.ref.SoftReference}. The purpose of this weak reference is to * share existing instances, not to cache it since it is cheap to rebuild. */ private transient Reference<ColorModel> colors; /** * A weak reference to the image specifier to be returned by {@link #getImageTypeSpecifier}. * We use weak reference because the image specifier contains a reference to the color model * and we don't want to prevent it to be garbage collected. See {@link #colors} for an * explanation about why we use weak instead of soft references. */ private transient Reference<ImageTypeSpecifier> specifier; /** * Creates a palette with the specified name. * * @param factory The originating factory. * @param name The palette name. * @param numBands The number of bands (usually 1) to assign to {@link #numBands}. * @param visibleBand The visible band (usually 0) to assign to {@link #visibleBand}. */ protected Palette(final PaletteFactory factory, final String name, final int numBands, final int visibleBand) { if (factory == null) { // Can't use factory.getErrorResources() here. throw new IllegalArgumentException(Errors.format( ErrorKeys.NULL_ARGUMENT_$1, "factory")); } if (name == null) { throw new IllegalArgumentException(factory.getErrorResources().getString( ErrorKeys.NULL_ARGUMENT_$1, "name")); } ensureInsideBounds(numBands, 0, 255); // This maximal value is somewhat arbitrary. ensureInsideBounds(visibleBand, 0, numBands-1); this.factory = factory; this.name = name.trim(); this.numBands = numBands; this.visibleBand = visibleBand; } /** * Ensures that the specified values in inside the expected bounds (inclusives). * * @throws IllegalArgumentException if the specified values are outside the bounds. */ final void ensureInsideBounds(final int value, final int min, final int max) throws IllegalArgumentException { if (value < min || value > max) { throw new IllegalArgumentException(factory.getErrorResources().getString( ErrorKeys.VALUE_OUT_OF_BOUNDS_$3, value, min, max)); } } /** * Returns the scale from <cite>normalized values</cite> (values in the range [0..1]) * to values in the range of this palette. */ double getScale() { return 1; } /** * Returns the offset from <cite>normalized values</cite> (values in the range [0..1]) * to values in the range of this palette. */ double getOffset() { return 0; } /** * Returns the color model for this palette. This method tries to reuse existing * color model if possible, since it may consume a significant amount of memory. * * @throws FileNotFoundException If the RGB values need to be read from a file and this file * (typically inferred from {@link #name}) is not found. * @throws IOException If an other find of I/O error occured. * @throws IIOException If an other kind of error prevent this method to complete. */ public synchronized ColorModel getColorModel() throws IOException { if (colors != null) { final ColorModel candidate = colors.get(); if (candidate != null) { return candidate; } } return getImageTypeSpecifier().getColorModel(); } /** * Returns the image type specifier for this palette. * * @throws FileNotFoundException If the RGB values need to be read from a file and this file * (typically inferred from {@link #name}) is not found. * @throws IOException If an other find of I/O error occured. * @throws IIOException If an other kind of error prevent this method to complete. */ public abstract ImageTypeSpecifier getImageTypeSpecifier() throws IOException; /** * Returns the image type specifier from the cache, or {@code null}. */ final ImageTypeSpecifier queryCache() { if (specifier != null) { final ImageTypeSpecifier candidate = specifier.get(); if (candidate != null) { return candidate; } } if (samples!=null && colors!=null) { final ColorModel candidate = colors.get(); if (candidate != null) { final ImageTypeSpecifier its = new ImageTypeSpecifier(candidate, samples); specifier = new WeakReference<ImageTypeSpecifier>(its); return its; } } return null; } /** * Puts the specified image specifier in the cache. */ final void cache(final ImageTypeSpecifier its) { samples = its.getSampleModel(); colors = new PaletteDisposer.Reference(this, its.getColorModel()); specifier = new WeakReference<ImageTypeSpecifier>(its); } /** * Returns the color palette as an image of the specified size. * This is useful for looking visually at a color palette. * * @param size The image size. The palette will be vertical if * <code>size.{@linkplain Dimension#height height}</code> > * <code>size.{@linkplain Dimension#width width }</code> * * @throws IOException if the color values can't be read. */ public RenderedImage getImage(final Dimension size) throws IOException { final IndexColorModel colors; final BufferedImage image; final WritableRaster raster; colors = (IndexColorModel) getColorModel(); raster = colors.createCompatibleWritableRaster(size.width, size.height); image = new BufferedImage(colors, raster, false, null); int xmin = raster.getMinX(); int ymin = raster.getMinY(); int width = raster.getWidth(); int height = raster.getHeight(); final boolean horizontal = size.width >= size.height; // Computation will be performed as if the image were horizontal. // If it is not, interchanges x and y values. if (!horizontal) { int tmp; tmp = xmin; xmin = ymin; ymin = tmp; tmp = width; width = height; height = tmp; } final int xmax = xmin + width; final int ymax = ymin + height; final double scale = getScale() / width; final double offset = getOffset(); for (int x=xmin; x<xmax; x++) { final double value = offset + scale*(x-xmin); for (int y=ymin; y<ymax; y++) { if (horizontal) { raster.setSample(x, y, 0, value); } else { raster.setSample(y, x, 0, value); } } } return image; } /** * Shows the palette in a windows. This is mostly for debugging purpose. * * @throws IOException if the color values can't be read. */ @SuppressWarnings("deprecation") public void show() throws IOException { final JFrame frame = new JFrame(toString()); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.add(new javax.media.jai.widget.ImageCanvas(getImage(new Dimension(256, 32)))); frame.pack(); frame.setVisible(true); } /** * Returns a hash value for this palette. */ @Override public int hashCode() { return name.hashCode() + 37*numBands + 17*visibleBand; } /** * Compares this palette with the specified object for equality. */ @Override public boolean equals(final Object object) { if (object != null && getClass().equals(object.getClass())) { final Palette that = (Palette) object; return this.numBands == that.numBands && this.visibleBand == that.visibleBand && Utilities.equals(this.name, that.name); /* * Note: we do not compare PaletteFactory on purpose, since two instances could be * identical except for the locale to use for formatting error messages. Because * Palettes are used as keys in the PaletteFactory.palettes pool, we don't want to * get duplicated palettes only because they format error messages differently. */ } return false; } }