This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 869a6d4541d78390e8083f65d82eba8b68666185
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Nov 30 14:13:41 2021 +0100

    Allow to specify default color palettes for netCDF variables.
    This is an internal API for now, with colors depending on conventions.
---
 .../apache/sis/coverage/grid/ImageRenderer.java    | 69 +++++++++++++++++++++-
 .../sis/internal/coverage/j2d/Colorizer.java       |  3 +-
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   | 31 ++++++++++
 .../org/apache/sis/internal/netcdf/Convention.java | 26 +++++++-
 .../org/apache/sis/internal/netcdf/Raster.java     | 28 ++++++++-
 .../apache/sis/internal/netcdf/RasterResource.java | 31 +++++++---
 6 files changed, 173 insertions(+), 15 deletions(-)

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 e269dc3..bf2f424 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
@@ -16,9 +16,11 @@
  */
 package org.apache.sis.coverage.grid;
 
-import java.util.Hashtable;
 import java.util.Arrays;
+import java.util.Hashtable;
+import java.util.function.Function;
 import java.nio.Buffer;
+import java.awt.Color;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.image.ColorModel;
@@ -37,6 +39,7 @@ import org.apache.sis.image.DataType;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.coverage.MismatchedCoverageRangeException;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.internal.coverage.j2d.Colorizer;
 import org.apache.sis.internal.coverage.j2d.DeferredProperty;
 import org.apache.sis.internal.coverage.j2d.RasterFactory;
@@ -247,7 +250,10 @@ public class ImageRenderer {
     private int[] bankIndices;
 
     /**
-     * The band to be made visible (usually 0). All other bands, if any will 
be ignored.
+     * The band to use for defining pixel colors when the image is displayed 
on screen.
+     * All other bands, if any, will exist in the raster but be ignored at 
display time.
+     *
+     * @see #setVisibleBand(int)
      */
     private int visibleBand;
 
@@ -258,6 +264,15 @@ public class ImageRenderer {
     private DataBuffer buffer;
 
     /**
+     * The colors to use for each category. Never {@code null}.
+     * The function may return {@code null}, which means transparent.
+     * The default value is {@link Colorizer#GRAYSCALE}.
+     *
+     * @see #setCategoryColors(Function)
+     */
+    private Function<Category,Color[]> colors;
+
+    /**
      * The properties to give to the image, or {@code null} if none.
      *
      * @see #addProperty(String, Object)
@@ -347,6 +362,7 @@ public class ImageRenderer {
         this.pixelStride    = toIntExact(pixelStride);
         this.scanlineStride = toIntExact(scanlineStride);
         this.offsetZ        = offsetZ;
+        this.colors         = Colorizer.GRAYSCALE;
     }
 
     /**
@@ -603,6 +619,53 @@ public class ImageRenderer {
     }
 
     /**
+     * Specifies the band to use for defining pixel colors when the image is 
displayed on screen.
+     * All other bands, if any, will exist in the raster but be ignored at 
display time.
+     * The default value is 0, the first (and often only) band.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * an {@link java.awt.image.IndexColorModel} will be used for displaying 
the image.</div>
+     *
+     * @param  band  the band to use for display purpose.
+     * @throws IllegalArgumentException if the given band is not between 0 
(inclusive)
+     *         and {@link #getNumBands()} (exclusive).
+     *
+     * @since 1.2
+     */
+    public void setVisibleBand(final int band) {
+        ArgumentChecks.ensureBetween("band", 0, getNumBands() - 1, band);
+        visibleBand = band;
+    }
+
+    /**
+     * Specifies the colors to apply for each category in a sample dimension.
+     * The given function can return {@code null}, which means transparent.
+     * If this method is never invoked, then the default is a grayscale for
+     * {@linkplain Category#isQuantitative() quantitative categories} and
+     * transparent for qualitative categories (typically "no data" values).
+     *
+     * <h4>Example</h4>
+     * the following code specifies a color palette from blue to red with 
white in the middle.
+     * This is useful for data with a clear 0 (white) in the middle of the 
range,
+     * with a minimal value equals to the negative of the maximal value.
+     *
+     * {@preformat java
+     *     setCategoryColors((category) -> category.isQuantitative() ? new 
Color[] {
+     *         Color.BLUE, Color.CYAN, Color.WHITE, Color.YELLOW, Color.RED
+     *     } : null);
+     * }
+     *
+     * @param colors  the colors to use for each category. The {@code colors} 
argument can not be null,
+     *                but {@code colors.apply(Category)} can return null.
+     *
+     * @since 1.2
+     */
+    public void setCategoryColors(final Function<Category,Color[]> colors) {
+        ArgumentChecks.ensureNonNull("colors", colors);
+        this.colors = colors;
+    }
+
+    /**
      * Creates a raster with the data specified by the last call to a {@code 
setData(…)} method.
      * The raster upper-left corner is located at the position given by {@link 
#getBounds()}.
      * The returned raster is often an instance of {@link WritableRaster}, but 
read-only rasters are also allowed.
@@ -666,7 +729,7 @@ public class ImageRenderer {
     @SuppressWarnings("UseOfObsoleteCollectionType")
     public RenderedImage createImage() {
         final Raster raster = createRaster();
-        final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE);
+        final Colorizer colorizer = new Colorizer(colors);
         final ColorModel colors;
         if (colorizer.initialize(bands[visibleBand]) || 
colorizer.initialize(raster.getSampleModel(), visibleBand)) {
             colors = colorizer.createColorModel(buffer.getDataType(), 
bands.length, visibleBand);
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
index 1b3a83f..26e1c45 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java
@@ -98,7 +98,8 @@ public final class Colorizer {
 
     /**
      * Blue to red color palette with white in the middle. Useful for data 
with a clear 0 (white)
-     * in the range center and negative and positive values (to appear blue 
and red respectively).
+     * in the middle of the range and with minimal value equals to the 
negative of maximal value
+     * (to appear blue and red respectively).
      * Used for debugging purposes; production code should use a {@code 
PaletteFactory} instead.
      */
     @Debug
diff --git 
a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
 
b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index 9df14da..3e23d77 100644
--- 
a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ 
b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -16,12 +16,15 @@
  */
 package org.apache.sis.internal.earth.netcdf;
 
+import java.util.Objects;
 import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.regex.Pattern;
+import java.util.function.Function;
+import java.awt.Color;
 import javax.measure.Unit;
 import javax.measure.format.ParserException;
 import org.opengis.referencing.crs.ProjectedCRS;
@@ -41,6 +44,7 @@ import 
org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
 import ucar.nc2.constants.CF;
@@ -536,4 +540,31 @@ public final class GCOM_C extends Convention {
         }
         return super.getUnitFallback(data);
     }
+
+    /**
+     * Returns the colors to use for each category, or {@code null} for the 
default colors.
+     *
+     * @param  data  the variable for which to get the colors.
+     * @return colors to use for each category, or {@code null} for the 
default.
+     */
+    @Override
+    public Function<Category,Color[]> getColors(final Variable data) {
+        if (QA_FLAG.equals(data.getName())) {
+            return (category) -> {
+                final NumberRange<?> range = category.getSampleRange();
+                if (Objects.equals(range.getMinValue(), range.getMaxValue())) {
+                    return null;        // A "no data" value.
+                }
+                /*
+                 * Following colors are not really appropriate for "QA_flag" 
because that variable is a bitmask
+                 * rather than a continuous coverage. Following code may be 
replaced by a better color palette
+                 * in a future version.
+                 */
+                return new Color[] {
+                    Color.BLUE, Color.CYAN, Color.YELLOW, Color.RED
+                };
+            };
+        }
+        return super.getColors(data);
+    }
 }
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
index 19f26c8..d73cf32 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
@@ -23,6 +23,8 @@ import java.util.Iterator;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Locale;
+import java.util.function.Function;
+import java.awt.Color;
 import javax.measure.Unit;
 import javax.measure.format.ParserException;
 import org.opengis.referencing.crs.ProjectedCRS;
