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`.
}
}