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

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

commit 01e0369fc47e13f250409017b02e7c8faa3aeb6d
Merge: 8ef62b01f5 76ac26bca2
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Mar 31 17:17:02 2026 +0200

    Merge branch 'geoapi-3.1'.
    Contains (non-exhaustive list):
    
    - Implementation of `TileMatrixSet` by the GeoTIFF reader.
    - Redesign of the controls over coverage rendering in JavaFX.
    - Optimizations in filters and expressions.
    - Work in the geometry package (incubator).
    - Bug fixes.

 .../sis/coverage/grid/ClippedGridCoverage.java     |   3 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java |  12 +-
 .../apache/sis/coverage/grid/GridCoverage2D.java   |   3 +-
 .../sis/coverage/grid/GridCoverageBuilder.java     |  53 +-
 .../apache/sis/coverage/grid/GridDerivation.java   |   2 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |  61 ++-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  24 +-
 .../main/org/apache/sis/feature/FeatureFormat.java |   8 +-
 .../feature/internal/shared/MovingFeatures.java    |  28 +-
 .../org/apache/sis/filter/ComparisonFilter.java    | 141 ++++-
 .../main/org/apache/sis/filter/LogicalFilter.java  |  15 +-
 .../main/org/apache/sis/filter/Optimization.java   |   2 +-
 .../sis/filter/base/BinaryFunctionWidening.java    |   3 +-
 .../main/org/apache/sis/filter/math/Predicate.java |  27 +-
 .../main/org/apache/sis/image/BandedIterator.java  |  23 +
 .../main/org/apache/sis/image/ComputedImage.java   |  20 +-
 .../main/org/apache/sis/image/ComputedTiles.java   |  26 +-
 .../main/org/apache/sis/image/ImageProcessor.java  |   4 +-
 .../main/org/apache/sis/image/PlanarImage.java     |   2 +-
 .../apache/sis/image/WritablePixelIterator.java    |  50 +-
 .../sis/image/internal/shared/ReshapedImage.java   | 128 +++--
 .../main/org/apache/sis/image/package-info.java    |   2 +-
 .../sis/coverage/grid/ClippedGridCoverageTest.java |   1 +
 .../apache/sis/coverage/grid/GridExtentTest.java   |  10 +-
 .../apache/sis/filter/ComparisonFilterTest.java    |  98 ++--
 .../apache/sis/filter/DynamicOptimizationTest.java |  19 +
 .../image/internal/shared/ReshapedImageTest.java   |  15 +-
 .../iso/extent/NotSpatioTemporalException.java     |   4 +-
 .../metadata/sql/internal/shared/ScriptRunner.java |   2 +-
 .../org.apache.sis.portrayal/main/module-info.java |   2 +-
 .../coverage/MultiResolutionCoverageLoader.java    |  46 +-
 .../org/apache/sis/map/coverage/RenderingData.java |  53 +-
 .../org/apache/sis/map/internal/Resources.java     | 102 ++++
 .../apache/sis/map/internal/Resources.properties   |  23 +
 .../org/apache/sis/map/internal/Resources_en.java  |  17 +-
 .../org/apache/sis/map/internal/Resources_fr.java  |  17 +-
 .../sis/map/internal/Resources_fr.properties       |  28 +
 .../org/apache/sis/map/internal}/package-info.java |  11 +-
 .../main/org/apache/sis/portrayal/Observable.java  |   2 -
 .../org/apache/sis/portrayal/PlanarCanvas.java     |   7 +-
 .../MultiResolutionCoverageLoaderTest.java         |  18 +-
 .../main/org/apache/sis/geometry/Shapes2D.java     |   2 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |   8 +-
 .../referencing/internal/shared/WKTUtilities.java  |   4 +-
 .../operation/CoordinateOperationContext.java      |  31 +-
 .../operation/CoordinateOperationFinder.java       |  30 +-
 .../operation/CoordinateOperationRegistry.java     |  63 ++-
 .../DefaultCoordinateOperationFactory.java         |  58 +-
 .../referencing/operation/SubOperationInfo.java    |   8 +-
 .../sis/referencing/operation/matrix/Matrices.java |   4 +-
 .../sis/referencing/operation/package-info.java    |   2 +-
 .../operation/transform/PassThroughTransform.java  |   2 +-
 .../main/module-info.java                          |   2 +-
 .../sis/storage/geotiff/CompressedSubset.java      |  18 +-
 .../org/apache/sis/storage/geotiff/DataCube.java   |  65 +--
 .../org/apache/sis/storage/geotiff/DataSubset.java |  36 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   2 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    | 218 +++++---
 .../sis/storage/geotiff/MultiResolutionImage.java  | 286 ----------
 .../org/apache/sis/storage/geotiff/Reader.java     |  22 +-
 .../apache/sis/storage/geotiff/package-info.java   |   2 +-
 .../sis/storage/geotiff/GeoTiffStoreTest.java      |  56 +-
 .../org/apache/sis/storage/geotiff/ReaderTest.java | 124 +++++
 .../org/apache/sis/storage/geotiff/WriterTest.java |   5 +-
 .../org/apache/sis/storage/geotiff/untiled.tiff    | Bin 0 -> 2284 bytes
 .../apache/sis/storage/netcdf/base/FeatureSet.java |  62 ++-
 .../sis/storage/netcdf/base/RasterResource.java    |   2 +
 .../org/apache/sis/io/stream/ChannelDataInput.java |   2 +-
 .../sis/storage/AbstractGridCoverageResource.java  |  82 ++-
 .../org/apache/sis/storage/CoverageSubset.java     |  11 +-
 .../main/org/apache/sis/storage/DataStore.java     |  18 +-
 .../apache/sis/storage/GridCoverageResource.java   |  41 +-
 .../main/org/apache/sis/storage/Resource.java      |   4 +-
 .../org/apache/sis/storage/StorageConnector.java   |  25 +-
 .../sis/storage/UnsupportedQueryException.java     |   1 +
 .../aggregate/BandAggregateGridResource.java       |   4 +-
 .../aggregate/ConcatenatedGridResource.java        |  12 +-
 .../apache/sis/storage/aggregate/GridSlice.java    |  12 +-
 .../sis/storage/aggregate/GridSliceLocator.java    |   5 +-
 .../sis/storage/base/GridResourceWrapper.java      |   6 +-
 .../main/org/apache/sis/storage/csv/Store.java     |  13 +-
 .../org/apache/sis/storage/event/StoreEvent.java   |   2 +-
 .../apache/sis/storage/event/StoreListeners.java   |  14 +-
 .../org/apache/sis/storage/event/package-info.java |   4 +-
 .../apache/sis/storage/image/SingleImageStore.java |   6 +-
 .../storage/image/WritableSingleImageStore.java    |   4 +-
 .../org/apache/sis/storage/internal/Resources.java |   5 +
 .../sis/storage/internal/Resources.properties      |   1 +
 .../sis/storage/internal/Resources_fr.properties   |   1 +
 .../apache/sis/storage/tiling/ImagePyramid.java    | 472 ++++++++++++++++
 .../apache/sis/storage/tiling/ImageTileMatrix.java | 531 ++++++++++++++++++
 .../apache/sis/storage/tiling/IterationDomain.java | 221 ++++++++
 .../main/org/apache/sis/storage/tiling/Tile.java   |   4 +-
 .../org/apache/sis/storage/tiling/TileMatrix.java  |  11 +-
 .../apache/sis/storage/tiling/TileMatrixSet.java   |  12 +-
 .../sis/storage/tiling/TileMatrixSetFormat.java    | 524 ++++++++++++++++++
 .../apache/sis/storage/tiling/TileReadEvent.java   | 234 ++++++++
 .../org/apache/sis/storage/tiling/TileStatus.java  |   2 +-
 .../{base => tiling}/TiledDeferredImage.java       |  21 +-
 .../{base => tiling}/TiledGridCoverage.java        | 500 +++++++++++------
 .../TiledGridCoverageResource.java}                | 599 +++++++++++++++++++--
 .../apache/sis/storage/tiling/TiledResource.java   |   4 +-
 .../apache/sis/storage/tiling/package-info.java    |  10 +-
 .../org/apache/sis/storage/CoverageQueryTest.java  |   8 +-
 .../storage/MemoryGridCoverageResourceTest.java    |   6 +-
 .../sis/storage/test/CoverageReadConsistency.java  |   2 +-
 .../main/org/apache/sis/io/TableAppender.java      |  16 +-
 .../main/org/apache/sis/io/TabularFormat.java      |   4 +-
 .../main/org/apache/sis/io/package-info.java       |   2 +-
 .../main/org/apache/sis/math/DecimalFunctions.java |  29 +-
 .../main/org/apache/sis/math/NumberType.java       |   2 +-
 .../main/org/apache/sis/math/Vector.java           |   2 +-
 .../main/org/apache/sis/util/ArgumentChecks.java   |  26 +-
 .../main/org/apache/sis/util/collection/Cache.java |   4 +-
 .../org/apache/sis/util/collection/Containers.java |  30 +-
 .../apache/sis/util/collection/DerivedList.java    |  31 +-
 .../sis/util/collection/WeakValueHashMap.java      |   6 +-
 .../sis/util/collection/WritableDerivedList.java   | 143 +++++
 .../sis/util/internal/shared/AbstractMap.java      |   2 +-
 .../apache/sis/util/internal/shared/Numerics.java  | 118 +++-
 .../org/apache/sis/util/resources/Vocabulary.java  |  20 +
 .../sis/util/resources/Vocabulary.properties       |   4 +
 .../sis/util/resources/Vocabulary_fr.properties    |   4 +
 .../org/apache/sis/math/DecimalFunctionsTest.java  |  32 +-
 .../{DerivedSetTest.java => DerivedListTest.java}  |  66 +--
 .../apache/sis/util/collection/DerivedMapTest.java |   1 +
 .../apache/sis/util/collection/DerivedSetTest.java |   1 +
 .../apache/sis/storage/geoheif/FromImageIO.java    |   9 +-
 .../main/org/apache/sis/storage/geoheif/Image.java |   3 +-
 .../apache/sis/storage/geoheif/ImageResource.java  |  34 +-
 .../src/org.apache.sis.gui/main/module-info.java   |   2 +-
 .../main/org/apache/sis/gui/Widget.java            |   8 +-
 .../apache/sis/gui/controls/FormatApplicator.java  |   6 +-
 .../apache/sis/gui/controls/SyncWindowList.java    |   6 +-
 .../apache/sis/gui/controls/ValueColorMapper.java  |  12 +-
 .../apache/sis/gui/coverage/BandRangeTable.java    |   8 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    | 335 ++++++++++--
 .../apache/sis/gui/coverage/CoverageControls.java  | 155 +++---
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  36 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   |  16 +-
 .../apache/sis/gui/coverage/GridSliceSelector.java |  12 +-
 .../sis/gui/coverage/ImagePropertyExplorer.java    |   6 +-
 ...IsolineRenderer.java => IsolineController.java} |  82 ++-
 .../apache/sis/gui/coverage/StyleController.java   | 173 ++++++
 .../sis/gui/coverage/StyledRenderingData.java      |   6 +-
 .../apache/sis/gui/coverage/TileMatrixSetPane.java | 499 +++++++++++++++++
 .../apache/sis/gui/coverage/ViewAndControls.java   |  22 +-
 .../org/apache/sis/gui/coverage/package-info.java  |   2 +-
 .../main/org/apache/sis/gui/dataset/LogViewer.java |   5 +-
 .../org/apache/sis/gui/dataset/ResourceCell.java   | 138 +++--
 .../apache/sis/gui/dataset/ResourceExplorer.java   |  52 +-
 .../org/apache/sis/gui/dataset/ResourceItem.java   | 178 +++---
 .../org/apache/sis/gui/dataset/ResourceTree.java   |  50 +-
 .../org/apache/sis/gui/dataset/RootResource.java   |  37 +-
 .../org/apache/sis/gui/dataset/WindowHandler.java  |  27 +-
 .../org/apache/sis/gui/dataset/package-info.java   |   2 +-
 .../apache/sis/gui/internal/AlignedTableCell.java  |  87 +++
 .../apache/sis/gui/internal/BackgroundThreads.java |   8 +-
 .../apache/sis/gui/internal/DataStoreOpener.java   |  57 +-
 .../apache/sis/gui/internal/ExceptionReporter.java |  36 +-
 .../org/apache/sis/gui/internal/GUIUtilities.java  |  16 -
 .../org/apache/sis/gui/internal/LogHandler.java    |   2 +-
 .../org/apache/sis/gui/internal/Resources.java     |  20 +
 .../apache/sis/gui/internal/Resources.properties   |   4 +
 .../sis/gui/internal/Resources_fr.properties       |   4 +
 .../apache/sis/gui/internal/ShapeConverter.java    | 153 ++++++
 .../main/org/apache/sis/gui/internal/Styles.java   |   6 +
 .../apache/sis/gui/internal/io/FileAccessView.java |   4 +-
 .../org/apache/sis/gui/map/GestureFollower.java    |  18 +-
 .../main/org/apache/sis/gui/map/MapCanvas.java     |  43 +-
 .../main/org/apache/sis/gui/map/MapCanvasAWT.java  |  14 +-
 .../main/org/apache/sis/gui/map/MapMenu.java       |  39 +-
 .../main/org/apache/sis/gui/map/StatusBar.java     | 165 ++++--
 .../main/org/apache/sis/gui/map/package-info.java  |   2 +-
 .../apache/sis/gui/map/style/ItemController.java   |  89 +++
 .../apache/sis/gui/map/style/MapContextView.java   | 143 +++++
 .../main/org/apache/sis/gui/map/style/MapItem.java |  36 +-
 .../org/apache/sis/gui/map/style/MapLayer.java     |  38 +-
 .../{referencing => map/style}/package-info.java   |  10 +-
 .../sis/gui/metadata/IdentificationInfo.java       |   2 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |   8 +-
 .../org/apache/sis/gui/referencing/MenuSync.java   |  57 +-
 .../gui/referencing/RecentReferenceSystems.java    | 103 ++--
 .../apache/sis/gui/referencing/package-info.java   |   2 +-
 .../sis/gui/controls/ValueColorMapperApp.java      |   7 +-
 .../main/org/apache/sis/storage/gdal/GDAL.java     | 147 +----
 .../apache/sis/storage/gdal/GDALStoreProvider.java | 126 ++++-
 .../org/apache/sis/storage/gdal/TiledCoverage.java |  12 +-
 .../org/apache/sis/storage/gdal/TiledResource.java |  33 +-
 .../org/apache/sis/storage/gdal/package-info.java  |   2 +-
 .../apache/sis/storage/panama/LibraryLoader.java   | 192 +++----
 .../sis/storage/panama/LibraryReference.java       | 207 +++++++
 .../apache/sis/storage/panama/LibraryStatus.java   |  51 +-
 .../apache/sis/storage/panama/NativeFunctions.java |  91 ++--
 .../org/apache/sis/storage/gdal/GDALStoreTest.java |   3 +-
 195 files changed, 8013 insertions(+), 2146 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
