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

commit 9627d2e9cca92fee58ebb298f1f0a2b52fd6b843
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Mar 27 16:14:38 2023 +0200

    Initial version of an `Colorizer` interface for building the `ColorModel` 
of a computed image.
    Replacement is not yet done everywhere.
    
    https://issues.apache.org/jira/browse/SIS-577
---
 .../coverage/grid/BandAggregateGridCoverage.java   |  18 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |  12 +-
 .../org/apache/sis/image/BandAggregateImage.java   |  32 ++-
 .../apache/sis/image/BandedSampleConverter.java    |  13 +-
 .../main/java/org/apache/sis/image/Colorizer.java  | 265 +++++++++++++++++++++
 .../org/apache/sis/image/CombinedImageLayout.java  |  43 ++--
 .../java/org/apache/sis/image/ComputedImage.java   |  22 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 113 +++++++--
 .../internal/coverage/j2d/ColorModelFactory.java   | 240 ++++++++++++-------
 .../sis/internal/coverage/j2d/ColorsForRange.java  |   6 +-
 .../sis/internal/coverage/j2d/ImageUtilities.java  |   2 +-
 .../apache/sis/image/BandAggregateImageTest.java   |   2 +-
 .../sis/util/collection/WeakValueHashMap.java      |  89 +++++--
 .../org/apache/sis/internal/netcdf/Convention.java |   3 +-
 .../aggregate/BandAggregateGridResource.java       |  19 +-
 .../aggregate/BandAggregateGridResourceTest.java   |   2 +-
 16 files changed, 663 insertions(+), 218 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
index 303f4d7b14..6b95c83080 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java
@@ -18,7 +18,6 @@ package org.apache.sis.coverage.grid;
 
 import java.util.Map;
 import java.util.TreeMap;
-import java.awt.image.ColorModel;
 import java.awt.image.RenderedImage;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.operation.TransformException;
