private void compact()

in endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorScaleBuilder.java [491:656]


    private void compact() {
        if (target != null) {
            return;
        }
        /*
         * If a source SampleDimension has been specified, verify if it provides a transfer function that we can
         * use directly. If this is the case, use the existing transfer function instead of inventing our own.
         */
        @SuppressWarnings("LocalVariableHidesMemberVariable")
        ColorsForRange[] entries = this.entries;
reuse:  if (source != null) {
            target = source.forConvertedValues(false);
            if (target.getSampleRange().filter(ColorScaleBuilder::isAlreadyScaled).isPresent()) {
                /*
                 * If we enter in this block, all sample values are already in the [0 … 255] range.
                 * If in addition there is no conversion to apply, then there is nothing to do.
                 */
                if (target == source) {
                    return;
                }
                /*
                 * We will need to replace ranges specified in the source `SampleDimensions` by ranges used in the
                 * colorized images. Prepare in advance a `mapper` with all replacements that we know about.
                 */
                final Map<NumberRange<?>,NumberRange<?>> mapper = new HashMap<>();
                for (final Category category : target.getCategories()) {
                    if (mapper.put(category.forConvertedValues(true).getSampleRange(), category.getSampleRange()) != null) {
                        break reuse;        // Duplicated range of values in source SampleDimensions (should not happen).
                    }
                }
                /*
                 * Do the replacements in a temporary `ranges` array before to write in the `entries` array
                 * because `entries` changes must be a "all or nothing" operation. We allow each range to be
                 * used as most once.
                 */
                final NumberRange<?>[] ranges = new NumberRange<?>[entries.length];
                for (int i=0; i<entries.length; i++) {
                    if ((ranges[i] = mapper.remove(entries[i].sampleRange)) == null) {
                        break reuse;            // Range not found or used twice.
                    }
                }
                for (int i=0; i<entries.length; i++) {
                    entries[i].sampleRange = ranges[i];
                }
                return;
            }
        }
        /*
         * If we reach this point, `source` sample dimensions were not specified or cannot be used for
         * getting a transfer function to the [0 … 255] range of values. We will need to create our own.
         * First, sort the entries for having transparent colors first.
         */
        Arrays.sort(entries);                               // Move transparent colors in first positions.
        double span  = 0;                                   // Total span of all non-NaN ranges.
        int lower    = 0;                                   // First available index in the [0 … 255] range.
        int deferred = 0;                                   // Number of entries deferred to next loop.
        int count    = entries.length;                      // Total number of valid entries.
        NumberRange<?> themes = null;                       // The range of values in a thematic map.
        final var mapper  = new HashMap<NumberRange<Integer>, ColorsForRange>();
        final var builder = new SampleDimension.Builder();
        /*
         * We will use the byte values range [0 … 255] with 0 reserved in priority for the most transparent pixels.
         * The first loop below processes NaN values, which are usually the ones associated to transparent pixels.
         * The second loop (from 0 to `deferred`) will process everything else.
         */
        for (int i=0; i<count; i++) {
            final ColorsForRange entry = entries[i];
            NumberRange<?> sourceRange = entry.sampleRange;
            if (!entry.isData) {
                if (lower >= MAX_VALUE) {
                    throw new IllegalArgumentException(Resources.format(Resources.Keys.TooManyQualitatives));
                }
                final NumberRange<Integer> targetRange = NumberRange.create(lower, true, ++lower, false);
                if (mapper.put(targetRange, entry) == null) {
                    final double value = sourceRange.getMinDouble();
                    /*
                     * In the usual case where we have a mix of quantitative and qualitative categories,
                     * the qualitative ones (typically "no data" categories) are associated to NaN.
                     * Values are real only if all categories are qualitatives (e.g. a thematic map).
                     * In such case we will create pseudo-quantitative categories for the purpose of
                     * computing a transfer function, but those categories should not be returned to user.
                     */
                    if (Double.isNaN(value)) {
                        builder.mapQualitative(entry.name(), targetRange, (float) value);
                    } else {
                        if (value == entry.sampleRange.getMaxDouble()) {
                            sourceRange = NumberRange.create(
                                    Math.min(value - 0.5, Math.nextDown(value)), true,
                                    Math.max(value + 0.5, Math.nextUp(value)), false);
                        }
                        builder.addQuantitative(entry.name(), targetRange, sourceRange);
                        themes = (themes != null) ? themes.unionAny(sourceRange) : sourceRange;
                    }
                }
            } else {
                final double s = sourceRange.getSpan();
                if (s > 0) {
                    // Range of real values: defer processing to next loop.
                    span += s;
                    System.arraycopy(entries, deferred, entries, deferred + 1, i - deferred);
                    entries[deferred++] = entry;
                } else {
                    // Invalid range: silently discard.
                    System.arraycopy(entries, i+1, entries, i, --count - i);
                    entries[count] = null;
                }
            }
        }
        /*
         * Following block is executed only if the sample dimension defines only qualitative categories.
         * This is the case of thematic (or classification) map. It may also happen because the coverage
         * defined only a "no data" value with no information about the "real" values. In such case we
         * generate an artificial quantitative category for mapping all remaining values to [0…255] range.
         * The actual category creation happen in the loop after this block.
         */
        if (deferred == 0 && themes != null) {
            if (defaultRange == null) {
                defaultRange = NumberRange.create(0, true, Short.MAX_VALUE + 1, false);
            }
            // Following loop will usually be executed only once.
            for (final NumberRange<?> sourceRange : defaultRange.subtractAny(themes)) {
                span += sourceRange.getSpan();
                final ColorsForRange[] tmp = Arrays.copyOf(entries, ++count);
                System.arraycopy(entries, deferred, tmp, ++deferred, count - deferred);
                tmp[deferred-1] = new ColorsForRange(null, sourceRange, null, true, null);
                entries = tmp;
            }
        }
        this.entries = entries = ArraysExt.resize(entries, count);      // Should be a no-op most of the times.
        /*
         * Above loop mapped all NaN values. Now map the real values. Usually, there is exactly one entry taking
         * all remaining values in the [0 … 255] range, but code below is tolerant to arbitrary number of ranges.
         */
        final int base = lower;
        final double toIndexRange = (MAX_VALUE + 1 - base) / span;
        span = 0;
        for (int i=0; i<deferred; i++) {
            final ColorsForRange entry = entries[i];
            span += entry.sampleRange.getSpan();
            final int upper = Math.toIntExact(Math.round(span * toIndexRange) + base);
            if (upper <= lower) {
                // May happen if too many qualitative categories have been added by previous loop.
                throw new IllegalArgumentException(Resources.format(Resources.Keys.TooManyQualitatives));
            }
            final NumberRange<Integer> samples = NumberRange.create(lower, true, upper, false);
            if (mapper.put(samples, entry) == null) {
                builder.addQuantitative(entry.name(), samples, entry.sampleRange);
            }
            lower = upper;
        }
        /*
         * At this point we created a `Category` instance for each given `ColorsForRange`.
         * Update the given `ColorsForRange` instances with new range values.
         */
        if (source != null) {
            builder.setName(source.getName());
        } else {
            builder.setName(VISUAL);
        }
        target = builder.build();
        for (final Category category : target.getCategories()) {
            final NumberRange<?> packed = category.getSampleRange();
            mapper.get(packed).sampleRange = packed;
            // A NullPointerException on above line would be a bug in our construction of `mapper`.
        }
    }