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 d0147d6a96 When no color is specified for a category or a range of 
sample values, and provided that `Colorizer` is used for styling an existing 
image, preserve the existing colors.
d0147d6a96 is described below

commit d0147d6a9634b5ee11881fcf693dcd1d179caff4
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Apr 1 18:35:42 2023 +0200

    When no color is specified for a category or a range of sample values,
    and provided that `Colorizer` is used for styling an existing image,
    preserve the existing colors.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  58 +++++++-----
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   |  18 ++--
 .../apache/sis/internal/gui/control/ColorCell.java |   2 +-
 .../java/org/apache/sis/coverage/Category.java     |   4 +-
 .../sis/coverage/grid/GridCoverageBuilder.java     |   2 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   7 +-
 .../apache/sis/image/BandedSampleConverter.java    |   2 +-
 .../main/java/org/apache/sis/image/Colorizer.java  |  22 ++---
 .../java/org/apache/sis/image/RecoloredImage.java  |   3 +-
 .../java/org/apache/sis/image/Visualization.java   |  13 ++-
 .../internal/coverage/j2d/ColorModelBuilder.java   |  56 ++++++++---
 .../internal/coverage/j2d/ColorModelFactory.java   |  13 ++-
 .../sis/internal/coverage/j2d/ColorsForRange.java  | 105 ++++++++++++++++-----
 .../coverage/j2d/ColorModelBuilderTest.java        |   4 +-
 .../sis/internal/map/coverage/RenderingData.java   |   2 +-
 .../java/org/apache/sis/measure/NumberRange.java   |   7 +-
 .../main/java/org/apache/sis/measure/Range.java    |  23 ++++-
 .../java/org/apache/sis/measure/RangeTest.java     |  17 +++-
 19 files changed, 248 insertions(+), 112 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 735b28b475..5cfe75de75 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -21,7 +21,6 @@ import java.util.EnumMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.Future;
-import java.util.function.Function;
 import java.util.logging.LogRecord;
 import java.io.IOException;
 import java.awt.Graphics2D;
@@ -54,7 +53,6 @@ import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
-import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -62,6 +60,7 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
+import org.apache.sis.image.Colorizer;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.Interpolation;
 import org.apache.sis.storage.GridCoverageResource;
