in endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CategoryList.java [187:365]
private CategoryList(final Category[] categories, CategoryList converse, final Number background) {
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
* instances, otherwise confusion will occur later. Note that the converse is not true: a list of
* converted categories may contain plain Category instances if the conversion is identity.
*/
final boolean isSampleToUnit = (converse == null);
if (isSampleToUnit) {
for (int i=0; i<count; i++) {
final Category c = categories[i];
if (c instanceof ConvertedCategory) {
categories[i] = new Category(c, null);
}
}
}
Arrays.sort(categories, Category.COMPARATOR);
/*
* 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 * 2];
minimums = new double[count];
@SuppressWarnings("LocalVariableHidesMemberVariable")
NumberRange<?> range = null;
int countOfFiniteRanges = 0;
for (int i=count; --i >= 0;) { // Reverse order for making computation of `range` more convenient.
final Category category = categories[i];
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
* be NaN because categories with NaN ranges are sorted last.
*/
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 >= 2) ? extremums : null;
assert ArraysExt.isSorted(minimums, false);
/*
* 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 IllegalSampleDimensionException(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.
*/
if (isSampleToUnit) {
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;
convertedCategories[i] = converted;
isQualitative &= converted.isConvertedQualitative();
isIdentity &= (category == converted);
}
if (isQualitative) {
converse = EMPTY;
} else if (isIdentity) {
converse = this;
} else {
converse = new CategoryList(convertedCategories, this, background);
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.converse = converse;
/*
* Make the first quantitative category applicable to all low values. This is consistent with
* the last quantitative category being applicable to all high values. Note that quantitative
* categories are always before qualitative categories (NaN values) in the `minimums` array.
*/
if (count != 0) {
if (!isNaN(minimums[0])) {
minimums[0] = Double.NEGATIVE_INFINITY;
}
/*
* If we are converting from sample values to units of measurement, we should not have NaN inputs.
* If it happens anyway, assume that we can propagate NaN sample values unchanged as output values
* if the user seems prepared to see NaN values.
*
* Design note: we could propagate sample NaN values unconditionally because converted values should
* always allow NaN. But even if NaN should be allowed, we are not sure that the user really expects
* them if no such value appears in the arguments (s)he provided. Given that NaN sample values are
* probably errors, we will let the `unmappedValue(double)` method throws an exception in such case.
*/
if (isSampleToUnit) {
final int n = converse.minimums.length;
if (n != 0 && isNaN(converse.minimums[n - 1])) {
fallback = 0;
return;
}
} else {
/*
* If a NaN value cannot be mapped to a sample value, keep the NaN value only if the 0 value
* (the result of casting NaN to integers) would not conflict with an existing category range.
* This check is important for "unit to sample" conversions, because we typically expect all
* results to be convertible to integers (ignoring rounding errors).
*/
if (background == null && converse.categories.length != 0) {
final NumberRange<?> cr = converse.categories[0].range;
final double cv = cr.getMinDouble();
if ((cv > 0) || (cv == 0 && !cr.isMinIncluded())) {
fallback = 0;
return;
}
}
}
}
/*
* If we cannot let NaN value be propagated, use the background value if available.
* Note that the background value given in argument is a sample value, so it can be
* used only for the "unit to sample" conversion. If that background value is zero,
* it will be interpreted as "let NaN values propagate" but it should be okay since
* NaN casted to integers become 0.
*/
fallback = (!isSampleToUnit && background != null) ? background.doubleValue() : Double.NaN;
}