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.
