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 9bc31b8f73 Fix a rounding error which sometime caused a border around 
images in the JavaFX application. Keep a null color model when the grid 
coverage categories define only a single NaN value. Javadoc and formatting.
9bc31b8f73 is described below

commit 9bc31b8f73d982246c39110600cc66f42b05bb84
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Dec 1 12:36:49 2025 +0100

    Fix a rounding error which sometime caused a border around images in the 
JavaFX application.
    Keep a null color model when the grid coverage categories define only a 
single NaN value.
    Javadoc and formatting.
---
 .../main/org/apache/sis/image/Colorizer.java       |  8 +--
 .../main/org/apache/sis/image/ImageLayout.java     | 57 ++++++++++++----
 .../main/org/apache/sis/image/ResampledImage.java  |  6 +-
 .../main/org/apache/sis/image/Visualization.java   |  6 +-
 .../image/internal/shared/ColorScaleBuilder.java   | 77 +++++++++++-----------
 .../sis/image/internal/shared/ColorsForRange.java  |  2 +-
 .../org/apache/sis/map/coverage/RenderingData.java | 14 +++-
 .../sis/gui/coverage/StyledRenderingData.java      |  2 +-
 8 files changed, 110 insertions(+), 62 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java
index fb5cd43032..7353b35caa 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Colorizer.java
@@ -245,9 +245,9 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      *
      * @see ImageProcessor#visualize(RenderedImage)
      */
