/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 1999-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.gui.swing.image; import javax.swing.JComponent; import javax.swing.SwingConstants; import javax.swing.plaf.ComponentUI; import java.awt.Font; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.measure.unit.Unit; 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; 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; import org.geotools.coverage.GridSampleDimension; import org.geotools.resources.Classes; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Loggings; import org.geotools.resources.i18n.LoggingKeys; import org.geotools.util.logging.Logging; import org.geotools.util.Utilities; /** * 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. * * <p> </p> * <p align="center"><img src="doc-files/ColorRamp.png"></p> * <p> </p> * * @since 2.3 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (PMO, IRD) */ @SuppressWarnings("serial") 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 ramp. Colors can be * set using one of the {@code setColors(...)} methods. */ public ColorRamp() { setOpaque(true); setUI(ui); } /** * Constructs a color ramp for the specified coverage. * * @param coverage The coverage for which to create a color ramp. */ 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}. * * @return The graduation to draw. */ public Graduation getGraduation() { return graduation; } /** * Sets 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. * <p> * The graduation minimum and maximum values should be both inclusive. * * @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) ? 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) ? 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<colors.length; i++) { colors[i] = new Color(model.getRed (i), model.getGreen(i), model.getBlue (i), model.getAlpha(i)); } } return setColors(colors); } /** * Sets the graduation and the colors from a sample dimension. * The default implementation fetchs the palette and the minimum and maximum values * from the supplied band, and then invokes {@link #setColors(Color[]) setColors} and * {@link #setGraduation setGraduation}. * * @param band The sample dimension, or {@code null}. * @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 #setColors(Color[]) * @see #getColors() * @see #getGraduation() */ @SuppressWarnings("fallthrough") public boolean setColors(SampleDimension band) { Color[] colors = EMPTY; Graduation graduation = null; /* * Gets the color palette, preferably from the "non-geophysics" view since it is usually * the one backed by an IndexColorModel. We assume that 'palette[i]' gives the color of * sample value 'i'. We will search for the largest range of valid sample integer values, * ignoring "nodata" values. Those "nodata" values appear usually at the begining or at * the end of the whole palette range. * * Note that the above algorithm works without Category. We try to avoid dependency * on categories because some applications don't use them. TODO: should we use this * algorithm only as a fallback (i.e. use categories when available)? */ if (band != null) { if (band instanceof GridSampleDimension) { band = ((GridSampleDimension) band).geophysics(false); } final int[][] palette = band.getPalette(); if (palette != null) { int lower = 0; // Will be inclusive int upper = 0; // Will be exclusive final double[] nodata = band.getNoDataValues(); final double[] sorted = new double[nodata!=null ? nodata.length + 2 : 2]; sorted[0] = -1; sorted[sorted.length - 1] = palette.length; if (nodata != null) { System.arraycopy(nodata, 0, sorted, 1, nodata.length); } Arrays.sort(sorted); for (int i=1; i<sorted.length; i++) { // Note: Don't cast to integer now, because we // want to take NaN and infinity in account. final double lo = Math.floor(sorted[i-1])+1; // "Nodata" always excluded final double hi = Math.ceil (sorted[i ]); // "Nodata" included if integer if (lo>=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<colors.length; i++) { int r=0, g=0, b=0, a=255; final int[] c = palette[i+lower]; if (c!=null) switch (c.length) { default: // Fall through case 4: a=c[3]; // Fall through case 3: b=c[2]; // Fall through case 2: g=c[1]; // Fall through case 1: r=c[0]; // Fall through case 0: break; } colors[i] = new Color(r,g,b,a); } } /* * Transforms the lower and upper sample values into minimum and maximum geophysics * values and creates the graduation. Note that the maximum value will be inclusive, * at the difference of upper value which was exclusive prior this point. */ if (upper > lower) { upper--; // Make it inclusive. } double min, max; try { final MathTransform1D tr = band.getSampleToGeophysics(); min = tr.transform(lower); max = tr.transform(upper); } catch (TransformException cause) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "band", band), cause); } if (min > 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}). * * @return The component orientation. */ 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}. * * @return {@code true} if graduation labels are drawn. */ public boolean isLabelVisibles() { return labelVisibles; } /** * Sets whatever the graduation labels should be painted on top of the colors ramp. * * @param visible {@code true} if graduation labels should be drawn. */ public void setLabelVisibles(final boolean visible) { labelVisibles = visible; } /** * Sets the label colors. A {@code null} value reset the automatic color. * * @see #getForeground */ @Override 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 taking 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; final double dx, dy; if (length == 0) { dx = 0; dy = 0; } else { dx = (double)(bounds.width - 2*MARGIN) / length; dy = (double)(bounds.height - 2*MARGIN) / length; 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 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) { /* * Prepares graduation writting. First, computes the color ramp width in pixels. * Then, computes the coefficients for conversion of graduation values to pixel * coordinates. */ 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 - dx; scale = visualLength / axisRange; offset = (bounds.getMinX() + MARGIN + 0.5*dx) - scale*axisMinimum; } else { visualLength = bounds.getHeight() - 2*MARGIN - dy; scale = -visualLength / axisRange; offset = (bounds.getMaxY() - MARGIN - 0.5*dy) + scale*axisMinimum; } if (hints == null) { hints = new RenderingHints(null); } final double valueToLocation = length / axisRange; Font font = getFont(); if (font == null) { font = Font.decode("SansSerif-10"); } final FontRenderContext context = graphics.getFontRenderContext(); hints.put(Graduation.VISUAL_AXIS_LENGTH, new Float((float)visualLength)); graphics.setColor(getForeground()); /* * Now write the 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)*valueToLocation),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; } } } /* * Writes units. */ 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 (check < left) { left = check; } } rectg.setRect(left, y - 0.5*height, width, height); } else { rectg.setRect(x - 0.5*width, bounds.getMinY() + height, width, height); } if (autoForeground) { graphics.setColor(getForeground(length-1)); } if (labelBounds==null || !labelBounds.intersects(rectg)) { graphics.drawGlyphVector(glyph, (float)rectg.getMinX(), (float)rectg.getMaxY()); } } } return labelBounds; } /** * Returns a graduation for the specified sample dimension, minimum and maximum values. This * method must returns a graduation of the appropriate class, usually {@link NumberGraduation} * or {@link LogarithmicNumberGraduation}. If the supplied {@code reuse} object is non-null and * is of the appropriate class, then this method can returns {@code reuse} without creating a * new graduation object. This method must set graduations's * {@linkplain AbstractGraduation#setMinimum minimum}, * {@linkplain AbstractGraduation#setMaximum maximum} and * {@linkplain AbstractGraduation#setUnit unit} according the values given in arguments. * * @param reuse The graduation to reuse if possible. * @param band The sample dimension to create graduation for. * @param minimum The minimal geophysics value to appears in the graduation. * @param maximum The maximal geophysics value to appears in the graduation. * @return A graduation for the supplied sample dimension. */ protected Graduation createGraduation(final Graduation reuse, final SampleDimension band, final double minimum, final double maximum) { MathTransform1D tr = band.getSampleToGeophysics(); boolean linear = false; boolean logarithmic = false; try { /* * An heuristic approach to determine if the transform is linear or logarithmic. * We look at the derivative, which should be constant everywhere for a linear * scale and be proportional to the inverse of 'x' for a logarithmic one. */ tr = tr.inverse(); final double EPS = 1E-6; // For rounding error. final double ratio = tr.derivative(minimum) / tr.derivative(maximum); if (Math.abs(ratio-1) <= EPS) { linear = true; } if (Math.abs(ratio*(minimum/maximum) - 1) <= EPS) { logarithmic = true; } } catch (TransformException exception) { // Transformation failed. We don't know if the scale is linear or logarithmic. // Continue anyway. A warning will be logged later in this method. } final Unit<?> units = band.getUnits(); AbstractGraduation graduation = (reuse instanceof AbstractGraduation) ? (AbstractGraduation) reuse : null; if (linear) { if (graduation==null || !graduation.getClass().equals(NumberGraduation.class)) { graduation = new NumberGraduation(units); } } else if (logarithmic) { if (graduation==null || !graduation.getClass().equals(LogarithmicNumberGraduation.class)) { graduation = new LogarithmicNumberGraduation(units); } } else { final Logger logger = Logging.getLogger(ColorRamp.class); final LogRecord record = Loggings.format(Level.WARNING, LoggingKeys.UNRECOGNIZED_SCALE_TYPE_$1, Classes.getShortClassName(tr)); record.setLoggerName(logger.getName()); logger.log(record); graduation = new NumberGraduation(units); } if (graduation == reuse) { graduation.setUnit(units); } graduation.setMinimum(minimum); graduation.setMaximum(maximum); return graduation; } /** * Returns an image representation for this color ramp. The image size will be this * {@linkplain #getSize widget size}. * * @return The color ramp as a buffered image. * * @since 2.4 */ public BufferedImage toImage() { final BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); final Graphics2D graphics = image.createGraphics(); paint(graphics, new Rectangle(0, 0, image.getWidth(), image.getHeight())); graphics.dispose(); return image; } /** * Returns a string representation for this color ramp. */ @Override public String toString() { int count=0; int i = 0; if (i < colors.length) { Color last = colors[i]; while (++i < colors.length) { Color c = colors[i]; if (!c.equals(last)) { last = c; count++; } } } return Classes.getShortClassName(this) + '[' + count + " colors]"; } /** * Notifies this component that it now has a parent component. This method * is invoked by <cite>Swing</cite> and shouldn't be directly used. */ @Override 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 <em>Swing</em> and shouldn't be directly used. */ @Override 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 (PMO, IRD) */ private final class UI extends ComponentUI implements PropertyChangeListener { /** * Retourne la dimension minimale de cette rampe de couleurs. */ @Override 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. */ @Override 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 <i>Swing</i> 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}. */ @Override 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(); } } }