@@ -35,6 +37,7 @@ import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.internal.referencing.LazySet;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.math.Vector;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.resources.Errors;
@@ -64,7 +67,7 @@ import ucar.nc2.constants.CF;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see <a href="https://issues.apache.org/jira/browse/SIS-315";>SIS-315</a>
  *
@@ -766,4 +769,25 @@ public class Convention {
     public Unit<?> getUnitFallback(final Variable data) throws ParserException 
{
         return null;
     }
+
+    /**
+     * Returns the band to use for defining pixel colors when the image is 
displayed on screen.
+     * All other bands, if any, will exist in the raster but be ignored at 
display time.
+     * The default value is 0, the first (and often only) band.
+     *
+     * @return the band on which {@link #getColors(Variable)} will apply.
+     */
+    public int getVisibleBand() {
+        return 0;
+    }
+
+    /**
+     * Returns the colors to use for each category, or {@code null} for the 
default colors.
+     *
+     * @param  data  the variable for which to get the colors.
+     * @return colors to use for each category, or {@code null} for the 
default.
+     */
+    public Function<Category,Color[]> getColors(final Variable data) {
+        return null;
+    }
 }
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
index c17ea2e..44196cd 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
@@ -17,10 +17,13 @@
 package org.apache.sis.internal.netcdf;
 
 import java.util.List;
+import java.util.function.Function;
+import java.awt.Color;
 import java.awt.image.DataBuffer;
 import java.awt.image.RenderedImage;
 import java.awt.image.RasterFormatException;
 import org.opengis.coverage.CannotEvaluateException;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
@@ -40,7 +43,7 @@ import org.apache.sis.coverage.grid.BufferedGridCoverage;
  * but it is {@link ImageRenderer} responsibility to perform this substitution 
as an optimization.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.0
  * @module
  */
