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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new c65d0e793d Add support for `FrameEpoch` element inside the `DYNAMIC` 
element.
c65d0e793d is described below

commit c65d0e793dc9370a846f0eb3d4316a4b65dda36a
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Sep 7 19:47:27 2025 +0200

    Add support for `FrameEpoch` element inside the `DYNAMIC` element.
---
 .../sis/coordinate/DefaultCoordinateMetadata.java  |  2 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    | 63 +++++++++++++++----
 .../apache/sis/referencing/crs/AbstractCRS.java    |  7 ++-
 .../org/apache/sis/referencing/crs/DynamicCRS.java | 73 ++++++++++++++++++++++
 .../referencing/factory/sql/EPSGDataAccess.java    | 13 +---
 .../org/apache/sis/referencing/internal/Epoch.java | 65 ++++++++++++++++++-
 .../apache/sis/referencing/privy/WKTKeywords.java  |  2 +
 .../sis/io/wkt/GeodeticObjectParserTest.java       | 31 +++++++++
 .../apache/sis/referencing/internal/EpochTest.java | 21 +++++--
 9 files changed, 243 insertions(+), 34 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/coordinate/DefaultCoordinateMetadata.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/coordinate/DefaultCoordinateMetadata.java
index 1aaa840548..74911cd66b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/coordinate/DefaultCoordinateMetadata.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/coordinate/DefaultCoordinateMetadata.java
@@ -199,7 +199,7 @@ public class DefaultCoordinateMetadata extends 
FormattableObject
     protected String formatTo(final Formatter formatter) {
         formatter.append(WKTUtilities.toFormattable(crs));
         if (epoch != null) {
-            formatter.append(new Epoch(epoch));
+            formatter.append(new Epoch(epoch, false));
         }
         return WKTKeywords.CoordinateMetadata;
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 2fda55c79c..96e320971d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -32,6 +32,7 @@ import java.text.NumberFormat;
 import java.text.ParsePosition;
 import java.text.ParseException;
 import java.time.Instant;
+import java.time.temporal.Temporal;
 import static java.util.Collections.singletonMap;
 import javax.measure.Unit;
 import javax.measure.Quantity;
@@ -69,6 +70,7 @@ import 
org.apache.sis.referencing.privy.EllipsoidalHeightCombiner;
 import org.apache.sis.referencing.privy.AxisDirections;
 import org.apache.sis.referencing.privy.WKTUtilities;
 import org.apache.sis.referencing.privy.WKTKeywords;
+import org.apache.sis.referencing.internal.Epoch;
 import org.apache.sis.referencing.internal.Legacy;
 import org.apache.sis.referencing.internal.VerticalDatumTypes;
 import org.apache.sis.referencing.internal.PositionalAccuracyConstant;
@@ -310,10 +312,10 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             && null == (object = parseAxis              (FIRST, element, null, 
 Units.METRE ))
             && null == (object = parsePrimeMeridian     (FIRST, element, 
false, Units.DEGREE))
             && null == (object = parseEnsemble          (FIRST, element, 
Datum.class, greenwich()))
-            && null == (object = parseDatum             (FIRST, element, 
greenwich()))
+            && null == (object = parseDatum             (FIRST, element, 
greenwich(), null))
             && null == (object = parseEllipsoid         (FIRST, element))
             && null == (object = parseToWGS84           (FIRST, element))
-            && null == (object = parseVerticalDatum     (FIRST, element, 
false))
+            && null == (object = parseVerticalDatum     (FIRST, element, null, 
false))
             && null == (object = parseTimeDatum         (FIRST, element))
             && null == (object = parseParametricDatum   (FIRST, element))
             && null == (object = parseEngineeringDatum  (FIRST, element, 
false))
@@ -1409,6 +1411,23 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
     }
 
