You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/11/19 16:22:19 UTC

[sis] 01/01: Bug fixes related to unmarshalling of GML documents. Those bugs were identified by OGC TestBed 18 D025 scenario. This is a port of 3 commits on `master` branch.

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 ec0eaa68d945dadbc82f028fd79dc46b296aa2cb
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Nov 19 17:04:11 2022 +0100

    Bug fixes related to unmarshalling of GML documents.
    Those bugs were identified by OGC TestBed 18 D025 scenario.
    This is a port of 3 commits on `master` branch.
---
 .../sis/internal/jaxb/IdentifierMapAdapter.java    |  30 +++++-
 .../sis/internal/jaxb/SpecializedIdentifier.java   |   2 +
 .../org/apache/sis/internal/jaxb/package-info.java |   2 +-
 .../org/apache/sis/internal/metadata/Merger.java   |   6 +-
 .../java/org/apache/sis/xml/NilObjectHandler.java  |   2 +-
 .../java/org/apache/sis/xml/ReferenceResolver.java |   8 +-
 .../referencing/CC_GeneralOperationParameter.java  |  39 +++++--
 .../jaxb/referencing/CC_GeneralParameterValue.java |  11 +-
 .../jaxb/referencing/CC_OperationMethod.java       |  11 +-
 .../jaxb/referencing/CC_OperationParameter.java    |  54 +++++++---
 .../internal/jaxb/referencing/package-info.java    |   2 +-
 .../sis/internal/referencing/AxisDirections.java   |  57 +++++++++--
 .../sis/parameter/AbstractParameterDescriptor.java |   4 +-
 .../sis/parameter/DefaultParameterDescriptor.java  |  70 ++++++++++---
 .../sis/parameter/DefaultParameterValue.java       |  13 ++-
 .../sis/parameter/DefaultParameterValueGroup.java  |   8 +-
 .../org/apache/sis/parameter/ParameterFormat.java  |   3 +-
 .../java/org/apache/sis/parameter/Parameters.java  |   9 +-
 .../sis/parameter/UnmodifiableParameterValue.java  |  13 ++-
 .../sis/referencing/crs/AbstractDerivedCRS.java    |  13 +--
 .../sis/referencing/cs/CoordinateSystems.java      |   7 ++
 .../org/apache/sis/referencing/cs/Normalizer.java  |  26 +++--
 .../operation/AbstractCoordinateOperation.java     |  20 +---
 .../operation/AbstractSingleOperation.java         |   9 +-
 .../operation/DefaultConcatenatedOperation.java    |   2 +-
 .../referencing/operation/DefaultConversion.java   |  16 +--
 .../operation/DefaultOperationMethod.java          |  26 +++--
 .../operation/DefaultPassThroughOperation.java     | 112 +++++++++++++++------
 .../apache/sis/referencing/operation/SubTypes.java |   2 +-
 .../java/org/apache/sis/io/wkt/WKTParserTest.java  |   1 +
 .../storage/csv/MovingFeatureIterator.java         |   3 +-
 .../org/apache/sis/internal/storage/csv/Store.java |   2 +-
 32 files changed, 406 insertions(+), 177 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java
index 95dcf08a4c..2dc9ae3436 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java
@@ -18,8 +18,10 @@ package org.apache.sis.internal.jaxb;
 
 import java.net.URI;
 import java.util.Set;
+import java.util.List;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.Collection;
 import java.util.Collections;
@@ -81,7 +83,7 @@ import static org.apache.sis.util.collection.Containers.hashMapCapacity;
  * This class is thread safe if the underlying identifier collection is thread safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.3
  *
  * @see org.apache.sis.xml.IdentifiedObject
  *
@@ -101,7 +103,10 @@ public class IdentifierMapAdapter extends AbstractMap<Citation,String> implement
 
     /**
      * The identifiers to wrap in a map view.
+     *
+     * @see #getIdentifiers(Class)
      */
+    @SuppressWarnings("serial")         // Not statically typed as Serializable.
     public final Collection<Identifier> identifiers;
 
     /**
@@ -113,6 +118,29 @@ public class IdentifierMapAdapter extends AbstractMap<Citation,String> implement
         this.identifiers = identifiers;
     }
 
+    /**
+     * Returns the identifiers as a collection of the specified type.
+     * The given type is the return type of the {@code getIdentifiers()} method which is delegating to this method.
+     * The returned collection is modifiable only if {@link #identifiers} is already of the desired type.
+     * This is the case for {@link org.apache.sis.metadata.iso.ISOMetadata#getIdentifiers()},
+     * which is the API for which we want modifiable collections.
+     *
+     * @param  type  {@code Collection.class}, {@code List.class} or {@code Set.class}.
+     * @return the identifiers as a collection of the specified type.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public final Collection<Identifier> getIdentifiers(final Class<?> type) {
+        if (!type.isInstance(identifiers)) {
+            if (type.isAssignableFrom(Set.class)) {
+                return new HashSet<>(identifiers);      // TODO: use Set.copyOf in JDK10.
+            }
+            if (type.isAssignableFrom(List.class)) {
+                return new ArrayList<>(identifiers);    // TODO: use List.copyOf in JDK10.
+            }
+        }
+        return identifiers;
+    }
+
     /**
      * If the given authority is a special case, returns its {@link NonMarshalledAuthority} integer enum.
      * Otherwise returns -1. See javadoc for more information about special cases.
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
index 235660151b..1f6f91b95d 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
@@ -60,6 +60,7 @@ public final class SpecializedIdentifier<T> implements Identifier, Cloneable, Se
      *
      * @see #getAuthority()
      */
+    @SuppressWarnings("serial")         // Not statically typed as Serializable.
     private final IdentifierSpace<T> authority;
 
     /**
@@ -72,6 +73,7 @@ public final class SpecializedIdentifier<T> implements Identifier, Cloneable, Se
      * @see #getValue()
      * @see #getCode()
      */
