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);
}