+    /**
+     * Parses a {@code "FrameEoch"} (WKT 2) element.
+     *
+     * @param  parent  the parent element.
+     * @return the frame epoch, or {@code null} if none.
+     * @throws ParseException if the {@code "FrameEoch"} element cannot be 
parsed.
+     */
+    private Temporal parseDynamic(final Element parent) throws ParseException {
+        final Element element = parent.pullElement(OPTIONAL, 
WKTKeywords.Dynamic);
+        if (element == null) {
+            return null;
+        }
+        Temporal epoch = Epoch.fromYear(pullElementAsDouble(element, 
WKTKeywords.FrameEpoch, MANDATORY), 0);
+        element.close(ignoredElements);
+        return epoch;
+    }
+
     /**
      * Parses an {@code "Ensemble"} (WKT 2) element.
      *
@@ -1490,12 +1509,15 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
      * @param  mode      {@link #FIRST}, {@link #OPTIONAL} or {@link 
#MANDATORY}.
      * @param  parent    the parent element.
      * @param  meridian  the prime meridian.
+     * @param  epoch     the frame epoch if the datum is dynamic, or {@code 
null} if static.
      * @return the {@code "Datum"} element as a {@link GeodeticDatum} object.
      * @throws ParseException if the {@code "Datum"} element cannot be parsed.
      *
      * @see 
org.apache.sis.referencing.datum.DefaultGeodeticDatum#formatTo(Formatter)
      */