index 5189105c56,35cb92fbff..83f759bb86
--- 
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
@@@ -94,10 -92,10 +94,10 @@@ import org.apache.sis.coverage.PointOut
   * @author  Martin Desruisseaux (IRD, Geomatys)
   * @author  Alexis Manin (Geomatys)
   * @author  Johann Sorel (Geomatys)
-  * @version 1.5
+  * @version 1.7
   * @since   1.0
   */
 -public class GridExtent implements GridEnvelope, LenientComparable, 
Serializable {
 +public class GridExtent implements Serializable, LenientComparable {
      /**
       * Serial number for inter-operability with different versions.
       */
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index 2ed70819fb,1586b58741..0f9c00b88e
--- 
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
@@@ -1880,6 -1881,26 +1880,26 @@@ public class GridGeometry implements Le
          }
      }
  
+     /**
+      * Returns a coordinate operation for transforming coordinates from the 
<abbr>CRS</abbr> of this grid geometry
+      * to the given <abbr>CRS</abbr>. The {@linkplain #getGeographicExtent() 
geographic bounding box} of this grid
+      * geometry is used as the desired domain of validity.
+      *
+      * @param  target  the target <abbr>CRS</abbr> of the desired operation.
+      * @return coordinate operation from the <abbr>CRS</abbr> of this grid 
geometry to the given <abbr>CRS</abbr>.
+      * @throws IncompleteGridGeometryException if this grid geometry has no 
<abbr>CRS</abbr>.
+      * @throws TransformException if the coordinate operation cannot be found.
+      *
+      * @since 1.7
+      */
+     public CoordinateOperation createChangeOfCRS(final 
CoordinateReferenceSystem target) throws TransformException {
+         try {
+             return findOperation(getCoordinateReferenceSystem(), target, 
geographicBBox());
+         } catch (FactoryException e) {
 -            throw new TransformException(e);
++            throw new TransformException(e.getLocalizedMessage(), e);
+         }
+     }
+ 
      /**
       * Creates a transform from cell coordinates in this grid to cell 
coordinates in the given grid.
       * The returned transform handles change of Coordinate Reference System 
and wraparound axes
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/MovingFeatures.java
index 30d6b6c1dc,a78a34c543..77e4ec4943
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/MovingFeatures.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/MovingFeatures.java
@@@ -124,4 -125,28 +124,28 @@@ public class MovingFeatures 
          }
          dest.characteristics().values().add(ct);
      }
+ 
+     /**
+      * Sets the "datetimes" characteristic on the given attribute to a single 
temporal value.
+      * If the {@code converter} is non-null, it will be used for converting 
the value to an {@link Instant} instance.
+      * Otherwise, the value is stored as-is as time elapsed in arbitrary 
units since an arbitrary epoch.
+      *
+      * @param  dest       the attribute on which to set time characteristic.
+      * @param  value      time in arbitrary units since an arbitrary epoch.
+      * @param  converter  the CRS to use for converting values to {@link 
Instant} instances, or {@code null}.
+      */
 -    public static void setTime(final Attribute<?> dest, final double value, 
final DefaultTemporalCRS converter) {
 -        final Attribute<?> ct;
++    public static void setTime(final AbstractAttribute<?> dest, final double 
value, final DefaultTemporalCRS converter) {
++        final AbstractAttribute<?> ct;
+         if (converter != null) {
+             final Instant instant = converter.toInstant(value);
 -            final Attribute<Instant> c = TIME_AS_INSTANTS.newInstance();
++            final AbstractAttribute<Instant> c = 
TIME_AS_INSTANTS.newInstance();
+             c.setValue(instant);
+             ct = c;
+         } else {
 -            final Attribute<Number> c = TIME_AS_NUMBERS.newInstance();
++            final AbstractAttribute<Number> c = TIME_AS_NUMBERS.newInstance();
+             c.setValue(value);
+             ct = c;
+         }
+         dest.characteristics().values().add(ct);
+     }
  }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
