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

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

commit 72024eaabc38223da6e608ff91b7a4639899c6dc
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jun 17 12:58:39 2026 +0200

    Make HEIF pyramid more tolerant to the case when the "grid to CRS" 
transform is not specified.
---
 .../org/apache/sis/coverage/grid/GridGeometry.java | 13 ++++--
 .../apache/sis/coverage/grid/GridGeometryTest.java | 38 ++++++++++++++++
 .../apache/sis/storage/tiling/ImagePyramid.java    | 50 ++++++++++++++++------
 .../apache/sis/util/internal/shared/Numerics.java  |  3 +-
 .../apache/sis/storage/geoheif/ImageResource.java  | 22 +++++++---
 .../org/apache/sis/storage/geoheif/Pyramid.java    |  8 ++--
 .../org/apache/sis/storage/isobmff/TreeNode.java   |  4 +-
 7 files changed, 107 insertions(+), 31 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index 93f547f599..77e8cffc39 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -269,7 +269,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
     protected final MathTransform cornerToCRS;
 
     /**
-     * An <em>estimation</em> of the grid resolution, in units of the CRS axes.
+     * An <em>estimation</em> of the grid resolution, in units of the 
<abbr>CRS</abbr> axes.
      * Computed from {@link #gridToCRS}, eventually together with {@link 
#extent}.
      * May be {@code null} if unknown. If non-null, the array length is equal 
to
      * the number of <abbr>CRS</abbr> dimensions.
@@ -280,7 +280,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
     protected final double[] resolution;
 
     /**
-     * Whether the conversions from grid coordinates to the <abbr>CRS</abbr> 
are linear, for each target axis.
+     * Whether the conversions from grid coordinates to the <abbr>CRS</abbr> 
are non-linear, for each target axis.
      * The bit located at {@code 1L << dimension} is set to 1 when the 
conversion at that dimension is non-linear.
      * The dimension indices are those of the CRS, not the grid. The use of 
{@code long} type limits the capacity
      * to 64 dimensions. But actually {@code GridGeometry} can contain more 
dimensions provided that index of the
@@ -385,7 +385,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
         final int dimension = other.getDimension();
         this.extent = extent;
         ensureDimensionMatches(dimension, extent);
-        if (toOther == null || toOther.isIdentity()) {
+        if (toOther == null || (toOther.isIdentity() && other.gridToCRS != 
null)) {
             gridToCRS   = other.gridToCRS;
             cornerToCRS = other.cornerToCRS;
             resolution  = other.resolution;
@@ -409,7 +409,12 @@ public class GridGeometry implements LenientComparable, 
Serializable {
                 cornerToCRS = null;
                 gridToCRS   = null;
                 resolution  = resolution(toOther, extent, 
PixelInCell.CELL_CENTER);     // Save resolution even if `gridToCRS` is null.
-                nonLinears  = findNonLinearTargets(toOther);
+                nonLinears  = findNonLinearTargets(toOther) | other.nonLinears;
+                if (other.resolution != null) {
+                    for (int i = Math.min(other.resolution.length, 
resolution.length); --i >= 0;) {
+                        resolution[i] *= other.resolution[i];
+                    }
+                }
             }
         }
         /*
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
index 4c9c242138..e981013b6a 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -282,6 +282,44 @@ public final class GridGeometryTest extends TestCase {
         verifyGridToCRS(grid);
     }
 
+    /**
+     * Tests {@link GridGeometry#GridGeometry(GridGeometry, GridExtent, 
MathTransform)} constructor
+     * with missing "grid to <abbr>CRS</abbr>" transform. We expect the 
resolution to still be defined.
+     *
+     * @throws TransformException if an error occurred while using the "grid 
to CRS" transform.
+     */
+    @Test
+    public void testFromOtherWithMissingTransform() throws TransformException {
+        GridExtent extent = new GridExtent(1260, 1970);
+        GridGeometry grid = new GridGeometry(extent, PixelInCell.CELL_CENTER, 
null, null);
+        assertNull(grid.resolution);
+        assertNull(grid.gridToCRS);
+        assertNull(grid.envelope);
+        /*
+         * Deriving a grid geometry with an identity transform should result 
in resolutions set to 1.
+         * This is needed for ensuring the validity of the grid geometry of 
the base level of a pyramid.
+         */
+        GridGeometry withResolution = new GridGeometry(grid, extent, 
MathTransforms.identity(2));
+        assertArrayEquals(new double[] {1, 1}, 
withResolution.getResolution(false));
+        assertNull(withResolution.gridToCRS);
+        assertNull(withResolution.envelope);
+        /*
+         * Derive a new grid geometry for a pyramid level of resolution 10×10 
larger than the base pyramid level.
+         * The resolution should be defined even if the "grid to CRS" and 
envelope are still missing.
+         */
+        extent = extent.resize(126, 197);
+        grid = new GridGeometry(grid, extent, MathTransforms.scale(10, 10));
+        assertArrayEquals(new double[] {10, 10}, grid.getResolution(false));
+        assertNull(grid.gridToCRS);
+        assertNull(grid.envelope);
+
+        // Same test, but starting from a resolution of 1. Should be 
equivalent.
+        grid = new GridGeometry(withResolution, extent, 
MathTransforms.scale(10, 10));
+        assertArrayEquals(new double[] {10, 10}, grid.getResolution(false));
+        assertNull(grid.gridToCRS);
+        assertNull(grid.envelope);
+    }
+
     /**
      * Tests construction from a <i>grid to CRS</i> having a 0.5 pixel 
translation.
      * This translation happens in transform mapping <i>pixel center</i> when 
the
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
index add5b5b2d8..4d416203a0 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
@@ -29,6 +29,7 @@ import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverageProcessor;
 import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
 import org.apache.sis.geometry.GeneralEnvelope;
@@ -72,6 +73,7 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
 
     /**
      * First valid index value (inclusive) in {@link #matrices}.
+     * This is the matrix of tiles at the coarsest resolution.
      */
     private final int lowerMatrixIndex;
 