+    @SuppressWarnings("serial")         // Not statically typed as Serializable.
     T value;
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/package-info.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/package-info.java
index 24080ec8e7..ba864f8ea2 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/package-info.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/package-info.java
@@ -35,7 +35,7 @@
  * @author  Cédric Briançon (Geomatys)
  * @author  Cullen Rombach (Image Matters)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.3
  * @module
  */
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
index d4e6d80b81..f1af008592 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
@@ -146,13 +146,13 @@ public class Merger {
          * we are going to merge those two metadata and verify that we are not in an infinite loop.
          * We will also verify that the target metadata does not contain a source, or vice-versa.
          */
-        {   // For keeping 'sourceDone' and 'targetDone' more local.
+        {   // For keeping `sourceDone` and `targetDone` more local.
             final Boolean sourceDone = done.put(source, Boolean.FALSE);
             final Boolean targetDone = done.put(target, Boolean.TRUE);
             if (sourceDone != null || targetDone != null) {
                 if (Boolean.FALSE.equals(sourceDone) && Boolean.TRUE.equals(targetDone)) {
                     /*
-                     * At least, the 'source' and 'target' status are consistent. Pretend that we have already
+                     * At least, the `source` and `target` status are consistent. Pretend that we have already
                      * merged those metadata since actually the merge operation is probably underway by the caller.
                      */
                     return true;
@@ -265,7 +265,7 @@ distribute:                 while (it.hasNext()) {
                     if (!success) {
                         if (dryRun) break;
                         merge(target, propertyName, sourceValue, targetValue);
-                        success = true;         // If no exception has been thrown by 'merged', assume the conflict solved.
+                        success = true;         // If no exception has been thrown by `merged`, assume the conflict solved.
                     }
                 }
             }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/xml/NilObjectHandler.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/NilObjectHandler.java
index bf4ce28745..4cb46aeeb6 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/xml/NilObjectHandler.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/NilObjectHandler.java
@@ -134,7 +134,7 @@ final class NilObjectHandler implements InvocationHandler {
                 }
                 case "getIdentifiers": {
                     return (attribute instanceof IdentifierMapAdapter) ?
-                            ((IdentifierMapAdapter) attribute).identifiers : null;
+                            ((IdentifierMapAdapter) attribute).getIdentifiers(method.getReturnType()) : null;
                 }
                 case "toString": {
                     return Strings.bracket(getInterface(proxy), attribute);
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/xml/ReferenceResolver.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/ReferenceResolver.java
index e570506272..9370c71f42 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/xml/ReferenceResolver.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/ReferenceResolver.java
@@ -68,8 +68,8 @@ public class ReferenceResolver {
      *   <li>{@link IdentifiedObject#getIdentifierMap()} will return a {@link java.util.Map}
      *       view over the given identifiers.</li>
      *   <li>All other methods except the ones inherited from the {@link Object} class will return
-     *       an empty collection, an empty array, {@code null}, {@link Double#NaN NaN}, 0 or
-     *       {@code false}, depending on the method return type.</li>
+     *       an empty collection, an empty array, {@code null}, {@link Double#NaN}, 0 or {@code false},
+     *       depending on the method return type.</li>
      * </ul>
      *
      * @param  <T>          the compile-time type of the {@code type} argument.
@@ -135,10 +135,10 @@ public class ReferenceResolver {
                 return type.cast(object);
             } else {
                 final short key;
-                final Object args;
+                final Object[] args;
                 if (object == null) {
                     key = Errors.Keys.NotABackwardReference_1;
-                    args = id;
+                    args = new Object[] {id};
                 } else {
                     key = Errors.Keys.UnexpectedTypeForReference_3;
                     args = new Object[] {id, type, object.getClass()};
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralOperationParameter.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralOperationParameter.java
index 717cec4cd2..c67f5b7249 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralOperationParameter.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralOperationParameter.java
@@ -38,11 +38,15 @@ import org.apache.sis.parameter.DefaultParameterValueGroup;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.GeodeticException;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.CorruptedObjectException;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.jaxb.gco.PropertyType;
 import org.apache.sis.internal.jaxb.Context;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.xml.IdentifiedObject;
+import org.apache.sis.xml.IdentifierSpace;
 
 
 /**
@@ -61,7 +65,7 @@ import org.apache.sis.internal.jaxb.Context;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -107,7 +111,7 @@ public final class CC_GeneralOperationParameter extends PropertyType<CC_GeneralO
     }
 
     /**
-     * Constructor for the {@link #wrap} method only.
+     * Constructor for the {@link #wrap(GeneralParameterDescriptor)} method only.
      */
     private CC_GeneralOperationParameter(final GeneralParameterDescriptor parameter) {
         super(parameter);
@@ -161,9 +165,32 @@ public final class CC_GeneralOperationParameter extends PropertyType<CC_GeneralO
     /**
      * Verifies that the given descriptor is non-null and contains at least a name.
      * This method is used after unmarshalling.
+     * We do this validation because parameter descriptors are mandatory and SIS classes need them.
+     * So we provide an error message here instead of waiting for a {@link NullPointerException}
+     * to occur in some arbitrary place.
+     *
+     * @param  descriptor  the descriptor to validate.
+     * @param  parent      the name of the element to report as the parent of {@code property}.
+     * @param  property    the name of the property to report as missing if an exception is thrown.
+     * @throws GeodeticException if the parameters are missing or invalid.
      */
-    static boolean isValid(final GeneralParameterDescriptor descriptor) {
-        return descriptor != null && descriptor.getName() != null;
+    static void validate(final GeneralParameterDescriptor descriptor, final String parent, final String property) {
+        if (descriptor == null || descriptor.getName() == null) {
+            short key = Errors.Keys.MissingComponentInElement_2;
+            String[] args = {parent, property};
+            /*
+             * The exception thrown by this method must be unchecked,
+             * otherwise JAXB just reports is without propagating it.
+             */
+            if (descriptor instanceof IdentifiedObject) {
+                final String link = ((IdentifiedObject) descriptor).getIdentifierMap().get(IdentifierSpace.XLINK);
+                if (link != null) {
+                    key = Errors.Keys.NotABackwardReference_1;
+                    args = new String[] {link};
+                }
+            }
+            throw new GeodeticException(Errors.format(key, args));
+        }
     }
 
     /**
@@ -259,8 +286,8 @@ public final class CC_GeneralOperationParameter extends PropertyType<CC_GeneralO
          * be invoked recursively for each parameter in the group.
          */
         final Map<String,Object> merged = new HashMap<>(expected);
-        merged.putAll(actual);  // May overwrite predefined properties.
-        mergeArrays(GeneralParameterDescriptor.ALIAS_KEY,       GenericName.class, provided.getAlias(),       merged, complete.getName());
+        merged.putAll(actual);                                      // May overwrite predefined properties.
+        mergeArrays(GeneralParameterDescriptor.ALIAS_KEY,       GenericName.class, provided.getAlias(), merged, complete.getName());
         mergeArrays(GeneralParameterDescriptor.IDENTIFIERS_KEY, Identifier.class,  provided.getIdentifiers(), merged, null);
         if (isGroup) {
             final List<GeneralParameterDescriptor> descriptors = ((ParameterDescriptorGroup) provided).descriptors();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralParameterValue.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralParameterValue.java
index 04ce67518b..7856c9dc1d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralParameterValue.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_GeneralParameterValue.java
@@ -24,7 +24,6 @@ import org.opengis.parameter.GeneralParameterValue;
 import org.apache.sis.parameter.DefaultParameterValue;
 import org.apache.sis.parameter.DefaultParameterValueGroup;
 import org.apache.sis.internal.jaxb.gco.PropertyType;
-import org.apache.sis.util.resources.Errors;
 
 
 /**
@@ -32,7 +31,7 @@ import org.apache.sis.util.resources.Errors;
  * package documentation for more information about JAXB and interface.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -110,13 +109,7 @@ public final class CC_GeneralParameterValue extends PropertyType<CC_GeneralParam
      * @param  parameter  the unmarshalled element.
      */
     public void setElement(final GeneralParameterValue parameter) {
-        if (!CC_GeneralOperationParameter.isValid(parameter.getDescriptor())) {
-            /*
-             * Descriptors are mandatory and SIS classes need them. Provide an error message
-             * here instead of waiting for a NullPointerException in some arbitrary place.
-             */
-            throw new IllegalArgumentException(Errors.format(Errors.Keys.MissingValueForProperty_1, "operationParameter"));
-        }
         metadata = parameter;
+        CC_GeneralOperationParameter.validate(parameter.getDescriptor(), "ParameterValue", "operationParameter");
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationMethod.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationMethod.java
index 0d565bc285..5d4ff2f852 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationMethod.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationMethod.java
@@ -32,7 +32,6 @@ import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.operation.OperationMethod;
 import org.apache.sis.internal.jaxb.Context;
 import org.apache.sis.internal.jaxb.gco.PropertyType;
-import org.apache.sis.internal.metadata.Identifiers;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.internal.referencing.provider.MapProjection;
 import org.apache.sis.parameter.DefaultParameterValue;
@@ -48,7 +47,7 @@ import org.apache.sis.util.ArraysExt;
  * package documentation for more information about JAXB and interface.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -108,14 +107,8 @@ public final class CC_OperationMethod extends PropertyType<CC_OperationMethod, O
      * @param  method  the unmarshalled element.
      */
     public void setElement(final DefaultOperationMethod method) {
-        if (!CC_GeneralOperationParameter.isValid(method.getParameters())) {
-            /*
-             * Parameters are mandatory and SIS classes need them. Provide an error message
-             * here instead of waiting for a NullPointerException in some arbitrary place.
-             */
-            throw new IllegalArgumentException(Identifiers.missingValueForProperty(method.getName(), "parameters"));
-        }
         metadata = method;
+        CC_GeneralOperationParameter.validate(method.getParameters(), "OperationMethod", "parameter");
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationParameter.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationParameter.java
index a00d2a31f0..7970ef1f42 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationParameter.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/CC_OperationParameter.java
@@ -37,7 +37,7 @@ import org.apache.sis.parameter.DefaultParameterDescriptor;
  * infer it from the actual value.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -118,6 +118,40 @@ public final class CC_OperationParameter extends PropertyType<CC_OperationParame
         metadata = parameter;
     }
 
+    /**
+     * Returns the base class of parameter values, or {@code null} if unknown.
+     * This method assumes that the parameter value does not yet have a descriptor
+     * (which happens at GML unmarshalling time) and that the type must be derived
+     * from the actual value.
+     *
+     * @param  param  the parameter from which to get the value class.
+     * @return base class of values, or {@code null} if unknown.
+     */
+    public static Class<?> valueClass(final ParameterValue<?> param) {
+        final Object value = param.getValue();
+        return (value != null) ? value.getClass() : null;
+    }
+
+    /**
+     * Saves the unit of measurement in a boundless range. This method should be invoked only when
+     * {@link #valueClass} is {@link Double} or {@code double[]}. It is the case in a well-formed GML.
+     *
+     * @param  param  the parameter from which to get the unit of measurement.
+     * @return unit of measurement wrapped in a boundless range, or {@code null} if none.
+     */
+    public static MeasurementRange<?> valueDomain(final ParameterValue<?> param) {
+        Unit<?> unit = param.getUnit();
+        if (unit == null) {
+            return null;
+        }
+        unit = unit.getSystemUnit();
+        if (Units.RADIAN.equals(unit)) {
+            unit = Units.DEGREE;
+        }
+        return MeasurementRange.create(Double.NEGATIVE_INFINITY, false,
+                                       Double.POSITIVE_INFINITY, false, unit);
+    }
+
     /**
      * Invoked by JAXB during unmarshalling of the enclosing {@code <gml:OperationParameter>},
      * before the child {@link DefaultParameterDescriptor}. This method stores the class and
@@ -129,21 +163,9 @@ public final class CC_OperationParameter extends PropertyType<CC_OperationParame
      */
     private void beforeUnmarshal(final Unmarshaller unmarshaller, final Object parent) {
         if (parent instanceof ParameterValue<?>) {
-            final Object value = ((ParameterValue<?>) parent).getValue();
-            if (value != null) {
-                valueClass = value.getClass();
-                Unit<?> unit = ((ParameterValue<?>) parent).getUnit();
-                if (unit != null) {
-                    unit = unit.getSystemUnit();
-                    if (Units.RADIAN.equals(unit)) {
-                        unit = Units.DEGREE;
-                    }
-                    assert (valueClass == Double.class) || (valueClass == double[].class) : valueClass;
-                    valueDomain = MeasurementRange.create(Double.NEGATIVE_INFINITY, false,
-                                                          Double.POSITIVE_INFINITY, false, unit);
-                }
-                Context.setWrapper(Context.current(), this);
-            }
+            valueClass  = valueClass ((ParameterValue<?>) parent);
+            valueDomain = valueDomain((ParameterValue<?>) parent);
+            Context.setWrapper(Context.current(), this);
         }
     }
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/package-info.java
index 90b01e5d26..f1ca5cea95 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/package-info.java
@@ -24,7 +24,7 @@
  * @author  Guilhem Legal (Geomatys)
  * @author  Cédric Briançon (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.3
  *
  * @see javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter
  *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AxisDirections.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AxisDirections.java
index 61b238cb1c..1ce30a6e87 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AxisDirections.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AxisDirections.java
@@ -44,7 +44,7 @@ import static org.apache.sis.util.CharSequences.*;
  * Utilities methods related to {@link AxisDirection}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.4
  * @module
  */
@@ -76,17 +76,31 @@ public final class AxisDirections extends Static {
     private static final int LAST_ORDINAL = DISPLAY_DOWN.ordinal();
 
     /**
-     * Distance from the origin in a polar coordinate system.
-     * Specified in ISO 19162 but not yet in ISO 19111.
+     * Forward direction.
+     * For an observer at the centre of the object this is will be towards its front, bow or nose.
+     * Added in ISO 19111:2019 (was not in ISO 19111:2007).
      *
-     * @since 0.7
+     * @since 1.3
+     */
+    @UML(identifier="forward", obligation=CONDITIONAL, specification=ISO_19162)
+    public static final AxisDirection FORWARD = AxisDirection.valueOf("FORWARD");
+    /*
+     * TODO: remove @Ignore in `WKTParserTest` after the code list values in this class have been removed.
      */
-    @UML(identifier="awayFrom", obligation=CONDITIONAL, specification=ISO_19162)
-    public static final AxisDirection AWAY_FROM = AxisDirection.valueOf("AWAY_FROM");
+
+    /**
+     * Starboard direction.
+     * For an observer at the centre of the object this will be towards its right.
+     * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+     *
+     * @since 1.3
+     */
+    @UML(identifier="starboard", obligation=CONDITIONAL, specification=ISO_19162)
+    public static final AxisDirection STARBOARD = AxisDirection.valueOf("STARBOARD");
 
     /**
      * Direction of geographic angles (bearing).
-     * Specified in ISO 19162 but not yet in ISO 19111.
+     * Added in ISO 19111:2019 (was not in ISO 19111:2007).
      *
      * @since 0.7
      */
@@ -95,13 +109,22 @@ public final class AxisDirections extends Static {
 
     /**
      * Direction of arithmetic angles. Used in polar coordinate systems.
-     * Specified in ISO 19162 but not yet in ISO 19111.
+     * Added in ISO 19111:2019 (was not in ISO 19111:2007).
      *
      * @since 0.7
      */
     @UML(identifier="counterClockwise", obligation=CONDITIONAL, specification=ISO_19162)
     public static final AxisDirection COUNTER_CLOCKWISE = AxisDirection.valueOf("COUNTER_CLOCKWISE");
 
+    /**
+     * Distance from the origin in a polar coordinate system.
+     * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+     *
+     * @since 0.7
+     */
+    @UML(identifier="awayFrom", obligation=CONDITIONAL, specification=ISO_19162)
+    public static final AxisDirection AWAY_FROM = AxisDirection.valueOf("AWAY_FROM");
+
     /**
      * For each direction, the opposite direction.
      * This map shall be immutable after construction.
@@ -345,6 +368,20 @@ public final class AxisDirections extends Static {
         return ordinal >= COLUMN_POSITIVE.ordinal() && ordinal <= ROW_NEGATIVE.ordinal();
     }
 
+    /**
+     * Arithmetic angle between forward/aft/port/starboard directions only.
+     * This is the angle as viewed from above the vehicle.
+     *
+     * @param  source  the start direction.
+     * @param  target  the final direction.
+     * @return the angle as a multiple of 90°, or {@link Integer#MIN_VALUE} if none.
+     */
+    public static int angleForVehicle(final AxisDirection source, final AxisDirection target) {
+        if (source == STARBOARD && target == FORWARD) return +1;
+        if (source == FORWARD && target == STARBOARD) return -1;
+        return Integer.MIN_VALUE;
+    }
+
     /**
      * Angle between geocentric directions only.
      *
@@ -367,7 +404,7 @@ public final class AxisDirections extends Static {
     }
 
     /**
-     * Angle between compass directions only (not for angle between direction along meridians).
+     * Arithmetic angle between compass directions only (not for angle between direction along meridians).
      *
      * @param  source  the start direction.
      * @param  target  the final direction.
@@ -394,7 +431,7 @@ public final class AxisDirections extends Static {
     }
 
     /**
-     * Angle between display directions only.
+     * Arithmetic angle between display directions only.
      *
      * @param  source  the start direction.
      * @param  target  the final direction.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java
index 7ad14d59cf..4e2d96d22d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java
@@ -124,7 +124,7 @@ public abstract class AbstractParameterDescriptor extends AbstractIdentifiedObje
      * The maximum number of times that values for this parameter group are required, as an unsigned short.
      * Value {@code 0xFFFF} (or -1) means an unrestricted number of occurrences.
      *
-     * <p>We use a short because this value is usually 1 or a very small number like 2 or 3. This also serve
+     * <p>We use a short because this value is usually 1 or a very small number like 2 or 3. It also serves
      * as a safety since a large number would be a bad idea with this parameter implementation.</p>
      *
      * <p><b>Consider this field as final!</b>
@@ -203,7 +203,7 @@ public abstract class AbstractParameterDescriptor extends AbstractIdentifiedObje
         maximumOccurs = crop(descriptor.getMaximumOccurs());
     }
 
-    // NOTE: There is no 'castOrCopy' static method in this class because AbstractParameterDescriptor is abstract.
+    // NOTE: There is no `castOrCopy` static method in this class because AbstractParameterDescriptor is abstract.
     // If nevertheless we choose to add such method in the future, then CC_GeneralOperationParameter.getElement()
     // should be simplified.
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterDescriptor.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterDescriptor.java
index e3005a2139..4e0cc586da 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterDescriptor.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterDescriptor.java
@@ -87,10 +87,12 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
 
     /**
      * The class that describe the type of parameter values.
+     * This field should be considered final after construction.
+     * This is declared non-final only for GML unmarshalling.
      *
      * @see #getValueClass()
      */
-    private final Class<T> valueClass;
+    private Class<T> valueClass;
 
     /**
      * A set of valid values (usually from a {@linkplain CodeList code list})
@@ -112,9 +114,12 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
      *       <code>valueClass.{@linkplain Class#getComponentType() getComponentType()}</code>.</li>
      * </ul>
      *
+     * This field should be considered final after construction.
+     * This is declared non-final only for GML unmarshalling.
+     *
      * @see #getValueDomain()
      */
-    private final Range<?> valueDomain;
+    private Range<?> valueDomain;
 
     /**
      * The default value for the parameter, or {@code null}.
@@ -272,7 +277,6 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
      *
      * @see #castOrCopy(ParameterDescriptor)
      */
-    @SuppressWarnings("unchecked")
     protected DefaultParameterDescriptor(final ParameterDescriptor<T> descriptor) {
         super(descriptor);
         valueClass   = descriptor.getValueClass();
@@ -399,6 +403,7 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
     @Override
     @SuppressWarnings("unchecked")
     public Comparable<T> getMinimumValue() {
+        final Range<?> valueDomain = this.valueDomain;
         return (valueDomain != null && valueDomain.getElementType() == valueClass)
                ? (Comparable<T>) valueDomain.getMinValue() : null;
     }
@@ -417,6 +422,7 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
     @Override
     @SuppressWarnings("unchecked")
     public Comparable<T> getMaximumValue() {
+        final Range<?> valueDomain = this.valueDomain;
         return (valueDomain != null && valueDomain.getElementType() == valueClass)
                ? (Comparable<T>) valueDomain.getMaxValue() : null;
     }
@@ -447,6 +453,7 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
      */
     @Override
     public Unit<?> getUnit() {
+        final Range<?> valueDomain = this.valueDomain;
         return (valueDomain instanceof MeasurementRange<?>) ? ((MeasurementRange<?>) valueDomain).unit() : null;
     }
 
@@ -517,10 +524,10 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
                 }
                 case STRICT: {
                     final DefaultParameterDescriptor<?> that = (DefaultParameterDescriptor<?>) object;
-                    return                    this.valueClass == that.valueClass   &&
-                           Objects.    equals(this.validValues,  that.validValues) &&
-                           Objects.    equals(this.valueDomain,  that.valueDomain) &&
-                           Objects.deepEquals(this.defaultValue, that.defaultValue);
+                    return                    valueClass == that.valueClass   &&
+                           Objects.    equals(validValues,  that.validValues) &&
+                           Objects.    equals(valueDomain,  that.valueDomain) &&
+                           Objects.deepEquals(defaultValue, that.defaultValue);
                 }
             }
         }
@@ -553,7 +560,7 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
 
 
     /**
-     * Constructs a new object in which every attributes are set to a null value.
+     * Constructs a new object in which attributes may be set to a null value.
      * <strong>This is not a valid object.</strong> This constructor is strictly
      * reserved to JAXB, which will assign values to the fields using reflection.
      *
@@ -570,16 +577,55 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
              * This unsafe cast would be forbidden if this constructor was public or used in any context where the
              * user can choose the value of <T>. But this constructor should be invoked only during unmarshalling,
              * after the creation of the ParameterValue (this is the reverse creation order than what we normally
-             * do through the public API). The 'valueClass' should be compatible with DefaultParameterValue.value,
+             * do through the public API). The `valueClass` should be compatible with DefaultParameterValue.value,
              * and the parameterized type visible to the user should be only <?>.
              */
             valueClass  = (Class) param.valueClass;
             valueDomain = param.valueDomain;
-        } else {
-            valueClass  = null;
-            valueDomain = null;
         }
         validValues  = null;
         defaultValue = null;
     }
+
+    /**
+     * Invoked by {@link DefaultParameterValue} when the descriptor is set after the value at unmarshalling time.
+     * There is two scenarios in a valid GML document. The first scenario is when the descriptor is defined inside
+     * the parameter value element, like below. In such case, {@link #valueClass} is defined at construction time
+     * by {@link #DefaultParameterDescriptor()} because the value is before the descriptor.
+     *
+     * {@preformat xml
+     *   <gml:ParameterValue>
+     *     <gml:value uom="…">…</gml:value>
+     *     <gml:operationParameter>
+     *       <gml:OperationParameter>
+     *         …
+     *       </gml:OperationParameter>
+     *     </gml:operationParameter>
+     *   </gml:ParameterValue>
+     * }
+     *
+     * In the second scenario shows below, the descriptor was defined before the value and is referenced by a link.
+     * In that case, {@link #valueClass} is {@code null} the first time that this method is invoked. It may become
+     * non-null if the same parameter descriptor is reused for many parameter values.
+     *
+     * {@preformat xml
+     *   <gml:ParameterValue>
+     *     <gml:value uom="…">…</gml:value>
+     *     <gml:operationParameter xlink:href="#LongitudeRotation"/>
+     *   </gml:ParameterValue>
+     * }
+     *
+     * This method modifies the state of this class despite the fact that it should be immutable.
+     * It is okay because we are updating an instance created during GML unmarshalling, and that
+     * instance should not have been given to user yet.
+     *
+     * @param  param  the parameter value from which to infer the value type.
+     */
+    @SuppressWarnings("unchecked")
+    final void setValueClass(final DefaultParameterValue<?> param) {
+        valueClass = (Class) Classes.findCommonClass(valueClass, CC_OperationParameter.valueClass(param));
+        if (valueDomain == null) {
+            valueDomain = CC_OperationParameter.valueDomain(param);
+        }
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java
index 3a3fd6be37..6c423cdcd0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValue.java
@@ -118,7 +118,7 @@ import static org.apache.sis.util.Utilities.deepEquals;
  * for modifying the behavior of all getter and setter methods.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @param  <T>  the type of the value stored in this parameter.
  *
@@ -1129,14 +1129,21 @@ convert:            if (componentType != null) {
 
     /**
      * Invoked by JAXB at unmarshalling time.
-     * May also be invoked by {@link DefaultParameterValueGroup} if the descriptor as been completed
+     * May also be invoked by {@link DefaultParameterValueGroup} if the descriptor has been completed
      * with additional information provided in the {@code <gml:group>} element of a descriptor group.
      *
      * @see #getDescriptor()
      */
     final void setDescriptor(final ParameterDescriptor<T> descriptor) {
         this.descriptor = descriptor;
-        assert (value == null) || descriptor.getValueClass().isInstance(value) : this;
+        if (descriptor instanceof DefaultParameterDescriptor<?>) {
+            ((DefaultParameterDescriptor<?>) descriptor).setValueClass(this);
+        }
+        /*
+         * A previous version was doing `assert descriptor.getValueClass().isInstance(value)`
+         * where the value class was inferred by `DefaultParameterDescriptor()`. But it does
+         * not always work, and the `NullPointerException` seems to be caught by JAXB.
+         */
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValueGroup.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValueGroup.java
index b0c28806aa..e28988010e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValueGroup.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterValueGroup.java
@@ -559,7 +559,7 @@ scan:   for (final GeneralParameterValue param : actual.values()) {
     /**
      * Invoked by JAXB for setting the group parameter descriptor. Those parameter are redundant with
      * the parameters associated to the values given to {@link #setValues(GeneralParameterValue[])},
-     * except the the group identification (name, <i>etc.</i>) and for any optional parameters which
+     * except for the group identification (name, <i>etc.</i>) and for any optional parameters which
      * were not present in the above {@code GeneralParameterValue} array.
      *
      * @see #getDescriptor()
@@ -611,9 +611,9 @@ scan:   for (final GeneralParameterValue param : actual.values()) {
      * Appends all parameter values. In this process, we may need to update the descriptor of some values
      * if those descriptors changed as a result of the above merge process.
      *
-     * @param parameters   The parameters to add, or {@code null} for {@link #values}.
-     * @param replacements The replacements to apply in the {@code GeneralParameterValue} instances.
-     * @param addTo        Where to store the new values.
+     * @param parameters    the parameters to add, or {@code null} for {@link #values}.
+     * @param replacements  the replacements to apply in the {@code GeneralParameterValue} instances.
+     * @param addTo         where to store the new values.
      */
     @SuppressWarnings({"unchecked", "AssignmentToCollectionOrArrayFieldFromParameter"})
     private void setValues(GeneralParameterValue[] parameters,
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java
index 7d7131bb37..e34af12dd4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java
@@ -692,7 +692,8 @@ public class ParameterFormat extends TabularFormat<Object> {
                 /*
                  * Writes the values, each on its own line, together with their unit of measurement.
                  */
-                final byte alignment = Number.class.isAssignableFrom(valueClass) ? TableAppender.ALIGN_RIGHT : TableAppender.ALIGN_LEFT;
+                final byte alignment = (valueClass != null && Number.class.isAssignableFrom(valueClass))
+                                     ? TableAppender.ALIGN_RIGHT : TableAppender.ALIGN_LEFT;
                 table.setCellAlignment(alignment);
                 final int length = row.values.size();
                 for (int i=0; i<length; i++) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java
index f20dd66569..ed74415469 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java
@@ -38,6 +38,7 @@ import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 
 
@@ -356,9 +357,15 @@ public abstract class Parameters implements ParameterValueGroup, Cloneable {
             if (descriptor instanceof DefaultParameterDescriptor<?>) {
                 return ((DefaultParameterDescriptor<?>) descriptor).getValueDomain();
             }
-            final Class<?> valueClass = descriptor.getValueClass();
+            Class<?> valueClass = descriptor.getValueClass();
             final Comparable<?> minimumValue = descriptor.getMinimumValue();
             final Comparable<?> maximumValue = descriptor.getMaximumValue();
+            if (valueClass == null) {       // Should never be null, but invalid objects exist.
+                valueClass = Classes.findCommonClass(Classes.getClass(minimumValue), Classes.getClass(maximumValue));
+                if (valueClass == null) {
+                    valueClass = Object.class;
+                }
+            }
             if ((minimumValue == null || valueClass.isInstance(minimumValue)) &&
                 (maximumValue == null || valueClass.isInstance(maximumValue)))
             {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValue.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValue.java
index bccdeb3951..4fa432ec27 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValue.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValue.java
@@ -49,7 +49,7 @@ import org.apache.sis.util.resources.Errors;
  * </div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.3
  *
  * @param <T>  the type of the value stored in this parameter.
  *
@@ -100,10 +100,13 @@ final class UnmodifiableParameterValue<T> extends DefaultParameterValue<T> {
     @Override
     public T getValue() {
         T value = super.getValue();
-        if (value instanceof Cloneable) try {
-            value = getDescriptor().getValueClass().cast(Cloner.cloneIfPublic(value));
-        } catch (CloneNotSupportedException e) {
-            throw new UnsupportedOperationException(Errors.format(Errors.Keys.CloneNotSupported_1, value.getClass()), e);
+        if (value instanceof Cloneable) {
+            final Class<T> type = getDescriptor().getValueClass();      // May be null after GML unmarshalling.
+            if (type != null) try {
+                value = type.cast(Cloner.cloneIfPublic(value));
+            } catch (CloneNotSupportedException e) {
+                throw new UnsupportedOperationException(Errors.format(Errors.Keys.CloneNotSupported_1, value.getClass()), e);
+            }
         }
         return value;
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/AbstractDerivedCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/AbstractDerivedCRS.java
index 0f167cd410..4b3a1316d9 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/AbstractDerivedCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/AbstractDerivedCRS.java
@@ -22,7 +22,6 @@ import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlSeeAlso;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
-import javax.xml.bind.ValidationException;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.datum.Datum;
 import org.opengis.referencing.crs.SingleCRS;
@@ -34,12 +33,12 @@ import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.geometry.MismatchedDimensionException;
+import org.apache.sis.referencing.GeodeticException;
 import org.apache.sis.referencing.operation.DefaultConversion;
 import org.apache.sis.internal.jaxb.referencing.CC_Conversion;
 import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
 import org.apache.sis.internal.metadata.ImplementationHelper;
 import org.apache.sis.internal.metadata.Identifiers;
-import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.system.Semaphores;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
@@ -54,7 +53,7 @@ import static org.apache.sis.util.Utilities.deepEquals;
  * (not by a {@linkplain org.apache.sis.referencing.datum.AbstractDatum datum}).
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @param <C>  the conversion type, either {@code Conversion} or {@code Projection}.
  *
@@ -82,6 +81,7 @@ abstract class AbstractDerivedCRS<C extends Conversion> extends AbstractCRS impl
      *
      * @see #getConversionFromBase()
      */
+    @SuppressWarnings("serial")         // Not statically typed as Serializable.
     private C conversionFromBase;
 
     /**
@@ -162,9 +162,6 @@ abstract class AbstractDerivedCRS<C extends Conversion> extends AbstractCRS impl
         if (properties != null) {
             factory = (MathTransformFactory) properties.get(ReferencingFactoryContainer.MT_FACTORY);
         }
-        if (factory == null) {
-            factory = DefaultFactories.forBuildin(MathTransformFactory.class);
-        }
         try {
             return DefaultConversion.castOrCopy(conversion).specialize(getConversionType(), baseCRS, this, factory);
         } catch (FactoryException e) {
@@ -332,7 +329,7 @@ abstract class AbstractDerivedCRS<C extends Conversion> extends AbstractCRS impl
      * coordinate system (CS). The CS information is required by {@code createConversionFromBase(…)}
      * in order to create a {@link MathTransform} with correct axis swapping and unit conversions.
      */
-    private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) throws ValidationException {
+    private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
         String property = "conversion";
         if (conversionFromBase != null) {
             final SingleCRS baseCRS = CC_Conversion.setBaseCRS(conversionFromBase, null);  // Clear the temporary value now.
@@ -350,6 +347,6 @@ abstract class AbstractDerivedCRS<C extends Conversion> extends AbstractCRS impl
          * and call to `getConversionFromBase()` will throw a ClassCastException if this instance is actually
          * a ProjectedCRS (because of the method overriding with return type covariance).
          */
-        throw new ValidationException(Identifiers.missingValueForProperty(getName(), property));
+        throw new GeodeticException(Identifiers.missingValueForProperty(getName(), property));
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
index d8134a7a9c..531b4a2401 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
@@ -239,6 +239,13 @@ public final class CoordinateSystems extends Static {
         if (c != Integer.MIN_VALUE) {
             return new Angle(c * 90);
         }
+        /*
+         * Check for FORWARD, AFT, PORT, STARBOARD.
+         */
+        c = AxisDirections.angleForVehicle(source, target);
+        if (c != Integer.MIN_VALUE) {
+            return new Angle(c * 90);
+        }
         /*
          * Check for DISPLAY_UP, DISPLAY_DOWN, etc. assuming a flat screen.
          * Note that we do not check for grid directions (COLUMN_POSITIVE,
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Normalizer.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Normalizer.java
index dfdc5672c3..8010bc2365 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Normalizer.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Normalizer.java
@@ -106,7 +106,7 @@ final class Normalizer implements Comparable<Normalizer> {
      *
      * @see #order(AxisDirection)
      */
-    private static final int SHIFT = 2;
+    private static final int SHIFT = 3;
 
     /**
      * Custom code list values to handle as if the where defined between two GeoAPI values.
@@ -115,12 +115,15 @@ final class Normalizer implements Comparable<Normalizer> {
      */
     private static final Map<AxisDirection,Integer> ORDER = new HashMap<>();
     static {
-        final Map<AxisDirection,Integer> m = ORDER;
         // Get ordinal of last compass direction defined by GeoAPI. We will continue on the horizontal plane.
-        final int horizontal = (AxisDirection.NORTH.ordinal() + (AxisDirections.COMPASS_COUNT - 1)) << SHIFT;
-        m.put(AxisDirections.AWAY_FROM,         horizontal + 1);
-        m.put(AxisDirections.COUNTER_CLOCKWISE, horizontal + 2);
-        m.put(AxisDirections.CLOCKWISE,         horizontal + 3);
+        int code = (AxisDirection.NORTH.ordinal() + (AxisDirections.COMPASS_COUNT - 1)) << SHIFT;
+        for (final AxisDirection d : new AxisDirection[] {
+            AxisDirections.FORWARD,
+            AxisDirections.STARBOARD,
+            AxisDirections.COUNTER_CLOCKWISE,
+            AxisDirections.CLOCKWISE,
+            AxisDirections.AWAY_FROM
+        }) ORDER.put(d, ++code);
     }
 
     /**
@@ -170,8 +173,9 @@ final class Normalizer implements Comparable<Normalizer> {
         if (d == 0) {
             final AxisDirection d1 = this.axis.getDirection();
             final AxisDirection d2 = that.axis.getDirection();
-            d = AxisDirections.angleForCompass(d2, d1);
-            if (d == Integer.MIN_VALUE) {
+            if ((d = AxisDirections.angleForCompass(d2, d1)) == Integer.MIN_VALUE &&
+                (d = AxisDirections.angleForVehicle(d2, d1)) == Integer.MIN_VALUE)
+            {
                 if (meridian != null) {
                     if (that.meridian != null) {
                         d = meridian.compareTo(that.meridian);
@@ -445,10 +449,10 @@ final class Normalizer implements Comparable<Normalizer> {
      */
     static AbstractCS forConvention(final CoordinateSystem cs, final AxesConvention convention) {
         switch (convention) {
-            case NORMALIZED:              // Fall through
+            case NORMALIZED:       // Fall through
             case DISPLAY_ORIENTED: return normalize(cs, convention, true);
-            case RIGHT_HANDED:            return normalize(cs, null, true);
-            case POSITIVE_RANGE:          return shiftAxisRange(cs);
+            case RIGHT_HANDED:     return normalize(cs, null, true);
+            case POSITIVE_RANGE:   return shiftAxisRange(cs);
             default: throw new AssertionError(convention);
         }
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 1528ffa922..6f7b8d5b96 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -670,23 +670,8 @@ check:      for (int isTarget=0; ; isTarget++) {        // 0 == source check; 1
     }
 
     /**
-     * Returns the object for transforming coordinates in the {@linkplain #getSourceCRS() source CRS}
-     * to coordinates in the {@linkplain #getTargetCRS() target CRS}.
-     *
-     * <h4>Use with interpolation CRS</h4>
-     * If the {@linkplain #getInterpolationCRS() interpolation CRS} is non-null, then the math transform
-     * input coordinates shall by (<var>interpolation</var>, <var>source</var>) tuples: for each value
-     * to transform, the interpolation point coordinates shall be first, followed by the source coordinates.
-     *
-     * <div class="note"><b>Example:</b>
-     * in a transformation between two {@linkplain org.apache.sis.referencing.crs.DefaultVerticalCRS vertical CRS},
-     * if the {@linkplain #getSourceCRS() source} coordinates are (<var>z</var>) values but the coordinate operation
-     * additionally requires (<var>x</var>,<var>y</var>) values for {@linkplain #getInterpolationCRS() interpolation}
-     * purpose, then the math transform input coordinates shall be (<var>x</var>,<var>y</var>,<var>z</var>) tuples in
-     * that order.</div>
-     *
-     * The interpolation coordinates will {@linkplain DefaultPassThroughOperation pass through the operation}
-     * and appear in the math transform outputs, in the same order than inputs.
+     * Returns the object for transforming coordinates in the source CRS to coordinates in the target CRS.
+     * The transform may be {@code null} if this coordinate operation is a defining conversion.
      *
      * @return the transform from source to target CRS, or {@code null} if not applicable.
      */
@@ -1208,6 +1193,7 @@ check:      for (int isTarget=0; ; isTarget++) {        // 0 == source check; 1
 
     /**
      * Invoked by JAXB after unmarshalling.
+     * May be overridden by subclasses.
      */
     void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
         computeTransientFields();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java
index 128cea0c0c..a44cc95a55 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java
@@ -49,6 +49,7 @@ import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.metadata.ImplementationHelper;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.metadata.Identifiers;
+import org.apache.sis.referencing.GeodeticException;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
@@ -408,7 +409,7 @@ class AbstractSingleOperation extends AbstractCoordinateOperation implements Sin
     private void setParameters(final GeneralParameterValue[] values) {
         if (parameters == null) {
             if (!(method instanceof DefaultOperationMethod)) {  // May be a non-null proxy if defined only by xlink:href.
-                throw new IllegalStateException(Identifiers.missingValueForProperty(getName(), "method"));
+                throw new GeodeticException(Identifiers.missingValueForProperty(getName(), "method"));
             }
             /*
              * The descriptors in the <gml:method> element do not know the class of parameter value
@@ -463,6 +464,12 @@ class AbstractSingleOperation extends AbstractCoordinateOperation implements Sin
     @Override
     final void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
         super.afterUnmarshal(unmarshaller, parent);
+        if (parameters == null && method != null) {
+            final ParameterDescriptorGroup descriptor = method.getParameters();
+            if (descriptor != null && descriptor.descriptors().isEmpty()) {
+                parameters = descriptor.createValue();
+            }
+        }
         final CoordinateReferenceSystem sourceCRS = super.getSourceCRS();
         final CoordinateReferenceSystem targetCRS = super.getTargetCRS();
         if (transform == null && sourceCRS != null && targetCRS != null && parameters != null) try {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
index 33808373a9..6106052ceb 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
@@ -242,7 +242,7 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp
             } else if (!step.isIdentity()) {
                 flattened.add(op);
             }
-            if (mtFactory != null && step != null) {
+            if (mtFactory != null) {
                 transform = (transform != null) ? mtFactory.createConcatenatedTransform(transform, step) : step;
             }
             /*
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java
index fe908c6503..ccc51273de 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java
@@ -36,6 +36,7 @@ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactor
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Utilities;
@@ -217,8 +218,8 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver
 
     /**
      * Constructs a new conversion with the same values than the specified one, together with the
-     * specified source and target CRS. While the source conversion can be an arbitrary one, it is
-     * typically a defining conversion.
+     * specified source and target CRS. While the source conversion can be an arbitrary one,
+     * it is typically a defining conversion.
      *
      * @param definition  the defining conversion.
      * @param source      the new source CRS.
@@ -376,13 +377,14 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver
      *
      * This {@code specialize(…)} method returns a conversion which implement at least the given {@code baseType}
      * interface, but may also implement a more specific GeoAPI interface if {@code specialize(…)} has been able
-     * to infer the type from this operation {@linkplain #getMethod() method}.
+     * to infer the type from the {@linkplain #getMethod() operation method}.
      *
      * @param  <T>        compile-time type of the {@code baseType} argument.
      * @param  baseType   the base GeoAPI interface to be implemented by the conversion to return.
      * @param  sourceCRS  the source CRS.
      * @param  targetCRS  the target CRS.
-     * @param  factory    the factory to use for creating a transform from the parameters or for performing axis changes.
+     * @param  factory    the factory to use for creating a transform from the parameters or for performing axis changes,
+     *                    or {@code null} for the default factory.
      * @return the conversion of the given type between the given CRS.
      * @throws ClassCastException if a contradiction is found between the given {@code baseType},
      *         the defining {@linkplain DefaultConversion#getInterface() conversion type} and
@@ -397,12 +399,11 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver
      */
     public <T extends Conversion> T specialize(final Class<T> baseType,
             final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS,
-            final MathTransformFactory factory) throws FactoryException
+            MathTransformFactory factory) throws FactoryException
     {
         ArgumentChecks.ensureNonNull("baseType",  baseType);
         ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
         ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
-        ArgumentChecks.ensureNonNull("factory",   factory);
         /*
          * Conceptual consistency check: verify that the new CRS use the same datum than the previous ones,
          * since the purpose of this method is not to apply datum changes. Datum changes are the purpose of
@@ -425,6 +426,9 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver
                 ensureCompatibleDatum("targetCRS", sourceCRS, super.getTargetCRS());
             }
         }
+        if (factory == null) {
+            factory = DefaultFactories.forBuildin(MathTransformFactory.class);
+        }
         return SubTypes.create(baseType, this, sourceCRS, targetCRS, factory);
     }
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultOperationMethod.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultOperationMethod.java
index 9d6b58bde6..e195d20aaf 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultOperationMethod.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultOperationMethod.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
 import java.util.Collections;
+import javax.xml.bind.Unmarshaller;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
@@ -117,7 +118,7 @@ import static org.apache.sis.util.ArgumentChecks.*;
  * {@link org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see DefaultConversion
  * @see DefaultTransformation
@@ -192,7 +193,8 @@ public class DefaultOperationMethod extends AbstractIdentifiedObject implements
      * The set of parameters, or {@code null} if none.
      *
      * <p><b>Consider this field as final!</b>
-     * This field is modified only at unmarshalling time by {@link #setDescriptors(GeneralParameterDescriptor[])}</p>
+     * This field is modified only at unmarshalling time by {@link #setDescriptors(GeneralParameterDescriptor[])}
+     * or {@link #afterUnmarshal(Unmarshaller, Object)}.</p>
      */
     @SuppressWarnings("serial")         // Not statically typed as Serializable.
     private ParameterDescriptorGroup parameters;
@@ -303,10 +305,7 @@ public class DefaultOperationMethod extends AbstractIdentifiedObject implements
         targetDimensions = transform.getTargetDimensions();
         if (transform instanceof Parameterized) {
             parameters = ((Parameterized) transform).getParameterDescriptors();
-        } else {
-            parameters = null;
         }
-        formula = null;
     }
 
     /**
@@ -632,9 +631,8 @@ public class DefaultOperationMethod extends AbstractIdentifiedObject implements
      * Returns the set of parameters.
      *
      * <div class="note"><b>Departure from the ISO 19111 standard:</b>
-     * this property is mandatory according ISO 19111, but may be null in Apache SIS if the
-     * {@link #DefaultOperationMethod(MathTransform)} constructor has been unable to infer it
-     * or if this {@code OperationMethod} has been read from an incomplete GML document.</div>
+     * this property is mandatory according ISO 19111, but may be {@code null} in Apache SIS if the
+     * {@link #DefaultOperationMethod(MathTransform)} constructor has been unable to infer it.</div>
      *
      * @return the parameters, or {@code null} if unknown.
      *
@@ -971,4 +969,16 @@ public class DefaultOperationMethod extends AbstractIdentifiedObject implements
         parameters = new DefaultParameterDescriptorGroup(IdentifiedObjects.getProperties(previous),
                 previous.getMinimumOccurs(), previous.getMaximumOccurs(), descriptors);
     }
+
+    /**
+     * Invoked by JAXB after unmarshalling. If the {@code <gml:OperationMethod>} element does not contain
+     * any {@code <gml:parameter>}, we assume that this is a valid parameterless operation (as opposed to
+     * an operation with unknown parameters). We need this assumption because, contrarily to GeoAPI model,
+     * the GML schema does not differentiate "no parameters" from "unspecified parameters".
+     */
+    private void afterUnmarshal(final Unmarshaller unmarshaller, final Object parent) {
+        if (parameters == null) {
+            parameters = CC_OperationMethod.group(super.getName(), new GeneralParameterDescriptor[0]);
+        }
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
index 950731c638..1d3b09fc96 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
@@ -19,24 +19,29 @@ package org.apache.sis.referencing.operation;
 import java.util.Map;
 import java.util.Arrays;
 import java.util.Objects;
+import javax.xml.bind.Unmarshaller;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
+import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.PassThroughOperation;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.CompoundCRS;
+import org.apache.sis.referencing.GeodeticException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.PassThroughTransform;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.metadata.ImplementationHelper;
 import org.apache.sis.util.UnsupportedImplementationException;
-import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.referencing.CRS;
 
 import static org.apache.sis.util.Utilities.deepEquals;
 
@@ -45,7 +50,7 @@ import static org.apache.sis.util.Utilities.deepEquals;
  * Specifies that a subset of a coordinate tuple is subject to a specific coordinate operation.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -111,9 +116,8 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp
                                        final int firstAffectedCoordinate,
                                        final int numTrailingCoordinates)
     {
-        super(properties, sourceCRS, targetCRS, null, MathTransforms.passThrough(
-                firstAffectedCoordinate, operation.getMathTransform(), numTrailingCoordinates));
-        ArgumentChecks.ensureNonNull("operation", operation);
+        super(properties, sourceCRS, targetCRS, null,
+              MathTransforms.passThrough(firstAffectedCoordinate, operation.getMathTransform(), numTrailingCoordinates));
         this.operation = operation;
     }
 
@@ -192,11 +196,11 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp
         final MathTransform transform = super.getMathTransform();
         if (transform instanceof PassThroughTransform) {
             return ((PassThroughTransform) transform).getModifiedCoordinates();
-        } else {
+        } else if (operation != null) {
             /*
-             * Should not happen with objects created by public methods since the constructor created the transform
-             * itself. However may happen with operations parsed from GML. As a fallback, search in the components
-             * of CompoundCRS. This is not a universal fallback, but work for the most straightforward cases.
+             * Should not happen with objects created by public methods since the constructor created the transform itself.
+             * However may happen with operations parsed from GML. As a fallback, search in the components of CompoundCRS.
+             * This is not a universal fallback, but works for the most straightforward cases.
              */
             final CoordinateReferenceSystem sourceCRS = super.getSourceCRS();
             if (sourceCRS instanceof CompoundCRS) {
@@ -214,8 +218,8 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp
                     firstAffectedCoordinate += dim;
                 }
             }
-            throw new UnsupportedImplementationException(transform.getClass());
         }
+        throw new UnsupportedImplementationException(transform.getClass());
     }
 
     /**
@@ -300,8 +304,8 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp
     private DefaultPassThroughOperation() {
         /*
          * A sub-operation is mandatory for SIS working. We do not verify its presence here because the verification
-         * would have to be done in an 'afterMarshal(…)' method and throwing an exception in that method causes the
-         * whole unmarshalling to fail. But the CC_CoordinateOperation adapter does some verifications.
+         * would have to be done in an `afterMarshal(…)` method and throwing an exception in that method causes the
+         * whole unmarshalling to fail. But the `CC_CoordinateOperation` adapter does some verifications.
          */
     }
 
@@ -327,37 +331,79 @@ public class DefaultPassThroughOperation extends AbstractCoordinateOperation imp
      */
     @XmlElement(name = "modifiedCoordinate", required = true)
     private int[] getIndices() {
-        final int[] indices = getModifiedCoordinates();
-        for (int i=0; i<indices.length; i++) {
-            indices[i]++;
+        final int[] dimensions = getModifiedCoordinates();
+        for (int i=0; i<dimensions.length; i++) {
+            dimensions[i]++;
         }
-        return indices;
+        return dimensions;
     }
 
     /**
      * Invoked by JAXB at unmarshalling time for setting the modified coordinates.
+     * This method needs to be invoked last, even if the {@code <gml:modifiedCoordinate>}
+     * elements are not last in the GML document. It is the case when using JAXB because
+     * multiple occurrences of {@code <gml:modifiedCoordinate>} are aggregated in an array.
      */
-    private void setIndices(final int[] coordinates) {
-        String missing = "sourceCRS";
-        final CoordinateReferenceSystem sourceCRS = super.getSourceCRS();
-        if (sourceCRS != null) {
-            missing = "modifiedCoordinate";
-            if (coordinates != null && coordinates.length != 0) {
-                missing = "coordOperation";
-                if (operation != null) {
-                    for (int i=1; i<coordinates.length; i++) {
-                        final int previous = coordinates[i-1];
-                        if (previous < 1 || coordinates[i] != previous + 1) {
-                            throw new IllegalArgumentException(Errors.format(
-                                    Errors.Keys.CanNotAssign_2, missing, Arrays.toString(coordinates)));
+    private void setIndices(final int[] dimensions) {
+        /*
+         * Argument and state validation.
+         */
+        String missing = "modifiedCoordinate";
+        FactoryException cause = null;
+        final int n = dimensions.length;
+        if (n != 0) {
+            if (!ArraysExt.isRange(dimensions[0], dimensions))  {
+                throw new GeodeticException(Errors.format(Errors.Keys.CanNotAssign_2, missing, Arrays.toString(dimensions)));
+            }
+            missing = "sourceCRS";
+            final CoordinateReferenceSystem sourceCRS = super.getSourceCRS();
+            if (sourceCRS != null) {
+                missing = "targetCRS";
+                final CoordinateReferenceSystem targetCRS = super.getTargetCRS();
+                if (targetCRS != null) {
+                    missing = "coordOperation";
+                    if (operation != null) {
+                        /*
+                         * If the operation is a defining operation, we need to replace it by a full operation.
+                         * After that, we can store the modified coordinate indices in the transform field.
+                         */
+                        MathTransform subTransform = operation.getMathTransform();
+                        if (operation instanceof Conversion) {
+                            CoordinateReferenceSystem sourceSub = operation.getSourceCRS();
+                            CoordinateReferenceSystem targetSub = operation.getTargetCRS();
+                            if (subTransform == null || sourceSub == null || targetSub == null) try {
+                                final int[] zeroBased = dimensions.clone();
+                                for (int i=0; i<n; i++) zeroBased[i]--;
+                                if (sourceSub == null) sourceSub = CRS.selectDimensions(sourceCRS, zeroBased);
+                                if (targetSub == null) targetSub = CRS.selectDimensions(targetCRS, zeroBased);
+                                operation = DefaultConversion.castOrCopy((Conversion) operation)
+                                            .specialize(Conversion.class, sourceSub, targetSub, null);
+                                subTransform = operation.getMathTransform();
+                            } catch (FactoryException e) {
+                                cause = e;
+                            }
+                        }
+                        if (subTransform != null) {
+                            transform = MathTransforms.passThrough(dimensions[0] - 1, subTransform,
+                                    ReferencingUtilities.getDimension(sourceCRS) - dimensions[n-1]);
+                            return;
                         }
                     }
-                    transform = MathTransforms.passThrough(coordinates[0] - 1, operation.getMathTransform(),
-                            ReferencingUtilities.getDimension(sourceCRS) - coordinates[coordinates.length - 1]);
-                    return;
                 }
             }
         }
-        throw new IllegalStateException(Errors.format(Errors.Keys.MissingComponentInElement_2, missing, "PassThroughOperation"));
+        throw new GeodeticException(Errors.format(Errors.Keys.MissingComponentInElement_2, "PassThroughOperation", missing), cause);
+    }
+
+    /**
+     * Invoked by JAXB after unmarshalling. If needed, this method tries to infer source/target CRS
+     * of the nested operation from the source/target CRS if the enclosing pass-through operation.
+     */
+    @Override
+    void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
+        super.afterUnmarshal(unmarshaller, parent);
+        if (transform == null) {
+            setIndices(ArraysExt.EMPTY_INT);        // Cause an exception to be thrown.
+        }
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/SubTypes.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/SubTypes.java
index 51e1e6445c..ea3e72e12f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/SubTypes.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/SubTypes.java
@@ -168,7 +168,7 @@ final class SubTypes {
                     conversion = new DefaultConversion(definition, sourceCRS, targetCRS, factory, actual);
                 }
                 /*
-                 * The DefaultConversion constructor may have used by MathTransformFactory for creating the actual
+                 * The DefaultConversion constructor may have used MathTransformFactory for creating the actual
                  * MathTransform object. In such case, we can use the knownledge that the factory has about the
                  * coordinate operation for refining again the type of the object to be returned.
                  */
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTParserTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTParserTest.java
index cb937dbd6c..9433d0f367 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTParserTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTParserTest.java
@@ -480,6 +480,7 @@ public final strictfp class WKTParserTest extends CRSParserTest {
      */
     @Test
     @Override
+    @org.junit.Ignore("Pending new AxisDirection code list in GeoAPI.")
     public void testEngineeringForShip() throws FactoryException {
         super.testEngineeringForShip();
         final CoordinateSystem cs = object.getCoordinateSystem();
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureIterator.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureIterator.java
index ad85cad854..6dfb161f0c 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureIterator.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureIterator.java
@@ -114,7 +114,8 @@ final class MovingFeatureIterator extends FeatureIterator implements Consumer<Lo
 
     /**
      * Executes the given action for the next moving feature or for all remaining moving features.
-     * This method assumes that the 4 first columns are as documented in the code inside constructor.
+     * This method assumes that the 4 first columns are identifier, start time, end time and
+     * optional attributes in that order.
      *
      * @param  action  the action to execute as soon as the {@code mfidref} change, or {@code null} if none.
      * @param  all     {@code true} for executing the given action on all remaining features.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
index 1322b78f86..27f9685fb7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
@@ -263,7 +263,7 @@ final class Store extends URIDataStore implements FeatureSet {
                             throw new DataStoreContentException(Resources.forLocale(getLocale())
                                     .getString(Resources.Keys.ShallBeDeclaredBefore_2, "@columns", "@stboundedby"));
                         }
-                        envelope = parseEnvelope(elements);     // Also set 'timeEncoding' and 'spatialDimensionCount'.
+                        envelope = parseEnvelope(elements);     // Also set `timeEncoding` and `spatialDimensionCount`.
                         dissociate |= (timeEncoding == null);   // Need to be updated before parseFeatureType(…) execution.
                         break;
                     }