index 28bd310aa7,eeeb7a83d8..80a49dd1a3
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
@@@ -28,11 -28,15 +28,12 @@@ import org.apache.sis.filter.base.Node
  import org.apache.sis.filter.base.BinaryFunctionWidening;
  import org.apache.sis.temporal.TimeMethods;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.CodeList;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.MatchAction;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.BinaryComparisonOperator;
 -import org.opengis.filter.BetweenComparisonOperator;
 -import org.opengis.filter.Literal;
 +// Specific to the main branch:
++import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.MatchAction;
 +import org.apache.sis.pending.geoapi.filter.ComparisonOperatorName;
 +import org.apache.sis.pending.geoapi.filter.BinaryComparisonOperator;
 +import org.apache.sis.pending.geoapi.filter.BetweenComparisonOperator;
  
  
  /**
@@@ -242,8 -313,13 +310,13 @@@ abstract class ComparisonFilter<R> exte
              evaluator = (methods != null) ? methods.predicate(temporalTest(), 
otherType) : null;
          }
  
+         /** Creates a new filter which is reusing an already determined 
evaluator. */
+         Time(final BiPredicate<T,S> evaluator) {
+             this.evaluator = evaluator;
+         }
+ 
          /** Delegates to the enclosing class.*/
 -        @Override public    CodeList<?>           getOperatorType()  {return 
ComparisonFilter.this.getOperatorType();}
 +        @Override public    Enum<?>               getOperatorType()  {return 
ComparisonFilter.this.getOperatorType();}
          @Override public    Class<? super R>      getResourceClass() {return 
ComparisonFilter.this.getResourceClass();}
          @Override public    List<Expression<R,?>> getExpressions()   {return 
ComparisonFilter.this.getExpressions();}
          @Override protected Collection<?>         getChildren()      {return 
ComparisonFilter.this.getChildren();}
@@@ -261,6 -337,46 +334,46 @@@
              }
              return false;
          }
+ 
+         /** Creates a new filter of the same type but different parameters. */
+         @Override public Filter<R> recreate(Expression<R,?>[] effective) {
+             return ComparisonFilter.this.recreate(effective).new 
Time<>(evaluator);
+         }
+     }
+ 
+     /**
+      * An optimized versions of this filter for the case where the operands 
are comparable.
+      */
+     private final class Comparables extends Node implements 
Optimization.OnFilter<R> {
+         /** For cross-version compatibility during (de)serialization. */
+         private static final long serialVersionUID = -8311546273569455269L;
+ 
+         /** Delegates to the enclosing class.*/
 -        @Override public    CodeList<?>           getOperatorType()  {return 
ComparisonFilter.this.getOperatorType();}
++        @Override public    Enum<?>               getOperatorType()  {return 
ComparisonFilter.this.getOperatorType();}
+         @Override public    Class<? super R>      getResourceClass() {return 
ComparisonFilter.this.getResourceClass();}
+         @Override public    List<Expression<R,?>> getExpressions()   {return 
ComparisonFilter.this.getExpressions();}
+         @Override protected Collection<?>         getChildren()      {return 
ComparisonFilter.this.getChildren();}
+ 
+         /** Creates a new filter of the same type but different parameters. */
+         @Override public Filter<R> recreate(Expression<R,?>[] effective) {
+             return ComparisonFilter.this.recreate(effective).new 
Comparables();
+         }
+ 
+         /** Determines if the test represented by this filter passes with the 
given operands. */
+         @Override public boolean test(final R candidate) {
+             final Object left = expression1.apply(candidate);
+             if (left != null) {
+                 final Object right = expression2.apply(candidate);
+                 if (right != null) {
+                     if (left.getClass() == right.getClass()) {
+                         @SuppressWarnings("unchecked")
+                         final int result = ((Comparable) 
left).compareTo(right);
+                         return fromCompareTo(result);
+                     }
+                 }
+             }
+             return false;
+         }
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Predicate.java
index 3316e25dfd,2426fc031b..333770b4c4
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Predicate.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Predicate.java
@@@ -25,8 -26,9 +25,9 @@@ import org.apache.sis.filter.base.Unary
  import org.apache.sis.feature.internal.shared.FeatureExpression;
  import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.Filter;
 +// Specific to the main branch:
++import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -78,6 -84,14 +83,14 @@@ final class Predicate<R> extends UnaryF
          return function.getFunctionName();
      }
  
