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 7090cb3ada Complete the migration to `Colorizer` in the 
`Visualization` class. Deprecate the `Map<NumberRange,Color[]>` argument in 
`ImageProcessor`. This is replaced by `Colorizer.forRanges(Map)`.
7090cb3ada is described below

commit 7090cb3ada08646cf9cc9e277c54b68235900c01
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Mar 28 14:09:54 2023 +0200

    Complete the migration to `Colorizer` in the `Visualization` class.
    Deprecate the `Map<NumberRange,Color[]>` argument in `ImageProcessor`.
    This is replaced by `Colorizer.forRanges(Map)`.
---
 .../apache/sis/internal/gui/ImageConverter.java    |  13 +-
 .../main/java/org/apache/sis/image/Colorizer.java  |  62 +++++---
 .../java/org/apache/sis/image/ImageProcessor.java  |  87 ++++++++--
 .../java/org/apache/sis/image/Interpolation.java   |   6 +-
 .../java/org/apache/sis/image/Visualization.java   | 177 +++++++++++++--------
 5 files changed, 237 insertions(+), 108 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
index 19a3e5e6cc..eebf6c0227 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
@@ -32,6 +32,7 @@ import javafx.scene.image.ImageView;
 import javafx.scene.image.PixelFormat;
 import javafx.scene.image.PixelWriter;
 import javafx.scene.image.WritableImage;
+import org.apache.sis.image.Colorizer;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.internal.map.coverage.RenderingWorkaround;
@@ -67,9 +68,9 @@ final class ImageConverter extends Task<Statistics[]> {
      * Colors to apply on the mask image when that image is overlay on top of 
another image.
      * Current value is a transparent yellow color.
      */
-    private static final Map<NumberRange<?>,Color[]> MASK_TRANSPARENCY = 
Map.of(
+    private static final Colorizer MASK_TRANSPARENCY = 
Colorizer.forRanges(Map.of(
             NumberRange.create(0, true, 0, true), new Color[] 
{ColorModelFactory.TRANSPARENT},
-            NumberRange.create(1, true, 1, true), new Color[] {new 
Color(0x30FFFF00, true)});
+            NumberRange.create(1, true, 1, true), new Color[] {new 
Color(0x30FFFF00, true)}));
 
     /**
      * The Java2D image to convert.
@@ -207,9 +208,13 @@ final class ImageConverter extends Task<Statistics[]> {
     private RenderedImage getMask(final ImageProcessor processor) {
         final Object mask = source.getProperty(PlanarImage.MASK_KEY);
         if (mask instanceof RenderedImage) try {
-            return processor.visualize((RenderedImage) mask, 
MASK_TRANSPARENCY);
+            processor.setColorizer(MASK_TRANSPARENCY);
+            return processor.visualize((RenderedImage) mask, (java.util.List) 
null);
         } catch (IllegalArgumentException e) {
-            // Ignore, we will not apply any mask. Declare 
PropertyView.setImage(…) as the public method.
+            /*
+             * Ignore, we will not apply any mask over the thumbnail image.
+             * `PropertyView.setImage(…)` is declared as the public method.
+             */
             Logging.recoverableException(LOGGER, PropertyView.class, 
"setImage", e);
         }
         return null;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
index 7f454b6cf6..330e7170e4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
@@ -26,6 +26,7 @@ import java.awt.Color;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.IndexColorModel;
+import java.awt.image.RenderedImage;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
@@ -81,7 +82,7 @@ public interface Colorizer extends Function<Colorizer.Target, 
Optional<ColorMode
         private final int visibleBand;
 
         /**
-         * Creates a new record with the sample model of the image to colorize.
+         * Creates a new target 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.
@@ -135,6 +136,17 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
         public OptionalInt getVisibleBand() {
             return (visibleBand >= 0) ? OptionalInt.of(visibleBand) : 
OptionalInt.empty();
         }
+
+        /**
+         * Returns {@code true} if {@code orElse(…)} should not try 
alternative colorizers.
+         * This is used only for {@link Visualization} operation, which is a 
special case
+         * because it merges 3 operations in a single one.
+         *
+         * @return whether {@link #orElse(Colorizer)} should not try 
alternative.
+         */
+        boolean isConsumed() {
+            return false;
+        }
     }
 
     /**
@@ -175,18 +187,24 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      *
      * @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.
+     *
+     * @see ImageProcessor#visualize(RenderedImage, List)
      */
     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();
