sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Revisit the API of methods related to ColorModel creations. This is in preparation for addition of an ImageProcessor.toIndexedColors(…) method.
Date Thu, 16 Jul 2020 16:08:45 GMT
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 2775b27  Revisit the API of methods related to ColorModel creations. This is in preparation for addition of an ImageProcessor.toIndexedColors(…) method.
2775b27 is described below

commit 2775b27f4fc4c739c77560cc434fe886945a1c3d
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Jul 16 10:39:15 2020 +0200

    Revisit the API of methods related to ColorModel creations.
    This is in preparation for addition of an ImageProcessor.toIndexedColors(…) method.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  20 +--
 .../org/apache/sis/gui/coverage/RenderingData.java |  14 +-
 .../sis/coverage/grid/BufferedGridCoverage.java    |   6 +-
 .../sis/coverage/grid/ConvertedGridCoverage.java   |   6 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java |  35 ++--
 .../apache/sis/coverage/grid/GridCoverage2D.java   |  16 +-
 .../sis/coverage/grid/GridCoverageBuilder.java     |   7 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   8 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 108 ++++++------
 .../java/org/apache/sis/image/RecoloredImage.java  | 183 +++++++++++----------
 .../internal/coverage/j2d/ColorModelFactory.java   |  75 +++++----
 .../coverage/grid/ConvertedGridCoverageTest.java   |   2 +-
 12 files changed, 245 insertions(+), 235 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 732f205..8a0f55b 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