+     /**
+      * Returns the nature of the operator.
+      */
+     @Override
 -    public CodeList<?> getOperatorType() {
 -        return FilteringFunction.valueOf(function.name());
++    public Enum<?> getOperatorType() {
++        return null;    // Supported in GeoAPI 3.1 branch.
+     }
+ 
      /**
       * Returns the type of values computed by this expression.
       */
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedIterator.java
index 7bca9dbf6a,71af21c212..87476c8069
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedIterator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/BandedIterator.java
@@@ -28,7 -28,11 +28,8 @@@ import java.awt.image.Raster
  import java.awt.image.RenderedImage;
  import java.awt.image.WritableRaster;
  import java.awt.image.WritableRenderedImage;
+ import java.util.function.IntBinaryOperator;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.grid.SequenceType;
 -
  
  /**
   * A pixel iterator reading values directly from a {@link DataBuffer} instead 
of using {@link Raster} API.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritablePixelIterator.java
index 1632d624a8,eccbade41e..8f7dc80cbe
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritablePixelIterator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/WritablePixelIterator.java
@@@ -23,8 -23,13 +23,10 @@@ import java.awt.image.Raster
  import java.awt.image.RenderedImage;
  import java.awt.image.WritableRaster;
  import java.awt.image.WritableRenderedImage;
+ import java.util.function.IntBinaryOperator;
  import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.util.ArgumentChecks;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.grid.SequenceType;
 -
  
  /**
   * A pixel iterator capable to write sample values. This iterator can edit 
pixel values in place,
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
index 914e9d44a0,5dd5449581..e09448da27
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
@@@ -22,10 -22,16 +22,11 @@@ import static org.junit.jupiter.api.Ass
  import org.apache.sis.test.TestCase;
  import static org.apache.sis.test.Assertions.assertSerializedEquals;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.ComparisonOperator;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.BinaryComparisonOperator;
 -import org.opengis.filter.BetweenComparisonOperator;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
+ import org.apache.sis.filter.visitor.FunctionNames;
 +import org.apache.sis.pending.geoapi.filter.ComparisonOperatorName;
 +import org.apache.sis.pending.geoapi.filter.BetweenComparisonOperator;
  
  
  /**
@@@ -44,18 -50,18 +45,18 @@@ public final class ComparisonFilterTes
      /**
       * Expressions used as constant for the tests.
       */
-     private final Expression<AbstractFeature,Integer> c05, c10, c20;
 -    private final Literal<Feature, Integer> c05, c10, c20;
++    private final Expression<AbstractFeature, Integer> c05, c10, c20;
  
      /**
-      * Expected name of the filter to be evaluated. The {@code evaluate(…)} 
methods
+      * Expected name of the filter to be evaluated. The {@code 
assertTestEqual(…)} methods
       * will compare {@link ComparisonFilter#getFunctionName()} against this 
value.
       */
      private ComparisonOperatorName expectedName;
  
      /**
-      * The filter tested by last call to {@code evaluate(…)} methods.
+      * The filter tested by last call to {@code assertTestEqual(…)} methods.
       */
 -    private ComparisonOperator<Feature> filter;
 +    private Filter<AbstractFeature> filter;
  
      /**
       * Creates a new test case.
@@@ -68,10 -74,23 +69,23 @@@
      }
  
      /**
-      * Evaluates the given filter. The {@link #expectedName} field must be 
set before this method is invoked.
+      * Performs various assertions on the given filter, including 
serialization.
       * This method assumes that the first expression of all filters is {@link 
#c10}.
+      *
+      * @param expected  the expected result of evaluating the given filter.
+      * @param filter    the filter to test.
       */
-     private boolean evaluate(final Filter<AbstractFeature> filter) {
 -    private void verify(final boolean expected, final 
BinaryComparisonOperator<Feature> filter) {
++    private void verify(final boolean expected, final Filter<AbstractFeature> 
filter) {
+         assertTestEquals(expected, filter);
+         assertSame(c10, filter.getExpressions().get(0));
+         assertSerializedEquals(filter);
+     }
+ 
+     /**
+      * Evaluates the given filter and asserts that the returned value is 
equal to the expected value.
+      * The {@link #expectedName} field must be set before this method is 
invoked.
+      */
 -    private void assertTestEquals(final boolean expected, final 
BinaryComparisonOperator<Feature> filter) {
++    private void assertTestEquals(final boolean expected, final 
Filter<AbstractFeature> filter) {
          this.filter = filter;
          assertInstanceOf(ComparisonFilter.class, filter);
          assertEquals(expectedName, filter.getOperatorType());
@@@ -80,9 -101,10 +96,10 @@@
      }
  
      /**
-      * Evaluates the given "Property is between" filter.
+      * Evaluates the given "Property is between" filter and asserts that the 
returned value
+      * is equal to the expected value.
       */
-     private boolean evaluate(final BetweenComparisonOperator<AbstractFeature> 
filter) {
 -    private void assertTestEquals(final boolean expected, final 
BetweenComparisonOperator<Feature> filter) {
++    private void assertTestEquals(final boolean expected, final 
BetweenComparisonOperator<AbstractFeature> filter) {
          this.filter = filter;
          assertInstanceOf(ComparisonFilter.Between.class, filter);
          assertEquals(expectedName, filter.getOperatorType());
@@@ -166,10 -185,20 +180,20 @@@
       */
      @Test
      public void testBetween() {
 -        expectedName = 
ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN);
 -        assertTestEquals(true,  factory.between(c10, c05, c20));
 -        assertTestEquals(false, factory.between(c20, c05, c10));
 -        assertTestEquals(false, factory.between(c05, c10, c20));
 +        expectedName = ComparisonOperatorName.PROPERTY_IS_BETWEEN;
-         assertTrue (evaluate((BetweenComparisonOperator<AbstractFeature>) 
factory.between(c10, c05, c20)));
-         assertFalse(evaluate((BetweenComparisonOperator<AbstractFeature>) 
factory.between(c20, c05, c10)));
-         assertFalse(evaluate((BetweenComparisonOperator<AbstractFeature>) 
factory.between(c05, c10, c20)));
++        assertTestEquals(true,  (BetweenComparisonOperator<AbstractFeature>) 
factory.between(c10, c05, c20));
++        assertTestEquals(false, (BetweenComparisonOperator<AbstractFeature>) 
factory.between(c20, c05, c10));
++        assertTestEquals(false, (BetweenComparisonOperator<AbstractFeature>) 
factory.between(c05, c10, c20));
+         assertSerializedEquals(filter);
+     }
+ 
+     /**
+      * Tests "Equal" between Boolean values.
+      */
+     @Test
+     public void testBooleans() {
+         expectedName = ComparisonOperatorName.PROPERTY_IS_EQUAL_TO;
+         assertTestEquals(true, factory.equal(factory.function("isNaN", c10), 
factory.literal(Boolean.FALSE)));
          assertSerializedEquals(filter);
      }
  }
diff --cc 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
index 771258cf89,9c66b588e6..913659ab05
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
@@@ -41,9 -49,10 +49,10 @@@ import static org.apache.sis.test.Asser
  import static org.apache.sis.feature.Assertions.assertGridToCornerEquals;
  import org.apache.sis.test.TestCase;
  import org.apache.sis.referencing.crs.HardCodedCRS;
+ import org.apache.sis.referencing.operation.HardCodedConversions;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import static org.opengis.test.Assertions.assertAxisDirectionsEqual;
 +// Specific to the main branch:
 +import static org.apache.sis.test.GeoapiAssert.assertAxisDirectionsEqual;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java
index b36543956b,6d8ca82e91..2945b8eb37
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/FeatureSet.java
@@@ -860,8 -892,12 +892,12 @@@ makeGeom:   if (!isEmpty) 
               * The time vector is the first vector after the geometry 
dimensions.
               */
              if (hasTime) {
-                 MovingFeatures.setTimes((AbstractAttribute<?>) 
feature.getProperty(TRAJECTORY),
-                                         coordinateValues[geometryDimension], 
timeCRS);
 -                final var t = (Attribute<?>) feature.getProperty(TRAJECTORY);
++                final var t = (AbstractAttribute<?>) 
feature.getProperty(TRAJECTORY);
+                 if (isTrajectory) {
+                     MovingFeatures.setTimes(t, 
coordinateValues[geometryDimension], timeCRS);
+                 } else {
+                     MovingFeatures.setTime(t, 
coordinateValues[geometryDimension].doubleValue(offset), timeCRS);
+                 }
              }
              action.accept(feature);
              dynamicPropertyPosition += length;         // Check for 
ArithmeticException is already done by `extent(…)` call.
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
index 7b61878220,d1be9768ed..b4cbd50308
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
@@@ -140,11 -141,13 +141,13 @@@ final class CoverageSubset extends Abst
       * @throws DataStoreException if an error occurred while reading 
definitions from the underlying data store.
       */
      @Override
-     public List<double[]> getResolutions() throws DataStoreException {
-         List<double[]> resolutions = source.getResolutions();
-         if (reduction != null) {
-             JDK16.toList(resolutions.stream()
+     public List<double[]> getAvailableResolutions() throws DataStoreException 
{
+         List<double[]> resolutions = source.getAvailableResolutions();
+         if (reduction != null) try {
+             resolutions = JDK16.toList(resolutions.stream()
 -                    .map((resolution) -> reduction.apply(new 
DirectPositionView.Double(resolution)).getCoordinates()));
 +                    .map((resolution) -> reduction.apply(new 
DirectPositionView.Double(resolution)).getCoordinate()));
+         } catch (BackingStoreException e) {
+             throw e.unwrapOrRethrow(DataStoreException.class);
          }
          return resolutions;
      }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
index 826f758270,58d5d7760b..7765fdbcdc
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
@@@ -226,10 -229,10 +227,10 @@@ final class Store extends URIDataStore 
          geometries = 
Geometries.factory(connector.getOption(OptionKey.GEOMETRY_LIBRARY));
          dissociate = 
connector.getOption(DataOptionKey.FOLIATION_REPRESENTATION) == 
FoliationRepresentation.FRAGMENTED;
          @SuppressWarnings("LocalVariableHidesMemberVariable") GeneralEnvelope 
envelope    = null;
 -        @SuppressWarnings("LocalVariableHidesMemberVariable") FeatureType     
featureType = null;
 +        @SuppressWarnings("LocalVariableHidesMemberVariable") 
DefaultFeatureType featureType = null;
          @SuppressWarnings("LocalVariableHidesMemberVariable") Foliation       
foliation   = null;
          try {
-             final List<String> elements = new ArrayList<>();
+             final var elements = new ArrayList<String>();
              source.mark(StorageConnector.READ_AHEAD_LIMIT);
              String line;
              while ((line = source.readLine()) != null) {
@@@ -497,10 -500,10 +498,10 @@@
       * @return the column metadata, or {@code null} if the given list does 
not contain enough elements.
       */
      @SuppressWarnings("rawtypes")               // "rawtypes" because of 
generic array creation.
 -    private FeatureType parseFeatureType(final List<String> elements) throws 
DataStoreException {
 -        AttributeType[] characteristics = null;
 +    private DefaultFeatureType parseFeatureType(final List<String> elements) 
throws DataStoreException {
 +        DefaultAttributeType[] characteristics = null;
          final int size = elements.size();
-         final List<AbstractIdentifiedType> properties = new ArrayList<>();
 -        final var properties = new ArrayList<PropertyType>();
++        final var properties = new ArrayList<AbstractIdentifiedType>();
          for (int i=1; i<size; i++) {
              final String name = elements.get(i);
              Class<?> type = null;
@@@ -566,10 -569,12 +567,12 @@@
              }
              properties.add(createProperty(name, type, minOccurrence, 
maxOccurrence, characteristics));
          }
-         // Do not use Map.of(…) because `name` may be null. Let constructor 
throw the exception.
-         final String name = 
IOUtilities.filenameWithoutExtension(super.getDisplayName());
+         String name = 
IOUtilities.filenameWithoutExtension(super.getDisplayName());
+         if (name == null) {
+             name = 
Vocabulary.forLocale(getLocale()).getString(Vocabulary.Keys.Unnamed);
+         }
          return new 
DefaultFeatureType(Collections.singletonMap(DefaultFeatureType.NAME_KEY, name),
 -                                      false, null, 
properties.toArray(PropertyType[]::new));
 +                                      false, null, 
properties.toArray(AbstractIdentifiedType[]::new));
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
index 0000000000,0629472801..3207e8debe
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
@@@ -1,0 -1,532 +1,531 @@@
+ /*
+  * 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.storage.tiling;
+ 
+ import java.util.Arrays;
+ import java.util.Optional;
+ import java.util.stream.Stream;
+ import java.util.stream.StreamSupport;
+ import java.awt.Rectangle;
+ import java.awt.image.RenderedImage;
+ import java.nio.file.Path;
+ import java.util.logging.Logger;
+ import org.opengis.util.GenericName;
+ import org.opengis.referencing.operation.TransformException;
+ import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.storage.MemoryGridCoverageResource;
+ import org.apache.sis.storage.DataStoreException;
+ import org.apache.sis.storage.NoSuchDataException;
+ import org.apache.sis.storage.UnsupportedQueryException;
+ import org.apache.sis.storage.InternalDataStoreException;
+ import org.apache.sis.storage.Resource;
+ import org.apache.sis.coverage.grid.GridExtent;
+ import org.apache.sis.coverage.grid.GridGeometry;
+ import org.apache.sis.coverage.grid.GridCoverage2D;
+ import org.apache.sis.coverage.grid.GridCoverageProcessor;
+ import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
+ import org.apache.sis.image.ComputedImage;
+ import org.apache.sis.image.internal.shared.ReshapedImage;
+ import org.apache.sis.pending.jdk.JDK18;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.iso.Names;
+ import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.collection.BackingStoreException;
+ import org.apache.sis.math.DecimalFunctions;
+ import org.apache.sis.storage.internal.Resources;
+ 
+ 
+ /**
+  * Default implementation of {@code TileMatrix} as a wrapper for a {@code 
GridCoverage}.
+  * The tile size must be specified at construction time and must be equal to 
the size of
+  * the tiles of the rendered image.
+  *
+  * <p>This class is needed only when the application needs details about the 
tiling scheme,
+  * for example in order to implement a Web Map Tile Service 
(<abbr>WMTS</abbr>).</p>
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ final class ImageTileMatrix implements TileMatrix {
+     /**
+      * Logger for the tiling package.
+      */
+     static final Logger LOGGER = 
Logger.getLogger("org.apache.sis.storage.tiling");
+ 
+     /**
+      * An alphanumeric identifier which is unique in the {@code 
TileMatrixSet} that contains this {@code TileMatrix}.
+      * The identifier contains the zoom level as a number encoded in 
<abbr>ASCII</abbr>.
+      */
+     private final GenericName identifier;
+ 
+     /**
+      * The resource for reading the tiles of this tile matrix.
+      */
+     private final TiledGridCoverageResource resource;
+ 
+     /**
+      * The coverage from which to get the rendered image which contains the 
tiles.
+      * This coverage uses deferred reading of tiles. Created when first 
requested.
+      *
+      * @see #coverage()
+      */
+     private TiledGridCoverage coverage;
+ 
+     /**
+      * Pattern for formatting tile indices using {@link java.util.Formatter}.
+      * The number of digits and the presence of a sign depend on the 
{@linkplain #tilingScheme tiling scheme}.
+      *
+      * @see #getTileIdentifier(long[])
+      */
+     private final String tileIndicesPattern;
+ 
+     /**
+      * Extent of valid tile indices and their relationship with "real world" 
coordinates.
+      * The (0, 0) tile indices should map to the tile in the upper-left 
corner. The last
+      * row and last column of tiles may contain partial tiles if the coverage 
size is not
+      * a divisor of the tile size.
+      */
+     private final GridGeometry tilingScheme;
+ 
+     /**
+      * Size of tiles, in number of {@linkplain #coverage} cells.
+      * The length of this array may be shorter than the number of dimensions 
of the grid coverage,
+      * because some {@link TiledGridCoverage} implementations may use some 
metadata (typically the
+      * image date) as a third grid dimension, but still manage all tiles as 
two-dimensional.
+      * In such case, all extra dimensions are assumed to have a size of 1.
+      */
+     private final int[] tileSize;
+ 
+     /**
+      * Values to add to tile coordinates, after multiplication by {@link 
#tileSize}, for getting cell coordinates.
+      *
+      * @see #tileToCell(long, int)
+      */
+     private final long[] tileToCell;
+ 
+     /**
+      * Values to add to tile coordinates in {@link #image} for getting a tile 
coordinates in {@link #tilingScheme}.
+      * Those values are updated when a new image is rendered.
+      */
+     private long imageToTileX, imageToTileY;
+ 
+     /**
+      * The image containing the tiles of the tile matrix. Computed when first 
needed, and may be
+      * recomputed multiple times with different offsets if the tile indices 
are larger than the
+      * capacity of 32-bits integers.
+      */
+     private RenderedImage image;
+ 
+     /**
+      * The grid coverage processor to use when tiles use a subset of the 
bands.
+      *
+      * @see #createResourceView(long[], RenderedImage)
+      */
+     private final GridCoverageProcessor processor;
+ 
+     /**
+      * Creates a new tile matrix for the given coverage.
+      *
+      * @param  identifier  identifier unique in the {@code TileMatrixSet} 
that contains this {@code TileMatrix}.
+      * @param  resource    the resource for reading the tiles of this tile 
matrix.
+      * @param  processor   the grid coverage processor to use when tiles use 
a subset of the bands.
+      * @throws TransformException if the "tile indices to CRS" transform 
cannot be computed.
+      */
+     ImageTileMatrix(final GenericName identifier,
+                     final TiledGridCoverageResource resource,
+                     final GridCoverageProcessor processor)
+             throws DataStoreException, TransformException
+     {
+         this.identifier = identifier;
+         this.processor  = processor;
+         this.resource   = resource;
+         this.tileSize   = resource.getTileSize();
+         final GridGeometry cellGrid = resource.getGridGeometry();
+         final GridExtent extent     = cellGrid.getExtent();
+         final int        dimension  = extent.getDimension();
+         final long[]     tileCount  = new long[dimension];
+         final MatrixSIS  toCells    = Matrices.createIdentity(dimension + 1);
+         this.tileToCell = new long[dimension];
+         final var pattern = new StringBuilder(6 * dimension);
+         for (int i=0; i<dimension; i++) {
+             long size = extent.getSize(i);
+             final long offset = extent.getLow(i);
+             if (i < tileSize.length) {
+                 final int scale = tileSize[i];
+                 toCells.setNumber(i, i, scale);
+                 size = JDK18.ceilDiv(size, scale);
+             }
+             toCells.setNumber(i, dimension, offset);
+             tileToCell[i] = offset;
+             tileCount [i] = size;
+             /*
+              * Prepare a pattern for formatting the tile indices.
+              * Indices are formatted with fixed number of digits,
+              * using the minimum number needed for the largest index.
+              */
+             if (i != 0) pattern.append(',');
+             
pattern.append("%0").append(DecimalFunctions.floorLog10(Math.max(tileCount[i] - 
1, 1)) + 1).append('d');
+         }
+         tilingScheme = new GridGeometry(cellGrid, extent.reshape(null, 
tileCount, false), MathTransforms.linear(toCells));
+         tileIndicesPattern = pattern.toString();
+     }
+ 
+     /**
+      * Returns an identifier which is unique in the {@code TileMatrixSet} 
that contains this {@code TileMatrix}.
+      * The identifier contains the zoom level as a number encoded in 
<abbr>ASCII</abbr>.
+      */
+     @Override
+     public GenericName getIdentifier() {
+         return identifier;
+     }
+ 
+     /**
+      * Returns the resolution (in units of CRS axes) at which tiles in this 
matrix should be used.
+      * The array length is the number of <abbr>CRS</abbr> dimensions, and 
value at index <var>i</var>
+      * is the resolution along CRS dimension <var>i</var> in units of the CRS 
axis <var>i</var>.
+      *
+      * @throws IncompleteGridGeometryException if the tiling scheme has no 
resolution.
+      *         Tile matrices with such tiling scheme should not have been 
constructed.
+      */
+     @Override
+     public double[] getResolution() {
+         try {
+             return resource.getGridGeometry().getResolution(false);
+         } catch (DataStoreException e) {
+             throw new BackingStoreException(e);
+         }
+     }
+ 
+     /**
+      * Returns the tile size of the given matrix if known, or {@code null} 
otherwise.
+      * This method returns a direct reference to the internal array, caller 
shall not modify.
+      */
+     static int[] getTileSize(final TileMatrix matrix) {
+         return (matrix instanceof ImageTileMatrix) ? ((ImageTileMatrix) 
matrix).tileSize : null;
+     }
+ 
+     /**
+      * Returns a description about how space is partitioned into individual 
tiled units.
+      * The description contains the extent of valid tile indices, the spatial 
reference system,
+      * and the conversion from tile indices to the spatial reference system 
coordinates.
+      *
+      * @return extent of valid tile indices and their relationship with "real 
world" coordinates.
+      */
+     @Override
+     public GridGeometry getTilingScheme() {
+         return tilingScheme;
+     }
+ 
+     /**
+      * Returns the coverage, which is read when first needed.
+      * This coverage uses deferred reading of tiles.
+      *
+      * @return the coverage from which to get the rendered image which 
contains the tiles.
+      * @throws DataStoreException if an error occurred during the 
construction of the coverage.
+      */
+     private synchronized TiledGridCoverage coverage() throws 
DataStoreException {
+         if (coverage == null) {
+             coverage = resource.readAtGetTileTime();
+         }
+         return coverage;
+     }
+ 
+     /**
+      * Fetches information about whether a tile exists, is missing or failed 
to load.
+      *
+      * @param  indices  indices of the requested tile (may be outside the 
tile matrix extent).
+      * @return information about the availability of the specified tile.
+      * @throws DataStoreException if fetching the tile status failed.
+      */
+     @Override
+     @SuppressWarnings("LocalVariableHidesMemberVariable")
+     public TileStatus getTileStatus(final long... indices) throws 
DataStoreException {
+         if (tilingScheme.getExtent().contains(indices)) try {
+             final TiledGridCoverage coverage;
+             final RenderedImage image;
+             final long imageToTileX;
+             final long imageToTileY;
+             synchronized (this) {
+                 coverage     = this.coverage;   // Never null if `image` is 
non-null.
+                 image        = this.image;
+                 imageToTileX = this.imageToTileX;
+                 imageToTileY = this.imageToTileY;
+             }
+             if (image != null) {
+                 final long tileX = 
Math.subtractExact(indices[coverage.xDimension], imageToTileX);
+                 final long x0 = image.getMinTileX();
+                 if (tileX >= x0 && tileX < x0 + image.getNumXTiles()) {
+                     final long tileY = 
Math.subtractExact(indices[coverage.yDimension], imageToTileY);
+                     final long y0 = image.getMinTileY();
+                     if (tileY >= y0 && tileY < y0 + image.getNumYTiles()) {
+                         return getTileStatus(image, Math.toIntExact(tileX), 
Math.toIntExact(tileY));
+                     }
+                 }
+             }
+             return TileStatus.UNKNOWN;
+         } catch (ArithmeticException e) {
+             Logging.ignorableException(LOGGER, ImageTileMatrix.class, 
"getTileStatus", e);
+         }
+         return TileStatus.OUTSIDE_EXTENT;
+     }
+ 
+     /**
+      * Returns the status of the tile at the given index.
+      * If the image is an instance of {@link ComputedImage},
+      * then this method checks whether the tile is in error.
+      *
+      * This method does not check whether the tile indexes are outside the 
image domain
+      * ({@link TileStatus#OUTSIDE_EXTENT}). This verification must be done by 
the caller.
+      *
+      * @param  image  image from which to get a tile status.
+      * @param  tileX  row index of the tile for which to get the status.
+      * @param  tileY  column index of the tile for which to get the status.
+      * @return status of the tile at the specified indexes.
+      */
+     private static TileStatus getTileStatus(final RenderedImage image, final 
int tileX, final int tileY) {
+         if (image instanceof ComputedImage) {
+             final var computed = (ComputedImage) image;
+             final var tiles = new Rectangle(tileX, tileY, 1, 1);
+             if (computed.hasErrorFlag(tiles)) {
+                 return TileStatus.IN_ERROR;
+             }
+         }
+         return TileStatus.EXISTS;
+     }
+ 
+     /**
+      * Gets a tile at the given indices if not missing.
+      *
+      * @param  indices  indices of the tile to fetch, as coordinates inside 
the matrix extent.
+      * @return the tile if it exists, or an empty value if the tile is 
missing.
+      * @throws NoSuchDataException if the given indices are outside the 
matrix extent.
+      * @throws DataStoreException if fetching the tile failed for another 
reason.
+      */
+     @Override
+     public Optional<Tile> getTile(final long... indices) throws 
DataStoreException {
+         final GridExtent extent = tilingScheme.getExtent();
+         if (extent.contains(indices)) try {
+             final Tile tile = iterator(extent.reshape(indices, indices, 
true)).createFirstTile();
+             return (tile.getStatus() == TileStatus.MISSING) ? 
Optional.empty() : Optional.of(tile);
+         } catch (ArithmeticException e) {
+             throw new UnsupportedQueryException(e);
+         }
+         throw new 
NoSuchDataException(Resources.format(Resources.Keys.TileIndexesOutOfBounds));
+     }
+ 
+     /**
+      * Retrieves a stream of existing tiles in the specified region.
+      * The stream contains the existing tiles that are inside the given 
region and excludes missing tiles.
+      * If a tile is {@linkplain TileStatus#IN_ERROR in error}, then the 
stream nevertheless return a tile
+      * but its {@link Tile#getResource()} method should throw the exception.
+      *
+      * <h4>Limitations</h4>
+      * The current implementation limits the size of the given extent to 
{@link Integer#MAX_VALUE}
+      * in each dimension. Note that this is a maximum size in tile indices, 
not in pixel coordinates.
+      *
+      * @param  indiceRanges  ranges of tile indices in all dimensions, or 
{@code null} for all tiles.
+      * @param  parallel  {@code true} for a parallel stream (if supported), 
or {@code false} for a sequential stream.
+      * @return stream of tiles, excluding missing tiles.
+      * @throws DataStoreException if the tiles can not be fetched in the 
given ranges of tile indexes.
+      */
+     @Override
+     public Stream<Tile> getTiles(GridExtent indiceRanges, final boolean 
parallel) throws DataStoreException {
 -        ArgumentChecks.ensureDimensionMatches("indiceRanges", 
tilingScheme.getDimension(), indiceRanges);
+         if (indiceRanges == null) {
+             indiceRanges = tilingScheme.getExtent();
+         }
+         try {
+             return StreamSupport.stream(iterator(indiceRanges).iterator(), 
parallel);
+         } catch (ArithmeticException e) {
+             throw new UnsupportedQueryException(e);
+         }
+     }
+ 
+     /**
+      * Creates an object which can be used for retrieving a single tile or a 
stream tiles.
+      *
+      * @param  indiceRanges  ranges of tile indices in all dimensions, or 
{@code null} for all tiles.
+      * @return a request which can be used for getting a tile or a stream of 
tiles in the given region.
+      * @throws DataStoreException if the tiles can not be fetched in the 
given ranges of tile indexes.
+      * @throws ArithmeticException if coordinate computation exceeds the 
capacity of 64-bits integers.
+      */
+     private synchronized IterationDomain<Tile> iterator(final GridExtent 
indiceRanges) throws DataStoreException {
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         final TiledGridCoverage coverage = coverage();
+         boolean retry = false;
+         do {    // This loop will be executed only 1 or 2 times.
+             if (image != null) {
+                 final long xmin, ymin, xmax, ymax;
+                 xmin = Math.subtractExact(indiceRanges.getLow 
(coverage.xDimension), imageToTileX);
+                 xmax = 
Math.subtractExact(indiceRanges.getHigh(coverage.xDimension), imageToTileX);
+                 final long x0 = image.getMinTileX();
+                 if (xmin >= x0 && xmax < x0 + image.getNumXTiles()) {
+                     ymin = Math.subtractExact(indiceRanges.getLow 
(coverage.yDimension), imageToTileY);
+                     ymax = 
Math.subtractExact(indiceRanges.getHigh(coverage.yDimension), imageToTileY);
+                     final long y0 = image.getMinTileY();
+                     if (ymin >= y0 && ymax < y0 + image.getNumYTiles()) {
+                         return new Iterator(Math.toIntExact(xmin),
+                                             Math.toIntExact(ymin),
+                                             Math.toIntExact(xmax),
+                                             Math.toIntExact(ymax));
+                     }
+                 }
+             }
+             /*
+              * Gets the bounds of the image to read. If deferred reading is 
supported,
+              * we can expand to the bounds of the whole coverage in order to 
perform a
+              * read operation (deferred) only once.
+              */
+             final GridExtent extent = coverage.getGridGeometry().getExtent();
+             final int dimension = extent.getDimension();
+             final var low  = new long[dimension];
+             final var high = new long[dimension];
+             for (int i=0; i<dimension; i++) {
+                 final long limit = Math.incrementExact(extent.getHigh(i));
+                 high[i] = Math.min(limit, 
tileToCell(Math.incrementExact(indiceRanges.getHigh(i)), i));
+                 low [i] = Math.max(extent.getLow(i), 
tileToCell(indiceRanges.getLow(i), i));
+                 final long span = high[i] - low[i];
+                 if (span < 0 || span > Integer.MAX_VALUE) {
+                     throw new 
ArithmeticException(resource.errors().getString(Errors.Keys.IntegerOverflow_1, 
Integer.SIZE));
+                 }
+                 if (coverage.deferredTileReading) {
+                     final long remain = Math.min(extent.getSize(i), 
Integer.MAX_VALUE) - span;
+                     final long after  = Math.min(remain >> 1, limit - 
high[i]);
+                     final long before = Math.min(remain - after, low[i] - 
extent.getLow(i));
+                     low [i] -= before;
+                     high[i] += after;
+                 }
+             }
+             image = coverage.render(extent.reshape(low, high, false));
+             imageToTileX = low[coverage.xDimension];
+             imageToTileY = low[coverage.yDimension];
+         } while ((retry = !retry) == true);
+         throw new InternalDataStoreException();     // Should never happen.
+     }
+ 
+     /**
+      * Converts the give tile coordinate in the given dimension to cell 
coordinates.
+      *
+      * @param  coordinate  the tile coordinate to convert.
+      * @param  dimension   the dimension of the coordinate to convert.
+      * @return the cell coordinate.
+      * @throws ArithmeticException if the result overflows the capacity of 
64-bits integers.
+      */
+     private long tileToCell(long coordinate, final int dimension) {
+         if (dimension < tileSize.length) {
+             coordinate = Math.multiplyExact(coordinate, tileSize[dimension]);
+         }
+         return Math.addExact(tileToCell[dimension], coordinate);
+     }
+ 
+     /**
+      * Factory for an iterator over tiles in ranges of user-specified tile 
indices.
+      */
+     private final class Iterator extends IterationDomain<Tile> {
+         /**
+          * Snapshot of {@link ImageTileMatrix#image}.
+          */
+         private final RenderedImage tiles;
+ 
+         /**
+          * Snapshot of {@link ImageTileMatrix#imageToTileX} and {@link 
ImageTileMatrix#imageToTileY}.
+          */
+         private final long offsetX, offsetY;
+ 
+         /**
+          * Creates a new request for tile iterators.
+          *
+          * @param xmin  first column index of tiles, inclusive.
+          * @param xmin  first row index of tiles, inclusive.
+          * @param xmax  last column index of tiles, inclusive.
+          * @param ymax  last row index of tiles, inclusive.
+          */
+         Iterator(final int xmin, final int ymin, final int xmax, final int 
ymax) {
+             super(xmin, ymin, xmax, ymax);
+             tiles   = image;
+             offsetX = imageToTileX;
+             offsetY = imageToTileY;
+         }
+ 
+         /**
+          * Creates the tile at the given indexes.
+          * The caller must ensure that the arguments are valid image tile 
indexes.
+          * This condition is not verified by this method.
+          */
+         @Override
+         protected Tile createTile(final int tileX, final int tileY) {
+             return new Tile() {
+                 /** This tile viewed as a resource, created when first 
requested. */
+                 private Resource resourceView;
+ 
+                 /** Returns the path to content of the tile if known. */
+                 @Override public Optional<Path> getContentPath() throws 
DataStoreException {
+                     return 
Optional.ofNullable(coverage().getContentPath(getIndices()));
+                 }
+ 
+                 /** Returns the indices of this tile in the {@code 
TileMatrix}. */
+                 @Override public long[] getIndices() {
+                     return new long[] {offsetX + tileX, offsetY + tileY};
+                 }
+ 
+                 /** Returns information about whether the tile failed to 
load. */
+                 @Override public TileStatus getStatus() {
+                     return getTileStatus(tiles, tileX, tileY);
+                 }
+ 
+                 /** Returns the tile content as a resource. */
+                 @Override public synchronized Resource getResource() throws 
DataStoreException {
+                     if (resourceView == null) {
+                         resourceView = createResourceView(getIndices(), 
ReshapedImage.singleTile(tiles, tileX, tileY));
+                     }
+                     return resourceView;
+                 }
+             };
+         }
+     }
+ 
+     /**
+      * Creates a resource for the tile at the given indices.
+      * The resource wraps a grid coverage, which is itself wrapping the given 
image.
+      * The given image should contains only the desired tile. The caller 
currently sets
+      * the tile indexes and image coordinates to (0,0), but this is not 
mandatory.
+      *
+      * @param  indices  indices of the tile, as coordinates inside the matrix 
extent.
+      * @param  tile     a rendered image which contains only the tile.
+      * @return resource for the specified tile.
+      */
+     private Resource createResourceView(final long[] indices, final 
RenderedImage tile) throws DataStoreException {
+         final Object[] args = new Object[indices.length];
+         Arrays.setAll(args, (i) -> indices[i]);
+         final GenericName id = Names.createScopedName(identifier, null, 
String.format(tileIndicesPattern, args));
+         final long[] low  = new long[indices.length];
+         final long[] high = new long[indices.length];
+         for (int i=0; i<indices.length; i++) {
+             final long size = (i < tileSize.length) ? tileSize[i] : 1;
+             low [i] = Math.addExact(tileToCell[i], 
Math.multiplyExact(indices[i], size));
+             high[i] = Math.addExact(low[i], size - 1);
+         }
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         final TiledGridCoverage coverage = coverage();
+         GridGeometry cellGrid = coverage.getGridGeometry();
+         final GridExtent extent = cellGrid.getExtent().reshape(low, high, 
true);
+         cellGrid = cellGrid.derive().subgrid(extent, null).build();
+         final var subset = new GridCoverage2D(cellGrid, 
coverage.getSampleDimensions(), tile);
+         return new MemoryGridCoverageResource(resource, id, subset, 
processor);
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
index 77aa7feb88,833361beb1..1958562d09
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
@@@ -45,15 -51,20 +51,20 @@@ import org.apache.sis.storage.Resource
  import org.apache.sis.storage.AbstractGridCoverageResource;
  import org.apache.sis.storage.DataStoreException;
  import org.apache.sis.storage.RasterLoadingStrategy;
+ import org.apache.sis.storage.event.StoreListeners;
  import org.apache.sis.measure.NumberRange;
  import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.resources.Errors;
  import org.apache.sis.util.internal.shared.Numerics;
+ import org.apache.sis.util.collection.BackingStoreException;
+ import org.apache.sis.util.collection.Containers;
+ import org.apache.sis.util.collection.ListOfUnknownSize;
  import org.apache.sis.util.collection.WeakValueHashMap;
- import static org.apache.sis.storage.base.TiledGridCoverage.X_DIMENSION;
- import static org.apache.sis.storage.base.TiledGridCoverage.Y_DIMENSION;
+ import org.apache.sis.util.iso.DefaultNameFactory;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.CannotEvaluateException;
 +// Specific to the main branch:
 +import org.apache.sis.coverage.CannotEvaluateException;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java
index a828701bed,c393e45edb..0a6e312be7
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArgumentChecks.java
@@@ -768,6 -769,30 +768,30 @@@ public final class ArgumentChecks 
       */
      public static void ensureDimensionMatches(final String name, final int 
expected, final int[] indices)
              throws MismatchedDimensionException
+     {
+         if (indices != null) {
+             final int dimension = indices.length;
+             if (dimension != expected) {
 -                throw new 
org.opengis.geometry.MismatchedDimensionException(Errors.format(
++                throw new MismatchedDimensionException(Errors.format(
+                         Errors.Keys.MismatchedDimension_3, name, expected, 
dimension));
+             }
+         }
+     }
+ 
+     /**
+      * Ensures that the given array of indices, if non-null, has the expected 
number of dimensions
+      * (taken as its length). This method does nothing if the given array is 
null.
+      *
+      * @param  name      the name of the argument to be checked. Used only if 
an exception is thrown.
+      * @param  expected  the expected number of dimensions.
+      * @param  indices   the array of indices to check for its number of 
dimensions, or {@code null}.
+      * @throws MismatchedDimensionException if the given array of indices is 
non-null and does not have
+      *         the expected number of dimensions (taken as its length).
+      *
+      * @since 1.7
+      */
+     public static void ensureDimensionMatches(final String name, final int 
expected, final long[] indices)
+             throws MismatchedDimensionException
      {
          if (indices != null) {
              final int dimension = indices.length;
diff --cc 
optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
index a6cc599843,3379cc7228..1893db341a
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
@@@ -69,10 -72,10 +72,10 @@@ import org.apache.sis.gui.map.StatusBar
   *
   * <h2>Limitations</h2>
   * Current implementation is restricted to {@link GridCoverage} instances, 
but a future
 - * implementation may generalize to {@link org.opengis.coverage.Coverage} 
instances.
 + * implementation may generalize to {@code org.opengis.coverage.Coverage} 
instances.
   *
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.5
+  * @version 1.7
   *
   * @see CoverageCanvas
   * @see GridView

Reply via email to