private CategoryList()

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;
    }