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

asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git

commit f7bef1427dd8d8afa3e8c827da6d89018e005c0a
Merge: 20ea158492 9db36b9428
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jun 19 19:32:02 2026 +0200

    Merge branch 'geoapi-3.1'.
    Contains developments of HEIF reader with support of `tili` image scheme 
and pyramid.

 .../sis/coverage/grid/BufferedGridCoverage.java    |   2 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |  38 ++
 .../org/apache/sis/coverage/grid/GridGeometry.java |  41 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   2 +-
 .../sis/geometry/wrapper/jts/GeometryWalker.java   | 184 ++++++++
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |  64 +--
 .../main/org/apache/sis/image/ComputedImage.java   |   2 +-
 .../main/org/apache/sis/image/ImageProcessor.java  |  34 +-
 .../main/org/apache/sis/image/MaskImage.java       |   7 +-
 .../main/org/apache/sis/image/OverviewImage.java   | 310 +++++++++++++
 .../main/org/apache/sis/image/PixelIterator.java   |   6 +-
 .../main/org/apache/sis/image/PlanarImage.java     |  29 +-
 .../sis/image/PositionalConsistencyImage.java      |   2 +
 .../main/org/apache/sis/image/ResampledImage.java  |   5 +-
 .../org/apache/sis/image/StatisticsCalculator.java |   8 +-
 .../main/org/apache/sis/image/UserProperties.java  |   2 +-
 .../image/internal/shared/DeferredProperty.java    |   5 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  38 ++
 .../apache/sis/geometry/wrapper/jts/JTSTest.java   |   8 +-
 .../org/apache/sis/image/BandedIteratorTest.java   |   1 +
 .../org/apache/sis/image/ComputedImageTest.java    |   1 +
 .../test/org/apache/sis/image/DataTypeTest.java    |   1 +
 .../test/org/apache/sis/image/ImageLayoutTest.java |   1 +
 .../org/apache/sis/image/ImageOverlayTest.java     |   1 +
 .../test/org/apache/sis/image/ImageTestCase.java   |   1 +
 .../org/apache/sis/image/InterpolationTest.java    |   1 +
 .../org/apache/sis/image/LinearIteratorTest.java   |   1 +
 .../test/org/apache/sis/image/MaskedImageTest.java |   1 +
 .../org/apache/sis/image/OverviewImageTest.java    | 135 ++++++
 .../test/org/apache/sis/image/TiledImageMock.java  |  17 +
 .../sis/map/coverage/RenderingWorkaround.java      |   2 +
 .../main/org/apache/sis/io/wkt/Formatter.java      |   2 +-
 .../main/module-info.java                          |   6 +-
 .../apache/sis/storage/geotiff/Compression.java    |  27 +-
 .../org/apache/sis/storage/geotiff/DataCube.java   |   8 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |   4 +-
 .../apache/sis/storage/geotiff/FormatModifier.java |  19 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  45 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  16 +-
 .../apache/sis/storage/geotiff/NativeMetadata.java |   6 +-
 .../apache/sis/storage/geotiff/WritableStore.java  |  36 +-
 .../org/apache/sis/storage/geotiff/Writer.java     |  49 ++-
 .../{Compression.java => CompressionMethod.java}   |  57 +--
 .../apache/sis/storage/geotiff/base/Predictor.java |  37 +-
 .../org/apache/sis/storage/geotiff/base/Tags.java  |   2 +-
 .../sis/storage/geotiff/inflater/CCITTRLE.java     |  14 +-
 .../storage/geotiff/inflater/CopyFromBytes.java    |   4 +-
 .../geotiff/inflater/HorizontalPredictor.java      |  19 +-
 .../sis/storage/geotiff/inflater/Inflater.java     |  45 +-
 .../apache/sis/storage/geotiff/inflater/LZW.java   |  16 +-
 .../sis/storage/geotiff/inflater/PackBits.java     |   3 +-
 .../sis/storage/geotiff/inflater/PixelChannel.java |  52 ---
 .../sis/storage/geotiff/inflater/package-info.java |   6 +-
 .../apache/sis/storage/geotiff/package-info.java   |   3 +
 .../sis/storage/geotiff/writer/TileMatrix.java     |  18 +-
 .../sis/storage/geotiff/GeoTiffStoreTest.java      |  78 ++++
 .../sis/storage/geotiff/base/CompressionTest.java  |  20 +-
 .../sis/storage/geotiff/inflater/CCITTRLETest.java |   3 +-
 .../org/apache/sis/storage/netcdf/base/Raster.java |   4 +-
 .../sis/storage/netcdf/classic/VariableInfo.java   |   6 +-
 .../org.apache.sis.storage/main/module-info.java   |   4 +
 .../org/apache/sis/io/stream/ChannelDataInput.java |  15 +-
 .../apache/sis/io/stream/HyperRectangleReader.java |   1 -
 .../apache/sis/io/stream/HyperRectangleWriter.java |   2 +-
 .../main/org/apache/sis/io/stream/Region.java      |  14 +-
 .../io/stream/inflater/CompressionException.java}  |  34 +-
 .../io/stream/inflater/ComputedByteChannel.java    | 115 +++++
 .../apache/sis/io/stream/inflater/Deflate.java}    |  35 +-
 .../sis/io/stream/inflater/InflaterChannel.java}   |  77 +---
 .../sis/io/stream}/inflater/PredictorChannel.java  |  35 +-
 .../sis/io/stream/inflater/package-info.java}      |  26 +-
 .../sis/storage/AbstractGridCoverageResource.java  |   9 +
 .../apache/sis/storage/GridCoverageResource.java   |   1 +
 .../sis/storage/aggregate/CoverageAggregator.java  |   1 -
 .../{base => aggregate}/PseudoResource.java        |   4 +-
 .../apache/sis/storage/base/OverviewIterator.java} |  35 +-
 .../storage/base/WritableTiledResourceSupport.java |  86 ++++
 .../apache/sis/storage/tiling/ImagePyramid.java    |  50 ++-
 .../apache/sis/storage/tiling/ImageTileMatrix.java |  10 +
 .../apache/sis/storage/tiling/TileMatrixSet.java   |   2 +-
 .../sis/storage/tiling/TileMatrixSetFormat.java    | 101 +++--
 .../apache/sis/storage/tiling/TileReadEvent.java   |   7 +-
 .../sis/storage/tiling/TiledGridCoverage.java      |  17 +-
 .../storage/tiling/TiledGridCoverageResource.java  |  30 +-
 .../io/stream/SubsampledRectangleWriterTest.java   |   4 +-
 .../apache/sis/util/internal/shared/Numerics.java  | 115 +----
 .../sis/storage/geoheif/CoverageBuilder.java       | 481 +++++++++------------
 .../apache/sis/storage/geoheif/FromImageIO.java    |  66 ++-
 .../apache/sis/storage/geoheif/GeoHeifStore.java   |  26 +-
 .../main/org/apache/sis/storage/geoheif/Image.java |  24 +-
 .../org/apache/sis/storage/geoheif/ImageModel.java | 322 ++++++++++++++
 .../apache/sis/storage/geoheif/ImageResource.java  | 123 ++++--
 .../org/apache/sis/storage/geoheif/Pyramid.java    | 211 ++++++++-
 .../sis/storage/geoheif/ResourceBuilder.java       | 245 +++++++----
 .../org/apache/sis/storage/geoheif/TiledImage.java | 119 +++++
 .../sis/storage/geoheif/UncompressedImage.java     | 167 +++++--
 .../sis/storage/isobmff/MainBoxRegistry.java       |  10 +
 .../org/apache/sis/storage/isobmff/Reader.java     |   4 +-
 .../org/apache/sis/storage/isobmff/TreeNode.java   |   4 +-
 .../apache/sis/storage/isobmff/base/ItemData.java  |  10 +-
 .../sis/storage/isobmff/base/ItemInfoEntry.java    |   3 +-
 .../isobmff/image/AuxiliaryImageReference.java     |  54 +++
 .../storage/isobmff/image/BaseImageReference.java  |  53 +++
 .../isobmff/image/PremultipliedImageReference.java |  53 +++
 .../storage/isobmff/image/ThumbnailReference.java  |  53 +++
 .../{geo => image}/TiledImageConfiguration.java    |   3 +-
 .../sis/storage/isobmff/mpeg/CleanAperture.java    |  57 +++
 .../apache/sis/storage/isobmff/mpeg/Component.java |  19 +-
 .../isobmff/mpeg/CompressedUnitsItemInfo.java      | 122 ++++++
 .../sis/storage/isobmff/mpeg/CompressionAV1.java   |  57 +++
 .../isobmff/mpeg/CompressionConfiguration.java     |  90 ++++
 .../sis/storage/isobmff/mpeg/CompressionJP2K.java  |  57 +++
 .../isobmff/mpeg/HevcConfigurationItem.java        |  57 +++
 .../apache/sis/storage/isobmff/mpeg/UnitType.java  |  71 +++
 .../main/org/apache/sis/gui/DataViewer.java        |  29 +-
 .../main/org/apache/sis/gui/Option.java            |  96 ++++
 .../apache/sis/gui/coverage/TileMatrixSetPane.java |  33 +-
 117 files changed, 3978 insertions(+), 1126 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
