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 062a8de71d263f94514009c167775fa11719162b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Sep 6 16:53:42 2025 +0200

    Add the support of `AXISMINVALUE`, `AXISMAXVALUE` and `RANGEMEANING` from 
ISO 19162:2019.
---
 .../main/org/apache/sis/io/wkt/AbstractParser.java |  73 ++++++++++++++
 .../main/org/apache/sis/io/wkt/Convention.java     |   4 +-
 .../main/org/apache/sis/io/wkt/Element.java        |   2 +-
 .../main/org/apache/sis/io/wkt/Formatter.java      |   2 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |  55 ++++-------
 .../org/apache/sis/io/wkt/MathTransformParser.java |  25 +++--
 .../main/org/apache/sis/io/wkt/Transliterator.java |  20 ++--
 .../cs/DefaultCoordinateSystemAxis.java            | 105 ++++++++++++++++-----
 .../apache/sis/referencing/privy/WKTKeywords.java  |  16 ++--
 .../sis/io/wkt/GeodeticObjectParserTest.java       |  18 ++++
 .../test/org/apache/sis/io/wkt/WKTFormatTest.java  |   1 +
 .../referencing/crs/DefaultGeographicCRSTest.java  |   4 +-
 .../referencing/crs/DefaultProjectedCRSTest.java   |   4 +-
 .../cs/DefaultCoordinateSystemAxisTest.java        |  41 ++++----
 14 files changed, 261 insertions(+), 109 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
index 44a49c5e9e..d6abe3c310 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
@@ -420,6 +420,79 @@ abstract class AbstractParser implements Parser {
         return unitFormat.parse(text);
     }
 
