/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.gui.swing.coverage;
import java.awt.Color;
import java.util.Locale;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.text.NumberFormat;
import java.io.IOException;
import java.io.Serializable;
import java.io.ObjectInputStream;
import javax.swing.ComboBoxModel;
import org.opengis.util.InternationalString;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.metadata.content.TransferFunctionType;
import org.apache.sis.math.MathFunctions;
import org.geotoolkit.util.Utilities;
import org.geotoolkit.util.Cloneable;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.Classes;
import org.geotoolkit.coverage.Category;
import org.geotoolkit.image.palette.PaletteFactory;
import org.geotoolkit.internal.InternalUtilities;
import org.geotoolkit.internal.coverage.ColorPalette;
import org.geotoolkit.internal.coverage.TransferFunction;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.geotoolkit.resources.Errors;
/**
* A single row in a {@link CategoryTable}. A row contains the minimal and maximal sample values,
* together with the <cite>transfer function</cite> type and coefficients. Those informations can
* be inferred from an existing {@link Category}, edited, then used for creating a new
* {@link Category}.
* <p>
* The attributes in a {@code CategoryRecord} are interdependent. Invoking any setter method
* may have an effect on other attributes. For example if the scale factor is changed, then
* the {@linkplain #getValueRange() range of values} will be recomputed accordingly.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.14
*
* @since 3.13
* @module
*/
public class CategoryRecord implements Cloneable, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 2445769517016833850L;
/**
* Some constants of interest for {@link #functionType}.
*/
static final int NONE=0, LINEAR=1, LOGARITHMIC=2, EXPONENTIAL=3;
/**
* The category represented by this record, or {@code null} if the field has
* been updated and the category has not yet been recreated.
*/
private Category category;
/**
* The category name.
*/
private String name;
/**
* Range of sample values.
*/
private int sampleMin, sampleMax;
/**
* The range of sample values. Computed when first needed.
*/
private transient NumberRange<Integer> sampleRange;
/**
* The range of geophysics values. Computed when first needed.
*/
private transient NumberRange<Double> valueRange;
/**
* The type of the transfer fonction.
* <p>
* <ul>
* <li>0: none</li>
* <li>1: linear</li>
* <li>2: logarithmic</li>
* <li>3: exponential</li>
* </ul>
*/
private int functionType;
/**
* The coefficients of the transfer function.
*/
private double offset, scale;
/**
* The number of fraction digits to use in the decimal format, or -1 if not yet computed.
*/
private transient int fractionDigits;
/**
* The name of a color palette, or the RGB code of a single color, or {@code null} if unknown.
* This is used for selection in a {@link org.geotoolkit.gui.swing.image.PaletteComboBox}.
*/
private String paletteName;
/**
* Creates a new row initialized to the [0 … 255] range of sample values,
* with no transfer function.
*/
public CategoryRecord() {
sampleMax = 255;
scale = 1;
}
/**
* Creates a new row initialized to the values inferred from the given category.
*
* @param category The category from which to infer the values.
* @param locale The locale to use for the localization of the category name,
* or {@code null} for an implementation-dependent default locale.
*
* @todo Current implementation does not yet recognize logarithmic transfer function.
*/
public CategoryRecord(Category category, final Locale locale) {
this(category, locale, null, null);
}
/**
* Creates a new row initialized to the values inferred from the given category.
*
* @param category The category from which to infer the values.
* @param locale The locale to use for the localization of the category name,
* or {@code null} for an implementation-dependent default locale.
* @param paletteFactory The factory to use for fetching the colors from their name,
* or {@code null} for the {@linkplain PaletteFactory#getDefault() default one}.
* @param palettes A list of palettes to use for inferring the palettes names,
* or {@code null} if none. This can be used for the common case where
* this list is already available from the {@link SampleDimensionPanel} GUI.
*/
CategoryRecord(Category category, final Locale locale,
PaletteFactory paletteFactory, ComboBoxModel<ColorPalette> palettes)
{
this.category = category = category.geophysics(false);
final InternationalString name = category.getName();
if (name != null) {
this.name = name.toString(locale);
}
final TransferFunction tf = new TransferFunction(category, locale);
sampleMin = tf.minimum;
sampleMax = tf.maximum;
scale = tf.getScale();
offset = tf.getOffset();
if (tf.isQuantitative) {
fractionDigits = -1;
}
if (tf.getType() != null) {
setTransferFunctionType(tf.getType());
}
if (tf.warning != null) {
warning("<init>", tf.warning);
}
paletteName = ColorPalette.findName(category.getColors(), palettes, paletteFactory);
}
/**
* Logs a warning from the given method with the given message.
*
* @param message The message to log.
*/
private static void warning(final String method, final String message) {
Logging.log(CategoryRecord.class, method, new LogRecord(Level.WARNING, message));
}
/**
* Returns the category represented by this record. If a category has been specified at
* construction time and no setter method changed the attributes, then that category is
* returned unchanged. Otherwise a new category is created and returned.
*
* @return The category represented by this record.
*/
public Category getCategory() {
return getCategory(null);
}
/**
* Returns the category represented by this record, using the given palette factory
* if a new category needs to be built.
*
* @param paletteFactory The factory to use for loading colors from a palette name,
* or {@code null} for the {@linkplain PaletteFactory#getDefault() default}.
* @return The category represented by this record.
*
* @since 3.14
*/
final Category getCategory(PaletteFactory paletteFactory) {
if (category == null) {
Color[] colors = null;
if (paletteName != null) {
if (paletteFactory == null) {
paletteFactory = PaletteFactory.getDefault();
}
try {
colors = paletteFactory.getColors(paletteName);
} catch (IOException e) {
warning("getCategory", e.toString());
// Leave 'colors' to null, which let Category chooses a default value.
}
} else {
colors = new Color[] {new Color(0,0,0,0)};
}
MathTransform1D sampleToGeophysics = null;
if (functionType != NONE) {
sampleToGeophysics = (MathTransform1D) MathTransforms.linear(scale, offset);
switch (functionType) {
case LOGARITHMIC: {
org.apache.sis.referencing.operation.transform.TransferFunction f =
new org.apache.sis.referencing.operation.transform.TransferFunction();
f.setType(TransferFunctionType.LOGARITHMIC);
sampleToGeophysics = MathTransforms.concatenate(
f.getTransform(), sampleToGeophysics);
break;
}
case EXPONENTIAL: {
org.apache.sis.referencing.operation.transform.TransferFunction f =
new org.apache.sis.referencing.operation.transform.TransferFunction();
f.setType(TransferFunctionType.EXPONENTIAL);
sampleToGeophysics = MathTransforms.concatenate(
sampleToGeophysics, f.getTransform());
break;
}
}
}
category = new Category(name, colors, getSampleRange(), sampleToGeophysics);
}
return category;
}
/**
* Returns the category name.
*
* @return The cateogory name, or {@code null} if none.
*/
public String getName() {
return name;
}
/**
* Sets the category name.
*
* @param name The new category name.
* @return {@code true} if this object changed as a result of this method call.
*/
public boolean setName(final String name) {
if (Objects.equals(name, this.name)) {
return false;
}
this.name = name;
category = null;
return true;
}
/**
* Returns the range of valid sample values.
*
* @param extremum {@code -1} for the range of valid minimal sample values, or
* {@code +1} for the range of valid maximal sample values.
*/
final NumberRange<Integer> getValidSamples(final int extremum) {
Integer min=null, max=null;
if (extremum < 0) {
// Minimal value can not be greater than 'sampleMax'.
max = sampleMax;
} else {
// Maximal value can not be less than 'sampleMin'.
min = sampleMin;
}
if (functionType == LOGARITHMIC) {
if (min != null && min <= 0) min = 1;
if (max != null && max <= 0) max = min;
}
return new NumberRange<>(Integer.class, min, true, max, true);
}
/**
* Returns the range of valid geophysics values.
*
* @param extremum {@code -1} for the range of valid minimal geophysics values, or
* {@code +1} for the range of valid maximal geophysics values.
*/
final NumberRange<Double> getValidValues(final int extremum) {
Double min=null, max=null;
final NumberRange<Double> range = getValueRange();
if (range != null) {
if (extremum < 0) {
// Minimal value can not be greater than 'sampleMax'.
max = range.getMaxValue();
} else {
// Maximal value can not be less than 'sampleMin'.
min = range.getMinValue();
}
}
if (functionType == EXPONENTIAL) {
if (min != null && min <= 0) min = Double.MIN_VALUE;
if (max != null && max <= 0) max = min;
}
return new NumberRange<>(Double.class, min, true, max, true);
}
/**
* Returns the range of sample values.
*
* @return The range of sample values (never {@code null}).
*/
public NumberRange<Integer> getSampleRange() {
if (sampleRange == null) {
sampleRange = NumberRange.create(sampleMin, true, sampleMax, true);
}
return sampleRange;
}
/**
* Sets the range of sample values.
*
* @param minimum The new minimal sample value, or {@code null} if unchanged.
* @param maximum The new maximal sample value, or {@code null} if unchanged.
* @return {@code true} if this object changed as a result of this method call.
*/
public boolean setSampleRange(final Integer minimum, final Integer maximum) {
boolean changed = false;
if (minimum != null) {
int min = minimum;
if (min != sampleMin) {
if (functionType == LOGARITHMIC && min <= 0) {
min = 1; // log(min) requires min > 0.
}
if (min > sampleMax) {
sampleMax = min;
}
sampleMin = min;
changed = true;
}
}
if (maximum != null) {
int max = maximum;
if (max != sampleMax) {
if (functionType == LOGARITHMIC && max <= 0) {
max = 1; // log(max) requires max > 0.
}
if (max < sampleMin) {
sampleMin = max;
}
sampleMax = max;
changed = true;
}
}
if (changed) {
sampleRange = null;
valueRange = null;
category = null;
}
return changed;
}
/*
* NOTE: The relationship between sample values and geophysics values are implemented in
* getValueRange(), setValueRange() and the various methods computing the minimal
* and maximal allowed values. If those formulas are changed, then the labels in
* CategoryTable.CellRenderer shall be modified accordingly.
*/
/**
* Returns the range of geophysics value, or {@code null} if there is no transfer function.
* If non-null, the returned range is computed from the range of sample values and the
* coefficients of the transfer function.
*
* @return The range of geophysics value, or {@code null}.
*/
public NumberRange<Double> getValueRange() {
if (functionType == NONE) {
return null;
}
if (valueRange == null) {
double min = sampleMin;
double max = sampleMax;
if (functionType == LOGARITHMIC) {
min = Math.log10(min);
max = Math.log10(max);
}
min = offset + scale * min;
max = offset + scale * max;
if (min > max) {
final double tmp = min;
min = max;
max = tmp;
}
if (functionType == EXPONENTIAL) {
min = MathFunctions.pow10(min);
max = MathFunctions.pow10(max);
}
valueRange = NumberRange.create(min, true, max, true);
}
return valueRange;
}
/**
* Sets the range of geophysics values. This method compute the offset and scale factors
* of the transfer function in order to match the given range.
*
* @param minimum The new minimal geophysics value, or {@code null} if unchanged.
* @param maximum The new maximal geophysics value, or {@code null} if unchanged.
* @return {@code true} if this object changed as a result of this method call.
*/
public boolean setValueRange(final Number minimum, final Number maximum) {
boolean changed = false;
double xmin = sampleMin;
double xmax = sampleMax;
switch (functionType) {
case LOGARITHMIC: {
xmin = Math.log10(xmin);
xmax = Math.log10(xmax);
break;
}
case NONE: {
functionType = LINEAR;
valueRange = null;
changed = true;
break;
}
}
double ymin = (minimum != null) ? minimum.doubleValue() : getValueRange().getMinDouble(true);
double ymax = (maximum != null) ? maximum.doubleValue() : getValueRange().getMaxDouble(true);
if (ymin > ymax) {
final double tmp = ymin;
ymin = ymax;
ymax = tmp;
}
if (functionType == EXPONENTIAL) {
ymin = Math.log10(Math.max(ymin, Double.MIN_VALUE));
ymax = Math.log10(Math.max(ymax, Double.MIN_VALUE));
}
double scale = (xmin != xmax) ? Math.abs(ymax - ymin) / (xmax - xmin) : 1;
if (this.scale < 0) {
scale = -scale;
}
final double offset = ymin - scale * xmin;
if (!Utilities.equals(offset, this.offset) || !Utilities.equals(scale, this.scale)) {
this.offset = offset;
this.scale = scale;
valueRange = null;
category = null;
fractionDigits = -1;
changed = true;
}
return changed;
}
/**
* Returns the transfer function type, or {@code null} if the category is not quantitative.
*
* @return The transfer function type, or {@code null}.
*/
public TransferFunctionType getTransferFunctionType() {
switch (functionType) {
case LINEAR: return TransferFunctionType.LINEAR;
case LOGARITHMIC: return TransferFunctionType.LOGARITHMIC;
case EXPONENTIAL: return TransferFunctionType.EXPONENTIAL;
default: return null;
}
}
/**
* Sets the transfer function type. IF the given argument is null ot an unknown code,
* then this method set the transfer function type to "none".
*
* @param type The new transfer function type, or {@code null} for a qualitative category.
* @return {@code true} if this object changed as a result of this method call.
*/
public boolean setTransferFunctionType(final TransferFunctionType type) {
final int code;
if (TransferFunctionType.LINEAR.equals(type)) {
code = LINEAR;
} else if (TransferFunctionType.LOGARITHMIC.equals(type)) {
code = LOGARITHMIC;
// log(sample) requires sample > 0.
if (sampleMin <= 0) {sampleMin = 1; sampleRange = null;}
if (sampleMax <= 0) {sampleMax = 1; sampleRange = null;}
} else if (TransferFunctionType.EXPONENTIAL.equals(type)) {
code = EXPONENTIAL;
} else {
code = NONE;
}
if (code == functionType) {
return false;
}
functionType = code;
valueRange = null;
category = null;
return true;
}
/**
* Returns a coefficient of the transfer function, or {@code null} if the category is not
* quantitative. The coefficient to fetch is determined by the {@code order} argument.
* Current implementation accepts only 0 (the offset) or 1 (the scale), but subclasses
* can add higher order.
*
* @param order 0 for the offset, or 1 for the scale factor.
* @return The requested coefficient of the transfer function, or {@code null}.
* @throws IllegalArgumentException If the {@code order} argument is out of bounds.
*/
public Double getCoefficient(final int order) throws IllegalArgumentException {
if (functionType == NONE) {
return null;
}
final double value;
switch (order) {
case 0: value = offset; break;
case 1: value = scale; break;
default: {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_2, "order", order));
}
}
return value;
}
/**
* Sets a coefficient of the transfer function.
*
* @param order 0 for the offset, or 1 for the scale factor.
* @param coeff The new coefficient value.
* @return {@code true} if this object changed as a result of this method call.
* @throws IllegalArgumentException If the {@code order} argument is out of bounds.
*/
public boolean setCoefficient(final int order, final double coeff) throws IllegalArgumentException {
final double old;
switch (order) {
case 0: old = offset; offset = coeff; break;
case 1: old = scale; scale = coeff; break;
default: {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_2, "order", order));
}
}
boolean changed = !Utilities.equals(old, coeff);
if (functionType == NONE) {
functionType = LINEAR;
changed = true;
}
if (changed) {
valueRange = null;
category = null;
if (order != 0) {
fractionDigits = -1;
}
}
return changed;
}
/**
* Sets a coefficient of the transfer function, rounding it is reasonable.
* This is for internal usage by {@link CategoryTable} only.
*/
final boolean setCoefficient(final int order, final Number coeff) throws IllegalArgumentException {
return setCoefficient(order, InternalUtilities.adjustForRoundingError(coeff.doubleValue(), 3600, 8));
}
/**
* Configure the given {@linkplain java.text.DecimalFormat} for use in formatting the
* {@linkplain #getValueRange() value range} and the {@linkplain #getCoefficient(int)
* coefficients}.
*/
final void configure(final NumberFormat format) {
if (fractionDigits < 0) {
InternalUtilities.configure(format, scale, 9);
fractionDigits = format.getMaximumFractionDigits();
}
format.setMinimumFractionDigits(Math.min(fractionDigits, 3));
format.setMaximumFractionDigits(Math.min(fractionDigits+3, 9));
}
/**
* Returns the colors, as a palette name or as a RGB code.
* The syntax of the returned string is described in
* {@link org.geotoolkit.gui.swing.image.PaletteComboBox#getSelectedItem()}.
*
* @return The palette name or RGB code, or {@code null} if none.
*
* @since 3.14
*/
public String getPaletteName() {
return paletteName;
}
/**
* Sets the colors, as a palette name or as a RGB code.
* The syntax of the string argument is described in
* {@link org.geotoolkit.gui.swing.image.PaletteComboBox#getSelectedItem()}.
* <p>
* A list of available palette names is provided by the {@link PaletteFactory} javadoc.
*
* @param name The palette name or RGB code, or {@code null} if none.
* @return {@code true} if this object changed as a result of this method call.
*
* @since 3.14
*/
public boolean setPaletteName(final String name) {
final boolean changed = !Objects.equals(name, paletteName);
if (changed) {
paletteName = name;
category = null;
}
return changed;
}
/**
* Returns a clone of this record.
*/
@Override
public CategoryRecord clone() {
try {
return (CategoryRecord) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
/**
* Invoked on deserialization. This method restores some transient fields.
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
fractionDigits = -1;
}
/**
* Returns a string representation of this record, for debugging purpose.
* The current implementation formats the value in the same order than the
* column order documented in {@link CategoryTable}.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this))
.append('[').append(sampleMin).append(" \u2026 ").append(sampleMax);
final NumberRange<Double> range = getValueRange();
if (range != null) {
buffer.append(", ").append(range.getMinValue()).append(" \u2026 ").append(range.getMaxValue())
.append(", ").append(getTransferFunctionType().name())
.append(", ").append(getCoefficient(0))
.append(", ").append(getCoefficient(1));
}
return buffer.append(']').toString();
}
}