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 4e347cda9d Tune the `Colorizer` contract for saying that a null 
`Color[]` array means to use default colors, which are not necessarily 
transparent. If the range has more than one value, that default is now 
grayscale.
4e347cda9d is described below

commit 4e347cda9d295a552e2acf08d91ae4c2786b25cd
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Mar 31 20:09:34 2023 +0200

    Tune the `Colorizer` contract for saying that a null `Color[]` array
    means to use default colors, which are not necessarily transparent.
    If the range has more than one value, that default is now grayscale.
    
    JavaFX application:
    Add an "Original colors" item in the table of category colors.
    It uses the above-cited new meaning for null `Color[]` array.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |   7 --
 .../apache/sis/gui/coverage/CoverageControls.java  |   3 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   | 116 +++++++++------------
 .../apache/sis/internal/gui/control/ColorCell.java |  31 ++++--
 .../internal/gui/control/ColorColumnHandler.java   |  31 +-----
 .../apache/sis/internal/gui/control/ColorRamp.java |  51 +++++++--
 .../sis/internal/gui/control/ValueColorMapper.java |  12 ---
 .../sis/internal/gui/control/package-info.java     |   2 +-
 .../sis/gui/coverage/CoverageStylingApp.java       |   5 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   2 +-
 .../java/org/apache/sis/image/BandSelectImage.java |   2 +-
 .../main/java/org/apache/sis/image/Colorizer.java  |  51 +++++++--
 .../java/org/apache/sis/image/ImageProcessor.java  |   7 +-
 .../java/org/apache/sis/image/Visualization.java   |  13 ++-
 .../internal/coverage/j2d/ColorModelBuilder.java   |  80 ++++++++++----
 .../internal/coverage/j2d/ColorModelFactory.java   |  15 +--
 .../sis/internal/coverage/j2d/ColorsForRange.java  |  45 +++++---
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../org/apache/sis/internal/netcdf/Raster.java     |  11 +-
 .../apache/sis/internal/netcdf/RasterResource.java |   4 +-
 22 files changed, 287 insertions(+), 208 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 07ecc32463..735b28b475 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
@@ -411,13 +411,6 @@ public class CoverageCanvas extends MapCanvasAWT {
         interpolationProperty.set(interpolation);
     }
 
