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

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

commit e2d787424034364d91fb30e635bcf97f1e7c75ce
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Oct 16 15:10:40 2025 +0200

    If a TIFF `DateTime` tag exists, use it as the temporal coordinate of the 
grid geometry.
    This behavior is specified by the Defense Geospatial Information Working 
Group (DGIWG).
    
    https://issues.apache.org/jira/browse/SIS-620
---
 .../sis/coverage/grid/GridCoverageProcessor.java   |   2 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  15 +--
 .../apache/sis/coverage/grid/GridOrientation.java  |  34 ++++--
 .../org/apache/sis/coverage/grid/package-info.java |   2 +-
 .../org.apache.sis.metadata/main/module-info.java  |   2 +-
 .../org/apache/sis/metadata/sql/package-info.java  |   2 +-
 .../org/apache/sis/temporal/LenientDateFormat.java |   5 +-
 .../main/module-info.java                          |   2 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |  25 ++++-
 .../referencing/factory/sql/EPSGDataAccess.java    |   2 +-
 .../sis/referencing/factory/sql/package-info.java  |   2 +-
 .../shared/ReferencingFactoryContainer.java        |   5 -
 .../internal/shared/TemporalAccessor.java          |   2 +-
 .../main/module-info.java                          |   2 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  30 ------
 .../sis/storage/geotiff/ImageFileDirectory.java    |  95 +++++++++++------
 .../sis/storage/geotiff/MultiResolutionImage.java  |  27 ++---
 .../org/apache/sis/storage/geotiff/Writer.java     |  20 ++--
 .../org/apache/sis/storage/geotiff/base/Tags.java  |   2 +-
 .../apache/sis/storage/geotiff/package-info.java   |   2 +-
 .../sis/storage/geotiff/reader/CRSBuilder.java     |   6 +-
 .../geotiff/reader/GridGeometryBuilder.java        | 118 +++++++++++++++------
 .../apache/sis/storage/geotiff/reader/Type.java    |   2 +
 .../sis/storage/geotiff/reader/XMLMetadata.java    |   2 +-
 .../sis/storage/geotiff/writer/GeoEncoder.java     |  72 ++++++++++++-
 .../main/module-info.java                          |   2 +-
 .../org/apache/sis/storage/sql/package-info.java   |   2 +-
 .../org.apache.sis.storage/main/module-info.java   |   2 +-
 .../sis/storage/aggregate/CoverageAggregator.java  |   2 +-
 .../apache/sis/storage/base/MetadataFetcher.java   |  28 ++---
 .../main/org/apache/sis/storage/csv/Store.java     |   2 +-
 .../org/apache/sis/storage/csv/TimeEncoding.java   |   7 +-
 .../src/org.apache.sis.util/main/module-info.java  |   2 +-
 .../main/org/apache/sis/math/Statistics.java       |   2 +-
 34 files changed, 333 insertions(+), 194 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index c4648c90a4..b4b2f9a674 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -720,7 +720,7 @@ public class GridCoverageProcessor implements Cloneable {
      * @since 1.5
      */
     public GridCoverage appendDimension(final GridCoverage source, final 
Instant lower, final Duration span) {
-        final DefaultTemporalCRS crs = 
DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.TRUNCATED_JULIAN.crs());
+        final DefaultTemporalCRS crs = 
DefaultTemporalCRS.castOrCopy(CommonCRS.defaultTemporal());
         double scale  = crs.toValue(span);
         double offset = crs.toValue(lower);
         long   index  = Numerics.roundAndClamp(offset / scale);             // 
See comment in above method.
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 7a73ef7192..ea203379e0 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
@@ -134,7 +134,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   1.0
  */
 public class GridGeometry implements LenientComparable, Serializable {
@@ -222,9 +222,10 @@ public class GridGeometry implements LenientComparable, 
Serializable {
     protected final GridExtent extent;
 
     /**
-     * The geodetic envelope, or {@code null} if unknown. If non-null, this 
envelope is usually the grid {@link #extent}
-     * {@linkplain #gridToCRS transformed} to real world coordinates. The 
Coordinate Reference System} (CRS) of this
-     * envelope defines the "real world" CRS of this grid geometry.
+     * The spatiotemporal extent in units of the <abbr>CRS</abbr>, or {@code 
null} if unknown.
+     * If non-null, this envelope is usually the grid {@link #extent} 
{@linkplain #gridToCRS transformed}
+     * to real world coordinates, with the lower coordinates inclusive and the 
upper coordinates exclusive.
+     * The Coordinate Reference System (CRS) of this envelope defines the 
"real world" CRS of this grid geometry.
      *
      * @see #ENVELOPE
      * @see #getEnvelope()
@@ -683,7 +684,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
             this.envelope = null;
         } else {
             this.envelope = target;
-            if (extent != null) {
+            if (extent != null && orientation != GridOrientation.UNKNOWN) {
                 // A non-null `sourceDimensions` implies non-null 
`orientation`.
                 if (sourceDimensions != null && 
orientation.canReorderGridAxis) {
                     if (!ArraysExt.isRange(0, sourceDimensions)) {
@@ -1138,8 +1139,8 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * Returns the start time and end time of coordinates of the grid.
      * If the grid has no temporal dimension, then this method returns an 
empty array.
      * If only the start time or end time is defined, then returns an array of 
length 1.
-     * Otherwise this method returns an array of length 2 with the start time 
in the first element
-     * and the end time in the last element.
+     * Otherwise this method returns an array of length 2 with the start time 
(inclusive)
+     * in the first element and the end time (exclusive) in the last element.
      *
      * @return time range as an array of length 0 (if none), 1 or 2.
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridOrientation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridOrientation.java
index 6fc1594eb3..d5c31f9b56 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridOrientation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridOrientation.java
@@ -30,10 +30,15 @@ import org.apache.sis.util.resources.Errors;
  * For example, the conversion from grid coordinates to CRS coordinates may 
flip the <var>y</var> axis
  * (grid coordinates increasing toward down on screen), or may swap 
<var>x</var> and <var>y</var> axes, <i>etc.</i>
  * The constants enumerated in this class cover only a few common cases where 
the grid is
- * <a 
href="https://en.wikipedia.org/wiki/Axis-aligned_object";>axis-aligned</a> with 
the CRS.
+ * <a 
href="https://en.wikipedia.org/wiki/Axis-aligned_object";>axis-aligned</a> with 
the <abbr>CRS</abbr>.
+ *
+ * <h4>Custom orientations</h4>
+ * For creating a custom orientations, one of the constants defined in this 
class can be used as a starting point.
+ * Then, the {@link #flipGridAxis(int)}, {@link 
#useVariantOfCRS(AxesConvention)} or {@link #canReorderGridAxis(boolean)}
+ * methods can be invoked.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.6
  *
  * @see GridGeometry#GridGeometry(GridExtent, Envelope, GridOrientation)
  *
@@ -143,6 +148,15 @@ public final class GridOrientation implements Serializable 
{
      */
     public static final GridOrientation DISPLAY = new GridOrientation(2, 
AxesConvention.DISPLAY_ORIENTED, false);
 
+    /**
+     * Unknown image orientation. The {@linkplain 
GridGeometry#getGridToCRS(PixelInCell) grid to CRS}
+     * transforms inferred from this orientation will be null.
+     * All methods in this class invoked on the {@code UNKNOWN} instance will 
return {@code UNKNOWN}.
+     *
+     * @since 1.6
+     */
+    public static final GridOrientation UNKNOWN = new GridOrientation(0, null, 
true);
+
     /**
      * Set of grid axes to reverse, as a bit mask. For any dimension 
<var>i</var>, the bit
      * at {@code 1L << i} is set to 1 if the grid axis at that dimension 
should be flipped.
@@ -163,7 +177,7 @@ public final class GridOrientation implements Serializable {
 
     /**
      * Whether {@link GridExtent} can be rewritten with a different axis order
-     * for matching the CRS axis order specified by {@link #crsVariant}.
+     * for matching the <abbr>CRS</abbr> axis order specified by {@link 
#crsVariant}.
      * If {@code false}, then axis order changes will be handled in the {@code 
gridToCRS} transform instead.
      *
      * @see #canReorderGridAxis(boolean)
@@ -196,6 +210,9 @@ public final class GridOrientation implements Serializable {
         if (dimension >= Long.SIZE) {
             throw new 
ArithmeticException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, 
dimension + 1));
         }
+        if (this == UNKNOWN) {
+            return this;
+        }
         return new GridOrientation(flippedAxes ^ (1L << dimension), 
crsVariant, canReorderGridAxis);
     }
 
@@ -227,7 +244,7 @@ public final class GridOrientation implements Serializable {
      * @see #DISPLAY
      */
     public GridOrientation useVariantOfCRS(final AxesConvention variant) {
-        if (variant == crsVariant) {
+        if (variant == crsVariant || this == UNKNOWN) {
             return this;
         }
         if (variant == AxesConvention.NORMALIZED || variant == 
AxesConvention.ORIGINAL) {
@@ -246,7 +263,7 @@ public final class GridOrientation implements Serializable {
      * @return a grid orientation equals to this one except that it has the 
specified flag.
      */
     public GridOrientation canReorderGridAxis(final boolean enabled) {
-        if (enabled == canReorderGridAxis) {
+        if (enabled == canReorderGridAxis || this == UNKNOWN) {
             return this;
         }
         return new GridOrientation(flippedAxes, crsVariant, enabled);
@@ -261,7 +278,7 @@ public final class GridOrientation implements Serializable {
     @Override
     public boolean equals(final Object other) {
         if (other instanceof GridOrientation) {
-            final GridOrientation that = (GridOrientation) other;
+            final var that = (GridOrientation) other;
             return flippedAxes == that.flippedAxes &&
                    crsVariant  == that.crsVariant  &&
                    canReorderGridAxis == that.canReorderGridAxis;
@@ -283,7 +300,10 @@ public final class GridOrientation implements Serializable 
{
      */
     @Override
     public String toString() {
-        final StringBuilder buffer = new 
StringBuilder(getClass().getSimpleName()).append('[');
+        if (this == UNKNOWN) {
+            return "UNKNOWN";
+        }
+        final var buffer = new 
StringBuilder(getClass().getSimpleName()).append('[');
         String separator = "";
         if (flippedAxes != 0) {
             buffer.append("flip={");
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/package-info.java
index 8fde1c33e5..81b06aaef5 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/package-info.java
@@ -41,7 +41,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   1.0
  */
 package org.apache.sis.coverage.grid;
diff --git a/endorsed/src/org.apache.sis.metadata/main/module-info.java 
b/endorsed/src/org.apache.sis.metadata/main/module-info.java
index e66e26c614..e76f65657f 100644
--- a/endorsed/src/org.apache.sis.metadata/main/module-info.java
+++ b/endorsed/src/org.apache.sis.metadata/main/module-info.java
@@ -23,7 +23,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Cédric Briançon (Geomatys)
  * @author  Cullen Rombach (Image Matters)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 module org.apache.sis.metadata {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/package-info.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/package-info.java
index d6396799d0..30d5b7ae9b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/package-info.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/package-info.java
@@ -42,7 +42,7 @@
  *
  * @author  Touraïvane (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.6
  *
  * @see org.apache.sis.referencing.factory.sql
  *
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java
index c3672d4178..09176ac3da 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java
@@ -124,7 +124,7 @@ public final class LenientDateFormat extends DateFormat {
     }
 
     /**
-     * Parses the given date as an instant, assuming UTC timezone if 
unspecified.
+     * Parses the given date as an instant, assuming <abbr>UTC</abbr> timezone 
if unspecified.
      *
      * @param  text   the text to parse as an instant in UTC timezone by 
default, or {@code null}.
      * @return the instant for the given text, or {@code null} if the given 
text was null.
@@ -135,7 +135,8 @@ public final class LenientDateFormat extends DateFormat {
     }
 
     /**
-     * Parses the given date as an instant, assuming UTC timezone if 
unspecified.
+     * Parses the given date as an instant, assuming <abbr>UTC</abbr> timezone 
if unspecified.
+     * This method is tolerant to date and time separated by a space instead 
of the {@code 'T'} character.
      *
      * @param  text   the text to parse as an instant in UTC timezone by 
default.
      * @param  lower  index of the first character to parse.
diff --git a/endorsed/src/org.apache.sis.referencing/main/module-info.java 
b/endorsed/src/org.apache.sis.referencing/main/module-info.java
index 27146fb6ca..2457ee6ede 100644
--- a/endorsed/src/org.apache.sis.referencing/main/module-info.java
+++ b/endorsed/src/org.apache.sis.referencing/main/module-info.java
@@ -21,7 +21,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
  * @author  Maxime Gavens (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 module org.apache.sis.referencing {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
index ddd75f1fed..d446fd47cb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
@@ -572,11 +572,33 @@ public enum CommonCRS {
      * their datum can be arbitrary.</p>
      *
      * @return the default two-dimensional geographic CRS with 
(<var>longitude</var>, <var>latitude</var>) axis order.
+     *
+     * @see #defaultTemporal()
      */
     public static GeographicCRS defaultGeographic() {
         return DEFAULT.normalizedGeographic();
     }
 
+    /**
+     * Returns the default temporal <abbr>CRS</abbr> used by the Apache 
<abbr>SIS</abbr> library.
+     * The current implementation uses {@linkplain Temporal#TRUNCATED_JULIAN 
truncated Julian days},
+     * which are the number of days elapsed since May 24, 1968 at 00:00 
<abbr>UTC</abbr>.
+     * However, this default <abbr>CRS</abbr> may change in any future 
<abbr>SIS</abbr> version.
+     * For handling the coordinate values in a way independent of 
<abbr>CRS</abbr> changes,
+     * the can be converted with {@link DefaultTemporalCRS#toInstant(double)}.
+     *
+     * @return the default temporal <abbr>CRS</abbr>.
+     *
+     * @see #defaultGeographic()
+     * @see Temporal#TRUNCATED_JULIAN
+     * @see DefaultTemporalCRS#toInstant(double)
+     *
+     * @since 1.6
+     */
+    public static TemporalCRS defaultTemporal() {
+        return Temporal.TRUNCATED_JULIAN.crs();
+    }
+
     /**
      * Returns a two-dimensional geographic CRS with axes in the non-standard 
but computationally convenient
      * (<var>longitude</var>, <var>latitude</var>) order. The coordinate 
system axes will be oriented toward
@@ -1553,6 +1575,7 @@ public enum CommonCRS {
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.5
      *
+     * @see #defaultTemporal()
      * @see Engineering#TIME
      *
      * @since 0.4
@@ -1618,7 +1641,7 @@ public enum CommonCRS {
          * (by contrast, the IUGS definition is only about duration).
          *
          * <h4>Application to geodesy</h4>
-         * The tropical year is the unit of measurement used in EPSG geodetic 
database for year duration.
+         * The tropical year is the unit of measurement used in the 
<abbr>EPSG</abbr> geodetic dataset for year duration.
          * It it used for rate of changes such as "centimeters per year". Its 
identifier is EPSG:1029.
          *
          * @see Units#TROPICAL_YEAR
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index b0a8fc0bc9..8ab167b668 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -160,7 +160,7 @@ import org.opengis.referencing.ObjectDomain;
  * @author  Matthias Basler
  * @author  Andrea Aime (TOPP)
  * @author  Johann Sorel (Geomatys)
- * @version 1.5
+ * @version 1.6
  *
  * @see <a 
href="https://sis.apache.org/tables/CoordinateReferenceSystems.html";>List of 
authority codes</a>
  *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java
index bde70bffc3..3df5fafcea 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java
@@ -83,7 +83,7 @@
  * @author  Jody Garnett (Refractions)
  * @author  Didier Richard (IGN)
  * @author  John Grange
- * @version 1.5
+ * @version 1.6
  *
  * @see org.apache.sis.metadata.sql
  *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
index 05d07eccc0..e745d32268 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
@@ -51,14 +51,9 @@ import org.opengis.util.Factory;
  * A container of factories frequently used together.
  * Provides also some utility methods working with factories.
  *
- * This class may be temporary until we choose a dependency injection framework
- * See <a href="https://issues.apache.org/jira/browse/SIS-102";>SIS-102</a>.
- *
  * <p>This class is not thread safe. Synchronization, if needed, is caller's 
responsibility.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- *
- * @see <a href="https://issues.apache.org/jira/browse/SIS-102";>SIS-102</a>
  */
 public class ReferencingFactoryContainer implements Localized {
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/TemporalAccessor.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/TemporalAccessor.java
index e348b50eba..bab0c03bf3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/TemporalAccessor.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/TemporalAccessor.java
@@ -101,7 +101,7 @@ public final class TemporalAccessor {
             startTime = endTime;
             endTime = null;
         }
-        final Instant[] times = new Instant[(endTime != null) ? 2 : 1];
+        final var times = new Instant[(endTime != null) ? 2 : 1];
         switch (times.length) {
             default: times[1] = endTime;        // Fall through.
             case 1:  times[0] = startTime;      // Fall through.
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java 
b/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
index d65fe43c7f..3e4d1b77c9 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
@@ -22,7 +22,7 @@
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Minh Chinh Vu (VNSC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.8
  */
 module org.apache.sis.storage.geotiff {
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 3ffe5083f8..1152138fe2 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -19,11 +19,7 @@ package org.apache.sis.storage.geotiff;
 import java.util.Set;
 import java.util.List;
 import java.util.Locale;
-import java.util.TimeZone;
 import java.util.Optional;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.time.ZoneId;
 import java.net.URI;
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -121,18 +117,6 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      */
     final Locale dataLocale;
 
-    /**
-     * The timezone for the date and time parsing, or {@code null} for the 
default.
-     */
-    private final ZoneId timezone;
-
-    /**
-     * The object to use for parsing and formatting dates. Created when first 
needed.
-     *
-     * @see #getDateFormat()
-     */
-    private transient DateFormat dateFormat;
-
     /**
      * The {@link GeoTiffStoreProvider#LOCATION} parameter value, or {@code 
null} if none.
      * This is used for information purpose only, not for actual reading 
operations.
@@ -261,7 +245,6 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
 
         compression = connector.getOption(Compression.OPTION_KEY);
         dataLocale  = connector.getOption(OptionKey.LOCALE);
-        timezone    = connector.getOption(OptionKey.TIMEZONE);
         location    = connector.getStorageAs(URI.class);
         path        = connector.getStorageAs(Path.class);
         try {
@@ -498,19 +481,6 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         return (path != null) ? Optional.of(new FileSet(path)) : 
Optional.empty();
     }
 
-    /**
-     * Returns the object to use for parsing and formatting dates.
-     */
-    final DateFormat getDateFormat() {
-        if (dateFormat == null) {
-            dateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", 
Locale.US);
-            if (timezone != null) {
-                dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
-            }
-        }
-        return dateFormat;
-    }
-
     /**
      * Returns the reader if it is not closed, or throws an exception 
otherwise.
      *
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index ab44a85017..c8368e99a2 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -17,7 +17,8 @@
 package org.apache.sis.storage.geotiff;
 
 import java.io.IOException;
-import java.text.ParseException;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Optional;
@@ -34,6 +35,7 @@ import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
 import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.citation.DateType;
+import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.util.GenericName;
 import org.opengis.util.NameSpace;
 import org.opengis.util.FactoryException;
@@ -65,13 +67,14 @@ import 
org.apache.sis.util.internal.shared.UnmodifiableArrayList;
 import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.internal.shared.Strings;
 import org.apache.sis.metadata.iso.DefaultMetadata;
+import org.apache.sis.temporal.LenientDateFormat;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.pending.jdk.JDK18;
 
 
 /**
- * An Image File Directory (FID) in a TIFF image.
+ * An Image File Directory (FID) in a <abbr>TIFF</abbr> image.
  *
  * <h2>Thread-safety</h2>
  * Public methods should be synchronized because they can be invoked directly 
by users.
@@ -160,8 +163,6 @@ final class ImageFileDirectory extends DataCube {
      *
      * <p><b>Note:</b>
      * the {@link #imageHeight} attribute is named {@code ImageLength} in TIFF 
specification.</p>
-     *
-     * @see #getExtent()
      */
     private long imageWidth = -1, imageHeight = -1;
 
@@ -386,6 +387,15 @@ final class ImageFileDirectory extends DataCube {
      */
     private Predictor predictor;
 
+    /**
+     * The date/time found in the {@code DATE_TIME} tag, or {@code null} if 
none.
+     * According the <abbr>TIFF</abbr> specification, this is the image 
creation date.
+     * But the <abbr>DGIWG</abbr> specification reinterprets that information 
as the time when
+     * the imagery values were collected, which is a point on the 
<abbr>CRS</abbr> temporal axis.
+     * However, we will use this information that way only if {@link 
#referencing} is non-null.
+     */
+    public Instant imageDate;
+
     /**
      * A helper class for building Coordinate Reference System and complete 
related metadata.
      * Contains the following information:
@@ -415,7 +425,7 @@ final class ImageFileDirectory extends DataCube {
     }
 
     /**
-     * The grid geometry created by {@link GridGeometryBuilder#build(Reader, 
long, long)}.
+     * The grid geometry created by {@link 
GridGeometryBuilder#build(StoreListeners, long, long, Instant)}.
      * It has 2 or 3 dimensions, depending on whether the CRS declares a 
vertical axis or not.
      *
      * @see #getGridGeometry()
@@ -524,7 +534,7 @@ final class ImageFileDirectory extends DataCube {
      * @param  count  the number of values to read.
      * @return {@code null} on success, or the unrecognized value otherwise.
      * @throws IOException if an error occurred while reading the stream.
-     * @throws ParseException if the value need to be parsed as date and the 
parsing failed.
+     * @throws DateTimeParseException if the value need to be parsed as date 
and the parsing failed.
      * @throws NumberFormatException if the value need to be parsed as number 
and the parsing failed.
      * @throws ArithmeticException if the value cannot be represented in the 
expected Java type.
      * @throws IllegalArgumentException if a value which was expected to be a 
singleton is not.
@@ -1009,13 +1019,25 @@ final class ImageFileDirectory extends DataCube {
             }
             /*
              * Date and time of image creation. The format is: "YYYY:MM:DD 
HH:MM:SS" with 24-hour clock.
+             * The <abbr>DGIWG</abbr> specification requires that all 
date/time stamps are expressed in
+             * Coordinated Universal Time (UTC), and that they represent the 
date/time when the imagery
+             * values were collected.
              *
              * Destination: metadata/identificationInfo/citation/date
+             *        Also: gridGeometry/gridToCRS
              */
             case TAG_DATE_TIME: {
-                for (final String value : type.readAsStrings(input(), count, 
encoding())) {
-                    
metadata.addCitationDate(reader.store.getDateFormat().parse(value).toInstant(),
-                            DateType.CREATION, 
ImageMetadataBuilder.Scope.RESOURCE);
+                for (String value : type.readAsStrings(input(), count, 
encoding())) {
+                    // The presence of a letter would be an invalid date 
according TIFF and DGIWG specifications.
+                    // If present anyway, parse as an ISO date. If failure, 
warning will be logged by the caller.
+                    if (!CharSequences.isUpperCase(value)) {
+                        final String[] parts = (String[]) 
CharSequences.split(value, ' ');
+                        if (parts.length == 2) {
+                            value = parts[0].replace(':', '-') + 'T' + 
parts[1] + 'Z';
+                        }
+                    }
+                    imageDate = LenientDateFormat.parseInstantUTC(value);
+                    metadata.addCitationDate(imageDate, DateType.CREATION, 
ImageMetadataBuilder.Scope.RESOURCE);
                 }
                 break;
             }
@@ -1450,22 +1472,45 @@ final class ImageFileDirectory extends DataCube {
     }
 
     /**
-     * If this IFD has no grid geometry information, derives a grid geometry 
by applying a scale factor
-     * on the grid geometry of another IFD. Information about bands are also 
copied if compatible.
+     * If this <abbr>IFD</abbr> has no grid geometry, derives this information
+     * by scaling the grid geometry of the specified image at full resolution.
+     * Information about bands are also copied if compatible. The scale 
factors are returned,
+     * with the scale of the temporal dimension defined to 1 for telling that 
the time does not change.
+     *
+     * <h4>Conditions</h4>
      * This method should be invoked only when {@link #isReducedResolution()} 
is {@code true}.
      *
      * @param  fullResolution  the full-resolution image.
-     * @param  scales  <var>size of full resolution image</var> / <var>size of 
this image</var> for each grid axis.
-     */
-    final void initReducedResolution(final ImageFileDirectory fullResolution, 
final double[] scales)
-            throws DataStoreException, TransformException
-    {
+     * @return <var>size of full resolution image</var> / <var>size of this 
image</var> for each grid axis.
+     */
+    final double[] initReducedResolution(final ImageFileDirectory 
fullResolution) throws DataStoreException, TransformException {
+        final GridGeometry geometry = fullResolution.getGridGeometry();
+        final GridExtent fullExtent = geometry.getExtent();
+        final int dimension = fullExtent.getDimension();
+        final var axisTypes = new DimensionNameType[dimension];
+        final var scales    = new double[dimension];
+        final var high      = new long[dimension];
+        for (int i=0; i<dimension; i++) {
+            axisTypes[i] = fullExtent.getAxisType(i).orElse(null);
+            final long size;
+            switch (i) {
+                case 0:  size = imageWidth;  break;
+                case 1:  size = imageHeight; break;
+                default: scales[i] = 1; continue;
+            }
+            scales[i] = fullExtent.getSize(i, false) / size;
+            high[i] = size - 1;
+        }
         if (referencing == null) {
-            gridGeometry = new GridGeometry(fullResolution.getGridGeometry(), 
getExtent(), MathTransforms.scale(scales));
+            gridGeometry = new GridGeometry(
+                    geometry,
+                    new GridExtent(axisTypes, null, high, true),
+                    MathTransforms.scale(scales));
         }
         if (samplesPerPixel == fullResolution.samplesPerPixel) {
             sampleDimensions = fullResolution.getSampleDimensions();
         }
+        return scales;
     }
 
     /**
@@ -1483,7 +1528,6 @@ final class ImageFileDirectory extends DataCube {
      * <h4>Thread-safety</h4>
      * This method must be thread-safe because it can be invoked directly by 
the user.
      *
-     * @see #getExtent()
      * @see #getTileSize()
      */
     @Override
@@ -1492,12 +1536,12 @@ final class ImageFileDirectory extends DataCube {
             GridGeometry domain = gridGeometry;
             if (domain == null) {
                 if (referencing != null) try {
-                    domain = referencing.build(reader.store.listeners(), 
imageWidth, imageHeight);
+                    domain = referencing.build(reader.store.listeners(), 
imageWidth, imageHeight, imageDate);
                 } catch (FactoryException e) {
                     throw new 
DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1,
 filename()), e);
                 } else {
                     // Fallback if the TIFF file has no GeoKeys.
-                    domain = new GridGeometry(getExtent(), null, null);
+                    domain = new GridGeometry(new GridExtent(imageWidth, 
imageHeight), null, null);
                 }
                 final CoverageModifier.Source source = source();
                 gridGeometry = (source != null) ? 
reader.store.customizer.customize(source, domain) : domain;
@@ -1506,16 +1550,6 @@ final class ImageFileDirectory extends DataCube {
         }
     }
 
-    /**
-     * Returns the image width and height without building the full grid 
geometry.
-     *
-     * @see #getTileSize()
-     * @see #getGridGeometry()
-     */
-    final GridExtent getExtent() {
-        return new GridExtent(imageWidth, imageHeight);
-    }
-
     /**
      * Returns the minimum and maximum non-fill values in the specified band.
      * This is the values explicitly defined in <abbr>TIFF</abbr> tags if 
present,
@@ -1709,7 +1743,6 @@ final class ImageFileDirectory extends DataCube {
      * Returns the size of tiles. This is also the size of the image sample 
model.
      * The number of dimensions is always 2 for {@code ImageFileDirectory}.
      *
-     * @see #getExtent()
      * @see #getSampleModel(int[])
      */
     @Override
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
index 097b4bf978..75792a41a8 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
@@ -40,6 +40,7 @@ import org.apache.sis.storage.base.GridResourceWrapper;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.internal.shared.DirectPositionView;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import static 
org.apache.sis.storage.geotiff.reader.GridGeometryBuilder.BIDIMENSIONAL;
 
 
 /**
@@ -150,7 +151,9 @@ final class MultiResolutionImage extends 
GridResourceWrapper implements StoreRes
     }
 
     /**
-     * Returns the resolution (in units of CRS axes) for the given level.
+     * Returns the resolution (in units of <abbr>CRS</abbr> axes) for the 
given level.
+     * If there is a temporal dimension, its resolution is set to NaN because 
we don't
+     * know the duration.
      *
      * @param  level  the desired resolution level, numbered from finest to 
coarsest resolution.
      * @return resolution at the specified level, not cloned (caller shall not 
modify).
@@ -158,17 +161,12 @@ final class MultiResolutionImage extends 
GridResourceWrapper implements StoreRes
     private double[] resolution(final int level) throws DataStoreException {
         double[] resolution = resolutions[level];
         if (resolution == null) try {
-            final ImageFileDirectory image      = getImageFileDirectory(level);
-            final ImageFileDirectory base       = getImageFileDirectory(0);
-            final GridGeometry       geometry   = base.getGridGeometry();
-            final GridExtent         fullExtent = geometry.getExtent();
-            final GridExtent         subExtent  = image.getExtent();
-            final double[] scales = new double[fullExtent.getDimension()];
-            for (int i=0; i<scales.length; i++) {
-                scales[i] = fullExtent.getSize(i, false) / 
subExtent.getSize(i, false);
-            }
-            image.initReducedResolution(base, scales);
+            final ImageFileDirectory image = getImageFileDirectory(level);
+            final ImageFileDirectory base  = getImageFileDirectory(0);
+            final double[] scales = image.initReducedResolution(base);
+            final GridGeometry geometry = base.getGridGeometry();
             if (geometry.isDefined(GridGeometry.GRID_TO_CRS)) {
+                final GridExtent fullExtent = geometry.getExtent();
                 DirectPosition poi = new 
DirectPositionView.Double(fullExtent.getPointOfInterest(PixelInCell.CELL_CENTER));
                 MatrixSIS gridToCRS = 
MatrixSIS.castOrCopy(geometry.getGridToCRS(PixelInCell.CELL_CENTER).derivative(poi));
                 resolution = gridToCRS.multiply(scales);
@@ -176,7 +174,10 @@ final class MultiResolutionImage extends 
GridResourceWrapper implements StoreRes
                 // Assume an identity transform for the `gridToCRS` of full 
resolution image.
                 resolution = scales;
             }
-            for (int i=0; i<resolution.length; i++) {
+            // Set to NaN only after all matrix multiplications are done.
+            int i = Math.min(BIDIMENSIONAL, resolution.length);
+            Arrays.fill(scales, BIDIMENSIONAL, i, Double.NaN);
+            while (--i >= 0) {
                 resolution[i] = Math.abs(resolution[i]);
             }
             resolutions[level] = resolution;
@@ -257,7 +258,7 @@ final class MultiResolutionImage extends 
GridResourceWrapper implements StoreRes
         synchronized (getSynchronizationLock()) {
 finer:      while (--level > 0) {
                 final double[] resolution = resolution(level);
-                for (int i=0; i<request.length; i++) {
+                for (int i = Math.min(request.length, BIDIMENSIONAL); --i >= 
0;) {
                     if (!(request[i] >= resolution[i])) {            // Use 
`!` for catching NaN.
                         continue finer;
                     }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 3926b00baf..f3aba2e396 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -21,7 +21,6 @@ import java.io.IOException;
 import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
 import java.util.ArrayDeque;
 import java.util.List;
 import java.util.Deque;
@@ -38,7 +37,6 @@ import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
 import javax.measure.IncommensurableException;
 import org.opengis.util.FactoryException;
 import org.opengis.metadata.Metadata;
-import org.opengis.metadata.citation.CitationDate;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.image.DataType;
@@ -55,8 +53,8 @@ import org.apache.sis.storage.geotiff.writer.GeoEncoder;
 import org.apache.sis.storage.geotiff.writer.ReformattedImage;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.UpdatableWrite;
-import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Fraction;
@@ -298,6 +296,7 @@ final class Writer extends IOBase implements Flushable {
      * @throws DataStoreException if the given {@code image} has a property
      *         which is not supported by TIFF specification or by this writer.
      */
+    @SuppressWarnings("UseSpecificCatch")
     public final long append(final RenderedImage image, final GridGeometry 
grid, final Metadata metadata)
             throws IOException, DataStoreException
     {
@@ -395,23 +394,18 @@ final class Writer extends IOBase implements Flushable {
         }
         /*
          * Metadata (optional) and GeoTIFF. They are managed by separated 
classes.
+         * Note that `TAG_DATE_TIME` has slightly different interpretation 
depending
+         * on whether we are writing GeoTIFF (`geoKeys != null`) or plain TIFF.
          */
         final double[][] statistics = image.statistics(numBands);
         final  short[][] shortStats = toShorts(statistics, sampleFormat);
-        final MetadataFetcher<String> mf = new 
MetadataFetcher<>(store.dataLocale) {
-            @Override protected boolean accept(final CitationDate info) {
-                return super.accept(info) || creationDate != null;          // 
Limit to a singleton.
-            }
-
-            @Override protected String convertDate(final Date date) {
-                return store.getDateFormat().format(date);
-            }
-        };
+        final var mf = new MetadataFetcher(store.dataLocale);
         mf.accept(metadata);
         GeoEncoder geoKeys = null;
         if (grid != null) try {
             geoKeys = new GeoEncoder(store.listeners());
             geoKeys.write(grid, mf);
+            mf.creationDate = geoKeys.imageDate();  // Unconditional, even if 
the list of empty.
         } catch (IncompleteGridGeometryException | CannotEvaluateException | 
TransformException e) {
             throw new IncompatibleResourceException(e.getMessage(), 
e).addAspect("gridGeometry");
         } catch (FactoryException | IncommensurableException | 
RuntimeException e) {
@@ -460,7 +454,7 @@ final class Writer extends IOBase implements Flushable {
         writeTag((short) TAG_PLANAR_CONFIGURATION,       (short) 
TIFFTag.TIFF_SHORT, planarConfiguration);
         writeTag((short) TAG_RESOLUTION_UNIT,            (short) 
TIFFTag.TIFF_SHORT, RESOLUTION_UNIT_NONE);
         writeTag((short) TAG_SOFTWARE,                   /* TIFF_ASCII */      
      mf.software);
-        writeTag((short) TAG_DATE_TIME,                  /* TIFF_ASCII */      
      mf.creationDate);
+        writeTag((short) TAG_DATE_TIME,                  /* TIFF_ASCII */      
      GeoEncoder.creationDates(mf.creationDate));
         writeTag((short) TAG_ARTIST,                     /* TIFF_ASCII */      
      mf.party);
         writeTag((short) TAG_HOST_COMPUTER,              /* TIFF_ASCII */      
      mf.procedure);
         if (compression.usePredictor()) {
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
index 50eb9c9f38..426a5588eb 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
@@ -52,7 +52,7 @@ public final class Tags {
 
     /**
      * Embedded XML-encoded instance documents prepared using 19139-based 
schema.
-     * This is an OGC DGIWG extension tag.
+     * This is an <abbr>OGC</abbr> <abbr>DGIWG</abbr> extension tag.
      */
     public static final short GEO_METADATA = (short) 0xC6DD;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java
index d5d2f35134..603b212edc 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java
@@ -33,7 +33,7 @@
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Minh Chinh Vu (VNSC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.8
  */
 package org.apache.sis.storage.geotiff;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
index 7900e1de85..4604c24a3a 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
@@ -82,7 +82,6 @@ import org.apache.sis.util.internal.shared.Strings;
 import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.storage.event.StoreListeners;
@@ -179,14 +178,14 @@ public final class CRSBuilder extends 
ReferencingFactoryContainer {
 
     /**
      * Suggested value for a general description of the transformation form 
grid coordinates to "real world" coordinates.
-     * This is computed by {@link #build(Vector, Vector, String)} and made 
available as additional information to the caller.
+     * This is computed by {@link #build(GeoKeysLoader)} and made available as 
additional information to the caller.
      */
     public String description;
 
     /**
      * {@code POINT} if {@link GeoKeys#RasterType} is {@link 
GeoCodes#RasterPixelIsPoint},
      * {@code AREA} if it is {@link GeoCodes#RasterPixelIsArea}, or null if 
unspecified.
-     * This is computed by {@link #build(Vector, Vector, String)} and made 
available to the caller.
+     * This is computed by {@link #build(GeoKeysLoader)} and made available to 
the caller.
      */
     public CellGeometry cellGeometry;
 
@@ -512,6 +511,7 @@ public final class CRSBuilder extends 
ReferencingFactoryContainer {
      * @throws ClassCastException if an object defined by an EPSG code is not 
of the expected type.
      * @throws FactoryException if an error occurred during objects creation 
with the factories.
      */
+    @SuppressWarnings("UseSpecificCatch")
     public CoordinateReferenceSystem build(final GeoKeysLoader source) throws 
FactoryException {
         try {
             source.logger = this;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
index 68d704b4f8..ec1d0d93ed 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.storage.geotiff.reader;
 
+import java.time.Instant;
 import java.util.NoSuchElementException;
 import org.opengis.util.FactoryException;
 import org.opengis.util.NoSuchIdentifierException;
@@ -25,12 +26,17 @@ import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.crs.DefaultTemporalCRS;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.event.StoreListeners;
@@ -78,13 +84,9 @@ import org.apache.sis.math.Vector;
  */
 public final class GridGeometryBuilder extends GeoKeysLoader {
     /**
-     * Default scale factory to apply if a row in the model transformation 
contains only zero values.
-     * The matrix in a GeoTIFF file is always of size 4×4 even if the 
<abbr>CRS</abbr> is two-dimensional.
-     * In the latter case, the matrix row for the third dimension has only 
zero values. That row should be
-     * discarded when building the final {@link GridGeometry}, but there is 
sometime inconsistency between
-     * the number of <abbr>CRS</abbr> dimensions and which matrix rows have 
been assigned non-zero values.
+     * Number of dimensions of the horizontal part.
      */
-    private static final double DEFAULT_SCALE_FACTOR = 1;
+    public static final int BIDIMENSIONAL = 2;
 
     //  
╔════════════════════════════════════════════════════════════════════════════════╗
     //  ║                                                                      
          ║
@@ -108,7 +110,7 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
     public Vector modelTiePoints;
 
     /**
-     * The conversion from grid coordinates to CRS coordinates as an affine 
transform.
+     * The conversion from grid coordinates to <abbr>CRS</abbr> coordinates as 
an affine transform.
      * The "grid to CRS" transform can be determined in different ways, from 
simpler to more complex:
      *
      * <ul>
@@ -174,14 +176,18 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
 
     /**
      * Suggested value for a general description of the transformation form 
grid coordinates to "real world" coordinates.
-     * This information is obtained as a side-effect of {@link 
#build(StoreListeners, long, long)} call.
+     * This information is obtained as a side-effect of {@link 
#build(StoreListeners, long, long, Instant)} call.
+     *
+     * @see #completeMetadata(GridGeometry, MetadataBuilder)
      */
     private String description;
 
     /**
      * {@code POINT} if {@link GeoKeys#RasterType} is {@link 
GeoCodes#RasterPixelIsPoint},
      * {@code AREA} if it is {@link GeoCodes#RasterPixelIsArea}, or null if 
unspecified.
-     * This information is obtained as a side-effect of {@link 
#build(StoreListeners, long, long)} call.
+     * This information is obtained as a side-effect of {@link 
#build(StoreListeners, long, long, Instant)} call.
+     *
+     * @see #completeMetadata(GridGeometry, MetadataBuilder)
      */
     private CellGeometry cellGeometry;
 
@@ -236,7 +242,7 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
              * Grid to CRS conversion:  crs = grid × scale + translation
              * We rearrange as:         translation = crs - grid × scale
              * where:                   grid   =  modelTiePoints[i]
-             *                          crs    =  modelTiePoints[i + 
RECORD_LENGTH/2]
+             *                          crs    =  modelTiePoints[i + 
RECORD_LENGTH / BIDIMENSIONAL]
              *                          scale  =  affine(i,i)  —  on the 
diagonal
              */
             if (distance != Double.POSITIVE_INFINITY) {
@@ -245,7 +251,7 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
                 final int trCol  = affine.getNumCol() - 1;
                 for (int j=0; j<numDim; j++) {
                     final double src = -modelTiePoints.doubleValue(nearest + 
j);
-                    final double tgt =  modelTiePoints.doubleValue(nearest + j 
+ Localization.RECORD_LENGTH / 2);
+                    final double tgt =  modelTiePoints.doubleValue(nearest + j 
+ Localization.RECORD_LENGTH / BIDIMENSIONAL);
                     var t = DoubleDouble.of(src, 
decimal).multiply(affine.getNumber(j,j), decimal).add(tgt, decimal);
                     affine.setNumber(j, trCol, t);
                 }
@@ -264,14 +270,17 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
      * @param  listeners  the listeners where to report warnings.
      * @param  width      the image width in pixels.
      * @param  height     the image height in pixels.
+     * @param  imageDate  the date/time found in the {@code DATE_TIME} tag, or 
{@code null} if none.
      * @return the grid geometry, guaranteed non-null.
      * @throws FactoryException if an error occurred while creating a CRS or a 
transform.
      */
     @SuppressWarnings("fallthrough")
-    public GridGeometry build(final StoreListeners listeners, final long 
width, final long height) throws FactoryException {
+    public GridGeometry build(final StoreListeners listeners, final long 
width, final long height, final Instant imageDate)
+            throws FactoryException
+    {
         CoordinateReferenceSystem crs = null;
         if (keyDirectory != null) {
-            final CRSBuilder helper = new CRSBuilder(listeners);
+            final var helper = new CRSBuilder(listeners);
             try {
                 crs = helper.build(this);
                 description  = helper.description;
@@ -289,47 +298,90 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
             }
         }
         /*
-         * If the CRS is non-null, then it is either two- or three-dimensional.
-         * The `affine` matrix may be for a greater number of dimensions, so it
-         * may need to be reduced.
+         * If the CRS is non-null, then the spatial part is either two- or 
three-dimensional.
+         * A temporal axis may be added to the non-null CRS.
          */
-        int n = (crs != null) ? crs.getCoordinateSystem().getDimension() : 2;
-        final var axisTypes = new DimensionNameType[n];
-        final var high = new long[n];
-        switch (n) {
+        final double timeCoordinate;
+        final TemporalCRS temporalCRS;
+        final int spatialDimension = (crs != null) ? 
crs.getCoordinateSystem().getDimension() : BIDIMENSIONAL;
+        final int dimension;
+        if (imageDate != null) {
+            dimension = spatialDimension + 1;
+            temporalCRS = CommonCRS.defaultTemporal();
+            timeCoordinate = 
DefaultTemporalCRS.castOrCopy(temporalCRS).toValue(imageDate);
+            if (crs != null) {
+                crs = CRS.compound(crs, temporalCRS);
+            }
+        } else {
+            dimension = spatialDimension;
+            timeCoordinate = Double.NaN;
+            temporalCRS = null;
+        }
+        final var axisTypes = new DimensionNameType[dimension];
+        final var high = new long[dimension];
+        if (temporalCRS != null) {
+            axisTypes[spatialDimension] = DimensionNameType.TIME;
+        }
+        switch (spatialDimension) {
             default: axisTypes[2] = DimensionNameType.VERTICAL; // Fallthrough 
everywhere.
             case 2:  axisTypes[1] = DimensionNameType.ROW;      high[1] = 
height - 1;
             case 1:  axisTypes[0] = DimensionNameType.COLUMN;   high[0] = 
width  - 1;
             case 0:  break;
         }
         final var extent = new GridExtent(axisTypes, null, high, true);
-        boolean pixelIsPoint = (cellGeometry == CellGeometry.POINT);
         final MathTransformFactory factory = 
DefaultMathTransformFactory.provider();
+        PixelInCell anchor = (cellGeometry == CellGeometry.POINT) ? 
PixelInCell.CELL_CENTER : PixelInCell.CELL_CORNER;
         GridGeometry gridGeometry;
         try {
             MathTransform gridToCRS = null;
             if (affine != null) {
-                final Matrix m = Matrices.resizeAffine(affine, ++n, n);
-                Matrices.forceNonZeroScales(m, DEFAULT_SCALE_FACTOR);
+                /*
+                 * The `affine` matrix is always 4×4 in a GeoTIFF file, which 
may be larger than requested.
+                 * Resize the matrix to the size that we need. Maybe the last 
dimension, initially ignored,
+                 * become used by the temporal dimension, so we need to clear 
that dimension for safety.
+                 */
+                final Matrix m = Matrices.resizeAffine(affine, dimension + 1, 
dimension + 1);
+                if (temporalCRS != null) {
+                    for (int i=0; i <= dimension; i++) {
+                        m.setElement(spatialDimension, i, 0);
+                        m.setElement(i, spatialDimension, 0);
+                    }
+                    m.setElement(spatialDimension, dimension, timeCoordinate);
+                    m.setElement(spatialDimension, spatialDimension, 
Double.NaN);   // Unknown duration.
+                }
+                /*
+                 * If the CRS has no vertical component, then the matrix row 
and column for the vertical coordinates
+                 * should be ignored. However, we observed inconsistency in 
some GeoTIFF files between the number of
+                 * CRS dimensions and the matrix rows which have been assigned 
non-zero values. Because rows of only
+                 * zero values cause problems, we assign a NaN value in one of 
their columns.
+                 */
+                Matrices.forceNonZeroScales(m, Double.NaN);
                 gridToCRS = factory.createAffineTransform(m);
             } else if (modelTiePoints != null) {
-                pixelIsPoint = true;
+                anchor    = PixelInCell.CELL_CENTER;
                 gridToCRS = Localization.nonLinear(modelTiePoints);
-                gridToCRS = factory.createPassThroughTransform(0, gridToCRS, n 
- 2);
+                gridToCRS = factory.createPassThroughTransform(0, gridToCRS, 
spatialDimension - BIDIMENSIONAL);
+                if (temporalCRS != null) {
+                    gridToCRS = MathTransforms.compound(gridToCRS, 
MathTransforms.linear(Double.NaN, timeCoordinate));
+                }
             }
-            gridGeometry = new GridGeometry(extent, pixelIsPoint ? 
PixelInCell.CELL_CENTER : PixelInCell.CELL_CORNER, gridToCRS, crs);
+            gridGeometry = new GridGeometry(extent, anchor, gridToCRS, crs);
         } catch (TransformException e) {
+            /*
+             * Note: we catch TransformExceptions because they may be caused 
by erroneous data in the GeoTIFF file,
+             * but let FactoryExceptions propagate because they are more 
likely to be a SIS configuration problem.
+             */
             GeneralEnvelope envelope = null;
             if (crs != null) {
                 envelope = new GeneralEnvelope(crs);
                 envelope.setToNaN();
+                if (temporalCRS != null && anchor == PixelInCell.CELL_CENTER) {
+                    // The coordinate is the lower value (start) of the time 
range.
+                    envelope.setRange(spatialDimension, timeCoordinate, 
Double.NaN);
+                }
             }
-            gridGeometry = new GridGeometry(extent, envelope, 
GridOrientation.HOMOTHETY);
+            gridGeometry = new GridGeometry(extent, envelope, 
GridOrientation.UNKNOWN);
             canNotCreate(listeners, e);
-            /*
-             * Note: we catch TransformExceptions because they may be caused 
by erroneous data in the GeoTIFF file,
-             * but let FactoryExceptions propagate because they are more 
likely to be a SIS configuration problem.
-             */
         }
         keyDirectory      = null;            // Not needed anymore, so let GC 
do its work.
         numericParameters = null;
@@ -344,7 +396,7 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
      *
      * <h4>Prerequisite</h4>
      * <ul>
-     *   <li>{@link #build(StoreListeners, long, long)} must have been invoked 
successfully before this method.</li>
+     *   <li>{@link #build(StoreListeners, long, long, Instant)} must have 
been invoked successfully before this method.</li>
      *   <li>{@link ImageFileDirectory} must have filled its part of metadata 
before to invoke this method.</li>
      * </ul>
      *
@@ -358,7 +410,7 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
      *   <li>{@code metadata/referenceSystemInfo}</li>
      * </ul>
      *
-     * @param  gridGeometry  the grid geometry computed by {@link 
#build(StoreListeners, long, long)}.
+     * @param  gridGeometry  the grid geometry computed by {@link 
#build(StoreListeners, long, long, Instant)}.
      * @param  metadata      the helper class where to write metadata values.
      * @throws NumberFormatException if a numeric value was stored as a string 
and cannot be parsed.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
index 093f27c106..89532b5401 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
@@ -615,6 +615,8 @@ public enum Type {
 
     /**
      * Reads the value as strings. There is usually exactly one string, but an 
arbitrary amount is allowed.
+     * The default implementation assumes that the vector contains numerical 
data, which is the case of all
+     * types except {@link #ASCII}. This method is overridden for handling any 
text in the {@code ASCII} case.
      *
      * @param  input    the input from where to read the value.
      * @param  length   the string length, including the final NUL byte.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
index 7a5a64e482..9daaeb1969 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
@@ -76,7 +76,7 @@ public final class XMLMetadata implements Filter {
 
     /**
      * The bytes to decode as an XML document.
-     * DGIWG specification mandates UTF-8 encoding.
+     * <abbr>DGIWG</abbr> specification mandates UTF-8 encoding.
      */
     private byte[] bytes;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
index 03b2f6b0d7..12f9f057e7 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
@@ -20,6 +20,10 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.EnumMap;
 import java.util.logging.Level;
+import java.time.Instant;
+import java.time.Duration;
+import java.time.temporal.Temporal;
+import java.time.format.DateTimeFormatter;
 import static javax.imageio.plugins.tiff.GeoTIFFTagSet.TAG_GEO_ASCII_PARAMS;
 import static javax.imageio.plugins.tiff.GeoTIFFTagSet.TAG_GEO_DOUBLE_PARAMS;
 import javax.measure.Unit;
@@ -55,6 +59,7 @@ import org.opengis.parameter.ParameterValue;
 import org.apache.sis.measure.Units;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.StringBuilders;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.internal.shared.Strings;
 import org.apache.sis.util.internal.shared.CollectionsExt;
@@ -120,6 +125,14 @@ public final class GeoEncoder {
      */
     private String citation;
 
+    /**
+     * The temporal coordinate of the image, or {@code null} if none.
+     * This is extracted from the temporal dimension of the grid geometry.
+     *
+     * @see #imageDate()
+     */
+    private Instant imageDate;
+
     /**
      * Whether the map projection is a pseudo-projection. The latter has no 
GeoTIFF code.
      * Therefore, they need to be replaced by the non-pseudo variant with 
adjustments of
@@ -257,7 +270,7 @@ public final class GeoEncoder {
      * @throws IncompleteGridGeometryException if the grid geometry is 
incomplete.
      * @throws IncompatibleResourceException if the grid geometry cannot be 
encoded.
      */
-    public void write(GridGeometry grid, final MetadataFetcher<?> metadata)
+    public void write(GridGeometry grid, final MetadataFetcher metadata)
             throws FactoryException, TransformException, 
IncommensurableException, IncompatibleResourceException
     {
         grid = grid.shiftGridToZeros();
@@ -265,8 +278,8 @@ public final class GeoEncoder {
         isPoint  = CollectionsExt.first(metadata.cellGeometry) == 
CellGeometry.POINT;
         final var anchor = isPoint ? PixelInCell.CELL_CENTER : 
PixelInCell.CELL_CORNER;
         /*
-         * Get the dimension indices of the two-dimensional slice to write. 
They should be the first dimensions,
-         * but we allow those dimensions to appear elsewhere.
+         * Get the dimension indices of the two-dimensional slice to write.
+         * They should be the first dimensions, but we allow those dimensions 
to appear elsewhere.
          */
         final int[] dimensions = 
grid.getExtent().getSubspaceDimensions(BIDIMENSIONAL);
         final GridGeometry horizontal = grid.selectDimensions(dimensions);
@@ -276,6 +289,20 @@ public final class GeoEncoder {
                 String message = 
resources().getString(Resources.Keys.CanNotEncodeNonLinearModel);
                 throw new 
IncompatibleResourceException(message).addAspect("gridToCRS");
             }
+            /*
+             * Extract the temporal coordinate. This information is stored for 
restitution by the
+             * `imageDate()` method. This information, even absent, shall 
unconditionally replace
+             * the information obtained from metadata for avoiding 
misinterpretation at read time.
+             */
+            final Instant[] time = grid.getTemporalExtent();
+            if (time.length != 0) {
+                imageDate = time[0];
+                if (isPoint && time.length > 1) {
+                    // TODO: replace by the following when allowed to compile 
for JDK23:
+                //  imageDate = 
imageDate.plus(imageDate.until(t[1]).dividedBy(2));
+                    imageDate = imageDate.plus(Duration.between(imageDate, 
time[1]).dividedBy(2));
+                }
+            }
         }
         /*
          * Write the horizontal component of the CRS. We need to take the CRS
@@ -861,6 +888,45 @@ public final class GeoEncoder {
         return JDK15.isEmpty(asciiParams) ? null : 
List.of(asciiParams.toString());
     }
 
+    /**
+     * Formats the given date in the way that it should be encoded in the 
{@code DATE_TIME} tag.
+     * According the <abbr>TIFF</abbr> specification, this tag contains the 
image creation date.
+     * But the <abbr>DGIWG</abbr> specification reinterprets that information 
as the time when
+     * the imagery values were collected, which is a point on the 
<abbr>CRS</abbr> temporal axis.
+     *
+     * <p>The list given to this method should be the list returned by {@link 
#imageDate()} when
+     * the image contains spatiotemporal referencing information. But the 
argument may also be a
+     * list obtained from other source when the image to write has no 
spatiotemporal coordinates,
+     * in which case we are writing a plain <abbr>TIFF</abbr> image and the 
date may be obtained
+     * from ISO 19115 metadata.</p>
+     *
+     * @param  imageDate  the date to encode, or {@code null} or empty if none.
+     * @return the first date encoded as a string, or {@code null} if none.
+     */
+    public static List<String> creationDates(final List<Temporal> imageDate) {
+        if (imageDate == null || imageDate.isEmpty()) {
+            return null;
+        }
+        var value = new 
StringBuilder(DateTimeFormatter.ISO_INSTANT.format(imageDate.get(0)));
+        int s = value.lastIndexOf(".");
+        if (s >= 0) value.setLength(s);
+        StringBuilders.replace(value, "-", ":");
+        StringBuilders.replace(value, "T", " ");
+        StringBuilders.replace(value, "Z", "");
+        return List.of(value.toString());
+    }
+
+    /**
+     * Returns the temporal coordinate of the image, or an empty list if none.
+     * This information shall unconditionally replace the information obtained 
from metadata, even when
+     * the list is empty, for avoiding misinterpretation of the spatiotemporal 
location at reading time.
+     *
+     * @return the temporal coordinate of the image as a list of 0 or 1 
element.
+     */
+    public List<Temporal> imageDate() {
+        return (imageDate != null) ? List.of(imageDate) : List.of();
+    }
+
     /**
      * Returns the coefficients of the affine transform, or {@code null} if 
none.
      * Array length is fixed to 16 elements, for a 4×4 matrix in row-major 
order.
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java 
b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java
index c34df3d4f4..bb0104a6d9 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java
@@ -42,7 +42,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
  * @author  Guilhem Legal (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   1.0
  */
 module org.apache.sis.storage.sql {
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
index ec2b149779..c764390369 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java
@@ -56,7 +56,7 @@
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   1.0
  */
 package org.apache.sis.storage.sql;
diff --git a/endorsed/src/org.apache.sis.storage/main/module-info.java 
b/endorsed/src/org.apache.sis.storage/main/module-info.java
index 527a1e2d5e..8b7fe8b1c5 100644
--- a/endorsed/src/org.apache.sis.storage/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage/main/module-info.java
@@ -20,7 +20,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 module org.apache.sis.storage {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java
index 22b3c11e6b..5de1f71862 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/CoverageAggregator.java
@@ -370,7 +370,7 @@ search: synchronized (members) {
          * but a future version may use the state of this 
`CoverageAggregator`, for example making a better
          * effort to align the resources on the same "gridToCRS" transform.
          */
-        final var crs = 
DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.TRUNCATED_JULIAN.crs());
+        final var crs = 
DefaultTemporalCRS.castOrCopy(CommonCRS.defaultTemporal());
         double scale  = crs.toValue(span);
         double offset = crs.toValue(lower);
         long   index  = Numerics.roundAndClamp(offset / scale);             // 
See comment in above method.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
index b9d249cb25..02d0a6715a 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
@@ -17,7 +17,6 @@
 package org.apache.sis.storage.base;
 
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -60,10 +59,8 @@ import org.opengis.metadata.citation.Responsibility;
  * API of this class may change in any future SIS versions.
  *
  * @author  Martin Desruisseaux (Geomatys)
- *
- * @param  <T>  type of temporal objects.
  */
-public abstract class MetadataFetcher<T> {
+public class MetadataFetcher {
     /**
      * Types of date to accept as a date of last update, in preference order.
      */
@@ -112,7 +109,7 @@ public abstract class MetadataFetcher<T> {
      *
      * <p>Path: {@code metadata/identificationInfo/citation/date}</p>
      */
-    public List<T> creationDate;
+    public List<Temporal> creationDate;
 
     /**
      * Dates of the last update, or {@code null} if none.
@@ -121,7 +118,7 @@ public abstract class MetadataFetcher<T> {
      *
      * @see #lastUpdate(Metadata)
      */
-    public List<T> lastUpdate;
+    public List<Temporal> lastUpdate;
 
     /**
      * Type of the {@link #lastUpdate} values as an index in the {@link 
#LAST_UPDATE_TYPES} array.
@@ -186,7 +183,7 @@ public abstract class MetadataFetcher<T> {
      * @param  accept    the method to invoke for each element.
      * @param  elements  the collection of elements, or {@code null} if none.
      */
-    private <E> void forEach(final BiPredicate<MetadataFetcher<T>,E> accept, 
final Iterable<? extends E> elements) {
+    private <E> void forEach(final BiPredicate<MetadataFetcher, E> accept, 
final Iterable<? extends E> elements) {
         if (elements != null) {
             for (final E info : elements) {
                 if (info != null && accept.test(this, info)) break;
@@ -434,30 +431,19 @@ public abstract class MetadataFetcher<T> {
      * @param  clear   whether to clear the list before to add the date.
      * @return the collection where the date was added.
      */
-    private List<T> addDate(List<T> target, final CitationDate value, final 
boolean clear) {
-        @SuppressWarnings("deprecation")
-        final Date date = value.getDate();
+    private List<Temporal> addDate(List<Temporal> target, final CitationDate 
value, final boolean clear) {
+        final Temporal date = value.getReferenceDate();
         if (date != null) {
             if (target == null) {
                 target = new ArrayList<>(2);        // We will usually have 
only one element.
             } else if (clear) {
                 target.clear();
             }
-            target.add(convertDate(date));
+            target.add(date);
         }
         return target;
     }
 
-    /**
-     * Converts the given date into the object to store.
-     * The {@code <T>} type may be for example {@code <String>}
-     * with a string representation specified by the format implemented by the 
store.
-     *
-     * @param  date  the date to convert.
-     * @return subclass-dependent object representing the given date.
-     */
-    protected abstract T convertDate(final Date date);
-
     /**
      * Returns the first date of type {@link DateType#LAST_UPDATE}.
      * If there is no last update, then this method fallbacks on {@link 
DateType#LAST_REVISION}.
diff --git 
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
index 4d190f4c48..f0b43e0d6d 100644
--- 
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
@@ -441,7 +441,7 @@ final class Store extends URIDataStore implements 
FeatureSet {
             if (startTime != null) {
                 final TemporalCRS temporal;
                 if (isTimeAbsolute) {
-                    temporal = TimeEncoding.DEFAULT.crs();
+                    temporal = CommonCRS.defaultTemporal();
                     timeEncoding = TimeEncoding.ABSOLUTE;
                 } else {
                     temporal = builder.createTemporalCRS(startTime, timeUnit);
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
index d48618192a..bcf7fe7164 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/TimeEncoding.java
@@ -36,15 +36,10 @@ import org.apache.sis.measure.Units;
  * @author  Martin Desruisseaux (Geomatys)
  */
 class TimeEncoding extends SurjectiveConverter<String,Instant> {
-    /**
-     * The temporal coordinate reference system to use for {@link #ABSOLUTE} 
time encoding.
-     */
-    static final CommonCRS.Temporal DEFAULT = 
CommonCRS.Temporal.TRUNCATED_JULIAN;
-
     /**
      * Times are formatted as ISO dates.
      */
-    static final TimeEncoding ABSOLUTE = new TimeEncoding(DEFAULT.datum(), 
Units.DAY) {
+    static final TimeEncoding ABSOLUTE = new 
TimeEncoding(CommonCRS.defaultTemporal().getDatum(), Units.DAY) {
         @Override public Instant apply(final String time) {
             return LenientDateFormat.parseInstantUTC(time);
         }
diff --git a/endorsed/src/org.apache.sis.util/main/module-info.java 
b/endorsed/src/org.apache.sis.util/main/module-info.java
index 23e4d4def6..84025a789f 100644
--- a/endorsed/src/org.apache.sis.util/main/module-info.java
+++ b/endorsed/src/org.apache.sis.util/main/module-info.java
@@ -22,7 +22,7 @@
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 module org.apache.sis.util {
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Statistics.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Statistics.java
index e9e287a54e..677458deaf 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Statistics.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Statistics.java
@@ -391,7 +391,7 @@ public class Statistics implements DoubleConsumer, 
LongConsumer, Cloneable, Seri
     }
 
     /**
-     * Multiplies the statistics by the given factor. The given scale factory 
is also applied
+     * Multiplies the statistics by the given factor. The given scale factor 
is also applied
      * recursively on the {@linkplain #differences() differences} statistics, 
if any.
      * Invoking this method transforms the statistics as if every values given 
to the
      * {@code accept(…)} had been first multiplied by the given factor.

Reply via email to