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