-    public static Colorizer forRanges(final Map<NumberRange<?>,Color[]> 
colors) {
-        final var list = new 
ArrayList<Map.Entry<NumberRange<?>,Color[]>>(colors.size());
-        for (final Map.Entry<NumberRange<?>,Color[]> entry : 
colors.entrySet()) {
+    public static Colorizer forRanges(final Map<NumberRange<?>, Color[]> 
colors) {
+        final var list = new ArrayList<Map.Entry<NumberRange<?>, 
Color[]>>(colors.size());
+        for (final Map.Entry<NumberRange<?>, Color[]> entry : 
colors.entrySet()) {
             var range = entry.getKey();
             var value = entry.getValue();
             if (value != null) {
@@ -310,7 +310,7 @@ public interface Colorizer extends 
Function<Colorizer.Target, Optional<ColorMode
      *
      * @see ImageProcessor#visualize(RenderedImage)
      */
-    public static Colorizer forCategories(final Function<Category,Color[]> 
colors) {
+    public static Colorizer forCategories(final Function<Category, Color[]> 
colors) {
         ArgumentChecks.ensureNonNull("colors", colors);
         return (target) -> {
             if (target instanceof Visualization.Target) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageLayout.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageLayout.java
index ba515b31b2..3871542240 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageLayout.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageLayout.java
@@ -244,8 +244,11 @@ public class ImageLayout {
         if (Objects.equals(sampleModel, model) && width == preferredTileWidth 
&& height == preferredTileHeight) {
             return this;
         }
-        return new ImageLayout(model, new Dimension(width, height),
-                isTileSizeAdjustmentAllowed, isImageBoundsAdjustmentAllowed, 
isPartialTilesAllowed,
+        return new ImageLayout(model,
+                new Dimension(width, height),
+                isTileSizeAdjustmentAllowed,
+                isImageBoundsAdjustmentAllowed,
+                isPartialTilesAllowed,
                 getPreferredMinTile());
     }
 
@@ -259,9 +262,14 @@ public class ImageLayout {
      * @see #isTileSizeAdjustmentAllowed
      */
     public ImageLayout allowTileSizeAdjustments(boolean allowed) {
-        if (isTileSizeAdjustmentAllowed == allowed) return this;
+        if (isTileSizeAdjustmentAllowed == allowed) {
+            return this;
+        }
         return new ImageLayout(sampleModel,
-                getPreferredTileSize(), allowed, 
isImageBoundsAdjustmentAllowed, isPartialTilesAllowed,
+                getPreferredTileSize(),
+                allowed,
+                isImageBoundsAdjustmentAllowed,
+                isPartialTilesAllowed,
                 getPreferredMinTile());
     }
 
@@ -275,9 +283,14 @@ public class ImageLayout {
      * @see #isImageBoundsAdjustmentAllowed
      */
     public ImageLayout allowImageBoundsAdjustments(boolean allowed) {
-        if (isImageBoundsAdjustmentAllowed == allowed) return this;
+        if (isImageBoundsAdjustmentAllowed == allowed) {
+            return this;
+        }
         return new ImageLayout(sampleModel,
-                getPreferredTileSize(), isTileSizeAdjustmentAllowed, allowed, 
isPartialTilesAllowed,
+                getPreferredTileSize(),
+                isTileSizeAdjustmentAllowed,
+                allowed,
+                isPartialTilesAllowed,
                 getPreferredMinTile());
     }
 
@@ -291,9 +304,14 @@ public class ImageLayout {
      * @see #isPartialTilesAllowed
      */
     public ImageLayout allowPartialTiles(boolean allowed) {
-        if (isPartialTilesAllowed == allowed) return this;
+        if (isPartialTilesAllowed == allowed) {
+            return this;
+        }
         return new ImageLayout(sampleModel,
-                getPreferredTileSize(), isTileSizeAdjustmentAllowed, 
isImageBoundsAdjustmentAllowed, allowed,
+                getPreferredTileSize(),
+                isTileSizeAdjustmentAllowed,
+                isImageBoundsAdjustmentAllowed,
+                allowed,
                 getPreferredMinTile());
     }
 
@@ -319,7 +337,10 @@ public class ImageLayout {
             return this;
         }
         return new ImageLayout(sampleModel,
-                preferredTileSize, isTileSizeAdjustmentAllowed, 
isImageBoundsAdjustmentAllowed, isPartialTilesAllowed,
+                preferredTileSize,
+                isTileSizeAdjustmentAllowed,
+                isImageBoundsAdjustmentAllowed,
+                isPartialTilesAllowed,
                 preferredMinTile);
     }
 
@@ -337,8 +358,11 @@ public class ImageLayout {
             return this;
         }
         return new ImageLayout(sampleModel,
-                size, isTileSizeAdjustmentAllowed, 
isImageBoundsAdjustmentAllowed,
-                isPartialTilesAllowed, getPreferredMinTile());
+                size,
+                isTileSizeAdjustmentAllowed,
+                isImageBoundsAdjustmentAllowed,
+                isPartialTilesAllowed,
+                getPreferredMinTile());
     }
 
     /**
@@ -506,6 +530,11 @@ public class ImageLayout {
          * Optionally adjust the image bounds for making it divisible by the 
tile size.
          */
         if (isImageBoundsAdjustmentAllowed && bounds != null && 
!bounds.isEmpty()) {
+            /*
+             * If we wanted to clip to the source image size, it would be done 
here.
+             * But we don't do that because we don't know if the caller wants 
to apply a scale
+             * factor between the source image and the image for which we 
create a sample model.
+             */
             final int sx = sizeToAdd(bounds.width,  tileWidth);
             final int sy = sizeToAdd(bounds.height, tileHeight);
             if ((bounds.width  += sx) < 0) bounds.width  -= tileWidth;     // 
if (overflow) reduce to valid range.
@@ -585,6 +614,12 @@ public class ImageLayout {
      *         have the same number of bands as the given {@code numBands} 
argument.
      */
     public SampleModel createSampleModel(final DataType dataType, final 
Rectangle bounds, final int numBands) {
+        /*
+         * Note: there is no `RenderedImage` argument (we assume a null 
`image` argument value) because
+         * this method is used for creating sample model that are practically 
independent of the source
+         * image. For example, the new color model may support transparency, 
which makes the check done
+         * by `suggestTileSize(…)` for `image` opacity undesirable.
+         */
         ArgumentChecks.ensureNonNull("bounds", bounds);
         if (sampleModel != null) {
             checkBandCount(numBands);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
index 9bba2001aa..0fdd61773f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
@@ -122,7 +122,7 @@ public class ResampledImage extends ComputedImage {
 
     /**
      * Same as {@link #toSource} but with the addition of a shift for taking 
in account the number of pixels required
-     * for interpolations. For example if a bicubic interpolation needs 4×4 
pixels, then the source coordinates that
+     * for interpolations. For example, if a bicubic interpolation needs 4×4 
pixels, then the source coordinates that
      * we need are not the coordinates of the pixel we want to interpolate, 
but 1 or 2 pixels before for making room
      * for interpolation support.
      *
@@ -186,7 +186,7 @@ public class ResampledImage extends ComputedImage {
     /**
      * Creates a new image which will resample the given image. The resampling 
operation is defined
      * by a potentially non-linear transform from <em>this</em> image to the 
specified <em>source</em> image.
-     * That transform should map {@linkplain 
org.apache.sis.coverage.grid.PixelInCell#CELL_CENTER pixel centers}.
+     * That transform shall map {@linkplain 
org.apache.sis.coverage.grid.PixelInCell#CELL_CENTER pixel centers}.
      *
      * <p>The {@code sampleModel} determines the tile size and the target data 
type. This is often the same sample
      * model than the one used by the {@code source} image, but may also be 
different for forcing a different tile
@@ -197,7 +197,7 @@ public class ResampledImage extends ComputedImage {
      *
      * <p>If a pixel in this image cannot be mapped to a pixel in the source 
image, then the sample values are set
      * to {@code fillValues}. If the given array is {@code null}, or if any 
element in the given array is {@code null},
-     * then the default fill value is NaN for floating point data types or 
zero for integer data types.
+     * then the default fill value is {@link Float#NaN} for floating point 
data types or zero for integer data types.
      * If the array is shorter than the number of bands, then above-cited 
default values are used for missing values.
      * If longer than the number of bands, extraneous values are ignored.</p>
      *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
index c5449d50cd..6a7b97239c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/Visualization.java
@@ -244,9 +244,9 @@ final class Visualization extends ResampledImage {
                 if (source instanceof ImageAdapter) {
                     source = ((ImageAdapter) source).source;
                 } else if (source instanceof ResampledImage) {
-                    final ResampledImage r = (ResampledImage) source;
-                    toSource = MathTransforms.concatenate(toSource, 
r.toSource);
-                    source   = r.getSource();
+                    final var resampled = (ResampledImage) source;
+                    toSource = MathTransforms.concatenate(toSource, 
resampled.toSource);
+                    source   = resampled.getSource();
                 } else {
                     break;
                 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorScaleBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorScaleBuilder.java
index 9a921f1c33..c00d609d7a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorScaleBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorScaleBuilder.java
@@ -111,7 +111,7 @@ public final class ColorScaleBuilder {
      * Applies a gray scale to quantitative category and transparent colors to 
qualitative categories.
      * This is a possible argument for the {@link #ColorScaleBuilder(Function, 
ColorModel, boolean)} constructor.
      */
-    public static final Function<Category,Color[]> GRAYSCALE =
+    public static final Function<Category, Color[]> GRAYSCALE =
             (category) -> category.isQuantitative() ? new Color[] 
{Color.BLACK, Color.WHITE} : null;
 
     /**
@@ -126,7 +126,7 @@ public final class ColorScaleBuilder {
      * the default colors for that category will be the same as {@link 
#GRAYSCALE}:
      * grayscale for quantitative categories and transparent for qualitative 
categories.
      */
-    private final Function<Category,Color[]> colors;
+    private final Function<Category, Color[]> colors;
 
     /**
      * The colors to use for each range of values in the source image.
@@ -187,11 +187,11 @@ public final class ColorScaleBuilder {
      * @param  inherited  the colors to use as fallback if some ranges have 
undefined colors, or {@code null}.
      *                    Should be non-null only for styling an exiting image 
before visualization.
      */
-    public ColorScaleBuilder(final 
Collection<Map.Entry<NumberRange<?>,Color[]>> colors, final ColorModel 
inherited) {
+    public ColorScaleBuilder(final Collection<Map.Entry<NumberRange<?>, 
Color[]>> colors, final ColorModel inherited) {
         entries = ColorsForRange.list(colors, inherited);
         inheritedColors = inherited;
         this.colors = GRAYSCALE;
-        for (final Map.Entry<NumberRange<?>,Color[]> entry : colors) {
+        for (final Map.Entry<NumberRange<?>, Color[]> entry : colors) {
             final NumberRange<?> range = entry.getKey();
             if (range.getMinDouble() < 0 || range.getMaxDouble() >= MAX_VALUE 
+ 1) {
                 compact = true;
@@ -216,7 +216,7 @@ public final class ColorScaleBuilder {
      *                    Should be non-null only for styling an exiting image 
before visualization.
      * @param  compact    Whether to rescale the range of sample values to the 
{@link #TYPE_COMPACT} range.
      */
-    public ColorScaleBuilder(final Function<Category,Color[]> colors, final 
ColorModel inherited, final boolean compact) {
+    public ColorScaleBuilder(final Function<Category, Color[]> colors, final 
ColorModel inherited, final boolean compact) {
         this.colors = (colors != null) ? colors : GRAYSCALE;
         inheritedColors = inherited;
         this.compact = compact;
@@ -256,39 +256,40 @@ public final class ColorScaleBuilder {
      */
     public boolean initialize(final SampleModel model, final SampleDimension 
source) {
         checkInitializationStatus(false);
-        if (source != null) {
-            this.source = source;
-            final List<Category> categories = source.getCategories();
-            if (!categories.isEmpty()) {
-                boolean isUndefined = true;
-                boolean missingNodata = true;
-
-                @SuppressWarnings("LocalVariableHidesMemberVariable")
-                ColorsForRange[] entries = new 
ColorsForRange[categories.size()];
-                for (int i=0; i<entries.length; i++) {
-                    final var range = new ColorsForRange(categories.get(i), 
colors, inheritedColors);
-                    isUndefined &= range.isUndefined();
-                    missingNodata &= range.isData;
-                    entries[i] = range;
-                }
-                if (!isUndefined) {
-                    /*
-                     * If the model uses floating point values and there is no 
"no data" category, add one.
-                     * We force a "no data" category because floating point 
values may be NaN.
-                     */
-                    if (missingNodata && (model == null || 
!DataType.isInteger(model))) {
-                        final int count = entries.length;
-                        entries = Arrays.copyOf(entries, count + 1);
-                        entries[count] = new ColorsForRange(TRANSPARENT,
-                                NumberRange.create(Float.class, Float.NaN), 
null, false, inheritedColors);
-                    }
-                    // Leave `target` to null. It will be computed by 
`compact()` if needed.
-                    this.entries = entries;
-                    return true;
-                }
-            }
+        if (source == null) {
+            return false;
         }
-        return false;
+        this.source = source;
+        final List<Category> categories = source.getCategories();
+        if (categories.isEmpty()) {
+            return false;
+        }
+        boolean isUndefined   = true;
+        boolean missingNodata = true;
+        ColorsForRange[] ranges = new ColorsForRange[categories.size()];
+        for (int i=0; i < ranges.length; i++) {
+            final var range = new ColorsForRange(categories.get(i), colors, 
inheritedColors);
+            isUndefined   &= range.isUndefined();
+            missingNodata &= range.isData;
+            ranges[i]      = range;
+        }
+        if (isUndefined || (!missingNodata && ranges.length <= 1)) {
+            // No color specified, or only a single NaN value.
+            return false;
+        }
+        /*
+         * If the model uses floating point values and there is no "no data" 
category, add one.
+         * We force a "no data" category because floating point values may be 
NaN.
+         */
+        if (missingNodata && (model == null || !DataType.isInteger(model))) {
+            final int count = ranges.length;
+            ranges = Arrays.copyOf(ranges, count + 1);
+            ranges[count] = new ColorsForRange(TRANSPARENT,
+                    NumberRange.create(Float.class, Float.NaN), null, false, 
inheritedColors);
+        }
+        // Leave `target` to null. It will be computed by `compact()` if 
needed.
+        entries = ranges;
+        return true;
     }
 
     /**
@@ -425,7 +426,7 @@ public final class ColorScaleBuilder {
         final List<Category> categories = target.getCategories();
 
         @SuppressWarnings("LocalVariableHidesMemberVariable")
-        final ColorsForRange[] entries = new ColorsForRange[categories.size()];
+        final var entries = new ColorsForRange[categories.size()];
         for (int i=0; i<entries.length; i++) {
             final Category category = categories.get(i);
             final var range = new 
ColorsForRange(category.forConvertedValues(true), colors, inheritedColors);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorsForRange.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorsForRange.java
index a0af7595ac..0c82eda28a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorsForRange.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ColorsForRange.java
@@ -97,7 +97,7 @@ final class ColorsForRange implements 
Comparable<ColorsForRange> {
      * @param  inherited  the original colors to use as fallback, or {@code 
null} if none.
      *                    Should be non-null only for styling an exiting image 
before visualization.
      */
-    ColorsForRange(final Category category, final Function<Category,Color[]> 
colors, final ColorModel inherited) {
+    ColorsForRange(final Category category, final Function<Category, Color[]> 
colors, final ColorModel inherited) {
         this.name        = category.getName();
         this.sampleRange = category.getSampleRange();
         this.isData      = category.isQuantitative();
diff --git 
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
 
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
index 95379abcb4..6b8a2ff765 100644
--- 
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
+++ 
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/RenderingData.java
@@ -62,6 +62,7 @@ import org.apache.sis.system.Modules;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.internal.shared.CloneAccess;
+import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.measure.Quantities;
@@ -657,7 +658,16 @@ public class RenderingData implements CloneAccess {
         MathTransforms.getDomain(cornerToDisplay).ifPresent((domain) -> {
             Shapes2D.intersect(bounds, domain, 0, 1);
         });
-        Shapes2D.transform(MathTransforms.bidimensional(cornerToDisplay), 
bounds, bounds);
+        /*
+         * For computing the bounds of the resampled image, we need to round 
to a smaller rectangle.
+         * Otherwise, interpolation will require coordinates slightly outside 
the source image bounds,
+         * which produce NaN values (often rendered as black borders) in that 
target image.
+         */
+        final Rectangle2D resampled = 
Shapes2D.transform(MathTransforms.bidimensional(cornerToDisplay), bounds, null);
+        bounds.x      = (int)  Math.ceil (resampled.getMinX() - 
Numerics.COMPARISON_THRESHOLD);
+        bounds.y      = (int)  Math.ceil (resampled.getMinY() - 
Numerics.COMPARISON_THRESHOLD);
+        bounds.width  = (int) (Math.floor(resampled.getMaxX() + 
Numerics.COMPARISON_THRESHOLD) - bounds.x);
+        bounds.height = (int) (Math.floor(resampled.getMaxY() + 
Numerics.COMPARISON_THRESHOLD) - bounds.y);
         /*
          * Verify if wraparound is really necessary. We do this check because 
the `displayToCenter` transform
          * may be used for every pixels, so it is worth to make that transform 
more efficient if possible.
@@ -894,6 +904,8 @@ public class RenderingData implements CloneAccess {
     /**
      * Returns a string representation for debugging purposes.
      * The string content may change in any future version.
+     *
+     * @return a string representation for debugging purposes.
      */
     @Override
     public String toString() {
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
index 2fee34261e..95ecbabc4e 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
@@ -67,7 +67,7 @@ final class StyledRenderingData extends RenderingData {
     final RenderedImage recolor() throws DataStoreException {
         RenderedImage image = getSourceImage();
         if (selectedDerivative != Stretching.NONE) {
-            final Map<String,Object> modifiers = statistics();
+            final Map<String, Object> modifiers = statistics();
             if (selectedDerivative == Stretching.AUTOMATIC) {
                 modifiers.put("multStdDev", 3);
             }

Reply via email to