@@ -447,9 +447,9 @@ public class CoverageCanvas extends MapCanvasAWT {
         private RenderedImage resampledImage;
 
         /**
-         * The resampled image after color ramp stretching or other operation applied.
+         * The resampled image after color ramp stretching and/or index color model applied.
          */
-        private RenderedImage filteredImage;
+        private RenderedImage recoloredImage;
 
         /**
          * The filtered image with tiles computed in advance. The set of prefetched
@@ -486,7 +486,7 @@ public class CoverageCanvas extends MapCanvasAWT {
             displayBounds      = canvas.getDisplayBounds();
             if (data.validateCRS(objectiveCRS)) {
                 resampledImage = canvas.resampledImages.get(Stretching.NONE);
-                filteredImage  = canvas.resampledImages.get(data.selectedDerivative);
+                recoloredImage = canvas.resampledImages.get(data.selectedDerivative);
             }
         }
 
@@ -523,14 +523,14 @@ public class CoverageCanvas extends MapCanvasAWT {
                             & ~(AffineTransform.TYPE_IDENTITY | AffineTransform.TYPE_TRANSLATION)) == 0;
                 }
                 if (!isResampled) {
-                    filteredImage = null;
+                    recoloredImage = null;
                     resampledImage = data.resample(objectiveCRS, objectiveToDisplay);
                     resampledToDisplay = data.getTransform(objectiveToDisplay);
                 }
-                if (filteredImage == null) {
-                    filteredImage = data.filter(resampledImage, displayBounds);
+                if (recoloredImage == null) {
+                    recoloredImage = data.recolor(resampledImage);
                 }
-                prefetchedImage = data.prefetch(filteredImage, resampledToDisplay, displayBounds);
+                prefetchedImage = data.prefetch(recoloredImage, resampledToDisplay, displayBounds);
             } finally {
                 LogHandler.loadingStop(id);
             }
@@ -571,15 +571,15 @@ public class CoverageCanvas extends MapCanvasAWT {
             resampledImages.clear();
             resampledImages.put(Stretching.NONE, newValue);
         }
-        resampledImages.put(data.selectedDerivative, worker.filteredImage);
+        resampledImages.put(data.selectedDerivative, worker.recoloredImage);
         /*
          * Notify the "Image properties" tab that the image changed.
          */
         if (imageProperty != null) {
-            imageProperty.setImage(worker.filteredImage, worker.getVisibleImageBounds());
+            imageProperty.setImage(worker.recoloredImage, worker.getVisibleImageBounds());
         }
         if (statusBar != null) {
-            final Object value = worker.filteredImage.getProperty(PlanarImage.POSITIONAL_ACCURACY_KEY);
+            final Object value = worker.recoloredImage.getProperty(PlanarImage.POSITIONAL_ACCURACY_KEY);
             Quantity<Length> accuracy = null;
             if (value instanceof Quantity<?>[]) {
                 for (final Quantity<?> q : (Quantity<?>[]) value) {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index 100d066..cb41d64 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -24,7 +24,6 @@ import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
-import java.awt.geom.Rectangle2D;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.CoordinateOperation;
@@ -258,16 +257,15 @@ final class RenderingData implements Cloneable {
      * In current implementation, the only operations are stretching the color ramp.
      *
      * @param  resampledImage  the image computed by {@link #resample(CoordinateReferenceSystem, LinearTransform)}.
-     * @param  displayBounds   size and location of the display device, in pixel units.
      * @return image with operation applied and color ramp stretched. May be the same instance than given image.
      */
-    final RenderedImage filter(RenderedImage resampledImage, final Rectangle2D displayBounds) {
+    final RenderedImage recolor(final RenderedImage resampledImage) {
         if (selectedDerivative != Stretching.NONE) {
             final Map<String,Object> modifiers = new HashMap<>(4);
             /*
              * Select the original image as the source of statistics. It saves computation time (no need
              * to recompute the statistics when the projection is changed) and provides more stable visual
-             * output (color ramp computed from same standard deviation in "automatic" mode).
+             * output when standard deviations are used for configuring the color ramp.
              */
             if (statistics == null) {
                 statistics = processor.getStatistics(data, null);
@@ -285,20 +283,20 @@ final class RenderingData implements Cloneable {
      * Computes immediately, possibly using many threads, the tiles that are going to be displayed.
      * The returned instance should be used only for current rendering event; it should not be cached.
      *
-     * @param  filteredImage       the image computed by {@link #filter(RenderedImage, Rectangle2D)}.
+     * @param  recoloredImage      the image computed by {@link #recolor(RenderedImage)}.
      * @param  resampledToDisplay  the transform computed by {@link #getTransform(LinearTransform)}.
      * @param  displayBounds       size and location of the display device, in pixel units.
      * @return a temporary image with tiles intersecting the display region already computed.
      */
-    final RenderedImage prefetch(final RenderedImage filteredImage, final AffineTransform resampledToDisplay,
+    final RenderedImage prefetch(final RenderedImage recoloredImage, final AffineTransform resampledToDisplay,
                                  final Envelope2D displayBounds)
     {
         try {
-            return processor.prefetch(filteredImage, (Rectangle) AffineTransforms2D.transform(
+            return processor.prefetch(recoloredImage, (Rectangle) AffineTransforms2D.transform(
                         resampledToDisplay.createInverse(), displayBounds, new Rectangle()));
         } catch (NoninvertibleTransformException e) {
             recoverableException(e);
-            return filteredImage;
+            return recoloredImage;
         }
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
index 8cb915a..e21cedb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.coverage.grid;
 
-import java.util.Collection;
+import java.util.List;
 import java.awt.image.DataBuffer;
 import java.awt.image.DataBufferByte;
 import java.awt.image.DataBufferDouble;
@@ -106,7 +106,7 @@ public class BufferedGridCoverage extends GridCoverage {
      * @throws IllegalGridGeometryException if the grid extent is larger than the data buffer capacity.
      * @throws ArithmeticException if the number of cells is larger than 64 bits integer capacity.
      */
-    public BufferedGridCoverage(final GridGeometry domain, final Collection<? extends SampleDimension> range, final DataBuffer data) {
+    public BufferedGridCoverage(final GridGeometry domain, final List<? extends SampleDimension> range, final DataBuffer data) {
         super(domain, range);
         this.data = data;
         ArgumentChecks.ensureNonNull("data", data);
@@ -147,7 +147,7 @@ public class BufferedGridCoverage extends GridCoverage {
      * @param  dataType  one of {@code DataBuffer.TYPE_*} constants, the native data type used to store the coverage values.
      * @throws ArithmeticException if the grid size is too large.
      */
-    public BufferedGridCoverage(final GridGeometry grid, final Collection<? extends SampleDimension> bands, final int dataType) {
+    public BufferedGridCoverage(final GridGeometry grid, final List<? extends SampleDimension> bands, final int dataType) {
         super(grid, bands);
         final int n = Math.toIntExact(getSampleCount(grid.getExtent(), bands.size()));
         switch (dataType) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
index d1c0595..b846cae 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
@@ -20,7 +20,6 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Optional;
-import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
 import org.opengis.geometry.DirectPosition;
@@ -30,8 +29,6 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.internal.coverage.j2d.BandedSampleConverter;
-import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.RasterFactory;
 import org.apache.sis.measure.NumberRange;
 
@@ -269,8 +266,7 @@ final class ConvertedGridCoverage extends GridCoverage {
          * That image should never be null. But if an implementation wants to do so, respect that.
          */
         if (image != null) {
-            final ColorModel colorModel = createColorModel(Math.max(0, ImageUtilities.getVisibleBand(image)), dataType);
-            image = BandedSampleConverter.create(image, null, dataType, colorModel, getRanges(), converters);
+            return convert(image, dataType, converters);
         }
         return image;
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index c5bdc33..a625761 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -17,7 +17,6 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.List;
-import java.util.Collection;
 import java.util.Locale;
 import java.util.Optional;
 import java.awt.image.ColorModel;
@@ -26,12 +25,15 @@ import java.awt.image.RenderedImage;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
+import org.apache.sis.internal.coverage.j2d.BandedSampleConverter;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTable;
@@ -91,7 +93,7 @@ public abstract class GridCoverage {
      * @throws NullPointerException if an argument is {@code null} or if the list contains a null element.
      * @throws IllegalArgumentException if the {@code range} list is empty.
      */
-    protected GridCoverage(final GridGeometry domain, final Collection<? extends SampleDimension> ranges) {
+    protected GridCoverage(final GridGeometry domain, final List<? extends SampleDimension> ranges) {
         ArgumentChecks.ensureNonNull ("domain", domain);
         ArgumentChecks.ensureNonEmpty("ranges", ranges);
         gridGeometry = domain;
@@ -157,7 +159,7 @@ public abstract class GridCoverage {
     /**
      * Returns the range of values in each sample dimension, or {@code null} if none.
      */
-    final NumberRange<?>[] getRanges() {
+    private NumberRange<?>[] getRanges() {
         NumberRange<?>[] ranges = null;
         for (int i=0; i<sampleDimensions.length; i++) {
             final Optional<NumberRange<?>> r = sampleDimensions[i].getSampleRange();
@@ -180,17 +182,6 @@ public abstract class GridCoverage {
     }
 
     /**
-     * Creates a color model for the expected range of sample values.
-     *
-     * @param  visibleBand  the band to be made visible (usually 0). All other bands (if any) will be ignored.
-     * @param  dataType     the color model type as one {@link java.awt.image.DataBuffer} constants.
-     * @return proposed color model, or {@code null} if none.
-     */
-    final ColorModel createColorModel(final int visibleBand, final int dataType) {
-        return ColorModelFactory.createColorModel(sampleDimensions, visibleBand, dataType, ColorModelFactory.GRAYSCALE);
-    }
-
-    /**
      * Returns the converted or package view, or {@code null} if not yet computed.
      * It is caller responsibility to ensure that this method is invoked in a synchronized block.
      */
@@ -245,6 +236,22 @@ public abstract class GridCoverage {
     }
 
     /**
+     * Creates a new image of the given data type which will compute values using the given converters.
+     *
+     * @param  source      the image for which to convert sample values.
+     * @param  dataType    the type of this image resulting from conversion of given image.
+     * @param  converters  the transfer functions to apply on each band of the source image.
+     * @return the image which compute converted values from the given source.
+     */
+    final RenderedImage convert(final RenderedImage source, final int dataType, final MathTransform1D[] converters) {
+        final int visibleBands = Math.max(0, ImageUtilities.getVisibleBand(source));
+        final ColorModel cm = ColorModelFactory.createColorModel(dataType, sampleDimensions.length, visibleBands,
+                                    sampleDimensions[visibleBands].getCategories(), ColorModelFactory.GRAYSCALE);
+
+       return BandedSampleConverter.create(source, null, dataType, cm, getRanges(), converters);
+    }
+
+    /**
      * Creates a new function for computing or interpolating sample values at given locations.
      * That function accepts {@link DirectPosition} in arbitrary Coordinate Reference System;
      * conversion to grid indices are applied as needed.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index a281172..e27fd61 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -19,7 +19,6 @@ package org.apache.sis.coverage.grid;
 import java.util.List;
 import java.util.Arrays;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.concurrent.atomic.AtomicReference;
 import java.text.NumberFormat;
@@ -30,7 +29,6 @@ import java.awt.Rectangle;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
-import java.awt.image.ColorModel;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.util.NameFactory;
@@ -44,7 +42,6 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
-import org.apache.sis.internal.coverage.j2d.BandedSampleConverter;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.util.collection.TableColumn;
@@ -151,8 +148,7 @@ public class GridCoverage2D extends GridCoverage {
     {
         super(source.gridGeometry, range);
         final int dataType = ConvertedGridCoverage.getDataType(range, isConverted, source);
-        final ColorModel colorModel = createColorModel(Math.max(0, ImageUtilities.getVisibleBand(source.data)), dataType);
-        data           = BandedSampleConverter.create(source.data, null, dataType, colorModel, getRanges(), converters);
+        data           = convert(source.data, dataType, converters);
         gridToImageX   = source.gridToImageX;
         gridToImageY   = source.gridToImageY;
         xDimension     = source.xDimension;
@@ -208,7 +204,7 @@ public class GridCoverage2D extends GridCoverage {
      * @throws IllegalArgumentException if the image number of bands is not the same than the number of sample dimensions.
      * @throws ArithmeticException if the distance between grid location and image location exceeds the {@code long} capacity.
      */
-    public GridCoverage2D(GridGeometry domain, final Collection<? extends SampleDimension> range, RenderedImage data) {
+    public GridCoverage2D(GridGeometry domain, final List<? extends SampleDimension> range, RenderedImage data) {
         /*
          * The complex nesting of method calls below is a workaround for RFE #4093999
          * ("Relax constraint on placement of this()/super() call in constructors").
@@ -334,7 +330,7 @@ public class GridCoverage2D extends GridCoverage {
      *
      * @see GridGeometry#GridGeometry(GridExtent, Envelope)
      */
-    public GridCoverage2D(final Envelope domain, final Collection<? extends SampleDimension> range, final RenderedImage data) {
+    public GridCoverage2D(final Envelope domain, final List<? extends SampleDimension> range, final RenderedImage data) {
         super(createGridGeometry(data, domain), defaultIfAbsent(range, data, ImageUtilities.getNumBands(data)));
         this.data = data;   // Non-null verified by createGridGeometry(…, data).
         xDimension   = 0;
@@ -402,8 +398,8 @@ public class GridCoverage2D extends GridCoverage {
      * @param  numBands  the number of bands in the given image, or 0 if none.
      * @return the given list of sample dimensions if it was non-null, or a default list otherwise.
      */
-    static Collection<? extends SampleDimension> defaultIfAbsent(Collection<? extends SampleDimension> range,
-                                                                 final RenderedImage data, final int numBands)
+    static List<? extends SampleDimension> defaultIfAbsent(List<? extends SampleDimension> range,
+                                                           final RenderedImage data, final int numBands)
     {
         if (range == null) {
             final short[] names = (data != null) ? ImageUtilities.bandNames(data) : ArraysExt.EMPTY_SHORT;
@@ -430,7 +426,7 @@ public class GridCoverage2D extends GridCoverage {
      * However this class has a little bit of tolerance to missing sample model; it may happen
      * when the image is used only as a matrix storage.
      */
-    private static void verifyBandCount(final Collection<? extends SampleDimension> range, final RenderedImage data) {
+    private static void verifyBandCount(final List<? extends SampleDimension> range, final RenderedImage data) {
         if (range != null) {
             final SampleModel sm = data.getSampleModel();
             if (sm != null) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index 81aea94..c6f5cd3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -432,7 +432,7 @@ public class GridCoverageBuilder {
      */
     public GridCoverage build() throws IllegalStateException {
         GridGeometry grid = domain;                                 // May be replaced by an instance with extent.
-        Collection<? extends SampleDimension> bands = ranges;       // May be replaced by a non-null value.
+        List<? extends SampleDimension> bands = ranges;             // May be replaced by a non-null value.
         /*
          * If not already done, create the image from the raster. We try to create the most standard objects
          * when possible: a BufferedImage (from Java2D), then later a GridCoverage2D (from SIS public API).
@@ -462,9 +462,8 @@ public class GridCoverageBuilder {
                  */
                 bands = GridCoverage2D.defaultIfAbsent(bands, null, raster.getNumBands());
                 final int dataType = raster.getSampleModel().getDataType();
-                final ColorModel colors = ColorModelFactory.createColorModel(
-                        bands.toArray(new SampleDimension[bands.size()]),
-                        visibleBand, dataType, ColorModelFactory.GRAYSCALE);
+                final ColorModel colors = ColorModelFactory.createColorModel(dataType, bands.size(), visibleBand,
+                                            bands.get(visibleBand).getCategories(), ColorModelFactory.GRAYSCALE);
                 /*
                  * Create an image from the raster. We favor BufferedImage instance when possible,
                  * and fallback on TiledImage only if the BufferedImage can not be created.
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 3d32da1..b01351b 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
@@ -245,6 +245,7 @@ public class ImageRenderer {
      *
      * @see #addProperty(String, Object)
      */
+    @SuppressWarnings("UseOfObsoleteCollectionType")
     private Hashtable<String,Object> properties;
 
     /**
@@ -417,6 +418,7 @@ public class ImageRenderer {
      *
      * @since 1.1
      */
+    @SuppressWarnings("UseOfObsoleteCollectionType")
     public void addProperty(final String key, final Object value) {
         ArgumentChecks.ensureNonNull("key",   key);
         ArgumentChecks.ensureNonNull("value", value);
@@ -617,10 +619,11 @@ public class ImageRenderer {
      * @throws RasterFormatException if a call to a {@link WritableRaster} factory method failed.
      * @throws ArithmeticException if a property of the image to construct exceeds the capacity of 32 bits integers.
      */
+    @SuppressWarnings("UseOfObsoleteCollectionType")
     public RenderedImage image() {
         final WritableRaster raster = raster();
-        final ColorModel colors = ColorModelFactory.createColorModel(bands, visibleBand,
-                                    buffer.getDataType(), ColorModelFactory.GRAYSCALE);
+        final ColorModel colors = ColorModelFactory.createColorModel(buffer.getDataType(), bands.length, visibleBand,
+                                                    bands[visibleBand].getCategories(), ColorModelFactory.GRAYSCALE);
 
         SliceGeometry supplier = null;
         if (imageGeometry == null) {
@@ -663,6 +666,7 @@ public class ImageRenderer {
         /**
          * Creates a new buffered image wrapping the given raster.
          */
+        @SuppressWarnings("UseOfObsoleteCollectionType")
         Untiled(final ColorModel colors, final WritableRaster raster, final Hashtable<?,?> properties,
                 final GridGeometry geometry, final SliceGeometry supplier)
         {
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 a8da696..e87ed5f 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
@@ -448,53 +448,55 @@ public class ImageProcessor implements Cloneable {
      */
     public Statistics[] getStatistics(final RenderedImage source, final Shape areaOfInterest) {
         ArgumentChecks.ensureNonNull("source", source);
-        Object property = source.getProperty(StatisticsCalculator.STATISTICS_KEY);
-        if (!(property instanceof Statistics[])) {
-            final boolean parallel, failOnException;
-            final Filter errorListener;
-            synchronized (this) {
-                parallel        = parallel(source);
-                failOnException = failOnException();
-                errorListener   = errorListener();
+        if (areaOfInterest == null) {
+            final Object property = source.getProperty(StatisticsCalculator.STATISTICS_KEY);
+            if (property instanceof Statistics[]) {
+                return (Statistics[]) property;
             }
-            /*
-             * No need to check if the given source is already an instance of StatisticsCalculator.
-             * The way AnnotatedImage cache mechanism is implemented, if statistics result already
-             * exist, they will be used.
-             */
-            final AnnotatedImage calculator = new StatisticsCalculator(source, areaOfInterest, parallel, failOnException);
-            property = calculator.getProperty(StatisticsCalculator.STATISTICS_KEY);
-            calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener);
         }
+        final boolean parallel, failOnException;
+        final Filter errorListener;
+        synchronized (this) {
+            parallel        = parallel(source);
+            failOnException = failOnException();
+            errorListener   = errorListener();
+        }
+        /*
+         * No need to check if the given source is already an instance of StatisticsCalculator.
+         * The way AnnotatedImage cache mechanism is implemented, if statistics results already
+         * exist, they will be used.
+         */
+        final AnnotatedImage calculator = new StatisticsCalculator(source, areaOfInterest, parallel, failOnException);
+        final Object property = calculator.getProperty(StatisticsCalculator.STATISTICS_KEY);
+        calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener);
         return (Statistics[]) property;
     }
 
     /**
      * Returns an image with statistics (minimum, maximum, mean, standard deviation) on each bands.
-     * The property value will be computed when first requested (it is not computed by this method).
+     * The property value will be computed when first requested (it is not computed immediately by this method).
      *
      * <p>If {@code areaOfInterest} is {@code null}, then the default is as below:</p>
      * <ul>
      *   <li>If the {@value StatisticsCalculator#STATISTICS_KEY} property value exists in the given image,
      *       then that image is returned as-is. Note that the existing property value is not necessarily
      *       statistics for the whole image.
-     *       They are whatever statistics the property provided considered as representative.</li>
+     *       They are whatever statistics the property provider considers as representative.</li>
      *   <li>Otherwise an image augmented with a {@value StatisticsCalculator#STATISTICS_KEY} property value
      *       is returned.</li>
      * </ul>
      *
-     * @param  source          the image for which to provide statistics (may be {@code null}).
+     * @param  source          the image for which to provide statistics.
      * @param  areaOfInterest  pixel coordinates of the area of interest, or {@code null} for the default.
      * @return an image with an {@value StatisticsCalculator#STATISTICS_KEY} property.
-     *         May be {@code image} if the given argument is null or already has a statistics property.
+     *         May be {@code image} if the given argument already has a statistics property.
      *
      * @see #getStatistics(RenderedImage, Shape)
      * @see StatisticsCalculator#STATISTICS_KEY
      */
     public RenderedImage statistics(final RenderedImage source, final Shape areaOfInterest) {
-        if (source == null || (areaOfInterest == null &&
-                ArraysExt.contains(source.getPropertyNames(), StatisticsCalculator.STATISTICS_KEY)))
-        {
+        ArgumentChecks.ensureNonNull("source", source);
+        if (areaOfInterest == null && ArraysExt.contains(source.getPropertyNames(), StatisticsCalculator.STATISTICS_KEY)) {
             return source;
         }
         final boolean parallel, failOnException;
@@ -506,36 +508,18 @@ public class ImageProcessor implements Cloneable {
     }
 
     /**
-     * Returns an image with the same sample values than the given image, but with its color ramp stretched between the specified bounds.
-     * For example in a gray scale image, pixels with the minimum value will be black and pixels with the maximum value will be white.
-     * This operation is a kind of <cite>tone mapping</cite>, a technique used in image processing to map one set of colors to another.
-     * The mapping applied by this method is conceptually a simple linear transform (a scale and an offset)
-     * applied on sample values before they are mapped to their colors.
-     *
-     * <p>Current implementation can stretch only gray scale images (it may be extended to indexed color models
-     * in a future version). If this method can not stretch the color ramp, for example because the given image
-     * is an RGB image, then the image is returned unchanged.</p>
-     *
-     * @param  source    the image to recolor (may be {@code null}).
-     * @param  minimum   the sample value to display with the first color of the color ramp (black in a grayscale image).
-     * @param  maximum   the sample value to display with the last color of the color ramp (white in a grayscale image).
-     * @return the image with color ramp stretched between the given bounds, or {@code image} unchanged if the operation
-     *         can not be applied on the given image.
-     */
-    public RenderedImage stretchColorRamp(final RenderedImage source, final double minimum, final double maximum) {
-        ArgumentChecks.ensureFinite("minimum", minimum);
-        ArgumentChecks.ensureFinite("maximum", maximum);
-        return RecoloredImage.create(source, minimum, maximum);
-    }
-
-    /**
      * Returns an image with the same sample values than the given image, but with its color ramp stretched between
-     * automatically determined bounds. This is the same operation than {@link #stretchColorRamp(RenderedImage,
-     * double, double) stretchColorRamp(…)} except that the minimum and maximum values are determined by
-     * {@linkplain #getStatistics(RenderedImage, Shape) statistics} on the image:
-     * a range of value is determined first from the {@linkplain Statistics#minimum() minimum} and
-     * {@linkplain Statistics#maximum() maximum} values found in the image, optionally narrowed to an interval
-     * of some {@linkplain Statistics#standardDeviation(boolean) standard deviations} around the mean value.
+     * specified or inferred bounds. For example in a gray scale image, pixels with the minimum value will be black
+     * and pixels with the maximum value will be white. This operation is a kind of <cite>tone mapping</cite>,
+     * a technique used in image processing to map one set of colors to another. The mapping applied by this method
+     * is conceptually a simple linear transform (a scale and an offset) applied on sample values before they are
+     * mapped to their colors.
+     *
+     * <p>The minimum and maximum value can be either specified explicitly,
+     * or determined from {@linkplain #getStatistics(RenderedImage, Shape) statistics} on the image.
+     * In the later case a range of value is determined first from the {@linkplain Statistics#minimum() minimum}
+     * and {@linkplain Statistics#maximum() maximum} values found in the image, optionally narrowed to an interval
+     * of some {@linkplain Statistics#standardDeviation(boolean) standard deviations} around the mean value.</p>
      *
      * <p>Narrowing with standard deviations is useful for data having a Gaussian distribution, as often seen in nature.
      * In such distribution, 99.9% of the data are between the mean ± 3×<var>standard deviation</var>, but some values
@@ -555,6 +539,14 @@ public class ImageProcessor implements Cloneable {
      *     <th>Purpose</th>
      *     <th>Values</th>
      *   </tr><tr>
+     *     <td>{@code "minimum"}</td>
+     *     <td>Minimum value (omit for computing from statistics).</td>
+     *     <td>{@link Number}</td>
+     *   </tr><tr>
+     *     <td>{@code "maximum"}</td>
+     *     <td>Maximum value (omit for computing from statistics).</td>
+     *     <td>{@link Number}</td>
+     *   </tr><tr>
      *     <td>{@code "multStdDev"}</td>
      *     <td>Multiple of the standard deviation.</td>
      *     <td>{@link Number} (typical values: 1.5, 2 or 3)</td>
@@ -569,18 +561,24 @@ public class ImageProcessor implements Cloneable {
      *   </tr>
      * </table>
      *
-     * @param  source     the image to recolor (may be {@code null}).
+     * <h4>Limitation</h4>
+     * Current implementation can stretch only gray scale images (it may be extended to indexed color models
+     * in a future version). If this method can not stretch the color ramp, for example because the given image
+     * is an RGB image, then the image is returned unchanged.
+     *
+     * @param  source     the image to recolor.
      * @param  modifiers  modifiers for narrowing the range of values, or {@code null} if none.
      * @return the image with color ramp stretched between the automatic bounds,
      *         or {@code image} unchanged if the operation can not be applied on the given image.
      */
     public RenderedImage stretchColorRamp(final RenderedImage source, final Map<String,?> modifiers) {
-        return RecoloredImage.create(this, source, modifiers);
+        ArgumentChecks.ensureNonNull("source", source);
+        return RecoloredImage.stretchColorRamp(this, source, modifiers);
     }
 
     /**
      * Changes the color ramp of the given image. The given image must use an {@link IndexColorModel}
-     * with the same number of colors than the given {@code ARGB} length.
+     * with the same number of colors than the given {@code ARGB} array length.
      *
      * @param  source  the image for which to replace the color model.
      * @param  ARGB    Alpha=Red=Green=Blue codes of new color map.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
index e305976..238cb85 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
@@ -34,9 +34,9 @@ import org.apache.sis.util.Classes;
 
 /**
  * An image with the same sample values than the wrapped image but a different color model.
- * Current implementation can only apply a gray scale. Future implementations may detect
- * the existing color model and try to preserve colors (for example by building an indexed
- * color model).
+ * The only interesting member method is {@link #getColorModel()}, which returns the model
+ * specified at construction time. All other non-trivial methods are static helper methods
+ * for {@link ImageProcessor}, defined here for reducing {@link ImageProcessor} size.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -46,6 +46,8 @@ import org.apache.sis.util.Classes;
 final class RecoloredImage extends ImageAdapter {
     /**
      * The color model to associate with this recolored image.
+     *
+     * @see #getColorModel()
      */
     private final ColorModel colors;
 
@@ -58,110 +60,115 @@ final class RecoloredImage extends ImageAdapter {
     }
 
     /**
-     * Wraps the given image with its colors ramp scaled between the given bounds. If the given image is
-     * already using a color ramp for the given range of values, then that image is returned unchanged.
+     * Returns an image with the same sample values than the given image, but with its color ramp stretched
+     * between specified or inferred bounds. The mapping applied by this method is conceptually a linear
+     * transform applied on sample values before they are mapped to their colors.
      *
-     * @param  source       the image to recolor.
-     * @param  visibleBand  the band to make visible.
-     * @param  minimum      the sample value to display with the first color of the color ramp (black in a grayscale image).
-     * @param  maximum      the sample value to display with the last color of the color ramp (white in a grayscale image).
-     * @return the image with color ramp rescaled between the given bounds. May be the given image returned as-is.
-     */
-    private static RenderedImage create(RenderedImage source, final int visibleBand, final double minimum, final double maximum) {
-        final SampleModel sm = source.getSampleModel();
-        final int dataType = sm.getDataType();
-        final ColorModel colors = ColorModelFactory.createGrayScale(dataType, sm.getNumBands(), visibleBand, minimum, maximum);
-        for (;;) {
-            if (colors.equals(source.getColorModel())) {
-                return source;
-            } else if (source instanceof RecoloredImage) {
-                source = ((RecoloredImage) source).source;
-            } else {
-                break;
-            }
-        }
-        return ImageProcessor.unique(new RecoloredImage(source, colors));
-    }
-
-    /**
-     * Implementation of {@link ImageProcessor#stretchColorRamp(RenderedImage, double, double)}.
-     * Defined in this class for reducing {@link ImageProcessor} size.
-     *
-     * @param  source    the image to recolor (may be {@code null}).
-     * @param  minimum   the sample value to display with the first color of the color ramp (black in a grayscale image).
-     * @param  maximum   the sample value to display with the last color of the color ramp (white in a grayscale image).
-     * @return the image with color ramp stretched between the given bounds, or {@code image} unchanged if the operation
-     *         can not be applied on the given image.
-     */
-    static RenderedImage create(final RenderedImage source, final double minimum, final double maximum) {
-        if (!(minimum < maximum)) {
-            throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2, minimum, maximum));
-        }
-        final int visibleBand = ImageUtilities.getVisibleBand(source);
-        if (visibleBand >= 0) {
-            return create(source, visibleBand, minimum, maximum);
-        }
-        return source;
-    }
-
-    /**
-     * Implementation of {@link ImageProcessor#stretchColorRamp(RenderedImage, Map)}.
-     * Defined in this class for reducing {@link ImageProcessor} size.
-     * See above-cited public method for the list of modifier keys recognized by this method.
+     * <p>Current implementation can stretch only gray scale images (it may be extended to indexed color models
+     * in a future version). If this method can not stretch the color ramp, for example because the given image
+     * is an RGB image, then the image is returned unchanged.</p>
      *
      * @param  processor  the processor to use for computing statistics if needed.
-     * @param  source     the image to recolor (may be {@code null}).
+     * @param  source     the image to recolor (can be {@code null}).
      * @param  modifiers  modifiers for narrowing the range of values, or {@code null} if none.
      * @return the image with color ramp stretched between the automatic bounds,
      *         or {@code image} unchanged if the operation can not be applied on the given image.
+     *
+     * @see ImageProcessor#stretchColorRamp(RenderedImage, Map)
      */
-    static RenderedImage create(final ImageProcessor processor, final RenderedImage source, final Map<String,?> modifiers) {
-        RenderedImage statsSource   = source;
-        Statistics[]  statsAllBands = null;
-        Statistics    statistics    = null;
-        double        deviations    = Double.POSITIVE_INFINITY;
-        if (modifiers != null) {
-            Object value = modifiers.get("multStdDev");
-            if (value instanceof Number) {
-                deviations = ((Number) value).doubleValue();
-                ArgumentChecks.ensureStrictlyPositive("multStdDev", deviations);
-            }
-            value = modifiers.get("statistics");
-            if (value instanceof RenderedImage) {
-                statsSource = (RenderedImage) value;
-            } else if (value instanceof Statistics) {
-                statistics = (Statistics) value;
-            } else if (value instanceof Statistics[]) {
-                statsAllBands = (Statistics[]) value;
-            }
-        }
+    static RenderedImage stretchColorRamp(final ImageProcessor processor, final RenderedImage source, final Map<String,?> modifiers) {
         final int visibleBand = ImageUtilities.getVisibleBand(source);
         if (visibleBand >= 0) {
-            if (statistics == null) {
-                if (statsAllBands == null) {
-                    final Object areaOfInterest = modifiers.get("areaOfInterest");
-                    statsAllBands = processor.getStatistics(statsSource,
-                            (areaOfInterest instanceof Shape) ? (Shape) areaOfInterest : null);
+            RenderedImage statsSource   = source;
+            Statistics[]  statsAllBands = null;
+            Statistics    statistics    = null;
+            double        minimum       = Double.NaN;
+            double        maximum       = Double.NaN;
+            double        deviations    = Double.POSITIVE_INFINITY;
+            /*
+             * Extract and validate parameter values.
+             * No calculation started at this stage.
+             */
+            if (modifiers != null) {
+                final Object minValue = modifiers.get("minimum");
+                if (minValue instanceof Number) {
+                    minimum = ((Number) minValue).doubleValue();
+                }
+                final Object maxValue = modifiers.get("maximum");
+                if (maxValue instanceof Number) {
+                    maximum = ((Number) maxValue).doubleValue();
                 }
-                if (statsAllBands != null && visibleBand < statsAllBands.length) {
-                    statistics = statsAllBands[visibleBand];
+                if (minimum >= maximum) {
+                    throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalRange_2, minValue, maxValue));
+                }
+                Object value = modifiers.get("multStdDev");
+                if (value instanceof Number) {
+                    deviations = ((Number) value).doubleValue();
+                    ArgumentChecks.ensureStrictlyPositive("multStdDev", deviations);
+                }
+                value = modifiers.get("statistics");
+                if (value instanceof RenderedImage) {
+                    statsSource = (RenderedImage) value;
+                } else if (value instanceof Statistics) {
+                    statistics = (Statistics) value;
+                } else if (value instanceof Statistics[]) {
+                    statsAllBands = (Statistics[]) value;
                 }
             }
-            if (statistics != null) {
-                deviations *= statistics.standardDeviation(true);
-                final double mean    = statistics.mean();
-                final double minimum = Math.max(statistics.minimum(), mean - deviations);
-                final double maximum = Math.min(statistics.maximum(), mean + deviations);
-                if (minimum < maximum) {
-                    return create(source, visibleBand, minimum, maximum);
+            /*
+             * If minimum and maximum values were not explicitly specified,
+             * compute them from statistics.
+             */
+            if (Double.isNaN(minimum) || Double.isNaN(maximum)) {
+                if (statistics == null) {
+                    if (statsAllBands == null) {
+                        final Object areaOfInterest = modifiers.get("areaOfInterest");
+                        statsAllBands = processor.getStatistics(statsSource,
+                                (areaOfInterest instanceof Shape) ? (Shape) areaOfInterest : null);
+                    }
+                    if (statsAllBands != null && visibleBand < statsAllBands.length) {
+                        statistics = statsAllBands[visibleBand];
+                    }
+                }
+                if (statistics != null) {
+                    deviations *= statistics.standardDeviation(true);
+                    final double mean = statistics.mean();
+                    if (Double.isNaN(minimum)) minimum = Math.max(statistics.minimum(), mean - deviations);
+                    if (Double.isNaN(maximum)) maximum = Math.min(statistics.maximum(), mean + deviations);
                 }
             }
+            /*
+             * Wraps the given image with its colors ramp scaled between the given bounds. If the given image is
+             * already using a color ramp for the given range of values, then that image is returned unchanged.
+             */
+            if (minimum < maximum) {
+                final SampleModel sm     = source.getSampleModel();
+                final ColorModel  colors = ColorModelFactory.createGrayScale(sm.getDataType(), sm.getNumBands(), visibleBand, minimum, maximum);
+                RenderedImage     parent = source;
+                for (;;) {
+                    if (colors.equals(parent.getColorModel())) {
+                        return parent;
+                    } else if (parent instanceof RecoloredImage) {
+                        parent = ((RecoloredImage) parent).source;
+                    } else {
+                        break;
+                    }
+                }
+                return ImageProcessor.unique(new RecoloredImage(parent, colors));
+            }
         }
         return source;
     }
 
     /**
-     * Implementation of {@link ImageProcessor#recolor(RenderedImage, int[])}.
+     * Changes the color ramp of the given image. The given image must use an {@link IndexColorModel}
+     * with the same number of colors than the given {@code ARGB} array length.
+     *
+     * @param  source  the image for which to replace the color model.
+     * @param  ARGB    Alpha=Red=Green=Blue codes of new color map.
+     * @return image using the given color map.
+     *
+     * @see ImageProcessor#recolor(RenderedImage, int[])
      */
     static RenderedImage recolor(final RenderedImage source, final int[] ARGB) {
         String expected, actual;                                // To be used in case of error.
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 19d4fd8..1d6a3d6 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
@@ -17,9 +17,10 @@
 package org.apache.sis.internal.coverage.j2d;
 
 import java.util.Map;
+import java.util.List;
 import java.util.Arrays;
 import java.util.Comparator;
-import java.util.LinkedHashMap;
+import java.util.AbstractMap;
 import java.util.function.Function;
 import java.awt.Transparency;
 import java.awt.Color;
@@ -30,7 +31,6 @@ import java.awt.image.PackedColorModel;
 import java.awt.image.ComponentColorModel;
 import java.awt.image.DataBuffer;
 import org.apache.sis.coverage.Category;
-import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
@@ -56,7 +56,7 @@ public final class ColorModelFactory {
 
     /**
      * Applies a gray scale to quantitative category and transparent colors to qualitative categories.
-     * This is a possible argument for {@link #createColorModel(SampleDimension[], int, int, Function)}.
+     * This is a possible argument for {@link #createColorModel(int, int, int, List, Function)}.
      */
     public static final Function<Category,Color[]> GRAYSCALE =
             (category) -> category.isQuantitative() ? new Color[] {Color.BLACK, Color.WHITE} : null;
@@ -101,6 +101,7 @@ public final class ColorModelFactory {
 
     /**
      * The minimum (inclusive) and maximum (exclusive) sample values.
+     * This is used only if {@link #type} is not {@link DataBuffer#TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT}.
      */
     private final double minimum, maximum;
 
@@ -150,22 +151,22 @@ 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.
+     *
+     * @see #createColorModel(Map.Entry[], int, int, int)
      */
-    private ColorModelFactory(final Map<? extends NumberRange<?>, ? extends Color[]> categories,
+    private ColorModelFactory(final Map.Entry<NumberRange<?>, Color[]>[] colors,
                               final int visibleBand, final int numBands, final int type)
     {
         this.visibleBand = visibleBand;
         this.numBands    = numBands;
         this.type        = type;
-        @SuppressWarnings({"unchecked", "rawtypes"})
-        final Map.Entry<NumberRange<?>, Color[]>[] entries = categories.entrySet().toArray(new Map.Entry[categories.size()]);
-        Arrays.sort(entries, RANGE_COMPARATOR);
+        Arrays.sort(colors, RANGE_COMPARATOR);
         int     count   = 0;
-        int[]   starts  = new int[entries.length + 1];
-        int[][] codes   = new int[entries.length][];
+        int[]   starts  = new int[colors.length + 1];
+        int[][] codes   = new int[colors.length][];
         double  minimum = Double.POSITIVE_INFINITY;
         double  maximum = Double.NEGATIVE_INFINITY;
-        for (final Map.Entry<NumberRange<?>, Color[]> entry : entries) {
+        for (final Map.Entry<NumberRange<?>, Color[]> entry : colors) {
             final NumberRange<?> range = entry.getKey();
             final double min = range.getMinDouble(true);
             final double max = range.getMaxDouble(false);
@@ -225,6 +226,9 @@ public final class ColorModelFactory {
         /*
          * If the requested type is any type not supported by IndexColorModel,
          * fallback on a generic (but very slow!) color model.
+         *
+         * TODO: current implementation ignores ARGB codes.
+         *       But a future implementation may use them.
          */
         if (type != DataBuffer.TYPE_BYTE && type != DataBuffer.TYPE_USHORT) {
             return createGrayScale(type, numBands, visibleBand, minimum, maximum);
@@ -303,27 +307,28 @@ public final class ColorModelFactory {
     }
 
     /**
-     * Returns a color model interpolated for the ranges in the given sample dimensions.
-     * This method builds up the color model from each category in the visible sample dimension.
+     * Returns a color model interpolated for the ranges in the given categories.
      * Returned instances of {@link ColorModel} are shared among all callers in the running virtual machine.
      *
-     * @param  bands        the sample dimensions for which to create a color model.
-     * @param  visibleBand  the band to be made visible (usually 0). All other bands (if any) will be ignored.
      * @param  type         the color model type. One of {@link DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT},
-     *                      {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE}.
+     *                      {@link DataBuffer#TYPE_SHORT}, {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
+     *                      or {@link DataBuffer#TYPE_DOUBLE}.
+     * @param  numBands     number of bands.
+     * @param  visibleBand  the band to be made visible (usually 0). All other bands (if any) will be ignored.
+     * @param  categories   description of value ranges in the visible band.
      * @param  colors       the colors to use for each category. The function may return {@code null}, which means transparent.
      * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges.
      */
-    public static ColorModel createColorModel(final SampleDimension[] bands,
-            final int visibleBand, final int type, Function<Category,Color[]> colors)
+    public static ColorModel createColorModel(final int type, final int numBands, final int visibleBand,
+                                final List<Category> categories, final Function<Category,Color[]> colors)
     {
-        ArgumentChecks.ensureNonNull("bands",  bands);
-        ArgumentChecks.ensureNonNull("colors", colors);
-        final Map<NumberRange<?>, Color[]> ranges = new LinkedHashMap<>();
-        for (final Category category : bands[visibleBand].getCategories()) {
-            ranges.put(category.getSampleRange(), colors.apply(category));
+        @SuppressWarnings({"unchecked", "rawtypes"})               // Generic array creation.
+        final Map.Entry<NumberRange<?>, Color[]>[] ranges = new Map.Entry[categories.size()];
+        for (int i=0; i<ranges.length; i++) {
+            final Category category = categories.get(i);
+            ranges[i] = new AbstractMap.SimpleImmutableEntry<>(category.getSampleRange(), colors.apply(category));
         }
-        return createColorModel(ranges, visibleBand, bands.length, type);
+        return createColorModel(ranges, visibleBand, numBands, type);
     }
 
     /**
@@ -331,28 +336,28 @@ public final class ColorModelFactory {
      * This method builds up the color model from each set of colors associated to ranges in the given map.
      * Returned instances of {@link ColorModel} are shared among all callers in the running virtual machine.
      *
-     * @param  categories   the colors associated to ranges of sample values.
+     * <p>The given ranges are rounded to nearest integers and clamped to the range of 32 bits integer values.
+     * The associated arrays of colors do not need to have a length equals to {@code upper} − {@code lower};
+     * color interpolations will be applied as needed.</p>
+     *
+     * @param  colors       the colors associated to ranges of sample values.
      * @param  visibleBand  the band to be made visible (usually 0). All other bands, if any will be ignored.
      * @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  type         the color model type. One of {@link DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT},
-     *                      {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE}.
+     *                      {@link DataBuffer#TYPE_SHORT}, {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT}
+     *                      or {@link DataBuffer#TYPE_DOUBLE}.
      * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges.
      */
-    public static ColorModel createColorModel(final Map<? extends NumberRange<?>, ? extends Color[]> categories,
-            final int visibleBand, final int numBands, final int type)
+    public static ColorModel createColorModel(final Map.Entry<NumberRange<?>, Color[]>[] colors,
+                                              final int visibleBand, final int numBands, final int type)
     {
-        ArgumentChecks.ensureNonNull("categories", categories);
+        ArgumentChecks.ensureNonNull("colors", colors);
         ArgumentChecks.ensureBetween("visibleBand", 0, numBands - 1, visibleBand);
-        final ColorModelFactory key = new ColorModelFactory(categories, visibleBand, numBands, type);
+        final ColorModelFactory key = new ColorModelFactory(colors, visibleBand, numBands, type);
         synchronized (PIECEWISES) {
-            ColorModel model = PIECEWISES.get(key);
-            if (model == null) {
-                model = key.createColorModel();
-                PIECEWISES.put(key, model);
-            }
-            return model;
+            return PIECEWISES.computeIfAbsent(key, ColorModelFactory::createColorModel);
         }
     }
 
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
index 455e310..9109296 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
@@ -63,7 +63,7 @@ public final strictfp class ConvertedGridCoverageTest extends TestCase {
                 new AffineTransform2D(1, 0, 0, 1, 1, 0), HardCodedCRS.WGS84);
 
         final BufferedGridCoverage coverage = new BufferedGridCoverage(
-                grid, Collections.singleton(sd), DataBuffer.TYPE_SHORT);
+                grid, Collections.singletonList(sd), DataBuffer.TYPE_SHORT);
 
         coverage.data.setElem(0, -1);
         coverage.data.setElem(1,  3);


Mime
View raw message