/* * 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(); } } }