@@ -57,20 +60,37 @@ final class Raster extends BufferedGridCoverage {
     private final int[] bandOffsets;
 
     /**
+     * The band to use for defining pixel colors when the image is displayed 
on screen.
+     * All other bands, if any, will exist in the raster but be ignored at 
display time.
+     *
+     * @see Convention#getVisibleBand()
+     */
+    private final int visibleBand;
+
+    /**
      * Name to display in error messages. Not to be used for processing.
      */
     private final String label;
 
     /**
+     * The colors to use for each category, or {@code null} for default.
+     * The function may return {@code null}, which means transparent.
+     */
+    private final Function<Category,Color[]> colors;
+
+    /**
      * Creates a new raster from the given resource.
      */
     Raster(final GridGeometry domain, final List<SampleDimension> range, final 
DataBuffer data,
-            final int pixelStride, final int[] bandOffsets, final String label)
+           final String label, final int pixelStride, final int[] bandOffsets, 
final int visibleBand,
+           final Function<Category,Color[]> colors)
     {
         super(domain, range, data);
         this.label       = label;
+        this.colors      = colors;
         this.pixelStride = pixelStride;
         this.bandOffsets = bandOffsets;
+        this.visibleBand = visibleBand;
     }
 
     /**
@@ -85,6 +105,10 @@ final class Raster extends BufferedGridCoverage {
             if (bandOffsets != null) {
                 renderer.setInterleavedPixelOffsets(pixelStride, bandOffsets);
             }
+            if (colors != null) {
+                renderer.setCategoryColors(colors);
+            }
+            renderer.setVisibleBand(visibleBand);
             return renderer.createImage();
         } catch (IllegalArgumentException | ArithmeticException | 
RasterFormatException e) {
             throw new 
CannotEvaluateException(Resources.format(Resources.Keys.CanNotRender_2, label, 
e), e);
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 7001f2e..4fa11f7 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -152,6 +152,14 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
     private final int bandDimension;
 
     /**
+     * The band to use for defining pixel colors when the image is displayed 
on screen.
+     * All other bands, if any, will exist in the raster but be ignored at 
display time.
+     *
+     * @see Convention#getVisibleBand()
+     */
+    private final int visibleBand;
+
+    /**
      * Path to the netCDF file for information purpose, or {@code null} if 
unknown.
      *
      * @see #getComponentFiles()
@@ -182,13 +190,14 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
             final int numBands, final int bandDim, final Object lock)
     {
         super(decoder.listeners);
-        data          = bands.toArray(new Variable[bands.size()]);
-        ranges        = new SampleDimension[numBands];
-        identifier    = decoder.nameFactory.createLocalName(decoder.namespace, 
name);
-        location      = decoder.location;
+        this.lock     = lock;
         gridGeometry  = grid;
         bandDimension = bandDim;
-        this.lock     = lock;
+        location      = decoder.location;
+        identifier    = decoder.nameFactory.createLocalName(decoder.namespace, 
name);
+        visibleBand   = decoder.convention().getVisibleBand();
+        ranges        = new SampleDimension[numBands];
+        data          = bands.toArray(new Variable[bands.size()]);
         assert data.length == (bandDimension >= 0 ? 1 : ranges.length);
     }
 
@@ -523,7 +532,7 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
          * Adds the "missing value" or "fill value" as qualitative categories. 
 If a value has both roles, use "missing value"
          * as category name. If the sample values are already real values, 
then the "no data" values have been replaced by NaN
          * values by Variable.replaceNaN(Object). The qualitative categories 
constructed below must be consistent with the NaN
-         * values created by 'replaceNaN'.
+         * values created by `replaceNaN`.
          */
         boolean setBackground = true;
         int ordinal = band.hasRealValues() ? 0 : -1;
@@ -659,7 +668,7 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
             }
             /*
              * Iterate over netCDF variables in the order they appear in the 
file, not in the order requested
-             * by the 'range' argument.  The intent is to perform sequential 
I/O as much as possible, without
+             * by the `range` argument.  The intent is to perform sequential 
I/O as much as possible, without
              * seeking backward. In the (uncommon) case where bands are one of 
the variable dimension instead
              * than different variables, the reading of the whole variable 
occurs during the first iteration.
              */
@@ -721,11 +730,17 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
                 throw new DataStoreContentException(canNotReadFile(), e);
             }
         }
+        /*
+         * At this point the I/O operation is completed and sample values have 
been stored in a NIO buffer.
+         * Provide to `Raster` all information needed for building a 
`RenderedImage` when requested.
+         */
         if (imageBuffer == null) {
             throw new 
DataStoreContentException(Errors.getResources(getLocale()).getString(Errors.Keys.UnsupportedType_1,
 dataType.name()));
         }
+        final Variable main = data[visibleBand];
         final Raster raster = new Raster(domain, 
UnmodifiableArrayList.wrap(bands), imageBuffer,
-                rangeIndices.getPixelStride(), bandOffsets, 
String.valueOf(identifier));
+                String.valueOf(identifier), rangeIndices.getPixelStride(), 
bandOffsets, visibleBand,
+                main.decoder.convention().getColors(main));
         logReadOperation(location, domain, startTime);
         return raster;
     }

Reply via email to