/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.image.palette;
import java.util.Objects;
import java.awt.image.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Dimension;
import java.awt.geom.Rectangle2D;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.io.IOException;
import java.io.FileNotFoundException;
import javax.imageio.ImageTypeSpecifier;
import org.geotoolkit.resources.Errors;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.NullArgumentException;
/**
* 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.
*
* {@section Sharing <code>IndexColorModel</code> instances}
* 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.
*
* @author Martin Desruisseaux (IRD)
* @author Antoine Hnawia (IRD)
* @author Quentin Boileau (Geomatys)
* @version 3.21
*
* @since 2.4
* @module
*/
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)
{
ArgumentChecks.ensureNonNull("factory", factory); // Can't use factory.getErrorResources() here.
if (name == null) {
throw new NullArgumentException(factory.getErrorResources().getString(
Errors.Keys.NullArgument_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(
Errors.Keys.ValueOutOfBounds_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.
*
* @return The color model 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 kind of I/O error occurred.
*/
public synchronized ColorModel getColorModel() throws FileNotFoundException, 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. The default implementation first check
* if the specified still in the cache. If not, then the {@link #createImageTypeSpecifier()}
* method is invoked and its result is stored in the cache for future reuse.
*
* @return The image type specified 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 kind of I/O error occurred.
*/
public synchronized ImageTypeSpecifier getImageTypeSpecifier() throws FileNotFoundException, IOException {
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<>(its);
return its;
}
}
final ImageTypeSpecifier its = createImageTypeSpecifier();
samples = its.getSampleModel();
colors = new PaletteDisposer(this, its.getColorModel());
specifier = new WeakReference<>(its);
return its;
}
/**
* Creates a new image type specifier for this palette. This method is invoked by
* {@link #getImageTypeSpecifier()} when the specifier is not present in the cache.
*
* @return The image type specified 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 kind of I/O error occurred.
*
* @since 3.11
*/
protected abstract ImageTypeSpecifier createImageTypeSpecifier() throws FileNotFoundException, IOException;
/**
* Returns the color palette as an image of the specified size.
* This is useful for looking visually at a color palette.
* <p>
* This method uses the color model created by {@link #getColorModel()} and does not write
* any text in the image. Consequently the colors in the returned image should be identical
* to the colors of the data rendered using this {@code 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>
* @return The color palette as an image of the given size.
* @throws IOException if the color values can't be read.
*/
public RenderedImage getImage(final Dimension size) throws IOException {
final IndexColorModel colors = (IndexColorModel) getColorModel();
final WritableRaster raster = colors.createCompatibleWritableRaster(size.width, size.height);
final BufferedImage 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;
}
/**
* Returns the color palette as a smoothed image of the specified size, together with the
* palette name. The image returned by this method differs from {@link #getImage(Dimension)}
* in three ways:
* <p>
* <ul>
* <li>The image returned by this method uses a different color model than the images to be
* produced by this {@code Palette}.</li>
* <li>The colors in this image are interpolated in order to produce a smooth image.
* Consequently, some colors in the returned image may not exist in the palette.</li>
* <li>The palette name is written in the returned image.</li>
* </ul>
* <p>
* <strong>WARNING: Some colors in the returned image may be misleading - they may have no
* geophysical meaning.</strong> Consider for instance an image of Sea Surface Temperature
* with colors ranging from blue (cold) to red (hot). The correct way to interpolate is to
* first interpolate the temperature, then use the color map for fetching the color associated
* to that temperature. With this approach, interpolation between cold and hot water will
* produce warm water, often displayed in yellow or green. But {@code getLegend} method will
* produce purple colors - because it performed the interpolation in ARGB space - which is
* wrong.
*
* @param size The image size.
* @return The color palette as an image of the given size.
* @throws IOException if the color values can't be read.
*
* @since 3.21
*/
public RenderedImage getLegend(final Dimension size) throws IOException {
final BufferedImage image = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g = (Graphics2D) image.getGraphics();
final Color[] colors = factory.getColors(name);
final float[] fractions = new float[colors.length];
for (int i=0; i<colors.length; i++) {
fractions[i] = (float) i / (colors.length - 1);
}
final float centerY = size.height * 0.5f;
g.setPaint(new LinearGradientPaint(
0, centerY, // The gradiant axis start in user space.
size.width, centerY, // The gradient axis end in user space.
fractions, // Numbers ranging from 0 to 1 specifying the distribution of colors along the grandient.
colors)); // Colors corresponding to each fractional values.
g.fillRect(0, 0, size.width, size.height);
g.setColor(Color.WHITE);
final Font font = new Font("Dialog", Font.BOLD, 13);
final Rectangle2D nameBounds = g.getFontMetrics(font).getStringBounds(name, g);
g.setFont(font);
g.drawString(name,
(float) (size.width - nameBounds.getWidth()) / 2,
(float) (size.height + nameBounds.getHeight()) / 2);
g.dispose();
return image;
}
/**
* Returns a hash value for this palette. See {@link #equals(Object)} for information
* about which attributes can be used in the computation.
*/
@Override
public int hashCode() {
return name.hashCode() + 31*numBands + visibleBand;
}
/**
* Compares this palette with the specified object for equality. This method shall compare only
* the values given to the constructor. It shall not trig the {@link ColorModel} construction,
* because this {@code equals} method is used by {@link PaletteFactory#palettes} in order to
* check if an existing {@code Palette} instance can be reused.
*
* @param object The object to compare with this palette for equality.
* @return {@code true} if the given object is equal to this palette.
*/
@Override
public boolean equals(final Object object) {
if (object != null && getClass() == object.getClass()) {
final Palette that = (Palette) object;
return this.numBands == that.numBands &&
this.visibleBand == that.visibleBand &&
Objects.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;
}
}