/*
* Geotools 2 - OpenSource mapping toolkit
* (C) 2003, Geotools Project Management Committee (PMC)
* (C) 2000, Institut de Recherche pour le Développement
* (C) 1999, Pêches et Océans Canada
*
* 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.geotools.gui.swing;
// Graphical user interface
import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.plaf.ComponentUI;
// Graphics
import java.awt.Font;
import java.awt.Paint;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.IndexColorModel;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
// Geometry
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
// Miscellaneous
import java.util.List;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.media.jai.PropertySource;
import javax.units.Unit;
// OpenGIS dependencies
import org.opengis.coverage.Coverage;
import org.opengis.coverage.SampleDimension;
import org.opengis.coverage.PaletteInterpretation;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
// Axis
import org.geotools.axis.Graduation;
import org.geotools.axis.TickIterator;
import org.geotools.axis.NumberGraduation;
import org.geotools.axis.AbstractGraduation;
import org.geotools.axis.LogarithmicNumberGraduation;
// Geotools dependencies
import org.geotools.coverage.GridSampleDimension;
import org.geotools.resources.Utilities;
import org.geotools.resources.image.CoverageUtilities;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Logging;
import org.geotools.resources.i18n.LoggingKeys;
/**
* A color ramp with a graduation. The colors can be specified with a {@link SampleDimension},
* an array of {@link Color}s or an {@link IndexColorModel} object, and the graduation is
* specified with a {@link Graduation} object. The resulting {@code ColorRamp} object
* is usually painted together with a remote sensing image, for example in a
* {@link org.geotools.gui.swing.MapPane} object.
*
*
*
*
*
* @since 2.2
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*/
public class ColorRamp extends JComponent {
/**
* Margin (in pixel) on each sides: top, left, right and bottom of the color ramp.
*/
private static final int MARGIN = 10;
/**
* An empty list of colors.
*/
private static final Color[] EMPTY = new Color[0];
/**
* The graduation to write over the color ramp.
*/
private Graduation graduation;
/**
* Graduation units. This is constructed from {@link Graduation#getUnit} and cached
* for faster rendering.
*/
private String units;
/**
* The colors to paint (never {@code null}).
*/
private Color[] colors = EMPTY;
/**
* {@code true} if tick label must be display.
*/
private boolean labelVisibles = true;
/**
* {@code true} if tick label can be display with an automatic color. The
* automatic color will be white or black depending the background color.
*/
private boolean autoForeground = true;
/**
* {@code true} if the color bar should be drawn horizontally,
* or {@code false} if it should be drawn vertically.
*/
private boolean horizontal = true;
/**
* Rendering hints for the graduation. This include the color bar
* length, which is used for the space between ticks.
*/
private transient RenderingHints hints;
/**
* The tick iterator used during the last painting. This iterator will be reused as mush
* as possible in order to reduce garbage-collections.
*/
private transient TickIterator reuse;
/**
* A temporary buffer for conversions from RGB to HSB
* values. This is used by {@link #getForeground(int)}.
*/
private transient float[] HSB;
/**
* The {@link ComponentUI} object for computing preferred
* size, drawn the component and handle some events.
*/
private final UI ui = new UI();
/**
* Constructs an initially empty color bar. Colors can be
* set using one of the {@code setColors(...)} methods.
*/
public ColorRamp() {
setOpaque(true);
setUI(ui);
}
/**
* Constructs a color bar for the specified coverage.
*/
public ColorRamp(final Coverage coverage) {
this();
setColors(coverage);
}
/**
* Returns the graduation to paint over colors. If the graduation is
* not yet defined, then this method returns {@code null}.
*/
public Graduation getGraduation() {
return graduation;
}
/**
* Set the graduation to paint on top of the color bar. The graduation can be set also
* by a call to {@link #setColors(SampleDimension)} and {@link #setColors(Coverage)}.
* This method will fire a property change event with the {@code "graduation"} name.
*
* @param graduation The new graduation, or {@code null} if none.
* @return {@code true} if this object changed as a result of this call.
*/
public boolean setGraduation(final Graduation graduation) {
final Graduation oldGraduation = this.graduation;
if (graduation != oldGraduation) {
if (oldGraduation != null) {
oldGraduation.removePropertyChangeListener(ui);
}
if (graduation != null) {
graduation.addPropertyChangeListener(ui);
}
this.graduation = graduation;
units = null;
if (graduation != null) {
final Unit unit = graduation.getUnit();
if (unit != null) {
units = unit.toString();
}
}
}
final boolean changed = !Utilities.equals(graduation, oldGraduation);
if (changed) {
repaint();
}
firePropertyChange("graduation", oldGraduation, graduation);
return changed;
}
/**
* Returns the colors painted by this {@code ColorRamp}.
*
* @return The colors (never {@code null}).
*/
public Color[] getColors() {
return (colors.length!=0) ? (Color[])colors.clone() : colors;
}
/**
* Sets the colors to paint.
* This method will fire a property change event with the {@code "colors"} name.
*
* @param colors The colors to paint.
* @return {@code true} if the state of this {@code ColorRamp} changed as a result of this call.
*
* @see #setColors(Coverage)
* @see #setColors(SampleDimension)
* @see #setColors(IndexColorModel)
* @see #getColors()
* @see #getGraduation()
*/
public boolean setColors(final Color[] colors) {
final Color[] oldColors = this.colors;
this.colors = (colors!=null && colors.length!=0) ? (Color[])colors.clone() : EMPTY;
final boolean changed = !Arrays.equals(oldColors, this.colors);
if (changed) {
repaint();
}
firePropertyChange("colors", oldColors, colors);
return changed;
}
/**
* Sets the colors to paint from an {@link IndexColorModel}. The default implementation
* fetches the colors from the index color model and invokes {@link #setColors(Color[])}.
*
* @param model The colors to paint.
* @return {@code true} if the state of this {@code ColorRamp} changed as a result of this call.
*
* @see #setColors(Coverage)
* @see #setColors(SampleDimension)
* @see #setColors(Color[])
* @see #getColors()
* @see #getGraduation()
*/
public boolean setColors(final IndexColorModel model) {
final Color[] colors;
if (model == null) {
colors = EMPTY;
} else {
colors = new Color[model.getMapSize()];
for (int i=0; i=0 && hi<=palette.length && (hi-lo)>(upper-lower)) {
lower = (int) lo;
upper = (int) hi;
}
}
/*
* We now know the range of values to show on the palette. Creates the colors from
* the palette. Only palette using RGB colors are understood at this time, but the
* graduation (after this block) is still created for all kind of palette.
*/
if (PaletteInterpretation.RGB.equals(band.getPaletteInterpretation())) {
colors = new Color[upper-lower];
for (int i=0; i max) {
// This case occurs typically when displaying a color ramp for
// sea bathymetry, for which floor level are negative numbers.
min = -min;
max = -max;
}
if (!(min <= max)) {
// This case occurs if one or both values is NaN.
throw new IllegalArgumentException(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, "band", band));
}
graduation = createGraduation(this.graduation, band, min, max);
}
}
return setGraduation(graduation) | setColors(colors); // Realy |, not ||
}
/**
* Sets the graduation and the colors from a coverage.
* The default implementation fetchs the visible sample dimension from the specified coverage,
* and then invokes {@link #setColors(Color[]) setColors} and
* {@link #setGraduation setGraduation}.
*
* @param coverage The coverage, or {@code null}.
* @return {@code true} if the state of this {@code ColorRamp} changed as a result of this call.
*
* @see #setColors(IndexColorModel)
* @see #setColors(SampleDimension)
* @see #getColors()
* @see #getGraduation()
*/
public boolean setColors(final Coverage coverage) {
SampleDimension band = null;
if (coverage != null) {
band = coverage.getSampleDimension(CoverageUtilities.getVisibleBand(band));
}
return setColors(band);
}
/**
* Returns the component's orientation (horizontal or vertical).
* It should be one of the following constants:
* ({@link SwingConstants#HORIZONTAL} or {@link SwingConstants#VERTICAL}).
*/
public int getOrientation() {
return (horizontal) ? SwingConstants.HORIZONTAL : SwingConstants.VERTICAL;
}
/**
* Set the component's orientation (horizontal or vertical).
*
* @param orient {@link SwingConstants#HORIZONTAL} or {@link SwingConstants#VERTICAL}.
*/
public void setOrientation(final int orient) {
switch (orient) {
case SwingConstants.HORIZONTAL: horizontal=true; break;
case SwingConstants.VERTICAL: horizontal=false; break;
default: throw new IllegalArgumentException(String.valueOf(orient));
}
}
/**
* Tests if graduation labels are paint on top of the
* colors ramp. Default value is {@code true}.
*/
public boolean isLabelVisibles() {
return labelVisibles;
}
/**
* Sets whatever the graduation labels should be painted on top of the colors ramp.
*/
public void setLabelVisibles(final boolean visible) {
labelVisibles = visible;
}
/**
* Sets the label colors. A {@code null} value reset the automatic color.
*
* @see #getForeground
*/
public void setForeground(final Color color) {
super.setForeground(color);
autoForeground = (color==null);
}
/**
* Returns a color for label at the specified index. The default color will be
* black or white, depending of the background color at the specified index.
*/
private Color getForeground(final int colorIndex) {
final Color color = colors[colorIndex];
HSB = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), HSB);
return (HSB[2]>=0.5f) ? Color.black : Color.white;
}
/**
* Paint the color ramp. This method doesn't need to restore
* {@link Graphics2D} to its initial state once finished.
*
* @param graphics The graphic context in which to paint.
* @param bounds The bounding box where to paint the color ramp.
* @return Bounding box of graduation labels (NOT takind in account the color ramp
* behind them), or {@code null} if no label has been painted.
*/
private Rectangle2D paint(final Graphics2D graphics, final Rectangle bounds) {
final int length = colors.length;
if (length != 0) {
int i=0, lastIndex=0;
Color color=colors[i];
Color nextColor=color;
int R,G,B;
int nR = R = color.getRed ();
int nG = G = color.getGreen();
int nB = B = color.getBlue ();
final int ox = bounds.x + MARGIN;
final int oy = bounds.y + bounds.height - MARGIN;
final double dx = (double)(bounds.width -2*MARGIN)/length;
final double dy = (double)(bounds.height-2*MARGIN)/length;
final Rectangle2D.Double rect = new Rectangle2D.Double();
rect.setRect(bounds);
while (++i <= length) {
if (i != length) {
nextColor = colors[i];
nR = nextColor.getRed ();
nG = nextColor.getGreen();
nB = nextColor.getBlue ();
if (R==nR && G==nG && B==nB) {
continue;
}
}
if (horizontal) {
rect.x = ox+dx*lastIndex;
rect.width = dx*(i-lastIndex);
if (lastIndex == 0) {
rect.x -= MARGIN;
rect.width += MARGIN;
}
if (i == length) {
rect.width += MARGIN;
}
} else {
rect.y = oy-dy*i;
rect.height = dy*(i-lastIndex);
if (lastIndex == 0) {
rect.height += MARGIN;
}
if (i == length) {
rect.y -= MARGIN;
rect.height += MARGIN;
}
}
graphics.setColor(color);
graphics.fill(rect);
lastIndex = i;
color = nextColor;
R = nR;
G = nG;
B = nB;
}
}
Rectangle2D labelBounds=null;
if (labelVisibles && graduation!=null) {
/*
* Prépare l'écriture de la graduation. On vérifie quelle longueur
* (en pixels) a la rampe de couleurs et on calcule les coéfficients
* qui permettront de convertir les valeurs logiques en coordonnées pixels.
*/
double x = bounds.getCenterX();
double y = bounds.getCenterY();
final double axisRange = graduation.getRange();
final double axisMinimum = graduation.getMinimum();
final double visualLength, scale, offset;
if (horizontal) {
visualLength = bounds.getWidth() - 2*MARGIN;
scale = visualLength/axisRange;
offset = (bounds.getMinX()+MARGIN) - scale*axisMinimum;
} else {
visualLength = bounds.getHeight() - 2*MARGIN;
scale = -visualLength/axisRange;
offset = (bounds.getMaxY()-MARGIN) + scale*axisMinimum;
}
if (hints==null) hints = new RenderingHints(null);
final RenderingHints hints = this.hints;
final double ratio = length/axisRange;
final Font font = getFont();
final FontRenderContext context = graphics.getFontRenderContext();
hints.put(Graduation.VISUAL_AXIS_LENGTH, new Float((float)visualLength));
graphics.setColor(getForeground());
/*
* Procède à l'écriture de la graduation.
*/
for (final TickIterator ticks = reuse = graduation.getTickIterator(hints, reuse);
ticks.hasNext(); ticks.nextMajor())
{
if (ticks.isMajorTick()) {
final GlyphVector glyph = font.createGlyphVector(context, ticks.currentLabel());
final Rectangle2D rectg = glyph.getVisualBounds();
final double width = rectg.getWidth();
final double height = rectg.getHeight();
final double value = ticks.currentPosition();
final double position = value*scale+offset;
final int colorIndex = Math.min(Math.max((int)Math.round(
(value-axisMinimum)*ratio),0), length-1);
if (horizontal) x=position;
else y=position;
rectg.setRect(x-0.5*width, y-0.5*height, width, height);
if (autoForeground) {
graphics.setColor(getForeground(colorIndex));
}
graphics.drawGlyphVector(glyph, (float)rectg.getMinX(), (float)rectg.getMaxY());
if (labelBounds != null) {
labelBounds.add(rectg);
} else {
labelBounds = rectg;
}
}
}
/*
* Ecrit les unités.
*/
if (units != null) {
final GlyphVector glyph = font.createGlyphVector(context, units);
final Rectangle2D rectg = glyph.getVisualBounds();
final double width = rectg.getWidth();
final double height = rectg.getHeight();
if (horizontal) {
double left = bounds.getMaxX()-width;
if (labelBounds != null) {
final double check = labelBounds.getMaxX()+4;
if (checkSwing and shouldn't be directly used.
*/
public void addNotify() {
super.addNotify();
if (graduation != null) {
graduation.removePropertyChangeListener(ui); // Avoid duplication
graduation.addPropertyChangeListener(ui);
}
}
/**
* Notifies this component that it no longer has a parent component.
* This method is invoked by Swing and shouldn't be directly used.
*/
public void removeNotify() {
if (graduation != null) {
graduation.removePropertyChangeListener(ui);
}
super.removeNotify();
}
/**
* Classe ayant la charge de dessiner la rampe de couleurs, ainsi que
* de calculer l'espace qu'elle occupe. Cette classe peut aussi réagir
* à certains événements.
*
* @version $Id$
* @author Martin Desruisseaux
*/
private final class UI extends ComponentUI implements PropertyChangeListener {
/**
* Retourne la dimension minimale de cette rampe de couleurs.
*/
public Dimension getMinimumSize(final JComponent c) {
return (((ColorRamp) c).horizontal) ? new Dimension(2*MARGIN,16)
: new Dimension(16,2*MARGIN);
}
/**
* Retourne la dimension préférée de cette rampe de couleurs.
*/
public Dimension getPreferredSize(final JComponent c) {
return (((ColorRamp) c).horizontal) ? new Dimension(256,16)
: new Dimension(16,256);
}
/**
* Dessine la rampe de couleurs vers le graphique spécifié. Cette méthode a
* l'avantage d'être appelée automatiquement par Swing avec une copie
* d'un objet {@link Graphics}, ce qui nous évite d'avoir à le remettre dans
* son état initial lorsqu'on a terminé le traçage de la rampe de couleurs.
* On n'a pas cet avantage lorsque l'on ne fait que redéfinir
* {@link JComponent#paintComponent}.
*/
public void paint(final Graphics graphics, final JComponent component) {
final ColorRamp ramp = (ColorRamp) component;
if (ramp.colors != null) {
final Rectangle bounds=ramp.getBounds();
bounds.x = 0;
bounds.y = 0;
ramp.paint((Graphics2D) graphics, bounds);
}
}
/**
* Méthode appelée automatiquement chaque fois qu'une propriété de l'axe a changée.
*/
public void propertyChange(final PropertyChangeEvent event) {
repaint();
}
}
}