+            if (target instanceof Visualization.Target) {
+                ((Visualization.Target) target).rangeColors = colors;
+            } else {
+                final OptionalInt visibleBand = target.getVisibleBand();
+                if (!visibleBand.isEmpty()) {
+                    final SampleModel model = target.getSampleModel();
+                    final int numBands = model.getNumBands();
+                    return 
Optional.ofNullable(factory.createColorModel(model.getDataType(), numBands, 
visibleBand.getAsInt()));
+                }
             }
-            final SampleModel model = target.getSampleModel();
-            final int numBands = model.getNumBands();
-            return 
Optional.ofNullable(factory.createColorModel(model.getDataType(), numBands, 
visibleBand.getAsInt()));
+            return Optional.empty();
         };
     }
 
@@ -202,18 +220,24 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      *
      * @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.
+     *
+     * @see ImageProcessor#visualize(RenderedImage, List)
      */
     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 ColorModelBuilder(colors);
-                    c.initialize(model, ranges.get(visibleBand));
-                    return 
Optional.ofNullable(c.createColorModel(model.getDataType(), 
model.getNumBands(), visibleBand));
+            if (target instanceof Visualization.Target) {
+                ((Visualization.Target) target).categoryColors = colors;
+            } else {
+                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 ColorModelBuilder(colors);
+                        c.initialize(model, ranges.get(visibleBand));
+                        return 
Optional.ofNullable(c.createColorModel(model.getDataType(), 
model.getNumBands(), visibleBand));
+                    }
                 }
             }
             return Optional.empty();
