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 be6450a  Fix a `NullPointerException` when visualizing an image with a 
"no data" value and no other category. In such case we have to synthetize a 
temporary category (not shown to user) for the remaining range of values.
be6450a is described below

commit be6450ad3f4885587bad4b162180802e3793fa05
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Nov 5 19:20:13 2021 +0100

    Fix a `NullPointerException` when visualizing an image with a "no data" 
value and no other category.
    In such case we have to synthetize a temporary category (not shown to user) 
for the remaining range of values.
---
 .../sis/internal/coverage/j2d/Colorizer.java       | 117 +++++++++++++++------
 .../sis/internal/coverage/j2d/ColorsForRange.java  |   7 +-
 2 files changed, 93 insertions(+), 31 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
index 806ef60..b11dc09 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
@@ -37,6 +37,7 @@ import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.Debug;
@@ -61,7 +62,7 @@ import org.apache.sis.util.Debug;
  * product as different depth or different time.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see ColorModelType
  * @see ColorModelFactory#createColorModel(int, int, int, Collection)
@@ -106,7 +107,7 @@ public final class Colorizer {
                 Color.BLUE, Color.CYAN, Color.WHITE, Color.YELLOW, Color.RED} 
: null;
 
     /**
-     * The colors to use for each category.
+     * The colors to use for each category. Never {@code null}.
      * The function may return {@code null}, which means transparent.
      */
     private final Function<Category,Color[]> colors;