-    /**
-     * Returns the colors to use for given categories of sample values, or 
{@code null} is unspecified.
-     */
-    final Function<Category, java.awt.Color[]> getCategoryColors() {
-        return data.processor.getCategoryColors();
-    }
-
     /**
      * 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.
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 b277c36422..48e6437384 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
@@ -49,7 +49,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * The controls are updated when the coverage shown in {@link CoverageCanvas} 
is changed.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.1
  */
 final class CoverageControls extends ViewAndControls {
@@ -221,7 +221,6 @@ final class CoverageControls extends ViewAndControls {
      */
     final void copyStyling(final CoverageControls c) {
         styling.copyStyling(c.styling);
-        view.setCategoryColors(c.view.getCategoryColors() == null ? null : 
styling);
         GUIUtilities.copySelection(c.stretching, stretching);
         GUIUtilities.copySelection(c.interpolation, interpolation);
     }
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 8f2a2d51a7..987f588f7a 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
@@ -17,10 +17,10 @@
 package org.apache.sis.gui.coverage;
 
 import java.awt.Color;
-import java.util.Arrays;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.function.Function;
 import javafx.geometry.Pos;
 import javafx.scene.control.TableCell;
@@ -30,14 +30,13 @@ import javafx.scene.control.MenuItem;
 import javafx.collections.ObservableList;
 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.internal.coverage.j2d.ColorModelBuilder;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.ImmutableObjectProperty;
 import org.apache.sis.internal.gui.control.ColorRamp;
 import org.apache.sis.internal.gui.control.ColorColumnHandler;
 import org.apache.sis.util.resources.Vocabulary;
-import org.opengis.util.InternationalString;
 
 
 /**
@@ -47,7 +46,7 @@ import org.opengis.util.InternationalString;
  * that may change in any future version.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.1
  */
 final class CoverageStyling extends ColorColumnHandler<Category> implements 
Function<Category,Color[]> {
@@ -56,12 +55,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
      *
      * @see #key(Category)
      */
-    private final Map<String,int[]> customizedColors;
-
-    /**
-     * The fallback to use if no color is defined in this {@code 
CoverageStyling} for a category.
-     */
-    private final Function<Category,Color[]> fallback;
+    private final Map<String,ColorRamp> customizedColors;
 
     /**
      * The view to notify when a color changed, or {@code null} if none.
@@ -74,22 +68,17 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
     CoverageStyling(final CoverageCanvas canvas) {
         customizedColors = new HashMap<>();
         this.canvas = canvas;
-        if (canvas != null) {
-            final Function<Category, Color[]> c = canvas.getCategoryColors();
-            if (c != null) {
-                fallback = c;
-                return;
-            }
-        }
-        fallback = ColorModelBuilder.GRAYSCALE;
     }
 
     /**
-     * Copy styling information from the given source.
+     * Copies styling information from the given source.
      * This is used when the user clicks on "New window" button.
      */
     final void copyStyling(final CoverageStyling source) {
         customizedColors.putAll(source.customizedColors);
+        if (canvas != null) {
+            canvas.setCategoryColors(customizedColors.isEmpty() ? null : this);
+        }
     }
 
     /**
@@ -111,79 +100,72 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
      * Returns the key to use in {@link #customizedColors} for the given 
category.
      */
     private static String key(final Category category) {
-        return category.getName().toString(Locale.ENGLISH);
+        return category.getName().toString(Locale.ROOT);
     }
 
     /**
-     * Associates colors to the given category.
+     * Returns the colors to apply for the given category as an observable 
value.
      *
-     * @param  colors  the new color for the given category, or {@code null} 
for resetting default value.
-     */
-    final void setARGB(final Category category, final int[] colors) {
-        final String key = key(category);
-        final int[] old;
-        if (colors != null && colors.length != 0) {
-            old = customizedColors.put(key, colors);
-        } else {
-            old = customizedColors.remove(key);
-        }
-        if (canvas != null && !Arrays.equals(colors, old)) {
-            canvas.setCategoryColors(this);                     // Causes a 
repaint event.
-        }
-    }
-
-    /**
-     * Returns the colors to apply for the given category, or {@code null} for 
transparent.
-     * Does the same work as {@link #apply(Category)}, but returns colors as 
an array of ARGB codes.
-     * Contrarily to {@link #apply(Category)}, this method may return 
references to internal arrays;
-     * <strong>do not modify.</strong>
+     * @param  row  the row item for which to get color to show in color cell. 
Never {@code null}.
+     * @return the color(s) to use for the given row, or {@code null} if none 
(transparent).
      */
     @Override
-    protected int[] getARGB(final Category category) {
-        int[] ARGB = customizedColors.get(key(category));
-        if (ARGB == null) {
-            final Color[] colors = fallback.apply(category);
-            if (colors != null) {
-                ARGB = new int[colors.length];
-                for (int i=0; i<colors.length; i++) {
-                    ARGB[i] = colors[i].getRGB();
-                }
+    protected ObservableValue<ColorRamp> getObservableValue(final Category 
category) {
+        ColorRamp ramp = customizedColors.get(key(category));
+        if (ramp == null) {
+            if (!category.isQuantitative()) {
+                return null;
             }
+            ramp = ColorRamp.DEFAULT;
         }
-        return ARGB;
+        return new ImmutableObjectProperty<>(ramp);
     }
 
     /**
-     * Returns the colors to apply for the given category, or {@code null} for 
transparent.
-     * This method returns copies of internal arrays; changes to the returned 
array do not
-     * affect this {@code CoverageStyling} (assuming {@link #fallback} also 
does copies).
+     * Returns the colors to apply for the given category, or {@code null} for 
default.
+     * This method returns copies of internal arrays; changes to the returned 
array do
+     * not affect this {@code CoverageStyling}.
      *
      * @param  category  the category for which to get the colors.
-     * @return colors to apply for the given category, or {@code null}.
+     * @return colors to apply for the given category, or {@code null} if the 
category is unrecognized.
      */
     @Override
     public Color[] apply(final Category category) {
-        final int[] ARGB = customizedColors.get(key(category));
-        if (ARGB != null) {
-            final Color[] colors = new Color[ARGB.length];
-            for (int i=0; i<colors.length; i++) {
-                colors[i] = new Color(ARGB[i], true);
+        final ColorRamp ramp = customizedColors.get(key(category));
+        if (ramp != null) {
+            final int[] ARGB = ramp.colors;
+            if (ARGB != null) {
+                final Color[] colors = new Color[ARGB.length];
+                for (int i=0; i<colors.length; i++) {
+                    colors[i] = new Color(ARGB[i], true);
+                }
+                return colors;
             }
-            return colors;
         }
-        return fallback.apply(category);
+        return null;
     }
 
     /**
-     * Invoked when users confirmed that (s)he wants to use the selected 
colors.
+     * Associates colors to the given category.
+     * This is invoked when users confirmed that (s)he wants to use the 
selected 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.
      * @return the type of color (solid or gradient) shown for the given value.
      */
     @Override
-    protected ColorRamp.Type applyColors(final Category category, ColorRamp 
colors) {
-        setARGB(category, (colors != null) ? colors.colors : null);
+    protected ColorRamp.Type applyColors(final Category category, final 
ColorRamp colors) {
+        final String key = key(category);
+        final ColorRamp old;
+        if (colors != null) {
+            old = customizedColors.put(key, colors);
+        } else {
+            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.
+        }
         return category.isQuantitative() ? ColorRamp.Type.GRADIENT : 
ColorRamp.Type.SOLID;
     }
 
@@ -195,7 +177,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
      *                     (this argument would be removed if this method was 
public API).
      */
     final TableView<Category> createCategoryTable(final Resources resources, 
final Vocabulary vocabulary) {
-        final TableColumn<Category,String> name = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
+        final var name = new 
TableColumn<Category,String>(vocabulary.getString(Vocabulary.Keys.Name));
         name.setCellValueFactory(CoverageStyling::getCategoryName);
         name.setCellFactory(CoverageStyling::createNameCell);
         name.setEditable(false);
@@ -224,7 +206,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category> implements Func
      */
     private static TableCell<Category,String> createNameCell(final 
TableColumn<Category,String> column) {
         @SuppressWarnings("unchecked")
-        final TableCell<Category,String> cell = (TableCell<Category,String>) 
TableColumn.DEFAULT_CELL_FACTORY.call(column);
+        final var cell = (TableCell<Category,String>) 
TableColumn.DEFAULT_CELL_FACTORY.call(column);
         cell.setAlignment(Pos.CENTER_LEFT);
         return cell;
     }
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 ccb8283410..87e3df3fe1 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
@@ -34,7 +34,6 @@ import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Rectangle;
-import org.apache.sis.internal.gui.GUIUtilities;
 
 
 /**
@@ -48,7 +47,7 @@ import org.apache.sis.internal.gui.GUIUtilities;
  * {@link EventHandler} is for reacting to user color selection using the 
control shown.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @param  <S>  the type of row data as declared in the {@code TableView} 
generic type.
  *
@@ -73,8 +72,8 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
 
     /**
      * The type of color ramp as determined by {@link 
ColorColumnHandler#applyColors(Object, ColorRamp)}.
-     * This is updated by {@link #updateItem(ColorRamp, boolean)} when the 
value changes and stored for
-     * keeping that value stable (this class does not support mutable colors 
type).
+     * This is updated by {@link #updateItem(ColorRamp, boolean)} when the 
value changes, and is stored
+     * for keeping that value stable (this class does not support mutable 
colors type).
      * May be {@code null} if there are no values in the row of this cell.
      */
     private ColorRamp.Type type;
@@ -102,7 +101,6 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
      */
     ColorCell(final ColorColumnHandler<S> handler) {
         this.handler = handler;
-        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
         setOnMouseClicked(ColorCell::mouseClicked);
     }
 
@@ -168,7 +166,7 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
                         colorRampChooser.setEditable(false);
                         colorRampChooser.setMaxWidth(Double.MAX_VALUE);
                         colorRampChooser.setCellFactory((column) -> new 
RampChoice());
-                        
colorRampChooser.getItems().setAll(ColorRamp.GRAYSCALE, ColorRamp.BELL);
+                        colorRampChooser.getItems().setAll(ColorRamp.DEFAULTS);
                         updateColorRampChooser(getItem());
                     }
                     control = colorRampChooser;
@@ -184,6 +182,7 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
                 control.setOnAction(this);
             }
         }
+        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
         setGraphic(control);
         return control;
     }
@@ -195,16 +194,22 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
     private final class RampChoice extends ListCell<ColorRamp> {
         /** Creates a new combo box choice. */
         RampChoice() {
-            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
             setMaxWidth(Double.POSITIVE_INFINITY);
         }
 
         /** Sets the colors to show in the combo box item. */
         @Override protected void updateItem(final ColorRamp colors, final 
boolean empty) {
             super.updateItem(colors, empty);
-            if (colors == null) {
+            if (empty || colors == null) {
+                setText(null);
+                setGraphic(null);
+            } else if (colors.isTransparent()) {
+                setContentDisplay(ContentDisplay.TEXT_ONLY);
+                setText(colors.toString());
                 setGraphic(null);
             } else {
+                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+                setText(null);
                 Rectangle r = (Rectangle) getGraphic();
                 if (r == null) {
                     r = createRectangle(-40);
@@ -328,21 +333,27 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
     private void setColorItem(final ColorRamp colors) {
         assert controlNotFocused();
         Rectangle view = null;
+        String label = null;
         if (colors != null) {
             final Paint paint = colors.paint();
             if (paint != null) {
+                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                 if (colorView == null) {
                     colorView = createRectangle(WIDTH_ADJUST);
                 }
                 view = colorView;
                 view.setFill(paint);
+            } else {
+                setContentDisplay(ContentDisplay.TEXT_ONLY);
+                label = colors.toString();
             }
         }
         setGraphic(view);
+        setText(label);
     }
 
     /**
-     * Removes the control in the cell and paint the color in a rectangle 
instead.
+     * Removes the control in the cell and paints the color in a rectangle 
instead.
      * This method does nothing if the control is already hidden.
      *
      * <p>This method sets the focus to the table before to remove the combo 
box.
@@ -444,7 +455,7 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> 
implements EventHandler<
             final Object value = ((ComboBoxBase<?>) 
event.getSource()).getValue();
             final ColorRamp colors;
             if (value instanceof Color) {
-                colors = new ColorRamp(GUIUtilities.toARGB((Color) value));
+                colors = new ColorRamp((Color) value);
             } else {
                 // A ClassCastException here would be a bug in ColorCell 
editors management.
                 colors = (ColorRamp) value;
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
index c1f58cbd5c..27a25dc25a 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
@@ -23,7 +23,6 @@ import javafx.scene.control.TableView;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TablePosition;
 import javafx.beans.value.ObservableValue;
-import org.apache.sis.internal.gui.ImmutableObjectProperty;
 
 
 /**
@@ -36,7 +35,7 @@ import org.apache.sis.internal.gui.ImmutableObjectProperty;
  * that may change in any future version.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  *
  * @param  <S>  the type of row data as declared in the {@link TableView} 
generic type.
  *
@@ -62,39 +61,19 @@ public abstract class ColorColumnHandler<S> implements 
Callback<TableColumn.Cell
     protected abstract ColorRamp.Type applyColors(S row, ColorRamp colors);
 
     /**
-     * Gets the ARGB codes of colors to show in the cell for the given row 
data.
-     * This method is sufficient when the color(s) can be changed only by 
calls to
-     * {@link #applyColors(S, ColorRamp)}. If the color(s) may change 
externally,
-     * then {@link #getObservableValue(S)} should be overridden too.
-     *
-     * @param  row  the row item for which to get ARGB codes to show in color 
cell.
-     * @return the colors as ARGB codes, or {@code null} if none (transparent).
-     */
-    protected abstract int[] getARGB(S row);
-
-    /**
-     * Returns the color associated to given row as an observable value. The 
default implementation creates
-     * an unmodifiable value derived from {@link #getARGB(S)}. It is okay if 
the color(s) cannot be changed
-     * in other way than by calls to {@link #applyColors(Object, ColorRamp)}. 
If this assumption does not hold,
-     * then subclasses should override this method and return the observable 
which is mutated when the value change.
+     * Returns the color associated to given row as an observable value.
      *
      * @param  row  the row item for which to get color to show in color cell. 
Never {@code null}.
-     * @return the color(s) to use for the given row, or {@code null} if none 
(transparent).
+     * @return the color(s) to use for the given row, or {@code null} for 
default.
      */
-    protected ObservableValue<ColorRamp> getObservableValue(S row) {
-        final int[] ARGB = getARGB(row);
-        if (ARGB != null) {
-            return new ImmutableObjectProperty<>(new ColorRamp(ARGB));
-        }
-        return null;
-    }
+    protected abstract ObservableValue<ColorRamp> getObservableValue(S row);
 
     /**
      * Invoked by {@link TableColumn} for computing the value of a {@link 
ColorCell}.
      * This method is public as an implementation side-effect; do not rely on 
that.
      *
      * @param  cell  the row value together with references to column and 
table where the show the color cell.
-     * @return the color cell value, or {@code null} if none (transparent).
+     * @return the color cell value, or {@code null} for default (original 
color, grayscale or transparent).
      */
     @Override
     public final ObservableValue<ColorRamp> call(final 
TableColumn.CellDataFeatures<S,ColorRamp> cell) {
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
index d212cc1055..d4572999e7 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
@@ -17,6 +17,7 @@
 package org.apache.sis.internal.gui.control;
 
 import java.util.Arrays;
+import java.util.Objects;
 import javafx.scene.control.ColorPicker;
 import javafx.scene.control.ComboBox;
 import javafx.scene.paint.Color;
@@ -33,9 +34,10 @@ import org.apache.sis.util.resources.Vocabulary;
  * A single color or a gradient of colors shown as a rectangle in a {@link 
ColorCell}.
  * Can also produce a string representation to be shown in a list.
  * Instances should be considered immutable.
+ * The same instance may be shared by many cells.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @see ColorCell#getItem()
  *
@@ -59,14 +61,25 @@ public final class ColorRamp {
     }
 
     /**
-     * Default color ramp.
+     * Original colors of the rendered image.
      */
-    static final ColorRamp GRAYSCALE = new ColorRamp(0xFF000000, 0xFFFFFFFF);
+    public static final ColorRamp DEFAULT = new 
ColorRamp(Vocabulary.format(Vocabulary.Keys.OriginalColors));
 
     /**
-     * Blue – Cyan – White – Yellow – Red.
+     * Grayscale color ramp.
      */
-    static final ColorRamp BELL = new ColorRamp(0xFF0000FF, 0xFF00FFFF, 
0xFFFFFFFF, 0xFFFFFF00, 0xFFFF0000);
+    private static final ColorRamp GRAYSCALE = new ColorRamp(0xFF000000, 
0xFFFFFFFF);
+
+    /**
+     * Default colors to put in a {@link ColorCell} combox box.
+     * This array shall not be modified.
+     */
+    static final ColorRamp[] DEFAULTS = {
+        DEFAULT,
+        GRAYSCALE,
+        new ColorRamp(0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFFFFFF00, 
0xFFFF0000),  // Blue – Cyan – White – Yellow – Red.
+        new ColorRamp(0xFF0000FF, 0xFFFF00FF, 0xFFFF0000)                      
     // Blue – Magenta – Red.
+    };
 
     /**
      * ARGB codes of this single color or color ramp.
@@ -75,6 +88,8 @@ public final class ColorRamp {
      * <p><strong>This array should be read-only.</strong> We make it public 
because this class is internal.
      * If this {@code ColorRamp} class moves to public API, then we would need 
to replace this public access
      * by an accessor doing a copy.</p>
+     *
+     * @see #isTransparent()
      */
     public final int[] colors;
 
@@ -112,11 +127,21 @@ public final class ColorRamp {
 
     /**
      * Creates a new item for the given colors.
+     *
+     * @param  colors  ARGB codes of this single color or color ramp.
      */
-    ColorRamp(final int... colors) {
+    public ColorRamp(final int... colors) {
         this.colors = colors;
     }
 
+    /**
+     * Creates a new item for the given text.
+     */
+    private ColorRamp(final String text) {
+        colors = null;
+        name = text;
+    }
+
     /**
      * Returns {@code true} if this ramp has no color with a non-zero 
transparency.
      * If this method returns {@code false}, then {@link #colors} is 
guaranteed non-empty.
@@ -158,7 +183,7 @@ public final class ColorRamp {
      * @return color or gradient paint for table cell, or {@code null} if none.
      */
     final Paint paint() {
-        if (paint == null) {
+        if (paint == null && colors != null) {
             switch (colors.length) {
                 case 0: break;
                 case 1: {
@@ -223,11 +248,17 @@ public final class ColorRamp {
      * Returns whether the given object is equal to this {@code ColorRamp}.
      * This is used for locating this {@code ColorRamp} in a {@link ComboBox}.
      *
-     * @param  other  the object to compare with {@code this} for equality.
+     * @param  obj  the object to compare with {@code this} for equality.
      * @return whether the given object is equal to this color ramp.
      */
     @Override
-    public boolean equals(final Object other) {
-        return (other instanceof ColorRamp) && Arrays.equals(colors, 
((ColorRamp) other).colors);
+    public boolean equals(final Object obj) {
+        if (obj instanceof ColorRamp) {
+            final ColorRamp other = (ColorRamp) obj;
+            if (Arrays.equals(colors, other.colors)) {
+                return (colors != null) || Objects.equals(name, other.name);
+            }
+        }
+        return false;
     }
 }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
index 39fd1e53a4..c7f2a0c888 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
@@ -230,18 +230,6 @@ public final class ValueColorMapper extends TabularWidget {
         ColumnHandler() {
         }
 
-        /**
-         * Returns the colors to apply for the given step, or {@code null} for 
transparent.
-         * This method is defined for safety but should not be invoked; use 
{@link #getObservableValue(S)} instead.
-         *
-         * @param  level  the value for which to get the color to show in 
color cell.
-         */
-        @Override
-        protected int[] getARGB(final Step level) {
-            final ColorRamp r = level.color.get();
-            return (r != null) ? r.colors : null;
-        }
-
         /**
          * Returns the color associated to given row as an observable value.
          *
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
index c14d01de17..f932e882d2 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
@@ -24,7 +24,7 @@
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.1
  */
 package org.apache.sis.internal.gui.control;
diff --git 
a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
 
b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
index 7c66339826..05ace09b00 100644
--- 
a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
+++ 
b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
@@ -27,6 +27,7 @@ import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.gui.Resources;
+import org.apache.sis.internal.gui.control.ColorRamp;
 import org.apache.sis.measure.Units;
 
 
@@ -34,7 +35,7 @@ import org.apache.sis.measure.Units;
  * Shows category table built by {@link CoverageStyling} with arbitrary data.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
  * @since   1.1
  */
 public final class CoverageStylingApp extends Application {
@@ -77,7 +78,7 @@ public final class CoverageStylingApp extends Application {
                 .build();
 
         final CoverageStyling styling = new CoverageStyling(null);
-        styling.setARGB(band.getCategories().get(1), new int[] {0xFF607080});
+        styling.applyColors(band.getCategories().get(1), new 
ColorRamp(0xFF607080));
         final TableView<Category> table = styling.createCategoryTable(
                 Resources.forLocale(null), Vocabulary.getResources((Locale) 
null));
         table.getItems().setAll(band.getCategories());
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 b9005bd109..31545550ea 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
@@ -667,7 +667,7 @@ public class ImageRenderer {
      * transparent for qualitative categories (typically "no data" values).
      *
      * <h4>Example</h4>
-     * the following code specifies a color palette from blue to red with 
white in the middle.
+     * The following code specifies a color palette from blue to red with 
white in the middle.
      * This is useful for data with a clear 0 (white) in the middle of the 
range,
      * with a minimal value equals to the negative of the maximal value.
      *
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index 43ade8f563..9f12e4c2df 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@ -87,7 +87,7 @@ final class BandSelectImage extends SourceAlignedImage {
      * @param  bands   the bands to select. Should be a clone of 
user-specified argument
      *                 for protection against user changes in the given array.
      */
-    static RenderedImage create(final RenderedImage source, final int[] bands) 
{
+    static RenderedImage create(final RenderedImage source, final int... 
bands) {
         final int numBands = ImageUtilities.getNumBands(source);
         if (bands.length == numBands && ArraysExt.isRange(0, bands)) {
             return source;
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 d21f3d2895..8888913868 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
@@ -181,9 +181,27 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
 
     /**
      * Creates a colorizer which will interpolate colors in multiple ranges of 
values.
-     * When the image data type is 8 or 16 bits integer, this colorizer 
creates {@link IndexColorModel} instances.
+     * The range of pixel values are specified by {@link NumberRange} elements,
+     * and the colors to interpolate in each range are specified by {@code 
Color[]} arrays.
+     * Empty arrays (i.e. no color) are interpreted as an explicit request for 
full transparency.
+     *
+     * <p>When the image data type is 8 or 16 bits integer,
+     * this colorizer creates {@link IndexColorModel} instances.
      * For other kinds of data type such as floating points,
-     * this colorizer creates a non-standard (and potentially slow) color 
model.
+     * 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.
+     * In current implementation, the defaults are:
+     *
+     * <ul>
+     *   <li>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.
@@ -195,7 +213,7 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      * @see ImageProcessor#visualize(RenderedImage)
      */
     public static Colorizer forRanges(final Map<NumberRange<?>,Color[]> 
colors) {
-        ArgumentChecks.ensureNonEmpty("colors", colors.entrySet());
+        // Can not use `Map.copyOf(colors)` because it may contain null values.
         final var factory = ColorModelFactory.piecewise(colors);
         return (target) -> {
             if (target instanceof Visualization.Target) {
@@ -220,14 +238,31 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      * The given function provides a way to colorize images without knowing in 
advance the numerical values of pixels.
      * For example, instead of specifying <cite>"pixel value 0 is blue, 1 is 
green, 2 is yellow"</cite>,
      * the given function allows to specify <cite>"Lakes are blue, Forests are 
green, Sand is yellow"</cite>.
-     * The function can return {@code null} or empty color arrays for some 
categories,
-     * which are interpreted as fully transparent pixels.
      *
-     * <p>This colorizer is used when {@link Target#getRanges()} provides a 
non-empty value.
+     * <h4>Default colors</h4>
+     * The given function can return {@code null} or empty color arrays for 
some categories.
+     * An empty array (i.e. no color) is interpreted as an explicit request 
for transparency.
+     * But null arrays are interpreted as unrecognized category,
+     * in which case the defaults are implementation dependent.
+     * In current implementation, the defaults are:
+     *
+     * <ul>
+     *   <li>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,
      * which is itself typically fetched from {@link 
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
-     * If no sample dimension information is available, then this colorizer do 
not build a color model.
-     * A fallback can be specified with {@link #orElse(Colorizer)}.</p>
+     * If no sample dimension information is available,
+     * or if the specified function did not returned at non-null value for at 
least one category,
+     * then this colorizer does not build a color model.
+     * A fallback can be specified with {@link #orElse(Colorizer)}.
      *
      * @param  colors  colors to use for arbitrary categories of sample values.
      * @return a colorizer which will apply colors determined by the {@link 
Category} of sample values.
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index dea9f52adf..1e71d8569d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -405,6 +405,7 @@ public class ImageProcessor implements Cloneable {
      */
     public synchronized void setColorizer(final Colorizer colorizer) {
         this.colorizer = colorizer;
+        colors = null;
     }
 
     /**
@@ -438,8 +439,10 @@ public class ImageProcessor implements Cloneable {
      */
     @Deprecated(since="1.4", forRemoval=true)
     public synchronized void setCategoryColors(final 
Function<Category,Color[]> colors) {
-        setColorizer(colors != null ? Colorizer.forCategories(colors) : null);
-        this.colors = colors;
+        if (colors != this.colors) {
+            setColorizer(colors != null ? Colorizer.forCategories(colors) : 
null);
+            this.colors = colors;
+        }
     }
 
     /**
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 7237500a48..d83417ecba 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
@@ -264,7 +264,7 @@ final class Visualization extends ResampledImage {
                     break;
                 }
             }
-            source = BandSelectImage.create(source, new int[] {visibleBand});
+            source = BandSelectImage.create(source, visibleBand);
             final SampleDimension visibleSD = (sampleDimensions != null && 
visibleBand < sampleDimensions.length)
                                             ? sampleDimensions[visibleBand] : 
null;
             /*
@@ -298,8 +298,9 @@ final class Visualization extends ResampledImage {
              */
             boolean initialized;
             final ColorModelBuilder builder;
-            if (target.rangeColors != null) {
-                builder = new ColorModelBuilder(target.rangeColors.entrySet());
+            final var rangeColors = target.rangeColors;
+            if (rangeColors != null && !rangeColors.isEmpty()) {
+                builder = new ColorModelBuilder(rangeColors.entrySet());
                 initialized = true;
             } else {
                 /*
@@ -307,6 +308,7 @@ final class Visualization extends ResampledImage {
                  * in various ways: sample dimensions, scaled color model, or 
image statistics in last resort.
                  */
                 builder = new ColorModelBuilder(target.categoryColors);
+                final ColorModel colorModel = coloredSource.getColorModel();
                 initialized = 
builder.initialize(coloredSource.getSampleModel(), visibleSD);
                 if (initialized) {
                     /*
@@ -315,14 +317,15 @@ 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(coloredSource.getColorModel());
+                    builder.rescaleMainRange(colorModel);
                 } else {
                     /*
+                     * At this point there is no more user-supplied colors 
(through `Colorizer`) that we can use.
                      * If we have not been able to use the SampleDimension, 
try to use the ColorModel or SampleModel.
                      * There is no call to `rescaleMainRange(…)` because the 
following code already uses the range
                      * specified by the ColorModel, if available.
                      */
-                    initialized = 
builder.initialize(coloredSource.getColorModel());
+                    initialized = builder.initialize(colorModel);
                     if (!initialized) {
                         if (coloredSource instanceof RecoloredImage) {
                             final RecoloredImage colored = (RecoloredImage) 
coloredSource;
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 055c491d22..455899fa16 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
@@ -111,8 +111,16 @@ public final class ColorModelBuilder {
             (category) -> category.isQuantitative() ? new Color[] 
{Color.BLACK, Color.WHITE} : null;
 
     /**
-     * The colors to use for each category. Never {@code null}.
-     * The function may return {@code null}, which means transparent.
+     * The colors to use for each category. Never {@code null} (default value 
is grayscale).
+     * The function may return {@code null}, which means that the category is 
not recognized.
+     * If no category is recognized, no {@link ColorModel} will be built using 
that function.
+     * An empty array is interpreted as a color specified as transparent.
+     *
+     * <h4>Default value</h4>
+     * Default value is {@link #GRAYSCALE}.
+     * If the function returns {@code null} for an unrecognized category,
+     * the default colors for that category will be the same as {@link 
#GRAYSCALE}:
+     * grayscale for quantitative categories and transparent for qualitative 
categories.
      */
     private final Function<Category,Color[]> colors;
 
@@ -151,11 +159,15 @@ public final class ColorModelBuilder {
      * The {@code ColorModelBuilder} is considered initialized after this 
constructor;
      * callers shall <strong>not</strong> invoke an {@code initialize(…)} 
method.
      *
+     * <p>The {@code colors} map shall not be null or empty but may contain 
{@code null} values.
+     * Null values default to a fully transparent color when the range 
contains a single value,
+     * 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.
-     *                 A {@code null} entry value means transparent.
      */
     public ColorModelBuilder(final 
Collection<Map.Entry<NumberRange<?>,Color[]>> colors) {
-        ArgumentChecks.ensureNonNull("colors", colors);
+        ArgumentChecks.ensureNonEmpty("colors", colors);
         entries = ColorsForRange.list(colors);
         this.colors = GRAYSCALE;
     }
@@ -165,7 +177,7 @@ public final class ColorModelBuilder {
      * 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}, which means 
transparent.
+     *                 The function may return {@code null} for unrecognized 
categories.
      */
     public ColorModelBuilder(final Function<Category,Color[]> colors) {
         this.colors = (colors != null) ? colors : GRAYSCALE;
@@ -206,26 +218,30 @@ public final class ColorModelBuilder {
             this.source = source;
             final List<Category> categories = source.getCategories();
             if (!categories.isEmpty()) {
+                boolean isUndefined = true;
                 boolean missingNodata = true;
                 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);
-                    missingNodata &= category.isQuantitative();
+                    final var range = new ColorsForRange(categories.get(i), 
colors);
+                    isUndefined &= range.isUndefined();
+                    missingNodata &= range.isData;
+                    entries[i] = range;
                 }
-                /*
-                 * If the model uses floating point values and there is no "no 
data" category, add one.
-                 * We force a "no data" category because floating point values 
may be NaN.
-                 */
-                if (missingNodata && (model == null || 
!ImageUtilities.isIntegerType(model))) {
-                    final int count = entries.length;
-                    entries = Arrays.copyOf(entries, count + 1);
-                    entries[count] = new ColorsForRange(TRANSPARENT,
-                            NumberRange.create(Float.class, Float.NaN), null, 
false);
+                if (!isUndefined) {
+                    /*
+                     * If the model uses floating point values and there is no 
"no data" category, add one.
+                     * We force a "no data" category because floating point 
values may be NaN.
+                     */
+                    if (missingNodata && (model == null || 
!ImageUtilities.isIntegerType(model))) {
+                        final int count = entries.length;
+                        entries = Arrays.copyOf(entries, count + 1);
+                        entries[count] = new ColorsForRange(TRANSPARENT,
+                                NumberRange.create(Float.class, Float.NaN), 
null, false);
+                    }
+                    // Leave `target` to null. It will be computed by 
`compact()` if needed.
+                    this.entries = entries;
+                    return true;
                 }
-                // Leave `target` to null. It will be computed by `compact()` 
if needed.
-                this.entries = entries;
-                return true;
             }
         }
         return false;
@@ -274,6 +290,26 @@ public final class ColorModelBuilder {
                 initialize(scs.offset, scs.maximum);
                 return true;
             }
+            /*
+             * If the color model uses integer type, compute the maximal value 
based on the number of bits.
+             * The main use case is `IndexColorModel` with values on 16 bits 
but with a color ramp that does
+             * not exploit the full range allowed by 16 bits.
+             */
+            if (ImageUtilities.isIntegerType(source.getTransferType())) {
+                long maximum = Numerics.bitmask(source.getPixelSize()) - 1;
+                long minimum = 0;
+                if (source instanceof IndexColorModel) {
+                    final IndexColorModel indexed = (IndexColorModel) source;
+                    int t = indexed.getMapSize();
+                    if (t <= maximum) maximum = t - 1L;     // Inclusive.
+                    t = indexed.getTransparentPixel();
+                    if (t == 0) minimum = 1;
+                }
+                if (minimum < maximum) {
+                    initialize(minimum, maximum);
+                    return true;
+                }
+            }
         }
         return false;
     }
@@ -315,7 +351,7 @@ 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, category.getName() == 
TRANSPARENT ? GRAYSCALE : colors);
+            entries[i] = new ColorsForRange(category, colors);
         }
         this.entries = entries;
     }
@@ -494,7 +530,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, new 
Color[] {Color.BLACK, Color.WHITE}, true);
+                tmp[deferred-1] = new ColorsForRange(null, sourceRange, null, 
true);
                 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 1b1aad4e5b..510fa6cd61 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
@@ -299,14 +299,17 @@ public final class ColorModelFactory {
     /**
      * Prepares a factory of color models interpolated for the ranges in the 
given map entries.
      * The {@link ColorModel} instances will be shared among all callers in 
the running virtual machine.
+     * The {@code colors} map shall not be null or empty but may contain 
{@code null} values.
+     * Null values default to fully transparent color when the range contains 
a single value,
+     * and to grayscale otherwise. Empty arrays of colors are interpreted as 
explicitly transparent.
      *
      * @param  colors  the colors to use for each range of sample values.
-     *                 The map may contain {@code null} values, which means 
transparent.
      * @return a factory of color model suitable for {@link RenderedImage} 
objects with values in the given ranges.
      */
     public static ColorModelFactory piecewise(final Map<NumberRange<?>, 
Color[]> colors) {
-        return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 
0, DEFAULT_VISIBLE_BAND,
-                                                       
ColorsForRange.list(colors.entrySet())));
+        final var entries = colors.entrySet();
+        ArgumentChecks.ensureNonEmpty("colors", entries);
+        return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 
0, DEFAULT_VISIBLE_BAND, ColorsForRange.list(entries)));
     }
 
     /**
@@ -474,6 +477,7 @@ public final class ColorModelFactory {
     public static ColorModel createColorScale(final int dataType, final int 
numBands, final int visibleBand,
                                               final double lower, final double 
upper, final Color... colors)
     {
+        ArgumentChecks.ensureNonEmpty("colors", colors);
         return createPiecewise(dataType, numBands, visibleBand, new 
ColorsForRange[] {
             new ColorsForRange(null, new NumberRange<>(Double.class, lower, 
true, upper, false), colors, true)
         });
@@ -849,10 +853,7 @@ public final class ColorModelFactory {
             
buffer.append(System.lineSeparator()).append(CharSequences.spaces(9 - 
start.length())).append(start);
             if (i < ARGB.length) {
                 buffer.append('…');
-                final int[] colors = ARGB[i];
-                if (colors != null) {
-                    ColorsForRange.appendColorRange(buffer, colors.length, (j) 
-> colors[j]);
-                }
+                ColorsForRange.appendColorRange(buffer, ARGB[i]);
             }
         }
         return buffer.toString();
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 d8012bd6e8..31894df212 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
@@ -18,8 +18,8 @@ package org.apache.sis.internal.coverage.j2d;
 
 import java.util.Map;
 import java.util.Collection;
+import java.util.Objects;
 import java.util.function.Function;
-import java.util.function.IntUnaryOperator;
 import java.awt.Color;
 import java.awt.image.IndexColorModel;
 import org.apache.sis.coverage.Category;
@@ -33,7 +33,7 @@ import org.apache.sis.util.ArraysExt;
  * used only the time needed for {@link ColorModelFactory#createPiecewise(int, 
int, int, ColorsForRange[])}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  *
  * @see ColorModelFactory#createPiecewise(int, int, int, ColorsForRange[])
  *
@@ -55,7 +55,12 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
 
     /**
      * The colors to apply on the range of sample values.
-     * A null or empty array means transparent.
+     * An empty array means that the category is explicitly specified as 
transparent.
+     * A null value means that the category is unrecognized, in which case the 
default
+     * is grayscale for quantitative category and transparent for qualitative 
category.
+     *
+     * @see #isUndefined()
+     * @see #toARGB()
      */
     private final Color[] colors;
 
@@ -84,7 +89,7 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
      *
      * @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 transparent.
+     * @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).
      */
     ColorsForRange(final CharSequence name, final NumberRange<?> sampleRange, 
final Color[] colors, final boolean isData) {
@@ -95,6 +100,14 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
         this.isData      = isData;
     }
 
+    /**
+     * Returns {@code true} if no color has been specified for this range.
+     * Note that "undefined" is not the same as fully transparent color.
+     */
+    final boolean isUndefined() {
+        return colors == null;
+    }
+
     /**
      * Converts {@linkplain Map#entrySet() map entries} to an array of {@code 
ColorsForRange} entries.
      * The {@link #category} of each entry is left to null.
@@ -108,7 +121,9 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
         final ColorsForRange[] entries = new ColorsForRange[colors.size()];
         int n = 0;
         for (final Map.Entry<NumberRange<?>,Color[]> entry : colors) {
-            entries[n++] = new ColorsForRange(null, entry.getKey(), 
entry.getValue(), true);
+            final NumberRange<?> range = entry.getKey();
+            boolean singleton = Objects.equals(range.getMinValue(), 
range.getMaxValue());
+            entries[n++] = new ColorsForRange(null, range, entry.getValue(), 
!singleton);
         }
         return ArraysExt.resize(entries, n);            // `resize` should not 
be needed, but we are paranoiac.
     }
@@ -119,9 +134,7 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
     @Override
     public String toString() {
         final StringBuilder buffer = new StringBuilder(name).append(": 
").append(sampleRange);
-        if (colors != null) {
-            appendColorRange(buffer, colors.length, (i) -> colors[i].getRGB());
-        }
+        appendColorRange(buffer, toARGB());
         return buffer.toString();
     }
 
@@ -135,14 +148,14 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
      * @param  count   number of ARGB codes.
      * @param  colors  providers of ARGB codes for given indices.
      */
-    static void appendColorRange(final StringBuilder buffer, final int count, 
final IntUnaryOperator colors) {
-        if (count != 0) {
+    static void appendColorRange(final StringBuilder buffer, final int[] 
colors) {
+        if (colors != null && colors.length != 0) {
             String s = " → ARGB[";
             int i = 0;
             do {
-                
buffer.append(s).append(Integer.toHexString(colors.applyAsInt(i)).toUpperCase());
+                
buffer.append(s).append(Integer.toHexString(colors[i]).toUpperCase());
                 s = " … ";
-            } while (i < (i = count-1));
+            } while (i < (i = colors.length - 1));
             buffer.append(']');
         }
     }
@@ -165,8 +178,8 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
     private int getAlpha() {
         int max = 0;
         if (colors != null) {
-            for (int i=0; i<colors.length; i++) {
-                final int alpha = colors[i].getAlpha();
+            for (final Color color : colors) {
+                final int alpha = color.getAlpha();
                 if (alpha > max) {
                     if (alpha >= 0xFF) {
                         return 0xFF;
@@ -174,6 +187,8 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
                     max = alpha;
                 }
             }
+        } else if (isData) {
+            return 0xFF;
         }
         return max;
     }
@@ -199,6 +214,8 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
             if ((combined & 0xFF000000) != 0) {
                 return ARGB;
             }
+        } else if (isData) {
+            return new int[] {0xFF000000, 0xFFFFFFFF};
         }
         return ArraysExt.EMPTY_INT;
     }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index 24be11c4a3..25ef1259f5 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -952,6 +952,11 @@ public final class Vocabulary extends 
IndexedResourceBundle {
          */
         public static final short OriginInCellCenter = 155;
 
+        /**
+         * Original colors
+         */
+        public static final short OriginalColors = 272;
+
         /**
          * Other surface
          */
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index 21efb2b707..e3d1182324 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -193,6 +193,7 @@ Operations              = Operations
 Optional                = Optional
 Options                 = Options
 Origin                  = Origin
+OriginalColors          = Original colors
 OriginInCellCenter      = Origin in a cell center
 Others                  = Others
 OtherSurface            = Other surface
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 39059bff17..d804f40bca 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -200,6 +200,7 @@ Operations              = Op\u00e9rations
 Optional                = Optionnel
 Options                 = Options
 Origin                  = Origine
+OriginalColors          = Couleurs originales
 OriginInCellCenter      = Origine au centre d\u2019une cellule
 Others                  = Autres
 OtherSurface            = Autre surface
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
index bc8c71c109..7b46de2c08 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
@@ -40,7 +40,7 @@ import org.apache.sis.coverage.grid.BufferedGridCoverage;
  * but it is {@link ImageRenderer} responsibility to perform this substitution 
as an optimization.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.0
  */
 final class Raster extends BufferedGridCoverage {
@@ -63,11 +63,6 @@ final class Raster extends BufferedGridCoverage {
      */
     private final int visibleBand;
 
-    /**
-     * Name to display in error messages. Not to be used for processing.
-     */
-    private final String label;
-
     /**
      * The colors to use for each category, or {@code null} for default.
      * The function may return {@code null}, which means transparent.
@@ -80,18 +75,16 @@ final class Raster extends BufferedGridCoverage {
      * @param domain       the grid extent, CRS and conversion from cell 
indices to CRS.
      * @param range        sample dimensions for each image band.
      * @param data         the sample values, potentially multi-banded.
-     * @param lebel        name to display in error messages. Not to be used 
for processing.
      * @param pixelStride  increment to apply on index for moving to the next 
pixel in the same band.
      * @param bandOffsets  offsets to add to sample index in each band, or 
{@code null} if none.
      * @param visibleBand  the band to use for defining pixel colors when the 
image is displayed on screen.
      * @param colors       the colors to use for each category, or {@code 
null} for default.
      */
     Raster(final GridGeometry domain, final List<SampleDimension> range, final 
DataBuffer data,
-           final String label, final int pixelStride, final int[] bandOffsets, 
final int visibleBand,
+           final int pixelStride, final int[] bandOffsets, final int 
visibleBand,
            final Function<Category,Color[]> colors)
     {
         super(domain, range, data);
-        this.label       = label;
         this.colors      = colors;
         this.pixelStride = pixelStride;
         this.bandOffsets = bandOffsets;
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 48c8c07a0d..5f819e8105 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -67,7 +67,7 @@ import org.apache.sis.internal.storage.StoreResource;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.0
  */
 public final class RasterResource extends AbstractGridCoverageResource 
implements StoreResource, ResourceOnFileSystem {
@@ -727,7 +727,7 @@ public final class RasterResource extends 
AbstractGridCoverageResource implement
         }
         final Variable main = data[visibleBand];
         final Raster raster = new Raster(targetDomain, 
UnmodifiableArrayList.wrap(bands), imageBuffer,
-                String.valueOf(identifier), rangeIndices.getPixelStride(), 
bandOffsets, visibleBand,
+                rangeIndices.getPixelStride(), bandOffsets, visibleBand,
                 main.decoder.convention().getColors(main));
         logReadOperation(location, targetDomain, startTime);
         return raster;

Reply via email to