index f8c358d1c4,bc099923d4..bff8d5336e
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
@@@ -878,7 -920,9 +879,8 @@@ public class GridExtent implements Seri
       * @see #getLow(int)
       * @see #getHigh(int)
       * @see #resize(long...)
+      * @see #SIZES_COMPARATOR
       */
 -    @Override
      public long getSize(final int index) {
          final int dimension = getDimension();
          Objects.checkIndex(index, dimension);
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java
index 0000000000,1c381c7e96..02cb6c9403
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java
@@@ -1,0 -1,313 +1,310 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.sis.image;
+ 
+ import java.awt.Rectangle;
+ import java.awt.image.ColorModel;
+ import java.awt.image.Raster;
+ import java.awt.image.WritableRaster;
+ import java.awt.image.RenderedImage;
+ import java.awt.image.ImagingOpException;
+ import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.util.Disposable;
+ import org.apache.sis.util.internal.shared.Numerics;
+ import org.apache.sis.image.internal.shared.ImageUtilities;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.grid.SequenceType;
 -
+ 
+ /**
+  * An image which is the result of averaging 4 pixels of the image at higher 
resolution.
+  * It can be seen as a special case of {@link ResampledImage} with bilinear 
interpolation
+  * at the exact center of a block of 4 pixels.
+  *
+  * @todo Add an auxiliary image with contains the rest of the division by 4 
(when sample values are integers)
+  *       or the number of averaged sample values (when sample values are 
floating points).
+  *       Use that information when computing overview of overviews, for 
better accuracy.
+  *
+  * @author  Estelle Idée (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ final class OverviewImage extends ComputedImage {
+     /**
+      * The image origin.
+      */
+     private final int minX, minY;
+ 
+     /**
+      * The image size in pixels.
+      */
+     private final int width, height;
+ 
+     /**
+      * The offset to add after conversion from target to source pixel 
coordinates.
+      * This is either 0 or 1.
+      */
+     private final byte offsetX, offsetY;
+ 
+     /**
+      * Creates a new image which will create an overview of the given image.
+      *
+      * @param  source  the image at higher resolution.
+      */
+     OverviewImage(final RenderedImage source) {
+         this(targetBounds(ImageUtilities.getBounds(source)), source);
+     }
+ 
+     /** Workaround for RFE #4093999 ("Relax constraint on placement of 
this()/super() call in constructors"). */
+     private static Rectangle targetBounds(final Rectangle bounds) {
+         bounds.x     >>= 1;     // Round toward negative infinity.
+         bounds.y     >>= 1;
+         bounds.width  /= 2;     // Round toward 0.
+         bounds.height /= 2;
+         return bounds;
+     }
+ 
+     /** Workaround for RFE #4093999 ("Relax constraint on placement of 
this()/super() call in constructors"). */
+     private OverviewImage(final Rectangle bounds, final RenderedImage source) 
{
+         super(ImageLayout.DEFAULT.createCompatibleSampleModel(source, 
bounds), source);
+         offsetX = (byte) (source.getMinX() & 1);    // TODO: move before 
`targetBounds(…)` after RFE #4093999.
+         offsetY = (byte) (source.getMinY() & 1);
+         minX    = bounds.x;
+         minY    = bounds.y;
+         width   = bounds.width;
+         height  = bounds.height;
+     }
+ 
+     /**
+      * Returns the color model of this resampled image.
+      * Default implementation assumes that this image has the same color 
model as the source image.
+      *
+      * @return the color model, or {@code null} if unspecified.
+      */
+     @Override
+     public ColorModel getColorModel() {
+         return getSource().getColorModel();
+     }
+ 
+     /**
+      * Returns the minimum tile index in the <var>x</var> direction.
+      */
+     @Override
+     public final int getMinTileX() {
+         return getSource().getMinTileX() / 2;   // Round toward zero.
+     }
+ 
+     /**
+      * Returns the minimum tile index in the <var>y</var> direction.
+      */
+     @Override
+     public final int getMinTileY() {
+         return getSource().getMinTileY() / 2;
+     }
+ 
+     /**
+      * Returns the minimum <var>x</var> coordinate (inclusive) of this image.
+      */
+     @Override
+     public final int getMinX() {
+         return minX;
+     }
+ 
+     /**
+      * Returns the minimum <var>y</var> coordinate (inclusive) of this image.
+      */
+     @Override
+     public final int getMinY() {
+         return minY;
+     }
+ 
+     /**
+      * Returns the number of columns in this image.
+      */
+     @Override
+     public final int getWidth() {
+         return width;
+     }
+ 
+     /**
+      * Returns the number of rows in this image.
+      */
+     @Override
+     public final int getHeight() {
+         return height;
+     }
+ 
+     /**
+      * Invoked when a tile needs to be computed or updated.
+      *
+      * @param  tileX  the column index of the tile to compute.
+      * @param  tileY  the row index of the tile to compute.
+      * @param  tile   if the tile already exists but needs to be updated, the 
tile to update. Otherwise {@code null}.
+      * @return computed tile for the given indices.
+      */
+     @Override
+     protected Raster computeTile(final int tileX, final int tileY, 
WritableRaster tile) {
+         if (tile == null) {
+             tile = createTile(tileX, tileY);
+         }
+         Rectangle bounds = tile.getBounds();
+         bounds.width  <<= 1;
+         bounds.height <<= 1;
+         bounds.x      <<= 1;
+         bounds.y      <<= 1;
+         bounds.x += offsetX;
+         bounds.y += offsetY;
+         final PixelIterator it = new PixelIterator.Builder()
+                 .setIteratorOrder(SequenceType.LINEAR)
+                 .setRegionOfInterest(bounds)
+                 .create(getSource());
+         /*
+          * The iterator may have intersected the given bounds with the source 
image bounds.
+          * Therefore, we derive the limits from these bounds instead of from 
tile bounds.
+          * It should cover the whole valid area of the tile.
+          */
+         bounds = it.getDomain();
+         if (((bounds.width | bounds.height) & 1) != 0) {
+             throw new 
ImagingOpException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, 
tileY));
+         }
+         bounds.x      >>= 1;    // Round toward negative infinity.
+         bounds.y      >>= 1;
+         bounds.width  >>= 1;
+         bounds.height >>= 1;
+         final int numBands = tile.getNumBands();
+         final var buffer   = new double[Math.multiplyExact(bounds.width, 
numBands)];
+         final var counts   = new byte[buffer.length];
+         double[]  left     = null;
+         double[]  right    = null;
+         final int ymax = bounds.y + bounds.height;
+         for (int y = bounds.y; y < ymax; y++) {
+             int x = bounds.x;
+             /*
+              * Memorize the sum of two consecutive pixels for all pixels in 
the current source row.
+              * The `counts` array contains the number of valid values, which 
will by 2, 3 or 4 on
+              * the assumption that the next row will not contain NaN value 
(verified in next loop).
+              */
+             for (int i=0; i < buffer.length;) {
+                 if (it.next()) {
+                     left = it.getPixel(left);
+                     if (it.next()) {
+                         right = it.getPixel(right);
+                         for (int b=0; b<numBands; b++) {
+                             byte count = 4;
+                             double sum = left[b] + right[b];
+                             if (Double.isNaN(sum)) {
+                                 // Give precedence to the left side if both 
sides are NaN.
+                                 count = (Double.isNaN(sum = right[b]) &&
+                                          Double.isNaN(sum =  left[b])) ? 
(byte) 2 : (byte) 3;
+                             }
+                             buffer[i] = sum;
+                             counts[i++] = count;
+                         }
+                         continue;
+                     }
+                 }
+                 throw new 
ImagingOpException(Resources.format(Resources.Keys.OutOfIteratorDomain_2, 
i/numBands + x, y));
+             }
+             /*
+              * Read the next row and compute the average with the previous 
row which was memorized by above loop.
+              * If some values are NaN, the number of valid values gien by 
`counts` is adjusted.
+              */
+             for (int i=0; i < buffer.length; x++) {
+                 if (it.next()) {
+                     left = it.getPixel(left);
+                     if (it.next()) {
+                         right = it.getPixel(right);
+                         for (int b=0; b<numBands; b++, i++) {
+                             int  count = counts[i];
+                             double add = left[b] + right[b];
+                             double sum = add + buffer[i];
+                             // Test `isNaN(sum)` first because it will be 
false in the vast majority of cases.
+                             if (Double.isNaN(sum) && Double.isNaN(sum = add)) 
{
+                                 sum = buffer[i];
+                                 if (Double.isNaN(add = right[b]) && 
Double.isNaN(add = left[b])) {
+                                     count -= 2;     // The two values of the 
current row are invalid.
+                                 } else {
+                                     count--;        // Exactly one value of 
the current row is valid.
+                                     sum = Double.isNaN(sum) ? add : sum + add;
+                                 }
+                                 if (count <= 1) {
+                                     // Avoid a division by 0 in order to 
preserve the NaN bits pattern.
+                                     left[b] = sum;
+                                     continue;
+                                 }
+                             }
+                             left[b] = sum / count;
+                         }
+                         tile.setPixel(x, y, left);
+                         continue;
+                     }
+                 }
+                 throw new 
ImagingOpException(Resources.format(Resources.Keys.OutOfIteratorDomain_2, x, 
y));
+             }
+         }
+         return tile;
+     }
+ 
+     /**
+      * Notifies the source image that tiles will be computed soon in the 
given region.
+      * If the source image is an instance of {@link ComputedImage}, then this 
method
+      * forwards the notification to it.
+      */
+     @Override
+     protected Disposable prefetch(final Rectangle tiles) {
+         final RenderedImage source = getSource();
+         if (source instanceof PlanarImage) {
+             final long xmin = 2L * tiles.x + offsetX;
+             final long ymin = 2L * tiles.y + offsetY;
+             final long xmax = 2L * tiles.width  + xmin;
+             final long ymax = 2L * tiles.height + ymin;
+             final int x = Numerics.clamp(xmin);
+             final int y = Numerics.clamp(ymin);
+             return ((PlanarImage) source).prefetch(
+                     new Rectangle(x, y, Numerics.clamp(xmax - x),
+                                         Numerics.clamp(ymax - y)));
+         }
+         return super.prefetch(tiles);
+     }
+ 
+     /**
+      * Compares the given object with this image for equality.
+      *
+      * @param  object  the object to compare with this image.
+      * @return {@code true} if the given object is an image performing the 
same overview as this image.
+      */
+     @Override
+     public boolean equals(final Object object) {
+         if (equalsBase(object)) {
+             final var other = (OverviewImage) object;
+             return minX    == other.minX    &&
+                    minY    == other.minY    &&
+                    width   == other.width   &&
+                    height  == other.height  &&
+                    offsetX == other.offsetX &&
+                    offsetY == other.offsetY;
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns a hash code value for this image. The {@link #minX}, {@link 
#minY}, {@link #width} and {@link #height}
+      * fields are included in the hash computation as a matter of principle, 
but this is actually not very important
+      * because they are derived information.
+      */
+     @Override
+     public int hashCode() {
+         return hashCodeBase() + (minX + 31*(minY + 31*(width + 31*height)));
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
index d1420045a3,4c38c1cf32..19300ba65c
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
@@@ -170,8 -170,8 +170,8 @@@ public final class JTSTest extends Test
              wrapper.setCoordinateReferenceSystem(crs);
              final GeneralEnvelope envelope = wrapper.getEnvelope();
              assertEquals(crs, envelope.getCoordinateReferenceSystem());
-             assertArrayEquals(new double[] {5, 6, Double.NaN}, 
envelope.getLowerCorner().getCoordinate());
-             assertArrayEquals(new double[] {5, 6, Double.NaN}, 
envelope.getUpperCorner().getCoordinate());
 -            assertArrayEquals(new double[] {5, 6, 7}, 
envelope.getLowerCorner().getCoordinates());
 -            assertArrayEquals(new double[] {5, 6, 7}, 
envelope.getUpperCorner().getCoordinates());
++            assertArrayEquals(new double[] {5, 6, 7}, 
envelope.getLowerCorner().getCoordinate());
++            assertArrayEquals(new double[] {5, 6, 7}, 
envelope.getUpperCorner().getCoordinate());
          }
      }
  
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java
index 0000000000,e67639cbd0..b5c0fc961b
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java
@@@ -1,0 -1,138 +1,135 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.sis.image;
+ 
+ import java.awt.Point;
+ import java.awt.image.RenderedImage;
+ import java.util.Random;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.grid.SequenceType;
 -
+ // Test dependencies
+ import org.junit.jupiter.api.Test;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ import org.apache.sis.test.TestUtilities;
+ 
+ 
+ /**
+  * Tests {@link OverviewImage}.
+  *
+  * @author  Estelle Idée (Geomatys)
+  */
+ @SuppressWarnings("exports")
+ public final class OverviewImageTest extends TestCase {
+     /**
+      * Creates a new test case.
+      */
+     public OverviewImageTest() {
+     }
+ 
+     /**
+      * Tests on an image filled with integer values.
+      */
+     @Test
+     public void testOnIntegers() {
+         testForType(DataType.INT);
+     }
+ 
+     /**
+      * Tests on an image filled with floating point values.
+      * Some random values are set to NaN.
+      */
+     @Test
+     public void testOnFloats() {
+         testForType(DataType.DOUBLE);
+     }
+ 
+     /**
+      * Runs the test on an image of the specified type.
+      *
+      * @param  type  type of data stored in the image.
+      */
+     private static void testForType(final DataType type) {
+         final Random r = TestUtilities.createRandomNumberGenerator();
+         final var source = new TiledImageMock(
+                 type.toDataBufferType(),
+                 r.nextInt( 2) +  1,     // num bands
+                 r.nextInt( 9) -  4,     // min X
+                 r.nextInt( 9) -  4,     // min Y
+                 r.nextInt(20) + 10,     // width
+                 r.nextInt(20) + 10,     // height
+                 r.nextInt( 5) +  5,     // tile width
+                 r.nextInt( 5) +  5,     // tile height
+                 r.nextInt( 9) -  4,     // min tile X
+                 r.nextInt( 9) -  4,     // min tile Y
+                 true);                  // banded
+ 
+         source.initializeAllTiles();
+         if (!type.isInteger()) {
+             source.setRandomNaN(r);
+         }
+         verify(source, new OverviewImage(source), type.isInteger());
+     }
+ 
+     /**
+      * Verifies an image which is expected to be the result of an image 
overview operation.
+      *
+      * @param  source     the image used for computing the overview.
+      * @param  target     the result of the image overview operation.
+      * @param  isInteger  whether the images use an integer type.
+      */
+     public static void verify(final RenderedImage source, final RenderedImage 
target, final boolean isInteger) {
+         assertEquals(source.getWidth()  / 2, target.getWidth());
+         assertEquals(source.getHeight() / 2, target.getHeight());
+         final int offsetX = source.getMinX() & 1;
+         final int offsetY = source.getMinY() & 1;
+ 
+         double[] p00 = null, p01 = null, p10 = null, p11 = null;
+         double[] actual = null;
+ 
+         final PixelIterator itSource = new 
PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(source);
+         final PixelIterator itTarget = PixelIterator.create(target);
+         int count = 0;
+         while (itTarget.next()) {
+             final Point p = itTarget.getPosition();
+             final int sx = p.x * 2 + offsetX;
+             final int sy = p.y * 2 + offsetY;
+ 
+             // Read 2×2 block from source.
+             itSource.moveTo(sx, sy);      p00 = itSource.getPixel(p00);
+             assertTrue(itSource.next());  p01 = itSource.getPixel(p01);
+             itSource.moveTo(sx, sy + 1);  p10 = itSource.getPixel(p10);
+             assertTrue(itSource.next());  p11 = itSource.getPixel(p11);
+ 
+             actual = itTarget.getPixel(actual);
+             for (int b = 0; b < actual.length; b++) {
+                 int n = 0;
+                 double sum = 0, v;
+                 if (!Double.isNaN(v = p00[b])) {sum += v; n++;}
+                 if (!Double.isNaN(v = p01[b])) {sum += v; n++;}
+                 if (!Double.isNaN(v = p10[b])) {sum += v; n++;}
+                 if (!Double.isNaN(v = p11[b])) {sum += v; n++;}
+                 double expected = (n != 0) ? sum / n : Double.NaN;
+                 if (isInteger) {
+                     expected = (int) expected;
+                 }
+                 assertEquals(expected, actual[b], 1E-10, () -> "Mismatch at 
(" + p.x + ", " + p.y + ')');
+             }
+             count++;
+         }
+         assertEquals(target.getWidth() * target.getHeight(), count);
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
index 8c8716b0f1,bc28951b95..9f0dd77ca0
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
@@@ -132,6 -133,8 +133,8 @@@ public final class TileReadEvent extend
                  tr = MathTransforms.concatenate(tr, 
crsToObjective.getMathTransform());
                  imageToObjective = MathTransforms.bidimensional(tr);
                  this.crsToObjective = crsToObjective;   // Store only after 
the rest was successful.
+             } catch (FactoryException e) {
 -                throw new TransformException(e);
++                throw new TransformException(e.toString(), e);
              }
              return imageToObjective;
          }

Reply via email to