@@ -407,21 +406,44 @@ public class CoverageCanvas extends MapCanvasAWT {
      * @see #interpolationProperty
      */
     public final void setInterpolation(final Interpolation interpolation) {
-        assert Platform.isFxApplicationThread();
         interpolationProperty.set(interpolation);
     }
 
     /**
-     * Sets the colors to use for given categories in image. Invoking this 
method causes a repaint event,
-     * so it should be invoked only if at least one color is known to have 
changed.
+     * Sets the colorization algorithm to apply on rendered images.
+     * Should be an algorithm based on coverage categories.
      *
-     * @param  colors  colors to use for arbitrary categories of sample 
values, or {@code null} for default.
+     * <p>{@code CoverageCanvas} can not detect when the given colorizer 
changes its internal state.
+     * The {@link #stylingChanged()} method should be invoked explicitly when 
such change occurs.</p>
+     *
+     * @param colorizer colorization algorithm to apply on computed image, or 
{@code null} for default.
      */
-    final void setCategoryColors(final Function<Category, java.awt.Color[]> 
colors) {
+    final void setColorizer(final Colorizer colors) {
+        data.processor.setColorizer(colors);
+        stylingChanged();
+    }
+
+    /**
+     * Invoked by {@link CoverageControls} when the user selected a new color 
stretching mode.
+     * The sample values are assumed the same, only the image appearance is 
modified.
+     */
+    final void setStretching(final Stretching selection) {
         if (TRACE) {
-            trace("setCategoryColors(Function): causes repaint.");
+            trace("setStretching(%s)", selection);
         }
-        data.processor.setCategoryColors(colors);
+        if (data.selectedDerivative != selection) {
+            data.selectedDerivative = selection;
+            stylingChanged();
+        }
+    }
+
+    /**
+     * Invoked when image colors changed. Derived features such are isolines 
are assumed unchanged.
+     * This method should be invoked explicitly when the {@link Colorizer} 
changes its internal state.
+     *
+     * @see #clearRenderedImage()
+     */
+    final void stylingChanged() {
         resampledImage = null;
         requestRepaint();
     }
@@ -701,8 +723,7 @@ public class CoverageCanvas extends MapCanvasAWT {
             trace("onInterpolationSpecified(%s)", newValue);
         }
         data.processor.setInterpolation(newValue);
-        resampledImage = null;
-        requestRepaint();
+        stylingChanged();
     }
 
     /**
@@ -1114,21 +1135,6 @@ public class CoverageCanvas extends MapCanvasAWT {
         return 
Shapes2D.transform(MathTransforms.bidimensional(getObjectiveToDisplay().inverse()),
 displayBounds, null);
     }
 
-    /**
-     * Invoked by {@link CoverageControls} when the user selected a new color 
stretching mode.
-     * The sample values are assumed the same; only the image appearance is 
modified.
-     */
-    final void setStyling(final Stretching selection) {
-        if (TRACE) {
-            trace("setStyling(%s)", selection);
-        }
-        if (data.selectedDerivative != selection) {
-            data.selectedDerivative = selection;
-            resampledImage = null;
-            requestRepaint();
-        }
-    }
-
     /**
      * Invoked when an exception occurred while computing a transform but the 
painting process can continue.
      */
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index 48e6437384..719edb2fb2 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -122,7 +122,7 @@ final class CoverageControls extends ViewAndControls {
              *   - Color stretching
              */
             interpolation = InterpolationConverter.button(view);
-            stretching = Stretching.createButton((p,o,n) -> 
view.setStyling(n));
+            stretching = Stretching.createButton((p,o,n) -> 
view.setStretching(n));
             final GridPane valuesControl = Styles.createControlGrid(0,
                 label(vocabulary, Vocabulary.Keys.Interpolation, 
interpolation),
                 label(vocabulary, Vocabulary.Keys.Stretching, stretching));
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
index 987f588f7a..07fa93f7e1 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -32,6 +32,7 @@ import javafx.beans.value.ObservableValue;
 import javafx.scene.control.ContextMenu;
 import org.opengis.util.InternationalString;
 import org.apache.sis.coverage.Category;
+import org.apache.sis.image.Colorizer;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.ImmutableObjectProperty;
 import org.apache.sis.internal.gui.control.ColorRamp;
@@ -53,7 +54,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
     /**
      * Customized colors selected by user. Keys are English names of 
categories.
      *
-     * @see #key(Category)
+     * @see #apply(Category)
      */
     private final Map<String,ColorRamp> customizedColors;
 
@@ -65,9 +66,13 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
     /**
      * Creates a new styling instance.
      */
+    @SuppressWarnings("ThisEscapedInObjectConstruction")
     CoverageStyling(final CoverageCanvas canvas) {
         customizedColors = new HashMap<>();
         this.canvas = canvas;
+        if (canvas != null) {
+            canvas.setColorizer(Colorizer.forCategories(this));
+        }
     }
 
     /**
@@ -77,7 +82,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
     final void copyStyling(final CoverageStyling source) {
         customizedColors.putAll(source.customizedColors);
         if (canvas != null) {
-            canvas.setCategoryColors(customizedColors.isEmpty() ? null : this);
+            canvas.stylingChanged();
         }
     }
 
@@ -92,7 +97,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
         customizedColors.clear();
         items.setAll(content);
         if (canvas != null) {
-            canvas.setCategoryColors(null);
+            canvas.stylingChanged();
         }
     }
 
@@ -146,8 +151,8 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
     }
 
     /**
-     * Associates colors to the given category.
-     * This is invoked when users confirmed that (s)he wants to use the 
selected colors.
+     * Associates colors to the given category. This method is invoked when 
new categories are shown
+     * in table column managed by this {@code CoverageStyling}, and when user 
selects new colors.
      *
      * @param  category  the category for which to assign new color(s).
      * @param  colors    the new color for the given category, or {@code null} 
for resetting default value.
@@ -163,8 +168,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
             old = customizedColors.remove(key);
         }
         if (canvas != null && !Objects.equals(colors, old)) {
-            canvas.setCategoryColors(customizedColors.isEmpty() ? null : this);
-            // Above method call causes a repaint event even if value is the 
same.
+            canvas.stylingChanged();
         }
         return category.isQuantitative() ? ColorRamp.Type.GRADIENT : 
ColorRamp.Type.SOLID;
     }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
index 87e3df3fe1..c4a206d230 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
@@ -292,7 +292,7 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
             if (row != null) {
                 final S item = row.getItem();
                 if (item != null) {
-                    type = handler.applyColors(item, colors);
+                    type = handler.applyColors(item, (colors != 
ColorRamp.DEFAULT) ? colors : null);
                 }
             }
         }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
index 59efc35e42..df3901ef6c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
@@ -179,8 +179,8 @@ public class Category implements Serializable {
      * Creates a copy of the given category except for the {@link #converse} 
and {@link #toConverse} fields.
      * This constructor serves two purposes:
      * <ul>
-     *   <li>If {@code caller} is null, then {@link #toConverse} is is set to 
identity.
-     *       This is used only if a user specify a {@code ConvertedCategory} 
to {@link SampleDimension} constructor.
+     *   <li>If {@code caller} is null, then {@link #toConverse} is set to 
identity.
+     *       This is used only if a user specifies a {@code ConvertedCategory} 
to {@link SampleDimension} constructor.
      *       Such converted category can only come from another {@code 
SampleDimension} and may have inconsistent
      *       information for the new sample dimension that the user is 
creating.</li>
      *   <li>If {@code caller} is non-null, then {@link #toConverse} is set to 
the same transform than {@code copy} and
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index 15a52e41ad..5e46bc3419 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -470,7 +470,7 @@ public class GridCoverageBuilder {
                  */
                 bands = GridCoverage2D.defaultIfAbsent(bands, null, 
raster.getNumBands());
                 final SampleModel sm = raster.getSampleModel();
-                final ColorModelBuilder colorizer = new 
ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+                final ColorModelBuilder colorizer = new 
ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
                 final ColorModel colors;
                 if (colorizer.initialize(sm, bands.get(visibleBand)) || 
colorizer.initialize(sm, visibleBand)) {
                     colors = 
colorizer.createColorModel(ImageUtilities.getBandType(sm), bands.size(), 
visibleBand);
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index 31545550ea..1d5bd9e17e 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -661,8 +661,9 @@ public class ImageRenderer {
 
     /**
      * Specifies the colors to apply for each category in a sample dimension.
-     * The given function can return {@code null}, which means transparent.
-     * If this method is never invoked, then the default is a grayscale for
+     * The given function can return {@code null} for unrecognized categories.
+     * If this method is never invoked, or if a category is unrecognized,
+     * then the default is a grayscale for
      * {@linkplain Category#isQuantitative() quantitative categories} and
      * transparent for qualitative categories (typically "no data" values).
      *
@@ -752,7 +753,7 @@ public class ImageRenderer {
     @SuppressWarnings("UseOfObsoleteCollectionType")
     public RenderedImage createImage() {
         final Raster raster = createRaster();
-        final ColorModelBuilder colorizer = new ColorModelBuilder(colors);
+        final ColorModelBuilder colorizer = new ColorModelBuilder(colors, 
null);
         final ColorModel colors;
         final SampleModel sm = raster.getSampleModel();
         if (colorizer.initialize(sm, bands[visibleBand]) || 
colorizer.initialize(sm, visibleBand)) {
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
index 8cc2527aa5..3ba4de609c 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
@@ -238,7 +238,7 @@ class BandedSampleConverter extends ComputedImage {
             if (sampleDimensions != null && visibleBand >= 0 && visibleBand < 
sampleDimensions.length) {
                 sd = sampleDimensions[visibleBand];
             }
-            final var builder = new 
ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+            final var builder = new 
ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
             if (builder.initialize(source.getSampleModel(), sd) ||
                 builder.initialize(source.getColorModel()))
             {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
index 8888913868..f926f3e84e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
@@ -191,18 +191,19 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      * this colorizer creates a non-standard (and potentially slow) color 
model.</p>
      *
      * <h4>Default colors</h4>
-     * The {@code colors} map shall not be null or empty but may contain 
{@code null} values.
-     * Those null values are translated to default sets of colors in an 
implementation dependent way.
+     * The given {@code colors} map can associate to some keys a null or an 
empty color arrays.
+     * An empty array (i.e. no color) is interpreted as an explicit request 
for transparency.
+     * But null values are interpreted as unspecified colors,
+     * in which case the defaults are implementation dependent.
      * In current implementation, the defaults are:
      *
      * <ul>
-     *   <li>If the range minimum and maximum values are not equal, default to 
grayscale colors.</li>
+     *   <li>If this colorizer is used for {@linkplain 
ImageProcessor#visualize(RenderedImage) visualization},
+     *       try to keep the existing colors of the image to visualize.</li>
+     *   <li>Otherwise if the range minimum and maximum values are not equal, 
default to grayscale colors.</li>
      *   <li>Otherwise default to a fully transparent color.</li>
      * </ul>
      *
-     * Those defaults may change in any future Apache SIS version.
-     * For example a future version may first tries to preserve the existing 
colors of an image.
-     *
      * <h4>Limitations</h4>
      * In current implementation, the non-standard color model ignores the 
specified colors.
      * If the image data type is not 8 or 16 bits integer, the colors are 
always grayscale.
@@ -247,14 +248,13 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      * In current implementation, the defaults are:
      *
      * <ul>
-     *   <li>If all categories are unrecognized, then the colorizer returns an 
empty value.</li>
+     *   <li>If this colorizer is used for {@linkplain 
ImageProcessor#visualize(RenderedImage) visualization},
+     *       try to keep the existing colors of the image to visualize.</li>
+     *   <li>Otherwise if all categories are unrecognized, then the colorizer 
returns an empty value.</li>
      *   <li>Otherwise, {@linkplain Category#isQuantitative() quantitative} 
categories default to grayscale colors.</li>
      *   <li>Otherwise qualitative categories default to a fully transparent 
color.</li>
      * </ul>
      *
-     * Those defaults may change in any future Apache SIS version.
-     * For example a future version may first tries to preserve the existing 
colors of an image.
-     *
      * <h4>Conditions</h4>
      * This colorizer is used when {@link Target#getRanges()} provides a 
non-empty value.
      * That value is typically fetched from the {@value 
PlanarImage#SAMPLE_DIMENSIONS_KEY} image property,
@@ -280,7 +280,7 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
                     final List<SampleDimension> ranges = 
target.getRanges().orElse(null);
                     if (visibleBand < ranges.size()) {
                         final SampleModel model = target.getSampleModel();
-                        final var c = new ColorModelBuilder(colors);
+                        final var c = new ColorModelBuilder(colors, null);
                         if (c.initialize(model, ranges.get(visibleBand))) {
                             return 
Optional.ofNullable(c.createColorModel(model.getDataType(), 
model.getNumBands(), visibleBand));
                         }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
index ce24f13234..564d45b88f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
@@ -304,8 +304,7 @@ final class RecoloredImage extends ImageAdapter {
             Arrays.fill(ARGB, end+1, validMax+1, icm.getRGB(validMax));
             final float scale = (float) ((validMax - validMin) / (maximum - 
minimum));
             for (int i = start; i <= end; i++) {
-                final float s = (i - start) * scale + validMin;
-                ARGB[i] = icm.getRGB(Math.round(s));
+                ARGB[i] = icm.getRGB(Math.round((i - start) * scale) + 
validMin);
             }
             final SampleModel sm = source.getSampleModel();
             cm = ColorModelFactory.createIndexColorModel(sm.getNumBands(), 
visibleBand, ARGB, icm.hasAlpha(), icm.getTransparentPixel());
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index d83417ecba..ed6313022e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -287,6 +287,7 @@ final class Visualization extends ResampledImage {
             if (colorizer != null) {
                 colorModel = colorizer.apply(target).orElse(null);
             }
+            final ColorModel sourceCM = coloredSource.getColorModel();
             /*
              * Get a `ColorModelBuilder` which will compute the `ColorModel` 
of destination image.
              * There is different ways to setup the builder, depending on 
which `Colorizer` is used.
@@ -300,15 +301,14 @@ final class Visualization extends ResampledImage {
             final ColorModelBuilder builder;
             final var rangeColors = target.rangeColors;
             if (rangeColors != null && !rangeColors.isEmpty()) {
-                builder = new ColorModelBuilder(rangeColors.entrySet());
+                builder = new ColorModelBuilder(rangeColors.entrySet(), 
sourceCM);
                 initialized = true;
             } else {
                 /*
                  * Ranges of sample values were not specified explicitly. 
Instead, we will try to infer them
                  * in various ways: sample dimensions, scaled color model, or 
image statistics in last resort.
                  */
-                builder = new ColorModelBuilder(target.categoryColors);
-                final ColorModel colorModel = coloredSource.getColorModel();
+                builder = new ColorModelBuilder(target.categoryColors, 
sourceCM);
                 initialized = 
builder.initialize(coloredSource.getSampleModel(), visibleSD);
                 if (initialized) {
                     /*
@@ -317,7 +317,7 @@ final class Visualization extends ResampledImage {
                      * determined by the SampleModel, then user enhanced 
contrast by a call to `stretchColorRamp(…)`.
                      * We want to preserve that contrast enhancement.
                      */
-                    builder.rescaleMainRange(colorModel);
+                    builder.rescaleMainRange(sourceCM);
                 } else {
                     /*
                      * At this point there is no more user-supplied colors 
(through `Colorizer`) that we can use.
@@ -325,7 +325,7 @@ final class Visualization extends ResampledImage {
                      * There is no call to `rescaleMainRange(…)` because the 
following code already uses the range
                      * specified by the ColorModel, if available.
                      */
-                    initialized = builder.initialize(colorModel);
+                    initialized = builder.initialize(sourceCM);
                     if (!initialized) {
                         if (coloredSource instanceof RecoloredImage) {
                             final RecoloredImage colored = (RecoloredImage) 
coloredSource;
@@ -353,6 +353,9 @@ final class Visualization extends ResampledImage {
                 builder.getSampleToIndexValues()            // Must be after 
`compactColorModel(…)`.
             };
             if (shortcut) {
+                if (converters[0].isIdentity() && colorModel.equals(sourceCM)) 
{
+                    return coloredSource;
+                }
                 interpolation = Interpolation.NEAREST;
             } else {
                 interpolation = combine(interpolation.toCompatible(source), 
converters);
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
index 455899fa16..2243187865 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
@@ -51,8 +51,9 @@ import org.apache.sis.util.resources.Vocabulary;
  * <ol>
  *   <li>Create a new {@link ColorModelBuilder} instance.</li>
  *   <li>Invoke one of {@code initialize(…)} methods.</li>
- *   <li>Invoke {@link #createColorModel(int, int, int)}.</li>
- *   <li>Discards {@code ColorModelBuilder}; each instance should be used only 
once.</li>
+ *   <li>Invoke {@link #createColorModel(int, int, int)} or {@link 
#compactColorModel(int, int)}.</li>
+ *   <li>Invoke {@link #getSampleToIndexValues()} if this auxiliary 
information is useful.</li>
+ *   <li>Discards {@code ColorModelBuilder}. Each instance shall be used only 
once.</li>
  * </ol>
  *
  * There is no {@code initialize(Raster)} or {@code initialize(RenderedImage)} 
method because if those methods
@@ -126,8 +127,8 @@ public final class ColorModelBuilder {
 
     /**
      * 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.
+     * This array is initially null and created by an {@code initialize(…)} 
method.
+     * After initialization, this array shall not contain null element.
      */
     private ColorsForRange[] entries;
 
@@ -146,14 +147,26 @@ public final class ColorModelBuilder {
      * <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>
+     *
+     * @see #getSampleToIndexValues()
      */
     private SampleDimension target;
 
     /**
-     * Default range of values to use if no explicitly specified by a {@link 
Category}.
+     * Default range of values to use if not explicitly specified by a {@link 
Category}.
      */
     private NumberRange<?> defaultRange;
 
+    /**
+     * Colors to inherit if a range of values is undefined, or {@code null} if 
none.
+     * This field should be non-null only when this builder is used for 
styling an image before visualization.
+     * This field should be null when this builder is created for creating a 
new image because the meaning of
+     * pixel values may be completely different (i.e. meaning of {@linkplain 
#source} may not be applicable).
+     *
+     * @see ColorsForRange#isUndefined()
+     */
+    private final ColorModel inheritedColors;
+
     /**
      * Creates a new colorizer which will apply colors on the given range of 
values in source image.
      * The {@code ColorModelBuilder} is considered initialized after this 
constructor;
@@ -164,11 +177,14 @@ public final class ColorModelBuilder {
      * and to grayscale colors otherwise.
      * Empty arrays of colors are interpreted as explicitly transparent.</p>
      *
-     * @param  colors  the colors to use for each range of values in source 
image.
+     * @param  colors     the colors to use for each range of values in source 
image.
+     * @param  inherited  the colors to use as fallback if some ranges have 
undefined colors, or {@code null}.
+     *                    Should be non-null only for styling an exiting image 
before visualization.
      */
-    public ColorModelBuilder(final 
Collection<Map.Entry<NumberRange<?>,Color[]>> colors) {
+    public ColorModelBuilder(final 
Collection<Map.Entry<NumberRange<?>,Color[]>> colors, final ColorModel 
inherited) {
         ArgumentChecks.ensureNonEmpty("colors", colors);
-        entries = ColorsForRange.list(colors);
+        entries = ColorsForRange.list(colors, inherited);
+        inheritedColors = inherited;
         this.colors = GRAYSCALE;
     }
 
@@ -176,11 +192,19 @@ public final class ColorModelBuilder {
      * Creates a new colorizer which will use the given function for 
determining the colors to apply.
      * Callers need to invoke an {@code initialize(…)} method after this 
constructor.
      *
-     * @param  colors  the colors to use for each category, or {@code null} 
for default.
-     *                 The function may return {@code null} for unrecognized 
categories.
+     * <p>The {@code inherited} parameter is non-null when this builder is 
created for styling
+     * an existing image before visualization. This parameter should be null 
when this builder
+     * is created for creating a new image, even when that new image is 
derived from a source,
+     * because the meaning of pixel values may be completely different.</p>
+     *
+     * @param  colors     the colors to use for each category, or {@code null} 
for default.
+     *                    The function may return {@code null} for 
unrecognized categories.
+     * @param  inherited  the colors to use as fallback for unrecognized 
categories, or {@code null}.
+     *                    Should be non-null only for styling an exiting image 
before visualization.
      */
-    public ColorModelBuilder(final Function<Category,Color[]> colors) {
+    public ColorModelBuilder(final Function<Category,Color[]> colors, final 
ColorModel inherited) {
         this.colors = (colors != null) ? colors : GRAYSCALE;
+        inheritedColors = inherited;
     }
 
     /**
@@ -222,7 +246,7 @@ public final class ColorModelBuilder {
                 boolean missingNodata = true;
                 ColorsForRange[] entries = new 
ColorsForRange[categories.size()];
                 for (int i=0; i<entries.length; i++) {
-                    final var range = new ColorsForRange(categories.get(i), 
colors);
+                    final var range = new ColorsForRange(categories.get(i), 
colors, inheritedColors);
                     isUndefined &= range.isUndefined();
                     missingNodata &= range.isData;
                     entries[i] = range;
@@ -236,7 +260,7 @@ public final class ColorModelBuilder {
                         final int count = entries.length;
                         entries = Arrays.copyOf(entries, count + 1);
                         entries[count] = new ColorsForRange(TRANSPARENT,
-                                NumberRange.create(Float.class, Float.NaN), 
null, false);
+                                NumberRange.create(Float.class, Float.NaN), 
null, false, inheritedColors);
                     }
                     // Leave `target` to null. It will be computed by 
`compact()` if needed.
                     this.entries = entries;
@@ -351,7 +375,9 @@ public final class ColorModelBuilder {
         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, colors);
+            final var range = new 
ColorsForRange(category.forConvertedValues(true), colors, inheritedColors);
+            range.sampleRange = category.getSampleRange();
+            entries[i] = range;
         }
         this.entries = entries;
     }
@@ -530,7 +556,7 @@ reuse:  if (source != null) {
                 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);
+                tmp[deferred-1] = new ColorsForRange(null, sourceRange, null, 
true, null);
                 entries = tmp;
             }
         }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
index 510fa6cd61..69410a46a4 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
@@ -209,7 +209,7 @@ public final class ColorModelFactory {
                             }
                         }
                     }
-                    codes [  count] = entry.toARGB();
+                    codes [  count] = entry.toARGB(upper - lower);
                     starts[  count] = lower;
                     starts[++count] = upper;
                 }
@@ -309,7 +309,8 @@ public final class ColorModelFactory {
     public static ColorModelFactory piecewise(final Map<NumberRange<?>, 
Color[]> colors) {
         final var entries = colors.entrySet();
         ArgumentChecks.ensureNonEmpty("colors", entries);
-        return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 
0, DEFAULT_VISIBLE_BAND, ColorsForRange.list(entries)));
+        final var ranges = ColorsForRange.list(entries, null);
+        return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 
0, DEFAULT_VISIBLE_BAND, ranges));
     }
 
     /**
@@ -479,7 +480,7 @@ public final class ColorModelFactory {
     {
         ArgumentChecks.ensureNonEmpty("colors", colors);
         return createPiecewise(dataType, numBands, visibleBand, new 
ColorsForRange[] {
-            new ColorsForRange(null, new NumberRange<>(Double.class, lower, 
true, upper, false), colors, true)
+            new ColorsForRange(null, new NumberRange<>(Double.class, lower, 
true, upper, false), colors, true, null)
         });
     }
 
@@ -779,7 +780,7 @@ public final class ColorModelFactory {
     /**
      * Copies {@code colors} into {@code ARGB} array from index {@code lower} 
inclusive to index {@code upper} exclusive.
      * If {@code upper-lower} is not equal to the length of {@code colors} 
array, then colors will be interpolated.
-     * The given {@code colors} array must be initialized with zero values in 
the {@code lower} … {@code upper} range.
+     * The given {@code ARGB} array must be initialized with zero values in 
the {@code lower} … {@code upper} range.
      *
      * @param  colors  colors to copy into the {@code ARGB} array.
      * @param  ARGB    array of integer to write ARGB values into.
@@ -796,6 +797,10 @@ public final class ColorModelFactory {
             case 1: ARGB[lower] = colors[0];                            // 
fall through
             case 0: return;
         }
+        if (upper - lower == colors.length) {
+            System.arraycopy(colors, 0, ARGB, 0, colors.length);
+            return;
+        }
         /*
          * Prepares the coefficients for the iteration.
          * The non-final ones will be updated inside the loop.
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 31894df212..76dc35f90a 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
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.Objects;
 import java.util.function.Function;
 import java.awt.Color;
+import java.awt.image.ColorModel;
 import java.awt.image.IndexColorModel;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.measure.NumberRange;
@@ -53,6 +54,13 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
      */
     NumberRange<?> sampleRange;
 
+    /**
+     * The range of sample values as originally specified.
+     * Contrarily to {@link #sampleRange}, this range will not be modified by 
{@code compact()}.
+     * This is used for fetching colors from {@link #inheritedColors} if 
{@link #colors} is null.
+     */
+    private final NumberRange<?> originalSampleRange;
+
     /**
      * The colors to apply on the range of sample values.
      * An empty array means that the category is explicitly specified as 
transparent.
@@ -60,10 +68,22 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
      * is grayscale for quantitative category and transparent for qualitative 
category.
      *
      * @see #isUndefined()
-     * @see #toARGB()
+     * @see #toARGB(int)
      */
     private final Color[] colors;
 
+    /**
+     * The original colors, or {@code null} if unspecified.
+     * This is used as a fallback if {@link #colors} is null.
+     * This field should be non-null only when this {@code ColorsForRange} is 
created for
+     * styling an image before visualization. It should be null when creating 
a new image,
+     * because the meaning of pixel values (i.e. the sample dimensions) may be 
different.
+     *
+     * @see #originalSampleRange
+     * @see ColorModelBuilder#inheritedColors
+     */
+    private final ColorModel inheritedColors;
+
     /**
      * {@code true} if this entry should be taken as data, or {@code false} if 
it should be ignored.
      * Entry to ignore are entries associated to NaN values.
@@ -73,57 +93,71 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
     /**
      * Creates a new instance for the given category.
      *
-     * @param  category  the category for which this {@code ColorsForRange} is 
created, or {@code null}.
-     * @param  colors    colors to apply on the category.
+     * @param  category   the category for which this {@code ColorsForRange} 
is created.
+     * @param  colors     colors to apply on the category.
+     * @param  inherited  the original colors to use as fallback, or {@code 
null} if none.
+     *                    Should be non-null only for styling an exiting image 
before visualization.
      */
-    ColorsForRange(final Category category, final Function<Category,Color[]> 
colors) {
-        final CharSequence name = category.getName();
-        this.name        = (name != null) ? name : sampleRange.toString();
+    ColorsForRange(final Category category, final Function<Category,Color[]> 
colors, final ColorModel inherited) {
+        this.name        = category.getName();
         this.sampleRange = category.getSampleRange();
-        this.colors      = colors.apply(category);
         this.isData      = category.isQuantitative();
+        this.colors      = colors.apply(category);
+        inheritedColors  = inherited;
+        originalSampleRange = sampleRange;
     }
 
     /**
      * Creates a new instance for the given range of values.
      *
-     * @param  name         a name identifying the range of values, or {@code 
null} for automatic.
-     * @param  sampleRange  range of sample values on which the colors will be 
applied.
-     * @param  colors       colors to apply on the range of sample values, or 
{@code null} for default.
-     * @param  isData       whether this entry should be taken as main data 
(not fill values).
+     * @param  name          a name identifying the range of values, or {@code 
null} for automatic.
+     * @param  sampleRange   range of sample values on which the colors will 
be applied.
+     * @param  colors        colors to apply on the range of sample values, or 
{@code null} for default.
+     * @param  isData        whether this entry should be taken as main data 
(not fill values).
+     * @param  inherited     the original colors to use as fallback, or {@code 
null} if none.
+     *                       Should be non-null only for styling an exiting 
image before visualization.
      */
-    ColorsForRange(final CharSequence name, final NumberRange<?> sampleRange, 
final Color[] colors, final boolean isData) {
+    ColorsForRange(final CharSequence name, final NumberRange<?> sampleRange, 
final Color[] colors,
+                   final boolean isData, final ColorModel inherited)
+    {
         ArgumentChecks.ensureNonNull("sampleRange", sampleRange);
         this.name        = (name != null) ? name : sampleRange.toString();
-        this.sampleRange = sampleRange;
-        this.colors      = colors;
+        this.sampleRange = originalSampleRange = sampleRange;
         this.isData      = isData;
+        this.colors      = colors;
+        inheritedColors  = inherited;
     }
 
     /**
      * Returns {@code true} if no color has been specified for this range.
      * Note that "undefined" is not the same as fully transparent color.
+     *
+     * <p>If no colors were explicitly defined but a fallback exists, then 
this method considers
+     * this range as defined for allowing {@link ColorModelBuilder} to inherit 
those colors with
+     * the range of values specified by {@link #originalSampleRange}. We 
conceptually accept any
+     * {@link #inheritedColors} even if {@link #toARGB(int)} can not handle 
all of them.</p>
      */
     final boolean isUndefined() {
-        return colors == null;
+        return colors == null && inheritedColors == null;
     }
 
     /**
      * 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.
+     * @param  colors     the colors to use for each range of sample values.
+     *                    A {@code null} entry value means transparent.
+     * @param  inherited  the original color model from which to inherit 
undefined colors, or {@code null} if none.
      * @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) {
+    static ColorsForRange[] list(final 
Collection<Map.Entry<NumberRange<?>,Color[]>> colors, final ColorModel 
inherited) {
         final ColorsForRange[] entries = new ColorsForRange[colors.size()];
         int n = 0;
         for (final Map.Entry<NumberRange<?>,Color[]> entry : colors) {
             final NumberRange<?> range = entry.getKey();
             boolean singleton = Objects.equals(range.getMinValue(), 
range.getMaxValue());
-            entries[n++] = new ColorsForRange(null, range, entry.getValue(), 
!singleton);
+            entries[n++] = new ColorsForRange(null, range, entry.getValue(), 
!singleton, inherited);
         }
         return ArraysExt.resize(entries, n);            // `resize` should not 
be needed, but we are paranoiac.
     }
@@ -134,7 +168,7 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
     @Override
     public String toString() {
         final StringBuilder buffer = new StringBuilder(name).append(": 
").append(sampleRange);
-        appendColorRange(buffer, toARGB());
+        appendColorRange(buffer, toARGB(2));
         return buffer.toString();
     }
 
@@ -197,9 +231,10 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
      * Returns the ARGB codes for the colors.
      * If all colors are transparent, returns an empty array.
      *
-     * @return ARGB codes for the given colors. Never {@code null} but may be 
empty.
+     * @param  length  desired array length. This is only a hint and may be 
ignored.
+     * @return ARGB codes for this color ramp. Never {@code null} but may be 
empty.
      */
-    final int[] toARGB() {
+    final int[] toARGB(final int length) {
         if (colors != null) {
             int combined = 0;
             final int[] ARGB = new int[colors.length];
@@ -214,6 +249,32 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
             if ((combined & 0xFF000000) != 0) {
                 return ARGB;
             }
+        } else if (!originalSampleRange.isEmpty() && inheritedColors 
instanceof IndexColorModel) {
+            /*
+             * If colors are undefined, try to inherit them from the original 
colors.
+             * If the number of available colors is larger than the desired 
number,
+             * this block returns a subset of the inherited colors.
+             */
+            final IndexColorModel icm = (IndexColorModel) inheritedColors;
+            int offset = Math.round((float) 
originalSampleRange.getMinDouble(true));
+            int numSrc = Math.round((float) 
originalSampleRange.getMaxDouble()) - offset;
+            if (originalSampleRange.isMinIncluded()) numSrc++;
+            final int[] ARGB;
+            if (numSrc <= length) {
+                ARGB = new int[numSrc];
+                if (offset == 0 && numSrc == icm.getMapSize()) {
+                    icm.getRGBs(ARGB);
+                } else for (int i=0; i<numSrc; i++) {
+                    ARGB[i] = icm.getRGB(i + offset);
+                }
+            } else {
+                ARGB = new int[length];
+                final float scale = ((float) (numSrc-1)) / (length-1);
+                for (int i=0; i<length; i++) {
+                    ARGB[i] = icm.getRGB(Math.round(i * scale) + offset);
+                }
+            }
+            return ARGB;
         } else if (isData) {
             return new int[] {0xFF000000, 0xFFFFFFFF};
         }
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
index 66bb12b931..012e1dcae6 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
@@ -52,7 +52,7 @@ public final class ColorModelBuilderTest extends TestCase {
         final ColorModelBuilder colorizer = new ColorModelBuilder(List.of(
                 new SimpleEntry<>(NumberRange.create(0, true,  0, true), new 
Color[] {Color.GRAY}),
                 new SimpleEntry<>(NumberRange.create(1, true,  1, true), new 
Color[] {ColorModelFactory.TRANSPARENT}),
-                new SimpleEntry<>(NumberRange.create(2, true, 15, true), new 
Color[] {Color.BLUE, Color.WHITE, Color.RED})));
+                new SimpleEntry<>(NumberRange.create(2, true, 15, true), new 
Color[] {Color.BLUE, Color.WHITE, Color.RED})), null);
         /*
          * No conversion of sample values should be necessary because the
          * above-given ranges already fit in a 4-bits IndexColormodel.
@@ -99,7 +99,7 @@ public final class ColorModelBuilderTest extends TestCase {
                 .addQualitative ("Error", MathFunctions.toNanFloat(3))
                 .setName("Temperature").build();
 
-        final ColorModelBuilder colorizer = new 
ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+        final ColorModelBuilder colorizer = new 
ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
         assertTrue("initialize", colorizer.initialize(null, sd));
         final IndexColorModel cm = (IndexColorModel) 
colorizer.compactColorModel(1, 0);     // Must be first.
         /*
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 6c51dd0946..0b5f9efe5b 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -662,7 +662,7 @@ public class RenderingData implements Cloneable {
          */
         if (CREATE_INDEX_COLOR_MODEL) {
             final ColorModelType ct = 
ColorModelType.find(recoloredImage.getColorModel());
-            if (ct.isSlow || (ct.useColorRamp && processor.getColorizer() != 
null)) try {
+            if (ct.isSlow || ct.useColorRamp) try {
                 SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(dataRanges);
                 return processor.visualize(recoloredImage, bounds, 
displayToCenter);
             } finally {
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
index 80f72e2717..5be9072aa1 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
@@ -108,11 +108,10 @@ public class NumberRange<E extends Number & Comparable<? 
super E>> extends Range
     /**
      * Returns a unique instance of the given range, except if the range is 
empty.
      *
-     * <div class="note"><b>Rational:</b>
-     * we exclude empty ranges because the {@link Range#equals(Object)} 
consider them as equal.
+     * <h4>Rational</h4>
+     * We exclude empty ranges because the {@link Range#equals(Object)} 
consider them as equal.
      * Consequently, if empty ranges were included in the pool, this method 
would return in some
      * occasions an empty range with different values than the given {@code 
range} argument.
-     * </div>
      *
      * We use this method only for caching range of wrapper of primitive types 
({@link Byte},
      * {@link Short}, <i>etc.</i>) because those types are known to be 
immutable.
@@ -645,7 +644,7 @@ public class NumberRange<E extends Number & Comparable<? 
super E>> extends Range
 
     /**
      * Computes the difference between minimum and maximum values. If numbers 
are integers, the difference is computed
-     * using inclusive values (e.g. equivalent to <code>{@linkplain 
#getMinDouble(boolean) getMinDouble}(true)</code>).
+     * using inclusive values (e.g. using <code>{@linkplain 
#getMinDouble(boolean) getMinDouble}(true)</code>).
      * Otherwise the minimum and maximum values are used as-is
      * (because making them inclusive is considered an infinitely small 
change).
      *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
index 6e63f3f36b..ffe769cf1b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
@@ -81,7 +81,7 @@ import org.apache.sis.util.Numbers;
  * @author  Joe White
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Jody Garnett (for parameterized type inspiration)
- * @version 1.0
+ * @version 1.4
  *
  * @param <E>  the type of range elements, typically a {@link Number} subclass 
or {@link java.util.Date}.
  *
@@ -269,7 +269,7 @@ public class Range<E extends Comparable<? super E>> 
implements CheckedContainer<
      * Returns {@code true} if this range is empty. A range is empty if the
      * {@linkplain #getMinValue() minimum value} is greater than the
      * {@linkplain #getMaxValue() maximum value}, or if they are equal while
-     * at least one of them is exclusive.
+     * at least one of them is exclusive, or if both bounds are NaN.
      *
      * <h4>API note</h4>
      * This method is final because often used by the internal implementation.
@@ -286,8 +286,25 @@ public class Range<E extends Comparable<? super E>> 
implements CheckedContainer<
         if (c < 0) {
             return false;                               // Minimum is smaller 
than maximum.
         }
+        if (c != 0) {                                   // Minimum is NaN or 
greater than maximum.
+            return !isNaN(minValue);
+        }
         // If min and max are equal, then the range is empty if at least one 
of them is exclusive.
-        return (c != 0) || !isMinIncluded || !isMaxIncluded;
+        if (!isMinIncluded || !isMaxIncluded) {
+            return true;
+        }
+        return isNaN(minValue);                         // At this point if 
min is NaN, max is also NaN.
+    }
+
+    /**
+     * Returns {@code true} if the given value is NaN. This method tests only 
the primitive wrappers
+     * because the behavior of their {@code compareTo(…)} method is clearly 
documented. Calls to this
+     * method assume that NaNs are considered by {@code compareTo(…)} as 
greater than all other values.
+     */
+    private static boolean isNaN(final Object value) {
+        if (value instanceof Double) return ((Double) value).isNaN();
+        if (value instanceof Float)  return ((Float)  value).isNaN();
+        return false;
     }
 
     /**
diff --git 
a/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java 
b/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java
index f2b2750853..d266990144 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java
@@ -30,7 +30,7 @@ import static org.apache.sis.test.Assert.*;
  *
  * @author  Joe White
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   0.3
  */
 public final class RangeTest extends TestCase {
@@ -103,6 +103,21 @@ public final class RangeTest extends TestCase {
         new Range(String.class, 123.233, true, 8740.09, true);
     }
 
+    /**
+     * Tests {@link Range#isEmpty()}.
+     */
+    @Test
+    public void testIsEmpty() {
+        assertFalse(new Range<>(Float.class, 3f,        true, 5f,        
true).isEmpty());
+        assertFalse(new Range<>(Float.class, 3f,        true, 3f,        
true).isEmpty());
+        assertTrue (new Range<>(Float.class, 3f,        true, 3f,       
false).isEmpty());
+        assertTrue (new Range<>(Float.class, 3f,       false, 3f,        
true).isEmpty());
+        assertTrue (new Range<>(Float.class, 3f,       false, 3f,       
false).isEmpty());
+        assertFalse(new Range<>(Float.class, Float.NaN, true, 5f,        
true).isEmpty());
+        assertFalse(new Range<>(Float.class, 3f,        true, Float.NaN, 
true).isEmpty());
+        assertTrue (new Range<>(Float.class, Float.NaN, true, Float.NaN, 
true).isEmpty());
+    }
+
     /**
      * Tests the {@link Range#contains(Comparable)} method.
      */

Reply via email to