/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 1999-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.internal.swing;
import javax.swing.SwingConstants;
import java.awt.Font;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GradientPaint;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.AbstractMap;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import javax.measure.Unit;
import org.apache.sis.measure.UnitFormat;
import org.opengis.coverage.SampleDimension;
import org.opengis.coverage.PaletteInterpretation;
import org.opengis.metadata.content.TransferFunctionType;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.Classes;
import org.geotoolkit.display.axis.Graduation;
import org.geotoolkit.display.axis.TickIterator;
import org.geotoolkit.display.axis.NumberGraduation;
import org.geotoolkit.display.axis.AbstractGraduation;
import org.geotoolkit.display.axis.LogarithmicNumberGraduation;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.resources.Loggings;
import org.geotoolkit.resources.Errors;
import org.apache.sis.referencing.operation.transform.TransferFunction;
/**
* Paints color ramps with a graduation. This class provides the implementation of
* {@link org.geotoolkit.gui.swing.image.ColorRamp}. It has been factored out in order
* to be leveraged in other modules without introducing a dependency to Swing widgets.
*
* @author Martin Desruisseaux (MPO, IRD, Geomatys)
* @version 3.16
*
* @since 3.10 (derived from 1.1)
* @module
*/
@SuppressWarnings("serial") // Used only for Swing serialization.
public class ColorRamp implements Serializable {
/**
* The default margin (in pixel) on each sides: top, left, right and bottom of the color ramp.
* This margin is added in order to keep some space for the first and the last graduation label,
* otherwise that graduation would be partially outside the color ramp area.
*/
public static final int MARGIN = 10;
/**
* Small tolerance factor for rounding error.
*/
private static final double EPS = 1E-6;
/**
* The locale for formatting error messages, or {@code null} for the default locale.
*/
private Locale locale;
/**
* The graduation to write over the color ramp.
*/
private Graduation graduation;
/**
* The object to use for creating the unit symbol.
* This is created when first needed.
*/
private transient UnitFormat unitFormat;
/**
* Graduation units. This is constructed from {@link Graduation#getUnit()} and cached
* for faster rendering.
*/
private String units;
/**
* The colors to paint as ARGB values (never {@code null}).
*/
private int[] colors = ArraysExt.EMPTY_INT;
/**
* {@code true} if colors should be interpolated.
*
* @since 3.16
*/
public boolean interpolationEnabled = true;
/**
* {@code true} if tick labels shall be painted.
*/
public boolean labelVisibles = true;
/**
* {@code true} if tick labels can be painted with an automatic color. The
* automatic color will be white or black depending on the background color.
*/
public 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;
/**
* Constructs an initially empty color ramp. Colors can be
* set using one of the {@code setColors(...)} methods.
*/
public ColorRamp() {
}
/**
* Returns the locale to use for formatting error messages and graduation labels,
* or {@code null} for the default locale.
*
* @return The locale to use for formatting error messages and graduation labels.
*
* @since 3.16
*/
public Locale getLocale() {
return locale;
}
/**
* Sets the locale to use for formatting error messages and graduation labels.
* As an alternative, users can override the {@link #getLocale()} method instead
* than invoking this {@code setLocale(...)} method.
*
* @param locale The locale to use for formatting error messages and graduation labels.
*
* @since 3.16
*/
public void setLocale(final Locale locale) {
this.locale = locale;
unitFormat = null;
}
/**
* Returns {@code false} if the methods having a {@code Color[][]} return type are allowed
* to return {@code null} unconditionally. This is more efficient for callers which are not
* interested to fire property change events.
* <p>
* The default implementation returns {@code false} in every cases. Subclasses shall
* override this method with a cheap test if they want to be informed about changes.
*
* @return Whatever the caller wants to be informed about color changes.
*/
protected boolean reportColorChanges() {
return false;
}
/**
* 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 final Graduation getGraduation() {
return graduation;
}
/**
* Sets the graduation to paint on top of the color bar.
* The graduation minimum and maximum values should be both inclusive.
*
* @param graduation The new graduation, or {@code null} if none.
* @return The old graduation, or {@code null} if none.
*/
public final Graduation setGraduation(final Graduation graduation) {
final Graduation oldGraduation = this.graduation;
this.graduation = graduation;
units = null;
if (graduation != null) {
final Unit<?> unit = graduation.getUnit();
if (unit != null) {
if (unitFormat == null) {
unitFormat = new UnitFormat(locale != null ? locale : Locale.getDefault());
}
units = unitFormat.format(unit);
}
}
return oldGraduation;
}
/**
* Sets the graduation to the given range of values. The {@code sampleToGeophysics} argument
* is used in order to determine whatever the scale is linear or logarithmic.
*
* @param range The range of values, or {@code null} for removing the graduation.
* @param sampleToGeophysics The <cite>sample to geophysics</cite> transform.
*
* @since 3.16
*/
public final void setGraduation(final MeasurementRange<?> range, final MathTransform1D sampleToGeophysics) {
setGraduation(range != null ? createDefaultGraduation(graduation, sampleToGeophysics,
range.getMinDouble(), range.getMaxDouble(), range.unit(), getLocale()) : null);
}
/**
* Returns {@code true} if some colors are defined.
*
* @return {@code true} if some colors are defined.
*/
public final boolean hasColors() {
return colors != null;
}
/**
* Returns the colors painted by this {@code ColorRamp}.
*
* @return The colors (never {@code null}).
*/
public final Color[] getColors() {
return getColors(colors, new HashMap<Integer,Color>());
}
/**
* Creates an array of {@link Color} values from the given array of ARGB values.
*
* @param ARGB The array of ARGB values.
* @param share A map of {@link Color} instances previously created, or an empty map if none.
* @return The array of color instances.
*/
private static Color[] getColors(final int[] ARGB, final Map<Integer,Color> share) {
final Color[] colors = new Color[ARGB.length];
for (int i=0; i<colors.length; i++) {
final Integer value = ARGB[i];
Color ci = share.get(value);
if (ci == null) {
ci = new Color(value, true);
share.put(value, ci);
}
colors[i] = ci;
}
return colors;
}
/**
* Sets the colors to paint.
*
* @param colors The colors to paint, or {@code null} if none.
* @return The old and new colors, or {@code null} if there is no change.
*/
public final Color[][] setColors(final Color... colors) {
final Map<Integer,Color> share = new HashMap<>();
int[] ARGB = null;
if (colors != null) {
ARGB = new int[colors.length];
for (int i=0; i<colors.length; i++) {
final Color c = colors[i];
share.put(ARGB[i] = c.getRGB(), c);
}
}
return setColors(ARGB, share);
}
/**
* Sets the colors to paint as an array of ARGB values. This method is the most
* efficient one if the colors were already available as an array of ARGB values.
*
* @param colors The colors to paint, or {@code null} if none.
* @return The old and new colors, or {@code null} if there is no change.
*/
public final Color[][] setColors(final int... colors) {
return setColors((colors != null) ? colors.clone() : null, null);
}
/**
* Sets the colors to paint as an array of ARGB values.
*
* @param newColors The colors to paint, or {@code null} if none.
* @param share A map of {@link Color} instances previously created, or {@code null} if none.
* @return The old and new colors, or {@code null} if there is no change.
*/
private Color[][] setColors(int[] newColors, Map<Integer,Color> share) {
if (newColors == null) {
newColors = ArraysExt.EMPTY_INT;
}
final int[] oldColors = colors;
colors = newColors;
if (!reportColorChanges() || Arrays.equals(oldColors, newColors)) {
return null;
}
if (share == null) {
share = new HashMap<>();
}
return new Color[][] {getColors(oldColors, share), getColors(newColors, share)};
}
/**
* Sets the graduation and the colors from a sample dimension.
*
* @param band The sample dimension, or {@code null} if none.
*/
public final void setColors(final SampleDimension band) {
final Map.Entry<Graduation,Color[]> entry = getColors(band);
setGraduation(entry.getKey());
setColors(entry.getValue());
}
/**
* Returns the graduation and the colors from a sample dimension. This is caller
* responsibility to invoke {@code setColors} and {@code setGraduation} with the
* returned values.
*
* @param band The sample dimension, or {@code null} if none.
* @return The pair of graduation and colors.
*/
@SuppressWarnings("fallthrough")
public final Map.Entry<Graduation,Color[]> getColors(SampleDimension band) {
Color[] colors = null;
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 beginning 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(illegalBand(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(illegalBand(band));
}
graduation = createGraduation(this.graduation, band, min, max);
}
}
return new AbstractMap.SimpleEntry<>(graduation, colors);
}
/**
* Formats an error message for an illegal sample dimension.
*/
private String illegalBand(final SampleDimension band) {
return Errors.getResources(getLocale()).getString(Errors.Keys.IllegalArgument_2, "band", 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 final int getOrientation() {
return (horizontal) ? SwingConstants.HORIZONTAL : SwingConstants.VERTICAL;
}
/**
* Sets the component's orientation (horizontal or vertical).
*
* @param orient {@link SwingConstants#HORIZONTAL} or {@link SwingConstants#VERTICAL}.
* @return The old orientation.
*/
public final int setOrientation(final int orient) {
final int old = getOrientation();
switch (orient) {
case SwingConstants.HORIZONTAL: horizontal=true; break;
case SwingConstants.VERTICAL: horizontal=false; break;
default: throw new IllegalArgumentException(String.valueOf(orient));
}
return old;
}
/**
* 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 int color = colors[colorIndex];
final int R = ((color >>> 16) & 0xFF);
final int G = ((color >>> 8) & 0xFF);
final int B = ( color & 0xFF);
HSB = Color.RGBtoHSB(R, G, B, HSB);
return (HSB[2] >= 0.5f) ? Color.BLACK : Color.WHITE;
}
/**
* Paints 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.
* @param font The font to use for the label, or {@code null} for a default font.
* @param foreground The color to use for label, or {@code null} for a default color.
* @return box of graduation labels (NOT taking in account the color ramp behind them),
* or {@code null} if no label has been painted.
*/
public final Rectangle2D paint(final Graphics2D graphics, final Rectangle bounds, Font font, Color foreground) {
final int margin = labelVisibles ? MARGIN : 0;
final int[] colors = this.colors;
final int length = colors.length;
final double dx, dy;
if (length == 0) {
dx = 0;
dy = 0;
} else {
final boolean interpolate = interpolationEnabled && length >= 2 &&
(horizontal ? bounds.width : bounds.height) >= length;
final int numSteps = interpolate ? length-1 : length;
dx = (bounds.width - 2*margin) / (double) numSteps;
dy = (bounds.height - 2*margin) / (double) numSteps;
boolean paintedMargin = false;
int lastIndex = 0;
int thisARGB = colors[0];
int nextARGB = thisARGB;
Color thisColor = new Color(thisARGB, true);
final int ox = bounds.x + margin;
final int oy = bounds.y + bounds.height - margin;
final Rectangle2D.Double rect = new Rectangle2D.Double();
rect.setRect(bounds);
for (int i=0; i<=length; i++) {
if (i != length) {
nextARGB = colors[i];
if (nextARGB == thisARGB && !interpolate) {
continue;
}
}
if (horizontal) {
rect.x = ox + dx*lastIndex;
rect.width = dx * (i-lastIndex);
if (!paintedMargin) {
rect.x -= margin;
rect.width += margin;
paintedMargin = true;
}
if (i == length) {
if (interpolate) {
rect.width = margin;
} else {
rect.width += margin;
}
}
} else {
rect.y = oy - dy*i;
rect.height = dy * (i-lastIndex);
if (!paintedMargin) {
rect.height += margin;
paintedMargin = true;
}
if (i == length) {
if (interpolate) {
rect.y = 0;
rect.height = margin;
} else {
rect.y -= margin;
rect.height += margin;
}
}
}
final Color nextColor = new Color(nextARGB, true);
if (interpolate && thisARGB != nextARGB) {
final double x1, y1, x2, y2;
if (horizontal) {
x1 = rect.getMinX();
x2 = rect.getMaxX();
y1 = y2 = rect.getCenterY();
} else {
y1 = rect.getMaxY();
y2 = rect.getMinY();
x1 = x2 = rect.getCenterX();
}
graphics.setPaint(new GradientPaint(
(float) x1, (float) y1, thisColor,
(float) x2, (float) y2, nextColor));
} else {
graphics.setColor(thisColor);
}
graphics.fill(rect);
lastIndex = i;
thisARGB = nextARGB;
thisColor = nextColor;
}
}
Rectangle2D labelBounds = null;
if (labelVisibles && graduation!=null) {
/*
* Prepares graduation writing. 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.getSpan();
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;
if (font == null) {
font = Font.decode("SansSerif-10");
}
final FontRenderContext context = graphics.getFontRenderContext();
hints.put(Graduation.VISUAL_AXIS_LENGTH, (float) visualLength);
if (foreground == null) {
foreground = Color.BLACK;
}
graphics.setColor(foreground);
/*
* Now write the graduation.
*/
final TickIterator ticks = graduation.getTickIterator(hints, reuse);
for (reuse=ticks; !ticks.isDone(); 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. If
* the supplied {@code reuse} object is non-null and is of the appropriate class, then this
* method can return {@code reuse} without creating a new graduation object. Otherwise this
* method must returns a graduation of the appropriate class, usually {@link NumberGraduation}
* or {@link LogarithmicNumberGraduation}.
* <p>
* In every cases, this method must set graduations
* {@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 appear in the graduation.
* @param maximum The maximal geophysics value to appear 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)
{
return createDefaultGraduation(reuse, band.getSampleToGeophysics(),
minimum, maximum, band.getUnits(), getLocale());
}
/**
* Default implementation of {@code createGraduation}.
*
* @param reuse The graduation to reuse if possible.
* @param tr The <cite>sample to geophysics</cite> transform.
* @param minimum The minimal geophysics value to appear in the graduation.
* @param maximum The maximal geophysics value to appear in the graduation.
* @param units The units of the minimal and maximal values.
* @param locale The locale, or {@code null} for the default locale.
* @return A graduation for the supplied sample dimension.
*/
public static Graduation createDefaultGraduation(final Graduation reuse, MathTransform1D tr,
final double minimum, final double maximum, final Unit<?> units, final Locale locale)
{
AbstractGraduation graduation = (reuse instanceof AbstractGraduation) ?
(AbstractGraduation) reuse : null;
if (tr instanceof LinearTransform) {
if (graduation == null || graduation.getClass() != NumberGraduation.class) {
graduation = new NumberGraduation(units);
}
} else {
final TransferFunction f = new TransferFunction();
f.setTransform(tr);
if (TransferFunctionType.EXPONENTIAL.equals(f.getType())) { // The *inverse* of 'tr' is logarithmic.
if (graduation == null || graduation.getClass() != LogarithmicNumberGraduation.class) {
graduation = new LogarithmicNumberGraduation(units);
}
} else {
final Logger logger = Logging.getLogger("org.geotoolkit.image");
final LogRecord record = Loggings.getResources(locale).getLogRecord(Level.WARNING,
Loggings.Keys.UnrecognizedScaleType_1, Classes.getShortClassName(tr));
record.setLoggerName(logger.getName());
logger.log(record);
graduation = new NumberGraduation(units);
}
}
if (locale != null) {
graduation.setLocale(locale);
}
if (graduation == reuse) {
graduation.setUnit(units);
}
graduation.setMinimum(minimum);
graduation.setMaximum(maximum);
return graduation;
}
/**
* Paints a color ramp for the given range of values. This convenience method is provided
* mostly for {@link org.geotoolkit.coverage.sql.LayerEntry#getColorRamp} implementation,
* which invoke this method through reflection (not directly in order to avoid a direct
* dependencies of {@code geotk-coverage-sql} toward {@code geotk-display}.
* <p>
* See {@link org.geotoolkit.coverage.sql.Layer#getColorRamp(int, MeasurementRange, Map)}
* for a description of the expected content of the {@code properties} map.
*
* @param range The range for the graduation, or {@code null} if no graduation should
* be written.
* @param properties An optional map of properties controlling the rendering.
* @param colors The colors to use in the color ramp.
* @param sampleToGeophysics The <cite>sample to geophysics</cite> transform,
* used in order to determine if the graduation is linear or logarithmic.
* @param locale The locale to use for formatting labels.
* @return The color ramp as an image, or {@code null} if none.
* @throws IllegalArgumentException If the units of the given range are incompatible
* with the units of measurement found in this layer.
* @throws CoverageStoreException If an error occurred while creating the color ramp.
*
* @see org.geotoolkit.coverage.sql.LayerEntry#getColorRamp(int, MeasurementRange, Map)
*
* @since 3.16
*/
public static BufferedImage paint(final MeasurementRange<?> range, final Color[] colors,
final MathTransform1D sampleToGeophysics, final Locale locale, final Map<String,?> properties)
{
Dimension size = null;
Font font = null;
Color color = null;
Graphics2D graphics = null;
if (properties != null) {
size = (Dimension) properties.get("size");
font = (Font) properties.get("font");
color = (Color) properties.get("foreground");
graphics = (Graphics2D) properties.get("graphics");
}
if (size == null) {
size = new Dimension(400, 20);
}
final ColorRamp cr = new ColorRamp();
cr.labelVisibles = (range != null);
cr.setLocale(locale);
cr.setColors(colors);
cr.setGraduation(range, sampleToGeophysics);
if (graphics != null) {
cr.paint(graphics, new Rectangle(size), font, color);
return null;
}
return cr.toImage(size.width, size.height, font, color);
}
/**
* Returns an image representation for this color ramp.
*
* @param width The image width.
* @param height The image height.
* @param font The font to use for the label, or {@code null} for a default font.
* @param foreground The color to use for label, or {@code null} for a default color.
* @return The color ramp as a buffered image.
*/
public final BufferedImage toImage(final int width, final int height, final Font font, final Color foreground) {
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
final Graphics2D graphics = image.createGraphics();
paint(graphics, new Rectangle(image.getWidth(), image.getHeight()), font, foreground);
graphics.dispose();
return image;
}
/**
* Returns a string representation for this color ramp.
*
* @param caller The caller class.
* @return A string representation of the color ramp.
*/
public final String toString(final Class<?> caller) {
final int[] colors = this.colors;
int count = 0;
int i = 0;
if (i < colors.length) {
int last = colors[i];
while (++i < colors.length) {
int c = colors[i];
if (c != last) {
last = c;
count++;
}
}
}
return Classes.getShortName(caller) + '[' + count + " colors]";
}
/**
* Returns a string representation for this color ramp.
*/
@Override
public final String toString() {
return toString(getClass());
}
}