/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.imageio.metadata;
import java.awt.image.DataBuffer;
import org.geotools.util.NumberRange;
/**
* A {@code <Band>} element in
* {@linkplain SpatioTemporalMetadataFormat metadata format}.
*
* @author Martin Desruisseaux
* @author Daniele Romagnoli, GeoSolutions
* @author Alessio Fabiani, GeoSolutions
*
*
*
* @source $URL: http://svn.osgeo.org/geotools/trunk/modules/unsupported/coverage-experiment/coverage-core/src/main/java/org/geotools/imageio/metadata/Band.java $
*/
public class Band extends MetadataAccessor {
/** * */
final ChildList<Category> categories;
/** * */
final Statistics statistics;
/**
* Creates a parser for a band. This constructor should not be invoked
* directly; use {@link SpatioTemporalMetadata#getBand} instead.
*
* @param metadata
* The metadata which contains this band.
* @param bandIndex
* The band index for this instance.
*/
protected Band(final SpatioTemporalMetadata metadata,
final int bandIndex) {
this(metadata.getSampleDimensions(), bandIndex);
selectChild(bandIndex);
}
/**
* Creates a parser for a band. This constructor should not be invoked
* directly; use {@link SpatioTemporalMetadata#getBand} instead.
*
* @param parent
* The set of all bands.
* @param bandIndex
* The band index for this instance.
*/
Band(final ChildList<Band> parent, final int bandIndex) {
super(parent);
selectChild(bandIndex);
this.categories = new ChildList.Categories(this);
this.statistics = new Statistics(this, SpatioTemporalMetadataFormat.MD_BD_STATISTICS, null);
}
/**
* Returns the name for this band, or {@code null} if none.
*/
public String getName() {
return getString(SpatioTemporalMetadataFormat.MD_COMM_NAME);
}
/**
* Sets the name for this band.
*
* @param name
* The band name, or {@code null} if none.
*/
public void setName(final String name) {
setString(SpatioTemporalMetadataFormat.MD_COMM_NAME, name);
}
/**
* Returns the range of valid values for this band. The range use the
* {@link Integer} type if possible, or the {@link Double} type otherwise.
* Note that range {@linkplain NumberRange#getMinValue minimum value},
* {@linkplain NumberRange#getMaxValue maximum value} or both may be null if
* no {@code "minValue"} or {@code "maxValue"} attribute were found for the
* {@code "Bands/Band"} element.
*/
public NumberRange getValidRange() {
Number minimum, maximum;
minimum = getInteger("minValue");
maximum = getInteger("maxValue");
final Class<? extends Number> type;
if (minimum == null || maximum == null) {
minimum = getDouble("minValue");
maximum = getDouble("maxValue");
type = Double.class;
} else {
type = Integer.class;
}
// Note: minimum and/or maximum may be null, in which case the range in
// unbounded.
return new NumberRange(type, minimum, true, maximum, true);
}
/**
* Sets the range of valid values. The values should be integers most of the
* time since they are packed values (often index in a color palette). But
* floating point values are allowed too.
* <p>
* If the minimal or maximal value may be unknown, consider invoking
* <code>{@link #setPackedValues setPackedValues}(minValue, maxValue, …)</code>
* instead. The later can infers default bounds according a given data type.
*
* @param minValue
* The minimal valid <em>packed</em> value, or
* {@link Double#NEGATIVE_INFINITY} if none.
* @param maxValue
* The maximal valid <em>packed</em> value, or
* {@link Double#POSITIVE_INFINITY} if none.
*
* @see #setPackedValues
*/
public void setValidRange(final double minValue, final double maxValue) {
final int minIndex = (int) minValue;
final int maxIndex = (int) maxValue;
if (minIndex == minValue && maxIndex == maxValue) {
setInteger("minValue", minIndex);
setInteger("maxValue", maxIndex);
} else {
setDouble("minValue", minValue);
setDouble("maxValue", maxValue);
}
}
/**
* Returns the fill values for this band, or {@code null} if none.
*/
public double[] getNoDataValues() {
return getDoubles("fillValues", true);
}
/**
* Sets the fill values for this band. This method formats all fill values
* as integers if possible, or all values as floating points otherwise. We
* apply a "all or nothing" rule for consistency.
*
* @param fillValues
* The packed values used for missing data, or {@code null}
* if none.
*
* @see #setPackedValues
*/
public void setNoDataValues(final double[] fillValues) {
if (fillValues != null) {
int[] asIntegers = new int[fillValues.length];
for (int i = 0; i < fillValues.length; i++) {
final double value = fillValues[i];
if ((asIntegers[i] = (int) value) != value) {
asIntegers = null; // Not integers; stop the check.
break;
}
}
if (asIntegers != null) {
setIntegers("fillValues", asIntegers);
return;
}
}
setDoubles("fillValues", fillValues);
}
/**
* Defines valid and fill <em>packed</em> values as a combinaison of
* <code>{@linkplain #setValidRange(double,double) setValidRange}(minValue, maxValue)</code>
* and
* <code>{linkplain #setNoDataValues(double[]) setNoDataValues}(fillValues)</code>.
* <p>
* If the minimal or maximal value is
* {@linkplain Double#isInfinite infinite} and the data type is an integer
* type, then this method replaces the infinite values by default bounds
* inferred from the data type and the fill values.
*
* @param minValue
* The minimal valid <em>packed</em> value, or
* {@link Double#NEGATIVE_INFINITY} if unknown.
* @param maxValue
* The maximal valid <em>packed</em> value, or
* {@link Double#POSITIVE_INFINITY} if unknown.
* @param fillValues
* The packed values used for missing data, or {@code null}
* if none.
* @param dataType
* The raw data type as one of {@link DataBuffer} constants,
* or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
*
* @see #setValidRange
* @see #setNoDataValues
*/
public void setPackedValues(double minValue, double maxValue, final double[] fillValues, final int dataType) {
minValue = replaceInfinity(minValue, fillValues, dataType);
maxValue = replaceInfinity(maxValue, fillValues, dataType);
setValidRange(minValue, maxValue);
setNoDataValues(fillValues);
}
/**
* If the specified value is infinity, then replace that values by a bounds
* inferred from the specified fill values and data type.
*
* @param value
* The value.
* @param fillValues
* The packed values used for missing data, or {@code null}
* if none.
* @param dataType
* The raw data type as one of {@link DataBuffer} constants,
* or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
*/
private static double replaceInfinity(double value, final double[] fillValues, final int dataType) {
final boolean negative;
if (value == Double.NEGATIVE_INFINITY) {
negative = true;
} else if (value == Double.POSITIVE_INFINITY) {
negative = false;
} else {
return value;
}
final double midValue;
switch (dataType) {
default: {
// Unsigned integer: computes the upper bound according the data
// length.
final long range = 1L << DataBuffer.getDataTypeSize(dataType);
value = negative ? 0 : range - 1;
midValue = range >>> 1;
break;
}
case DataBuffer.TYPE_SHORT: {
value = negative ? Short.MIN_VALUE : Short.MAX_VALUE;
midValue = 0;
break;
}
case DataBuffer.TYPE_INT: {
value = negative ? Integer.MIN_VALUE : Integer.MAX_VALUE;
midValue = 0;
break;
}
case DataBuffer.TYPE_FLOAT:
case DataBuffer.TYPE_DOUBLE:
case DataBuffer.TYPE_UNDEFINED: {
// Unbounded or undefined type: nothing to do.
return value;
}
}
/*
* Considers only the fill values that are close to the bounds we just
* computed. We use the middle value (always 0 for signed data type) as
* the threshold for choosing which bounds is close to that value. In
* other words, for signed data type we consider only positive or
* negative fill values (depending the 'value' sign), not both in same
* time.
*
* For each fill value to consider, reduces the range of valid values in
* such a way that it doesn't include that fill value. The exclusion is
* performed by subtracting 1, which should be okay since we known at
* this stage that the data type is integer.
*/
if (fillValues != null) {
double valueDistance = Math.abs(value - midValue);
for (int i = 0; i < fillValues.length; i++) {
final double fillValue = fillValues[i];
if ((fillValue < midValue) == negative) {
final double fillDistance = Math.abs(fillValue - midValue);
if (fillDistance <= valueDistance) {
valueDistance = fillDistance;
value = fillValue - 1; // Value must be exclusive.
}
}
}
}
return value;
}
/**
* Returns the scale factor from packed to geophysics values, or {@code 1}
* if none.
*/
public double getScale() {
final Double scale = getDouble(SpatioTemporalMetadataFormat.MD_BD_SCALE);
return (scale != null) ? scale.doubleValue() : 1.0;
}
/**
* Sets the scale factor for this band.
*
* @param scale
* The scale from packed to geophysics values, or {@code 1}
* if none.
*/
public void setScale(final double scale) {
setDouble(SpatioTemporalMetadataFormat.MD_BD_SCALE, scale);
}
/**
* Returns the UoM if any.
*/
public String getUoM() {
return getString(SpatioTemporalMetadataFormat.MD_BD_UOM);
}
/**
* Sets the UoM for this band.
*
* @param UoM the Unit Of Measure of this band.
*/
public void setUoM(final String uom) {
setString(SpatioTemporalMetadataFormat.MD_BD_UOM, uom);
}
/**
* Returns the offset from packed to geophysics values, or {@code 0} if
* none.
*/
public double getOffset() {
final Double offset = getDouble(SpatioTemporalMetadataFormat.MD_BD_OFFSET);
return (offset != null) ? offset.doubleValue() : 0.0;
}
/**
* Sets the offset for this band.
*
* @param offset
* The offset from packed to geophysics values, or {@code 0}
* if none.
*/
public void setOffset(final double offset) {
setDouble("offset", offset);
}
/**
*
*/
public void addCategory(final String name, final String description,
final String range) {
Category candidate = this.categories.addChild();
candidate.setString(SpatioTemporalMetadataFormat.MD_BD_CAT_NAME, name);
candidate.setString(SpatioTemporalMetadataFormat.MD_BD_CAT_DESCRIPTION, description);
candidate.setString(SpatioTemporalMetadataFormat.MD_BD_CAT_RANGE, range);
}
// ////////////////////////////////////////////////////////////////////////
//
// Category methods
//
// ////////////////////////////////////////////////////////////////////////
static final class Category extends MetadataAccessor {
protected Category(final Band metadata, final int index) {
super(metadata);
selectChild(index);
}
/**
* Creates a parser for an category.
*
* @param parent
* The set of all categories.
* @param index
* The categories index for this instance.
*/
Category(final ChildList<Category> parent, final int index) {
super(parent);
selectChild(index);
}
}
// ////////////////////////////////////////////////////////////////////////
//
// Statistics
//
// ////////////////////////////////////////////////////////////////////////
static final class Statistics extends MetadataAccessor {
/** Histogram node * */
final MetadataAccessor histogram;
/**
* Implicit constructor.
*
* @param parentNode
* @param parentPath
* @param childPath
*/
protected Statistics(MetadataAccessor parentNode, String parentPath, String childPath) {
super(parentNode, parentPath, childPath);
this.histogram = new MetadataAccessor(this,
SpatioTemporalMetadataFormat.MD_BD_STAT_HISTOGRAM, null);
}
/**
* Sets Statistics information.
*
* @param mean
* @param mode
* @param stdDev
*/
public void setStatistics(final String mean, final String mode, final String stdDev) {
this.setString(SpatioTemporalMetadataFormat.MD_BD_STAT_MEAN, mean);
this.setString(SpatioTemporalMetadataFormat.MD_BD_STAT_MODE, mode);
this.setString(SpatioTemporalMetadataFormat.MD_BD_STAT_STDDEV, stdDev);
}
/**
* Sets Statistics Histogram information.
*
* @param bins
* @param values
*/
public void setHistogram(final String bins, final String values) {
this.histogram.setString(SpatioTemporalMetadataFormat.MD_BD_STAT_HIST_BINS, bins);
this.histogram.setString(SpatioTemporalMetadataFormat.MD_BD_STAT_HIST_VALUES, values);
}
/**
* Returns Statistics Mean.
*
* @return
*/
public String getMean() {
return this.getString(SpatioTemporalMetadataFormat.MD_BD_STAT_MEAN);
}
/**
* Returns Statistics Mode.
*
* @return
*/
public String getMode() {
return this.getString(SpatioTemporalMetadataFormat.MD_BD_STAT_MODE);
}
/**
* Returns Statistics Standard Deviation.
*
* @return
*/
public String getStdDev() {
return this.getString(SpatioTemporalMetadataFormat.MD_BD_STAT_STDDEV);
}
/**
* Returns Statistics Histogram bins.
*
* @return
*/
public String getHistogramBins() {
return this.histogram.getString(SpatioTemporalMetadataFormat.MD_BD_STAT_HIST_BINS);
}
/**
* Returns Statistics Histogram values.
*
* @return
*/
public String getHistogramValues() {
return this.histogram.getString(SpatioTemporalMetadataFormat.MD_BD_STAT_HIST_VALUES);
}
}
}