-    private GeodeticDatum parseDatum(final int mode, final Element parent, 
final PrimeMeridian meridian) throws ParseException {
+    private GeodeticDatum parseDatum(final int mode, final Element parent, 
final PrimeMeridian meridian, final Temporal epoch)
+            throws ParseException
+    {
         final Element element = parent.pullElement(mode, WKTKeywords.Datum, 
WKTKeywords.GeodeticDatum);
         if (element == null) {
             return null;
@@ -1509,7 +1531,10 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         }
         final DatumFactory datumFactory = factories.getDatumFactory();
         try {
-            return datumFactory.createGeodeticDatum(properties, ellipsoid, 
meridian);
+            if (epoch == null) {
+                return datumFactory.createGeodeticDatum(properties, ellipsoid, 
meridian);
+            }
+            return datumFactory.createGeodeticDatum(properties, ellipsoid, 
meridian, epoch);
         } catch (FactoryException exception) {
             throw element.parseFailed(exception);
         }
@@ -1526,11 +1551,12 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
      *
      * @param  mode    {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
      * @param  parent  the parent element.
+     * @param  epoch   the frame epoch if the datum is dynamic, or {@code 
null} if static.
      * @param  isWKT1  {@code true} if the parent is a WKT 1 element.
      * @return the {@code "VerticalDatum"} element as a {@link VerticalDatum} 
object.
      * @throws ParseException if the {@code "VerticalDatum"} element cannot be 
parsed.
      */
-    private VerticalDatum parseVerticalDatum(final int mode, final Element 
parent, final boolean isWKT1)
+    private VerticalDatum parseVerticalDatum(final int mode, final Element 
parent, final Temporal epoch, final boolean isWKT1)
             throws ParseException
     {
         final Element element = parent.pullElement(mode,
@@ -1548,9 +1574,13 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         if (method == null) {
             method = VerticalDatumTypes.fromDatum(name, null, null);
         }
+        final Map<String, Object> properties = parseAnchorAndClose(element, 
name);
         final DatumFactory datumFactory = factories.getDatumFactory();
         try {
-            return 
datumFactory.createVerticalDatum(parseAnchorAndClose(element, name), method);
+            if (epoch == null) {
+                return datumFactory.createVerticalDatum(properties, method);
+            }
+            return datumFactory.createVerticalDatum(properties, method, epoch);
         } catch (FactoryException exception) {
             throw element.parseFailed(exception);
         }
@@ -1942,8 +1972,9 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             if (meridian == null) {
                 meridian = greenwich();
             }
+            final Temporal epoch = parseDynamic(element);
             final DatumEnsemble<GeodeticDatum> ensemble = 
parseEnsemble(OPTIONAL, element, GeodeticDatum.class, meridian);
-            final GeodeticDatum datum = parseDatum(ensemble == null ? 
MANDATORY : OPTIONAL, element, meridian);
+            final GeodeticDatum datum = parseDatum(ensemble == null ? 
MANDATORY : OPTIONAL, element, meridian, epoch);
             final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
             final Map<String,?> properties = parseMetadataAndClose(element, 
name, datumOrEnsemble);
             if (cs instanceof EllipsoidalCS) {                                 
 // By far the most frequent case.
@@ -2016,8 +2047,9 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
             }
         }
         if (baseCRS == null) {      // The most usual case.
+            final Temporal epoch = parseDynamic(element);
             ensemble = parseEnsemble(OPTIONAL, element, VerticalDatum.class, 
null);
-            datum = parseVerticalDatum(ensemble == null ? MANDATORY : 
OPTIONAL, element, isWKT1);
+            datum = parseVerticalDatum(ensemble == null ? MANDATORY : 
OPTIONAL, element, epoch, isWKT1);
         }
         final IdentifiedObject datumOrEnsemble = (datum != null) ? datum : 
ensemble;
         final CoordinateSystem cs;
@@ -2035,8 +2067,11 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
                  * The `parseVerticalDatum(…)` method may have been unable to 
resolve the realization method.
                  * But sometimes the axis (which was not available when we 
created the datum) provides
                  * more information. Verify if we can have a better type now, 
and if so rebuild the datum.
+                 *
+                 * TODO: remove this hack. It is mostly for old standard. The 
check for dynamic datum
+                 * is a dirty trick for checking if we have a newer standard.
                  */
-                if (method == null && datum != null) {
+                if (method == null && datum != null && !(datum instanceof 
DynamicReferenceFrame)) {
                     var type = 
VerticalDatumTypes.fromDatum(datum.getName().getCode(), datum.getAlias(), 
cs.getAxis(0));
                     if (type != null) {
                         final DatumFactory datumFactory = 
factories.getDatumFactory();
@@ -2146,9 +2181,9 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
          * In the latter case, the datum is null and we have instead 
DerivingConversion element from a BaseParametricCRS.
          */
         DatumEnsemble<ParametricDatum> ensemble = null;
-        ParametricDatum datum = null;
-        SingleCRS baseCRS = null;
-        Conversion fromBase = null;
+        ParametricDatum datum    = null;
+        SingleCRS       baseCRS  = null;
+        Conversion      fromBase = null;
         if (!isBaseCRS) {
             /*
              * UNIT[…] in DerivedCRS parameters are mandatory according ISO 
19162 and the specification does not said
@@ -2349,7 +2384,9 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
                         number, AxisDirection.UNSPECIFIED, Units.UNITY);
             }
             final Map<String,Object> properties = 
parseMetadataAndClose(element, name, baseCRS);
-            final Map<String,Object> axisName = 
singletonMap(CoordinateSystem.NAME_KEY, AxisDirections.appendTo(new 
StringBuilder("CS"), axes));
+            final Map<String,Object> axisName = singletonMap(
+                    CoordinateSystem.NAME_KEY,
+                    AxisDirections.appendTo(new StringBuilder("CS"), axes));
             final var derivedCS = new AbstractCS(axisName, axes);
             /*
              * Creates a derived CRS from the information found in a WKT 1 
{@code FITTED_CS} element.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
index a30da11b1c..275ed71dfb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractCRS.java
@@ -490,9 +490,14 @@ public class AbstractCRS extends AbstractReferenceSystem 
implements CoordinateRe
             final Function<D, FormattableObject> toFormattable,
             final Function<C, D> asDatum)
     {
+        final boolean supportsDynamic = 
formatter.getConvention().supports(Convention.WKT2_2019);
         if (datum != null) {
+            if (supportsDynamic) {
+                formatter.append(DynamicCRS.createIfDynamic(datum));
+                formatter.newLine();
+            }
             formatter.appendFormattable(datum, toFormattable);
-        } else if (formatter.getConvention().supports(Convention.WKT2_2019)) {
+        } else if (supportsDynamic) {
             formatter.appendFormattable(crs.getDatumEnsemble(), 
DefaultDatumEnsemble::castOrCopy);
         } else {
             // Apply `toFormattable` unconditionally for forcing a conversion 
of ensemble to datum.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DynamicCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DynamicCRS.java
new file mode 100644
index 0000000000..31bbc386b1
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DynamicCRS.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.crs;
+
+import java.time.temporal.Temporal;
+import org.opengis.referencing.datum.Datum;
+import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.io.wkt.FormattableObject;
+import org.apache.sis.referencing.internal.Epoch;
+import org.apache.sis.referencing.privy.WKTKeywords;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.referencing.datum.DynamicReferenceFrame;
+
+
+/**
+ * An element inserted in the <abbr>WKT</abbr> formatting of dynamic 
<abbr>CRS</abbr>.
+ *
+ * @todo {@code MODEL} sub-element is not yet supported.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class DynamicCRS extends FormattableObject {
+    /**
+     * The reference frame epoch.
+     */
+    private final Temporal epoch;
+
+    /**
+     * Creates a new element.
+     *
+     * @param  epoch  the reference frame epoch.
+     */
+    private DynamicCRS(final Temporal epoch) {
+        this.epoch = epoch;
+    }
+
+    /**
+     * Returns a {@code DYNAMIC} element for the given datum, or {@code null} 
if the datum is not dynamic.
+     */
+    static DynamicCRS createIfDynamic(final Datum datum) {
+        if (datum instanceof DynamicReferenceFrame) {
+            return new DynamicCRS(((DynamicReferenceFrame) 
datum).getFrameReferenceEpoch());
+        }
+        return null;
+    }
+
+    /**
+     * Formats this epoch as a <i>Well Known Text</i> {@code 
CoordinateMetadata[…]} element.
+     *
+     * @param  formatter  the formatter where to format the inner content of 
this WKT element.
+     * @return {@code "Dynamic"}.
+     */
+    @Override
+    protected String formatTo(final Formatter formatter) {
+        formatter.append(new Epoch(epoch, true));
+        return WKTKeywords.Dynamic;
+    }
+}
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 9e4474feb5..5cf8fe2fc9 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
@@ -45,7 +45,6 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.time.DateTimeException;
 import java.time.LocalDate;
-import java.time.Year;
 import java.time.temporal.Temporal;
 import javax.measure.Unit;
 import javax.measure.quantity.Angle;
@@ -87,6 +86,7 @@ import org.apache.sis.referencing.privy.CoordinateOperations;
 import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
 import org.apache.sis.referencing.internal.DeferredCoordinateOperation;
 import org.apache.sis.referencing.internal.DeprecatedCode;
+import org.apache.sis.referencing.internal.Epoch;
 import org.apache.sis.referencing.internal.EPSGParameterDomain;
 import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
 import org.apache.sis.referencing.internal.PositionalAccuracyConstant;
@@ -965,16 +965,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * @throws SQLException if an error occurred while querying the database.
      */
     private static Temporal getOptionalEpoch(final ResultSet result, final int 
columnIndex) throws SQLException {
-        final double epoch = getOptionalDouble(result, columnIndex);
-        if (Double.isNaN(epoch)) {
-            return null;
-        }
-        final var year = Year.of((int) epoch);
-        final long day = Math.round((epoch - year.getValue()) * year.length());
-        if (day == 0) {
-            return year;
-        }
-        return year.atMonth(year.atDay(Math.toIntExact(day)).getMonth());
+        return Epoch.fromYear(getOptionalDouble(result, columnIndex), 0);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
index de6020319d..0c3f131d3d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
@@ -17,6 +17,7 @@
 package org.apache.sis.referencing.internal;
 
 import java.time.Instant;
+import java.time.LocalDate;
 import java.time.OffsetDateTime;
 import java.time.Year;
 import java.time.YearMonth;
@@ -33,6 +34,7 @@ import org.apache.sis.util.privy.Constants;
 /**
  * Epoch of a coordinate set or of a dynamic reference frame.
  * This is a temporary object used for Well-Known Text formatting.
+ * Contains also utility methods for conversion from/to temporal objects.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -47,12 +49,19 @@ public final class Epoch extends FormattableObject {
      */
     public final int precision;
 
+    /**
+     * Whether this epoch is the frame epoch instead of the coordinate epoch.
+     */
+    private final boolean frame;
+
     /**
      * Converts the given epoch to a fractional year.
      *
      * @param  epoch  the epoch to express as a fractional year.
+     * @param  frame  whether this epoch is the frame epoch instead of the 
coordinate epoch
      */
-    public Epoch(Temporal epoch) {
+    public Epoch(Temporal epoch, final boolean frame) {
+        this.frame = frame;
         if (epoch instanceof Instant) {
             epoch = OffsetDateTime.ofInstant((Instant) epoch, ZoneOffset.UTC);
         }
@@ -77,15 +86,65 @@ public final class Epoch extends FormattableObject {
         }
     }
 
+    /**
+     * Returns the temporal object from the given year.
+     * The type of the returned object depends on the {@code precision} 
argument:
+     *
+     * <li>
+     *   <ul>= 0: returns {@link Year}, unless there is a fraction part in 
which case returns {@link YearMonth}.</ul>
+     *   <ul>≤ 2: returns {@link YearMonth}.</ul>
+     *   <ul>≤ 3: returns {@link LocalDate}.</ul>
+     *   <ul>Other cases not yet implemented, but may be in the future.</ul>
+     * </li>
+     *
+     * @param  epoch      the epoch as a fractional year.
+     * @param  precision  number of valid digits in the given epoch.
+     * @return the given epoch as a temporal object, or {@code null} if the 
given value is NaN.
+     */
+    public static Temporal fromYear(final double epoch, final int precision) {
+        if (Double.isNaN(epoch)) {
+            return null;
+        }
+        Year   year = Year.of((int) epoch);
+        double time = epoch - year.getValue();
+        long   day  = Math.round(time * year.length());
+        if (day == 0 && precision <= 0) return year;
+        final LocalDate date = year.atDay(Math.toIntExact(day + 1));
+        if (precision <= 2) return year.atMonth(date.getMonth());
+        return date;
+    }
+
+    /**
+     * Returns the temporal object from a year given as a string.
+     * The precision is determined by the number of digits that are explicitly 
written, even if zero.
+     *
+     * @param  epoch  the epoch as a fractional year.
+     * @return the given epoch as a temporal object, or {@code null} if the 
given value is NaN.
+     * @throws NumberFormatException if the given string cannot be parsed as 
number.
+     */
+    public static Temporal fromYear(final String epoch) {
+        int precision = 0;
+        int i = epoch.indexOf('.');
+        if (i >= 0) {
+            final int length = epoch.length();
+            while (++i < length) {
+                final char c = epoch.charAt(i);
+                if (c < '0' || c > '9') break;
+                precision++;
+            }
+        }
+        return fromYear(Double.parseDouble(epoch), precision);
+    }
+
     /**
      * Formats this epoch as a <i>Well Known Text</i> {@code 
CoordinateMetadata[…]} element.
      *
      * @param  formatter  the formatter where to format the inner content of 
this WKT element.
-     * @return {@code "Epoch"}.
+     * @return {@code "Epoch"} or {@code "FrameEpoch"}.
      */
     @Override
     protected String formatTo(final Formatter formatter) {
         formatter.append(value, precision);
-        return WKTKeywords.Epoch;
+        return frame ? WKTKeywords.FrameEpoch : WKTKeywords.Epoch;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java
index c2fe067df1..76bf6005cd 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java
@@ -92,6 +92,7 @@ public final class WKTKeywords extends Static {
             Ensemble         = "Ensemble",
             Member           = "Member",
             EnsembleAccuracy = "EnsembleAccuracy",
+            Dynamic          = "Dynamic",
             ToWGS84          = "ToWGS84";
 
     /**
@@ -228,6 +229,7 @@ public final class WKTKeywords extends Static {
      */
     public static final String
             CoordinateMetadata = "CoordinateMetadata",
+            FrameEpoch         = "FrameEpoch",
             Epoch              = "Epoch",
             Point              = "Point";
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index 54790bb884..77c961fd07 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -22,6 +22,8 @@ import java.util.Locale;
 import java.time.Instant;
 import java.text.ParsePosition;
 import java.text.ParseException;
+import java.time.Year;
+import java.time.temporal.Temporal;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
 import org.opengis.referencing.IdentifiedObject;
@@ -979,6 +981,35 @@ public final class GeodeticObjectParserTest extends 
EPSGDependentTestCase {
                   "AXIS[" + axis + "]]");
     }
 
+    /**
+     * Tests the parsing of a vertical <abbr>CRS</abbr>.
+     *
+     * @throws ParseException if the parsing failed.
+     */
+    @Test
+    public void testVerticalCRS() throws ParseException {
+        final VerticalCRS crs = parse(VerticalCRS.class,
+                "VerticalCRS[“RH2000 height”,\n" +
+                "  Dynamic[FrameEpoch[2000]],\n" +
+                "  VerticalDatum[“Rikets hojdsystem 2000”],\n" +
+                "  CS[vertical, 1],\n" +
+                "    Axis[“Gravity-related height (H)”, up],\n" +
+                "    Unit[“metre”, 1],\n" +
+                "  Usage[\n" +
+                "    Scope[“Geodesy, engineering survey.”],\n" +
+                "    Area[“Sweden - onshore.”],\n" +
+                "    BBox[55.28, 10.93, 69.07, 24.17]],\n" +
+                "  Id[“EPSG”, 5613, “12.013”, 
URI[“urn:ogc:def:crs:EPSG:12.013:5613”]],\n" +
+                "  Remark[“Replaces RH70 (CRS code 5718) from 2005.”]]");
+
+        assertNameAndIdentifierEqual("RH2000 height", 5613, crs);
+        assertNameAndIdentifierEqual("Rikets hojdsystem 2000", 0, 
crs.getDatum());
+        Temporal epoch = assertInstanceOf(DynamicReferenceFrame.class, 
crs.getDatum()).getFrameReferenceEpoch();
+        assertEquals(Year.of(2000), epoch);
+        assertEquals("Geodesy, engineering survey.", 
getSingleton(crs.getDomains()).getScope().toString());
+        assertEquals("Replaces RH70 (CRS code 5718) from 2005.", 
crs.getRemarks().orElseThrow().toString());
+    }
+
     /**
      * Returns the conversion from {@code north} to {@code south}.
      */
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/EpochTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/EpochTest.java
index 4cd3f1fbbf..5a0a91bfee 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/EpochTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/EpochTest.java
@@ -20,6 +20,7 @@ import java.time.Year;
 import java.time.YearMonth;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.Month;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -46,7 +47,7 @@ public final class EpochTest extends TestCase {
      */
     @Test
     public void testYear() {
-        var epoch = new Epoch(Year.of(2010));
+        var epoch = new Epoch(Year.of(2010), false);
         assertEquals(2010, epoch.value);
         assertEquals(0, epoch.precision);
         assertEquals("Epoch[2010]", epoch.toString());
@@ -57,12 +58,12 @@ public final class EpochTest extends TestCase {
      */
     @Test
     public void testYearMonth() {
-        var epoch = new Epoch(YearMonth.of(2016, 1));
+        var epoch = new Epoch(YearMonth.of(2016, 1), false);
         assertEquals(2016, epoch.value);
         assertEquals(2, epoch.precision);
         assertEquals("Epoch[2016.00]", epoch.toString());
 
-        epoch = new Epoch(YearMonth.of(2016, 7));
+        epoch = new Epoch(YearMonth.of(2016, 7), false);
         assertEquals(2016.49726775956, epoch.value, 1E-11);
         assertEquals(2, epoch.precision);
         assertEquals("Epoch[2016.50]", epoch.toString());
@@ -73,7 +74,7 @@ public final class EpochTest extends TestCase {
      */
     @Test
     public void testLocalDate() {
-        var epoch = new Epoch(LocalDate.of(2016, 7, 20));
+        var epoch = new Epoch(LocalDate.of(2016, 7, 20), false);
         assertEquals(2016.54918032787, epoch.value, 1E-11);
         assertEquals(3, epoch.precision);
         assertEquals("Epoch[2016.549]", epoch.toString());
@@ -84,9 +85,19 @@ public final class EpochTest extends TestCase {
      */
     @Test
     public void testLocalDateTime() {
-        var epoch = new Epoch(LocalDateTime.of(2014, 2, 15, 10, 40));
+        var epoch = new Epoch(LocalDateTime.of(2014, 2, 15, 10, 40), false);
         assertEquals(2014.12450532725, epoch.value, 1E-11);
         assertEquals(8, epoch.precision);
         assertEquals("Epoch[2014.12450533]", epoch.toString());
     }
+
+    /**
+     * Tests {@link Epoch#fromYear(String)}.
+     */
+    @Test
+    public void testFromYear() {
+        assertEquals(     Year.of(2010),                
Epoch.fromYear("2010"));
+        assertEquals(YearMonth.of(2010, Month.JANUARY), 
Epoch.fromYear("2010.0"));
+        assertEquals(YearMonth.of(2010, Month.APRIL),   
Epoch.fromYear("2010.3"));
+    }
 }

Reply via email to