@@ -89,6 +91,11 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
      */
     private Envelope envelope;
 
+    /**
+     * Whether {@link #envelope} has been computed. The result may still be 
null.
+     */
+    private boolean isEnvelopeComputed;
+
     /**
      * The grid coverage processor to use when tiles use a subset of the bands.
      *
@@ -298,28 +305,45 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
 
     /**
      * Returns an envelope that encompasses all {@code TileMatrix} instances 
in this set.
+     * The envelope may be missing if the "grid to <abbr>CRS</abbr>" transform 
is missing.
      *
-     * @throws IncompleteGridGeometryException if a tiling scheme has no 
envelope. While not strictly mandatory,
-     *         for now we consider that missing extent or missing "grid to 
CRS" transform is probably an error.
+     * @throws IncompleteGridGeometryException if the envelope is defined at 
the coarsest level
+     *         but missing in some other levels (should never happen).
      */
     @Override
     public Optional<Envelope> getEnvelope() {
         synchronized (matrices) {
-            if (envelope == null) {
-                int i = lowerMatrixIndex;
-                Envelope mayReuse = 
getOrCreateLevel(i).getTilingScheme().getEnvelope();
-                final var union = new GeneralEnvelope(mayReuse);
+            if (!isEnvelopeComputed) {
+                GeneralEnvelope union = null;
+                Envelope mayReuse = null;
+                int level = lowerMatrixIndex;
                 ImageTileMatrix tm;
-                while ((tm = getOrCreateLevel(++i)) != null) {
-                    final Envelope e = tm.getTilingScheme().getEnvelope();
-                    union.add(e);
-                    if (union.equals(e, 0, false)) {
-                        mayReuse = e;
+                while ((tm = getOrCreateLevel(level)) != null) {
+                    final GridGeometry scheme = tm.getTilingScheme();
+                    if (union != null) {
+                        /*
+                         * Intentional `IncompleteGridGeometryException` if 
the envelope is missing.
+                         * If we found at least one envelope in previous 
levels, there is no reason
+                         * why the envelope wouldn't be defined for all 
levels. If one is missing,
+                         * this is probably a bug.
+                         */
+                        final Envelope e = scheme.getEnvelope();
+                        union.add(e);
+                        if (union.equals(e, 0, false)) {
+                            mayReuse = e;
+                        }
+                    } else if (scheme.isDefined(GridGeometry.ENVELOPE)) {
+                        mayReuse = scheme.getEnvelope();
+                        union = new GeneralEnvelope(mayReuse);
                     }
+                    level++;
+                }
+                if (union != null) {
+                    envelope = union.equals(mayReuse, 0, false) ? mayReuse : 
new ImmutableEnvelope(union);
                 }
-                envelope = (union == null || union.equals(mayReuse, 0, false)) 
? mayReuse : new ImmutableEnvelope(union);
+                isEnvelopeComputed = true;
             }
-            return Optional.of(envelope);
+            return Optional.ofNullable(envelope);
         }
     }
 
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
index 2c6a4cab9a..6440d7ac52 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
@@ -319,7 +319,8 @@ public final class Numerics {
      * Returns the result of {@code numerator / denominator} but potentially 
more accurate.
      * If any argument cannot be converted to {@code double} without accuracy 
lost but the
      * integer part of the result of the division can be represented 
accurately, then this
-     * method provides a better result than {@code numerator / (double) 
denominator}.
+     * method may provide a better result than {@code numerator / (double) 
denominator}.
+     * This method is useful only when {@code numerator} ≥ {@code denominator}.
      *
      * @param  numerator    numerator of the division.
      * @param  denominator  denominator of the division.
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
index 1d54bd888f..1aa77f32c1 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
@@ -182,19 +182,29 @@ final class ImageResource extends 
TiledGridCoverageResource implements StoreReso
      * Declares that this image is the pyramid level of the given base grid.
      * This method does nothing if this image already has its own "grid to 
<abbr>CRS</abbr>" transform.
      *
-     * @param  base  grid geometry of the pyramid level at the finest 
resolution.
+     * <p>If {@code base} is null, then the base is assumed to be the grid 
geometry of this level.
+     * Configuring a level relative to itself is useful when the grid geometry 
is incomplete,
+     * in which case the resolution of the base level is set to 1.
+     * This is needed because resolutions are mandatory in a grid.</p>
+     *
+     * @param  base  grid geometry of the pyramid level at the finest 
resolution, or {@code null} if this level.
+     * @return grid geometry of the pyramid level at the finest resolution 
({@code base} if it was non-null).
      * @throws TransformException if an error occurred while deriving the 
"grid to <abbr>CRS</abbr>" transform.
      */
-    final void setPyramidLevelOf(final GridGeometry base) throws 
TransformException {
+    final GridGeometry setPyramidLevelOf(GridGeometry base) throws 
TransformException {
+        final boolean self = (base == null);
+        if (self) base = gridGeometry;
         if (!gridGeometry.isDefined(GridGeometry.GRID_TO_CRS)) {
-            final GridExtent extent = gridGeometry.getExtent();
-            final GridExtent baseExtent = base.getExtent();
+            final GridExtent levelExtent = gridGeometry.getExtent();
+            final GridExtent baseExtent  = base.getExtent();
             final var factors = new double[baseExtent.getDimension()];
             for (int i = 0; i < factors.length; i++) {
-                factors[i] = Numerics.divide(baseExtent.getSize(i), 
extent.getSize(i));
+                factors[i] = 1 / Numerics.divide(levelExtent.getSize(i), 
baseExtent.getSize(i));
             }
-            gridGeometry = new GridGeometry(base, extent, 
MathTransforms.scale(factors));
+            gridGeometry = new GridGeometry(base, levelExtent, 
MathTransforms.scale(factors));
+            if (self) base = gridGeometry;
         }
+        return base;
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
index b93aafa9e7..9cf9d01abd 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Pyramid.java
@@ -75,11 +75,9 @@ final class Pyramid extends TiledGridCoverageResource 
implements TiledGridCovera
         tileSizeX = pyramid.tileSizeX;
         tileSizeY = pyramid.tileSizeY;
         this.levels = levels;
-        final GridGeometry base = levels[0].getGridGeometry();
-        if (base.isDefined(GridGeometry.GRID_TO_CRS)) {
-            for (int i = 1; i < levels.length; i++) {
-                levels[i].setPyramidLevelOf(base);
-            }
+        GridGeometry base = null;
+        for (ImageResource level : levels) {
+            base = level.setPyramidLevelOf(base);
         }
     }
 
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
index 186393e01b..690b5e74d1 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
@@ -561,13 +561,13 @@ public abstract class TreeNode {
         }
 
         /**
-         * Returns the name of the given field with camel-case converted to a 
sentence of words.
+         * Returns the name of the given field with camel-case converted to a 
sentence.
          *
          * @param  field  the field for which to get the name.
          * @return field name as a sequence of words.
          */
         private static String camelCaseToWords(final Field field) {
-            return CharSequences.camelCaseToWords(field.getName(), 
false).toString();
+            return CharSequences.camelCaseToWords(field.getName(), 
true).toString();
         }
     }
 

Reply via email to