@@ -75,15 +74,10 @@ final class BandAggregateGridCoverage extends GridCoverage {
      */
     private final DataType dataType;
 
-    /**
-     * The color model to apply on aggregated image, or {@code null} for 
default.
-     * If {@code null}, the color model will be inferred from the aggregated 
number
-     * of bands and the sample data type.
-     */
-    private final ColorModel colors;
-
     /**
      * The processor to use for creating images.
+     * The processor {@linkplain ImageProcessor#getColorizer() colorizer}
+     * will determine the color model applied on the aggregated images.
      */
     private final ImageProcessor processor;
 
@@ -91,20 +85,16 @@ final class BandAggregateGridCoverage extends GridCoverage {
      * Creates a new band aggregated coverage from the given sources.
      *
      * @param  aggregate  the source grid coverages together with bands to 
select.
-     * @param  colors     the color model to apply on aggregated image, or 
{@code null} for default.
      * @param  processor  the processor to use for creating images.
      * @throws IllegalArgumentException if there is an incompatibility between 
some source coverages
      *         or if some band indices are duplicated or outside their range 
of validity.
      */
-    BandAggregateGridCoverage(final MultiSourcesArgument<GridCoverage> 
aggregate, final ColorModel colors,
-                              final ImageProcessor processor)
-    {
+    BandAggregateGridCoverage(final MultiSourcesArgument<GridCoverage> 
aggregate, final ImageProcessor processor) {
         super(aggregate.domain(GridCoverage::getGridGeometry), 
aggregate.ranges());
         this.sources           = aggregate.sources();
         this.bandsPerSource    = aggregate.bandsPerSource();
         this.numBands          = aggregate.numBands();
         this.sourceOfGridToCRS = aggregate.sourceOfGridToCRS();
-        this.colors            = colors;
         this.processor         = processor;
         this.dataType          = sources[0].getBandType();
         for (int i=1; i < sources.length; i++) {
@@ -150,7 +140,7 @@ final class BandAggregateGridCoverage extends GridCoverage {
         for (int i=0; i<images.length; i++) {
             images[i] = sources[i].render(sliceExtent);
         }
-        return processor.aggregateBands(images, bandsPerSource, colors);
+        return processor.aggregateBands(images, bandsPerSource);
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index ea557d7a04..31465949a6 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -652,13 +652,13 @@ public class GridCoverageProcessor implements Cloneable {
      * @return the aggregated coverage, or {@code sources[0]} returned 
directly if only one coverage was supplied.
      * @throws IllegalGridGeometryException if a grid geometry is not 
compatible with the others.
      *
-     * @see #aggregateRanges(GridCoverage[], int[][], ColorModel)
+     * @see #aggregateRanges(GridCoverage[], int[][])
      * @see ImageProcessor#aggregateBands(RenderedImage...)
      *
      * @since 1.4
      */
     public GridCoverage aggregateRanges(final GridCoverage... sources) {
-        return aggregateRanges(sources, null, null);
+        return aggregateRanges(sources, (int[][]) null);
     }
 
     /**
@@ -674,24 +674,22 @@ public class GridCoverageProcessor implements Cloneable {
      * @param  sources  coverages whose bands shall be aggregated, in order. 
At least one coverage must be provided.
      * @param  bandsPerSource  bands to use for each source coverage, in order.
      *                  May be {@code null} or may contain {@code null} 
elements.
-     * @param  colors   the color model to apply on aggregated image, or 
{@code null} for inferring
-     *                  a default color model using aggregated number of bands 
and sample data type.
      * @return the aggregated coverage, or one of the sources if it can be 
used directly.
      * @throws IllegalGridGeometryException if a grid geometry is not 
compatible with the others.
      * @throws IllegalArgumentException if some band indices are duplicated or 
outside their range of validity.
      *
-     * @see ImageProcessor#aggregateBands(RenderedImage[], int[][], ColorModel)
+     * @see ImageProcessor#aggregateBands(RenderedImage[], int[][])
      *
      * @since 1.4
      */
-    public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] 
bandsPerSource, ColorModel colors) {
+    public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] 
bandsPerSource) {
         final var aggregate = new MultiSourcesArgument<>(sources, 
bandsPerSource);
         aggregate.identityAsNull();
         aggregate.validate(GridCoverage::getSampleDimensions);
         if (aggregate.isIdentity()) {
             return aggregate.sources()[0];
         }
-        return new BandAggregateGridCoverage(aggregate, colors, 
imageProcessor);
+        return new BandAggregateGridCoverage(aggregate, imageProcessor);
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
index d0abc2f628..fd7aaa5758 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java
@@ -77,14 +77,14 @@ final class BandAggregateImage extends ComputedImage {
      *
      * @param  sources          images to combine, in order.
      * @param  bandsPerSource   bands to use for each source image, in order. 
May contain {@code null} elements.
-     * @param  colors           the color model to use for this image, or 
{@code null} for automatic.
+     * @param  colorizer        provider of color model to use for this image, 
or {@code null} for automatic.
      * @throws IllegalArgumentException if there is an incompatibility between 
some source images
      *         or if some band indices are duplicated or outside their range 
of validity.
      * @return the band aggregate image.
      */
     @Workaround(library="JDK", version="1.8")
-    static RenderedImage create(RenderedImage[] sources, int[][] 
bandsPerSource, ColorModel colors) {
-        var image = new BandAggregateImage(CombinedImageLayout.create(sources, 
bandsPerSource), colors);
+    static RenderedImage create(RenderedImage[] sources, int[][] 
bandsPerSource, Colorizer colorizer) {
+        var image = new BandAggregateImage(CombinedImageLayout.create(sources, 
bandsPerSource), colorizer);
         if (image.filteredSources.length == 1) {
             final RenderedImage c = image.filteredSources[0];
             if (image.colorModel == null) {
@@ -101,25 +101,21 @@ final class BandAggregateImage extends ComputedImage {
     /**
      * Creates a new aggregation of bands.
      *
-     * @param  layout  pixel and tile coordinate spaces of this image, 
together with sample model.
-     * @param  colors  the color model to use for this image, or {@code null} 
for automatic.
+     * @param  layout     pixel and tile coordinate spaces of this image, 
together with sample model.
+     * @param  colorizer  provider of color model to use for this image, or 
{@code null} for automatic.
      */
-    private BandAggregateImage(final CombinedImageLayout layout, ColorModel 
colors) {
+    private BandAggregateImage(final CombinedImageLayout layout, final 
Colorizer colorizer) {
         super(layout.sampleModel, layout.sources);
         final Rectangle r = layout.domain;
-        minX     = r.x;
-        minY     = r.y;
-        width    = r.width;
-        height   = r.height;
-        minTileX = layout.minTileX;
-        minTileY = layout.minTileY;
-        if (colors == null) {
-            colors = layout.createColorModel();
-        } else {
-            layout.ensureCompatible("colors", colors);
-        }
-        colorModel = colors;
+        minX            = r.x;
+        minY            = r.y;
+        width           = r.width;
+        height          = r.height;
+        minTileX        = layout.minTileX;
+        minTileY        = layout.minTileY;
         filteredSources = layout.getFilteredSources();
+        colorModel      = layout.createColorModel(colorizer);
+        ensureCompatible(colorModel);
     }
 
     /** Returns the information inferred at construction time. */
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 2c55e46362..ab8ee294a0 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
@@ -32,6 +32,7 @@ import java.lang.reflect.Array;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
@@ -185,14 +186,14 @@ class BandedSampleConverter extends ComputedImage {
      * @param  converters    the transfer functions to apply on each band of 
the source image.
      * @param  targetType    the type of this image resulting from conversion 
of given image.
      *                       Shall be one of {@link DataBuffer} constants.
-     * @param  colorModel    the color model for the expected range of values, 
or {@code null}.
+     * @param  colorizer     provider of color model for the expected range of 
values, or {@code null}.
      * @return the image which compute converted values from the given source.
      *
      * @see ImageProcessor#convert(RenderedImage, NumberRange[], 
MathTransform1D[], DataType, ColorModel)
      */
     static BandedSampleConverter create(RenderedImage source, final 
ImageLayout layout,
             final NumberRange<?>[] sourceRanges, final MathTransform1D[] 
converters,
-            final int targetType, final ColorModel colorModel)
+            final int targetType, final Colorizer colorizer)
     {
         /*
          * Since this operation applies its own ColorModel anyway, skip 
operation that was doing nothing else
@@ -203,6 +204,14 @@ class BandedSampleConverter extends ComputedImage {
         }
         final int numBands = converters.length;
         final BandedSampleModel sampleModel = 
layout.createBandedSampleModel(targetType, numBands, source, null);
+        final int visibleBand = ImageUtilities.getVisibleBand(source);
+        ColorModel colorModel = null;
+        if (colorizer != null) {
+            colorModel = colorizer.apply(new Colorizer.Target(sampleModel, 
null, visibleBand)).orElse(null);
+        }
+        if (colorModel == null) {
+            colorModel = ColorModelFactory.createGrayScale(sampleModel, 
visibleBand, null);
+        }
         /*
          * If the source image is writable, then changes in the converted 
image may be retro-propagated
          * to that source image. If we fail to compute the required inverse 
transforms, log a notice at
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
new file mode 100644
index 0000000000..50aecab8b0
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.util.Map;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.function.Function;
+import java.awt.Color;
+import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.IndexColorModel;
+import org.apache.sis.coverage.Category;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * Colorization algorithm to apply for colorizing a computed image.
+ * The {@link #apply(Target)} method is invoked when {@link ImageProcessor} 
needs a new color model for
+ * the computation result. The {@link Target} argument contains information 
about the image to colorize,
+ * in particular the {@link SampleModel} of the computed image. The 
colorization result is optional,
+ * i.e. the {@code apply(Target)} method may return an empty value if it does 
not support the target.
+ * In the latter case the caller will fallback on a default color model, 
typically a grayscale.
+ *
+ * <p>Constants or static methods in this interface provide colorizers for 
common cases.
+ * For example {@link #ARGB} interprets image bands as Red, Green, Blue and 
optionally Alpha channels.
+ * Colorizers can be chained with {@link #orElse(Colorizer)} for trying 
different strategies until one succeeds.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+public interface Colorizer extends Function<Colorizer.Target, 
Optional<ColorModel>> {
+    /**
+     * Information about the computed image to colorize.
+     * The most important information is the {@link SampleModel}, as the 
inferred color model must be
+     * {@linkplain ColorModel#isCompatibleSampleModel(SampleModel) compatible 
with the sample model}.
+     * A {@code Target} instance may also contain contextual information
+     * such as the {@link SampleDimension}s of the target coverage.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.4
+     * @since   1.4
+     */
+    class Target {
+        /**
+         * Sample model of the computed image to colorize.
+         */
+        private final SampleModel model;
+
+        /**
+         * Description of the bands of the computed image to colorize, or 
{@code null} if none.
+         */
+        private final List<SampleDimension> ranges;
+
+        /**
+         * The band to colorize if the colorization algorithm uses only one 
band.
+         * Ignored if the colorization uses many bands (e.g. {@link #ARGB}).
+         * A negative value means that no visible band has been specified.
+         */
+        private final int visibleBand;
+
+        /**
+         * Creates a new record with the sample model of the image to colorize.
+         *
+         * @param  model        sample model of the computed image to colorize 
(mandatory).
+         * @param  ranges       description of the bands of the computed image 
to colorize, or {@code null} if none.
+         * @param  visibleBand  the band to colorize if the colorization 
algorithm uses only one band, or -1 if none.
+         */
+        public Target(final SampleModel model, final List<SampleDimension> 
ranges, final int visibleBand) {
+            this.model  = Objects.requireNonNull(model);
+            this.ranges = (ranges != null) ? List.copyOf(ranges) : null;
+            final int numBands = model.getNumBands();
+            if (visibleBand < 0) {
+                if (numBands == 1) {
+                    this.visibleBand = ColorModelFactory.DEFAULT_VISIBLE_BAND;
+                    return;
+                }
+            } else if (visibleBand < numBands) {
+                this.visibleBand = visibleBand;
+                return;
+            }
+            this.visibleBand = -1;
+        }
+
+        /**
+         * Returns the sample model of the computed image to colorize.
+         * The color model created by {@link #apply(Target)}
+         * must be compatible with this sample model.
+         *
+         * @return computed image sample model (never null).
+         * @see ColorModel#isCompatibleSampleModel(SampleModel)
+         */
+        public SampleModel getSampleModel() {
+            return model;
+        }
+
+        /**
+         * Returns a description of the bands of the image to colorize.
+         * This is typically obtained by {@link 
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
+         *
+         * @return description of the bands of the image to colorize.
+         */
+        public Optional<List<SampleDimension>> getRanges() {
+            return Optional.ofNullable(ranges);
+        }
+
+        /**
+         * Returns the band to colorize if the colorization algorithm uses 
only one band.
+         * The value is always positive and less than the number of bands of 
the sample model.
+         * This information is ignored if the colorization uses many bands 
(e.g. {@link #ARGB}).
+         *
+         * @return the band to colorize if the colorization algorithm uses 
only one band.
+         */
+        public OptionalInt getVisibleBand() {
+            return (visibleBand >= 0) ? OptionalInt.of(visibleBand) : 
OptionalInt.empty();
+        }
+    }
+
+    /**
+     * RGB(A) color model for images storing 8 bits integer on 3 or 4 bands.
+     * The color model is RGB for image having 3 bands, or ARGB for images 
having 4 bands.
+     */
+    Colorizer ARGB = (target) -> 
Optional.ofNullable(ColorModelFactory.createRGB(target.getSampleModel()));
+
+    /**
+     * Creates a colorizer which will interpolate the given colors in the 
given range of values.
+     * 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.
+     *
+     * <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.
+     *
+     * @param  lower   the minimum sample value, inclusive.
+     * @param  upper   the maximum sample value, exclusive.
+     * @param  colors  the colors to use for the specified range of sample 
values.
+     * @return a colorizer which will interpolate the given colors in the 
given range of values.
+     */
+    public static Colorizer forRange(final double lower, final double upper, 
final Color... colors) {
+        ArgumentChecks.ensureNonEmpty("colors", colors);
+        return forRanges(Map.of(new NumberRange<>(Double.class, lower, true, 
upper, false), colors));
+    }
+
+    /**
+     * 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.
+     * For other kinds of data type such as floating points,
+     * this colorizer creates a non-standard (and potentially slow) color 
model.
+     *
+     * <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.
+     *
+     * @param  colors  the colors to use for the specified range of sample 
values.
+     * @return a colorizer which will interpolate the given colors in the 
given range of values.
+     */
+    public static Colorizer forRanges(final Map<NumberRange<?>,Color[]> 
colors) {
+        ArgumentChecks.ensureNonEmpty("colors", colors.entrySet());
+        final var factory = ColorModelFactory.piecewise(colors);
+        return (target) -> {
+            final OptionalInt visibleBand = target.getVisibleBand();
+            if (visibleBand.isEmpty()) {
+                return Optional.empty();
+            }
+            final SampleModel model = target.getSampleModel();
+            final int numBands = model.getNumBands();
+            return 
Optional.ofNullable(factory.createColorModel(model.getDataType(), numBands, 
visibleBand.getAsInt()));
+        };
+    }
+
+    /**
+     * Creates a colorizer which will associate colors to coverage categories.
+     * 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>.
+     *
+     * <p>This colorizer is used when {@link Target#getRanges()} provides a 
non-empty value.
+     * The given function can return {@code null} or empty arrays for some 
categories,
+     * which are interpreted as fully transparent pixels.</p>
+     *
+     * @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.
+     */
+    public static Colorizer forCategories(final Function<Category,Color[]> 
colors) {
+        ArgumentChecks.ensureNonNull("colors", colors);
+        return (target) -> {
+            final int visibleBand = target.getVisibleBand().orElse(-1);
+            if (visibleBand >= 0) {
+                final List<SampleDimension> ranges = 
target.getRanges().orElse(null);
+                if (visibleBand < ranges.size()) {
+                    final SampleModel model = target.getSampleModel();
+                    final var c = new 
org.apache.sis.internal.coverage.j2d.Colorizer(colors);
+                    c.initialize(model, ranges.get(visibleBand));
+                    return 
Optional.ofNullable(c.createColorModel(model.getDataType(), 
model.getNumBands(), visibleBand));
+                }
+            }
+            return Optional.empty();
+        };
+    }
+
+    /**
+     * Creates a colorizer which will use the specified color model instance 
if compatible with the target.
+     *
+     * @param  colors  the color model instance to use.
+     * @return a colorizer which will try to apply the specified color model 
<i>as-is</i>.
+     */
+    public static Colorizer forInstance(final ColorModel colors) {
+        ArgumentChecks.ensureNonNull("colors", colors);
+        return (target) -> 
colors.isCompatibleSampleModel(target.getSampleModel()) ? Optional.of(colors) : 
Optional.empty();
+    }
+
+    /**
+     * Returns the color model to use for an image having the given sample 
model.
+     * If this function does not support the creation of a color model for the 
given sample model,
+     * then an empty value is returned. In the latter case, caller will 
typically fallback on grayscale.
+     * Otherwise if a non-empty value is returned, then that color model shall 
be
+     * {@linkplain ColorModel#isCompatibleSampleModel(SampleModel) compatible}
+     * with the {@linkplain Target#getSampleModel() target sample model}.
+     *
+     * @param  model  the sample model of the image for which to create a 
color model.
+     * @return the color model to use for the specified sample model.
+     */
+    @Override
+    Optional<ColorModel> apply(Target model);
+
+    /**
+     * Returns a new colorizer which will apply the specified alternative
+     * if this colorizer can not infer a color model.
+     *
+     * @param  alternative  the alternative strategy for creating a color 
model.
+     * @return a new colorizer which will attempt to apply {@code this} first,
+     *         then fallback on the specified alternative this colorizer did 
not produced a result.
+     */
+    default Colorizer orElse(final Colorizer alternative) {
+        ArgumentChecks.ensureNonNull("alternative", alternative);
+        return (model) -> {
+            var result = apply(model);
+            if (result.isEmpty()) {
+                result = alternative.apply(model);
+            }
+            return result;
+        };
+    }
+}
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
index 8cacace734..2ab7c0ef27 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.image;
 
+import java.util.Optional;
 import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
@@ -24,7 +25,6 @@ import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
 import org.apache.sis.util.Workaround;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.FrequencySortedSet;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.coverage.j2d.ImageLayout;
@@ -310,18 +310,15 @@ final class CombinedImageLayout extends ImageLayout {
     }
 
     /**
-     * Builds a default color model with RGB(A) colors or the colors of the 
first visible band.
-     * If the combined image has 3 or 4 bands and the data type is 8 bits 
integer (bytes),
-     * then this method returns a RGB or RGBA color model depending if there 
is 3 or 4 bands.
-     * Otherwise if {@link ImageUtilities#getVisibleBand(RenderedImage)} finds 
that a source image
-     * declares a visible band, then the returned color model will reuse the 
colors of that band.
+     * Builds a default color model with the colors of the first visible band 
found in source images.
+     * If a band is declared visible according {@link 
ImageUtilities#getVisibleBand(RenderedImage)},
+     * then the returned color model will reuse the colors of that visible 
band.
      * Otherwise a grayscale color model is built with a value range inferred 
from the data-type.
+     *
+     * @param  colorizer  user-supplied provider of color model, or {@code 
null} if none.
      */
-    final ColorModel createColorModel() {
-        ColorModel colors = ColorModelFactory.createRGB(sampleModel);
-        if (colors != null) {
-            return colors;
-        }
+    final ColorModel createColorModel(final Colorizer colorizer) {
+        ColorModel colors = null;
         int visibleBand = ColorModelFactory.DEFAULT_VISIBLE_BAND;
         int base = 0;
 search: for (int i=0; i < sources.length; i++) {
@@ -344,28 +341,16 @@ search: for (int i=0; i < sources.length; i++) {
             }
             base += (bands != null) ? bands.length : 
ImageUtilities.getNumBands(source);
         }
+        if (colorizer != null) {
+            Optional<ColorModel> candidate = colorizer.apply(new 
Colorizer.Target(sampleModel, null, visibleBand));
+            if (candidate.isPresent()) {
+                return candidate.get();
+            }
+        }
         colors = ColorModelFactory.derive(colors, sampleModel.getNumBands(), 
visibleBand);
         if (colors != null) {
             return colors;
         }
         return ColorModelFactory.createGrayScale(sampleModel, visibleBand, 
null);
     }
-
-    /**
-     * Ensures that a user-supplied color model is compatible.
-     *
-     * @param  name  parameter name of the user-supplied color model.
-     * @param  cm    the color model to validate. Can be {@code null}.
-     * @throws IllegalArgumentException if the color model is incompatible.
-     */
-    void ensureCompatible(final String name, final ColorModel cm) {
-        final String reason = PlanarImage.verifyCompatibility(sampleModel, cm);
-        if (reason != null) {
-            String message = 
Resources.format(Resources.Keys.IncompatibleColorModel);
-            if (!reason.isEmpty()) {
-                message = message + ' ' + 
Errors.format(Errors.Keys.IllegalValueForProperty_2, reason, name);
-            }
-            throw new IllegalArgumentException(message);
-        }
-    }
 }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index a8f78f1a35..859cb95883 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -29,6 +29,7 @@ import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import java.awt.image.RenderedImage;
+import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.TileObserver;
 import java.awt.image.ImagingOpException;
@@ -36,6 +37,7 @@ import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.Disposable;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.resources.Errors;
@@ -116,7 +118,7 @@ import org.apache.sis.internal.feature.Resources;
  * if the change to dirty state happened after the call to {@link 
#getTile(int, int) getTile(…)}.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
  * @since   1.1
  */
 public abstract class ComputedImage extends PlanarImage implements Disposable {
@@ -257,6 +259,24 @@ public abstract class ComputedImage extends PlanarImage 
implements Disposable {
         reference = new ComputedTiles(this, ws);    // Create cleaner last 
after all arguments have been validated.
     }
 
+    /**
+     * Ensures that a user-supplied color model is compatible.
+     * This is a helper method for argument validation in sub-classes 
constructors.
+     *
+     * @param  colors  the color model to validate. Can be {@code null}.
+     * @throws IllegalArgumentException if the color model is incompatible.
+     */
+    final void ensureCompatible(final ColorModel colors) {
+        final String reason = verifyCompatibility(sampleModel, colors);
+        if (reason != null) {
+            String message = 
Resources.format(Resources.Keys.IncompatibleColorModel);
+            if (!reason.isEmpty()) {
+                message = message + ' ' + 
Errors.format(Errors.Keys.IllegalValueForProperty_2, 
Classes.getShortClassName(colors), reason);
+            }
+            throw new IllegalArgumentException(message);
+        }
+    }
+
     /**
      * Returns a weak reference to this image. Using weak reference instead of 
strong reference may help to
      * reduce memory usage when recomputing the image is cheap. This method 
should not be public because the
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 b01eede431..78a52cb585 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
@@ -73,8 +73,7 @@ import org.apache.sis.coverage.grid.GridCoverageProcessor;
  *   </li><li>
  *     {@linkplain #setFillValues(Number...) Fill values} to use for pixels 
that cannot be computed.
  *   </li><li>
- *     {@linkplain #setCategoryColors(Function) Category colors} for mapping 
sample values
- *     (identified by their range, name or unit of measurement) to colors.
+ *     {@linkplain #setColorizer(Colorizer) Colorization algorithm} to apply 
for colorizing a computed image.
  *   </li><li>
  *     {@linkplain #setImageResizingPolicy(Resizing) Image resizing policy} to 
apply
  *     if a requested image size prevent the image to be tiled.
@@ -216,13 +215,25 @@ public class ImageProcessor implements Cloneable {
      */
     private Number[] fillValues;
 
+    /**
+     * Colorization algorithm to apply on computed image.
+     * A null value means to use implementation-specific default.
+     *
+     * @see #getColorizer()
+     * @see #setColorizer(Colorizer)
+     */
+    private Colorizer colorizer;
+
     /**
      * Colors to use for arbitrary categories of sample values. This function 
can return {@code null}
      * or empty arrays for some categories, which are interpreted as fully 
transparent pixels.
      *
      * @see #getCategoryColors()
      * @see #setCategoryColors(Function)
+     *
+     * @deprecated Replaced by {@link #colorizer}.
      */
+    @Deprecated(since="1.4", forRemoval=true)
     private Function<Category,Color[]> colors;
 
     /**
@@ -360,12 +371,50 @@ public class ImageProcessor implements Cloneable {
         fillValues = (values != null) ? values.clone() : null;
     }
 
+    /**
+     * Returns the colorization algorithm to apply on computed images, or 
{@code null} for default.
+     * This method returns the value set by the last call to {@link 
#setColorizer(Colorizer)}.
+     *
+     * @return colorization algorithm to apply on computed image, or {@code 
null} for default.
+     *
+     * @since 1.4
+     */
+    public synchronized Colorizer getColorizer() {
+        return colorizer;
+    }
+
+    /**
+     * Sets the colorization algorithm to apply on computed images.
+     * The colorizer is invoked when the rendered image produced by an {@code 
ImageProcessor} operation
+     * needs a {@link ColorModel} which is not straightforward.
+     *
+     * <h4>Examples</h4>
+     * <p>The color model of a {@link #resample(RenderedImage, Rectangle, 
MathTransform) resample(…)}
+     * operation is straightforward: it is the same {@link ColorModel} than 
the source image.
+     * Consequently the colorizer is not invoked for that operation.</p>
+     *
+     * <p>But by contrast, the color model of an {@link 
#aggregateBands(RenderedImage...) aggregateBands(…)}
+     * operation can not be determined in such straightforward way.
+     * If three or four bands are aggregated, should they be interpreted as an 
(A)RGB image?
+     * The {@link Colorizer} allows to specify the desired behavior.</p>
+     *
+     * @param colorizer colorization algorithm to apply on computed image, or 
{@code null} for default.
+     *
+     * @since 1.4
+     */
+    public synchronized void setColorizer(final Colorizer colorizer) {
+        this.colorizer = colorizer;
+    }
+
     /**
      * Returns the colors to use for given categories of sample values, or 
{@code null} is unspecified.
      * This method returns the function set by the last call to {@link 
#setCategoryColors(Function)}.
      *
      * @return colors to use for arbitrary categories of sample values, or 
{@code null} for default.
+     *
+     * @deprecated Replaced by {@link #getColorizer()}.
      */
+    @Deprecated(since="1.4", forRemoval=true)
     public synchronized Function<Category,Color[]> getCategoryColors() {
         return colors;
     }
@@ -383,8 +432,12 @@ public class ImageProcessor implements Cloneable {
      * empty arrays for some categories, which are interpreted as fully 
transparent pixels.</p>
      *
      * @param  colors  colors to use for arbitrary categories of sample 
values, or {@code null} for default.
+     *
+     * @deprecated Replaced by {@link #setColorizer(Colorizer)}.
      */
+    @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;
     }
 
@@ -837,19 +890,19 @@ public class ImageProcessor implements Cloneable {
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method 
parameters:
      * <ul>
-     *   <li>(none)</li>
+     *   <li>{@linkplain #getColorizer() Colorizer}.</li>
      * </ul>
      *
      * @param  sources  images whose bands shall be aggregated, in order. At 
least one image must be provided.
      * @return the aggregated image, or {@code sources[0]} returned directly 
if only one image was supplied.
      * @throws IllegalArgumentException if there is an incompatibility between 
some source images.
      *
-     * @see #aggregateBands(RenderedImage[], int[][], ColorModel)
+     * @see #aggregateBands(RenderedImage[], int[][])
      *
      * @since 1.4
      */
     public RenderedImage aggregateBands(final RenderedImage... sources) {
-        return aggregateBands(sources, null, null);
+        return aggregateBands(sources, (int[][]) null);
     }
 
     /**
@@ -872,21 +925,24 @@ public class ImageProcessor implements Cloneable {
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method 
parameters:
      * <ul>
-     *   <li>(none)</li>
+     *   <li>{@linkplain #getColorizer() Colorizer}.</li>
      * </ul>
      *
      * @param  sources  images whose bands shall be aggregated, in order. At 
least one image must be provided.
      * @param  bandsPerSource  bands to use for each source image, in order. 
May contain {@code null} elements.
-     * @param  colors   the color model to apply on aggregated image, or 
{@code null} for inferring a default color model
-     *                  using aggregated number of bands and sample data type.
      * @return the aggregated image, or one of the sources if it can be used 
directly.
      * @throws IllegalArgumentException if there is an incompatibility between 
some source images
      *         or if some band indices are duplicated or outside their range 
of validity.
      *
      * @since 1.4
      */
-    public RenderedImage aggregateBands(RenderedImage[] sources, int[][] 
bandsPerSource, ColorModel colors) {
-        return BandAggregateImage.create(sources, bandsPerSource, colors);
+    public RenderedImage aggregateBands(RenderedImage[] sources, int[][] 
bandsPerSource) {
+        ArgumentChecks.ensureNonEmpty("sources", sources);
+        final Colorizer colorizer;
+        synchronized (this) {
+            colorizer = this.colorizer;
+        }
+        return BandAggregateImage.create(sources, bandsPerSource, colorizer);
     }
 
     /**
@@ -939,7 +995,7 @@ public class ImageProcessor implements Cloneable {
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method 
parameters:
      * <ul>
-     *   <li>(none)</li>
+     *   <li>{@linkplain #getColorizer() Colorizer}.</li>
      * </ul>
      *
      * <h4>Result relationship with source</h4>
@@ -950,13 +1006,14 @@ public class ImageProcessor implements Cloneable {
      * @param  sourceRanges  approximate ranges of values for each band in 
source image, or {@code null} if unknown.
      * @param  converters    the transfer functions to apply on each band of 
the source image.
      * @param  targetType    the type of data in the image resulting from 
conversions.
-     * @param  colorModel    color model of resulting image, or {@code null}.
      * @return the image which computes converted values from the given source.
      *
      * @see GridCoverageProcessor#convert(GridCoverage, MathTransform1D[], 
Function)
+     *
+     * @since 1.4
      */
     public RenderedImage convert(final RenderedImage source, final 
NumberRange<?>[] sourceRanges,
-                MathTransform1D[] converters, final DataType targetType, final 
ColorModel colorModel)
+                                 MathTransform1D[] converters, final DataType 
targetType)
     {
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("converters", converters);
@@ -967,12 +1024,33 @@ public class ImageProcessor implements Cloneable {
             ArgumentChecks.ensureNonNullElement("converters", i, 
converters[i]);
         }
         final ImageLayout layout;
+        final Colorizer colorizer;
         synchronized (this) {
             layout = this.layout;
+            colorizer = this.colorizer;
         }
         // No need to clone `sourceRanges` because it is not stored by 
`BandedSampleConverter`.
-        return unique(BandedSampleConverter.create(source, layout,
-                sourceRanges, converters, targetType.toDataBufferType(), 
colorModel));
+        return unique(BandedSampleConverter.create(source, layout, 
sourceRanges, converters,
+                                                   
targetType.toDataBufferType(), colorizer));
+    }
+
+    /**
+     * @deprecated Replaced by {@link #convert(RenderedImage, 
NumberRange<?>[], MathTransform1D[], DataType)}
+     *             with a color model inferred from the {@link Colorizer}.
+     *
+     * @param  colorModel  color model of resulting image, or {@code null}.
+     */
+    @Deprecated(since="1.4", forRemoval=true)
+    public synchronized RenderedImage convert(final RenderedImage source, 
final NumberRange<?>[] sourceRanges,
+                MathTransform1D[] converters, final DataType targetType, final 
ColorModel colorModel)
+    {
+        final Colorizer old = colorizer;
+        try {
+            colorizer = Colorizer.forInstance(colorModel);
+            return convert(source, sourceRanges, converters, targetType);
+        } finally {
+            colorizer = old;
+        }
     }
 
     /**
@@ -1301,6 +1379,7 @@ public class ImageProcessor implements Cloneable {
             final Interpolation interpolation;
             final Number[]      fillValues;
             final ImageLayout   layout;
+            final Colorizer     colorizer;
             final Function<Category,Color[]> colors;
             final Quantity<?>[] positionalAccuracyHints;
             synchronized (this) {
@@ -1309,6 +1388,7 @@ public class ImageProcessor implements Cloneable {
                 interpolation           = this.interpolation;
                 fillValues              = this.fillValues;
                 layout                  = this.layout;
+                colorizer               = this.colorizer;
                 colors                  = this.colors;
                 positionalAccuracyHints = this.positionalAccuracyHints;
             }
@@ -1317,6 +1397,7 @@ public class ImageProcessor implements Cloneable {
                       errorHandler.equals(other.errorHandler)     &&
                       executionMode.equals(other.executionMode)   &&
                       interpolation.equals(other.interpolation)   &&
+                      Objects.equals(colorizer, other.colorizer)  &&
                       Objects.equals(colors, other.colors)        &&
                       Arrays.equals(fillValues, other.fillValues) &&
                       Arrays.equals(positionalAccuracyHints, 
other.positionalAccuracyHints);
@@ -1332,7 +1413,7 @@ public class ImageProcessor implements Cloneable {
      */
     @Override
     public synchronized int hashCode() {
-        return Objects.hash(getClass(), errorHandler, executionMode, colors, 
interpolation, layout)
+        return Objects.hash(getClass(), errorHandler, executionMode, 
colorizer, interpolation, layout)
                 + 37 * Arrays.hashCode(fillValues)
                 + 39 * Arrays.hashCode(positionalAccuracyHints);
     }
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 0fc2398eed..1b1aad4e5b 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
@@ -29,6 +29,7 @@ import java.awt.image.DirectColorModel;
 import java.awt.image.ComponentColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.DataBuffer;
+import java.awt.image.RenderedImage;
 import org.apache.sis.image.DataType;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.internal.util.Numerics;
@@ -43,6 +44,7 @@ import org.apache.sis.util.Debug;
 
 /**
  * A factory for {@link ColorModel} objects built from a sequence of colors.
+ * Instances of {@code ColorModelFactory} are immutable and thread-safe.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
@@ -52,8 +54,10 @@ import org.apache.sis.util.Debug;
  */
 public final class ColorModelFactory {
     /**
-     * Band to make visible if an image contains many bands
-     * but a color map is defined for only one band.
+     * Band to make visible if an image contains many bands but a color map is 
defined for only one band.
+     * Should always be zero, because this is the only value guaranteed to be 
always present.
+     * This constant is used mostly for making easier to identify locations in 
Java code
+     * where this default value is hard-coded.
      */
     public static final int DEFAULT_VISIBLE_BAND = 0;
 
@@ -74,9 +78,11 @@ public final class ColorModelFactory {
 
     /**
      * A pool of color models previously created by {@link 
#createColorModel()}.
+     * This is stored in a static map instead of in a {@link 
ColorModelFactory} field for allowing
+     * the {@code ColorModelFactory} reference to be cleared when the color 
model is no longer used.
      *
-     * <div class="note"><b>Note:</b>
-     * we use {@linkplain java.lang.ref.WeakReference weak references} instead 
of {@linkplain java.lang.ref.SoftReference
+     * <h4>Implementation note</h4>
+     * We use {@linkplain java.lang.ref.WeakReference weak references} instead 
of {@linkplain java.lang.ref.SoftReference
      * soft references} because the intent is not to cache the values. The 
intent is to share existing instances in order
      * to reduce memory usage. Rational:
      *
@@ -88,9 +94,8 @@ public final class ColorModelFactory {
      *       for saving the few milliseconds requiring for building a new 
color model. Client code should retains their own
      *       reference to a {@link ColorModel} if they plan to reuse it often 
in a short period of time.</li>
      * </ul>
-     * </div>
      */
-    private static final Map<ColorModelFactory,ColorModel> PIECEWISES = new 
WeakValueHashMap<>(ColorModelFactory.class);
+    private static final WeakValueHashMap<ColorModelFactory,ColorModel> 
PIECEWISES = new WeakValueHashMap<>(ColorModelFactory.class);
 
     /**
      * Comparator for sorting ranges by their minimal value.
@@ -135,9 +140,9 @@ public final class ColorModelFactory {
      * The number of pieces (segments) is {@code pieceStarts.length}. The last 
element of this array is the index after the
      * end of the last piece. The indices are integers. Never {@code null} but 
may be empty.
      *
-     * <div class="note"><b>Note:</b>
-     * indices as unsigned short are not sufficient because in the worst case 
the last next index will be 65536,
-     * which would be converted to 0 as a short, causing several exceptions 
afterward.</div>
+     * <h4>Implementation note</h4>
+     * Unsigned short type is not sufficient because in the worst case the 
last next index will be 65536,
+     * which would be converted to 0 as a short, causing several exceptions 
afterward.
      */
     private final int[] pieceStarts;
 
@@ -150,7 +155,12 @@ public final class ColorModelFactory {
     /**
      * Constructs a new {@code ColorModelFactory}. This object will be used as 
a key in a {@link Map},
      * so this is not really a {@code ColorModelFactory} but a kind of "{@code 
ColorModelKey}" instead.
-     * However, since this constructor is private, user does not need to know 
that.
+     * However, since this constructor is private, users do not need to know 
that implementation details.
+     *
+     * @param  dataType     one of the {@link DataBuffer} constants.
+     * @param  numBands     the number of bands (usually 1).
+     * @param  visibleBand  the visible band (usually 0).
+     * @param  colors       colors associated to their range of values.
      *
      * @see #createPiecewise(int, int, int, ColorsForRange[])
      */
@@ -224,9 +234,115 @@ public final class ColorModelFactory {
     }
 
     /**
-     * Constructs the color model from the {@code codes} and {@link #ARGB} 
data.
-     * This method is invoked the first time the color model is created, or 
when
-     * the value in the cache has been discarded.
+     * Creates a new instance with the same colors than the specified one but 
different type and number of bands.
+     * The color arrays are shared, not cloned.
+     *
+     * @param  dataType     one of the {@link DataBuffer} constants.
+     * @param  numBands     the number of bands (usually 1).
+     * @param  visibleBand  the visible band (usually 0).
+     * @param  colors       colors associated to their range of values.
+     */
+    private ColorModelFactory(final int dataType, final int numBands, final 
int visibleBand, final ColorModelFactory colors) {
+        this.dataType    = dataType;
+        this.numBands    = numBands;
+        this.visibleBand = visibleBand;
+        this.minimum     = colors.minimum;
+        this.maximum     = colors.maximum;
+        this.pieceStarts = colors.pieceStarts;
+        this.ARGB        = colors.ARGB;
+    }
+
+    /**
+     * Compares this object with the specified object for equality.
+     * Defined for using {@code ColorModelFactory} as key in a hash map.
+     * This method is public as an implementation side-effect.
+     *
+     * @param  other the other object to compare for equality.
+     * @return whether the two objects are equal.
+     * @hidden
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other == this) {
+            return true;
+        }
+        if (other instanceof ColorModelFactory) {
+            final ColorModelFactory that = (ColorModelFactory) other;
+            return this.dataType    == that.dataType
+                && this.numBands    == that.numBands
+                && this.visibleBand == that.visibleBand
+                && this.minimum     == that.minimum         // Should never be 
NaN.
+                && this.maximum     == that.maximum
+                && Arrays.equals(pieceStarts, that.pieceStarts)
+                && Arrays.deepEquals(ARGB, that.ARGB);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash-code value for use as key in a hash map.
+     * This method is public as an implementation side-effect.
+     *
+     * @return a hash code for using this factory as a key.
+     * @hidden
+     */
+    @Override
+    public int hashCode() {
+        final int categoryCount = pieceStarts.length - 1;
+        int code = 962745549 + (numBands*31 + visibleBand)*31 + categoryCount;
+        for (int i=0; i<categoryCount; i++) {
+            code += Arrays.hashCode(ARGB[i]);
+        }
+        return code;
+    }
+
+    /**
+     * 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.
+     *
+     * @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())));
+    }
+
+    /**
+     * Gets or creates a color model for the specified type and number of 
bands.
+     * This method returns a shared color model if a previous instance exists.
+     *
+     * @param  dataType     the color model type. One of {@link 
DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT},
+     *                      {@link DataBuffer#TYPE_SHORT}, {@link 
DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
+     *                      or {@link DataBuffer#TYPE_DOUBLE}.
+     * @param  numBands     the number of bands for the color model (usually 
1). The returned color model will render only
+     *                      the {@code visibleBand} and ignore the others, but 
the existence of all {@code numBands} will
+     *                      be at least tolerated. Supplemental bands, even 
invisible, are useful for processing.
+     * @param  visibleBand  the band to be made visible (usually 0). All other 
bands (if any) will be ignored.
+     * @return the color model for the specified type and number of bands.
+     */
+    public ColorModel createColorModel(final int dataType, final int numBands, 
final int visibleBand) {
+        return new ColorModelFactory(dataType, numBands, visibleBand, 
this).getColorModel();
+    }
+
+    /**
+     * Returns the color model associated to this {@code ColorModelFactory} 
instance.
+     * This method always returns a unique color model instance,
+     * even if this {@code ColorModelFactory} instance is new.
+     *
+     * @return the color model associated to this instance.
+     */
+    private ColorModel getColorModel() {
+        synchronized (PIECEWISES) {
+            return PIECEWISES.computeIfAbsent(this, 
ColorModelFactory::createColorModel);
+        }
+    }
+
+    /**
+     * Constructs the color model from the {@link #ARGB} data.
+     * This method is invoked the first time the color model is created,
+     * or when the value in the cache has been discarded.
      */
     private ColorModel createColorModel() {
         /*
@@ -248,7 +364,7 @@ public final class ColorModelFactory {
             final int[] nBits = {
                 DataBuffer.getDataTypeSize(dataType)
             };
-            return CACHE.unique(new ComponentColorModel(cs, nBits, false, 
true, Transparency.OPAQUE, dataType));
+            return unique(new ComponentColorModel(cs, nBits, false, true, 
Transparency.OPAQUE, dataType));
         }
         /*
          * Interpolates the colors in the color palette. Colors that do not 
fall
@@ -273,68 +389,6 @@ public final class ColorModelFactory {
         return createIndexColorModel(numBands, visibleBand, colorMap, true, 
transparent);
     }
 
-    /**
-     * Public as an implementation side-effect.
-     *
-     * @return a hash code.
-     */
-    @Override
-    public int hashCode() {
-        final int categoryCount = pieceStarts.length - 1;
-        int code = 962745549 + (numBands*31 + visibleBand)*31 + categoryCount;
-        for (int i=0; i<categoryCount; i++) {
-            code += Arrays.hashCode(ARGB[i]);
-        }
-        return code;
-    }
-
-    /**
-     * Public as an implementation side-effect.
-     *
-     * @param  other the other object to compare for equality.
-     * @return whether the two objects are equal.
-     */
-    @Override
-    public boolean equals(final Object other) {
-        if (other == this) {
-            return true;
-        }
-        if (other instanceof ColorModelFactory) {
-            final ColorModelFactory that = (ColorModelFactory) other;
-            return this.dataType    == that.dataType
-                && this.numBands    == that.numBands
-                && this.visibleBand == that.visibleBand
-                && this.minimum     == that.minimum         // Should never be 
NaN.
-                && this.maximum     == that.maximum
-                && Arrays.equals(pieceStarts, that.pieceStarts)
-                && Arrays.deepEquals(ARGB, that.ARGB);
-        }
-        return false;
-    }
-
-    /**
-     * Returns a color model interpolated for the ranges in the given map 
entries.
-     * Returned instances of {@link ColorModel} are shared among all callers 
in the running virtual machine.
-     *
-     * @param  dataType     the color model type. One of {@link 
DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT},
-     *                      {@link DataBuffer#TYPE_SHORT}, {@link 
DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
-     *                      or {@link DataBuffer#TYPE_DOUBLE}.
-     * @param  numBands     the number of bands for the color model (usually 
1). The returned color model will render only
-     *                      the {@code visibleBand} and ignore the others, but 
the existence of all {@code numBands} will
-     *                      be at least tolerated. Supplemental bands, even 
invisible, are useful for processing.
-     * @param  visibleBand  the band to be made visible (usually 0). All other 
bands (if any) will be ignored.
-     * @param  colors       the colors to use for each range of sample values.
-     *                      The map may contain {@code null} values, which 
means transparent.
-     * @return a color model suitable for {@link java.awt.image.RenderedImage} 
objects with values in the given ranges.
-     *
-     * @see Colorizer
-     */
-    public static ColorModel createPiecewise(final int dataType, final int 
numBands, final int visibleBand,
-                                             final Map<NumberRange<?>, 
Color[]> colors)
-    {
-        return createPiecewise(dataType, numBands, visibleBand, 
ColorsForRange.list(colors.entrySet()));
-    }
-
     /**
      * Returns a color model interpolated for the given ranges and colors.
      * This method builds up the color model from each set of colors 
associated to ranges in the given entries.
@@ -352,15 +406,12 @@ public final class ColorModelFactory {
      *                      be at least tolerated. Supplemental bands, even 
invisible, are useful for processing.
      * @param  visibleBand  the band to be made visible (usually 0). All other 
bands, if any, will be ignored.
      * @param  colors       the colors associated to ranges of sample values.
-     * @return a color model suitable for {@link java.awt.image.RenderedImage} 
objects with values in the given ranges.
+     * @return a color model suitable for {@link RenderedImage} objects with 
values in the given ranges.
      */
     static ColorModel createPiecewise(final int dataType, final int numBands, 
final int visibleBand,
                                       final ColorsForRange[] colors)
     {
-        final ColorModelFactory key = new ColorModelFactory(dataType, 
numBands, visibleBand, colors);
-        synchronized (PIECEWISES) {
-            return PIECEWISES.computeIfAbsent(key, 
ColorModelFactory::createColorModel);
-        }
+        return new ColorModelFactory(dataType, numBands, visibleBand, 
colors).getColorModel();
     }
 
     /**
@@ -375,7 +426,7 @@ public final class ColorModelFactory {
      * @param  ARGB         an array of ARGB values.
      * @param  hasAlpha     indicates whether alpha values are contained in 
the {@code ARGB} array.
      * @param  transparent  the transparent pixel, or -1 for auto-detection.
-     * @return An index color model for the specified array.
+     * @return an index color model for the specified array of ARGB values.
      */
     public static IndexColorModel createIndexColorModel(final int numBands, 
final int visibleBand, final int[] ARGB,
             final boolean hasAlpha, final int transparent)
@@ -398,8 +449,19 @@ public final class ColorModelFactory {
     }
 
     /**
-     * Returns a color model interpolated for the given range of values. This 
is a convenience method for
-     * {@link #createPiecewise(int, int, int, Map)} when the map contains only 
one element.
+     * Returns a unique instance of the given color model.
+     * This method is a shortcut used when the return type does not need to be 
a specialized type.
+     *
+     * @param  cm  the color model.
+     * @return a unique instance of the given color model.
+     */
+    private static ColorModel unique(final ColorModel cm) {
+        return CACHE.unique(cm);
+    }
+
+    /**
+     * Returns a color model interpolated for the given range of values.
+     * This is a convenience method for {@link #piecewise(Map)} when the map 
contains only one element.
      *
      * @param  dataType     the color model type.
      * @param  numBands     the number of bands for the color model (usually 
1).
@@ -407,7 +469,7 @@ public final class ColorModelFactory {
      * @param  lower        the minimum value, inclusive.
      * @param  upper        the maximum value, exclusive.
      * @param  colors       the colors to use for the range of sample values.
-     * @return a color model suitable for {@link java.awt.image.RenderedImage} 
objects with values in the given ranges.
+     * @return a color model suitable for {@link RenderedImage} objects with 
values in the given ranges.
      */
     public static ColorModel createColorScale(final int dataType, final int 
numBands, final int visibleBand,
                                               final double lower, final double 
upper, final Color... colors)
@@ -421,7 +483,7 @@ public final class ColorModelFactory {
      * Creates a color model for opaque images storing pixels as real numbers.
      * The color model can have an arbitrary number of bands, but in current 
implementation only one band is used.
      *
-     * <p><b>Warning:</b> the use of this color model is very slow.
+     * <p><b>Warning:</b> the use of this color model may be very slow.
      * It should be used only when no standard color model can be used.</p>
      *
      * @param  dataType       the color model type as one of {@code 
DataBuffer.TYPE_*} constants.
@@ -445,7 +507,7 @@ public final class ColorModelFactory {
             final ScaledColorSpace cs = new ScaledColorSpace(numComponents, 
visibleBand, minimum, maximum);
             cm = new ScaledColorModel(cs, dataType);
         }
-        return CACHE.unique(cm);
+        return unique(cm);
     }
 
     /**
@@ -578,7 +640,7 @@ public final class ColorModelFactory {
             cm = new 
ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), numBits, 
hasAlpha, false,
                             hasAlpha ? Transparency.TRANSLUCENT : 
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
         }
-        return CACHE.unique(cm);
+        return unique(cm);
     }
 
     /**
@@ -660,7 +722,7 @@ public final class ColorModelFactory {
             // TODO: handle other color models.
             return null;
         }
-        return CACHE.unique(subset);
+        return unique(subset);
     }
 
     /**
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 58cceeb1d6..a284b51ec9 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
@@ -29,13 +29,13 @@ import org.apache.sis.util.ArraysExt;
 
 
 /**
- * Colors to apply on a range of sample values. Instances of {@code 
ColorsForRange} are temporary, used only
- * the time needed for {@link ColorModelFactory#createColorModel(int, int, 
int, ColorsForRange[])}.
+ * Colors to apply on a range of sample values. Instances of {@code 
ColorsForRange} are usually temporary,
+ * used only the time needed for {@link ColorModelFactory#createPiecewise(int, 
int, int, ColorsForRange[])}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.3
  *
- * @see ColorModelFactory#createColorModel(int, int, int, ColorsForRange[])
+ * @see ColorModelFactory#createPiecewise(int, int, int, ColorsForRange[])
  *
  * @since 1.1
  */
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
index 397eec973e..8b6bdbb63c 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java
@@ -156,7 +156,7 @@ public final class ImageUtilities extends Static {
             }
             final SampleModel sm = image.getSampleModel();
             if (sm != null && sm.getNumBands() == 1) {           // Should 
never be null, but we are paranoiac.
-                return 0;
+                return ColorModelFactory.DEFAULT_VISIBLE_BAND;
             }
         }
         return -1;
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
index 5af127340f..2de3cd9135 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java
@@ -156,7 +156,7 @@ public final class BandAggregateImageTest extends TestCase {
             new int[] {1},      // Take second band of image 1.
             null,               // Take all bands of image 2.
             new int[] {0}       // Take first band of image 1.
-        }, null);
+        });
         assertNotNull(result);
         assertEquals(minX,   result.getMinX());
         assertEquals(minY,   result.getMinY());
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
 
b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
index c31b6aa324..466a5d2c0b 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
@@ -23,6 +23,7 @@ import java.util.AbstractSet;
 import java.util.Iterator;
 import java.util.Objects;
 import java.util.Arrays;
+import java.util.function.Function;
 import java.lang.ref.WeakReference;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.ArraysExt;
@@ -72,7 +73,7 @@ import static org.apache.sis.util.collection.WeakEntry.*;
  * then the caller can synchronize on {@code this}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.4
  *
  * @param <K>  the class of key elements.
  * @param <V>  the class of value elements.
@@ -290,7 +291,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> 
{
      * @return whether {@link #count} matches the expected value.
      */
     @Debug
-    final boolean isValid() {
+    private boolean isValid() {
         if (!Thread.holdsLock(this)) {
             throw new AssertionError();
         }
@@ -316,7 +317,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> 
{
      *
      * @param  key  the key (cannot be null).
      */
-    final int keyHashCode(final Object key) {
+    private int keyHashCode(final Object key) {
         switch (comparisonMode) {
             case IDENTITY:    return System.identityHashCode(key);
             case EQUALS:      return key.hashCode();
@@ -331,7 +332,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> 
{
      * @param  k1  the first key (cannot be null).
      * @paral  k2  the second key.
      */
-    final boolean keyEquals(final Object k1, final Object k2) {
+    private boolean keyEquals(final Object k1, final Object k2) {
         switch (comparisonMode) {
             case IDENTITY:    return k1 == k2;
             case EQUALS:      return k1.equals(k2);
@@ -340,6 +341,49 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
         }
     }
 
+    /**
+     * Locates the entry for the given key and, if present, invokes the given 
getter method.
+     *
+     * @param  <R>           type of value returned by the getter method.
+     * @param  key           key of the entry to search in this map.
+     * @param  getter        getter method to invoke on the entry.
+     * @param  defaultValue  value to return if there is no entry for the 
given key.
+     * @return result of the getter function invoked on the entry, or the 
default value if there is no entry.
+     */
+    @SuppressWarnings("unchecked")
+    private synchronized <R> R get(final Object key, final Function<Entry,R> 
getter, final R defaultValue) {
+        assert isValid();
+        if (key != null) {
+            final Entry[] table = this.table;
+            final int index = (keyHashCode(key) & HASH_MASK) % table.length;
+            for (Entry e = table[index]; e != null; e = (Entry) e.next) {
+                if (keyEquals(key, e.key)) {
+                    return getter.apply(e);
+                }
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * If this map contains the specified key, returns the instance contained 
in this map.
+     * Otherwise returns the given {@code key} instance.
+     *
+     * <p>This method can be useful when the keys are potentially large 
objects.
+     * It allows to opportunistically share existing instances, a little bit 
like
+     * when using {@link WeakHashSet} except that this method does not add the 
given
+     * key to this map if not present.</p>
+     *
+     * @param  key  key to look for in this map.
+     * @return the key instance in this map which is equal to the specified 
key, or {@code key} if none.
+     *
+     * @since 1.4
+     */
+    @SuppressWarnings("unchecked")
+    public K intern(final K key) {
+        return get(key, Entry::getKey, key);
+    }
+
     /**
      * Returns {@code true} if this map contains a mapping for the specified 
key.
      * Null keys are considered never present.
@@ -349,15 +393,15 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
      */
     @Override
     public boolean containsKey(final Object key) {
-        return get(key) != null;
+        return get(key, Function.identity(), null) != null;
     }
 
     /**
-     * Returns {@code true} if this map maps one or more keys to this value.
+     * Returns {@code true} if this map maps one or more keys to the specified 
value.
      * Null values are considered never present.
      *
      * @param  value  value whose presence in this map is to be tested.
-     * @return {@code true} if this map maps one or more keys to this value.
+     * @return {@code true} if this map maps one or more keys to the specified 
value.
      */
     @Override
     public synchronized boolean containsValue(final Object value) {
@@ -373,19 +417,24 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
      * @return the value to which this map maps the specified key.
      */
     @Override
-    @SuppressWarnings("unchecked")
-    public synchronized V get(final Object key) {
-        assert isValid();
-        if (key != null) {
-            final Entry[] table = this.table;
-            final int index = (keyHashCode(key) & HASH_MASK) % table.length;
-            for (Entry e = table[index]; e != null; e = (Entry) e.next) {
-                if (keyEquals(key, e.key)) {
-                    return e.get();
-                }
-            }
-        }
-        return null;
+    public V get(final Object key) {
+        return get(key, Entry::get, null);
+    }
+
+    /**
+     * Returns the value to which this map maps the specified key.
+     * Returns {@code defaultValue} if the map contains no mapping for this 
key.
+     * Null keys are considered never present.
+     *
+     * @param  key  key whose associated value is to be returned.
+     * @param  defaultValue  the default mapping of the key.
+     * @return the value to which this map maps the specified key.
+     *
+     * @since 1.4
+     */
+    @Override
+    public V getOrDefault(final Object key, final V defaultValue) {
+        return get(key, Entry::get, defaultValue);
     }
 
     /**
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
index 6f81080122..e3cf89b6e3 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
@@ -34,6 +34,7 @@ import 
org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.internal.referencing.LazySet;
+import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.coverage.Category;
@@ -780,7 +781,7 @@ public class Convention {
      * @return the band on which {@link #getColors(Variable)} will apply.
      */
     public int getVisibleBand() {
-        return 0;
+        return ColorModelFactory.DEFAULT_VISIBLE_BAND;
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
index 9720025fb9..adb90fecb4 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java
@@ -19,7 +19,6 @@ package org.apache.sis.storage.aggregate;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Optional;
-import java.awt.image.ColorModel;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.apache.sis.coverage.SampleDimension;
@@ -60,7 +59,7 @@ import org.apache.sis.util.collection.BackingStoreException;
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.4
  *
- * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][], 
ColorModel)
+ * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][])
  *
  * @since 1.4
  */
@@ -109,13 +108,6 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
      */
     private final int[][] bandsPerSource;
 
-    /**
-     * The color model to apply on aggregated image, or {@code null} for 
default.
-     * If {@code null}, the color model will be inferred from the aggregated 
number
-     * of bands and the sample data type.
-     */
-    private final ColorModel colors;
-
     /**
      * The processor to use for creating grid coverages.
      */
@@ -130,7 +122,7 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
      * @throws IllegalGridGeometryException if a grid geometry is not 
compatible with the others.
      */
     public BandAggregateGridResource(final GridCoverageResource... sources) 
throws DataStoreException {
-        this(null, null, sources, null, null, null);
+        this(null, null, sources, null, null);
     }
 
     /**
@@ -161,15 +153,13 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
      * @param  sources         resources whose bands shall be aggregated, in 
order. At least one resource must be provided.
      * @param  bandsPerSource  sample dimensions for each source. May be 
{@code null} or may contain {@code null} elements.
      * @param  processor       the processor to use for creating grid 
coverages, or {@code null} for a default processor.
-     * @param  colors          the color model to apply on aggregated image, 
or {@code null} for inferring
-     *                         a default color model using aggregated number 
of bands and sample data type.
      * @throws DataStoreException if an error occurred while fetching the grid 
geometry or sample dimensions from a resource.
      * @throws IllegalGridGeometryException if a grid geometry is not 
compatible with the others.
      * @throws IllegalArgumentException if some band indices are duplicated or 
outside their range of validity.
      */
     public BandAggregateGridResource(final Resource parent, final GenericName 
name,
             final GridCoverageResource[] sources, final int[][] bandsPerSource,
-            final GridCoverageProcessor processor, final ColorModel colors) 
throws DataStoreException
+            final GridCoverageProcessor processor) throws DataStoreException
     {
         super(parent);
         try {
@@ -181,7 +171,6 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
             this.sampleDimensions = List.copyOf(aggregate.ranges());
             this.bandsPerSource   = aggregate.bandsPerSource();
             this.processor        = (processor != null) ? processor : new 
GridCoverageProcessor();
-            this.colors           = colors;
         } catch (BackingStoreException e) {
             throw e.unwrapOrRethrow(DataStoreException.class);
         }
@@ -374,6 +363,6 @@ public class BandAggregateGridResource extends 
AbstractGridCoverageResource {
             cursorIndex = source;
             bandsToLoad[numBandsToLoad++] = bandsForCurrentSource[cursorIndex 
- cursorBase];
         }
-        return processor.aggregateRanges(coverages, coverageBands, colors);
+        return processor.aggregateRanges(coverages, coverageBands);
     }
 }
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
index e4a1b8e434..c447fa6aca 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java
@@ -108,7 +108,7 @@ public final class BandAggregateGridResourceTest extends 
TestCase {
         final LocalName testName = Names.createLocalName(null, null, 
"test-name");
         aggregation = new BandAggregateGridResource(null, testName,
                 new GridCoverageResource[] {firstAndSecondBands, 
thirdAndFourthBands, fifthAndSixthBands},
-                new int[][] {null, new int[] {1, 0}, new int[] {1}}, null, 
null);
+                new int[][] {null, new int[] {1, 0}, new int[] {1}}, null);
 
         assertEquals(testName, aggregation.getIdentifier().orElse(null));
         assertAllPixelsEqual(aggregation.read(null), 101, 102, 104, 103, 106);

Reply via email to