@@ -114,6 +115,7 @@ public final class Colorizer {
     /**
      * The colors to use for each range of values in the source image.
      * Entries will be sorted and modified in place.
+     * The array may be null if unspecified, but shall not contain null 
element.
      */
     private ColorsForRange[] entries;
 
@@ -128,21 +130,30 @@ public final class Colorizer {
      * The sample dimension for values after conversion, or {@code null} if 
not yet computed.
      * May be the same than {@link #source} or {@code 
source.forConvertedValues(true)} if one
      * of those values is suitable, or a new sample dimension created by 
{@link #compact()}.
+     *
+     * <p>This sample dimension should not be returned to the user because it 
may not contain meaningful values.
+     * For example it may contain an "artificial" transfer function for 
computing a {@link MathTransform1D} from
+     * source range to the [0 … 255] value range.</p>
      */
     private SampleDimension target;
 
     /**
+     * Default range of values to use if no explicitly specified by a {@link 
Category}.
+     */
+    private NumberRange<?> defaultRange;
+
+    /**
      * Creates a new colorizer which will apply colors on the given range of 
values in source image.
      * The {@code Colorizer} is considered initialized after this constructor;
      * callers shall <strong>not</strong> invoke an {@code initialize(…)} 
method.
      *
      * @param  colors  the colors to use for each range of values in source 
image.
-     *                 A {@code null} value means transparent.
+     *                 A {@code null} entry value means transparent.
      */
     public Colorizer(final Collection<Map.Entry<NumberRange<?>,Color[]>> 
colors) {
         ArgumentChecks.ensureNonNull("colors", colors);
         entries = ColorsForRange.list(colors);
-        this.colors = null;
+        this.colors = GRAYSCALE;
     }
 
     /**
@@ -190,12 +201,13 @@ public final class Colorizer {
             this.source = source;
             final List<Category> categories = source.getCategories();
             if (!categories.isEmpty()) {
-                entries = new ColorsForRange[categories.size()];
+                final ColorsForRange[] entries = new 
ColorsForRange[categories.size()];
                 for (int i=0; i<entries.length; i++) {
                     final Category category = categories.get(i);
                     entries[i] = new ColorsForRange(category, 
category.getSampleRange(), colors.apply(category));
                 }
                 // Leave `target` to null. It will be computed by `compact()` 
if needed.
+                this.entries = entries;
                 return true;
             }
         }
@@ -269,19 +281,20 @@ public final class Colorizer {
         checkInitializationStatus(false);
         ArgumentChecks.ensureFinite("minimum", minimum);
         ArgumentChecks.ensureFinite("maximum", maximum);
+        defaultRange = NumberRange.create(minimum, true, maximum, true);
         target = new SampleDimension.Builder()
                 .setBackground(null, 0)
                 
.addQuantitative(Vocabulary.formatInternational(Vocabulary.Keys.Data),
-                        NumberRange.create(1, true, MAX_VALUE, true),
-                        NumberRange.create(minimum, true, maximum, 
true)).build();
+                        NumberRange.create(1, true, MAX_VALUE, true), 
defaultRange).build();
 
         source = target.forConvertedValues(true);
         final List<Category> categories = source.getCategories();
-        entries = new ColorsForRange[categories.size()];
+        final ColorsForRange[] entries = new ColorsForRange[categories.size()];
         for (int i=0; i<entries.length; i++) {
             final Category category = categories.get(i);
             entries[i] = new ColorsForRange(category, 
category.forConvertedValues(false).getSampleRange(), colors.apply(category));
         }
+        this.entries = entries;
     }
 
     /**
@@ -303,8 +316,8 @@ public final class Colorizer {
             final ColorSpace cs = original.getColorSpace();
             if (cs instanceof ScaledColorSpace) {
                 final ScaledColorSpace scs = (ScaledColorSpace) cs;
-                final double  minimum = scs.offset;
-                final double  maximum = scs.maximum;
+                final double minimum = scs.offset;
+                final double maximum = scs.maximum;
                 ColorsForRange widest = null;
                 double widestSpan = 0;
                 for (final ColorsForRange entry : entries) {
@@ -315,8 +328,9 @@ public final class Colorizer {
                         widest = entry;
                     }
                 }
+                defaultRange = NumberRange.create(minimum, true, maximum, 
false);
                 if (widest != null && widestSpan != 
widest.sampleRange.getSpan()) {
-                    widest.sampleRange = NumberRange.create(minimum, true, 
maximum, false);
+                    widest.sampleRange = defaultRange;
                     target = null;      // For recomputing the transfer 
function later.
                 }
             }
@@ -344,9 +358,14 @@ public final class Colorizer {
          * 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.
          */
+        ColorsForRange[] entries = this.entries;
 reuse:  if (source != null) {
             target = source.forConvertedValues(false);
             if 
(target.getSampleRange().filter(Colorizer::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;
                 }
@@ -378,7 +397,7 @@ reuse:  if (source != null) {
             }
         }
         /*
-         * IF we reach this point, `source` sample dimensions were not 
specified or can not be used for
+         * If we reach this point, `source` sample dimensions were not 
specified or can not 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.
          */
@@ -387,23 +406,42 @@ reuse:  if (source != null) {
         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 Map<NumberRange<Integer>,ColorsForRange> mapper = new 
HashMap<>();
         final SampleDimension.Builder 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 processes everything else.
+         * The second loop (from 0 to `deferred`) will process everything else.
          */
         for (int i=0; i<count; i++) {
             final ColorsForRange entry = entries[i];
-            final double s = entry.sampleRange.getSpan();
-            if (Double.isNaN(s)) {
+            NumberRange<?> sourceRange = entry.sampleRange;
+            final double s = sourceRange.getSpan();
+            if (!entry.isData()) {
                 if (lower >= MAX_VALUE) {
                     throw new 
IllegalArgumentException(Resources.format(Resources.Keys.TooManyQualitatives));
                 }
-                final NumberRange<Integer> samples = NumberRange.create(lower, 
true, ++lower, false);
-                if (mapper.put(samples, entry) == null) {
-                    builder.mapQualitative(entry.name(), samples, (float) s);
+                final NumberRange<Integer> targetRange = 
NumberRange.create(lower, true, ++lower, false);
+                if (mapper.put(targetRange, entry) == null) {
+                    final CharSequence name = entry.name();
+                    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(name, targetRange, (float) 
value);
+                    } else {
+                        if (value == entry.sampleRange.getMaxDouble()) {
+                            sourceRange = NumberRange.create(value - 0.5, 
true, value + 0.5, false);
+                        }
+                        builder.addQuantitative(name, targetRange, 
sourceRange);
+                        themes = (themes != null) ? 
themes.unionAny(sourceRange) : sourceRange;
+                    }
                 }
             } else if (s > 0) {
                 // Range of real values: defer processing to next loop.
@@ -417,6 +455,27 @@ reuse:  if (source != 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 = new ColorsForRange[++count];
+                System.arraycopy(entries, deferred, tmp, ++deferred, count - 
deferred);
+                tmp[deferred-1] = new ColorsForRange(null, sourceRange, new 
Color[] {Color.BLACK, Color.WHITE});
+                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 amount of ranges.
          */
@@ -425,19 +484,17 @@ reuse:  if (source != null) {
         span = 0;
         for (int i=0; i<deferred; i++) {
             final ColorsForRange entry = entries[i];
-            if (entry != null) {
-                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;
+            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`.
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
index 32cf7e2..3236904 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
@@ -75,6 +75,11 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
     /**
      * Converts {@linkplain Map#entrySet() map entries} to an array of {@code 
ColorsForRange} entries.
      * The {@link #category} of each entry is left to null.
+     *
+     * @param  colors  the colors to use for each range of sample values.
+     *                 A {@code null} entry value means transparent.
+     * @return colors to use for each range of values in the source image.
+     *         Never null and does not contain null elements.
      */
     static ColorsForRange[] list(final 
Collection<Map.Entry<NumberRange<?>,Color[]>> colors) {
         final ColorsForRange[] entries = new ColorsForRange[colors.size()];
@@ -87,7 +92,7 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
 
     /**
      * Returns {@code true} if this entry should be taken as data, or {@code 
false} if it should be ignored.
-     * Entry to ignore and entries associated to NaN values.
+     * Entry to ignore are entries associated to NaN values.
      */
     final boolean isData() {
         return category == null || category.isQuantitative();

Reply via email to