@@ -255,10 +279,10 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      */
     default Colorizer orElse(final Colorizer alternative) {
         ArgumentChecks.ensureNonNull("alternative", alternative);
-        return (model) -> {
-            var result = apply(model);
-            if (result.isEmpty()) {
-                result = alternative.apply(model);
+        return (target) -> {
+            var result = apply(target);
+            if (result.isEmpty() && !target.isConsumed()) {
+                result = alternative.apply(target);
             }
             return result;
         };
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 78a52cb585..f1f30fd2d4 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
@@ -164,6 +164,7 @@ public class ImageProcessor implements Cloneable {
 
     /**
      * Properties (size, tile size, sample model, <i>etc.</i>) of destination 
images.
+     * Shall never be null. Default value is {@link ImageLayout#DEFAULT}.
      *
      * @see #getImageLayout()
      * @see #setImageLayout(ImageLayout)
@@ -1213,12 +1214,24 @@ public class ImageProcessor implements Cloneable {
      *
      * @param  source  the image to recolor for visualization purposes.
      * @param  colors  colors to use for each range of values in the source 
image.
-     * @return recolored image for visualization purposes only.
+     * @deprecated Replaced by {@link #visualize(RenderedImage, List)} with 
{@code null} list argument
+     *             and colors map inferred from the {@link Colorizer}.
      */
-    public RenderedImage visualize(final RenderedImage source, final 
Map<NumberRange<?>,Color[]> colors) {
+    @Deprecated(since="1.4", forRemoval=true)
+    public synchronized RenderedImage visualize(final RenderedImage source, 
final Map<NumberRange<?>,Color[]> colors) {
+        /*
+         * TODO: after removal of this method, search for usages of
+         * `visualize(RenderedImage, List)` and remove unecessary `(List) 
null` cast.
+         */
         ArgumentChecks.ensureNonNull("source", source);
         ArgumentChecks.ensureNonNull("colors", colors);
-        return visualize(new Visualization.Builder(source, colors.entrySet()));
+        final Colorizer old = colorizer;
+        try {
+            colorizer = Colorizer.forRanges(colors);
+            return visualize(new Visualization.Builder(null, source, null, 
null));
+        } finally {
+            colorizer = old;
+        }
     }
 
     /**
@@ -1227,31 +1240,75 @@ public class ImageProcessor implements Cloneable {
      * are used as-is (they are not copied or converted). Otherwise this 
operation will convert sample
      * values to unsigned bytes in order to enable the use of {@link 
IndexColorModel}.
      *
-     * <p>This method is similar to {@link #visualize(RenderedImage, Map)}
-     * except that the {@link Map} argument is splitted in two parts: the 
ranges (map keys) are
-     * {@linkplain Category#getSampleRange() encapsulated in 
<code>Category</code>} objects, themselves
-     * {@linkplain SampleDimension#getCategories() encapsulated in 
<code>SampleDimension</code>} objects.
-     * The colors (map values) are determined by a function receiving {@link 
Category} inputs.
+     * <p>The resulting image is suitable for visualization purposes, but 
should not be used for computation purposes.
+     * There is no guarantee about the number of bands in returned image or 
about which formula is used for converting
+     * floating point values to integer values.</p>
+     *
+     * <h4>Specifying colors for ranges of pixel values</h4>
+     * When no {@link SampleDimension} information is available, the 
recommended way to specify colors is like below.
+     * In this example, <var>min</var> and <var>max</var> are minimum and 
maximum values
+     * (inclusive in this example, but they could be exclusive as well) in the 
<em>source</em> image.
+     * Those extrema can be floating point values. This example specifies only 
one range of values,
+     * but arbitrary numbers of non-overlapping ranges are allowed.
+     *
+     * {@snippet lang="java" :
+     *     NumberRange<?> range = NumberRange.create(min, true, max, true);
+     *     Color[] colors = {Color.BLUE, Color.MAGENTA, Color.RED};
+     *     processor.setColorizer(Colorizer.forRanges(Map.of(range, colors)));
+     *     RenderedImage visualization = processor.visualize(source, null);
+     *     }
+     *
+     * The map given to the colorizer specifies the colors to use for 
different ranges of values in the source image.
+     * The ranges of values in the returned image may not be the same; this 
method is free to rescale them.
+     * The {@link Color} arrays may have any length; colors will be 
interpolated as needed for fitting
+     * the ranges of values in the destination image.
+     *
+     * <h4>Specifying colors for sample dimension categories</h4>
+     * If {@link SampleDimension} information is available, a more flexible 
way to specify colors
+     * is to associate colors to category names instead of predetermined 
ranges of pixel values.
+     * The ranges will be inferred indirectly, {@linkplain 
Category#getSampleRange() from the categories}
+     * themselves {@linkplain SampleDimension#getCategories() encapsulated in 
sample dimensions}.
+     * The colors are determined by a function receiving {@link Category} 
inputs.
+     *
+     * {@snippet lang="java" :
+     *     Map<String,Color[]> colors = Map.of(
+     *         "Temperature", new Color[] {Color.BLUE, Color.MAGENTA, 
Color.RED},
+     *         "Wind speed",  new Color[] {Color.GREEN, Color.CYAN, 
Color.BLUE});
+     *
+     *     processor.setColorizer(Colorizer.forCategories((category) ->
+     *         colors.get(category.getName().toString(Locale.ENGLISH))));
+     *
+     *     RenderedImage visualization = processor.visualize(source, ranges);
+     *     }
+     *
      * This separation makes easier to apply colors based on criterion other 
than numerical values.
      * For example, colors could be determined from {@linkplain 
Category#getName() category name} such as "Temperature",
      * or {@linkplain org.apache.sis.measure.MeasurementRange#unit() units of 
measurement}.
      * The {@link Color} arrays may have any length; colors will be 
interpolated as needed for fitting
-     * the ranges of values in the destination image.</p>
+     * the ranges of values in the destination image.
      *
-     * <p>The resulting image is suitable for visualization purposes, but 
should not be used for computation purposes.
-     * There is no guarantee about the number of bands in returned image or 
about which formula is used for converting
-     * floating point values to integer values.</p>
+     * <p>The two approaches can be combined. For example the following 
colorizer will choose colors based
+     * on sample dimensions if available, or fallback on predefined ranges of 
pixel values otherwise:</p>
+     *
+     * {@snippet lang="java" :
+     *     Function<Category,Color[]>  flexible   = ...;
+     *     Map<NumberRange<?>,Color[]> predefined = ...;
+     *     
processor.setColorizer(Colorizer.forCategories(flexible).orElse(Colorizer.forRanges(predefined)));
+     *     }
      *
      * <h4>Properties used</h4>
      * This operation uses the following properties in addition to method 
parameters:
      * <ul>
-     *   <li>{@linkplain #getCategoryColors() Category colors}.</li>
+     *   <li>{@linkplain #getColorizer() Colorizer}.</li>
      * </ul>
      *
      * @param  source  the image to recolor for visualization purposes.
      * @param  ranges  description of {@code source} bands, or {@code null} if 
none. This is typically
      *                 obtained by {@link 
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
      * @return recolored image for visualization purposes only.
+     *
+     * @see Colorizer#forRanges(Map)
+     * @see Colorizer#forCategories(Function)
      */
     public RenderedImage visualize(final RenderedImage source, final 
List<SampleDimension> ranges) {
         ArgumentChecks.ensureNonNull("source", source);
@@ -1285,7 +1342,7 @@ public class ImageProcessor implements Cloneable {
      *       if {@code bounds} size is not divisible by a tile size.</li>
      *   <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy 
hints}
      *       for enabling faster resampling at the cost of lower 
precision.</li>
-     *   <li>{@linkplain #getCategoryColors() Category colors}.</li>
+     *   <li>{@linkplain #getColorizer() Colorizer}.</li>
      * </ul>
      *
      * @param  source    the image to be resampled and recolored.
@@ -1313,7 +1370,7 @@ public class ImageProcessor implements Cloneable {
         synchronized (this) {
             builder.layout                  = layout;
             builder.interpolation           = interpolation;
-            builder.categoryColors          = colors;
+            builder.colorizer               = colorizer;
             builder.fillValues              = fillValues;
             builder.positionalAccuracyHints = positionalAccuracyHints;
         }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java
index 87bcd4442d..196c1c2e65 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Interpolation.java
@@ -110,13 +110,13 @@ public abstract class Interpolation {
      * If the given image uses an index color model, interpolating the indexed 
values does not produce the
      * expected colors. Safest approach is to disable completely 
interpolations in that case.
      *
-     * <div class="note"><b>Note:</b>
-     * we could interpolate if we knew that all index values, without 
exception (i.e. no index for missing values),
+     * <h4>Design note</h4>
+     * We could interpolate if we knew that all index values, without 
exception (i.e. no index for missing values),
      * are related to measurements by a linear function. In practice it rarely 
happens, because there is usually
      * at least one index value reserved for missing values. Scientific data 
in SIS are usually stored as floating
      * point type (with missing values mapped to NaN), which cannot be 
associated to {@link IndexColorModel}.
      * For now we do not try to perform a more sophisticated detection of 
which interpolations are allowed,
-     * but a future SIS version may revisit this policy if needed.</div>
+     * but a future SIS version may revisit this policy if needed.
      *
      * @return {@link #NEAREST} if interpolations should be restricted to 
nearest-neighbor, or {@code this} otherwise.
      */
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index 914b847f0d..4b8f38804f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -20,7 +20,6 @@ import java.util.Map;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
-import java.util.Collection;
 import java.util.function.Function;
 import java.util.function.DoubleUnaryOperator;
 import java.awt.Color;
@@ -34,7 +33,6 @@ import java.awt.image.WritableRaster;
 import java.awt.image.RenderedImage;
 import java.nio.DoubleBuffer;
 import javax.measure.Quantity;
-import org.apache.sis.coverage.Category;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
@@ -43,10 +41,12 @@ import 
org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.internal.coverage.SampleDimensions;
 import org.apache.sis.internal.coverage.CompoundTransform;
 import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
+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.feature.Resources;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.util.collection.BackingStoreException;
@@ -60,10 +60,56 @@ import org.apache.sis.util.collection.BackingStoreException;
  * {@link WritableRaster#setPixel(int, int, int[])} has more efficient 
implementations for integers.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   1.1
  */
 final class Visualization extends ResampledImage {
+    /**
+     * A colorization target handled in a special way by {@link Colorizer} 
factory methods.
+     * The fields in this class are set when the {@link 
Colorizer#apply(Target)} work needs
+     * to be done by the caller instead. This special case is needed because 
the ranges and
+     * categories specified by the user are relative to the source image while 
colorization
+     * operation needs ranges and categories relative to the target image.
+     */
+    static final class Target extends Colorizer.Target {
+        /**
+         * Colors to apply on the sample value ranges, as supplied by user.
+         */
+        Map<NumberRange<?>,Color[]> rangeColors;
+
+        /**
+         * Colors to apply on the sample dimensions, as supplied by user.
+         */
+        Function<Category,Color[]> categoryColors;
+
+        /**
+         * Whether the {@link Builder} has information about {@link 
SampleDimension} categories.
+         */
+        private final boolean hasCategories;
+
+        /**
+         * Creates a new target with the sample model of the image to colorize.
+         *
+         * @param  model          sample model of the computed image to 
colorize (mandatory).
+         * @param  visibleBand    the band to colorize if the colorization 
algorithm uses only one band, or -1 if none.
+         * @param  hasCategories  whether the builder has information about 
{@link SampleDimension} categories.
+         */
+        Target(final SampleModel model, final int visibleBand, final boolean 
hasCategories) {
+            super(model, null, visibleBand);
+            this.hasCategories = hasCategories;
+        }
+
+        /**
+         * Returns {@code true} if {@code orElse(…)} should not try 
alternative colorizers.
+         *
+         * @return whether {@link #orElse(Colorizer)} should not try 
alternative.
+         */
+        @Override
+        boolean isConsumed() {
+            return (rangeColors != null) || (hasCategories && categoryColors 
!= null);
+        }
+    }
+
     /**
      * Builds an image where all sample values are indices of colors in an 
{@link IndexColorModel}.
      * If the given image stores sample values as unsigned bytes or short 
integers, then those values
@@ -72,8 +118,8 @@ final class Visualization extends ResampledImage {
      *
      * <p>This builder accepts two kinds of input:</p>
      * <ul>
-     *   <li>Non-null {@code sourceBands} and {@link 
ImageProcessor#getCategoryColors()}.</li>
-     *   <li>Non-null {@code rangesAndColors}.</li>
+     *   <li>Non-null {@link #sourceBands} and {@link 
Target#categoryColors}.</li>
+     *   <li>Non-null {@link Target#rangeColors}.</li>
      * </ul>
      *
      * The resulting image is suitable for visualization purposes but should 
not be used for computation purposes.
@@ -91,7 +137,7 @@ final class Visualization extends ResampledImage {
         private static final int NUM_BANDS = 1;
 
         /** Band to make visible. */
-        private static final int VISIBLE_BAND = 0;
+        private static final int VISIBLE_BAND = 
ColorModelFactory.DEFAULT_VISIBLE_BAND;
 
         ////  ┌─────────────────────────────────────┐
         ////  │ Arguments given by user             │
@@ -109,9 +155,6 @@ final class Visualization extends ResampledImage {
         /** Description of {@link #source} bands, or {@code null} if none. */
         private List<SampleDimension> sourceBands;
 
-        /** Colors to apply for range of sample values in source image, or 
{@code null} if none. */
-        private Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors;
-
         ////  ┌─────────────────────────────────────┐
         ////  │ Given by ImageProcesor.configure(…) │
         ////  └─────────────────────────────────────┘
@@ -122,8 +165,8 @@ final class Visualization extends ResampledImage {
         /** Object to use for performing interpolations. */
         Interpolation interpolation;
 
-        /** The colors to use for given categories of sample values, or {@code 
null} is unspecified. */
-        Function<Category,Color[]> categoryColors;
+        /** Provider of colors to apply for range of sample values in source 
image, or {@code null} if none. */
+        Colorizer colorizer;
 
         /** Values to use for pixels in this image that cannot be mapped to 
pixels in source image. */
         Number[] fillValues;
@@ -161,20 +204,6 @@ final class Visualization extends ResampledImage {
             this.sourceBands = sourceBands;
         }
 
-        /**
-         * Creates a builder for a visualization image with colors specified 
for range of values.
-         * Current version assumes that target image bounds are the same than 
source image bounds
-         * and that there is no change of pixel coordinates, but this is not a 
real restriction.
-         * The {@code bounds} and {@code toSource} arguments could be added 
back in the future if useful.
-         *
-         * @param source           the image for which to replace the color 
model.
-         * @param rangesAndColors  range of sample values in source image 
associated to colors to apply.
-         */
-        Builder(final RenderedImage source, final 
Collection<Map.Entry<NumberRange<?>,Color[]>> rangesAndColors) {
-            this.source          = source;
-            this.rangesAndColors = rangesAndColors;
-        }
-
         /**
          * Returns an image where all sample values are indices of colors in 
an {@link IndexColorModel}.
          * If the source image stores sample values as unsigned bytes or short 
integers, then those values
@@ -195,59 +224,91 @@ final class Visualization extends ResampledImage {
          *         cannot be converted to sample values in the recolored image.
          */
         RenderedImage create(final ImageProcessor processor) throws 
NoninvertibleTransformException {
-            final int visibleBand = ImageUtilities.getVisibleBand(source);
+            final RenderedImage coloredSource = source;
+            final int visibleBand = 
ImageUtilities.getVisibleBand(coloredSource);
             if (visibleBand < 0) {
                 // This restriction may be relaxed in a future version if we 
implement conversion to RGB images.
                 throw new 
IllegalArgumentException(Resources.format(Resources.Keys.OperationRequiresSingleBand));
             }
+            /*
+             * Skip any previous `RecoloredImage` since we will replace the 
`ColorModel` by a new one.
+             * Keep only the band to make visible in order to reduce the 
amount of calculation during
+             * resampling and for saving memory.
+             */
+            while (source instanceof RecoloredImage) {
+                source = ((RecoloredImage) source).source;
+            }
+            source = BandSelectImage.create(source, new int[] {visibleBand});
+            /*
+             * If there is no conversion of pixel coordinates, there is no 
need for interpolations.
+             * In such case the `Visualization.computeTile(…)` implementation 
takes a shortcut which
+             * requires the tile layout of destination image to be the same as 
source image.
+             * Otherwise combine interpolation and value conversions in a 
single operation.
+             */
+            if (toSource == null) {
+                toSource = MathTransforms.identity(BIDIMENSIONAL);
+            }
+            final boolean shortcut = toSource.isIdentity() && (bounds == null 
|| ImageUtilities.getBounds(source).contains(bounds));
+            if (shortcut) {
+                layout = ImageLayout.fixedSize(source);
+            }
+            /*
+             * Sample values will be unconditionally converted to integers in 
the [0 … 255] range.
+             * The sample model is a mandatory argument before we invoke 
user-supplied colorizer,
+             * which must be done before to build the color model.
+             */
+            sampleModel = 
layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, 
source, bounds);
+            final Target target = new Target(sampleModel, visibleBand, 
sourceBands != null);
+            if (colorizer != null) {
+                colorModel = colorizer.apply(target).orElse(null);
+            }
             /*
              * Get a `ColorModelBuilder` which will compute the `ColorModel` 
of destination image.
-             * There is different ways to create colorizer, depending on which 
arguments were supplied by user.
+             * There is different ways to setup the builder, depending on 
which `Colorizer` is used.
              * In precedence order:
              *
-             *    - rangesAndColor  : 
Collection<Map.Entry<NumberRange<?>,Color[]>>
-             *    - sourceBands     : List<SampleDimension>
+             *    - rangeColors  : Map<NumberRange<?>,Color[]>
+             *    - sourceBands  : List<SampleDimension>
              *    - statistics
              */
             boolean initialized;
-            final ColorModelBuilder colorizer;
-            if (rangesAndColors != null) {
-                colorizer = new ColorModelBuilder(rangesAndColors);
+            final ColorModelBuilder builder;
+            if (target.rangeColors != null) {
+                builder = new ColorModelBuilder(target.rangeColors.entrySet());
                 initialized = true;
             } else {
                 /*
                  * Ranges of sample values were not specified explicitly. 
Instead, we will try to infer them
-                 * in various ways: sample dimensions, scaled color model, 
statistics in last resort.
+                 * in various ways: sample dimensions, scaled color model, or 
image statistics in last resort.
                  */
-                colorizer = new ColorModelBuilder(categoryColors);
-                initialized = (sourceBands != null) && 
colorizer.initialize(source.getSampleModel(), sourceBands.get(visibleBand));
+                builder = new ColorModelBuilder(target.categoryColors);
+                initialized = (sourceBands != null) && 
builder.initialize(coloredSource.getSampleModel(), 
sourceBands.get(visibleBand));
                 if (initialized) {
                     /*
                      * If we have been able to configure ColorModelBuilder 
using SampleDimension, apply an adjustment
                      * based on the ScaledColorModel if it exists. Use case: 
image is created with an IndexColorModel
-                     * determined by the SampleModel, then user enhanced 
contrast by a call to `stretchColorRamp(…)`
-                     * above. We want to preserve that contrast enhancement.
+                     * determined by the SampleModel, then user enhanced 
contrast by a call to `stretchColorRamp(…)`.
+                     * We want to preserve that contrast enhancement.
                      */
-                    colorizer.rescaleMainRange(source.getColorModel());
+                    builder.rescaleMainRange(coloredSource.getColorModel());
                 } else {
                     /*
                      * If we have not been able to use the SampleDimension, 
try to use the ColorModel or SampleModel.
                      * There is no call to `rescaleMainRange(…)` because the 
following code already uses the range
                      * specified by the ColorModel, if available.
                      */
-                    initialized = colorizer.initialize(source.getColorModel());
+                    initialized = 
builder.initialize(coloredSource.getColorModel());
                     if (!initialized) {
-                        if (source instanceof RecoloredImage) {
-                            final RecoloredImage colored = (RecoloredImage) 
source;
-                            colorizer.initialize(colored.minimum, 
colored.maximum);
+                        if (coloredSource instanceof RecoloredImage) {
+                            final RecoloredImage colored = (RecoloredImage) 
coloredSource;
+                            builder.initialize(colored.minimum, 
colored.maximum);
                             initialized = true;
                         } else {
-                            initialized = 
colorizer.initialize(source.getSampleModel(), visibleBand);
+                            initialized = 
builder.initialize(coloredSource.getSampleModel(), visibleBand);
                         }
                     }
                 }
             }
-            source = BandSelectImage.create(source, new int[] {visibleBand});  
             // Make single-banded.
             if (!initialized) {
                 /*
                  * If none of above `ColorModelBuilder` configurations worked, 
use statistics in last resort.
@@ -255,38 +316,20 @@ final class Visualization extends ResampledImage {
                  */
                 final DoubleUnaryOperator[] sampleFilters = 
SampleDimensions.toSampleFilters(processor, sourceBands);
                 final Statistics statistics = 
processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND];
-                colorizer.initialize(statistics.minimum(), 
statistics.maximum());
+                builder.initialize(statistics.minimum(), statistics.maximum());
             }
-            /*
-             * If we reach this point, sample values need to be converted to 
integers in [0 … 255] range.
-             * Skip any previous `RecoloredImage` since we are replacing the 
`ColorModel` by a new one.
-             */
-            while (source instanceof RecoloredImage) {
-                source = ((RecoloredImage) source).source;
+            if (colorModel == null) {
+                colorModel = builder.compactColorModel(NUM_BANDS, 
VISIBLE_BAND);
             }
-            colorModel = colorizer.compactColorModel(NUM_BANDS, VISIBLE_BAND);
             converters = new MathTransform1D[] {
-                colorizer.getSampleToIndexValues()          // Must be after 
`compactColorModel(…)`.
+                builder.getSampleToIndexValues()            // Must be after 
`compactColorModel(…)`.
             };
-            /*
-             * If there is no conversion of pixel coordinates, there is no 
need for interpolations.
-             * In such case the `Visualization.computeTile(…)` implementation 
takes a shortcut which
-             * requires the tile layout of destination image to be the same as 
source image.
-             */
-            if (toSource == null) {
-                toSource = MathTransforms.identity(BIDIMENSIONAL);
-            }
-            if (toSource.isIdentity() && (bounds == null || 
ImageUtilities.getBounds(source).contains(bounds))) {
-                layout        = ImageLayout.fixedSize(source);
+            if (shortcut) {
                 interpolation = Interpolation.NEAREST;
             } else {
                 interpolation = combine(interpolation.toCompatible(source), 
converters);
                 converters    = null;
             }
-            /*
-             * Final image creation after the tile layout has been chosen.
-             */
-            sampleModel = 
layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS, 
source, bounds);
             if (bounds == null) {
                 bounds = ImageUtilities.getBounds(source);
             }


Reply via email to