This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 151acd0 Allow some extrapolations when applying the transfer function
provided by SampleDimension. The reason is because the range of values given to
Category instances are often only estimations, so we don't want the transfer
function to fail because a value is slightly outside the estimated domain.
151acd0 is described below
commit 151acd06c47a73d9829b091d8077163a4ea56473
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jan 23 20:17:05 2019 +0100
Allow some extrapolations when applying the transfer function provided by
SampleDimension.
The reason is because the range of values given to Category instances are
often only estimations,
so we don't want the transfer function to fail because a value is slightly
outside the estimated domain.
---
.../java/org/apache/sis/coverage/Category.java | 84 ++--
.../java/org/apache/sis/coverage/CategoryList.java | 450 +++++++++++----------
.../org/apache/sis/coverage/ConvertedCategory.java | 2 +-
.../org/apache/sis/coverage/SampleDimension.java | 7 +-
.../org/apache/sis/coverage/SampleRangeFormat.java | 17 +-
.../main/java/org/apache/sis/coverage/ToNaN.java | 11 +-
.../org/apache/sis/coverage/CategoryListTest.java | 71 ++--
.../java/org/apache/sis/coverage/CategoryTest.java | 18 +-
.../org/apache/sis/internal/util/Numerics.java | 2 +-
.../src/main/java/org/apache/sis/math/Vector.java | 3 +-
.../org/apache/sis/measure/MeasurementRange.java | 12 +-
.../java/org/apache/sis/measure/NumberRange.java | 29 +-
.../org/apache/sis/measure/NumberRangeTest.java | 17 +-
13 files changed, 390 insertions(+), 333 deletions(-)
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
index 5b81052..29975b6 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
@@ -34,6 +34,8 @@ import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.iso.Types;
+import static java.lang.Double.doubleToRawLongBits;
+
/**
* Describes a sub-range of sample values in a sample dimension.
@@ -75,12 +77,13 @@ public class Category implements Serializable {
/**
* Serial number for inter-operability with different versions.
*/
- private static final long serialVersionUID = 6215962897884256696L;
+ private static final long serialVersionUID = 2630516005075467646L;
/**
- * Compares {@code Category} objects according their {@link #minimum}
value.
+ * Compares {@code Category} objects according their {@link
NumberRange#getMinDouble(boolean)} value.
*/
- static final Comparator<Category> COMPARATOR = (Category c1, Category c2)
-> Category.compare(c1.minimum, c2.minimum);
+ static final Comparator<Category> COMPARATOR = (Category c1, Category c2)
->
+ Category.compare(c1.range.getMinDouble(true),
c2.range.getMinDouble(true));
/**
* Compares two {@code double} values. This method is similar to {@link
Double#compare(double,double)}
@@ -88,8 +91,8 @@ public class Category implements Serializable {
*/
static int compare(final double v1, final double v2) {
if (Double.isNaN(v1) && Double.isNaN(v2)) {
- final long bits1 = Double.doubleToRawLongBits(v1);
- final long bits2 = Double.doubleToRawLongBits(v2);
+ final long bits1 = doubleToRawLongBits(v1);
+ final long bits2 = doubleToRawLongBits(v2);
if (bits1 < bits2) return -1;
if (bits1 > bits2) return +1;
}
@@ -104,20 +107,6 @@ public class Category implements Serializable {
final InternationalString name;
/**
- * The minimal and maximal sample value (inclusive). If {@link #range} is
non-null, then
- * those fields are equal to the following values, extracted for
performance reasons:
- *
- * <ul>
- * <li>{@code minimum == range.getMinDouble(true)}</li>
- * <li>{@code maximum == range.getMaxDouble(true)}</li>
- * </ul>
- *
- * If {@link #range} is null, then those values shall be one of the
multiple possible {@code NaN} values.
- * This means that this category stands for "no data" after all sample
values have been converted to real values.
- */
- final double minimum, maximum;
-
- /**
* The [minimum … maximum] range of values in this category (never {@code
null}). Notes:
*
* <ul>
@@ -177,8 +166,6 @@ public class Category implements Serializable {
protected Category(final Category copy) {
name = copy.name;
range = copy.range;
- minimum = copy.minimum;
- maximum = copy.maximum;
toConverse = copy.toConverse;
if (copy.converse == copy) {
converse = this;
@@ -203,10 +190,8 @@ public class Category implements Serializable {
* @param caller the converse, or {@code null} for {@code this}.
*/
Category(final Category copy, final Category caller) {
- name = copy.name;
- range = copy.range;
- minimum = copy.minimum;
- maximum = copy.maximum;
+ name = copy.name;
+ range = copy.range;
if (caller != null) {
toConverse = copy.toConverse;
converse = caller;
@@ -242,16 +227,16 @@ public class Category implements Serializable {
ArgumentChecks.ensureNonNull("toUnits", toUnits);
// The converse is not true: we allow 'units' to be null even if
'toUnits' is non-null.
}
- this.name = Types.toInternationalString(name);
- this.minimum = samples.getMinDouble(true);
- this.maximum = samples.getMaxDouble(true);
- final boolean isNaN = Double.isNaN(minimum);
+ this.name = Types.toInternationalString(name);
+ final double minimum = samples.getMinDouble(true);
+ final double maximum = samples.getMaxDouble(true);
+ final boolean isNaN = Double.isNaN(minimum);
/*
* Following arguments check uses '!' in comparison in order to reject
NaN values in quantitative category.
* For qualitative category, NaN is accepted provided that it is the
same NaN for both ends of the range.
*/
if (!(minimum <= maximum)) {
- if (toUnits != null || !isNaN ||
Double.doubleToRawLongBits(minimum) != Double.doubleToRawLongBits(maximum)) {
+ if (toUnits != null || !isNaN || doubleToRawLongBits(minimum) !=
doubleToRawLongBits(maximum)) {
throw new
IllegalArgumentException(Resources.format(Resources.Keys.IllegalCategoryRange_2,
name, samples));
}
}
@@ -338,13 +323,12 @@ public class Category implements Serializable {
minIncluded = maxIncluded;
maxIncluded = tmp;
}
- minimum = extremums[minIncluded ? 0 : 2];
// Store inclusive values.
- maximum = extremums[maxIncluded ? 1 : 3];
if (isQuantitative) {
range = new ConvertedRange(extremums, minIncluded, maxIncluded,
units);
} else {
+ final double minimum = extremums[minIncluded ? 0 : 2];
// Take inclusive value.
final float min = (float) minimum;
- if (Double.doubleToRawLongBits(minimum) ==
Double.doubleToRawLongBits(min)) {
+ if (doubleToRawLongBits(minimum) == doubleToRawLongBits(min)) {
range = NumberRange.create(Float.class, min);
} else {
range = NumberRange.create(Double.class, minimum);
@@ -436,13 +420,18 @@ public class Category implements Serializable {
* a text like "NaN #0".
*/
final Object getRangeLabel() {
- if (Double.isNaN(minimum)) {
- return "NaN #" + MathFunctions.toNanOrdinal((float) minimum);
- } else if (minimum == maximum) {
- return range.getMinValue();
- } else {
- return range;
+ if (range != null) { // Temporarily
null during object construction.
+ final Number minimum = range.getMinValue();
+ if (minimum != null && minimum.equals(range.getMaxValue())) {
+ final float f = minimum.floatValue();
+ if (Float.isNaN(f)) {
+ return "NaN #" + MathFunctions.toNanOrdinal(f);
+ } else {
+ return minimum;
+ }
+ }
}
+ return range;
}
/**
@@ -495,10 +484,19 @@ public class Category implements Serializable {
}
if (object != null && getClass().equals(object.getClass())) {
final Category that = (Category) object;
- return name.equals(that.name) && range.equals(that.range) &&
- Double.doubleToRawLongBits(minimum) ==
Double.doubleToRawLongBits(that.minimum) &&
- Double.doubleToRawLongBits(maximum) ==
Double.doubleToRawLongBits(that.maximum) &&
- toConverse.equals(that.toConverse);
+ if (name.equals(that.name)) {
+ final NumberRange<?> other = that.range;
+ /*
+ * The NumberRange.equals(Object) comparison is not sufficient
because it considers all NaN values as equal.
+ * For the purpose of Category, we need to distinguish the
different NaN values.
+ */
+ if (range == other || (range.equals(other)
+ && doubleToRawLongBits(range.getMinDouble()) ==
doubleToRawLongBits(other.getMinDouble())
+ && doubleToRawLongBits(range.getMaxDouble()) ==
doubleToRawLongBits(other.getMaxDouble())))
+ {
+ return toConverse.equals(that.toConverse);
+ }
+ }
}
return false;
}
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
index b9e4e4b..4242da6 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
@@ -37,10 +37,33 @@ import static java.lang.Double.doubleToRawLongBits;
/**
- * An immutable list of categories. Categories are sorted by their sample
values.
- * Overlapping ranges of sample values are not allowed. A {@code CategoryList}
can contains a mix
- * of qualitative and quantitative categories. The {@link #search(double)}
method is responsible
- * for finding the right category for an arbitrary sample value.
+ * An immutable list of categories and a <cite>transfer function</cite>
implementation backed by that list.
+ * The category list (exposed by the {@link java.util.List} interface) has the
following properties:
+ *
+ * <ul>
+ * <li>Categories are sorted by their sample values.</li>
+ * <li>Overlapping ranges of sample values are not allowed.</li>
+ * <li>A {@code CategoryList} can contain a mix of qualitative and
quantitative categories.</li>
+ * </ul>
+ *
+ * The transfer function exposed by the {@link MathTransform1D} interface is
used only if this list contains
+ * at least 2 categories. More specifically:
+ *
+ * <ul>
+ * <li>If this list contains 0 category, then the {@linkplain
SampleDimension#getTransferFunction() transfer function}
+ * shall be absent.</li>
+ * <li>If this list contains 1 category, then the transfer function should
be {@linkplain Category#getTransferFunction()
+ * the function provided by that single category}, without the
indirection level implemented by {@code CategoryList}.</li>
+ * <li>If this list contains 2 or more categories, then the transfer
function implementation provided by this
+ * {@code CategoryList} is necessary for {@linkplain #search(double)
searching the category} where belong
+ * each sample value.</li>
+ * </ul>
+ *
+ * The transfer function allows some extrapolations if a sample values to
convert falls in a gap between two categories.
+ * The category immediately below will be used (i.e. its domain is expanded up
to the next category), except if one category
+ * is qualitative while the next category is quantitative. In the later case,
the quantitative category has precedence.
+ * The reason for allowing some extrapolations is because the range of values
given to {@link Category} are often only
+ * estimations, and we don't want the transfer function to fail because a
value is slightly outside the estimated domain.
*
* <p>Instances of {@link CategoryList} are immutable and thread-safe.</p>
*
@@ -53,7 +76,7 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
/**
* Serial number for inter-operability with different versions.
*/
- private static final long serialVersionUID = 2647846361059903365L;
+ private static final long serialVersionUID = -457688134719705403L;
/**
* An empty list of categories.
@@ -70,38 +93,40 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
final NumberRange<?> range;
/**
- * List of {@link Category#minimum} values for each category in {@link
#categories}.
- * This array <strong>must</strong> be in increasing order. Actually, this
is the
- * need to sort this array that determines the element order in {@link
#categories}.
+ * List of minimum values (inclusive) for each category in {@link
#categories}, in strictly increasing order.
+ * For each category, {@code minimums[i]} is often equal to {@code
categories[i].range.getMinDouble(true)} but
+ * may also be lower for filling the gap between a quantitative category
and its preceding qualitative category.
+ * We do not store maximum values; range of a category is assumed to span
up to the start of the next category.
+ *
+ * <p>This array <strong>must</strong> be in increasing order, with {@link
Double#NaN} values last.
+ * This is the need to sort this array that determines the element order
in {@link #categories}.</p>
*/
private final double[] minimums;
/**
* The list of categories to use for decoding samples. This list must be
sorted in increasing
- * order of {@link Category#minimum}. Qualitative categories with NaN
values are last.
+ * order of {@link Category#range} minimum. Qualitative categories with
NaN values are last.
*/
private final Category[] categories;
/**
- * The category to use if {@link #search(double)} is invoked with a sample
value greater than all ranges in this list.
- * This is usually a reference to the last category to have a range of
real values. A {@code null} value means that no
- * extrapolation should be used. By extension, a {@code null} value also
means that {@link #search(double)} should not
- * try to find any fallback if the requested sample value does not fall in
a category range.
- *
- * <p>There is no explicit extrapolation field for values less than all
ranges in this list because the extrapolation
- * to use in such case is {@code categories[0]}.</p>
+ * Minimum and maximum values (inclusive) of {@link Category#converse} for
each category.
+ * For each category at index {@code i}, the converse minimum is at index
{@code i*2} and
+ * the converse maximum is at index {@code i*2+1}. This information is
used for ensuring
+ * that extrapolated values (i.e. the result of a conversion when the
input value was not
+ * in the range of any category) do not accidentally fall in the range of
another category.
+ * This field may be {@code null} if there is no need to perform such
verification because
+ * there is less than 2 categories bounded by real (non-NaN) values.
*/
- private final Category extrapolation;
+ private final double[] converseRanges;
/**
- * The last used category. We assume that this category is the most likely
to be requested in the next
- * {@code transform(…)} method invocation.
- *
- * <p>This field is not declared {@code volatile} because we will never
assign newly created objects to it.
- * It will always be a reference to an existing category, and it does not
matter if referenced category is
- * not really the last used one.</p>
+ * Index of the last used category. We assume that this category is the
most likely to be
+ * requested in the next {@code transform(…)} method invocation. This
field does not need
+ * to be volatile because it is not a problem if a thread see an outdated
value; this is
+ * only a hint, and the arrays used with this index are immutable.
*/
- private transient Category last;
+ private transient int lastUsed;
/**
* The {@code CategoryList} that describes values after {@linkplain
#getTransferFunction() transfer function}
@@ -122,11 +147,11 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* The constructor for the {@link #EMPTY} constant.
*/
private CategoryList() {
- range = null;
- minimums = ArraysExt.EMPTY_DOUBLE;
- categories = new Category[0];
- extrapolation = null;
- converse = this;
+ range = null;
+ minimums = ArraysExt.EMPTY_DOUBLE;
+ categories = new Category[0];
+ converseRanges = null;
+ converse = this;
}
/**
@@ -141,6 +166,8 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* @throws IllegalArgumentException if two or more categories have
overlapping sample value range.
*/
CategoryList(final Category[] categories, CategoryList converse) {
+ this.categories = categories;
+ final int count = categories.length;
/*
* If users specify Category instances themselves, maybe they took
existing instances from another
* sample dimension. A list of "non-converted" categories should not
contain any ConvertedCategory
@@ -148,7 +175,7 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* converted categories may contain plain Category instances if the
conversion is identity.
*/
if (converse == null) {
- for (int i=0; i<categories.length; i++) {
+ for (int i=0; i<count; i++) {
final Category c = categories[i];
if (c instanceof ConvertedCategory) {
categories[i] = new Category(c, null);
@@ -156,24 +183,19 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
}
Arrays.sort(categories, Category.COMPARATOR);
- this.categories = categories;
/*
- * Constructs the array of Category.minimum values. During the loop,
we make sure there is no overlapping ranges.
+ * Constructs the array of minimum values (inclusive). This array
shall be in increasing order since
+ * we sorted the categories based on that criterion. We also collect
the minimum and maximum values
+ * expected after conversion, but those values are not necessarily in
any order.
*/
+ final double[] extremums;
+ extremums = new double[count << 1];
+ minimums = new double[count];
+ int countOfFiniteRanges = 0;
NumberRange<?> range = null;
- minimums = new double[categories.length];
- for (int i=categories.length; --i >= 0;) {
+ for (int i=count; --i >= 0;) { // Reverse order for
making computation of 'range' more convenient.
final Category category = categories[i];
- minimums[i] = category.minimum;
- if (i != 0) {
- final Category previous = categories[i-1];
- if (Category.compare(category.minimum, previous.maximum) <= 0)
{
- throw new
IllegalArgumentException(Resources.format(Resources.Keys.CategoryRangeOverlap_4,
- previous.name, previous.getRangeLabel(),
- category.name, category.getRangeLabel()));
- }
- }
- if (!category.isConvertedQualitative()) {
+ if (!isNaN(minimums[i] = category.range.getMinDouble(true))) {
/*
* Initialize with the union of ranges at index 0 and index i.
In most cases, the result will cover the whole
* range so all future calls to 'range.unionAny(…)' will be
no-op. The 'categories[0].range' field should not
@@ -181,69 +203,107 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
*/
if (range == null) {
range = categories[0].range;
+ assert !isNaN(range.getMinDouble()) : range;
}
range = range.unionAny(category.range);
}
+ final int j = i << 1;
+ final NumberRange<?> cr = category.converse.range;
+ if (!isNaN(extremums[j | 1] = cr.getMaxDouble(true)) |
+ !isNaN(extremums[j ] = cr.getMinDouble(true)))
+ {
+ countOfFiniteRanges++;
+ }
}
this.range = range;
+ this.converseRanges = (countOfFiniteRanges > 1) ? extremums : null;
+ assert ArraysExt.isSorted(minimums, false);
/*
- * At this point we have two branches:
- *
- * - If we are creating the list of "samples to real values"
conversions, then we do not allow extrapolations
- * outside the ranges or categories given to this constructor
(extrapolation = null). In addition we need
- * to create the list of categories after conversion to real value.
- *
- * - If we are creating the list of "real values to samples"
conversions, then we need to search for the
- * extrapolation to use when 'search(double)' is invoked with a
value greater than all ranges in this
- * list. This is the last category to have a range of real
(non-NaN) numbers.
+ * Verify that the ranges do not overlap and perform adjustments in
'minimums' values for filling some gaps:
+ * if we find a qualitative category followed by a quantitative
category and empty space between them, then
+ * the quantitative category takes that empty space. We do not perform
similar check for the opposite side
+ * (quantitative followed by qualitative) because CategoryList does
not store maximum values; each category
+ * take all spaces up to the next category.
+ */
+ for (int i=1; i<count; i++) {
+ final Category category = categories[i];
+ final Category previous = categories[i-1];
+ final double minimum = minimums[i];
+ if (Category.compare(minimum, previous.range.getMaxDouble(true))
<= 0) {
+ throw new
IllegalArgumentException(Resources.format(Resources.Keys.CategoryRangeOverlap_4,
+ previous.name, previous.getRangeLabel(),
+ category.name, category.getRangeLabel()));
+ }
+ // No overlapping check for 'converse' ranges here; see next block
below.
+ final double limit = previous.range.getMaxDouble(false);
+ if (minimum > limit && previous.converse.isConvertedQualitative()
// (a>b) implies that values are not NaN.
+ && !category.converse.isConvertedQualitative())
+ {
+ minimums[i] = limit; // Expand the range of quantitative
'category' to the limit of qualitative 'previous'.
+ }
+ }
+ assert ArraysExt.isSorted(minimums, true);
+ /*
+ * If we are creating the list of "samples to real values"
conversions, we need to create the list of categories
+ * resulting from conversions to real values. Note that this will
indirectly test if some coverted ranges overlap,
+ * since this block invokes recursively this CategoryList constructor
with a non-null 'converse' argument. Note
+ * also that converted categories may not be in the same order.
*/
- Category extrapolation = null;
if (converse == null) {
- boolean hasConversion = false;
- boolean hasQuantitative = false;
- final Category[] convertedCategories = new
Category[categories.length];
- for (int i=0; i < convertedCategories.length; i++) {
+ boolean isQualitative = true;
+ boolean isIdentity = true;
+ final Category[] convertedCategories = new Category[count];
+ for (int i=0; i<count; i++) {
final Category category = categories[i];
final Category converted = category.converse;
- hasConversion |= (category != converted);
- hasQuantitative |= !converted.isConvertedQualitative();
convertedCategories[i] = converted;
+ isQualitative &= converted.isConvertedQualitative();
+ isIdentity &= (category == converted);
}
- if (hasQuantitative) {
- converse = hasConversion ? new
CategoryList(convertedCategories, this) : this;
- } else {
+ if (isQualitative) {
converse = EMPTY;
- }
- } else {
- for (int i=categories.length; --i >= 0;) {
- final Category category = categories[i];
- if (!isNaN(category.maximum)) {
- extrapolation = category;
- break;
+ } else if (isIdentity) {
+ converse = this;
+ } else {
+ converse = new CategoryList(convertedCategories, this);
+ if (converseRanges != null) {
+ /*
+ * For "samples to real values" conversion (only that
direction, not the converse) and only if there
+ * is two or more quantitative categories (should be very
rare), adjust the converted maximum values
+ * for filling gaps between converted categories.
+ */
+ for (int i=1; i<converseRanges.length; i+=2) {
+ final double maximum = converseRanges[i];
+ final int p = ~Arrays.binarySearch(converse.minimums,
maximum);
+ if (p >= 0 && p < count) {
+ double limit =
Math.nextDown(converse.minimums[p]); // Minimum value of next category - ε
+ if (isNaN(limit)) limit =
Double.POSITIVE_INFINITY; // Because NaN are last, no higher values.
+ if (limit > maximum) converseRanges[i] = limit;
// Expand this category to fill the gap.
+ if (p == 1) {
+ converseRanges[i-1] =
Double.NEGATIVE_INFINITY; // Consistent with converse.minimums[0] = −∞
+ }
+ } else if (p == count) {
+ converseRanges[i] = Double.POSITIVE_INFINITY;
// No higher category; take all the space.
+ }
+ }
}
}
}
- this.extrapolation = extrapolation;
- this.converse = converse;
- if (categories.length != 0) {
- last = categories[0];
+ this.converse = converse;
+ if (count != 0 && !isNaN(minimums[0])) {
+ minimums[0] = Double.NEGATIVE_INFINITY;
}
}
/**
- * Computes transient fields and potentially returns a shared instance.
+ * Returns a shared instance if applicable.
*
* @return the object to use after deserialization.
* @throws ObjectStreamException if the serialized object contains invalid
data.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private Object readResolve() throws ObjectStreamException {
- if (categories.length == 0) {
- return EMPTY;
- } else {
- last = categories[0];
- return this;
- }
+ return (categories.length == 0) ? EMPTY : this;
}
/**
@@ -265,49 +325,68 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
/**
- * Performs a bi-linear search of the specified value. This method is
similar to
- * {@link Arrays#binarySearch(double[],double)} except that it can
differentiate
- * the various NaN values.
+ * Performs a bi-linear search of the specified value in the given sorted
array. If an exact match is found,
+ * its index is returned. If no exact match is found, index of the highest
value smaller than {@code sample}
+ * is returned. If no such index exists, -1 is returned. Said otherwise,
if the return value is positive and
+ * the given array is {@link #minimums}, then this method returns the
index in the {@link #categories} array
+ * of the {@link Category} to use for a given sample value.
+ *
+ * <p>This method differs from {@link
Arrays#binarySearch(double[],double)} in the following aspects:</p>
+ * <ul>
+ * <li>If differentiates the various NaN values.</li>
+ * <li>It does not differentiate exact matches from insertion
points.</li>
+ * </ul>
+ *
+ * @param minimums {@link #minimums}.
+ * @param sample the sample value to search.
+ * @return index of the category to use, or -1 if none.
*/
- static int binarySearch(final double[] array, final double key) {
+ static int binarySearch(final double[] minimums, final double sample) {
int low = 0;
- int high = array.length - 1;
- final boolean keyIsNaN = isNaN(key);
+ int high = minimums.length - 1;
+ final boolean sampleIsNaN = isNaN(sample);
while (low <= high) {
final int mid = (low + high) >>> 1;
- final double midVal = array[mid];
- if (midVal < key) { // Neither value is
NaN, midVal is smaller.
+ final double midVal = minimums[mid];
+ if (midVal < sample) { // Neither value is
NaN, midVal is smaller.
low = mid + 1;
continue;
}
- if (midVal > key) { // Neither value is
NaN, midVal is larger.
+ if (midVal > sample) { // Neither value is
NaN, midVal is larger.
high = mid - 1;
continue;
}
final long midRawBits = doubleToRawLongBits(midVal);
- final long keyRawBits = doubleToRawLongBits(key);
- if (midRawBits == keyRawBits) {
- return mid; // Key found.
+ final long smpRawBits = doubleToRawLongBits(sample);
+ if (midRawBits == smpRawBits) {
+ return mid; // Exact match found.
}
final boolean midIsNaN = isNaN(midVal);
final boolean adjustLow;
- if (keyIsNaN) {
+ if (sampleIsNaN) {
/*
- * If (mid,key)==(!NaN, NaN): mid is lower.
+ * If (mid,sample)==(!NaN, NaN): mid is lower.
* If two NaN arguments, compare NaN bits.
*/
- adjustLow = (!midIsNaN || midRawBits < keyRawBits);
+ adjustLow = (!midIsNaN || midRawBits < smpRawBits);
} else {
/*
- * If (mid,key)==(NaN, !NaN): mid is greater.
+ * If (mid,sample)==(NaN, !NaN): mid is greater.
* Otherwise, case for (-0.0, 0.0) and (0.0, -0.0).
*/
- adjustLow = (!midIsNaN && midRawBits < keyRawBits);
+ adjustLow = (!midIsNaN && midRawBits < smpRawBits);
}
if (adjustLow) low = mid + 1;
else high = mid - 1;
}
- return ~low; // key not found.
+ /*
+ * If we reach this point and the sample is NaN, then it is not one of
the NaN values known
+ * to CategoryList constructor and can not be mapped to a category.
Otherwise we found the
+ * index of "insertion point" (~i). This means that 'sample' is lower
than category minimum
+ * at that index. Consequently if the sample value is inside the range
of some category, it
+ * can only be the previous category (~i-1).
+ */
+ return sampleIsNaN ? -1 : low - 1;
}
/**
@@ -318,67 +397,8 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* @return the category of the supplied value, or {@code null}.
*/
final Category search(final double sample) {
- /*
- * Search which category contains the given value.
- * Note: NaN values are at the end of 'minimums' array, so:
- *
- * 1) if 'value' is NaN, then 'i' will be the index of a NaN category.
- * 2) if 'value' is a real number, then 'i' may be the index of a
category
- * of real numbers or the first category containing NaN values.
- */
- int i = binarySearch(minimums, sample); //
Special 'binarySearch' for NaN
- if (i >= 0) {
- assert doubleToRawLongBits(sample) ==
doubleToRawLongBits(minimums[i]);
- return categories[i];
- }
- /*
- * If we reach this point and the value is NaN, then it is not one of
the NaN values known
- * to CategoryList constructor. Consequently we can not map a category
to this value.
- */
- if (isNaN(sample)) {
- return null;
- }
- assert i == Arrays.binarySearch(minimums, sample) : i;
- /*
- * 'binarySearch' found the index of "insertion point" (~i). This
means that 'sample' is lower
- * than 'Category.minimum' at that index. Consequently if the sample
value is inside the range
- * of some category, it can only be the previous category (~i-1).
- */
- i = ~i - 1;
- if (i >= 0) {
- final Category category = categories[i];
- assert sample > category.minimum : sample;
- if (sample <= category.maximum) {
- return category;
- }
- /*
- * At this point we determined that 'sample' is between two
categories. If extrapolations
- * are allowed, returns the category for the range closest to the
sample value.
- *
- * Assertion: 'next.minimum' shall not be smaller than 'sample',
otherwise it should have
- * been found by 'binarySearch'.
- */
- if (extrapolation != null) {
- if (++i < categories.length) {
- final Category next = categories[i];
- assert !(next.minimum <= sample) : sample; // '!'
for accepting NaN.
- return (next.minimum - sample < sample - category.maximum)
? next : category;
- }
- return extrapolation;
- }
- } else if (extrapolation != null) {
- /*
- * If the value is smaller than the smallest Category.minimum,
returns
- * the first category (except if there is only qualitative
categories).
- */
- if (categories.length != 0) {
- final Category category = categories[0];
- if (!isNaN(category.minimum)) {
- return category;
- }
- }
- }
- return null;
+ final int i = binarySearch(minimums, sample);
+ return (i >= 0) ? categories[i] : null;
}
/**
@@ -405,38 +425,26 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* the category has changed. The break condition (numPts >= 0) is near
the end of the loop,
* after we have done the conversion but before to change category.
*/
- Category category = last;
+ int index = lastUsed;
double value = Double.NaN;
for (int peekOff = srcOff; /* numPts >= 0 */; peekOff += direction) {
- final double minimum = category.minimum;
- final double maximum = category.maximum;
+ final double minimum = minimums[index];
+ final double limit = (index+1 < minimums.length) ?
minimums[index+1] : Double.NaN;
final long rawBits = doubleToRawLongBits(minimum);
while (--numPts >= 0) {
value = (srcFloat != null) ? srcFloat[peekOff] :
srcPts[peekOff];
if (value >= minimum) {
- if (!(value <= maximum || category == extrapolation)) {
- /*
- * If the value is greater than the [minimum …
maximum] range and extrapolation
- * is not allowed, then consider that the category has
changed; stop the search.
- */
- break;
+ if (value >= limit) {
+ break; //
Category has changed; stop the search.
}
- } else if (doubleToRawLongBits(value) != rawBits &&
- (isNaN(value) || extrapolation == null || category !=
categories[0]))
- {
- /*
- * If the value is not the expected NaN value, or the
value is a real number less than
- * the [minimum … maximum] range with extrapolation not
allowed, then consider that the
- * category has changed; stop the search.
- */
- break;
+ } else if (doubleToRawLongBits(value) != rawBits) {
+ break; // Not the
expected NaN value.
}
peekOff += direction;
}
/*
* The category has changed. Compute the start point (which
depends on 'direction') and perform
- * the conversion. If 'search' was allowed to search for the
nearest category, clamp all output
- * values in their category range.
+ * the conversion on many values in a single 'transform' method
call.
*/
int count = peekOff - srcOff; // May be
negative if we are going backward.
if (count < 0) {
@@ -444,7 +452,7 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
srcOff -= count - 1;
}
final int stepOff = srcOff + srcToDst;
- final MathTransform1D piece = category.toConverse;
+ final MathTransform1D piece = categories[index].toConverse;
if (srcFloat != null) {
if (dstFloat != null) {
piece.transform(srcFloat, srcOff, dstFloat, stepOff,
count);
@@ -459,15 +467,14 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
}
/*
- * If extrapolation may have happened, verify that transformed
values are in expected ranges.
- * Values out of range will be clamped.
+ * If we need safety against extrapolations (for avoiding that a
value falls in the range of another category),
+ * verify that transformed values are in expected ranges. Values
out of range will be clamped.
*/
- if (extrapolation != null) {
+ if (converseRanges != null) {
dstOff = srcOff + srcToDst;
- final Category converse = category.converse;
- if (dstFloat != null) { //
Loop for the 'float' version.
- final float min = (float) converse.minimum;
- final float max = (float) converse.maximum;
+ if (dstFloat != null) {
// Loop for the 'float' version.
+ final float min = (float) converseRanges[(index << 1) ];
+ final float max = (float) converseRanges[(index << 1) | 1];
while (--count >= 0) {
final float check = dstFloat[dstOff];
if (check < min) {
@@ -477,9 +484,9 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
dstOff++;
}
- } else { //
Loop for the 'double' version.
- final double min = converse.minimum;
- final double max = converse.maximum;
+ } else {
// Loop for the 'double' version.
+ final double min = converseRanges[(index << 1) ];
+ final double max = converseRanges[(index << 1) | 1];
while (--count >= 0) {
final double check = dstPts[dstOff];
if (check < min) {
@@ -497,17 +504,18 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
* category for the next points.
*/
if (numPts < 0) break;
- category = search(value);
- if (category == null) {
+ index = binarySearch(minimums, value);
+ if (index < 0) {
throw new
TransformException(Resources.format(Resources.Keys.NoCategoryForValue_1,
value));
}
srcOff = peekOff;
}
- last = category;
+ lastUsed = index;
}
/**
- * Transforms a list of coordinate point ordinal values.
+ * Transforms a list of coordinate point ordinal values. This method can
be invoked only if {@link #categories}
+ * contains at least two elements, otherwise a {@code MathTransform}
implementation from another package is used.
*/
@Override
public final void transform(double[] srcPts, int srcOff, double[] dstPts,
int dstOff, int numPts) throws TransformException {
@@ -515,7 +523,8 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
/**
- * Transforms a list of coordinate point ordinal values.
+ * Transforms a list of coordinate point ordinal values. This method can
be invoked only if {@link #categories}
+ * contains at least two elements, otherwise a {@code MathTransform}
implementation from another package is used.
*/
@Override
public final void transform(float[] srcPts, int srcOff, float[] dstPts,
int dstOff, int numPts) throws TransformException {
@@ -523,7 +532,8 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
/**
- * Transforms a list of coordinate point ordinal values.
+ * Transforms a list of coordinate point ordinal values. This method can
be invoked only if {@link #categories}
+ * contains at least two elements, otherwise a {@code MathTransform}
implementation from another package is used.
*/
@Override
public final void transform(float[] srcPts, int srcOff, double[] dstPts,
int dstOff, int numPts) throws TransformException {
@@ -531,7 +541,8 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
/**
- * Transforms a list of coordinate point ordinal values.
+ * Transforms a list of coordinate point ordinal values. This method can
be invoked only if {@link #categories}
+ * contains at least two elements, otherwise a {@code MathTransform}
implementation from another package is used.
*/
@Override
public final void transform(double[] srcPts, int srcOff, float[] dstPts,
int dstOff, int numPts) throws TransformException {
@@ -539,7 +550,8 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
}
/**
- * Transforms the specified value.
+ * Transforms the specified value. This method can be invoked only if
{@link #categories} contains at
+ * least two elements, otherwise a {@code MathTransform} implementation
from another package is used.
*
* @param value the value to transform.
* @return the transformed value.
@@ -547,28 +559,29 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
*/
@Override
public final double transform(double value) throws TransformException {
- Category category = last;
- if (!(value >= category.minimum && value <= category.maximum) &&
- doubleToRawLongBits(value) !=
doubleToRawLongBits(category.minimum))
+ int index = lastUsed;
+ final double minimum = minimums[index];
+ if (value >= minimum ? (index+1 < minimums.length && value >=
minimums[index+1])
+ : doubleToRawLongBits(value) !=
doubleToRawLongBits(minimum))
{
- category = search(value);
- if (category == null) {
+ index = binarySearch(minimums, value);
+ if (index < 0) {
throw new
TransformException(Resources.format(Resources.Keys.NoCategoryForValue_1,
value));
}
- last = category;
+ lastUsed = index;
}
- value = category.toConverse.transform(value);
- if (extrapolation != null) {
+ value = categories[index].toConverse.transform(value);
+ if (converseRanges != null) {
double bound;
- if (value < (bound = category.converse.minimum)) return bound;
- if (value > (bound = category.converse.maximum)) return bound;
+ if (value < (bound = converseRanges[(index << 1) ])) return
bound;
+ if (value > (bound = converseRanges[(index << 1) | 1])) return
bound;
}
- assert category == converse.search(value).converse : category;
return value;
}
/**
- * Gets the derivative of this function at a value.
+ * Gets the derivative of this function at a value. This method can be
invoked only if {@link #categories}
+ * contains at least two elements, otherwise a {@code MathTransform}
implementation from another package is used.
*
* @param value the value where to evaluate the derivative.
* @return the derivative at the specified point.
@@ -576,17 +589,18 @@ final class CategoryList extends AbstractList<Category>
implements MathTransform
*/
@Override
public final double derivative(final double value) throws
TransformException {
- Category category = last;
- if (!(value >= category.minimum && value <= category.maximum) &&
- doubleToRawLongBits(value) !=
doubleToRawLongBits(category.minimum))
+ int index = lastUsed;
+ final double minimum = minimums[index];
+ if (value >= minimum ? (index+1 < minimums.length && value >=
minimums[index+1])
+ : doubleToRawLongBits(value) !=
doubleToRawLongBits(minimum))
{
- category = search(value);
- if (category == null) {
+ index = binarySearch(minimums, value);
+ if (index < 0) {
throw new
TransformException(Resources.format(Resources.Keys.NoCategoryForValue_1,
value));
}
- last = category;
+ lastUsed = index;
}
- return category.toConverse.derivative(value);
+ return categories[index].toConverse.derivative(value);
}
/**
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
index fcfed80..16b91cc 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
@@ -36,7 +36,7 @@ final class ConvertedCategory extends Category {
/**
* Serial number for inter-operability with different versions.
*/
- private static final long serialVersionUID = -7164422654831370784L;
+ private static final long serialVersionUID = 336103757882427857L;
/**
* Creates a category storing the inverse of the "sample to real values"
transfer function. The {@link #toConverse}
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 68c0505..b1622ff 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -79,7 +79,7 @@ public class SampleDimension implements Serializable {
/**
* Serial number for inter-operability with different versions.
*/
- private static final long serialVersionUID = 6026936545776852758L;
+ private static final long serialVersionUID = -4966135180995819364L;
/**
* Identification for this sample dimension. Typically used as a way to
perform a band select by
@@ -646,7 +646,7 @@ public class SampleDimension implements Serializable {
name =
Vocabulary.formatInternational(Vocabulary.Keys.FillValue);
}
final NumberRange<?> samples = range(sample.getClass(), sample,
sample);
- // Use of 'getMinValue()' below shall be consistent with
this.remove(…).
+ // Use of 'getMinValue()' below shall be consistent with
ToNaN.remove(Category).
toNaN.background = samples.getMinValue();
add(new Category(name, samples, null, null, toNaN));
return this;
@@ -963,8 +963,7 @@ public class SampleDimension implements Serializable {
final Category c = categories[i];
System.arraycopy(categories, i+1, categories, i, --count -
i);
categories[count] = null;
- // Use of 'c.minimum' shall be consistent with
'this.setBackground(…)'.
- toNaN.remove(c.minimum, c.converse.minimum);
+ toNaN.remove(c);
return c;
}
};
diff --git
a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
index ccf4712..1a7865e 100644
---
a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
+++
b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
@@ -98,16 +98,21 @@ final class SampleRangeFormat extends RangeFormat {
for (int i=0; i<count; i++) {
int ndigits = 0;
for (final Category category : dimensions[i].getCategories()) {
- final Category converted = category.converted();
- final boolean isPacked =
(Double.doubleToRawLongBits(category.minimum) !=
Double.doubleToRawLongBits(converted.minimum))
- |
(Double.doubleToRawLongBits(category.maximum) !=
Double.doubleToRawLongBits(converted.maximum));
+ final NumberRange<?> sr = category.getSampleRange();
+ final NumberRange<?> cr = category.converted().range;
+ final double smin = sr.getMinDouble(true);
+ final double smax = sr.getMaxDouble(false);
+ final double cmin = cr.getMinDouble(true);
+ final double cmax = cr.getMaxDouble(false);
+ final boolean isPacked = (Double.doubleToRawLongBits(smin) !=
Double.doubleToRawLongBits(cmin))
+ | (Double.doubleToRawLongBits(smax) !=
Double.doubleToRawLongBits(cmax));
hasPackedValues |= isPacked;
/*
* If the sample values are already real values, pretend that
they are packed in bytes.
* The intent is only to compute an arbitrary number of
fraction digits.
*/
- final double range = isPacked ? ( category.maximum -
category.minimum) : 255;
- final double increment = (converted.maximum -
converted.minimum) / range;
+ final double range = isPacked ? (smax - smin) : 256;
+ final double increment = (cmax - cmin) / range;
if (!Double.isNaN(increment)) {
hasQuantitative = true;
final int n =
-Numerics.toExp10(Math.getExponent(increment));
@@ -232,7 +237,7 @@ final class SampleRangeFormat extends RangeFormat {
table.append(text);
table.nextColumn();
}
- table.append(category.name.toString(getLocale()));
+ table.append(category.getName().toString(getLocale()));
table.nextLine();
}
}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
index ea274e8..b27980d 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
@@ -112,12 +112,13 @@ search: if (!add(ordinal)) {
* This method does nothing if {@code converted} is not a NaN value, i.e.
if
* the category is quantitative instead than qualitative.
*
- * @param value the real value of the presumed qualitative category.
- * @param converted the converted value, which should be one of NaN
values.
+ * @param c the presumed qualitative category.
*/
- void remove(final double value, final double converted) {
- if (Double.isNaN(converted) &&
super.remove(MathFunctions.toNanOrdinal((float) converted))) {
- if (isBackground(value)) {
+ void remove(final Category c) {
+ final float converted = (float) c.converse.range.getMinDouble();
+ if (Float.isNaN(converted) &&
super.remove(MathFunctions.toNanOrdinal(converted))) {
+ // Use of 'c.getMinDouble()' shall be consistent with
'SampleDimension.Builder.setBackground(…)'.
+ if (isBackground(c.range.getMinDouble())) {
background = null;
}
}
diff --git
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
index 2e8bc25..5cc8347 100644
---
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
+++
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
@@ -52,9 +52,10 @@ public final strictfp class CategoryListTest extends
TestCase {
for (int i=1; i<size; i++) {
final Category current = categories.get(i );
final Category previous = categories.get(i-1);
- assertFalse( current.minimum > current.maximum);
- assertFalse(previous.minimum > previous.maximum);
- assertFalse(Category.compare(previous.maximum, current.minimum) >
0);
+ assertFalse( current.range.getMinDouble(true) >
current.range.getMaxDouble(true));
+ assertFalse(previous.range.getMinDouble(true) >
previous.range.getMaxDouble(true));
+ assertFalse(Category.compare(previous.range.getMaxDouble(true),
+ current.range.getMinDouble(true)) >
0);
}
}
@@ -111,12 +112,14 @@ public final strictfp class CategoryListTest extends
TestCase {
realNumberLimit += random.nextInt(10);
for (int i=0; i<100; i++) {
final double searchFor = random.nextInt(realNumberLimit);
- assertEquals("binarySearch", Arrays.binarySearch(array,
searchFor),
- CategoryList.binarySearch(array,
searchFor));
+ int expected = Arrays.binarySearch(array, searchFor);
+ if (expected < 0) expected = ~expected - 1;
+ assertEquals("binarySearch", expected,
CategoryList.binarySearch(array, searchFor));
}
/*
- * Previous test didn't tested NaN values (which is the main
difference
- * between binarySearch method in Arrays and CategoryList). Now
test it.
+ * Previous test didn't tested NaN values, which is the main
difference between Arrays.binarySearch(…) and
+ * CategoryList.binarySearch(…). Now test those NaNs. We fill the
last half of the array with NaN values;
+ * the first half keep original real values. Then we search
sometime real values, sometime NaN values.
*/
int nanOrdinalLimit = 0;
realNumberLimit /= 2;
@@ -132,26 +135,29 @@ public final strictfp class CategoryListTest extends
TestCase {
} else {
search =
MathFunctions.toNanFloat(random.nextInt(nanOrdinalLimit));
}
- int foundAt = CategoryList.binarySearch(array, search);
- if (foundAt >= 0) {
- assertEquals(Double.doubleToRawLongBits(search),
- Double.doubleToRawLongBits(array[foundAt]),
STRICT);
- } else {
- foundAt = ~foundAt;
- if (foundAt < array.length) {
- final double after = array[foundAt];
+ /*
+ * At this point, 'search' is a real value or a NaN value to
search.
+ */
+ final int foundAt = CategoryList.binarySearch(array, search);
+ if (foundAt < 0) {
+ // Expected only if the value to search is NaN or less
than all values in the array.
+ assertFalse(search >= array[0]);
+ } else if (foundAt >= array.length) {
+ // Expected only if the value to search is NaN or greater
than all values in the array.
+ assertFalse(search <= array[array.length - 1]);
+ } else if (Double.doubleToRawLongBits(array[foundAt]) !=
Double.doubleToRawLongBits(search)) {
+ final double before = array[foundAt];
+ assertFalse(search <= before);
+ if (!Double.isNaN(search)) {
+ assertFalse("isNaN", Double.isNaN(before));
+ }
+ if (foundAt + 1 < array.length) {
+ final double after = array[foundAt + 1];
assertFalse(search >= after);
if (Double.isNaN(search)) {
assertTrue("isNaN", Double.isNaN(after));
}
}
- if (foundAt > 0) {
- final double before = array[foundAt - 1];
- assertFalse(search <= before);
- if (!Double.isNaN(search)) {
- assertFalse("isNaN", Double.isNaN(before));
- }
- }
}
}
}
@@ -172,7 +178,7 @@ public final strictfp class CategoryListTest extends
TestCase {
}
/**
- * Tests the sample values range and converged values range after
construction of a list of categories.
+ * Tests the sample values range and converted values range after
construction of a list of categories.
*/
@Test
public void testRanges() {
@@ -219,20 +225,21 @@ public final strictfp class CategoryListTest extends
TestCase {
assertSame("100", categories[4].converse, list.converse.search( /*
transform(100) */ -97 ));
assertSame("110", categories[4].converse, list.converse.search( /*
transform(110) */ -107 ));
/*
- * Checks values outside the range of any category. For direct
conversion, no category shall be returned.
- * For inverse conversion, the nearest category shall be returned.
+ * Checks values outside the range of any category. The category
below requested value has its
+ * domain expanded up to the next category, except if one category is
qualitative and the other
+ * one is quantitative, in which case the quantitative category has
precedence.
*/
- assertNull( "-1", list.search( -1));
- assertNull( "2", list.search( 2));
- assertNull( "4", list.search( 4));
- assertNull( "9", list.search( 9));
- assertNull("120", list.search(120));
- assertNull("200", list.search(200));
+ assertSame( "-1", categories[0], list.search( -1));
+ assertSame( "2", categories[0], list.search( 2));
+ assertSame( "4", categories[2], list.search( 4));
+ assertSame( "9", categories[3], list.search( 9));
+ assertSame("120", categories[4], list.search(120));
+ assertSame("200", categories[4], list.search(200));
assertNull( "-1",
list.converse.search(MathFunctions.toNanFloat(-1))); // Nearest sample is 0
assertNull( "2",
list.converse.search(MathFunctions.toNanFloat( 2))); // Nearest sample is 3
assertNull( "4",
list.converse.search(MathFunctions.toNanFloat( 4))); // Nearest sample is 3
assertNull( "9",
list.converse.search(MathFunctions.toNanFloat( 9))); // Nearest sample is 10
- assertSame( "9", categories[3].converse, list.converse.search( /*
transform( 9) */ 5.9 )); // Nearest sample is 10
+ assertSame( "9", categories[4].converse, list.converse.search( /*
transform( 9) */ 5.9 )); // Nearest sample is 10
assertSame("120", categories[4].converse, list.converse.search( /*
transform(120) */ -117 )); // Nearest sample is 119
assertSame("200", categories[4].converse, list.converse.search( /*
transform(200) */ -197 )); // Nearest sample is 119
}
diff --git
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
index b0056f3..3e789e9 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
@@ -94,8 +94,6 @@ public final strictfp class CategoryTest extends TestCase {
*/
assertEquals ("name", "Random",
String.valueOf(category.name));
assertEquals ("name", "Random",
String.valueOf(category.getName()));
- assertEquals ("minimum", sample,
category.minimum, STRICT);
- assertEquals ("maximum", sample,
category.maximum, STRICT);
assertBoundEquals("range.minValue", sample,
category.range.getMinValue());
assertBoundEquals("range.maxValue", sample,
category.range.getMaxValue());
assertSame ("sampleRange", category.range,
category.getSampleRange());
@@ -114,8 +112,6 @@ public final strictfp class CategoryTest extends TestCase {
assertSame ("converted", converse,
converse.converted());
assertEquals ("name", "Random",
String.valueOf(converse.name));
assertEquals ("name", "Random",
String.valueOf(converse.getName()));
- assertTrue ("minimum",
Double.isNaN(converse.minimum));
- assertTrue ("maximum",
Double.isNaN(converse.maximum));
assertNaN ("range", converse.range);
assertNotNull("sampleRange",
converse.getSampleRange());
assertFalse ("measurementRange",
category.getMeasurementRange().isPresent());
@@ -168,14 +164,12 @@ public final strictfp class CategoryTest extends TestCase
{
assertEquals ("name", "Random",
String.valueOf(converse.name));
assertEquals ("name", "Random",
String.valueOf(category.getName()));
assertEquals ("name", "Random",
String.valueOf(converse.getName()));
- assertEquals ("minimum", lower,
category.minimum, STRICT);
- assertEquals ("maximum", upper,
category.maximum, STRICT);
- assertEquals ("minimum", lower*scale+offset,
converse.minimum, EPS);
- assertEquals ("maximum", upper*scale+offset,
converse.maximum, EPS);
+ assertEquals ("minimum", lower,
category.range.getMinDouble(true), STRICT);
+ assertEquals ("maximum", upper,
category.range.getMaxDouble(true), STRICT);
+ assertEquals ("minimum", lower*scale+offset,
converse.range.getMinDouble(true), EPS);
+ assertEquals ("maximum", upper*scale+offset,
converse.range.getMaxDouble(true), EPS);
assertBoundEquals("range.minValue", lower,
category.range.getMinValue());
assertBoundEquals("range.maxValue", upper,
category.range.getMaxValue());
- assertBoundEquals("range.minValue", converse.minimum,
converse.range.getMinValue());
- assertBoundEquals("range.maxValue", converse.maximum,
converse.range.getMaxValue());
assertSame ("sampleRange", category.range,
category.getSampleRange());
assertSame ("sampleRange", converse.range,
converse.getSampleRange());
assertSame ("measurementRange", converse.range,
category.getMeasurementRange().get());
@@ -216,8 +210,6 @@ public final strictfp class CategoryTest extends TestCase {
assertSame ("converse", category,
category.converse);
assertEquals ("name", "Random",
String.valueOf(category.name));
assertEquals ("name", "Random",
String.valueOf(category.getName()));
- assertEquals ("minimum", lower,
category.minimum, STRICT);
- assertEquals ("maximum", upper,
category.maximum, STRICT);
assertBoundEquals("range.minValue", lower,
category.range.getMinValue());
assertBoundEquals("range.maxValue", upper,
category.range.getMaxValue());
assertSame ("sampleRange", category.range,
category.getSampleRange());
@@ -238,8 +230,6 @@ public final strictfp class CategoryTest extends TestCase {
assertSame ("converse", category, category.converse);
assertEquals("name", "NaN",
String.valueOf(category.name));
assertEquals("name", "NaN",
String.valueOf(category.getName()));
- assertEquals("minimum", Double.NaN, category.minimum, STRICT);
- assertEquals("maximum", Double.NaN, category.maximum, STRICT);
assertNaN ("sampleRange", category.range);
assertEquals("range.minValue", Float.NaN, range.getMinValue());
assertEquals("range.maxValue", Float.NaN, range.getMaxValue());
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
index be52a51..fe51d38 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
@@ -58,7 +58,7 @@ public final class Numerics extends Static {
cache( 360);
cache(1000);
cache(Double.POSITIVE_INFINITY);
- cache(Double.NaN);
+ // Do not cache NaN values because Double.equals(Object) consider all
NaN as equal.
}
/**
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
index 2a6ff93..9d1651d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
@@ -32,7 +32,6 @@ import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.system.Loggers;
-import org.apache.sis.internal.util.Numerics;
import static org.apache.sis.util.ArgumentChecks.ensureValidIndex;
@@ -1236,7 +1235,7 @@ search: for (;;) {
*/
int i = 0;
do if (i >= length) {
- final Double NaN = Numerics.valueOf(Double.NaN);
+ final Double NaN = Double.NaN;
return createSequence(getElementType(), NaN, NaN, length);
} while (isNaN(i++));
/*
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
b/core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
index 59d03c5..969d6fd 100644
---
a/core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
+++
b/core/sis-utility/src/main/java/org/apache/sis/measure/MeasurementRange.java
@@ -151,14 +151,18 @@ public class MeasurementRange<E extends Number &
Comparable<? super E>> extends
public static MeasurementRange<?> createBestFit(final Number minValue,
final boolean isMinIncluded,
final Number maxValue, final boolean isMaxIncluded, final Unit<?>
unit)
{
- final Class<? extends Number> type = Numbers.widestClass(
- Numbers.narrowestClass(minValue),
Numbers.narrowestClass(maxValue));
+ final Class<? extends Number> type =
Numbers.widestClass(Numbers.narrowestClass(minValue),
+
Numbers.narrowestClass(maxValue));
if (type == null) {
return null;
}
- return unique(new MeasurementRange(type,
+ MeasurementRange range = new MeasurementRange(type,
Numbers.cast(minValue, type), isMinIncluded,
- Numbers.cast(maxValue, type), isMaxIncluded, unit));
+ Numbers.cast(maxValue, type), isMaxIncluded, unit);
+ if (!isOtherNaN(minValue) && !isOtherNaN(maxValue)) {
+ range = unique(range);
+ }
+ return range;
}
/**
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
index 1aad501..ac117d1 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
@@ -122,6 +122,23 @@ public class NumberRange<E extends Number & Comparable<?
super E>> extends Range
}
/**
+ * Returns {@code true} if the given value is a NaN value other than the
canonical {@link Float#NaN}
+ * or {@link Double#NaN} value. This is used for determining if the range
should be omitted from the
+ * {@link POOL} cache, since {@link #equals(Object)} considers all NaN
values as equal.
+ */
+ static boolean isOtherNaN(final Number n) {
+ if (n instanceof Double) {
+ final double value = (Double) n;
+ return Double.isNaN(value) && Double.doubleToRawLongBits(value) !=
0x7ff8000000000000L;
+ } else if (n instanceof Float) {
+ final float value = (Float) n;
+ return Float.isNaN(value) && Float.floatToRawIntBits(value) !=
0x7fc00000;
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Constructs a range containing a single value of the given type.
* The given value is used as the minimum and maximum values, inclusive.
*
@@ -134,7 +151,11 @@ public class NumberRange<E extends Number & Comparable<?
super E>> extends Range
* @since 1.0
*/
public static <N extends Number & Comparable<? super N>> NumberRange<N>
create(final Class<N> type, final N value) {
- return unique(new NumberRange<>(type, value, true, value, true));
+ NumberRange<N> range = new NumberRange<>(type, value, true, value,
true);
+ if (!isOtherNaN(value)) {
+ range = unique(range);
+ }
+ return range;
}
/**
@@ -310,7 +331,11 @@ public class NumberRange<E extends Number & Comparable<?
super E>> extends Range
}
final Number min = Numbers.cast(minValue, type);
final Number max = Objects.equals(minValue, maxValue) ? min :
Numbers.cast(maxValue, type);
- return unique(new NumberRange(type, min, isMinIncluded, max,
isMaxIncluded));
+ NumberRange range = new NumberRange(type, min, isMinIncluded, max,
isMaxIncluded);
+ if (!isOtherNaN(min) && !isOtherNaN(max)) {
+ range = unique(range);
+ }
+ return range;
}
/**
diff --git
a/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
b/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
index 2974fee..f9a381d 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.measure;
+import org.apache.sis.math.MathFunctions;
import org.junit.Test;
import org.apache.sis.test.TestCase;
import org.apache.sis.test.DependsOn;
@@ -27,7 +28,7 @@ import static org.junit.Assert.*;
* Tests the {@link NumberRange} class.
*
* @author Martin Desruisseaux (IRD)
- * @version 0.3
+ * @version 1.0
* @since 0.3
* @module
*/
@@ -37,6 +38,20 @@ import static org.junit.Assert.*;
})
public final strictfp class NumberRangeTest extends TestCase {
/**
+ * Tests {@link NumberRange#isOtherNaN(Number)}.
+ */
+ @Test
+ public void testIsOtherNaN() {
+ assertFalse(NumberRange.isOtherNaN(0));
+ assertFalse(NumberRange.isOtherNaN(0f));
+ assertFalse(NumberRange.isOtherNaN(0d));
+ assertFalse(NumberRange.isOtherNaN(Float.NaN));
+ assertFalse(NumberRange.isOtherNaN(Double.NaN));
+ assertFalse(NumberRange.isOtherNaN(MathFunctions.toNanFloat(0)));
+ assertTrue (NumberRange.isOtherNaN(MathFunctions.toNanFloat(1)));
+ }
+
+ /**
* Tests the endpoint values of a range of integers.
*/
@Test