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/15 09:23:24 UTC

[sis] 01/04: Bug fixes related to unmarshalling of GML documents. Those bugs were identified by OGC TestBed 18 D025 scenario.

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

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

commit eef4dfff3178e743e6baf2174d75ea72327fb77b
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Oct 28 17:48:13 2022 +0200

    Bug fixes related to unmarshalling of GML documents.
    Those bugs were identified by OGC TestBed 18 D025 scenario.
---
 .../sis/internal/jaxb/IdentifierMapAdapter.java    | 30 ++++++++-
 .../org/apache/sis/internal/jaxb/package-info.java |  2 +-
 .../java/org/apache/sis/xml/NilObjectHandler.java  |  2 +-
 .../java/org/apache/sis/xml/ReferenceResolver.java |  8 +--
 .../jaxb/referencing/CC_OperationParameter.java    | 54 +++++++++++-----
 .../internal/jaxb/referencing/package-info.java    |  2 +-
 .../sis/internal/referencing/AxisDirections.java   | 54 +++++++++++++---
 .../sis/parameter/DefaultParameterDescriptor.java  | 74 ++++++++++++++++++----
 .../sis/parameter/DefaultParameterValue.java       | 14 ++--
 .../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/cs/CoordinateSystems.java      |  7 ++
 .../org/apache/sis/referencing/cs/Normalizer.java  | 26 ++++----
 .../operation/AbstractSingleOperation.java         |  6 ++
 .../operation/DefaultConcatenatedOperation.java    |  2 +-
 .../operation/DefaultOperationMethod.java          | 26 +++++---
 .../storage/csv/MovingFeatureIterator.java         |  3 +-
 .../org/apache/sis/internal/storage/csv/Store.java |  2 +-
 20 files changed, 260 insertions(+), 85 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/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/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 11b8ee14f8..cca68226f5 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_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 385610001b..d081072712 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 32d5e1cab3..05326fc7c9 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,28 @@ 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="awayFrom", obligation=CONDITIONAL, specification=UNSPECIFIED)
-    public static final AxisDirection AWAY_FROM = AxisDirection.valueOf("AWAY_FROM");
+    @UML(identifier="forward", obligation=CONDITIONAL, specification=UNSPECIFIED)
+    public static final AxisDirection FORWARD = AxisDirection.valueOf("FORWARD");
+
+    /**
+     * 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=UNSPECIFIED)
+    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 +106,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=UNSPECIFIED)
     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=UNSPECIFIED)
+    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 +365,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 +401,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 +428,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/DefaultParameterDescriptor.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/DefaultParameterDescriptor.java
index 67a62c0c3d..9fadb81fe2 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
@@ -65,7 +65,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureCanCast;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @param <T>  the type of elements to be returned by {@link DefaultParameterValue#getValue()}.
  *
@@ -85,10 +85,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})
@@ -110,9 +112,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}.
@@ -270,7 +275,6 @@ public class DefaultParameterDescriptor<T> extends AbstractParameterDescriptor i
      *
      * @see #castOrCopy(ParameterDescriptor)
      */
-    @SuppressWarnings("unchecked")
     protected DefaultParameterDescriptor(final ParameterDescriptor<T> descriptor) {
         super(descriptor);
         valueClass   = descriptor.getValueClass();
@@ -374,6 +378,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;
     }
@@ -392,6 +397,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;
     }
@@ -422,6 +428,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;
     }
 
@@ -491,10 +498,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);
                 }
             }
         }
@@ -527,7 +534,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 reflexion.
      *
@@ -544,16 +551,57 @@ 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.
+     * @return the parameter descriptor to assign to the given parameter value.
+     */
+    @SuppressWarnings("unchecked")
+    final DefaultParameterDescriptor<T> setValueClass(final DefaultParameterValue<?> param) {
+        valueClass = (Class) Classes.findCommonClass(valueClass, CC_OperationParameter.valueClass(param));
+        if (valueDomain == null) {
+            valueDomain = CC_OperationParameter.valueDomain(param);
+        }
+        return this;
+    }
 }
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 88e41ac63b..7defaf722e 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,18 @@ 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;
+    final void setDescriptor(final ParameterDescriptor<T> p) {
+        descriptor = DefaultParameterDescriptor.castOrCopy(p).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 fe091065c6..cf7de09327 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 a2912b2ef4..3f9e127424 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
@@ -693,7 +693,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 e31bec5252..ccfade6322 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;
 
 
@@ -355,9 +356,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 a5ef86b428..0858703c2b 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/cs/CoordinateSystems.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/CoordinateSystems.java
index 1102f49e87..ea0b5bfe80 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 f1c38a8d8b..96ad930a6f 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/AbstractSingleOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java
index 7be7a93f96..8d89447e1c 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
@@ -463,6 +463,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 afa8ae1da8..c6ae10396a 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
@@ -243,7 +243,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/DefaultOperationMethod.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultOperationMethod.java
index 1232c0aad6..1660e4f234 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/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 06cc1f2de2..3478fbc723 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 a1a0ef0750..a169618dbb 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
@@ -261,7 +261,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;
                     }