+    /**
+     * Pulls an optional element which contains only a floating-point value.
+     *
+     * @param  parent  the element from which to pull a child element.
+     * @param  key     name of the element to pull.
+     * @return the value, or {@code null} if none.
+     * @throws ParseException if an element cannot be parsed.
+     */
+    final Double pullElementAsDouble(final Element parent, final String key) 
throws ParseException {
+        Element element = parent.pullElement(OPTIONAL, key);
+        if (element != null) {
+            double value = element.pullDouble(key);
+            element.close(ignoredElements);
+            return value;
+        }
+        return null;
+    }
+
+    /**
+     * Pulls an optional element which contains only an integer value.
+     *
+     * @param  parent  the element from which to pull a child element.
+     * @param  key     name of the element to pull.
+     * @return the value, or {@code null} if none.
+     * @throws ParseException if an element cannot be parsed.
+     */
+    final Integer pullElementAsInteger(final Element parent, final String key) 
throws ParseException {
+        Element element = parent.pullElement(OPTIONAL, key);
+        if (element != null) {
+            int value = element.pullInteger(key);
+            element.close(ignoredElements);
+            return value;
+        }
+        return null;
+    }
+
+    /**
+     * Pulls an optional element which contains only a string value.
+     *
+     * @param  parent  the element from which to pull a child element.
+     * @param  key     name of the element to pull.
+     * @return the value, or {@code null} if none.
+     * @throws ParseException if an element cannot be parsed.
+     */
+    final String pullElementAsString(final Element parent, final String key) 
throws ParseException {
+        Element element = parent.pullElement(OPTIONAL, key);
+        if (element != null) {
+            String value = element.pullString(key);
+            element.close(ignoredElements);
+            return value;
+        }
+        return null;
+    }
+
+    /**
+     * Pulls an optional element which contains only an enumeration value.
+     *
+     * @param  parent  the element from which to pull a child element.
+     * @param  key     name of the element to pull.
+     * @return the value, or {@code null} if none.
+     * @throws ParseException if an element cannot be parsed.
+     */
+    final String pullElementAsEnum(final Element parent, final String key) 
throws ParseException {
+        Element element = parent.pullElement(OPTIONAL, key);
+        if (element != null) {
+            Element value = element.pullVoidElement(key);
+            value  .close(ignoredElements);
+            element.close(ignoredElements);
+            return value.keyword;
+        }
+        return null;
+    }
+
     /**
      * Reports a non-fatal warning that occurred while parsing a WKT.
      *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java
index 30b94cdca9..7fc4c24e13 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java
@@ -77,7 +77,9 @@ public enum Convention {
      *   <li>By {@linkplain KeywordStyle#DEFAULT default}, this convention 
uses the keywords that are the closest matches
      *       to the Java interface names. For example, {@code "GeodeticCRS"} 
is preferred to {@code "GeodCRS"}.</li>
      *   <li>The {@code PrimeMeridian} element is omitted if the meridian is 
Greenwich.</li>
-     *   <li>The {@code Axis} element omits the {@code Order} sub-element.</li>
+     *   <li>The {@code Axis} element omits always the {@code Order} 
sub-element.</li>
+     *   <li>The {@code Axis} element omits the {@code AxisMinValue}, {@code 
AxisMinValue} and {@code RangeMeaning}
+     *       sub-elements if their values are the standard values for latitude 
or longitude axes.</li>
      *   <li>The {@code Unit} elements are less verbose:<ul>
      *     <li>{@code Ellipsoid} and {@code VerticalExtent} elements omit the 
{@code LengthUnit} sub-element
      *         if that unit is {@link org.apache.sis.measure.Units#METRE}.</li>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
index 87d0938106..e34dddbdd8 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
@@ -713,7 +713,7 @@ final class Element {
 
     /**
      * Removes and returns the next {@link Element} with no bracket.
-     * The key is used only for only for formatting an error message.
+     * The key is used only for formatting an error message.
      *
      * @param  key  the parameter name. Used only for formatting an error 
message.
      * @return the next {@link Element} among the children, with no bracket.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
index 4a8ec2309f..8aa6c12db6 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
@@ -1622,7 +1622,7 @@ public class Formatter implements Localized {
      */
     final boolean appendValue(final Object value) {
         if (value instanceof Number) {
-            final Number number = (Number) value;
+            final var number = (Number) value;
             if (Numbers.isInteger(number.getClass())) {
                 append(number.longValue());
             } else {
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 8e9e9d85fa..d64fc1d548 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
@@ -57,6 +57,7 @@ import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.DefaultObjectDomain;
 import org.apache.sis.referencing.cs.AbstractCS;
 import org.apache.sis.referencing.cs.CoordinateSystems;
+import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
 import org.apache.sis.referencing.crs.DefaultDerivedCRS;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
 import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
@@ -474,7 +475,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
          */
         final var domains = new ArrayList<ObjectDomain>();
         while ((element = parent.pullElement(OPTIONAL, WKTKeywords.Usage)) != 
null) {
-            final String scope = parseScope(element);
+            final String scope = pullElementAsString(element, 
WKTKeywords.Scope);
             final DefaultExtent extent = parseExtent(element);
             if (scope != null || extent != null) {
                 domains.add(new 
DefaultObjectDomain(Types.toInternationalString(scope), extent));
@@ -486,7 +487,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
              * Legacy (ISO 19162:2015) way to declare scope and extent:
              * directly inside de CRS element, without USAGE wrapper.
              */
-            final String scope = parseScope(parent);
+            final String scope = pullElementAsString(parent, 
WKTKeywords.Scope);
             if (scope != null) {
                 properties.put(ObjectDomain.SCOPE_KEY, scope);
             }
@@ -526,28 +527,6 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         return properties;
     }
 
-    /**
-     * Parses the {@code SCOPE} element.
-     * This element has the following pattern:
-     *
-     * {@snippet lang="wkt" :
-     *     SCOPE["Large scale topographic mapping and cadastre."]
-     *     }
-     *
-     * @param  parent  the parent element.
-     * @return the scope, or {@code null} if none.
-     * @throws ParseException if an element cannot be parsed.
-     */
-    private String parseScope(final Element parent) throws ParseException {
-        final Element element = parent.pullElement(OPTIONAL, 
WKTKeywords.Scope);
-        if (element != null) {
-            final String scope = element.pullString("scope");
-            element.close(ignoredElements);
-            return scope;
-        }
-        return null;
-    }
-
     /**
      * Parses the {@code AREA}, {@code BBOX}, {@code VERTICALEXTENT} and 
{@code TIMEEXTENT} elements if present.
      * These elements were directly inside the <abbr>CRS</abbr> element in 
<abbr>ISO</abbr> 19162:2015, but became
@@ -1127,24 +1106,30 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
         name         = transliterator.toLongAxisName       (csType, direction, 
name);
         abbreviation = transliterator.toUnicodeAbbreviation(csType, direction, 
abbreviation);
         /*
-         * At this point we are done and ready to create the 
CoordinateSystemAxis. But there is one last element
-         * specified by ISO 19162 but not in Apache SIS representation of 
axis: ORDER[n], which specify the axis
-         * ordering. If present we will store that value for processing by the 
`parseCoordinateSystem(…)` method.
+         * At this point, the mandatory properties are known. The next element 
is specified by ISO 19162
+         * but not stored in Apache SIS representation of axis: ORDER[n], 
which specifies the axis ordering.
+         * If present, we will store that value for processing by the 
`parseCoordinateSystem(…)` method.
+         */
+        final Integer order   = pullElementAsInteger(element, 
WKTKeywords.Order);
+        final Double  minimum = pullElementAsDouble (element, 
WKTKeywords.AxisMinValue);
+        final Double  maximum = pullElementAsDouble (element, 
WKTKeywords.AxisMaxValue);
+        final String  meaning = pullElementAsEnum   (element, 
WKTKeywords.RangeMeaning);
+        final Map<String, Object> properties = parseMetadataAndClose(element, 
name, null);
+        properties.put(DefaultCoordinateSystemAxis.MINIMUM_VALUE_KEY, minimum);
+        properties.put(DefaultCoordinateSystemAxis.MAXIMUM_VALUE_KEY, maximum);
+        properties.put(DefaultCoordinateSystemAxis.RANGE_MEANING_KEY,
+                Types.forCodeName(RangeMeaning.class, meaning, 
RangeMeaning::valueOf));
+        /*
+         * At this point, all mandatory and optional properties are known.
          */
-        final Element order = element.pullElement(OPTIONAL, WKTKeywords.Order);
-        Integer n = null;
-        if (order != null) {
-            n = order.pullInteger("order");
-            order.close(ignoredElements);
-        }
         final CoordinateSystemAxis axis;
         final CSFactory csFactory = factories.getCSFactory();
         try {
-            axis = 
csFactory.createCoordinateSystemAxis(parseMetadataAndClose(element, name, 
null), abbreviation, direction, unit);
+            axis = csFactory.createCoordinateSystemAxis(properties, 
abbreviation, direction, unit);
         } catch (FactoryException exception) {
             throw element.parseFailed(exception);
         }
-        if (axisOrder.put(axis, n) != null) {   // Opportunist check, 
effective for instances created by SIS factory.
+        if (axisOrder.put(axis, order) != null) {   // Opportunist check, 
effective for instances created by SIS factory.
             throw new UnparsableObjectException(errorLocale, 
Errors.Keys.DuplicatedElement_1,
                     new Object[] {Strings.bracket(WKTKeywords.Axis, name)}, 
element.offset);
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
index 59fe947a89..d2712e0309 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
@@ -68,7 +68,8 @@ class MathTransformParser extends AbstractParser {
     private static final String[] UNIT_KEYWORDS = {
         WKTKeywords.Unit,   // Ignored since it does not allow us to know the 
quantity dimension.
         WKTKeywords.LengthUnit, WKTKeywords.AngleUnit, WKTKeywords.ScaleUnit, 
WKTKeywords.TimeUnit,
-        WKTKeywords.ParametricUnit  // Ignored for the same reason as "Unit".
+        WKTKeywords.TemporalQuantity,   // Alternative keyword for "TimeUnit".
+        WKTKeywords.ParametricUnit      // Ignored for the same reason as 
"Unit".
     };
 
     /**
@@ -76,7 +77,7 @@ class MathTransformParser extends AbstractParser {
      * For each {@code UNIT_KEYWORDS[i]} element, the associated base unit is 
{@code BASE_UNIT[i-1]}.
      */
     private static final Unit<?>[] BASE_UNITS = {
-        Units.METRE, Units.RADIAN, Units.UNITY, Units.SECOND
+        Units.METRE, Units.RADIAN, Units.UNITY, Units.SECOND, Units.SECOND
     };
 
     /**
@@ -254,10 +255,22 @@ class MathTransformParser extends AbstractParser {
         if (element == null) {
             return null;
         }
-        final String  name   = element.pullString("name");
-        double        factor = element.pullDouble("factor");
         final int     index  = element.getKeywordIndex() - 1;
+        final String  name   = element.pullString("name");
         final Unit<?> unit   = parseUnitID(element);
+        final Unit<?> base   = (index >= 0 && index < BASE_UNITS.length) ? 
BASE_UNITS[index] : null;
+        /*
+         * The conversion factor form base unit is mandatory, except for 
temporal units
+         * because the conversion may not be exact (because of variable 
duration of day,
+         * month or year). Note however that Apache SIS 1.5 will fallback on 
constant
+         * factors anyway, therefore the problem described by ISO is not 
really solved.
+         */
+        double factor;
+        if (base == Units.SECOND && element.peekValue() == null) {
+            factor = Double.NaN;
+        } else {
+            factor = element.pullDouble("factor");
+        }
         element.close(ignoredElements);
         if (unit != null) {
             return unit;
@@ -268,11 +281,11 @@ class MathTransformParser extends AbstractParser {
          * In particular, the conversion factor for degrees is sometimes 
written as 0.01745329252 instead of
          * 0.017453292519943295.
          */
-        if (index >= 0 && index < BASE_UNITS.length) {
+        if (base != null && !Double.isNaN(factor)) {
             if (index < CONVERSION_FACTORS.length) {
                 factor = completeUnitFactor(CONVERSION_FACTORS[index], factor);
             }
-            return BASE_UNITS[index].multiply(factor);
+            return base.multiply(factor);
         }
         // If we cannot infer the base type, we have to rely on the name.
         try {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java
index 6d47f0ba38..f3359384bb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java
@@ -29,6 +29,7 @@ import org.apache.sis.referencing.privy.AxisDirections;
 import org.apache.sis.referencing.privy.WKTKeywords;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Characters;
+import org.apache.sis.util.OptionalCandidate;
 
 
 /**
@@ -181,17 +182,15 @@ public abstract class Transliterator implements 
Serializable {
     }
 
     /**
-     * Returns the axis name to format in WKT, or {@code null} if none. This 
method performs the mapping
-     * between the names of axes in memory (designated by <q>long axis 
names</q> in this class)
-     * and the names to format in the WKT (designated by <q>short axis 
names</q>).
+     * Returns the axis name to format in <abbr>WKT</abbr>, or {@code null} if 
none.
+     * This method performs the mapping between the names of axes in memory 
(designated by <dfn>long axis names</dfn>
+     * in this class) and the names to format in the <abbr>WKT</abbr> 
(designated by <dfn>short axis names</dfn>).
+     * The long axis names are defined by <abbr>ISO</abbr> 19111 — 
<cite>referencing by coordinates</cite> while
+     * the short axis names are defined by <abbr>ISO</abbr> 19162 — 
<cite>Well-known text representation
+     * of coordinate reference systems</cite>.
      *
-     * <div class="note"><b>Note:</b>
-     * the <q>long axis names</q> are defined by ISO 19111 — <cite>referencing 
by coordinates</cite>
-     * while the <q>short axis names</q> are defined by ISO 19162 — 
<cite>Well-known text representation
-     * of coordinate reference systems</cite>.</div>
-     *
-     * This method can return {@code null} if the name should be omitted.
-     * ISO 19162 recommends to omit the axis name when it is already given 
through the mandatory axis direction.
+     * <p>This method returns {@code null} if the name should be omitted. 
<abbr>ISO</abbr> 19162 recommends
+     * to omit the axis name when it is already given through the mandatory 
axis direction.</p>
      *
      * <p>The default implementation performs at least the following 
replacements:</p>
      * <ul>
@@ -208,6 +207,7 @@ public abstract class Transliterator implements 
Serializable {
      *
      * @see 
org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis#formatTo(Formatter)
      */
+    @OptionalCandidate
     public String toShortAxisName(final CoordinateSystem cs, final 
AxisDirection direction, final String name) {
         if (name.equalsIgnoreCase(AxisNames.GEODETIC_LATITUDE) ||              
 // ISO 19162:2015 §7.5.3(ii)
             name.equalsIgnoreCase(AxisNames.PLANETODETIC_LATITUDE))
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java
index 1eda1dc32e..e332509b3c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java
@@ -48,8 +48,10 @@ import org.apache.sis.referencing.privy.AxisDirections;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.io.wkt.Formatter;
@@ -57,8 +59,6 @@ import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.io.wkt.ElementKind;
 import org.apache.sis.io.wkt.Transliterator;
 import org.apache.sis.io.wkt.FormattableObject;
-import static org.apache.sis.util.ArgumentChecks.*;
-import static org.apache.sis.util.collection.Containers.property;
 
 
 /*
@@ -296,12 +296,12 @@ public class DefaultCoordinateSystemAxis extends 
AbstractIdentifiedObject implem
         this.abbreviation = abbreviation;
         this.direction    = direction;
         this.unit         = unit;
-        ensureNonEmpty("abbreviation", abbreviation);
-        ensureNonNull ("direction",    direction);
-        ensureNonNull ("unit",         unit);
-        Number  minimum = property(properties, MINIMUM_VALUE_KEY, 
Number.class);
-        Number  maximum = property(properties, MAXIMUM_VALUE_KEY, 
Number.class);
-        RangeMeaning rm = property(properties, RANGE_MEANING_KEY, 
RangeMeaning.class);
+        ArgumentChecks.ensureNonEmpty("abbreviation", abbreviation);
+        ArgumentChecks.ensureNonNull ("direction",    direction);
+        ArgumentChecks.ensureNonNull ("unit",         unit);
+        Number  minimum = Containers.property(properties, MINIMUM_VALUE_KEY, 
Number.class);
+        Number  maximum = Containers.property(properties, MAXIMUM_VALUE_KEY, 
Number.class);
+        RangeMeaning rm = Containers.property(properties, RANGE_MEANING_KEY, 
RangeMeaning.class);
         if (minimum == null && maximum == null && rm == null) {
             double min = Double.NEGATIVE_INFINITY;
             double max = Double.POSITIVE_INFINITY;
@@ -333,7 +333,7 @@ public class DefaultCoordinateSystemAxis extends 
AbstractIdentifiedObject implem
                         Errors.Keys.IllegalRange_2, minimumValue, 
maximumValue));
             }
             if ((minimumValue != NEGATIVE_INFINITY) || (maximumValue != 
POSITIVE_INFINITY)) {
-                ensureNonNull(RANGE_MEANING_KEY, rm);
+                ArgumentChecks.ensureNonNull(RANGE_MEANING_KEY, rm);
             } else {
                 rm = null;
             }
@@ -781,7 +781,7 @@ public class DefaultCoordinateSystemAxis extends 
AbstractIdentifiedObject implem
          */
         if (!isWKT1) {
             if (cs != null && !convention.isSimplified()) {
-                final Order order = Order.create(cs, this);
+                final Child order = Child.order(cs, this);
                 if (order != null) {
                     formatter.append(order);
                 } else {
@@ -791,19 +791,52 @@ public class DefaultCoordinateSystemAxis extends 
AbstractIdentifiedObject implem
             if (!formatter.hasContextualUnit(1)) {
                 formatter.append(getUnit());
             }
+            if (convention.supports(Convention.WKT2_2019)) {
+                boolean standard = false;
+                if (convention.isSimplified()) {
+                    dir = AxisDirections.absolute(dir);
+                    if (Units.DEGREE.equals(unit)) {
+                        if (dir.equals(AxisDirection.NORTH)) {
+                            standard = (minimumValue == Latitude.MIN_VALUE)
+                                    && (maximumValue == Latitude.MAX_VALUE)
+                                    && (rangeMeaning == RangeMeaning.EXACT);
+                        } else if (dir.equals(AxisDirection.EAST)) {
+                            standard = (minimumValue == Longitude.MIN_VALUE)
+                                    && (maximumValue == Longitude.MAX_VALUE)
+                                    && (rangeMeaning == 
RangeMeaning.WRAPAROUND);
+                        }
+                    } else if (Units.isLinear(unit) && cs instanceof 
SphericalCS) {
+                        if (dir.equals(AxisDirection.UP)) {
+                            standard = (minimumValue == 0)
+                                    && (maximumValue == 
Double.POSITIVE_INFINITY)
+                                    && (rangeMeaning == RangeMeaning.EXACT);
+                        }
+                    }
+                }
+                if (!standard) {
+                    final Child min = Child.range(getMinimumValue(), false);
+                    final Child max = Child.range(getMaximumValue(), true);
+                    if (min != null || max != null) {
+                        formatter.append(min);
+                        formatter.append(max);
+                        formatter.append(Child.range(getRangeMeaning()));
+                    }
+                }
+            }
         }
         return WKTKeywords.Axis;
     }
 
     /**
-     * The {@code ORDER[…]} element to be formatted inside {@code AXIS[…]} 
element.
-     * This is an element of WKT 2 only.
+     * The {@code ORDER[…]}, {@code AXISMINVALUE}, {@code AXISMAXVALUE} or 
{@code RANGEMEANING} element
+     * to be formatted inside an {@code AXIS[…]} element. They are elements of 
<abbr>WKT</abbr> 2 only.
      */
-    private static final class Order extends FormattableObject {
-        /**
-         * The sequence number to format inside the {@code ORDER[…]} element.
-         */
-        private final int index;
+    private static final class Child extends FormattableObject {
+        /** The <abbr>WKT</abbr> keyword of this element. */
+        private final String keyword;
+
+        /** The numerical or textual value to be written inside this child 
element. */
+        private final Object value;
 
         /**
          * Creates a new {@code ORDER[…]} element for the given axis in the 
given coordinate system.
@@ -817,13 +850,13 @@ public class DefaultCoordinateSystemAxis extends 
AbstractIdentifiedObject implem
          *
          * @se <a 
href="https://issues.apache.org/jira/browse/SIS-163";>SIS-163</a>
          */
-        static Order create(final CoordinateSystem cs, final 
DefaultCoordinateSystemAxis axis) {
-            Order order = null;
+        static Child order(final CoordinateSystem cs, final 
DefaultCoordinateSystemAxis axis) {
+            Child order = null;
             final int dimension = cs.getDimension();
             for (int i=0; i<dimension;) {
                 if (cs.getAxis(i++) == axis) {
                     if (order == null) {
-                        order = new Order(i);
+                        order = new Child(WKTKeywords.Order, i);
                     } else {
                         return null;
                     }
@@ -833,19 +866,39 @@ public class DefaultCoordinateSystemAxis extends 
AbstractIdentifiedObject implem
         }
 
         /**
-         * Creates new {@code ORDER[…]} element for the given sequential 
number.
+         * Creates a new {@code AXISMINVALUE} or {@code AXISMAXVALUE} element 
for the given value.
+         * If the value is not finite, then this method returns {@code null}.
+         */
+        static Child range(final double value, final boolean isMax) {
+            if (Double.isFinite(value)) {
+                return new Child(isMax ? WKTKeywords.AxisMaxValue : 
WKTKeywords.AxisMinValue, value);
+            }
+            return null;
+        }
+
+        /**
+         * Creates a new {@code RANGEMEANING} element for the given value.
+         * If the value is null, then this method returns {@code null}.
+         */
+        static Child range(final RangeMeaning value) {
+            return (value != null) ? new Child(WKTKeywords.RangeMeaning, 
value) : null;
+        }
+
+        /**
+         * Creates new child element with the given value.
          */
-        private Order(final int index) {
-            this.index = index;
+        private Child(final String keyword, final Object value) {
+            this.keyword = keyword;
+            this.value   = value;
         }
 
         /**
-         * Formats the {@code ORDER[…]} element.
+         * Formats the {@code ORDER[…]}, {@code AXISMINVALUE}, {@code 
AXISMAXVALUE} or {@code RANGEMEANING} element.
          */
         @Override
         protected String formatTo(final Formatter formatter) {
-            formatter.append(index);
-            return WKTKeywords.Order;
+            formatter.appendAny(value);
+            return keyword;
         }
     }
 
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 5e95187704..80ad11133c 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
@@ -65,12 +65,13 @@ public final class WKTKeywords extends Static {
      * Related to unit of measurements.
      */
     public static final String
-            Unit           = "Unit",
-            LengthUnit     = "LengthUnit",
-            AngleUnit      = "AngleUnit",
-            ScaleUnit      = "ScaleUnit",
-            TimeUnit       = "TimeUnit",
-            ParametricUnit = "ParametricUnit";
+            Unit             = "Unit",
+            LengthUnit       = "LengthUnit",
+            AngleUnit        = "AngleUnit",
+            ScaleUnit        = "ScaleUnit",
+            TimeUnit         = "TimeUnit",
+            TemporalQuantity = "TemporalQuantity",
+            ParametricUnit   = "ParametricUnit";
 
     /**
      * Related to {@link org.apache.sis.referencing.cs.AbstractCS}
@@ -79,6 +80,9 @@ public final class WKTKeywords extends Static {
     public static final String
             CS            = "CS",
             Axis          = "Axis",
+            AxisMinValue  = "AxisMinValue",
+            AxisMaxValue  = "AxisMaxValue",
+            RangeMeaning  = "RangeMeaning",
             Order         = "Order",
             Meridian      = "Meridian",
             PrimeMeridian = "PrimeMeridian",
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 5ad05410be..074b3c4774 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
@@ -34,6 +34,8 @@ import 
org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterValueGroup;
+import org.apache.sis.measure.Latitude;
+import org.apache.sis.measure.Longitude;
 import org.apache.sis.metadata.privy.AxisNames;
 import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
 import org.apache.sis.referencing.cs.CoordinateSystems;
@@ -184,12 +186,18 @@ public final class GeodeticObjectParserTest extends 
EPSGDependentTestCase {
         assertEquals("φ", axis.getAbbreviation(), "abbreviation");
         assertEquals(AxisDirection.NORTH, axis.getDirection(), "direction");
         assertEquals(Units.DEGREE, axis.getUnit(), "unit");
+        assertEquals(Latitude.MIN_VALUE, axis.getMinimumValue());
+        assertEquals(Latitude.MAX_VALUE, axis.getMaximumValue());
+        assertEquals(RangeMeaning.EXACT, axis.getRangeMeaning());
 
         axis = parse(CoordinateSystemAxis.class, 
"AXIS[“longitude”,EAST,order[2],UNIT[“degree”,0.0174532925199433]]");
         assertEquals("Longitude", axis.getName().getCode(), "name");
         assertEquals("λ", axis.getAbbreviation(), "abbreviation");
         assertEquals(AxisDirection.EAST, axis.getDirection(), "direction");
         assertEquals(Units.DEGREE, axis.getUnit(), "unit");
+        assertEquals(Longitude.MIN_VALUE, axis.getMinimumValue());
+        assertEquals(Longitude.MAX_VALUE, axis.getMaximumValue());
+        assertEquals(RangeMeaning.WRAPAROUND, axis.getRangeMeaning());
 
         axis = parse(CoordinateSystemAxis.class, "AXIS[“ellipsoidal height 
(h)”,up,ORDER[3],LengthUnit[“kilometre”,1000]]");
         assertEquals("Ellipsoidal height", axis.getName().getCode(), "name");
@@ -208,6 +216,16 @@ public final class GeodeticObjectParserTest extends 
EPSGDependentTestCase {
         assertEquals("X", axis.getAbbreviation(), "abbreviation");
         
assertEquals(CoordinateSystems.directionAlongMeridian(AxisDirection.SOUTH, 90), 
axis.getDirection(), "direction");
         assertEquals(Units.METRE, axis.getUnit(), "unit");
+
+        axis = parse(CoordinateSystemAxis.class, 
"AXIS[“longitude”,EAST,order[2],UNIT[“degree”,0.0174532925199433],"
+                + 
"AxisMinValue[0],AxisMaxValue[360],RangeMeaning[wraparound]]");
+        assertEquals("Longitude", axis.getName().getCode(), "name");
+        assertEquals("λ", axis.getAbbreviation(), "abbreviation");
+        assertEquals(AxisDirection.EAST, axis.getDirection(), "direction");
+        assertEquals(Units.DEGREE, axis.getUnit(), "unit");
+        assertEquals(RangeMeaning.WRAPAROUND, axis.getRangeMeaning());
+        assertEquals(  0, axis.getMinimumValue());
+        assertEquals(360, axis.getMaximumValue());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java
index 1a556fdeb6..45c21869bf 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java
@@ -458,6 +458,7 @@ public final class WKTFormatTest extends 
EPSGDependentTestCase {
     @Test
     public void testFragments() throws ParseException {
         format = new WKTFormat();
+        format.setConvention(Convention.WKT2_2015);
         format.addFragment("deg",    "UNIT[“degree”, 0.0174532925199433]");
         format.addFragment("Bessel", "SPHEROID[“Bessel 1841”, 6377397.155, 
299.1528128, AUTHORITY[“EPSG”,“7004”]]");
         format.addFragment("Tokyo",  "DATUM[“Tokyo”, $Bessel]");
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
index 931da16470..5d52def9d4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
@@ -247,8 +247,8 @@ public final class DefaultGeographicCRSTest extends 
TestCase {
                 "    Ellipsoid[“NTF”, 6378249.2, 293.4660212936269]],\n" +
                 "    PrimeMeridian[“Paris”, 2.5969213, Unit[“grad”, 
0.015707963267948967]],\n" +
                 "  CS[ellipsoidal, 2],\n" +
-                "    Axis[“Longitude (L)”, east],\n" +                      // 
See method javadoc.
-                "    Axis[“Latitude (B)”, north],\n" +
+                "    Axis[“Longitude (L)”, east, AxisMinValue[-200.0], 
AxisMaxValue[200.0], RangeMeaning[wraparound]],\n" +
+                "    Axis[“Latitude (B)”, north, AxisMinValue[-100.0], 
AxisMaxValue[100.0], RangeMeaning[exact]],\n" +
                 "    Unit[“grad”, 0.015707963267948967]]",
                 HardCodedCRS.NTF);
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java
index faf4f9e82a..9392c3a5fc 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java
@@ -274,8 +274,8 @@ public final class DefaultProjectedCRSTest extends 
TestCase.WithLogs {
                 "      Id[“EPSG”, 6807]],\n" +
                 "      PrimeMeridian[“Paris”, 2.5969213, Id[“EPSG”, 8903]],\n" 
+
                 "    CS[ellipsoidal, 2],\n" +
-                "      Axis[“Longitude (λ)”, east],\n" +
-                "      Axis[“Latitude (φ)”, north],\n" +
+                "      Axis[“Longitude (λ)”, east, AxisMinValue[-200.0], 
AxisMaxValue[200.0], RangeMeaning[wraparound]],\n" +
+                "      Axis[“Latitude (φ)”, north, AxisMinValue[-100.0], 
AxisMaxValue[100.0], RangeMeaning[exact]],\n" +
                 "      Unit[“grad”, 0.015707963267948967, Id[“EPSG”, 
9105]]],\n" +
                 "  Conversion[“Lambert zone II”,\n" +
                 "    Method[“Lambert Conic Conformal (1SP)”, Id[“EPSG”, 9801], 
Id[“GeoTIFF”, 9]],\n" +
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java
index 4c5cc5e087..cbd10b0438 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java
@@ -88,25 +88,28 @@ public final class DefaultCoordinateSystemAxisTest extends 
TestCase {
      */
     @Test
     public void testWKT() {
-        assertWktEquals(Convention.WKT2, "AXIS[“x”, east, LENGTHUNIT[“metre”, 
1]]",  X);
-        assertWktEquals(Convention.WKT2, "AXIS[“y”, north, LENGTHUNIT[“metre”, 
1]]", Y);
-        assertWktEquals(Convention.WKT2, "AXIS[“z”, up, LENGTHUNIT[“metre”, 
1]]",    Z);
-        assertWktEquals(Convention.WKT2, "AXIS[“Longitude (λ)”, east, 
ANGLEUNIT[“grad”, 0.015707963267948967]]",             LONGITUDE_gon);
-        assertWktEquals(Convention.WKT2, "AXIS[“Latitude (φ)”, north, 
ANGLEUNIT[“grad”, 0.015707963267948967]]",             LATITUDE_gon);
-        assertWktEquals(Convention.WKT2, "AXIS[“Altitude (h)”, up, 
LENGTHUNIT[“metre”, 1]]",                                 ALTITUDE);
-        assertWktEquals(Convention.WKT2, "AXIS[“Time (t)”, future, 
TIMEUNIT[“day”, 86400]]",                                 TIME);
-        assertWktEquals(Convention.WKT2, "AXIS[“Longitude (λ)”, east, 
ANGLEUNIT[“degree”, 0.017453292519943295]]",           GEODETIC_LONGITUDE);
-        assertWktEquals(Convention.WKT2, "AXIS[“Spherical longitude (θ)”, 
east, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LONGITUDE);
-        assertWktEquals(Convention.WKT2, "AXIS[“Latitude (φ)”, north, 
ANGLEUNIT[“degree”, 0.017453292519943295]]",           GEODETIC_LATITUDE);
-        assertWktEquals(Convention.WKT2, "AXIS[“Spherical latitude (Ω)”, 
north, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LATITUDE);
-
-        assertWktEquals(Convention.WKT1,     "AXIS[“x”, EAST]",  X);
-        assertWktEquals(Convention.WKT1,     "AXIS[“y”, NORTH]", Y);
-        assertWktEquals(Convention.WKT1,     "AXIS[“z”, UP]",    Z);
-        assertWktEquals(Convention.INTERNAL, "Axis[“Geodetic longitude (λ)”, 
east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]",  
GEODETIC_LONGITUDE);
-        assertWktEquals(Convention.INTERNAL, "Axis[“Spherical longitude (θ)”, 
east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", 
SPHERICAL_LONGITUDE);
-        assertWktEquals(Convention.INTERNAL, "Axis[“Geodetic latitude (φ)”, 
north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]",  
GEODETIC_LATITUDE);
-        assertWktEquals(Convention.INTERNAL, "Axis[“Spherical latitude (Ω)”, 
north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", 
SPHERICAL_LATITUDE);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“x”, east, 
LENGTHUNIT[“metre”, 1]]",  X);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“y”, north, 
LENGTHUNIT[“metre”, 1]]", Y);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“z”, up, 
LENGTHUNIT[“metre”, 1]]",    Z);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Longitude (λ)”, east, 
ANGLEUNIT[“grad”, 0.015707963267948967]]",             LONGITUDE_gon);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Latitude (φ)”, north, 
ANGLEUNIT[“grad”, 0.015707963267948967]]",             LATITUDE_gon);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Altitude (h)”, up, 
LENGTHUNIT[“metre”, 1]]",                                 ALTITUDE);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Time (t)”, future, 
TIMEUNIT[“day”, 86400]]",                                 TIME);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Longitude (λ)”, east, 
ANGLEUNIT[“degree”, 0.017453292519943295]]",           GEODETIC_LONGITUDE);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Spherical longitude (θ)”, 
east, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LONGITUDE);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Latitude (φ)”, north, 
ANGLEUNIT[“degree”, 0.017453292519943295]]",           GEODETIC_LATITUDE);
+        assertWktEquals(Convention.WKT2_2015, "AXIS[“Spherical latitude (Ω)”, 
north, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LATITUDE);
+        assertWktEquals(Convention.WKT1,      "AXIS[“x”, EAST]",  X);
+        assertWktEquals(Convention.WKT1,      "AXIS[“y”, NORTH]", Y);
+        assertWktEquals(Convention.WKT1,      "AXIS[“z”, UP]",    Z);
+        assertWktEquals(Convention.INTERNAL,  "Axis[“Geodetic longitude (λ)”, 
east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]",  
GEODETIC_LONGITUDE);
+        assertWktEquals(Convention.INTERNAL,  "Axis[“Spherical longitude (θ)”, 
east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", 
SPHERICAL_LONGITUDE);
+        assertWktEquals(Convention.INTERNAL,  "Axis[“Geodetic latitude (φ)”, 
north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]",  
GEODETIC_LATITUDE);
+        assertWktEquals(Convention.INTERNAL,  "Axis[“Spherical latitude (Ω)”, 
north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", 
SPHERICAL_LATITUDE);
+        assertWktEquals(Convention.WKT2_2019, "AXIS[“Longitude (λ)”, east, 
ANGLEUNIT[“grad”, 0.015707963267948967], "
+                + "AXISMINVALUE[-200.0], AXISMAXVALUE[200.0], 
RANGEMEANING[wraparound]]", LONGITUDE_gon);
+        assertWktEquals(Convention.WKT2_2019, "AXIS[“Latitude (φ)”, north, 
ANGLEUNIT[“grad”, 0.015707963267948967], "
+                + "AXISMINVALUE[-100.0], AXISMAXVALUE[100.0], 
RANGEMEANING[exact]]", LATITUDE_gon);
     }
 
     /**


Reply via email to