Merge branch 'geoapi-4.0' into geoapi-3.1
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
index 8a8a1e0..8ec168b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
@@ -42,7 +42,6 @@
 import org.apache.sis.gui.coverage.CoverageCanvas;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.coverage.grid.GridEvaluator;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.internal.system.Modules;
@@ -211,7 +210,7 @@
         /**
          * The object computing or interpolation sample values in the coverage.
          */
-        private GridEvaluator evaluator;
+        private GridCoverage.Evaluator evaluator;
 
         /**
          * The selection status of each band.
@@ -318,6 +317,7 @@
             }
             evaluator = coverage.forConvertedValues(true).evaluator();
             evaluator.setNullIfOutside(true);
+            evaluator.setWraparoundEnabled(true);
             canvas(property).ifPresent((c) -> setSlice(c.getSliceExtent()));
             if (previous != null && bands.equals(previous.getSampleDimensions())) {
                 // Same configuration than previous coverage.
@@ -479,7 +479,7 @@
          * @param  point  the cursor location in arbitrary CRS, or {@code null} if outside canvas region.
          * @return string representation of data under given position, or {@code null} if none.
          *
-         * @see GridEvaluator#apply(DirectPosition)
+         * @see GridCoverage.Evaluator#apply(DirectPosition)
          */
         @Override
         public String evaluate(final DirectPosition point) {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
index 454d496..d402b89 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/DataStoreOpener.java
@@ -260,10 +260,9 @@
                 /*
                  * Search for a title in metadata first because it has better chances to be human-readable
                  * compared to the resource identifier. If the title is the same text as the identifier,
-                 * then we will execute the code path for identifier unless the caller did not asked for
-                 * qualified name, in which case it would make no difference.
+                 * then execute the code path for identifier (i.e. try to find a more informative text).
                  */
-                GenericName name = qualified ? resource.getIdentifier().orElse(null) : null;
+                GenericName name = resource.getIdentifier().orElse(null);
                 Collection<? extends Identification> identifications = null;
                 final Metadata metadata = resource.getMetadata();
                 if (metadata != null) {
@@ -273,7 +272,7 @@
                             final Citation citation = identification.getCitation();
                             if (citation != null) {
                                 final String t = string(citation.getTitle(), locale);
-                                if (t != null && (name == null || !t.equals(name.toString()))) {
+                                if (t != null && (name == null || !t.equals(name.tip().toString()))) {
                                     return t;
                                 }
                             }
@@ -285,13 +284,8 @@
                  * We search for explicitly declared identifier first before to fallback on
                  * metadata identifier, because the latter is more subject to interpretation.
                  */
-                if (!qualified) {
-                    name = resource.getIdentifier().orElse(null);
-                }
                 if (name != null) {
-                    if (qualified) {
-                        name = name.toFullyQualifiedName();
-                    }
+                    name = qualified ? name.toFullyQualifiedName() : name.tip();
                     final String t = string(name.toInternationalString(), locale);
                     if (t != null) return t;
                 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java
index 1c8a824..bb5ddfb 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/SyncWindowList.java
@@ -36,8 +36,8 @@
 
 
 /**
- * Provides a widget for listing all available windows and selecting the ones to follow
- * on gesture events (zoom, pans, <i>etc</i>).
+ * Provides a widget for listing all available windows and selecting the ones
+ * on which to replicate gesture events (zoom, pans, <i>etc</i>).
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.3
@@ -46,19 +46,19 @@
  */
 public final class SyncWindowList extends TabularWidget implements ListChangeListener<WindowHandler> {
     /**
-     * Window containing a {@link MapCanvas} to follow on gesture events.
-     * Gestures are followed only if {@link #linked} is {@code true}.
+     * Window containing a {@link MapCanvas} on which to replicate gesture events.
+     * Gestures are replicated only if {@link #transformEnabled} is {@code true}.
      */
     private static final class Link extends GestureFollower {
         /**
-         * The "foreigner" view for which to follow the gesture.
+         * The "foreigner" view on which to replicate the gestures.
          */
         public final WindowHandler view;
 
         /**
-         * Creates a new row for a window to follow.
+         * Creates a new row for a window on which to replicate gestures.
          *
-         * @param  view    the "foreigner" view for which to follow the gesture.
+         * @param  view    the "foreigner" view on which to replicate the gesture events.
          * @param  source  the canvas which is the source of zoom, pan or rotation events.
          * @param  target  the canvas on which to apply the changes of zoom, pan or rotation.
          */
@@ -68,23 +68,23 @@
         }
 
         /**
-         * Converts the given list of handled to a list of table rows.
+         * Converts the given list of handlers to a list of table rows.
          *
          * @param  added   list of new items to put in the table.
          * @param  addTo   where to add the converted items.
          * @param  owner   item to exclude (because the referenced window is itself).
-         * @param  target  the canvas on which to apply the changes of zoom, pan or rotation.
+         * @param  source  the canvas for which to replicate the changes of zoom, pan or rotation.
          */
         static void wrap(final List<? extends WindowHandler> added, final List<Link> addTo,
-                         final WindowHandler owner, final MapCanvas target)
+                         final WindowHandler owner, final MapCanvas source)
         {
             final Link[] items = new Link[added.size()];
             int count = 0;
             try {
                 for (final WindowHandler view : added) {
                     if (view != owner) {
-                        final MapCanvas source = view.getCanvas().orElse(null);
-                        if (source != null) {
+                        final MapCanvas target = view.getCanvas().orElse(null);
+                        if (target != null) {
                             final Link item = new Link(view, source, target);;
                             items[count++] = item;          // Add now for disposing if an exception is thrown.
                             item.initialize();              // Invoked outside constructor for allowing disposal.
@@ -118,11 +118,11 @@
     private final WindowHandler owner;
 
     /**
-     * The canvas on which to apply the change of zoom, pan or rotation.
+     * The canvas for which to replicate the changes of zoom, pan or rotation.
      * Needs to be fetched only when first needed (not at construction time)
      * for avoiding a stack overflow.
      */
-    private MapCanvas target;
+    private MapCanvas source;
 
     /**
      * The component to be returned by {@link #getView()}.
@@ -244,9 +244,9 @@
      * Adds the given window handlers and items in {@link #table}.
      */
     private void addAll(final List<? extends WindowHandler> windows) {
-        if (target == null) {
-            target = owner.getCanvas().get();
+        if (source == null) {
+            source = owner.getCanvas().get();
         }
-        Link.wrap(windows, table.getItems(), owner, target);
+        Link.wrap(windows, table.getItems(), owner, source);
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
index b6a2766..55c7438 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ValueColorMapper.java
@@ -126,7 +126,7 @@
          * This method compares all properties, including visibility and color.
          *
          * @param  other  the other object to compare with this step.
-         * @return whether the other object is equals to this step.
+         * @return whether the other object is equal to this step.
          */
         @Override
         public boolean equals(final Object other) {
diff --git a/application/sis-openoffice/src/test/java/org/apache/sis/openoffice/TransformerTest.java b/application/sis-openoffice/src/test/java/org/apache/sis/openoffice/TransformerTest.java
index 5401e00..fdf383b 100644
--- a/application/sis-openoffice/src/test/java/org/apache/sis/openoffice/TransformerTest.java
+++ b/application/sis-openoffice/src/test/java/org/apache/sis/openoffice/TransformerTest.java
@@ -62,7 +62,7 @@
     }
 
     /**
-     * Asserts that the transformation result is equals to the expected result.
+     * Asserts that the transformation result is equal to the expected result.
      */
     static void assertPointsEqual(final double[][] expected, final double[][] actual, final double tolerance) {
         assertNotSame("transform", expected, actual);
diff --git a/core/sis-build-helper/src/main/java/org/apache/sis/util/resources/IndexedResourceCompiler.java b/core/sis-build-helper/src/main/java/org/apache/sis/util/resources/IndexedResourceCompiler.java
index bfd7c62..c14c9bd 100644
--- a/core/sis-build-helper/src/main/java/org/apache/sis/util/resources/IndexedResourceCompiler.java
+++ b/core/sis-build-helper/src/main/java/org/apache/sis/util/resources/IndexedResourceCompiler.java
@@ -563,7 +563,7 @@
                     if (endOfLine >= 0) {
                         if (buffer.substring(startLineToCompare, endOfLine).equals(line)) {
                             startLineToCompare = endOfLine + lineSeparator.length();
-                            continue;                   // Content is equals, do not set the `modified` flag.
+                            continue;                   // Content is equal, do not set the `modified` flag.
                         }
                     } else if (brackets == 0) {
                         break;              // Content finished at the same time, do not set the `modified` flag.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/BandedCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/BandedCoverage.java
index df5cec0..11d4140 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/BandedCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/BandedCoverage.java
@@ -49,7 +49,7 @@
  * {@link Evaluator#apply(DirectPosition)} method signatures.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -114,7 +114,7 @@
      *
      * <h4>Multi-threading</h4>
      * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
-     * a new {@link Evaluator} instance should be created for each thread by invoking this
+     * a new {@code Evaluator} instance should be created for each thread by invoking this
      * method multiply times.
      *
      * @return a new function for computing or interpolating sample values.
@@ -131,7 +131,7 @@
      *
      * @author  Johann Sorel (Geomatys)
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 1.1
+     * @version 1.3
      *
      * @see BandedCoverage#evaluator()
      *
@@ -141,6 +141,7 @@
     public interface Evaluator extends Function<DirectPosition, double[]> {
         /**
          * Returns the coverage from which this evaluator is computing sample values.
+         * This is the coverage on which the {@link BandedCoverage#evaluator()} method has been invoked.
          *
          * @return the source of sample values for this evaluator.
          */
@@ -166,6 +167,31 @@
         void setNullIfOutside(boolean flag);
 
         /**
+         * Returns {@code true} if this evaluator is allowed to wraparound coordinates that are outside the coverage.
+         * The initial value is {@code false}. This method may continue to return {@code false} even after a call to
+         * {@code setWraparoundEnabled(true)} if no wraparound axis has been found in the coverage CRS,
+         * or if automatic wraparound is not supported.
+         *
+         * @return {@code true} if this evaluator may wraparound coordinates that are outside the coverage.
+         *
+         * @since 1.3
+         */
+        boolean isWraparoundEnabled();
+
+        /**
+         * Specifies whether this evaluator is allowed to wraparound coordinates that are outside the coverage.
+         * If {@code true} and if a given coordinate is outside the coverage, then this evaluator may translate
+         * the point along a wraparound axis in an attempt to get the point inside the coverage. For example if
+         * the coverage CRS has a longitude axis, then the evaluator may translate the longitude value by a
+         * multiple of 360°.
+         *
+         * @param  allow  whether to allow wraparound of coordinates that are outside the coverage.
+         *
+         * @since 1.3
+         */
+        void setWraparoundEnabled(final boolean allow);
+
+        /**
          * Returns a sequence of double values for a given point in the coverage.
          * The CRS of the given point may be any coordinate reference system;
          * coordinate conversions will be applied as needed.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
index 019c4ea..d560e57 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
@@ -504,7 +504,7 @@
      * Compares the specified object with this category for equality.
      *
      * @param  object the object to compare with.
-     * @return {@code true} if the given object is equals to this category.
+     * @return {@code true} if the given object is equal to this category.
      */
     @Override
     public boolean equals(final Object object) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 700eddb..3341b59 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -458,7 +458,7 @@
      * Compares the specified object with this sample dimension for equality.
      *
      * @param  object  the object to compare with.
-     * @return {@code true} if the given object is equals to this sample dimension.
+     * @return {@code true} if the given object is equal to this sample dimension.
      */
     @Override
     public boolean equals(final Object object) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
index 8f20214..1be0b6f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BufferedGridCoverage.java
@@ -232,11 +232,11 @@
      * Creates a new function for computing or interpolating sample values at given locations.
      *
      * <h4>Multi-threading</h4>
-     * {@code GridEvaluator}s are not thread-safe. For computing sample values concurrently,
-     * a new {@link GridEvaluator} instance should be created for each thread.
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@link Evaluator} instance should be created for each thread.
      */
     @Override
-    public GridEvaluator evaluator() {
+    public Evaluator evaluator() {
         return new CellAccessor(this);
     }
 
@@ -289,7 +289,7 @@
     /**
      * Implementation of evaluator returned by {@link #evaluator()}.
      */
-    private static class CellAccessor extends GridEvaluator {
+    private static final class CellAccessor extends DefaultEvaluator {
         /**
          * A copy of {@link BufferedGridCoverage#data} reference.
          */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
index 5829121..b3f2b6d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
@@ -16,14 +16,12 @@
  */
 package org.apache.sis.coverage.grid;
 
-import java.util.Map;
 import java.util.List;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Optional;
 import java.awt.image.RenderedImage;
 import org.opengis.geometry.DirectPosition;
-import org.opengis.coverage.CannotEvaluateException;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
@@ -34,6 +32,9 @@
 import org.apache.sis.image.DataType;
 import org.apache.sis.image.ImageProcessor;
 
+// Branch-dependent imports
+import org.opengis.coverage.CannotEvaluateException;
+
 
 /**
  * Decorates a {@link GridCoverage} in order to convert sample values on the fly.
@@ -43,7 +44,7 @@
  *   <li>In calls to {@link #render(GridExtent)}, sample values are converted when first needed
  *       on a tile-by-tile basis then cached for future reuse. Note however that discarding the
  *       returned image may result in the lost of cached tiles.</li>
- *   <li>In calls to {@link GridEvaluator#apply(DirectPosition)}, the conversion is applied
+ *   <li>In calls to {@link GridCoverage.Evaluator#apply(DirectPosition)}, the conversion is applied
  *       on-the-fly each time in order to avoid the potentially costly tile computations.</li>
  * </ul>
  *
@@ -232,76 +233,33 @@
      * Creates a new function for computing or interpolating sample values at given locations.
      *
      * <h4>Multi-threading</h4>
-     * {@code GridEvaluator}s are not thread-safe. For computing sample values concurrently,
-     * a new {@link GridEvaluator} instance should be created for each thread.
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@link Evaluator} instance should be created for each thread.
      */
     @Override
-    public GridEvaluator evaluator() {
-        return new SampleConverter(this);
+    public Evaluator evaluator() {
+        return new SampleConverter();
     }
 
     /**
-     * Implementation of evaluator returned by {@link #evaluator()}.
+     * Implementation of evaluator returned by {@link ConvertedGridCoverage#evaluator()}.
+     * This evaluator delegates all operations to the {@link #source} coverage and converts
+     * the returned sample values.
      */
-    private static final class SampleConverter extends GridEvaluator {
-        /**
-         * The evaluator provided by source coverage.
-         */
-        private final GridEvaluator evaluator;
-
-        /**
-         * Conversions from {@linkplain #source source} values to converted values.
-         */
-        private final MathTransform1D[] converters;
-
+    private final class SampleConverter extends EvaluatorWrapper {
         /**
          * Creates a new evaluator for the enclosing coverage.
          */
-        SampleConverter(final ConvertedGridCoverage coverage) {
-            super(coverage);
-            evaluator  = coverage.source.evaluator();
-            converters = coverage.converters;
+        SampleConverter() {
+            super(source.evaluator());
         }
 
         /**
-         * Returns the default slice where to perform evaluation, or an empty map if unspecified.
+         * Returns the enclosing coverage.
          */
         @Override
-        public Map<Integer,Long> getDefaultSlice() {
-            return evaluator.getDefaultSlice();
-        }
-
-        /**
-         * Sets the default slice where to perform evaluation when the points do not have enough dimensions.
-         */
-        @Override
-        public void setDefaultSlice(Map<Integer,Long> slice) {
-            evaluator.setDefaultSlice(slice);
-        }
-
-        /**
-         * Returns {@code true} if this evaluator is allowed to wraparound coordinates that are outside the grid.
-         */
-        @Override
-        public boolean isWraparoundEnabled() {
-            return evaluator.isWraparoundEnabled();
-        }
-
-        /**
-         * Specifies whether this evaluator is allowed to wraparound coordinates that are outside the grid.
-         */
-        @Override
-        public void setWraparoundEnabled(final boolean allow) {
-            evaluator.setWraparoundEnabled(allow);
-        }
-
-        /**
-         * Forwards configuration to the wrapped evaluator.
-         */
-        @Override
-        public void setNullIfOutside(final boolean flag) {
-            evaluator.setNullIfOutside(flag);
-            super.setNullIfOutside(flag);
+        public GridCoverage getCoverage() {
+            return ConvertedGridCoverage.this;
         }
 
         /**
@@ -313,8 +271,9 @@
          */
         @Override
         public double[] apply(final DirectPosition point) throws CannotEvaluateException {
-            final double[] values = evaluator.apply(point);
+            final double[] values = super.apply(point);
             if (values != null) try {
+                final MathTransform1D[] converters = ConvertedGridCoverage.this.converters;
                 for (int i=0; i<converters.length; i++) {
                     values[i] = converters[i].transform(values[i]);
                 }
@@ -323,14 +282,6 @@
             }
             return values;
         }
-
-        /**
-         * Converts the specified geospatial position to grid coordinates.
-         */
-        @Override
-        public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException {
-            return evaluator.toGridCoordinates(point);
-        }
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DefaultEvaluator.java
similarity index 90%
rename from core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
rename to core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DefaultEvaluator.java
index 5e22a9d..aede5d3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DefaultEvaluator.java
@@ -31,8 +31,6 @@
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.coverage.PointOutsideCoverageException;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.util.CollectionsExt;
@@ -51,13 +49,17 @@
 
 import static java.util.logging.Logger.getLogger;
 
+// Branch-dependent imports
+import org.opengis.coverage.CannotEvaluateException;
+import org.opengis.coverage.PointOutsideCoverageException;
+
 
 /**
- * Computes or interpolates values of sample dimensions at given positions.
+ * Default implementation of {@link GridCoverage.Evaluator} for interpolating values at given positions.
  * Values are computed by calls to {@link #apply(DirectPosition)} and are returned as {@code double[]}.
  *
  * <h2>Multi-threading</h2>
- * Evaluators are not thread-safe. An instance of {@code GridEvaluator} should be created
+ * Evaluators are not thread-safe. An instance of {@code DefaultEvaluator} should be created
  * for each thread that need to compute sample values.
  *
  * <h2>Limitations</h2>
@@ -72,7 +74,7 @@
  * @since 1.1
  * @module
  */
-public class GridEvaluator implements GridCoverage.Evaluator {
+class DefaultEvaluator implements GridCoverage.Evaluator {
     /**
      * The coverage in which to evaluate sample values.
      */
@@ -156,22 +158,21 @@
 
     /**
      * Creates a new evaluator for the given coverage. This constructor is protected for allowing
-     * {@link GridCoverage} subclasses to provide their own {@code GridEvaluator} implementations.
+     * {@link GridCoverage} subclasses to provide their own {@code DefaultEvaluator} implementations.
      * For using an evaluator, invoke {@link GridCoverage#evaluator()} instead.
      *
      * @param  coverage  the coverage for which to create an evaluator.
      *
      * @see GridCoverage#evaluator()
      */
-    protected GridEvaluator(final GridCoverage coverage) {
+    protected DefaultEvaluator(final GridCoverage coverage) {
         ArgumentChecks.ensureNonNull("coverage", coverage);
         this.coverage = coverage;
     }
 
     /**
      * Returns the coverage from which this evaluator is fetching sample values.
-     * This is usually the coverage on which the {@link GridCoverage#evaluator()} method has been invoked,
-     * but not necessarily. Implementations are allowed to use a different coverage for efficiency.
+     * This is the coverage on which the {@link GridCoverage#evaluator()} method has been invoked.
      *
      * @return the source of sample values for this evaluator.
      */
@@ -183,7 +184,7 @@
     /**
      * Returns the default slice where to perform evaluation, or an empty map if unspecified.
      * Keys are dimensions from 0 inclusive to {@link GridGeometry#getDimension()} exclusive,
-     * and values are the grid coordinate of the slice in that dimension.
+     * and values are the grid coordinates of the slice in the dimension specified by the key.
      *
      * <p>This information allows to invoke {@link #apply(DirectPosition)} with for example two-dimensional points
      * even if the underlying coverage is three-dimensional. The missing coordinate values are replaced by the
@@ -193,6 +194,7 @@
      *
      * @since 1.3
      */
+    @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")     // Because the map is unmodifiable.
     public Map<Integer,Long> getDefaultSlice() {
         if (slice == null) {
@@ -204,7 +206,7 @@
 
     /**
      * Sets the default slice where to perform evaluation when the points do not have enough dimensions.
-     * A {@code null} argument restore the default value, which is to infer the slice from the coverage
+     * A {@code null} argument restores the default value, which is to infer the slice from the coverage
      * grid geometry.
      *
      * @param  slice  the default slice where to perform evaluation, or an empty map if none.
@@ -214,6 +216,8 @@
      *
      * @since 1.3
      */
+    @Override
+    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
     public void setDefaultSlice(Map<Integer,Long> slice) {
         if (!Objects.equals(this.slice, slice)) {
             if (slice != null) {
@@ -242,6 +246,7 @@
      *
      * @since 1.2
      */
+    @Override
     public boolean isWraparoundEnabled() {
         return (wraparoundAxes != 0);
     }
@@ -257,6 +262,7 @@
      *
      * @since 1.2
      */
+    @Override
     public void setWraparoundEnabled(final boolean allow) {
         wraparoundAxes = 0;
         if (allow) try {
@@ -409,28 +415,25 @@
     }
 
     /**
-     * Converts the specified geospatial position to grid coordinates. If the given position
-     * is associated to a non-null coordinate reference system (CRS) different than the
-     * {@linkplain #coverage} CRS, then this method automatically transforms that position to the
+     * Converts the specified geospatial position to grid coordinates.
+     * If the given position is associated to a non-null coordinate reference system (CRS) different than the
+     * {@linkplain #getCoverage() coverage} CRS, then this method automatically transforms that position to the
      * {@linkplain GridCoverage#getCoordinateReferenceSystem() coverage CRS} before to compute grid coordinates.
      *
      * <p>This method does not put any restriction on the grid coordinates result.
      * The result may be outside the {@linkplain GridGeometry#getExtent() grid extent}
      * if the {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform allows it.</p>
      *
-     * <p>The grid coordinates are relative to the grid of the coverage returned by {@link #getCoverage()}.
-     * This is usually the coverage on which the {@link GridCoverage#evaluator()} method has been invoked,
-     * but not necessarily. Implementations are allowed to use a different coverage for efficiency.</p>
-     *
      * @param  point  geospatial coordinates (in arbitrary CRS) to transform to grid coordinates.
      * @return the grid coordinates for the given geospatial coordinates.
      * @throws IncompleteGridGeometryException if the {@linkplain GridCoverage#getGridGeometry() grid geometry}
      *         does not define a "grid to CRS" transform, or if the given point has a non-null CRS but the
-     *         {@linkplain #coverage} does not {@linkplain GridCoverage#getCoordinateReferenceSystem() have a CRS}.
+     *         coverage does not {@linkplain GridCoverage#getCoordinateReferenceSystem() have a CRS}.
      * @throws TransformException if the given coordinates can not be transformed.
      *
      * @see FractionalGridCoordinates#toPosition(MathTransform)
      */
+    @Override
     public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException {
         ArgumentChecks.ensureNonNull("point", point);
         try {
@@ -475,7 +478,6 @@
          * If most cases, the work of this method ends here. The remaining code in this method
          * is for handling wraparound axes. If a coordinate is outside the coverage extent,
          * check if a wraparound on some axes would bring the coordinates inside the extent.
-         * The first step is to get the point closest to the extent.
          */
         long axes = wraparoundAxes;
         if (axes != 0) {
@@ -487,6 +489,12 @@
                 final double c = coordinates[i];
                 double border;
                 if (c < (border = wraparoundExtent[j++]) || c > (border = wraparoundExtent[j])) {
+                    /*
+                     * Detected that the point is outside the grid extent along an axis where wraparound is possible.
+                     * The first time that we find such axis, expand the coordinates array for storing two points.
+                     * The two points will have the same coordinates, except on all axes where the point is outside.
+                     * On those axes, the coordinate of the first point is set to the closest border of the grid.
+                     */
                     if (outsideAxes == 0) {
                         final int n = coordinates.length;
                         coordinates = Arrays.copyOf(coordinates, 2*Math.max(n, gridToWraparound.getTargetDimensions()));
@@ -502,7 +510,10 @@
             /*
              * If a coordinate was found outside the grid, transform to a CRS where we can apply shift.
              * It may be the same CRS than the coverage CRS or the source CRS, but not necessarily.
-             * Current version does not try to optimize by checking if `point` argument can be reused.
+             * For example if the CRS is projected, then we need to use a geographic intermediate CRS.
+             * In the common case where the source CRS is already geographic, the second point in the
+             * `coordinates` array after `transform(…)` will contain the same coordinates as `point`,
+             * but potentially with more dimensions.
              */
             if (outsideAxes != 0) {
                 gridToWraparound.transform(coordinates, 0, coordinates, 0, 2);
@@ -515,7 +526,7 @@
                          * then round that shift to an integer amount of periods. Modify the original
                          * coordinate by applying that modified translation.
                          */
-                        final int oi = i + s;
+                        final int oi = i + s;                       // Index of original coordinates.
                         double shift = coordinates[i] - coordinates[oi];
                         shift = Math.copySign(Math.ceil(Math.abs(shift) / period), shift) * period;
                         coordinates[oi] += shift;
@@ -536,7 +547,23 @@
                     }
                     outsideAxes &= ~(1L << i);
                 } while (outsideAxes != 0);
-                System.arraycopy(coordinates, 0, position.coordinates, 0, position.coordinates.length);
+                /*
+                 * Copy shifted coordinate values to the final `FractionalGridCoordinates`, except the NaN values.
+                 * NaN values may exist if the given `point` has less dimensions than the grid geometry, in which
+                 * case missing values have been replaced by `slice` values in the `target` array but not in the
+                 * `coordinates` array. We want to keep the `slice` values in the `target` array.
+                 *
+                 * TODO: to be strict, we should skip the copy only if `slice.containsKey(i)` is true, because it
+                 * could happen that a transform resulted in NaN values in other dimensions. But that check would
+                 * be costly, so we avoid it for now.
+                 */
+                final double[] target = position.coordinates;
+                for (int i = target.length; --i >= 0;) {
+                    final double value = coordinates[i];
+                    if (!Double.isNaN(value)) {
+                        target[i] = value;
+                    }
+                }
             }
         }
         return position;
@@ -627,6 +654,6 @@
      * @param  exception  the exception that occurred.
      */
     private static void recoverableException(final String caller, final TransformException exception) {
-        Logging.recoverableException(getLogger(Modules.RASTER), GridEvaluator.class, caller, exception);
+        Logging.recoverableException(getLogger(Modules.RASTER), DefaultEvaluator.class, caller, exception);
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java
index 67c0f3c..e91dffc 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java
@@ -94,13 +94,12 @@
      * That function accepts {@link DirectPosition} in arbitrary Coordinate Reference System;
      * conversions to grid indices are applied by the {@linkplain #source} as needed.
      *
-     * @todo The results returned by {@link GridEvaluator#toGridCoordinates(DirectPosition)}
-     *       would need to be transformed. But it would force us to return a wrapper, which
-     *       would add an indirection level for all others (more important) method calls.
-     *       Is it worth to do so?
+     * @todo The results returned by {@link GridCoverage.Evaluator#toGridCoordinates(DirectPosition)}
+     *       would need to be transformed. But it would force us to return a wrapper, which would add
+     *       an indirection level for all others (more important) method calls. Is it worth to do so?
      */
     @Override
-    public GridEvaluator evaluator() {
+    public Evaluator evaluator() {
         return source.evaluator();
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/EvaluatorWrapper.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/EvaluatorWrapper.java
new file mode 100644
index 0000000..014335b
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/EvaluatorWrapper.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import java.util.Map;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.operation.TransformException;
+
+// Branch-dependent imports
+import org.opengis.coverage.CannotEvaluateException;
+
+
+/**
+ * An evaluator which delegates all operations to another evaluator.
+ * The default implementation of all methods except {@link #getCoverage()} delegates to the source evaluator.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+abstract class EvaluatorWrapper implements GridCoverage.Evaluator {
+    /**
+     * The evaluator provided by source coverage.
+     * This is where all operations are delegated.
+     */
+    private final GridCoverage.Evaluator source;
+
+    /**
+     * Creates a new evaluator wrapper.
+     *
+     * @param  source  the evaluator to wrap.
+     */
+    EvaluatorWrapper(final GridCoverage.Evaluator source) {
+        this.source = source;
+    }
+
+    /**
+     * Returns whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds.
+     */
+    @Override
+    public boolean isNullIfOutside() {
+        return source.isNullIfOutside();
+    }
+
+    /**
+     * Specifies whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds.
+     */
+    @Override
+    public void setNullIfOutside(final boolean flag) {
+        source.setNullIfOutside(flag);
+    }
+
+    /**
+     * Returns {@code true} if this evaluator is allowed to wraparound coordinates that are outside the grid.
+     */
+    @Override
+    public boolean isWraparoundEnabled() {
+        return source.isWraparoundEnabled();
+    }
+
+    /**
+     * Specifies whether this evaluator is allowed to wraparound coordinates that are outside the grid.
+     */
+    @Override
+    public void setWraparoundEnabled(final boolean allow) {
+        source.setWraparoundEnabled(allow);
+    }
+
+    /**
+     * Returns the default slice where to perform evaluation, or an empty map if unspecified.
+     * This method should be overridden if this evaluator has been created for a coverage
+     * with a different grid geometry than the coverage of the wrapped evaluator.
+     */
+    @Override
+    public Map<Integer,Long> getDefaultSlice() {
+        return source.getDefaultSlice();
+    }
+
+    /**
+     * Sets the default slice where to perform evaluation when the points do not have enough dimensions.
+     * This method should be overridden if this evaluator has been created for a coverage
+     * with a different grid geometry than the coverage of the wrapped evaluator.
+     */
+    @Override
+    public void setDefaultSlice(Map<Integer,Long> slice) {
+        source.setDefaultSlice(slice);
+    }
+
+    /**
+     * Converts the specified geospatial position to grid coordinates.
+     * This method should be overridden if this evaluator has been created for a coverage
+     * with a different grid geometry than the coverage of the wrapped evaluator.
+     */
+    @Override
+    public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException {
+        return source.toGridCoordinates(point);
+    }
+
+    /**
+     * Returns a sequence of double values for a given point in the coverage.
+     * This method should be overridden if this evaluator is for a coverage
+     * doing some on-the-fly conversion of sample values.
+     *
+     * @param  point  the coordinate point where to evaluate.
+     * @throws CannotEvaluateException if the values can not be computed.
+     */
+    @Override
+    public double[] apply(final DirectPosition point) throws CannotEvaluateException {
+        return source.apply(point);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
index d91504d..2995c44 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
@@ -50,7 +50,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
  *
- * @see GridEvaluator#toGridCoordinates(DirectPosition)
+ * @see GridCoverage.Evaluator#toGridCoordinates(DirectPosition)
  *
  * @since 1.1
  * @module
@@ -71,7 +71,7 @@
      *
      * <div class="note"><b>Note:</b>
      * {@code FractionalGridCoordinates} are usually not created directly, but are instead obtained
-     * indirectly for example from the {@linkplain GridEvaluator#toGridCoordinates(DirectPosition)
+     * indirectly for example from the {@linkplain GridCoverage.Evaluator#toGridCoordinates(DirectPosition)
      * conversion of a geospatial position}.</div>
      *
      * @param  dimension  the number of dimensions.
@@ -348,7 +348,7 @@
      * @return the grid coordinates converted using the given transform.
      * @throws TransformException if the grid coordinates can not be converted by {@code gridToCRS}.
      *
-     * @see GridEvaluator#toGridCoordinates(DirectPosition)
+     * @see GridCoverage.Evaluator#toGridCoordinates(DirectPosition)
      */
     public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
         return gridToCRS.transform(new Position(this), null);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index bd0425e..1feabdb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Map;
 import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
@@ -26,6 +27,7 @@
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.measure.NumberRange;
@@ -301,8 +303,8 @@
      * conversions to grid indices are applied as needed.
      *
      * <h4>Multi-threading</h4>
-     * {@code GridEvaluator}s are not thread-safe. For computing sample values concurrently,
-     * a new {@link GridEvaluator} instance should be created for each thread by invoking this
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@code Evaluator} instance should be created for each thread by invoking this
      * method multiply times.
      *
      * @return a new function for computing or interpolating sample values.
@@ -310,8 +312,84 @@
      * @since 1.1
      */
     @Override
-    public GridEvaluator evaluator() {
-        return new GridEvaluator(this);
+    public Evaluator evaluator() {
+        return new DefaultEvaluator(this);
+    }
+
+    /**
+     * Interpolates values of sample dimensions at given positions.
+     * Values are computed by calls to {@link #apply(DirectPosition)} and are returned as {@code double[]}.
+     * This method extends {@link BandedCoverage.Evaluator} with the addition of some methods specific to
+     * gridded data.
+     *
+     * <h2>Multi-threading</h2>
+     * Evaluators are not thread-safe. An instance of {@code Evaluator} should be created
+     * for each thread that need to interpolate sample values.
+     *
+     * @author  Johann Sorel (Geomatys)
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.3
+     *
+     * @see GridCoverage#evaluator()
+     *
+     * @since 1.3
+     * @module
+     */
+    public interface Evaluator extends BandedCoverage.Evaluator {
+        /**
+         * Returns the coverage from which this evaluator is fetching sample values.
+         * This is the coverage on which the {@link GridCoverage#evaluator()} method has been invoked.
+         *
+         * @return the source of sample values for this evaluator.
+         */
+        @Override
+        GridCoverage getCoverage();
+
+        /**
+         * Returns the default slice where to perform evaluation, or an empty map if unspecified.
+         * Keys are dimensions from 0 inclusive to {@link GridGeometry#getDimension()} exclusive,
+         * and values are the grid coordinates of the slice in the dimension specified by the key.
+         *
+         * <p>This information allows to invoke {@link #apply(DirectPosition)} with for example
+         * two-dimensional points even if the underlying coverage is three-dimensional.
+         * The missing coordinate values are replaced by the values provided in the map.</p>
+         *
+         * @return the default slice where to perform evaluation, or an empty map if unspecified.
+         */
+        Map<Integer,Long> getDefaultSlice();
+
+        /**
+         * Sets the default slice where to perform evaluation when the points do not have enough dimensions.
+         * A {@code null} argument restores the default value, which is to infer the slice from the coverage
+         * grid geometry.
+         *
+         * @param  slice  the default slice where to perform evaluation, or an empty map if none.
+         * @throws IllegalArgumentException if the map contains an illegal dimension or grid coordinate value.
+         *
+         * @see GridExtent#getSliceCoordinates()
+         */
+        void setDefaultSlice(Map<Integer,Long> slice);
+
+        /**
+         * Converts the specified geospatial position to grid coordinates. If the given position is associated to
+         * a non-null coordinate reference system (CRS) different than the {@linkplain #getCoverage() coverage} CRS,
+         * then this method automatically transforms that position to the {@linkplain #getCoordinateReferenceSystem()
+         * coverage CRS} before to compute grid coordinates.
+         *
+         * <p>This method does not put any restriction on the grid coordinates result.
+         * The result may be outside the {@linkplain GridGeometry#getExtent() grid extent}
+         * if the {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform allows it.</p>
+         *
+         * @param  point  geospatial coordinates (in arbitrary CRS) to transform to grid coordinates.
+         * @return the grid coordinates for the given geospatial coordinates.
+         * @throws IncompleteGridGeometryException if the {@linkplain GridCoverage#getGridGeometry() grid geometry}
+         *         does not define a "grid to CRS" transform, or if the given point has a non-null CRS but the
+         *         coverage does not {@linkplain GridCoverage#getCoordinateReferenceSystem() have a CRS}.
+         * @throws TransformException if the given coordinates can not be transformed.
+         *
+         * @see FractionalGridCoordinates#toPosition(MathTransform)
+         */
+        FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException;
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index 273e8b2..c5d9337 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -502,20 +502,20 @@
      * Creates a new function for computing or interpolating sample values at given locations.
      *
      * <h4>Multi-threading</h4>
-     * {@code GridEvaluator}s are not thread-safe. For computing sample values concurrently,
-     * a new {@link GridEvaluator} instance should be created for each thread.
+     * {@code Evaluator}s are not thread-safe. For computing sample values concurrently,
+     * a new {@code Evaluator} instance should be created for each thread.
      *
      * @since 1.1
      */
     @Override
-    public GridEvaluator evaluator() {
+    public Evaluator evaluator() {
         return new PixelAccessor();
     }
 
     /**
      * Implementation of evaluator returned by {@link #evaluator()}.
      */
-    private final class PixelAccessor extends GridEvaluator {
+    private final class PixelAccessor extends DefaultEvaluator {
         /**
          * Creates a new evaluator for the enclosing coverage.
          */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index c875ec3..e71d284 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -715,7 +715,7 @@
      *
      * @param  index  the dimension for which to obtain the coordinate value.
      * @return the low coordinate value at the given dimension, inclusive.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() grid dimension}.
      *
      * @see #getLow()
@@ -733,7 +733,7 @@
      *
      * @param  index  the dimension for which to obtain the coordinate value.
      * @return the high coordinate value at the given dimension, <strong>inclusive</strong>.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() grid dimension}.
      *
      * @see #getHigh()
@@ -753,7 +753,7 @@
      *
      * @param  index  the dimension for which to obtain the size.
      * @return the number of integer grid coordinates along the given dimension.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() grid dimension}.
      * @throws ArithmeticException if the size is too large for the {@code long} primitive type.
      *
@@ -841,7 +841,7 @@
      *
      * @return grid coordinates for all dimensions where the grid has a size of 1.
      *
-     * @see GridEvaluator#setDefaultSlice(Map)
+     * @see GridCoverage.Evaluator#setDefaultSlice(Map)
      *
      * @since 1.3
      */
@@ -953,7 +953,7 @@
      *
      * @param  index  the dimension for which to obtain the axis type.
      * @return the axis type at the given dimension. May be absent if the type is unknown.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() grid dimension}.
      */
     public Optional<DimensionNameType> getAxisType(final int index) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index f3b1730..6456f23 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -245,7 +245,7 @@
     /**
      * An <em>estimation</em> of the grid resolution, in units of the CRS axes.
      * Computed from {@link #gridToCRS}, eventually together with {@link #extent}.
-     * May be {@code null} if unknown. If non-null, the array length is equals to
+     * May be {@code null} if unknown. If non-null, the array length is equal to
      * the number of CRS dimensions.
      *
      * @see #RESOLUTION
@@ -559,7 +559,7 @@
     }
 
     /**
-     * Ensures that the given dimension is equals to the expected value. If not, throws an exception.
+     * Ensures that the given dimension is equal to the expected value. If not, throws an exception.
      * This method assumes that the argument name is {@code "extent"}.
      *
      * @param extent    the extent to validate, or {@code null} if none.
@@ -1563,7 +1563,7 @@
      * This method delegates to {@code equals(object, ComparisonMode.STRICT)}.
      *
      * @param  object  the object to compare with.
-     * @return {@code true} if the given object is equals to this grid geometry.
+     * @return {@code true} if the given object is equal to this grid geometry.
      */
     @Override
     public boolean equals(final Object object) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index c0ab6e9..f129e9d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -379,7 +379,7 @@
     }
 
     /**
-     * Ensures that the given number is equals to the expected number of bands.
+     * Ensures that the given number is equal to the expected number of bands.
      * The given number shall be either 1 (case of interleaved sample model) or
      * {@link #getNumBands()} (case of banded sample model).
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java
index 9a11f4d..608d704 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractIdentifiedType.java
@@ -354,7 +354,7 @@
      * Compares this type with the given object for equality.
      *
      * @param  obj  the object to compare with this type.
-     * @return {@code true} if the given object is equals to this type.
+     * @return {@code true} if the given object is equal to this type.
      */
     @Override
     public boolean equals(final Object obj) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java b/core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java
index 72d8187..c7909b2 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java
@@ -33,7 +33,7 @@
 /**
  * Spatial operations between two geometries.
  * The nature of the operation depends on {@link #getOperatorType()}.
- * A standard set of spatial operators is equals, disjoin, touches,
+ * A standard set of spatial operators is equal, disjoin, touches,
  * within, overlaps, crosses, intersects, contains, beyond and BBOX.
  *
  * @author  Johann Sorel (Geomatys)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
index 158c2f9..f213150 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
@@ -800,7 +800,7 @@
     }
 
     /**
-     * Creates an operator that checks if first temporal operand is equals to the second.
+     * Creates an operator that checks if first temporal operand is equal to the second.
      *
      * @param  time1  expression fetching the first temporal value.
      * @param  time2  expression fetching the second temporal value.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
index 066e65a..ae9d2fc 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
@@ -29,7 +29,6 @@
 import org.apache.sis.feature.builder.PropertyTypeBuilder;
 import org.apache.sis.feature.builder.AttributeTypeBuilder;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.internal.util.XPaths;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
@@ -105,7 +104,7 @@
     @SuppressWarnings("unchecked")
     static <V> ValueReference<Feature,V> create(String xpath, final Class<V> type) {
         boolean isVirtual = false;
-        List<String> path = XPaths.split(xpath);
+        List<String> path = XPath.split(xpath);
 split:  if (path != null) {
             /*
              * If the XPath is like "/∗/property" where the root "/" is the feature instance,
@@ -114,7 +113,7 @@
              */
             final String head = path.get(0);                // List and items in the list are guaranteed non-empty.
             isVirtual = head.equals("/*");
-            if (isVirtual || head.charAt(0) != XPaths.SEPARATOR) {
+            if (isVirtual || head.charAt(0) != XPath.SEPARATOR) {
                 final int offset = isVirtual ? 1 : 0;       // Skip the "/*/" component at index 0.
                 final int last = path.size() - 1;
                 if (last >= offset) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java b/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java
new file mode 100644
index 0000000..cd86706
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.List;
+import java.util.ArrayList;
+import org.apache.sis.util.Static;
+import org.apache.sis.util.resources.Errors;
+
+import static org.apache.sis.util.CharSequences.*;
+
+
+/**
+ * Basic support of X-Path in {@link PropertyValue} expression.
+ * This is intended to be only a lightweight support, not a replacement for {@link javax.xml.xpath} implementations.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   0.4
+ * @module
+ */
+final class XPath extends Static {
+    /**
+     * The separator between path components.
+     */
+    public static final char SEPARATOR = '/';
+
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private XPath() {
+    }
+
+    /**
+     * Splits the given URL around the {@code '/'} separator, or returns {@code null} if there is no separator.
+     * By convention if the URL is absolute, then the leading {@code '/'} character is kept in the first element.
+     * For example {@code "/∗/property"} is splitted as two elements: {@code "/∗"} and {@code "property"}.
+     *
+     * <p>This method trims the whitespaces of components except the last one (the tip),
+     * for consistency with the case where this method returns {@code null}.</p>
+     *
+     * @param  xpath  the URL to split.
+     * @return the splitted URL with the heading separator kept in the first element, or {@code null}
+     *         if there is no separator. If non-null, the list always contains at least one element.
+     * @throws IllegalArgumentException if the XPath contains at least one empty component.
+     */
+    static List<String> split(final String xpath) {
+        int next = xpath.indexOf(SEPARATOR);
+        if (next < 0) {
+            return null;
+        }
+        final List<String> components = new ArrayList<>(4);
+        int start = skipLeadingWhitespaces(xpath, 0, next);
+        if (start < next) {
+            // No leading '/' (the characters before it are a path element, added below).
+            components.add(xpath.substring(start, skipTrailingWhitespaces(xpath, start, next)));
+            start = ++next;
+        } else {
+            // Keep the `start` position on the leading '/'.
+            next++;
+        }
+        while ((next = xpath.indexOf(SEPARATOR, next)) >= 0) {
+            components.add(trimWhitespaces(xpath, start, next).toString());
+            start = ++next;
+        }
+        components.add(xpath.substring(start));         // No whitespace trimming.
+        if (components.stream().anyMatch(String::isEmpty)) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
+        }
+        return components;
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
index bf19bde..33d1abf 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java
@@ -191,11 +191,11 @@
     }
 
     /**
-     * Tests {@link GridEvaluator#apply(DirectPosition)}.
+     * Tests {@link GridCoverage.Evaluator#apply(DirectPosition)}.
      */
     @Test
     public void testEvaluator() {
-        final GridEvaluator evaluator = createTestCoverage().evaluator();
+        final GridCoverage.Evaluator evaluator = createTestCoverage().evaluator();
         /*
          * Test evaluation at indeger indices. No interpolation should be applied.
          */
@@ -227,11 +227,8 @@
     }
 
     /**
-     * Tests {@link GridEvaluator#apply(DirectPosition)} with a wraparound on the longitude axis.
+     * Tests {@link GridCoverage.Evaluator#apply(DirectPosition)} with a wraparound on the longitude axis.
      * This method tests a coordinate that would be outside the grid if wraparound was not applied.
-     *
-     * @todo Not yet implemented. One potential place where to implement this functionality could be
-     *       {@link GridEvaluator#toGridPosition(DirectPosition)}.
      */
     @Test
     @DependsOnMethod("testEvaluator")
@@ -239,12 +236,12 @@
         final Matrix3 gridToCRS = new Matrix3();
         gridToCRS.m00 = 100;        // Scale
         gridToCRS.m02 = 100;        // Offset
-        final GridEvaluator evaluator = createTestCoverage(MathTransforms.linear(gridToCRS)).evaluator();
+        final GridCoverage.Evaluator evaluator = createTestCoverage(MathTransforms.linear(gridToCRS)).evaluator();
         evaluator.setWraparoundEnabled(true);
         assertArrayEquals(new double[] {2}, evaluator.apply(new DirectPosition2D(100, 0)), STRICT);
         assertArrayEquals(new double[] {5}, evaluator.apply(new DirectPosition2D(200, 0)), STRICT);
         /*
-         * Following tests fail if wraparound is not applied by `GridEvaluator`.
+         * Following tests fail if wraparound is not applied by `GridCoverage.Evaluator`.
          */
         assertArrayEquals(new double[] {5}, evaluator.apply(new DirectPosition2D(200 - 360, 0)), STRICT);
         assertArrayEquals(new double[] {2}, evaluator.apply(new DirectPosition2D(100 - 360, 0)), STRICT);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
index c8b0f69..8bf4b8d 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
@@ -647,7 +647,7 @@
         assertExtentEquals(new long[] {0, -3410}, new long[] {75, -3158}, result.getExtent());
         assertEnvelopeEquals(new Envelope2D(null,
                 -175,           // Expected minimum value.
-                  80,           // Not interresting for this test.
+                  80,           // Not interesting for this test.
                 -172 - -175,    // Expected maximum value minus minimum.
                   90 -   80),
                 result.getEnvelope(), 0.02);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/feature/FeatureTestCase.java b/core/sis-feature/src/test/java/org/apache/sis/feature/FeatureTestCase.java
index 51123b8..149443b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/FeatureTestCase.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/FeatureTestCase.java
@@ -126,7 +126,7 @@
 
     /**
      * Sets the attribute of the given name to the given value.
-     * First, this method verifies that the previous value is equals to the given one.
+     * First, this method verifies that the previous value is equal to the given one.
      * Then, this method set the attribute to the given value and check if the result.
      *
      * @param  name      the name of the attribute to set.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java
new file mode 100644
index 0000000..3c3ae24
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link XPath}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   0.4
+ * @module
+ */
+public final strictfp class XPathTest extends TestCase {
+    /**
+     * Tests {@link XPath#split(String)}.
+     */
+    @Test
+    public void testSplit() {
+        assertNull(XPath.split("property"));
+        assertArrayEquals(new String[] {"/property"},                    XPath.split("/property").toArray());
+        assertArrayEquals(new String[] {"Feature", "property", "child"}, XPath.split("Feature/property/child").toArray());
+        assertArrayEquals(new String[] {"/Feature", "property"},         XPath.split("/Feature/property").toArray());
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/InterpolationTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/InterpolationTest.java
index 42899a0..3d08d30 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/InterpolationTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/InterpolationTest.java
@@ -169,7 +169,7 @@
     }
 
     /**
-     * Verifies that a pixel value interpolated in the source image is equals to the expected value.
+     * Verifies that a pixel value interpolated in the source image is equal to the expected value.
      *
      * @param x         <var>x</var> coordinate in the source image, from {@value #XMIN} to {@value #XUP} (exclusive).
      * @param y         <var>y</var> coordinate in the source image, from {@value #YMIN} to {@value #YUP} (exclusive).
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java
index 294f0b9..22f9749 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/PixelIteratorTest.java
@@ -335,7 +335,7 @@
     }
 
     /**
-     * Verifies that actual iteration order is equals to the expected one.
+     * Verifies that actual iteration order is equal to the expected one.
      *
      * @param  singleTile  {@code true} if iteration occurs in a single tile, or {@code false} for the whole image.
      */
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index fa3794c..db88eca 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -50,6 +50,7 @@
     org.apache.sis.feature.FeatureOperationsTest.class,
     org.apache.sis.feature.FeatureFormatTest.class,
     org.apache.sis.feature.FeaturesTest.class,
+    org.apache.sis.filter.XPathTest.class,
     org.apache.sis.filter.CapabilitiesTest.class,
     org.apache.sis.filter.LeafExpressionTest.class,
     org.apache.sis.filter.LogicalFilterTest.class,
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/Context.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/Context.java
index 801ccad..034d1bb 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/Context.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/Context.java
@@ -378,7 +378,7 @@
     }
 
     /**
-     * Returns {@code true} if the GML version is equals or newer than the specified version.
+     * Returns {@code true} if the GML version is equal or newer than the specified version.
      * If no GML version was specified, then this method returns {@code true}, i.e. newest
      * version is assumed.
      *
@@ -387,7 +387,7 @@
      *
      * @param  context  the current context, or {@code null} if none.
      * @param  version  the version to compare to.
-     * @return {@code true} if the GML version is equals or newer than the specified version.
+     * @return {@code true} if the GML version is equal or newer than the specified version.
      *
      * @see #getVersion(String)
      */
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/lan/LocaleAndCharset.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/lan/LocaleAndCharset.java
index aaab4d7..2461e8b 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/lan/LocaleAndCharset.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/lan/LocaleAndCharset.java
@@ -106,7 +106,7 @@
 
     /**
      * Returns the key or the value of the given {@link Map.Entry}. If the given object is not a map entry
-     * or is null, then it is returned as-is. This later case should never happen (the object shall always be
+     * or is null, then it is returned as-is. This latter case should never happen (the object shall always be
      * a non-null map entry), but we nevertheless check for making the code more robust to ill-formed metadata.
      * We apply this tolerance because this method is used (indirectly) for {@code toString()} implementations,
      * and failure in those methods make debugging more difficult (string representations are often requested
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/TreeNode.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/TreeNode.java
index 87206bf..e2b3f25 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/TreeNode.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/TreeNode.java
@@ -381,7 +381,7 @@
          * in order to get the singular form instead of the plural one, because we will create one
          * node for each element in a collection.
          *
-         * <p>If the property name is equals, ignoring case, to the simple type name, then this method
+         * <p>If the property name is equal, ignoring case, to the simple type name, then this method
          * returns the subtype name (<a href="https://issues.apache.org/jira/browse/SIS-298">SIS-298</a>).
          * For example instead of:</p>
          *
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
index 9063a05..9e069dc 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
@@ -232,7 +232,7 @@
     }
 
     /**
-     * Returns 1 if the {@linkplain #getDenominator() denominator} is equals to 1, or 0 otherwise.
+     * Returns 1 if the {@linkplain #getDenominator() denominator} is equal to 1, or 0 otherwise.
      *
      * <div class="note"><b>Rational:</b>
      * This method is defined that way because scales smaller than 1 can
@@ -246,7 +246,7 @@
     }
 
     /**
-     * Returns 1 if the {@linkplain #getDenominator() denominator} is equals to 1, or 0 otherwise.
+     * Returns 1 if the {@linkplain #getDenominator() denominator} is equal to 1, or 0 otherwise.
      *
      * <div class="note"><b>Rational:</b>
      * This method is defined that way because scales smaller than 1 can
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataWriter.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataWriter.java
index cd4f0bf..4343c03 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataWriter.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataWriter.java
@@ -303,7 +303,7 @@
                 }
                 /*
                  * We have found a column to add. Check if the column actually needs to be added to the parent table
-                 * (if such parent exists). In most case, the answer is "no" and 'addTo' is equals to 'table'.
+                 * (if such parent exists). In most case, the answer is "no" and 'addTo' is equal to 'table'.
                  */
                 String addTo = table;
                 if (helper.dialect.supportsTableInheritance) {
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/AbstractName.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/AbstractName.java
index 89e8faa..26b31ae 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/AbstractName.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/AbstractName.java
@@ -277,14 +277,9 @@
      * @param  name  the name after which to write a separator.
      * @return the separator to write after the given name.
      */
-    static String separator(final GenericName name) {
-        if (name != null) {
-            final NameSpace scope = name.scope();
-            if (scope instanceof DefaultNameSpace) {
-                return ((DefaultNameSpace) scope).headSeparator;
-            }
-        }
-        return DefaultNameSpace.DEFAULT_SEPARATOR_STRING;
+    private static String headSeparator(final GenericName name) {
+        return (name != null) ? DefaultNameSpace.getSeparator(name.scope(), true)
+                              : DefaultNameSpace.DEFAULT_SEPARATOR_STRING;
     }
 
     /**
@@ -310,7 +305,7 @@
             final StringBuilder buffer = new StringBuilder();
             for (final LocalName name : getParsedNames()) {
                 if (insertSeparator) {
-                    buffer.append(separator(name));
+                    buffer.append(headSeparator(name));
                 }
                 insertSeparator = true;
                 buffer.append(name);
@@ -383,7 +378,7 @@
             final StringBuilder buffer = new StringBuilder();
             for (final LocalName name : parsedNames) {
                 if (insertSeparator) {
-                    buffer.append(separator(name));
+                    buffer.append(headSeparator(name));
                 }
                 insertSeparator = true;
                 buffer.append(name.toInternationalString().toString(locale));
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java
index cc78ce2..92fcc67 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java
@@ -37,8 +37,6 @@
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.util.Strings;
 
-import static org.apache.sis.util.iso.DefaultNameSpace.DEFAULT_SEPARATOR_STRING;
-
 
 /**
  * A factory for creating {@link AbstractName} objects.
@@ -294,12 +292,7 @@
      */
     @Override
     public GenericName parseGenericName(final NameSpace scope, final CharSequence name) {
-        final String separator;
-        if (scope instanceof DefaultNameSpace) {
-            separator = ((DefaultNameSpace) scope).separator;
-        } else {
-            separator = DEFAULT_SEPARATOR_STRING;
-        }
+        final String separator = DefaultNameSpace.getSeparator(scope, false);
         final int s = separator.length();
         final List<String> names = new ArrayList<>();
         int lower = 0;
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameSpace.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameSpace.java
index d0ba3d8..9fbe7bd 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameSpace.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameSpace.java
@@ -52,7 +52,7 @@
  * remain safe to call from multiple threads and do not change any public {@code NameSpace} state.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see DefaultScopedName
  * @see DefaultLocalName
@@ -98,12 +98,16 @@
     /**
      * The separator to insert between the namespace and the {@linkplain AbstractName#head() head}
      * of any name in that namespace.
+     *
+     * @see #getSeparator(NameSpace, boolean)
      */
-    final String headSeparator;
+    private final String headSeparator;
 
     /**
      * The separator to insert between the {@linkplain AbstractName#getParsedNames() parsed names}
      * of any name in that namespace.
+     *
+     * @see #getSeparator(NameSpace, boolean)
      */
     final String separator;
 
@@ -253,6 +257,31 @@
     }
 
     /**
+     * Returns the separator between name components in the given namespace.
+     * If the given namespace is an instance of {@code DefaultNameSpace}, then this method
+     * returns the {@code headSeparator} or {@code separator} argument given to the constructor.
+     * Otherwise this method returns the {@linkplain #DEFAULT_SEPARATOR default separator}.
+     *
+     * <div class="note"><b>API note:</b>
+     * this method is static because the {@code getSeparator(…)} method is not part of GeoAPI interfaces.
+     * A static method makes easier to use without {@code (if (x instanceof DefaultNameSpace)} checks.</div>
+     *
+     * @param  ns    the namespace for which to get the separator. May be {@code null}.
+     * @param  head  {@code true} for the separator between namespace and {@linkplain AbstractName#head() head}, or
+     *               {@code false} for the separator between {@linkplain AbstractName#getParsedNames() parsed names}.
+     * @return separator between name components.
+     *
+     * @since 1.3
+     */
+    public static String getSeparator(final NameSpace ns, final boolean head) {
+        if (ns instanceof DefaultNameSpace) {
+            final DefaultNameSpace ds = (DefaultNameSpace) ns;
+            return head ? ds.headSeparator : ds.separator;
+        }
+        return DEFAULT_SEPARATOR_STRING;
+    }
+
+    /**
      * Indicates whether this namespace is a "top level" namespace.  Global, or top-level
      * namespaces are not contained within another namespace. The global namespace has no
      * parent.
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java
index 9d06b2a..60d6bfc 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultRecordType.java
@@ -163,13 +163,13 @@
         this.container  = container;
         this.fieldTypes = computeTransientFields(fields);
         /*
-         * Ensure that the record namespace is equals to the schema name. For example if the schema
+         * Ensure that the record namespace is equal to the schema name. For example if the schema
          * name is "MyNameSpace", then the record type name can be "MyNameSpace:MyRecordType".
          */
         final LocalName   schemaName   = container.getSchemaName();
         final GenericName fullTypeName = typeName.toFullyQualifiedName();
         if (schemaName.compareTo(typeName.scope().name().tip()) != 0) {
-            throw new IllegalArgumentException(Errors.format(Errors.Keys.InconsistentNamespace_2, schemaName, fullTypeName));
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedNamespace_2, schemaName, fullTypeName));
         }
         final int size = size();
         for (int i=0; i<size; i++) {
@@ -179,7 +179,7 @@
                 throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalMemberType_2, name, type));
             }
             if (fullTypeName.compareTo(name.scope().name()) != 0) {
-                throw new IllegalArgumentException(Errors.format(Errors.Keys.InconsistentNamespace_2,
+                throw new IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedNamespace_2,
                         fullTypeName, name.toFullyQualifiedName()));
             }
         }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/package-info.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/package-info.java
index fadfbb6..105afcd 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/package-info.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/package-info.java
@@ -99,7 +99,7 @@
  * </table>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/xml/ValueConverter.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/ValueConverter.java
index 94d925e..b48f4dc 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/xml/ValueConverter.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/ValueConverter.java
@@ -66,7 +66,7 @@
  * {@code ValueConverter} to a (un)marshaller.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -304,13 +304,16 @@
     }
 
     /**
-     * Converts the given string to a unit. The default implementation is as below, omitting
-     * the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
-     * in case of error:
+     * Converts the given string to a unit.
+     * This method shall accept all the following forms (example for the metre unit):
      *
-     * {@preformat java
-     *     return Units.valueOf(value);
-     * }
+     * <ul>
+     *   <li>{@code m}</li>
+     *   <li>{@code EPSG:9001}</li>
+     *   <li>{@code urn:ogc:def:uom:epsg::9001}</li>
+     *   <li>{@code http://www.opengis.net/def/uom/EPSG/0/9001}</li>
+     *   <li>{@code http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])}</li>
+     * </ul>
      *
      * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
      * @param  value    the string to convert to a unit, or {@code null}.
@@ -323,6 +326,36 @@
     public Unit<?> toUnit(final MarshalContext context, String value) throws IllegalArgumentException {
         value = trimWhitespaces(value);
         if (value != null && !value.isEmpty()) try {
+            /*
+             * First, check for X-Paths like below:
+             *
+             *     http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])
+             *
+             * Technically the 'm' value in the X-Path is not necessarily a unit symbol.
+             * It is rather a reference to a definition like below:
+             *
+             * <uomItem>
+             *   <gml:BaseUnit gml:id="m">
+             *     <gml:description>
+             *       The metre is the length of the path travelled by ligth in vaccum during a time interval of 1/299 792 458 of a second
+             *     </gml:description>
+             *     <gml:identifier codeSpace="http://www.bipm.fr/en/si/base_units">metre</gml:identifier>
+             *     <gml:quantityType>length</gml:quantityType>
+             *     <gml:catalogSymbol codeSpace="http://www.bipm.org/en/si/base_units">m</gml:catalogSymbol>
+             *     <gml:unitsSystem xlink:href="http://www.bipm.fr/en/si"/>
+             *   </gml:BaseUnit>
+             * </uomItem>
+             *
+             * But current version of this method parses the anchor as if it was a unit symbol,
+             * because we do not have a resolution mechanism yet.
+             */
+            final int endOfURI = XPointer.endOfURI(value, 0);
+            if (endOfURI > 0) {
+                final String anchor = XPointer.UOM.reference(value.substring(0, endOfURI));
+                if (anchor != null) {
+                    value = anchor;
+                }
+            }
             return Units.valueOf(value);
         } catch (ParserException e) {
             if (!exceptionOccured(context, value, String.class, Unit.class, e)) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPointer.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/XPointer.java
similarity index 62%
rename from core/sis-utility/src/main/java/org/apache/sis/internal/util/XPointer.java
rename to core/sis-metadata/src/main/java/org/apache/sis/xml/XPointer.java
index 6bacf3b..284568d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPointer.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/XPointer.java
@@ -14,8 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.util;
-
+package org.apache.sis.xml;
 
 import static org.apache.sis.util.CharSequences.*;
 import static org.apache.sis.internal.util.DefinitionURI.regionMatches;
@@ -25,11 +24,11 @@
  * Parsers of pointers in x-paths, adapted to the syntax found in GML documents.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
-public enum XPointer {
+enum XPointer {
     /**
      * Pointer to units of measurement. Example:
      * {@code "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"})
@@ -63,7 +62,7 @@
     private int startOfFragment(final String url) {
         final int f = url.indexOf('#');
         if (f >= 1) {
-            final int i = url.lastIndexOf(XPaths.SEPARATOR, f-1) + 1;
+            final int i = url.lastIndexOf('/', f-1) + 1;
             for (final String document : documents) {
                 if (regionMatches(document, url, i, f)) {
                     return f + 1;
@@ -110,4 +109,47 @@
         }
         return null;
     }
+
+    /**
+     * If the given character sequences seems to be a URI, returns the presumed end of that URN.
+     * Otherwise returns -1.
+     * Examples:
+     * <ul>
+     *   <li>{@code "urn:ogc:def:uom:EPSG::9001"}</li>
+     *   <li>{@code "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"}</li>
+     * </ul>
+     *
+     * @param  uri     the URI candidate to verify.
+     * @param  offset  index of the first character to verify.
+     * @return index after the last character of the presumed URI, or -1 if this
+     *         method thinks that the given character sequence is not a URI.
+     */
+    public static int endOfURI(final CharSequence uri, int offset) {
+        boolean isURI = false;
+        int parenthesis = 0;
+        final int length = uri.length();
+scan:   while (offset < length) {
+            final int c = Character.codePointAt(uri, offset);
+            if (!Character.isLetterOrDigit(c)) {
+                switch (c) {
+                    case '#':                                           // Anchor in URL, presumed followed by xpointer.
+                    case ':': isURI |= (parenthesis == 0); break;       // Scheme or URN separator.
+                    case '_':
+                    case '-':                                           // Valid character in URL.
+                    case '%':                                           // Encoded character in URL.
+                    case '.':                                           // Domain name separator in URL.
+                    case '/': break;                                    // Path separator, but could also be division as in "m/s".
+                    case '(': parenthesis++; break;
+                    case ')': parenthesis--; break;
+                    default: {
+                        if (Character.isSpaceChar(c)) break;            // Not supposed to be valid, but be lenient.
+                        if (parenthesis != 0) break;
+                        break scan;                                     // Non-valid character outside parenthesis.
+                    }
+                }
+            }
+            offset += Character.charCount(c);
+        }
+        return isURI ? offset : -1;
+    }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/xml/package-info.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/package-info.java
index c12e529..c078aaf 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/xml/package-info.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/package-info.java
@@ -59,7 +59,7 @@
  * @author  Guilhem Legal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Cullen Rombach (Image Matters)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java b/core/sis-metadata/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java
index ea161da..7e09c87 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java
@@ -52,7 +52,7 @@
     private static final String TO_REPLACE = "xlink:href=“";
 
     /**
-     * Asserts that the content of the given map is equals to the given content, represented as a string.
+     * Asserts that the content of the given map is equal to the given content, represented as a string.
      * This method replaces the {@code xlink:href} value by the {@link XLink#toString()} value before to
      * compare with the map content. This is needed because the "special case rules" cause the {@code "href"}
      * identifier to be replaced by {@code "xlink:href"}.
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/SpecialCasesTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/SpecialCasesTest.java
index 365790b..70d623e 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/SpecialCasesTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/SpecialCasesTest.java
@@ -69,14 +69,14 @@
 
     /**
      * Invokes {@link SpecialCases#type(int, TypeValuePolicy)}
-     * and ensures that the result is equals to the expected value.
+     * and ensures that the result is equal to the expected value.
      */
     private void assertTypeEquals(final String name, final Class<?> expected) {
         assertEquals(name, expected, accessor.type(accessor.indexOf(name, true), TypeValuePolicy.ELEMENT_TYPE));
     }
 
     /**
-     * Invokes {@link SpecialCases#get(int, Object)} and ensures that the result is equals to the expected value.
+     * Invokes {@link SpecialCases#get(int, Object)} and ensures that the result is equal to the expected value.
      */
     private void assertPropertyEquals(final String name, final Object expected) {
         assertEquals(name, expected, accessor.get(accessor.indexOf(name, true), box));
@@ -84,7 +84,7 @@
 
     /**
      * Invokes {@link SpecialCases#set(int, Object, Object, int)} in {@code RETURN_PREVIOUS} mode with the given
-     * {@code newValue}, and ensures that the return value is equals to the given {@code oldValue}.
+     * {@code newValue}, and ensures that the return value is equal to the given {@code oldValue}.
      */
     private void assertPreviousEquals(final String name, final Object oldValue, final Object newValue) {
         final Object value = accessor.set(accessor.indexOf(name, true), box, newValue, PropertyAccessor.RETURN_PREVIOUS);
@@ -93,7 +93,7 @@
 
     /**
      * Invokes {@link SpecialCases#set(int, Object, Object, int)} in {@code APPEND} mode with the given
-     * {@code newValue}, and ensures that the return value is equals to the given {@code changed}.
+     * {@code newValue}, and ensures that the return value is equal to the given {@code changed}.
      */
     private void assertAppendResultEquals(final String name, final Boolean changed, final Object newValue) {
         final Object value = accessor.set(accessor.indexOf(name, true), box, newValue, PropertyAccessor.APPEND);
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBoxTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBoxTest.java
index 0dfa5d9..3c0582f 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBoxTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBoxTest.java
@@ -323,7 +323,7 @@
 
     /**
      * Asserts that the result of applying the {@code add} or {@code intersect} operation on {@code b1}
-     * is equals to the given values. This method tests also with horizontally flipped boxes, and tests
+     * is equal to the given values. This method tests also with horizontally flipped boxes, and tests
      * with interchanged boxes.
      *
      * @param union {@code true} for {@code b1.add(b2)}, or {@code false} for {@code b1.intersect(b2)}.
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/ExtentsTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/ExtentsTest.java
index 09667c1..134f1be 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/ExtentsTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/extent/ExtentsTest.java
@@ -147,7 +147,7 @@
     @Test
     public void testArea() {
         /*
-         * The nautical mile is equals to the length of 1 second of arc along a meridian or parallel at the equator.
+         * The nautical mile is equal to the length of 1 second of arc along a meridian or parallel at the equator.
          * Since we are using the GRS80 authalic sphere instead of WGS84, and since the nautical mile definition
          * itself is a little bit approximated, we add a slight empirical shift.
          */
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
index e7b78d4..29b2912 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
@@ -55,7 +55,7 @@
     }
 
     /**
-     * Asserts that the English title of the given citation is equals to the expected string.
+     * Asserts that the English title of the given citation is equal to the expected string.
      *
      * @param message   the message to report in case of test failure.
      * @param expected  the expected English title.
@@ -74,7 +74,7 @@
 
     /**
      * Asserts that the given citation has only one responsible party,
-     * and its English name is equals to the expected string.
+     * and its English name is equal to the expected string.
      *
      * @param message   the message to report in case of test failure.
      * @param expected  the expected English responsibly party name.
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java b/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
index d34df37..2711a6c 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
@@ -25,7 +25,7 @@
  * All tests from the {@code sis-metadata} module, in rough dependency order.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -59,6 +59,7 @@
     org.apache.sis.internal.test.DocumentComparatorTest.class,
     org.apache.sis.xml.NamespacesTest.class,
     org.apache.sis.xml.XLinkTest.class,
+    org.apache.sis.xml.XPointerTest.class,
     org.apache.sis.xml.NilReasonTest.class,
     org.apache.sis.xml.LegacyCodesTest.class,
     org.apache.sis.xml.ValueConverterTest.class,
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/AnnotationConsistencyCheck.java b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/AnnotationConsistencyCheck.java
index 6299f6a..f9d8a2e 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/AnnotationConsistencyCheck.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/AnnotationConsistencyCheck.java
@@ -910,7 +910,7 @@
                              getter.isAnnotationPresent(XmlElementRefs.class));
             }
             /*
-             * If the annotation is @XmlElement, ensure that XmlElement.name() is equals
+             * If the annotation is @XmlElement, ensure that XmlElement.name() is equal
              * to the UML identifier. Then verify that the namespace is the expected one.
              */
             if (element != null) {
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java
index 5b293b0..14a34c3 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java
@@ -715,7 +715,7 @@
      * The first line will contain the root of the tree. Other lines will contain
      * the child down in the hierarchy until the given node, inclusive.
      *
-     * <p>This method formats only a summary if the hierarchy is equals to the expected one.</p>
+     * <p>This method formats only a summary if the hierarchy is equal to the expected one.</p>
      *
      * @param  buffer         the buffer in which to append the formatted hierarchy.
      * @param  node           the node for which to format the parents.
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/TestCase.java b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/TestCase.java
index 925ba3b..74d6acd 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/TestCase.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/TestCase.java
@@ -196,7 +196,7 @@
     }
 
     /**
-     * Marshals the given object and ensure that the result is equals to the content of the given file.
+     * Marshals the given object and ensure that the result is equal to the content of the given file.
      *
      * @param  filename           the name of the XML file in the package of the final subclass of {@code this}.
      * @param  object             the object to marshal.
@@ -213,7 +213,7 @@
     }
 
     /**
-     * Marshals the given object and ensure that the result is equals to the content of the given file.
+     * Marshals the given object and ensure that the result is equal to the content of the given file.
      *
      * @param  filename           the name of the XML file in the package of the final subclass of {@code this}.
      * @param  object             the object to marshal.
@@ -231,7 +231,7 @@
     }
 
     /**
-     * Marshals the given object and ensure that the result is equals to the content of the given file,
+     * Marshals the given object and ensure that the result is equal to the content of the given file,
      * within a tolerance threshold for numerical values.
      *
      * @param  filename           the name of the XML file in the package of the final subclass of {@code this}.
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/xml/ValueConverterTest.java b/core/sis-metadata/src/test/java/org/apache/sis/xml/ValueConverterTest.java
index 0bbb05c..2d0d6dc 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/xml/ValueConverterTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/xml/ValueConverterTest.java
@@ -22,6 +22,9 @@
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
+import static org.apache.sis.measure.Units.METRE;
+import static org.apache.sis.measure.Units.DEGREE;
+import static org.apache.sis.measure.Units.RADIAN;
 import static org.junit.Assert.*;
 
 
@@ -29,7 +32,7 @@
  * Tests the {@link ValueConverter} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.3
  * @since   0.4
  * @module
  */
@@ -91,4 +94,17 @@
         assertEquals(StandardCharsets.ISO_8859_1, ValueConverter.DEFAULT.toCharset(null, "8859part1"));
         assertEquals(StandardCharsets.ISO_8859_1, ValueConverter.DEFAULT.toCharset(null, "ISO-8859-1"));
     }
+
+    /**
+     * Tests {@link ValueConverter#toUnit(MarshalContext, String)}.
+     */
+    @Test
+    public void testToUnit() {
+        assertSame(METRE,  ValueConverter.DEFAULT.toUnit(null, "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"));
+        assertSame(DEGREE, ValueConverter.DEFAULT.toUnit(null, "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='deg'])"));
+        assertSame(RADIAN, ValueConverter.DEFAULT.toUnit(null, "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='rad'])"));
+        assertSame(METRE,  ValueConverter.DEFAULT.toUnit(null, "gmxUom.xml#m"));
+        assertSame(METRE,  ValueConverter.DEFAULT.toUnit(null, "EPSG:9001"));
+        assertSame(DEGREE, ValueConverter.DEFAULT.toUnit(null, "urn:ogc:def:uom:EPSG::9102"));
+    }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPointerTest.java b/core/sis-metadata/src/test/java/org/apache/sis/xml/XPointerTest.java
similarity index 68%
rename from core/sis-utility/src/test/java/org/apache/sis/internal/util/XPointerTest.java
rename to core/sis-metadata/src/test/java/org/apache/sis/xml/XPointerTest.java
index c8e952c..ba7c817 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPointerTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/xml/XPointerTest.java
@@ -14,8 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.util;
+package org.apache.sis.xml;
 
+import org.apache.sis.util.Characters;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -26,7 +27,7 @@
  * Tests {@link XPointer}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -41,4 +42,17 @@
         assertEquals("m", XPointer.UOM.reference("http://standards.iso.org/ittf/PubliclyAvailableStandards/ISO_19139_Schemas/resources/uom/ML_gmxUom.xml#xpointer(//*[@gml:id='m'])"));
         assertEquals("m", XPointer.UOM.reference("../uom/ML_gmxUom.xml#xpointer(//*[@gml:id='m'])"));
     }
+
+    /**
+     * Tests the {@link XPointer#endOfURI(CharSequence, int)} method.
+     */
+    @Test
+    public void testEndOfURI() {
+        assertEquals(26, XPointer.endOfURI("urn:ogc:def:uom:EPSG::9001", 0));
+        assertEquals(80, XPointer.endOfURI("http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])", 0));
+        assertEquals(97, XPointer.endOfURI("http://schemas.opengis.net/iso/19139/20070417/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])", 0));
+        assertEquals(-1, XPointer.endOfURI("m/s", 0));
+        assertEquals(-1, XPointer.endOfURI("m.s", 0));
+        assertEquals(11, XPointer.endOfURI("EPSG" + Characters.NO_BREAK_SPACE + ": 9001", 0));
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java
index 51495bb..e310d99 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java
@@ -134,7 +134,7 @@
      *
      * @param  dimension  the dimension for the coordinate of interest.
      * @param  value      the coordinate value of interest.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() position dimension}.
      * @throws UnsupportedOperationException if this direct position is immutable.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java
index fd76528..6263e26 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java
@@ -356,7 +356,7 @@
      *
      * @param  dimension  the dimension for which to obtain the coordinate value.
      * @return the starting coordinate value at the given dimension.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() envelope dimension}.
      *
      * @see #getLowerCorner()
@@ -371,7 +371,7 @@
      *
      * @param  dimension  the dimension for which to obtain the coordinate value.
      * @return the starting coordinate value at the given dimension.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() envelope dimension}.
      *
      * @see #getUpperCorner()
@@ -388,7 +388,7 @@
      *
      * @param  dimension  the dimension for which to obtain the coordinate value.
      * @return the minimal coordinate value at the given dimension.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() envelope dimension}.
      */
     @Override
@@ -410,7 +410,7 @@
      *
      * @param  dimension  the dimension for which to obtain the coordinate value.
      * @return the maximal coordinate value at the given dimension.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() envelope dimension}.
      */
     @Override
@@ -425,7 +425,7 @@
 
     /**
      * Returns the median coordinate along the specified dimension.
-     * In most cases, the result is equals (minus rounding error) to:
+     * In most cases, the result is equal (minus rounding error) to:
      *
      * {@preformat java
      *     median = (getUpper(dimension) + getLower(dimension)) / 2;
@@ -443,7 +443,7 @@
      *
      * @param  dimension  the dimension for which to obtain the coordinate value.
      * @return the median coordinate at the given dimension, or {@link Double#NaN}.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() envelope dimension}.
      *
      * @see #getMedian()
@@ -478,7 +478,7 @@
 
     /**
      * Returns the envelope span (typically width or height) along the specified dimension.
-     * In most cases, the result is equals (minus rounding error) to:
+     * In most cases, the result is equal (minus rounding error) to:
      *
      * {@preformat java
      *     span = getUpper(dimension) - getLower(dimension);
@@ -494,7 +494,7 @@
      *
      * @param  dimension  the dimension for which to obtain the span.
      * @return the span (typically width or height) at the given dimension, or {@link Double#NaN}.
-     * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+     * @throws IndexOutOfBoundsException if the given index is negative or is equal or greater
      *         than the {@linkplain #getDimension() envelope dimension}.
      */
     @Override
@@ -874,7 +874,7 @@
                 /*
                  * If this envelope does not cross the anti-meridian but the given envelope does,
                  * then this envelope does not contain the given envelope except in the special
-                 * case where the envelope spanning is equals or greater than the axis spanning
+                 * case where the envelope spanning is equal or greater than the axis spanning
                  * (including the case where this envelope expands to infinities).
                  */
                 if ((lower0 == Double.NEGATIVE_INFINITY && upper0 == Double.POSITIVE_INFINITY) ||
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
index 194ae0e..c06a064 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
@@ -178,7 +178,7 @@
     /**
      * Value of {@link #desiredPrecisions} which cause {@link #accuracyText} to be shown.
      * For each dimension identified by {@link #groundDimensions}, if the corresponding
-     * value in {@link #desiredPrecisions} is equals or smaller to this threshold, then
+     * value in {@link #desiredPrecisions} is equal or smaller to this threshold, then
      * {@link #accuracyText} will be appended after the formatted coordinates.
      *
      * @see #desiredPrecisions
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
index c420ab4..e270524 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
@@ -833,7 +833,7 @@
             if ((includedMinValue & includedMaxValue & dimensionBitMask) == 0 && CoordinateOperations.isWrapAround(axis)) {
                 isWrapAroundAxis |= dimensionBitMask;
             }
-            // Restore `targetPt` to its initial state, which is equals to `centerPt`.
+            // Restore `targetPt` to its initial state, which is equal to `centerPt`.
             if (targetPt != null) {
                 targetPt.setOrdinate(i, centerPt[i]);
             }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java
index 30f6354..62ef1b3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java
@@ -23,6 +23,7 @@
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
 import javax.measure.quantity.Length;
+import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterNotFoundException;
@@ -63,11 +64,11 @@
  * However this class may move in a public package later if we feel confident that its API is mature enough.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.6
  * @module
  */
-public final class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> {
+public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> {
     /**
      * The geodetic datum, or {@code null} if none.
      */
@@ -517,8 +518,7 @@
     /**
      * Replaces the component starting at given index by the given component. This method can be used for replacing
      * e.g. the horizontal component of a CRS, or the vertical component, <i>etc.</i>. If a new compound CRS needs
-     * to be created and a {@linkplain #addName(org.opengis.util.GenericName) name has been specified}, that name
-     * will be used.
+     * to be created and a {@linkplain #addName(GenericName) name has been specified}, that name will be used.
      *
      * <h4>Limitations</h4>
      * Current implementation can replace exactly one component of {@link CompoundCRS}.
@@ -538,7 +538,11 @@
         final int srcDim = ReferencingUtilities.getDimension(source);
         final int repDim = ReferencingUtilities.getDimension(replacement);
         if (firstDimension == 0 && srcDim == repDim) {
-            return replacement;
+            /*
+             * conceptually return the replacement. But returning the original instance if applicable
+             * allows the caller to detect that a compound CRS does not need to be replaced.
+             */
+            return source.equals(replacement) ? source : replacement;
         }
         ArgumentChecks.ensureValidIndex(srcDim - repDim, firstDimension);
         if (source instanceof CompoundCRS) {
@@ -556,8 +560,8 @@
                     Object ids   = properties.remove(IdentifiedObject.IDENTIFIERS_KEY);
                     final CoordinateReferenceSystem nc = replaceComponent(c, firstDimension - lower, replacement);
                     /*
-                     * Restore the names and identifiers before to create the final CompoundCRS. If no name was specified,
-                     * reuse the primary name of existing CRS but not the identifiers.
+                     * Restore the names and identifiers before to create the final CompoundCRS.
+                     * If no name was specified, reuse the primary name of existing CRS but not the identifiers.
                      */
                     if (name == null) {
                         name = source.getName();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
index 3df50b8..c3825e8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
@@ -509,38 +509,33 @@
     }
 
     /**
-     * Sets the source and target ellipsoids and coordinate systems to values inferred from the given CRS.
-     * The ellipsoids will be non-null only if the given CRS is geographic (not geocentric).
+     * Creates a context with source and target ellipsoids and coordinate systems inferred from the given CRS.
+     * The ellipsoids will be non-null only if the given CRS is geodetic (geographic or geocentric).
      *
-     * @param  sourceCRS  the CRS from which to get the source coordinate system and ellipsoid.
-     * @param  targetCRS  the CRS from which to get the target coordinate system and ellipsoid.
-     * @param  context    a pre-allocated context, or {@code null} for creating a new one.
-     * @return the given context if it was non-null, or a new context otherwise.
+     * @param  sourceCRS  the CRS from which to get the source coordinate system and ellipsoid, or {@code null}.
+     * @param  targetCRS  the CRS from which to get the target coordinate system and ellipsoid, or {@code null}.
+     * @return the context to provides to math transform factory.
      */
     public static Context createTransformContext(final CoordinateReferenceSystem sourceCRS,
-            final CoordinateReferenceSystem targetCRS, Context context)
+                                                 final CoordinateReferenceSystem targetCRS)
     {
-        if (context == null) {
-            context = new Context();
+        final Context context = new Context();
+        if (sourceCRS instanceof GeodeticCRS) {
+            context.setSource((GeodeticCRS) sourceCRS);
+        } else if (sourceCRS != null) {
+            context.setSource(sourceCRS.getCoordinateSystem());
         }
-        final CoordinateSystem sourceCS = (sourceCRS != null) ? sourceCRS.getCoordinateSystem() : null;
-        final CoordinateSystem targetCS = (targetCRS != null) ? targetCRS.getCoordinateSystem() : null;
-        if (sourceCRS instanceof GeodeticCRS && sourceCS instanceof EllipsoidalCS) {
-            context.setSource((EllipsoidalCS) sourceCS, ((GeodeticCRS) sourceCRS).getDatum().getEllipsoid());
-        } else {
-            context.setSource(sourceCS);
-        }
-        if (targetCRS instanceof GeodeticCRS && targetCS instanceof EllipsoidalCS) {
-            context.setTarget((EllipsoidalCS) targetCS, ((GeodeticCRS) targetCRS).getDatum().getEllipsoid());
-        } else {
-            context.setTarget(targetCS);
+        if (targetCRS instanceof GeodeticCRS) {
+            context.setTarget((GeodeticCRS) targetCRS);
+        } else if (targetCRS != null) {
+            context.setTarget(targetCRS.getCoordinateSystem());
         }
         return context;
     }
 
     /**
      * Substitute for the deprecated {@link MathTransformFactory#createBaseToDerived createBaseToDerived(…)} method.
-     * This substitute use the full {@code targetCRS} instead of only the coordinate system of the target.
+     * This substitute uses the full {@code targetCRS} instead of only the coordinate system of the target.
      * This is needed for setting the {@code "tgt_semi_minor"} and {@code "tgt_semi_major"} parameters of
      * Molodensky transformation for example.
      *
@@ -561,7 +556,7 @@
     {
         if (factory instanceof DefaultMathTransformFactory) {
             return ((DefaultMathTransformFactory) factory).createParameterizedTransform(
-                    parameters, createTransformContext(sourceCRS, targetCRS, null));
+                    parameters, createTransformContext(sourceCRS, targetCRS));
         } else {
             // Fallback for non-SIS implementations. Work for map projections but not for Molodensky.
             return factory.createBaseToDerived(sourceCRS, parameters, targetCRS.getCoordinateSystem());
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbridgedMolodensky.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbridgedMolodensky.java
index afff092..329bf5c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbridgedMolodensky.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbridgedMolodensky.java
@@ -44,7 +44,7 @@
  *
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -86,15 +86,7 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
      */
     private AbridgedMolodensky(int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, targetDimensions, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * While Abridged Molodensky method is an approximation of geocentric translation, this is not exactly that.
-     */
-    @Override
-    int getType() {
-        return OTHER;
+        super(Type.MOLODENSKY, PARAMETERS, sourceDimensions, targetDimensions, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractLambert.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractLambert.java
index 4b73df5..25e79a8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractLambert.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractLambert.java
@@ -29,7 +29,7 @@
  * Base class of providers for all Lambert Conical projections.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.6
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -78,17 +78,7 @@
      * For subclass constructors only.
      */
     AbstractLambert(final ParameterDescriptorGroup parameters) {
-        super(parameters);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code ConicProjection.class}
-     */
-    @Override
-    public final Class<ConicProjection> getOperationType() {
-        return ConicProjection.class;
+        super(ConicProjection.class, parameters);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractMercator.java
index 417dfb6..e4ecf0e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractMercator.java
@@ -31,7 +31,7 @@
  * Base class of providers for all Mercator projections, and for Mercator-like projections.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -97,17 +97,7 @@
      * For subclass constructors only.
      */
     AbstractMercator(final ParameterDescriptorGroup parameters) {
-        super(parameters);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code CylindricalProjection.class}
-     */
-    @Override
-    public final Class<CylindricalProjection> getOperationType() {
-        return CylindricalProjection.class;
+        super(CylindricalProjection.class, parameters);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
index 78738e9..eb2e7aa 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
@@ -24,7 +24,8 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.ReferenceIdentifier;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.operation.SingleOperation;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.measure.Units;
 import org.apache.sis.measure.Latitude;
@@ -43,12 +44,15 @@
 
 import static java.util.logging.Logger.getLogger;
 
+// Branch-dependent imports
+import org.opengis.referencing.ReferenceIdentifier;
+
 
 /**
  * Base class for all providers defined in this package.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -57,7 +61,45 @@
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = 2239172887926695217L;
+    private static final long serialVersionUID = 1165868434518724597L;
+
+    /**
+     * The base interface of the {@code CoordinateOperation} instances that use this method.
+     *
+     * @see #getOperationType()
+     */
+    private final Class<? extends SingleOperation> operationType;
+
+    /**
+     * The base interface of the coordinate system of source/target coordinates.
+     * This is used for resolving some ambiguities at WKT parsing time.
+     */
+    public final Class<? extends CoordinateSystem> sourceCSType, targetCSType;
+
+    /**
+     * Flags whether the source and/or target ellipsoid are concerned by this operation. Those flags are read by
+     * {@link org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory} for determining if this
+     * operation has {@code "semi_major"}, {@code "semi_minor"}, {@code "src_semi_major"}, {@code "src_semi_minor"}
+     * parameters that may need to be filled with values inferred from the source or target
+     * {@link org.apache.sis.referencing.datum.DefaultGeodeticDatum}.
+     * Meaning of return values:
+     *
+     * <ul>
+     *   <li>({@code false},{@code false}) if neither the source coordinate system or the destination
+     *       coordinate system is ellipsoidal. There are no parameters that need to be completed.</li>
+     *   <li>({@code true},{@code false}) if this operation has {@code "semi_major"} and {@code "semi_minor"}
+     *       parameters that need to be set to the axis lengths of the source ellipsoid.</li>
+     *   <li>({@code false},{@code true}) if this operation has {@code "semi_major"} and {@code "semi_minor"}
+     *       parameters that need to be set to the axis lengths of the target ellipsoid.</li>
+     *   <li>({@code true},{@code true}) if this operation has {@code "src_semi_major"}, {@code "src_semi_minor"},
+     *       {@code "tgt_semi_major"} and {@code "tgt_semi_minor"} parameters that need to be set to the axis lengths
+     *       of the source and target ellipsoids.</li>
+     * </ul>
+     *
+     * Those flags are only hints. If the information is not provided, {@code DefaultMathTransformFactory}
+     * will try to infer it from the type of user-specified source and target CRS.
+     */
+    public final boolean sourceOnEllipsoid, targetOnEllipsoid;
 
     /**
      * Constructs a math transform provider from the given properties and a set of parameters.
@@ -67,27 +109,42 @@
      * @param targetDimension  number of dimensions in the target CRS of this operation method.
      * @param parameters       the set of parameters (never {@code null}).
      */
-    AbstractProvider(final Map<String,?> properties,
-                     final int sourceDimension,
-                     final int targetDimension,
-                     final ParameterDescriptorGroup parameters)
+    protected AbstractProvider(final Map<String,?> properties,
+                               final int sourceDimension,
+                               final int targetDimension,
+                               final ParameterDescriptorGroup parameters)
     {
         super(properties, sourceDimension, targetDimension, parameters);
+        operationType = SingleOperation.class;
+        sourceCSType  = CoordinateSystem.class;
+        targetCSType  = CoordinateSystem.class;
+        sourceOnEllipsoid = false;
+        targetOnEllipsoid = false;
     }
 
     /**
      * Constructs a math transform provider from a set of parameters. The provider name and
      * {@linkplain #getIdentifiers() identifiers} will be the same than the parameter ones.
      *
-     * @param sourceDimensions  number of dimensions in the source CRS of this operation method.
-     * @param targetDimensions  number of dimensions in the target CRS of this operation method.
-     * @param parameters        description of parameters expected by this operation.
+     * @param operationType      base interface of the {@code CoordinateOperation} instances that use this method.
+     * @param parameters         description of parameters expected by this operation.
+     * @param sourceDimensions   number of dimensions in the source CRS of this operation method.
+     * @param sourceCSType       base interface of the coordinate system of source coordinates.
+     * @param sourceOnEllipsoid  whether the operation needs source ellipsoid axis lengths.
+     * @param targetDimensions   number of dimensions in the target CRS of this operation method.
+     * @param targetCSType       base interface of the coordinate system of target coordinates.
+     * @param targetOnEllipsoid  whether the operation needs target ellipsoid axis lengths.
      */
-    AbstractProvider(final int sourceDimensions,
-                     final int targetDimensions,
-                     final ParameterDescriptorGroup parameters)
+    AbstractProvider(final Class<? extends SingleOperation> operationType, final ParameterDescriptorGroup parameters,
+                     final Class<? extends CoordinateSystem> sourceCSType, final int sourceDimensions, final boolean sourceOnEllipsoid,
+                     final Class<? extends CoordinateSystem> targetCSType, final int targetDimensions, final boolean targetOnEllipsoid)
     {
         super(toMap(parameters), sourceDimensions, targetDimensions, parameters);
+        this.operationType     = operationType;
+        this.sourceCSType      = sourceCSType;
+        this.targetCSType      = targetCSType;
+        this.sourceOnEllipsoid = sourceOnEllipsoid;
+        this.targetOnEllipsoid = targetOnEllipsoid;
     }
 
     /**
@@ -212,32 +269,14 @@
     }
 
     /**
-     * Flags whether the source and/or target ellipsoid are concerned by this operation. This method is invoked by
-     * {@link org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory} for determining if this
-     * operation has {@code "semi_major"}, {@code "semi_minor"}, {@code "src_semi_major"}, {@code "src_semi_minor"}
-     * parameters that may need to be filled with values inferred from the source or target
-     * {@link org.apache.sis.referencing.datum.DefaultGeodeticDatum}.
-     * Meaning of return values:
+     * Returns the interface implemented by the coordinate operation.
+     * This method returns the type specified at construction time.
      *
-     * <ul>
-     *   <li>0 if neither the source coordinate system or the destination coordinate system is ellipsoidal.
-     *       There are no parameters that need to be completed.</li>
-     *   <li>1 if this operation has {@code "semi_major"} and {@code "semi_minor"} parameters that need
-     *       to be set to the axis lengths of the source ellipsoid.</li>
-     *   <li>2 if this operation has {@code "semi_major"} and {@code "semi_minor"} parameters that need
-     *       to be set to the axis lengths of the target ellipsoid.</li>
-     *   <li>3 if this operation has {@code "src_semi_major"}, {@code "src_semi_minor"}, {@code "tgt_semi_major"}
-     *       and {@code "tgt_semi_minor"} parameters that need to be set to the axis lengths of the source and
-     *       target ellipsoids.</li>
-     * </ul>
-     *
-     * This method is just a hint. If the information is not provided, {@code DefaultMathTransformFactory}
-     * will try to infer it from the type of user-specified source and target CRS.
-     *
-     * @return 0, 1, 2 or 3.
+     * @return interface implemented by all coordinate operations that use this method.
      */
-    public int getEllipsoidsMask() {
-        return 0;
+    @Override
+    public final Class<? extends SingleOperation> getOperationType() {
+        return operationType;
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java
index d15a4a9..0de2d14 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractStereographic.java
@@ -30,7 +30,7 @@
  * Base class of providers for all Stereographic projections.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -79,17 +79,7 @@
      * For subclass constructors only.
      */
     AbstractStereographic(final ParameterDescriptorGroup parameters) {
-        super(parameters);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code PlanarProjection.class}
-     */
-    @Override
-    public final Class<PlanarProjection> getOperationType() {
-        return PlanarProjection.class;
+        super(PlanarProjection.class, parameters);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java
index 93b512d..5267604 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java
@@ -31,7 +31,7 @@
  *
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/albers_equal_area_conic.html">GeoTIFF parameters for Albers Equal-Area Conic</a>
  *
@@ -250,17 +250,7 @@
      * Constructs a new provider.
      */
     public AlbersEqualArea() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code ConicProjection.class}
-     */
-    @Override
-    public final Class<ConicProjection> getOperationType() {
-        return ConicProjection.class;
+        super(ConicProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal.java
index 7545371..eebde60 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal.java
@@ -19,6 +19,7 @@
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -29,11 +30,11 @@
 
 /**
  * The provider for <cite>"axis order reversal (2D)"</cite> (EPSG:9843).
- * This is a trivial operation that just swap the two axes.
+ * This is a trivial operation that just swap the two first axes.
  * The inverse operation is this operation itself.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -59,7 +60,7 @@
      * Constructs a provider with default parameters.
      */
     public AxisOrderReversal() {
-        super(2, 2, PARAMETERS);
+        this(PARAMETERS, 2);
     }
 
     /**
@@ -68,18 +69,10 @@
      * @param dimensions  number of dimensions in the source and target CRS of this operation method.
      * @param parameters  description of parameters expected by this operation.
      */
-    AxisOrderReversal(final int dimensions, final ParameterDescriptorGroup parameters) {
-        super(dimensions, dimensions, parameters);
-    }
-
-    /**
-     * Returns the operation type.
-     *
-     * @return interface implemented by all coordinate operations that use this method.
-     */
-    @Override
-    public final Class<Conversion> getOperationType() {
-        return Conversion.class;
+    AxisOrderReversal(final ParameterDescriptorGroup parameters, final int dimensions) {
+        super(Conversion.class, parameters,
+              CoordinateSystem.class, dimensions, false,
+              CoordinateSystem.class, dimensions, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal3D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal3D.java
index 019d62c..1ad20ce 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal3D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AxisOrderReversal3D.java
@@ -26,7 +26,7 @@
  * The inverse operation is this operation itself.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -47,6 +47,6 @@
      * Constructs a provider with default parameters.
      */
     public AxisOrderReversal3D() {
-        super(3, PARAMETERS);
+        super(PARAMETERS, 3);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AzimuthalEquidistantSpherical.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AzimuthalEquidistantSpherical.java
index e5727b7..294c733 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AzimuthalEquidistantSpherical.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AzimuthalEquidistantSpherical.java
@@ -28,7 +28,7 @@
  * This projection method has no EPSG code.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/azimuthal_equidistant.html">GeoTIFF parameters for Azimuthal Equidistant</a>
  *
@@ -60,15 +60,7 @@
      * Constructs a new provider.
      */
     public AzimuthalEquidistantSpherical() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     */
-    @Override
-    public Class<PlanarProjection> getOperationType() {
-        return PlanarProjection.class;
+        super(PlanarProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CassiniSoldner.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CassiniSoldner.java
index 307b019..b71609e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CassiniSoldner.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CassiniSoldner.java
@@ -19,6 +19,7 @@
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.Projection;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.operation.projection.NormalizedProjection;
 import org.apache.sis.parameter.Parameters;
@@ -29,7 +30,7 @@
  * This projection is similar to {@link TransverseMercator}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/cassini_soldner.html">GeoTIFF parameters for Cassini-Soldner</a>
  *
@@ -161,14 +162,14 @@
      * Constructs a new provider.
      */
     public CassiniSoldner() {
-        super(PARAMETERS);
+        this(PARAMETERS);
     }
 
     /**
      * Constructs a provider from a set of parameters.
      */
     CassiniSoldner(final ParameterDescriptorGroup parameters) {
-        super(parameters);
+        super(Projection.class, parameters);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation.java
index c56dde6..19250d6 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation.java
@@ -26,7 +26,7 @@
  * except that the rotation angles have the opposite sign.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -57,14 +57,6 @@
      * Constructs the provider.
      */
     public CoordinateFrameRotation() {
-        super(3, 3, PARAMETERS, null);
-    }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return FRAME_ROTATION;
+        super(Type.FRAME_ROTATION, PARAMETERS, 3, 3, null);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation2D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation2D.java
index 793ee0ba..0636a5c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation2D.java
@@ -27,7 +27,7 @@
  * except that the rotation angles have the opposite sign.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -66,7 +66,7 @@
      * Constructs a provider that can be resized.
      */
     CoordinateFrameRotation2D(GeodeticOperation[] redimensioned) {
-        super(2, 2, PARAMETERS, redimensioned);
+        super(Type.FRAME_ROTATION, PARAMETERS, 2, 2, redimensioned);
     }
 
     /**
@@ -76,12 +76,4 @@
     Class<CoordinateFrameRotation3D> variant3D() {
         return CoordinateFrameRotation3D.class;
     }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return FRAME_ROTATION;
-    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation3D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation3D.java
index 12807fb..3bad35f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation3D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/CoordinateFrameRotation3D.java
@@ -26,7 +26,7 @@
  * except that the rotation angles have the opposite sign.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -72,14 +72,6 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
      */
     private CoordinateFrameRotation3D(int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, targetDimensions, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return FRAME_ROTATION;
+        super(Type.FRAME_ROTATION, PARAMETERS, sourceDimensions, targetDimensions, redimensioned);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java
index aceb60b..690a331 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Equirectangular.java
@@ -22,6 +22,8 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.CylindricalProjection;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -61,7 +63,7 @@
  *
  * @author  John Grange
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see PseudoPlateCarree
  * @see <a href="http://geotiff.maptools.org/proj_list/equirectangular.html">GeoTIFF parameters for Equirectangular</a>
@@ -270,30 +272,13 @@
 
     /**
      * Constructs a new provider.
+     *
+     * @see MapProjection#MapProjection(Class, ParameterDescriptorGroup)
      */
     public Equirectangular() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code CylindricalProjection.class}
-     */
-    @Override
-    public Class<CylindricalProjection> getOperationType() {
-        return CylindricalProjection.class;
-    }
-
-    /**
-     * Notifies {@code DefaultMathTransformFactory} that map projections require
-     * values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
-     *
-     * @return 1, meaning that the operation requires a source ellipsoid.
-     */
-    @Override
-    public final int getEllipsoidsMask() {
-        return 1;
+        super(CylindricalProjection.class, PARAMETERS,
+              EllipsoidalCS.class, 2, true,
+              CartesianCS.class,   2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
index 73435de..8fab5d5 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
@@ -38,6 +38,8 @@
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.parameter.InvalidParameterValueException;
 import org.opengis.referencing.datum.Ellipsoid;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
@@ -84,7 +86,7 @@
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -242,7 +244,9 @@
                                   final ParameterDescriptorGroup parameters,
                                   final GeodeticOperation[] redimensioned)
     {
-        super(sourceDimensions, targetDimensions, parameters, redimensioned);
+        super(Transformation.class, parameters,
+              EllipsoidalCS.class, sourceDimensions, true,
+              EllipsoidalCS.class, targetDimensions, true, redimensioned);
     }
 
     /**
@@ -269,18 +273,6 @@
     }
 
     /**
-     * Notifies {@code DefaultMathTransformFactory} that map projections require values for the
-     * {@code "src_semi_major"}, {@code "src_semi_minor"} , {@code "tgt_semi_major"} and
-     * {@code "tgt_semi_minor"} parameters.
-     *
-     * @return 3, meaning that the operation requires source and target ellipsoids.
-     */
-    @Override
-    public int getEllipsoidsMask() {
-        return 3;
-    }
-
-    /**
      * Creates the source or the target ellipsoid. This is a temporary ellipsoid
      * used only at {@link InterpolatedGeocentricTransform} time, then discarded.
      *
@@ -337,6 +329,7 @@
         final Path file = pg.getMandatoryValue(FILE);
         final DatumShiftGridFile<Angle,Length> grid = getOrLoad(file,
                 isRecognized(file) ? new double[] {TX, TY, TZ} : null, PRECISION);
+
         MathTransform tr = createGeodeticTransformation(factory,
                 createEllipsoid(pg, Molodensky.TGT_SEMI_MAJOR,
                                     Molodensky.TGT_SEMI_MINOR, CommonCRS.ETRS89.ellipsoid()),   // GRS 1980 ellipsoid
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
index ff3ecc2..0488d2b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
@@ -28,6 +28,8 @@
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.Conversion;
+import org.opengis.referencing.operation.Transformation;
 import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.referencing.WKTKeywords;
@@ -58,16 +60,25 @@
  * "Geocentric translations" is an operation name defined by EPSG.</div>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.7
  * @module
  */
 @XmlTransient
 public abstract class GeocentricAffine extends GeodeticOperation {
     /**
+     * The transformation type (translation, frame rotation, <i>etc.</i>).
+     *
+     * @todo Merge with {@link DatumShiftMethod}.
+     *
+     * @see #type
+     */
+    protected enum Type {TRANSLATION, SEVEN_PARAM, FRAME_ROTATION, MOLODENSKY, CONVERSION};
+
+    /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 8291967302538661639L;
+    private static final long serialVersionUID = 5597594719123422532L;
 
     /**
      * The tolerance factor for comparing the {@link BursaWolfParameters} values.
@@ -206,27 +217,42 @@
     }
 
     /**
-     * Return value for {@link #getType()}.
+     * The transformation type (translation, frame rotation, <i>etc.</i>).
      */
-    static final int TRANSLATION=1, SEVEN_PARAM=2, FRAME_ROTATION=3, OTHER=0;
+    private final Type type;
 
     /**
      * Constructs a provider with the specified parameters.
      *
-     * @param sourceDimensions  number of dimensions in the source CRS of this operation method.
-     * @param targetDimensions  number of dimensions in the target CRS of this operation method.
-     * @param parameters        description of parameters expected by this operation.
-     * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
+     * @param type               the operation type as an enumeration value.
+     * @param parameters         description of parameters expected by this operation.
+     * @param sourceDimensions   number of dimensions in the source CRS of this operation method.
+     * @param sourceCSType       base interface of the coordinate system of source coordinates.
+     * @param sourceOnEllipsoid  whether the operation needs source ellipsoid axis lengths.
+     * @param targetDimensions   number of dimensions in the target CRS of this operation method.
+     * @param targetCSType       base interface of the coordinate system of target coordinates.
+     * @param targetOnEllipsoid  whether the operation needs target ellipsoid axis lengths.
+     * @param redimensioned      providers for all combinations between 2D and 3D cases, or {@code null}.
      */
-    GeocentricAffine(int sourceDimensions, int targetDimensions, ParameterDescriptorGroup parameters, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, targetDimensions, parameters, redimensioned);
+    GeocentricAffine(Type operationType, ParameterDescriptorGroup parameters,
+                     Class<? extends CoordinateSystem> sourceCSType, int sourceDimensions, boolean sourceOnEllipsoid,
+                     Class<? extends CoordinateSystem> targetCSType, int targetDimensions, boolean targetOnEllipsoid,
+                     GeodeticOperation[] redimensioned)
+    {
+        super((operationType == Type.CONVERSION) ? Conversion.class : Transformation.class, parameters,
+              sourceCSType, sourceDimensions, sourceOnEllipsoid,
+              targetCSType, targetDimensions, targetOnEllipsoid, redimensioned);
+        type = operationType;
     }
 
     /**
-     * Returns the operation sub-type as one of {@link #TRANSLATION}, {@link #SEVEN_PARAM},
-     * {@link #FRAME_ROTATION} or {@link #OTHER} constants.
+     * Constructs a provider with the specified parameters for an operation in Cartesian space.
      */
-    abstract int getType();
+    GeocentricAffine(Type operationType, ParameterDescriptorGroup parameters, int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned) {
+        this(operationType, parameters,
+             CartesianCS.class, sourceDimensions, false,
+             CartesianCS.class, targetDimensions, false, redimensioned);
+    }
 
     /**
      * Creates a math transform from the specified group of parameter values.
@@ -246,8 +272,8 @@
         final BursaWolfParameters parameters = new BursaWolfParameters(null, null);
         final Parameters pv = Parameters.castOrWrap(values);
         boolean reverseRotation = false;
-        switch (getType()) {
-            default:             throw new AssertionError();
+        switch (type) {
+            default:             throw new AssertionError(type);
             case FRAME_ROTATION: reverseRotation = true;                    // Fall through
             case SEVEN_PARAM:    parameters.rX = pv.doubleValue(RX);
                                  parameters.rY = pv.doubleValue(RY);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java
index 96a2e34..32f5e45 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java
@@ -21,6 +21,7 @@
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
@@ -38,8 +39,17 @@
  * operation methods performing <em>approximation</em> of above, even if they do not really pass
  * through geocentric coordinates.
  *
+ * <h2>Default values to verify</h2>
+ * This class assumes the following default values.
+ * Subclasses should verify if those default values are suitable from them:
+ *
+ * <ul>
+ *   <li>{@link #getOperationType()} defaults to {@link org.opengis.referencing.operation.Transformation}.</li>
+ *   <li>{@link #sourceCSType} and {@link #targetCSType} default to {@link EllipsoidalCS}.</li>
+ * </ul>
+ *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -157,27 +167,18 @@
     /**
      * Constructs a provider with the specified parameters.
      *
+     * @param type              the operation type as an enumeration value.
+     * @param parameters        description of parameters expected by this operation.
      * @param sourceDimensions  number of dimensions in the source CRS of this operation method.
      * @param targetDimensions  number of dimensions in the target CRS of this operation method.
-     * @param parameters        description of parameters expected by this operation.
      * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
      */
-    GeocentricAffineBetweenGeographic(int sourceDimensions, int targetDimensions,
-            ParameterDescriptorGroup parameters, GeodeticOperation[] redimensioned)
+    GeocentricAffineBetweenGeographic(Type operationType, ParameterDescriptorGroup parameters,
+            int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned)
     {
-        super(sourceDimensions, targetDimensions, parameters, redimensioned);
-    }
-
-    /**
-     * Notifies {@code DefaultMathTransformFactory} that this operation requires values for
-     * the {@code "src_semi_major"}, {@code "src_semi_minor"}, {@code "tgt_semi_major"} and
-     * {@code "tgt_semi_minor"} parameters.
-     *
-     * @return 3, meaning that the operation requires source and target ellipsoids.
-     */
-    @Override
-    public final int getEllipsoidsMask() {
-        return 3;
+        super(operationType, parameters,
+              EllipsoidalCS.class, sourceDimensions, true,
+              EllipsoidalCS.class, targetDimensions, true, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToGeographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToGeographic.java
index 3c67c3c..161d313 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToGeographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToGeographic.java
@@ -22,6 +22,8 @@
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.util.FactoryException;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.parameter.Parameters;
@@ -32,7 +34,7 @@
  * This provider creates transforms from geocentric to geographic coordinate reference systems.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see GeographicToGeocentric
  *
@@ -77,28 +79,9 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases.
      */
     private GeocentricToGeographic(int targetDimensions, GeodeticOperation[] redimensioned) {
-        super(3, targetDimensions, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * Returns the operation type.
-     *
-     * @return {@code Conversion.class}.
-     */
-    @Override
-    public Class<Conversion> getOperationType() {
-        return Conversion.class;
-    }
-
-    /**
-     * Notifies {@code DefaultMathTransformFactory} that Geographic/geocentric conversions
-     * require values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
-     *
-     * @return 2, meaning that the operation requires a target ellipsoid.
-     */
-    @Override
-    public int getEllipsoidsMask() {
-        return 2;
+        super(Conversion.class, PARAMETERS,
+              CartesianCS.class, 3, false,
+              EllipsoidalCS.class, targetDimensions, true, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToTopocentric.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToTopocentric.java
new file mode 100644
index 0000000..18141be
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricToTopocentric.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing.provider;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValue;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.operation.Conversion;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.EllipsoidToCentricTransform;
+import org.apache.sis.referencing.operation.matrix.Matrix4;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.measure.Units;
+import org.apache.sis.internal.util.Constants;
+
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.toRadians;
+
+
+/**
+ * The provider for the <cite>"Geocentric/topocentric conversions"</cite> (EPSG:9836).
+ * This operation is implemented using existing {@link MathTransform} implementations;
+ * there is no need for a class specifically for this transform.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public final class GeocentricToTopocentric extends AbstractProvider {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 6064563343153407987L;
+
+    /**
+     * The operation parameter descriptor for the <cite>Geocentric X of topocentric origin</cite> (X) parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> EPSG:    </td><td> Geocentric X of topocentric origin </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> ORIGIN_X;
+
+    /**
+     * The operation parameter descriptor for the <cite>Geocentric Y of topocentric origin</cite> (Y) parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> EPSG:    </td><td> Geocentric Y of topocentric origin </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> ORIGIN_Y;
+
+    /**
+     * The operation parameter descriptor for the <cite>Geocentric Z of topocentric origin</cite> (Z) parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> EPSG:    </td><td> Geocentric Z of topocentric origin </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>No default value</li>
+     * </ul>
+     */
+    private static final ParameterDescriptor<Double> ORIGIN_Z;
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    private static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        final ParameterBuilder builder = builder();
+        ORIGIN_X = builder
+                .addIdentifier("8837")
+                .addName("Geocentric X of topocentric origin")
+                .create(Double.NaN, Units.METRE);
+
+        ORIGIN_Y = builder
+                .addIdentifier("8838")
+                .addName("Geocentric Y of topocentric origin")
+                .create(Double.NaN, Units.METRE);
+
+        ORIGIN_Z = builder
+                .addIdentifier("8839")
+                .addName("Geocentric Z of topocentric origin")
+                .create(Double.NaN, Units.METRE);
+
+        PARAMETERS = builder
+                .addIdentifier("9836")
+                .addName("Geocentric/topocentric conversions")
+                .createGroupForMapProjection(ORIGIN_X, ORIGIN_Y, ORIGIN_Z);
+                // Not really a map projection, but we leverage the same axis parameters.
+    }
+
+    /**
+     * Constructs a provider for the 3-dimensional case.
+     */
+    public GeocentricToTopocentric() {
+        super(Conversion.class, PARAMETERS,
+              CartesianCS.class, 3, true,
+              CartesianCS.class, 3, false);
+    }
+
+    /**
+     * Creates a transform from the specified group of parameter values.
+     * The unit of measurement of input coordinates will be the units of the ellipsoid axes.
+     *
+     * @param  factory  the factory to use for creating the transform.
+     * @param  values   the parameter values that define the transform to create.
+     * @return the conversion from geocentric to topocentric coordinates.
+     * @throws FactoryException if an error occurred while creating a transform.
+     */
+    @Override
+    public MathTransform createMathTransform(final MathTransformFactory factory, final ParameterValueGroup values)
+            throws FactoryException
+    {
+        try {
+            return create(factory, Parameters.castOrWrap(values), false);
+        } catch (TransformException e) {
+            throw new FactoryException(e);
+        }
+    }
+
+    /**
+     * Implementation of {@link #createMathTransform(MathTransformFactory, ParameterValueGroup)}
+     * shared with {@link GeographicToTopocentric}.
+     *
+     * @param  factory     the factory to use for creating the transform.
+     * @param  values      the parameter values that define the transform to create.
+     * @param  geographic  {@code true} if the source coordinates are geographic, or
+     *                     {@code false} if the source coordinates are geocentric.
+     */
+    static MathTransform create(final MathTransformFactory factory, final Parameters values, final boolean geographic)
+            throws FactoryException, TransformException
+    {
+        final ParameterValue<?> ap = values.parameter(Constants.SEMI_MAJOR);
+        final Unit<Length> unit = ap.getUnit().asType(Length.class);
+        final double a = ap.doubleValue();
+        final double b = values.parameter(Constants.SEMI_MINOR).doubleValue(unit);
+        final double x, y, z, λ, φ;
+        final MathTransform toGeocentric;
+        if (geographic) {
+            /*
+             * Full conversion from (longitude, latitude, height) in degrees
+             * to geocentric coordinates in linear units (usually metres).
+             */
+            toGeocentric = EllipsoidToCentricTransform.createGeodeticConversion(factory,
+                    a, b, unit, true, EllipsoidToCentricTransform.TargetType.CARTESIAN);
+
+            final double[] origin = new double[] {
+                values.doubleValue(GeographicToTopocentric.ORIGIN_X),
+                values.doubleValue(GeographicToTopocentric.ORIGIN_Y),
+                values.doubleValue(GeographicToTopocentric.ORIGIN_Z)};
+
+            λ = toRadians(origin[0]);
+            φ = toRadians(origin[1]);
+            toGeocentric.transform(origin, 0, origin, 0, 1);
+            x = origin[0];
+            y = origin[1];
+            z = origin[2];
+        } else {
+            /*
+             * Shorter conversion from (longitude, latitude) in radians to
+             * geocentric coordinates as fractions of semi-major axis length.
+             * This conversion is used only in this block and is not kept.
+             */
+            toGeocentric = new EllipsoidToCentricTransform(
+                    a, b, unit, false, EllipsoidToCentricTransform.TargetType.CARTESIAN);
+
+            final double[] origin = new double[] {
+                (x = values.doubleValue(ORIGIN_X, unit)) / a,
+                (y = values.doubleValue(ORIGIN_Y, unit)) / a,
+                (z = values.doubleValue(ORIGIN_Z, unit)) / a};
+
+            toGeocentric.inverse().transform(origin, 0, origin, 0, 1);
+            λ = origin[0];         // Already in radians.
+            φ = origin[1];
+        }
+        final double sinλ = sin(λ);
+        final double cosλ = cos(λ);
+        final double sinφ = sin(φ);
+        final double cosφ = cos(φ);
+        /*
+         * Following transform uses the inverse of the matrix R given in EPSG guidance note
+         * because it allows us to put the (x,y,z) translation terms directly in the matrix.
+         */
+        MathTransform mt = factory.createAffineTransform(new Matrix4(
+                -sinλ,  -sinφ*cosλ,  cosφ*cosλ,  x,
+                 cosλ,  -sinφ*sinλ,  cosφ*sinλ,  y,
+                    0,   cosφ,       sinφ,       z,
+                    0,   0,          0,          1)).inverse();
+        if (geographic) {
+            mt = factory.createConcatenatedTransform(toGeocentric, mt);
+        }
+        return mt;
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation.java
index b2d75a4..30c55e1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation.java
@@ -26,7 +26,7 @@
  * can be set to a non-null value.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -52,14 +52,6 @@
      * Constructs the provider.
      */
     public GeocentricTranslation() {
-        super(3, 3, PARAMETERS, null);
-    }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return TRANSLATION;
+        super(Type.TRANSLATION, PARAMETERS, 3, 3, null);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation2D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation2D.java
index 2ddc373..9adfcfd 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation2D.java
@@ -27,7 +27,7 @@
  * terms can be set to a non-null value.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -65,7 +65,7 @@
      * Constructs a provider that can be resized.
      */
     GeocentricTranslation2D(GeodeticOperation[] redimensioned) {
-        super(2, 2, PARAMETERS, redimensioned);
+        super(Type.TRANSLATION, PARAMETERS, 2, 2, redimensioned);
     }
 
     /**
@@ -75,12 +75,4 @@
     Class<GeocentricTranslation3D> variant3D() {
         return GeocentricTranslation3D.class;
     }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return TRANSLATION;
-    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation3D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation3D.java
index 9a09fec..7e3ec85 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation3D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricTranslation3D.java
@@ -26,7 +26,7 @@
  * terms can be set to a non-null value.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -67,14 +67,6 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
      */
     private GeocentricTranslation3D(int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, targetDimensions, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return TRANSLATION;
+        super(Type.TRANSLATION, PARAMETERS, sourceDimensions, targetDimensions, redimensioned);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeodeticOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeodeticOperation.java
index d57b33c..1590b27 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeodeticOperation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeodeticOperation.java
@@ -18,9 +18,9 @@
 
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.SingleOperation;
-import org.opengis.referencing.operation.Transformation;
 
 
 /**
@@ -31,7 +31,7 @@
  * variants are specific to Apache SIS and can be fetched only by a call to {@link #redimension(int, int)}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -54,9 +54,13 @@
      * <strong>Do not modify this array after construction</strong>, since the same array is shared by many
      * objects and there is no synchronization.
      *
-     * @deprecated ISO 19111:2019 removed source/target dimensions attributes.
+     * <div class="note"><b>Historical note:</b>
+     * in ISO 19111:2007, the {@code OperationMethod} type had two attributes for the number of source
+     * and target dimensions. Those attributes have been removed in ISO 19111:2019 revision because not
+     * really needed in practice. However the EPSG database still distinguishes between 2D and 3D variants
+     * for some of those operations, so we still need the capability to switch operation methods according
+     * to the number of dimensions.</div>
      */
-    @Deprecated
     final GeodeticOperation[] redimensioned;
 
     /**
@@ -72,17 +76,24 @@
      *   <li>3 → 3 dimensions in {@code redimensioned[3]}</li>
      * </ol>
      *
-     * @param sourceDimensions  number of dimensions in the source CRS of this operation method.
-     * @param targetDimensions  number of dimensions in the target CRS of this operation method.
-     * @param parameters        description of parameters expected by this operation.
-     * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
+     * @param operationType      base interface of the {@code CoordinateOperation} instances that use this method.
+     * @param parameters         description of parameters expected by this operation.
+     * @param sourceDimensions   number of dimensions in the source CRS of this operation method.
+     * @param sourceCSType       base interface of the coordinate system of source coordinates.
+     * @param sourceOnEllipsoid  whether the operation needs source ellipsoid axis lengths.
+     * @param targetDimensions   number of dimensions in the target CRS of this operation method.
+     * @param targetCSType       base interface of the coordinate system of target coordinates.
+     * @param targetOnEllipsoid  whether the operation needs target ellipsoid axis lengths.
+     * @param redimensioned      providers for all combinations between 2D and 3D cases, or {@code null}.
      */
-    GeodeticOperation(final int sourceDimensions,
-                      final int targetDimensions,
-                      final ParameterDescriptorGroup parameters,
+    GeodeticOperation(Class<? extends SingleOperation> operationType, ParameterDescriptorGroup parameters,
+                      Class<? extends CoordinateSystem> sourceCSType, int sourceDimensions, boolean sourceOnEllipsoid,
+                      Class<? extends CoordinateSystem> targetCSType, int targetDimensions, boolean targetOnEllipsoid,
                       final GeodeticOperation[] redimensioned)
     {
-        super(sourceDimensions, targetDimensions, parameters);
+        super(operationType, parameters,
+              sourceCSType, sourceDimensions, sourceOnEllipsoid,
+              targetCSType, targetDimensions, targetOnEllipsoid);
         this.redimensioned = redimensioned;
     }
 
@@ -121,16 +132,6 @@
     }
 
     /**
-     * Returns the interface implemented by most coordinate operations that extends this class.
-     *
-     * @return default to {@link Transformation}.
-     */
-    @Override
-    public Class<? extends SingleOperation> getOperationType() {
-        return Transformation.class;
-    }
-
-    /**
      * The inverse of {@code GeodeticOperation} is usually the same operation with parameter signs inverted.
      *
      * @return {@code this} for most {@code GeodeticOperation} instances.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic2Dto3D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic2Dto3D.java
index 0d01f25..aa73a35 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic2Dto3D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic2Dto3D.java
@@ -40,7 +40,7 @@
  * format the inverse ({@code "INVERSE_MT"}) of 3D to 2D transform.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see Geographic3Dto2D
  *
@@ -86,7 +86,7 @@
      * Constructs a provider that can be resized.
      */
     Geographic2Dto3D(GeodeticOperation[] redimensioned) {
-        super(2, 3, PARAMETERS, redimensioned);
+        super(PARAMETERS, 2, 3, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic3Dto2D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic3Dto2D.java
index 8f3d26d..14030f8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic3Dto2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Geographic3Dto2D.java
@@ -39,7 +39,7 @@
  * The inverse operation arbitrarily sets the ellipsoidal height to zero.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see Geographic2Dto3D
  *
@@ -79,7 +79,7 @@
      * Constructs a provider that can be resized.
      */
     private Geographic3Dto2D(GeodeticOperation[] redimensioned) {
-        super(3, 2, PARAMETERS, redimensioned);
+        super(PARAMETERS, 3, 2, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java
index ec3470c..7f12ab2 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicOffsets.java
@@ -21,6 +21,8 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.apache.sis.parameter.ParameterBuilder;
@@ -35,7 +37,7 @@
  * but subclasses will provide different operations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -95,7 +97,7 @@
      * Constructs a provider with default parameters.
      */
     public GeographicOffsets() {
-        super(3, 3, PARAMETERS, new GeographicOffsets[4]);
+        this(3, 3, PARAMETERS, new GeographicOffsets[4]);
         redimensioned[0] = new GeographicOffsets2D(redimensioned);
         redimensioned[1] = new GeographicOffsets(2, 3, PARAMETERS, redimensioned);
         redimensioned[2] = new GeographicOffsets(3, 2, PARAMETERS, redimensioned);
@@ -106,9 +108,11 @@
      * For default constructors in this class and subclasses.
      */
     GeographicOffsets(int sourceDimensions, int targetDimensions,
-            ParameterDescriptorGroup parameters, GeodeticOperation[] redimensioned)
+                      ParameterDescriptorGroup parameters, GeodeticOperation[] redimensioned)
     {
-        super(sourceDimensions, targetDimensions, parameters, redimensioned);
+        super(Transformation.class, parameters,
+              EllipsoidalCS.class, sourceDimensions, false,
+              EllipsoidalCS.class, targetDimensions, false, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicRedimension.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicRedimension.java
index b1715cd..8c435cf 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicRedimension.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicRedimension.java
@@ -20,6 +20,7 @@
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -34,7 +35,7 @@
  * to {@link Geographic3Dto2D#redimension(int, int)} when the given number of dimensions are equal.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -49,30 +50,22 @@
      * Constructs a math transform provider from a set of parameters.
      * This is for sub-class constructors only.
      */
-    GeographicRedimension(final int sourceDimensions,
+    GeographicRedimension(final ParameterDescriptorGroup parameters,
+                          final int sourceDimensions,
                           final int targetDimensions,
-                          final ParameterDescriptorGroup parameters,
                           final GeodeticOperation[] redimensioned)
     {
-        super(sourceDimensions, targetDimensions, parameters, redimensioned);
+        super(Conversion.class, parameters,
+              CoordinateSystem.class, sourceDimensions, false,
+              CoordinateSystem.class, targetDimensions, false, redimensioned);
     }
 
     /**
      * Creates an identity operation of the given number of dimensions.
      */
     GeographicRedimension(final int dimension, final GeodeticOperation[] redimensioned) {
-        super(dimension, dimension, builder().setCodeSpace(Citations.SIS, Constants.SIS)
-                .addName("Identity " + dimension + 'D').createGroup(), redimensioned);
-    }
-
-    /**
-     * Returns the interface implemented by all coordinate operations that extends this class.
-     *
-     * @return default to {@link Conversion}.
-     */
-    @Override
-    public final Class<Conversion> getOperationType() {
-        return Conversion.class;
+        this(builder().setCodeSpace(Citations.SIS, Constants.SIS).addName("Identity " + dimension + 'D').createGroup(),
+             dimension, dimension, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java
index 7fa6fc4..0ab4bd1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java
@@ -41,7 +41,7 @@
  * This provider creates transforms from geographic to geocentric coordinate reference systems.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see GeocentricToGeographic
  *
@@ -116,17 +116,9 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases.
      */
     private GeographicToGeocentric(int sourceDimensions, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, 3, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * Returns the operation type.
-     *
-     * @return {@code Conversion.class}.
-     */
-    @Override
-    public Class<Conversion> getOperationType() {
-        return Conversion.class;
+        super(Conversion.class, PARAMETERS,
+              EllipsoidalCS.class, sourceDimensions, true,
+              CartesianCS.class, 3, false, redimensioned);
     }
 
     /**
@@ -152,17 +144,6 @@
     }
 
     /**
-     * Notifies {@code DefaultMathTransformFactory} that Geographic/geocentric conversions
-     * require values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
-     *
-     * @return 1, meaning that the operation requires a source ellipsoid.
-     */
-    @Override
-    public int getEllipsoidsMask() {
-        return 1;
-    }
-
-    /**
      * Specifies that the inverse of this operation is a different kind of operation.
      *
      * @return {@code null}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToTopocentric.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToTopocentric.java
new file mode 100644
index 0000000..368cd18
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToTopocentric.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing.provider;
+
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.operation.Conversion;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.measure.Units;
+import org.apache.sis.parameter.Parameters;
+
+
+/**
+ * The provider for the <cite>"Geographic/topocentric conversions"</cite> (EPSG:9837).
+ * This operation is implemented using existing {@link MathTransform} implementations;
+ * there is no need for a class specifically for this transform.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public final class GeographicToTopocentric extends AbstractProvider {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -3829993731324133815L;
+
+    /**
+     * The operation parameter descriptor for the <cite>Longitude of topocentric origin</cite> parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> EPSG:    </td><td> Longitude of topocentric origin </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>Value domain: [-180.0 … 180.0]°</li>
+     * </ul>
+     */
+    static final ParameterDescriptor<Double> ORIGIN_X;
+
+    /**
+     * The operation parameter descriptor for the <cite>Latitude of topocentric origin</cite> parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> EPSG:    </td><td> Latitude of topocentric origin </td></tr>
+     * </table>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>Value domain: [-90.0 … 90.0]°</li>
+     * </ul>
+     */
+    static final ParameterDescriptor<Double> ORIGIN_Y;
+
+    /**
+     * The operation parameter descriptor for the <cite>Ellipsoidal height of topocentric origin</cite> parameter value.
+     *
+     * <!-- Generated by ParameterNameTableGenerator -->
+     * <table class="sis">
+     *   <caption>Parameter names</caption>
+     *   <tr><td> EPSG:    </td><td> Ellipsoidal height of topocentric origin </td></tr>
+     * </table>
+     */
+    static final ParameterDescriptor<Double> ORIGIN_Z;
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    private static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        final ParameterBuilder builder = builder();
+        ORIGIN_X = createLongitude(builder
+                .addIdentifier("8835")
+                .addName("Longitude of topocentric origin"));
+
+        ORIGIN_Y = createLatitude(builder
+                .addIdentifier("8834")
+                .addName("Latitude of topocentric origin"), true);
+
+        ORIGIN_Z = builder
+                .addIdentifier("8836")
+                .addName("Ellipsoidal height of topocentric origin")
+                .create(0, Units.METRE);
+
+        PARAMETERS = builder
+                .addIdentifier("9837")
+                .addName("Geographic/topocentric conversions")
+                .createGroupForMapProjection(ORIGIN_Y, ORIGIN_X, ORIGIN_Z);
+                // Not really a map projection, but we leverage the same axis parameters.
+    }
+
+    /**
+     * Constructs a provider for the 3-dimensional case.
+     * While this operation method looks like a map projection because it has a
+     * {@link org.opengis.referencing.crs.GeographicCRS} source and
+     * {@link org.opengis.referencing.cs.CartesianCS} destination,
+     * it is classified in the "Coordinate Operations other than Map Projections" category in EPSG guidance note.
+     */
+    public GeographicToTopocentric() {
+        super(Conversion.class, PARAMETERS,
+              EllipsoidalCS.class, 3, true,
+              CartesianCS.class, 3, false);
+    }
+
+    /**
+     * Creates a transform from the specified group of parameter values.
+     * The unit of measurement of input coordinates will be the units of the ellipsoid axes.
+     *
+     * @param  factory  the factory to use for creating the transform.
+     * @param  values   the parameter values that define the transform to create.
+     * @return the conversion from geographic to topocentric coordinates.
+     * @throws FactoryException if an error occurred while creating a transform.
+     */
+    @Override
+    public MathTransform createMathTransform(final MathTransformFactory factory, final ParameterValueGroup values)
+            throws FactoryException
+    {
+        try {
+            return GeocentricToTopocentric.create(factory, Parameters.castOrWrap(values), true);
+        } catch (TransformException e) {
+            throw new FactoryException(e);
+        }
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Interpolation1D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Interpolation1D.java
index fe0942b..09e7603 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Interpolation1D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Interpolation1D.java
@@ -20,6 +20,7 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -34,7 +35,7 @@
  * The provider for interpolation of one-dimensional coordinates.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -73,17 +74,9 @@
      * Constructs a provider for the 1-dimensional case.
      */
     public Interpolation1D() {
-        super(1, 1, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type.
-     *
-     * @return {@code Conversion.class}.
-     */
-    @Override
-    public Class<Conversion> getOperationType() {
-        return Conversion.class;
+        super(Conversion.class, PARAMETERS,
+              CoordinateSystem.class, 1, false,
+              CoordinateSystem.class, 1, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java
index 18f34ba..17707f0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java
@@ -30,7 +30,7 @@
  * The provider for "<cite>Lambert Azimuthal Equal Area</cite>" projection (EPSG:9820).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/lambert_azimuthal_equal_area.html">GeoTIFF parameters for Lambert Azimuthal Equal Area</a>
  *
@@ -152,7 +152,7 @@
      * Constructs a new provider.
      */
     public LambertAzimuthalEqualArea() {
-        super(PARAMETERS);
+        this(PARAMETERS);
     }
 
     /**
@@ -161,17 +161,7 @@
      * @param  parameters  the set of parameters (never {@code null}).
      */
     LambertAzimuthalEqualArea(final ParameterDescriptorGroup parameters) {
-        super(parameters);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code PlanarProjection.class}
-     */
-    @Override
-    public Class<PlanarProjection> getOperationType() {
-        return PlanarProjection.class;
+        super(PlanarProjection.class, parameters);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java
index 1491426..6db14be 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualArea.java
@@ -30,7 +30,7 @@
  * The provider for <cite>"Lambert Cylindrical Equal Area"</cite> projection (EPSG:9835).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/cylindrical_equal_area.html">GeoTIFF parameters for Cylindrical Equal Area</a>
  *
@@ -164,17 +164,7 @@
      * Constructs a new provider.
      */
     public LambertCylindricalEqualArea() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code CylindricalProjection.class}
-     */
-    @Override
-    public final Class<CylindricalProjection> getOperationType() {
-        return CylindricalProjection.class;
+        super(CylindricalProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java
index 62d617f..2fa39b7 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertCylindricalEqualAreaSpherical.java
@@ -28,7 +28,7 @@
  * The provider for <cite>"Lambert Cylindrical Equal Area (Spherical)"</cite> projection (EPSG:9834).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -64,17 +64,7 @@
      * Constructs a new provider.
      */
     public LambertCylindricalEqualAreaSpherical() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code CylindricalProjection.class}
-     */
-    @Override
-    public final Class<CylindricalProjection> getOperationType() {
-        return CylindricalProjection.class;
+        super(CylindricalProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
index 89ecb62..ee3c81f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection.java
@@ -31,6 +31,8 @@
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -49,18 +51,18 @@
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.Debug;
 
 import static org.opengis.metadata.Identifier.AUTHORITY_KEY;
 
 
 /**
- * Base class for all map projection providers defined in this package. This base class defines some descriptors
- * for the most commonly used parameters. Subclasses will declare additional parameters and group them in a
+ * Base class for most two-dimensional map projection providers defined in this package.
+ * This base class defines some descriptors for the most commonly used parameters.
+ * Subclasses will declare additional parameters and group them in a
  * {@linkplain ParameterDescriptorGroup descriptor group} named {@code PARAMETERS}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -119,7 +121,7 @@
 
     /**
      * The ellipsoid eccentricity, computed from the semi-major and semi-minor axis lengths.
-     * This a SIS-specific parameter used mostly for debugging purpose.
+     * This a SIS-specific parameter.
      *
      * <!-- Generated by ParameterNameTableGenerator -->
      * <table class="sis">
@@ -132,7 +134,6 @@
      *   <li>No default value</li>
      * </ul>
      */
-    @Debug
     public static final DefaultParameterDescriptor<Double> ECCENTRICITY;
     static {
         final MeasurementRange<Double> valueDomain = MeasurementRange.createGreaterThan(0, Units.METRE);
@@ -185,20 +186,15 @@
      * Constructs a math transform provider from a set of parameters. The provider
      * {@linkplain #getIdentifiers() identifiers} will be the same than the parameter ones.
      *
-     * @param  parameters  the set of parameters (never {@code null}).
+     * @param  operationType  interface of the {@code CoordinateOperation} instances that use this projection.
+     * @param  parameters     the set of parameters (never {@code null}).
      */
-    protected MapProjection(final ParameterDescriptorGroup parameters) {
-        super(2, 2, parameters);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code Projection.class} or a sub-type.
-     */
-    @Override
-    public Class<? extends Projection> getOperationType() {
-        return Projection.class;
+    protected MapProjection(final Class<? extends Projection> operationType,
+                            final ParameterDescriptorGroup parameters)
+    {
+        super(operationType, parameters,
+              EllipsoidalCS.class, 2, true,
+              CartesianCS.class,   2, false);
     }
 
     /**
@@ -290,17 +286,6 @@
      */
     protected abstract NormalizedProjection createProjection(final Parameters parameters) throws ParameterNotFoundException;
 
-    /**
-     * Notifies {@code DefaultMathTransformFactory} that map projections require
-     * values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
-     *
-     * @return 1, meaning that the operation requires a source ellipsoid.
-     */
-    @Override
-    public final int getEllipsoidsMask() {
-        return 1;
-    }
-
 
 
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection3D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection3D.java
index 0796416..890e57c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection3D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MapProjection3D.java
@@ -21,7 +21,6 @@
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.OperationMethod;
-import org.opengis.referencing.operation.Projection;
 import org.opengis.util.FactoryException;
 
 
@@ -30,7 +29,7 @@
  * with only the ellipsoidal height which pass through.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  *
@@ -56,7 +55,9 @@
      * Constructs a three-dimensional map projection for the given two-dimensional projection.
      */
     MapProjection3D(final MapProjection proj) {
-        super(3, 3, proj.getParameters());
+        super(proj.getOperationType(), proj.getParameters(),
+              proj.sourceCSType, 3, proj.sourceOnEllipsoid,
+              proj.targetCSType, 3, proj.targetOnEllipsoid);
         redimensioned = proj;
     }
 
@@ -76,25 +77,6 @@
     }
 
     /**
-     * Returns the operation type for this map projection.
-     */
-    @Override
-    public Class<? extends Projection> getOperationType() {
-        return redimensioned.getOperationType();
-    }
-
-    /**
-     * Notifies {@code DefaultMathTransformFactory} that map projections require
-     * values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
-     *
-     * @return 1, meaning that the operation requires a source ellipsoid.
-     */
-    @Override
-    public int getEllipsoidsMask() {
-        return redimensioned.getEllipsoidsMask();
-    }
-
-    /**
      * Creates a three-dimensional map projections for the given parameters.
      * The ellipsoidal height is assumed to be in the third dimension.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ModifiedAzimuthalEquidistant.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ModifiedAzimuthalEquidistant.java
index cf3c3c9..25a6257 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ModifiedAzimuthalEquidistant.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ModifiedAzimuthalEquidistant.java
@@ -38,7 +38,7 @@
  * approximation.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/azimuthal_equidistant.html">GeoTIFF parameters for Azimuthal Equidistant</a>
  *
@@ -155,15 +155,7 @@
      * Constructs a new provider.
      */
     public ModifiedAzimuthalEquidistant() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     */
-    @Override
-    public Class<PlanarProjection> getOperationType() {
-        return PlanarProjection.class;
+        super(PlanarProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mollweide.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mollweide.java
index 57446c0..bca51a1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mollweide.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Mollweide.java
@@ -19,6 +19,7 @@
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.Projection;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.operation.projection.NormalizedProjection;
@@ -30,7 +31,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see <a href="https://mathworld.wolfram.com/MollweideProjection.html">Mathworld formulas</a>
  * @see <a href="http://geotiff.maptools.org/proj_list/mollweide.html">GeoTIFF parameters for Mollweide</a>
@@ -108,7 +109,7 @@
      * Constructs a new provider.
      */
     public Mollweide() {
-        super(PARAMETERS);
+        super(Projection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
index 6ae4795..3e6c8d2 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
@@ -59,7 +59,7 @@
  *
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -170,15 +170,7 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases.
      */
     private Molodensky(int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, targetDimensions, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * While Molodensky method is an approximation of geocentric translation, this is not exactly that.
-     */
-    @Override
-    int getType() {
-        return OTHER;
+        super(Type.MOLODENSKY, PARAMETERS, sourceDimensions, targetDimensions, redimensioned);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
index 94afa26..f648888 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
@@ -32,6 +32,7 @@
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Transformation;
@@ -55,7 +56,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rueben Schulz (UBC)
- * @version 0.8
+ * @version 1.3
  *
  * @see <a href="http://www.ngs.noaa.gov/cgi-bin/nadcon.prl">NADCON on-line computation</a>
  *
@@ -125,17 +126,9 @@
      * Creates a new provider.
      */
     public NADCON() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the base interface of the {@code CoordinateOperation} instances that use this method.
-     *
-     * @return fixed to {@link Transformation}.
-     */
-    @Override
-    public Class<Transformation> getOperationType() {
-        return Transformation.class;
+        super(Transformation.class, PARAMETERS,
+              EllipsoidalCS.class, 2, false,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv1.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv1.java
index 8d2c756..4865d99 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv1.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv1.java
@@ -21,6 +21,7 @@
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Transformation;
@@ -32,7 +33,7 @@
  * This transform requires data that are not bundled by default with Apache SIS.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -59,17 +60,9 @@
      * Creates a new provider.
      */
     public NTv1() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the base interface of the {@code CoordinateOperation} instances that use this method.
-     *
-     * @return fixed to {@link Transformation}.
-     */
-    @Override
-    public Class<Transformation> getOperationType() {
-        return Transformation.class;
+        super(Transformation.class, PARAMETERS,
+              EllipsoidalCS.class, 2, false,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
index 0651f60..88a2b1b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
@@ -39,6 +39,7 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Transformation;
@@ -62,7 +63,7 @@
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -109,17 +110,9 @@
      * Creates a new provider.
      */
     public NTv2() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the base interface of the {@code CoordinateOperation} instances that use this method.
-     *
-     * @return fixed to {@link Transformation}.
-     */
-    @Override
-    public Class<Transformation> getOperationType() {
-        return Transformation.class;
+        super(Transformation.class, PARAMETERS,
+              EllipsoidalCS.class, 2, false,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java
index 1298e9d..97fe742 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NorthPoleRotation.java
@@ -21,6 +21,7 @@
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -39,7 +40,7 @@
  * The 0° rotated meridian is defined as the meridian that runs through both the geographical and the rotated North pole.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see SouthPoleRotation
  * @see <a href="https://cfconventions.org/cf-conventions/cf-conventions.html#_rotated_pole">Rotated pole in CF-conventions</a>
@@ -138,17 +139,9 @@
      * Constructs a new provider.
      */
     public NorthPoleRotation() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code Conversion.class} or a sub-type.
-     */
-    @Override
-    public Class<? extends Conversion> getOperationType() {
-        return Conversion.class;
+        super(Conversion.class, PARAMETERS,
+              EllipsoidalCS.class, 2, false,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Orthographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Orthographic.java
index 4bd68a8..06bf4c4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Orthographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Orthographic.java
@@ -33,11 +33,11 @@
  *
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/orthographic.html">GeoTIFF parameters for Orthographic</a>
  *
- * @version 1.1
- * @since   1.1
+ * @since 1.1
  * @module
  */
 @XmlTransient
@@ -173,15 +173,7 @@
      * Constructs a new provider.
      */
     public Orthographic() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     */
-    @Override
-    public Class<PlanarProjection> getOperationType() {
-        return PlanarProjection.class;
+        super(PlanarProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Polyconic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Polyconic.java
index 069d956..ff15228 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Polyconic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Polyconic.java
@@ -30,7 +30,7 @@
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see <a href="http://geotiff.maptools.org/proj_list/polyconic.html">GeoTIFF parameters for Polyconic</a>
  *
@@ -136,17 +136,7 @@
      * Constructs a new provider.
      */
     public Polyconic() {
-        super(PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code ConicProjection.class}
-     */
-    @Override
-    public Class<ConicProjection> getOperationType() {
-        return ConicProjection.class;
+        super(ConicProjection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param.java
index 203f706..42c54f6 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param.java
@@ -56,14 +56,6 @@
      * Constructs the provider.
      */
     public PositionVector7Param() {
-        super(3, 3, PARAMETERS, null);
-    }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return SEVEN_PARAM;
+        super(Type.SEVEN_PARAM, PARAMETERS, 3, 3, null);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param2D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param2D.java
index 9ef0cd3..e0f2218 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param2D.java
@@ -25,7 +25,7 @@
  * The provider for <cite>"Position Vector transformation (geog2D domain)"</cite> (EPSG:9606).
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -68,7 +68,7 @@
      * Constructs a provider that can be resized.
      */
     PositionVector7Param2D(GeodeticOperation[] redimensioned) {
-        super(2, 2, PARAMETERS, redimensioned);
+        super(Type.SEVEN_PARAM, PARAMETERS, 2, 2, redimensioned);
     }
 
     /**
@@ -78,12 +78,4 @@
     Class<PositionVector7Param3D> variant3D() {
         return PositionVector7Param3D.class;
     }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return SEVEN_PARAM;
-    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param3D.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param3D.java
index f788490..707cc0c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param3D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PositionVector7Param3D.java
@@ -24,7 +24,7 @@
  * The provider for <cite>"Position Vector transformation (geog3D domain)"</cite> (EPSG:1037).
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -70,14 +70,6 @@
      * @param redimensioned     providers for all combinations between 2D and 3D cases, or {@code null}.
      */
     private PositionVector7Param3D(int sourceDimensions, int targetDimensions, GeodeticOperation[] redimensioned) {
-        super(sourceDimensions, targetDimensions, PARAMETERS, redimensioned);
-    }
-
-    /**
-     * Returns the type of this operation.
-     */
-    @Override
-    int getType() {
-        return SEVEN_PARAM;
+        super(Type.SEVEN_PARAM, PARAMETERS, sourceDimensions, targetDimensions, redimensioned);
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoPlateCarree.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoPlateCarree.java
index eb0a09e..b8a0513 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoPlateCarree.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoPlateCarree.java
@@ -19,6 +19,7 @@
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -35,7 +36,7 @@
  * axis units are degrees.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see Equirectangular
  *
@@ -60,18 +61,9 @@
      * Constructs a new provider.
      */
     public PseudoPlateCarree() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type. We do not declare that operation method as a
-     * {@link org.opengis.referencing.operation.Projection} because axis units are degrees.
-     *
-     * @return interface implemented by all coordinate operations that use this method.
-     */
-    @Override
-    public final Class<Conversion> getOperationType() {
-        return Conversion.class;
+        super(Conversion.class, PARAMETERS,
+              EllipsoidalCS.class, 2, false,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoSinusoidal.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoSinusoidal.java
new file mode 100644
index 0000000..bcf34ef
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/PseudoSinusoidal.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.referencing.provider;
+
+import javax.xml.bind.annotation.XmlTransient;
+import org.opengis.parameter.ParameterDescriptorGroup;
+
+
+/**
+ * The provider for <cite>"Pseudo sinusoidal equal-area"</cite> projection.
+ * This is similar to Pseudo-Mercator: uses spherical formulas but apply the result on an ellipsoid.
+ * This is sometime used with remote sensing data.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+@XmlTransient
+public final class PseudoSinusoidal extends Sinusoidal {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 6523477856049963388L;
+
+    /**
+     * Name of this projection.
+     */
+    public static final String NAME = "Pseudo sinusoidal";
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    private static ParameterDescriptorGroup parameters() {
+        return builder().addName(NAME)
+                .createGroupForMapProjection(AbstractMercator.toArray(PARAMETERS.descriptors(), 0));
+    }
+
+    /**
+     * Constructs a new provider.
+     */
+    public PseudoSinusoidal() {
+        super(parameters());
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SatelliteTracking.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SatelliteTracking.java
index 1d6a946..6d9a5f3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SatelliteTracking.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SatelliteTracking.java
@@ -20,6 +20,7 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.operation.Projection;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
@@ -37,7 +38,7 @@
  *
  * @author  Matthieu Bastianelli (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -177,7 +178,7 @@
      * Constructs a new provider.
      */
     public SatelliteTracking() {
-        super(PARAMETERS);
+        super(Projection.class, PARAMETERS);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Sinusoidal.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Sinusoidal.java
index 3c3dfff..fe7b7d8 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Sinusoidal.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Sinusoidal.java
@@ -20,6 +20,7 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.operation.Projection;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.parameter.Parameters;
@@ -32,7 +33,7 @@
  * This projection method has no associated EPSG code.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see <a href="https://en.wikipedia.org/wiki/Sinusoidal_projection">Sinusoidal projection on Wikipedia</a>
  * @see <a href="http://geotiff.maptools.org/proj_list/sinusoidal.html">GeoTIFF parameters for Sinusoidal</a>
@@ -41,18 +42,13 @@
  * @module
  */
 @XmlTransient
-public final class Sinusoidal extends MapProjection {
+public class Sinusoidal extends MapProjection {
     /**
      * For cross-version compatibility.
      */
     private static final long serialVersionUID = -3236247448683326299L;
 
     /**
-     * Name of this projection.
-     */
-    public static final String NAME = "Sinusoidal";
-
-    /**
      * The operation parameter descriptor for the <cite>Longitude of projection center</cite> (λ₀) parameter value.
      * Valid values range is [-180 … 180]° and default value is 0°.
      *
@@ -97,10 +93,10 @@
     /**
      * The group of all parameters expected by this coordinate operation.
      */
-    private static final ParameterDescriptorGroup PARAMETERS;
+    static final ParameterDescriptorGroup PARAMETERS;
     static {
         PARAMETERS = builder().setCodeSpace(Citations.OGC, Constants.OGC)
-                .addName      (NAME)
+                .addName      ("Sinusoidal")
                 .addName      ("Sanson-Flamsteed")
                 .addName      (Citations.GEOTIFF,  "CT_Sinusoidal")
                 .addIdentifier(Citations.GEOTIFF,  "24")
@@ -112,7 +108,16 @@
      * Constructs a new provider.
      */
     public Sinusoidal() {
-        super(PARAMETERS);
+        this(PARAMETERS);
+    }
+
+    /**
+     * Constructs a math transform provider from a set of parameters.
+     *
+     * @param  parameters  the set of parameters (never {@code null}).
+     */
+    Sinusoidal(final ParameterDescriptorGroup parameters) {
+        super(Projection.class, parameters);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java
index 72452c7..2aa80b0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/SouthPoleRotation.java
@@ -21,6 +21,7 @@
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -43,7 +44,7 @@
  * in UCAR netCDF library version 5.5.2.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see NorthPoleRotation
  *
@@ -149,17 +150,9 @@
      * Constructs a new provider.
      */
     public SouthPoleRotation() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this map projection.
-     *
-     * @return {@code Conversion.class} or a sub-type.
-     */
-    @Override
-    public Class<? extends Conversion> getOperationType() {
-        return Conversion.class;
+        super(Conversion.class, PARAMETERS,
+              EllipsoidalCS.class, 2, false,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java
index 455f1ea..b36da8b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/VerticalOffset.java
@@ -20,6 +20,8 @@
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.VerticalCS;
+import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Matrix;
@@ -46,12 +48,12 @@
  * <cite>"Vertical Offset"</cite> parameter value needs to be reversed if the target coordinate system axis is down.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.7
  * @module
  */
 @XmlTransient
-public final class VerticalOffset extends GeographicOffsets {
+public final class VerticalOffset extends GeodeticOperation {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -62,14 +64,16 @@
      */
     private static final ParameterDescriptorGroup PARAMETERS;
     static {
-        PARAMETERS = builder().addIdentifier("9616").addName("Vertical Offset").createGroup(TZ);
+        PARAMETERS = builder().addIdentifier("9616").addName("Vertical Offset").createGroup(GeographicOffsets.TZ);
     }
 
     /**
      * Constructs a provider with default parameters.
      */
     public VerticalOffset() {
-        super(1, 1, PARAMETERS, null);
+        super(Transformation.class, PARAMETERS,
+              VerticalCS.class, 1, false,
+              VerticalCS.class, 1, false, null);
     }
 
     /**
@@ -86,7 +90,7 @@
             throws ParameterNotFoundException
     {
         final Parameters pv = Parameters.castOrWrap(values);
-        return MathTransforms.translation(pv.doubleValue(TZ));
+        return MathTransforms.translation(pv.doubleValue(GeographicOffsets.TZ));
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Wraparound.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Wraparound.java
index 568799d..ff6749a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Wraparound.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Wraparound.java
@@ -21,6 +21,7 @@
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -34,7 +35,7 @@
  * Provider for {@link WraparoundTransform} (SIS-specific operation).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -112,17 +113,9 @@
      * Constructs a new provider.
      */
     public Wraparound() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type.
-     *
-     * @return interface implemented by all coordinate operations that use this method.
-     */
-    @Override
-    public final Class<Conversion> getOperationType() {
-        return Conversion.class;
+        super(Conversion.class, PARAMETERS,
+              CoordinateSystem.class, 2, false,
+              CoordinateSystem.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ZonedTransverseMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ZonedTransverseMercator.java
index 250b7bb..2095efc 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ZonedTransverseMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/ZonedTransverseMercator.java
@@ -22,6 +22,7 @@
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.operation.Projection;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -36,7 +37,7 @@
  * The provider for <cite>"Transverse Mercator Zoned Grid System"</cite> projection (EPSG:9824).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -104,31 +105,13 @@
 
     /**
      * Constructs a new provider.
+     * We do not classify this operation as a cylindrical projection
+     * because of the discontinuities between zones.
      */
     public ZonedTransverseMercator() {
-        super(2, 2, PARAMETERS);
-    }
-
-    /**
-     * Returns the operation type for this projection. We do not classify this operation as a cylindrical projection
-     * for now because of the discontinuities between zones. But we may revisit that choice in any future SIS version.
-     *
-     * @return {@code Projection.class} or a sub-type.
-     */
-    @Override
-    public Class<? extends Projection> getOperationType() {
-        return Projection.class;
-    }
-
-    /**
-     * Notifies {@code DefaultMathTransformFactory} that this projection requires
-     * values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
-     *
-     * @return 1, meaning that the operation requires a source ellipsoid.
-     */
-    @Override
-    public final int getEllipsoidsMask() {
-        return 1;
+        super(Projection.class, PARAMETERS,
+              EllipsoidalCS.class, 2, true,
+              EllipsoidalCS.class, 2, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
index 466e271..8c4f94d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/package-info.java
@@ -22,7 +22,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Matthieu Bastianelli (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see org.apache.sis.referencing.operation.transform.MathTransformProvider
  *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
index e6be679..052bf08 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -74,10 +74,12 @@
 import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent;
 import org.apache.sis.internal.metadata.AxisNames;
 import org.apache.sis.internal.metadata.TransformationAccuracy;
+import org.apache.sis.internal.referencing.provider.AbstractProvider;
 import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
 import org.apache.sis.internal.referencing.EllipsoidalHeightCombiner;
 import org.apache.sis.internal.referencing.VerticalDatumTypes;
 import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.referencing.WKTKeywords;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.Numerics;
@@ -98,7 +100,7 @@
  * @author  Rémi Eve (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -463,15 +465,15 @@
                     identifiers[n] = id;
                 }
                 properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifiers);
-                // REMINDER: values associated to IDENTIFIERS_KEY shall be recognized by 'toIdentifier(Object)'.
+                // REMINDER: values associated to IDENTIFIERS_KEY shall be recognized by `toIdentifier(Object)`.
             }
         }
         /*
          * Other metadata (SCOPE, AREA, etc.).  ISO 19162 said that at most one of each type shall be present,
          * but our parser accepts an arbitrary amount of some kinds of metadata. They can be recognized by the
-         * 'while' loop.
+         * `while` loop.
          *
-         * Most WKT do not contain any of those metadata, so we perform an 'isEmpty()' check as an optimization
+         * Most WKT do not contain any of those metadata, so we perform an `isEmpty()` check as an optimization
          * for those common cases.
          */
         if (!parent.isEmpty()) {
@@ -616,7 +618,7 @@
         /*
          * Consider the following element: UNIT[“kilometre”, 1000, ID[“EPSG”, “9036”]]
          *
-         *  - if the authority code (“9036”) refers to a unit incompatible with 'baseUnit' (“metre”), log a warning.
+         *  - if the authority code (“9036”) refers to a unit incompatible with `baseUnit` (“metre”), log a warning.
          *  - otherwise: 1) unconditionally replace the parsed unit (“km”) by the unit referenced by the authority code.
          *               2) if the new unit is not equivalent to the old one (i.e. different scale factor), log a warning.
          */
@@ -778,14 +780,14 @@
                  * two-dimensional Projected or three-dimensional Geocentric CRS.
                  */
                 case WKTKeywords.Cartesian: {
-                    if (!(datum instanceof GeodeticDatum)) {
+                    if (datum != null && !(datum instanceof GeodeticDatum)) {
                         throw parent.missingComponent(WKTKeywords.Axis);
                     }
                     if (defaultUnit == null) {
                         throw parent.missingComponent(WKTKeywords.LengthUnit);
                     }
                     if (is3D) {  // If dimension can not be 2, then CRS can not be Projected.
-                        return Legacy.standard(defaultUnit.asType(Length.class));
+                        return Legacy.standard(defaultUnit);
                     }
                     nx = AxisNames.EASTING;  x = "E";
                     ny = AxisNames.NORTHING; y = "N";
@@ -893,7 +895,7 @@
          * Example: "Compound CS: East (km), North (km), Up (m)."
          */
         final String name;
-        { // For keeping the 'buffer' variable local to this block.
+        {   // For keeping the `buffer` variable local to this block.
             final StringBuilder buffer = new StringBuilder();
             if (type != null && !type.isEmpty()) {
                 final int c = type.codePointAt(0);
@@ -1074,7 +1076,7 @@
         /*
          * At this point we are done and ready to create the CoordinateSystemAxis. But there is one last element
          * specified by ISO 19162 but not in Apache SIS representation of axis: ORDER[n], which specify the axis
-         * ordering. If present we will store that value for processing by the 'parseCoordinateSystem(…)' method.
+         * ordering. If present we will store that value for processing by the `parseCoordinateSystem(…)` method.
          */
         final Element order = element.pullElement(OPTIONAL, WKTKeywords.Order);
         Integer n = null;
@@ -1116,9 +1118,9 @@
             if (n2 != null) {
                 return n1 - n2;
             }
-            return -1;                      // Axis 1 before Axis 2 since the latter has no 'ORDER' element.
+            return -1;                      // Axis 1 before Axis 2 since the latter has no `ORDER` element.
         } else if (n2 != null) {
-            return +1;                      // Axis 2 before Axis 1 since the latter has no 'ORDER' element.
+            return +1;                      // Axis 2 before Axis 1 since the latter has no `ORDER` element.
         }
         return 0;
     }
@@ -1243,16 +1245,28 @@
     }
 
     /**
-     * Returns the number of source dimensions of the given operation method, or 2 if unspecified.
+     * Parses a {@code "GeodeticCRS"} (WKT 2) element where the number of dimensions and coordinate system type
+     * are derived from the operation method. This is used for parsing the base CRS component of derived CRS.
+     *
+     * @param  mode       {@link #OPTIONAL} or {@link #MANDATORY}.
+     * @param  parent     the parent element.
+     * @param  method     the operation method, or {@code null} if unknown.
+     * @throws ParseException if the {@code "GeodeticCRS"} element can not be parsed.
      */
-    private static int getSourceDimensions(final OperationMethod method) {
+    private SingleCRS parseBaseCRS(final int mode, final Element parent, final OperationMethod method)
+            throws ParseException
+    {
+        int dimension = 2;
+        String csType = WKTKeywords.ellipsoidal;
         if (method != null) {
-            final Integer dimension = method.getSourceDimensions();
-            if (dimension != null) {
-                return dimension;
+            final Integer d = method.getSourceDimensions();
+            if (d != null) dimension = d;
+            if (method instanceof AbstractProvider) {
+                csType = WKTUtilities.toType(CoordinateSystem.class, ((AbstractProvider) method).sourceCSType);
+                if (csType == null) csType = WKTKeywords.ellipsoidal;
             }
         }
-        return 2;
+        return parseGeodeticCRS(mode, parent, dimension, csType);
     }
 
     /**
@@ -1271,7 +1285,7 @@
         /*
          * The map projection method may be specified by an EPSG identifier (or any other authority),
          * which is preferred to the method name since the latter is potentially ambiguous. However not
-         * all CoordinateOperationFactory may accept identifier as an argument to 'getOperationMethod'.
+         * all CoordinateOperationFactory may accept identifier as an argument to `getOperationMethod(…)`.
          * So if an identifier is present, we will try to use it but fallback on the name if we can
          * not use the identifier.
          */
@@ -1626,7 +1640,7 @@
                  */
                 baseCRS = parseEngineeringCRS(OPTIONAL, element, true);
                 if (baseCRS == null) {
-                    baseCRS = parseGeodeticCRS(OPTIONAL, element, getSourceDimensions(fromBase.getMethod()), WKTKeywords.ellipsoidal);
+                    baseCRS = parseBaseCRS(OPTIONAL, element, fromBase.getMethod());
                     if (baseCRS == null) {
                         baseCRS = parseProjectedCRS(MANDATORY, element, true);
                     }
@@ -1746,15 +1760,16 @@
                     angularUnit = csUnit.asType(Angle.class);
                 } else {
                     angularUnit = Units.DEGREE;
-                    if (csUnit == null) {
+                    if (csUnit == null && csType != null) {
                         /*
                          * A UNIT[…] is mandatory either in the CoordinateSystem as a whole (csUnit != null),
                          * or inside each AXIS[…] component (csUnit == null). An exception to this rule is when
                          * parsing a BaseGeodCRS inside a ProjectedCRS or DerivedCRS, in which case axes are omitted.
-                         * We recognize those cases by a non-null 'csType' given in argument to this method.
+                         * We recognize those cases by a non-null `csType` given in argument to this method.
                          */
-                        if (WKTKeywords.ellipsoidal.equals(csType)) {
-                            csUnit = Units.DEGREE;                        // For BaseGeodCRS in ProjectedCRS.
+                        switch (csType) {
+                            case WKTKeywords.ellipsoidal: csUnit = Units.DEGREE; break;     // For BaseGeodCRS in ProjectedCRS.
+                            case WKTKeywords.Cartesian:   csUnit = Units.METRE;  break;
                         }
                     }
                 }
@@ -1803,11 +1818,11 @@
              */
             fromBase = parseDerivingConversion(OPTIONAL, element, WKTKeywords.DerivingConversion, csUnit, angularUnit);
             if (fromBase != null) {
-                baseCRS = parseGeodeticCRS(MANDATORY, element, getSourceDimensions(fromBase.getMethod()), WKTKeywords.ellipsoidal);
+                baseCRS = parseBaseCRS(MANDATORY, element, fromBase.getMethod());
             }
         }
         /*
-         * At this point, we have either a non-null 'datum' or non-null 'baseCRS' + 'fromBase'.
+         * At this point, we have either a non-null `datum` or non-null `baseCRS` + `fromBase`.
          * The coordinate system is parsed in the same way for both cases, but the CRS is created differently.
          */
         final CRSFactory crsFactory = factories.getCRSFactory();
@@ -1824,7 +1839,7 @@
              *     "(snip) the prime meridian’s <irm longitude> value shall be given in the
              *     same angular units as those for the horizontal axes of the geographic CRS."
              *
-             * This is a little bit different than using the 'angularUnit' variable directly,
+             * This is a little bit different than using the `angularUnit` variable directly,
              * since the WKT could have overwritten the unit directly in the AXIS[…] element.
              * So we re-fetch the angular unit. Normally, we will get the same value (unless
              * the previous value was null).
@@ -1920,7 +1935,7 @@
                     return crsFactory.createDerivedCRS(properties, baseCRS, fromBase, cs);
                 }
                 /*
-                 * The 'parseVerticalDatum(…)' method may have been unable to resolve the datum type.
+                 * The `parseVerticalDatum(…)` method may have been unable to resolve the datum type.
                  * But sometime the axis (which was not available when we created the datum) provides
                  * more information. Verify if we can have a better type now, and if so rebuild the datum.
                  */
@@ -2129,7 +2144,7 @@
                 isWKT1 ? null : WKTKeywords.Conversion, linearUnit, angularUnit);
         /*
          * Parse the coordinate system. The linear unit must be specified somewhere, either explicitly in each axis
-         * or for the whole CRS with the above 'csUnit' value. If 'csUnit' is null, then an exception will be thrown
+         * or for the whole CRS with the above `csUnit` value. If `csUnit` is null, then an exception will be thrown
          * with a message like "A LengthUnit component is missing in ProjectedCRS".
          *
          * However we make an exception if we are parsing a BaseProjCRS, since the coordinate system is unspecified
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Symbols.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Symbols.java
index 40e2a0e..0ae1e2a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Symbols.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Symbols.java
@@ -570,7 +570,7 @@
      *
      * <h4>Scientific notation</h4>
      * The {@link NumberFormat} created here does not use scientific notation. This is okay for many
-     * WKT formatting purpose since Earth ellipsoid axis lengths in metres are large enough for trigging
+     * WKT formatting purpose since Earth ellipsoid axis lengths in metres are large enough for triggering
      * scientific notation, while we want to express them as normal numbers with centimetre precision.
      * However this is problematic for small numbers like 1E-5. Callers may need to adjust the precision
      * depending on the kind of numbers (length or angle) to format.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
index a179791..30dcb36 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
@@ -625,7 +625,7 @@
 
         /**
          * Parses the current {@link #buffer} content as a WKT elements (possibly with children elements).
-         * This method does not build the full {@link IdentifiedObject}; this later part will be done only
+         * This method does not build the full {@link IdentifiedObject}; this latter part will be done only
          * when first needed.
          *
          * <p>If {@link #aliasKey} is non-null, the first WKT is taken as a {@linkplain WKTFormat#addFragment
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java
index 5d64aeb..3590268 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java
@@ -83,7 +83,7 @@
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Rémi Eve (IRD)
  * @author  Rueben Schulz (UBC)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html">WKT 2 specification</a>
  * @see <a href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html">Legacy WKT 1</a>
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 5801217..061a620 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
@@ -382,7 +382,7 @@
 
     /**
      * Invoked by JAXB for marshalling the {@link #minimumOccurs} value. Omit marshalling of this
-     * {@code gml:minimumOccurs} element if its value is equals to the default value, which is 1.
+     * {@code gml:minimumOccurs} element if its value is equal to the default value, which is 1.
      */
     @XmlElement(name = "minimumOccurs")
     @XmlSchemaType(name = "nonNegativeInteger")
@@ -393,7 +393,7 @@
 
     /**
      * Invoked by JAXB for marshalling the {@link #maximumOccurs} value. Omit marshalling of this
-     * {@code gml:maximumOccurs} element if its value is equals to the default value, which is 1.
+     * {@code gml:maximumOccurs} element if its value is equal to the default value, which is 1.
      *
      * <p>This property should not be marshalled in {@link DefaultParameterDescriptor} objects (the GML schema
      * does not allow that). It should be marshalled only for {@link DefaultParameterDescriptorGroup} objects.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java
index fc38819..6e96894 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterBuilder.java
@@ -420,10 +420,14 @@
      *     <td>Array of 1 or 2 elements mapped to {@code "standard_parallel_1"} and {@code "standard_parallel_2"}.</td></tr>
      * </table>
      *
-     * <div class="note"><b>Note:</b>
-     * When the {@code "earth_radius"} parameter is read, its value is the
-     * {@linkplain org.apache.sis.referencing.datum.DefaultEllipsoid#getAuthalicRadius() authalic radius}
-     * computed from the semi-major and semi-minor axis lengths.</div>
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>The {@code "standard_parallel"} parameter descriptor is added only if the {@code parameters} argument
+     *       contains {@code "standard_parallel_1"} and {@code "standard_parallel_2"} descriptors.</li>
+     *   <li>When the {@code "earth_radius"} parameter is read, its value is the
+     *       {@linkplain org.apache.sis.referencing.datum.DefaultEllipsoid#getAuthalicRadius() authalic radius}
+     *       computed from the semi-major and semi-minor axis lengths.</li>
+     * </ul>
      *
      * Map projection parameter groups always have a {@linkplain DefaultParameterDescriptorGroup#getMinimumOccurs()
      * minimum} and {@linkplain DefaultParameterDescriptorGroup#getMaximumOccurs() maximum occurrence} of 1,
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 27a5a4e..fc7ef71 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
@@ -22,6 +22,7 @@
 import java.io.Serializable;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.measure.Unit;
+import javax.measure.IncommensurableException;
 import org.opengis.util.MemberName;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
@@ -101,7 +102,7 @@
  * overriding one method has no impact on other methods.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.4
  * @module
  */
@@ -670,6 +671,40 @@
     }
 
     /**
+     * Returns the floating point value of the parameter identified by the given descriptor,
+     * converted to the given unit of measurement. See {@link #getValue(ParameterDescriptor)}
+     * for more information about how this method uses the given {@code parameter} argument.
+     *
+     * @param  parameter  the name or alias of the parameter to look for.
+     * @param  unit       the desired unit of measurement.
+     * @return the requested parameter value if it exists, or the <strong>non-null</strong>
+     *         {@linkplain DefaultParameterDescriptor#getDefaultValue() default value} otherwise.
+     * @throws ParameterNotFoundException if the given {@code parameter} name or alias is not legal for this group.
+     * @throws IllegalStateException if the value is not defined and there is no default value.
+     * @throws IllegalArgumentException if the specified unit is invalid for the parameter.
+     *
+     * @see DefaultParameterValue#doubleValue(Unit)
+     *
+     * @since 1.3
+     */
+    public double doubleValue(final ParameterDescriptor<? extends Number> parameter, final Unit<?> unit) throws ParameterNotFoundException {
+        ArgumentChecks.ensureNonNull("unit", unit);
+        final ParameterValue<?> value = getParameter(parameter);
+        if (value != null) {
+            return value.doubleValue(unit);
+        } else {
+            double d = defaultValue(parameter).doubleValue();
+            final Unit<?> source = parameter.getUnit();
+            if (source != null) try {
+                d = source.getConverterToAny(unit).convert(d);
+            } catch (IncommensurableException e) {
+                throw new IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleUnits_2, source, unit), e);
+            }
+            return d;
+        }
+    }
+
+    /**
      * Returns the floating point values of the parameter identified by the given descriptor.
      * See {@link #getValue(ParameterDescriptor)} for more information about how this method
      * uses the given {@code parameter} argument.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java
index 3695c53..761c47a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java
@@ -384,7 +384,7 @@
     }
 
     /**
-     * Verifies that the length of the given array is equals to the tensor rank.
+     * Verifies that the length of the given array is equal to the tensor rank.
      */
     private void verifyRank(final int[] indices) {
         if (indices.length != rank()) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java
index e6fc205..5f4fba9 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java
@@ -45,7 +45,7 @@
 
 
 /**
- * Base class of builders for various kind of {@link IdentifiedObject}. This class provides convenience methods
+ * Base class of builders for various kinds of {@link IdentifiedObject}. This class provides convenience methods
  * for filling the {@link #properties} map to be given to an {@link org.opengis.referencing.ObjectFactory}.
  * The main properties are:
  *
@@ -233,7 +233,7 @@
      * Verifies that {@code B} in {@code <B extends Builder<B>} is the expected class.
      * This method is for assertion purposes only.
      */
-    private static boolean verifyParameterizedType(final Class<?> expected) {
+    private static boolean verifyParameterizedType(Class<?> expected) {
         for (Class<?> c = expected; c != null; c = c.getSuperclass()) {
             Type type = c.getGenericSuperclass();
             if (type instanceof ParameterizedType) {
@@ -243,6 +243,8 @@
                     if (type == expected) return true;
                     throw new AssertionError(type);
                 }
+            } else {
+                expected = c.getSuperclass();
             }
         }
         return false;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 95e5755..a19ea82 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -1326,8 +1326,8 @@
      *       searches for a {@linkplain CompoundCRS#getComponents() component} where:
      *       <ul>
      *         <li>The {@linkplain org.apache.sis.referencing.cs.AbstractCS#getDimension() number of dimensions}
-     *             is equals to {@code upper - lower};</li>
-     *         <li>The sum of the number of dimensions of all previous CRS is equals to {@code lower}.</li>
+     *             is equal to {@code upper - lower};</li>
+     *         <li>The sum of the number of dimensions of all previous CRS is equal to {@code lower}.</li>
      *       </ul>
      *       If such component is found, then it is returned.</li>
      *   <li>Otherwise (i.e. no component match), this method returns {@code null}.</li>
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultDerivedCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
index 2661ea3..f8e57da 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultDerivedCRS.java
@@ -22,7 +22,6 @@
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-import org.opengis.parameter.GeneralParameterValue;
 import org.opengis.referencing.datum.Datum;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.VerticalDatum;
@@ -46,8 +45,8 @@
 import org.opengis.geometry.MismatchedDimensionException;
 import org.apache.sis.referencing.AbstractIdentifiedObject;
 import org.apache.sis.referencing.operation.DefaultConversion;
-import org.apache.sis.referencing.operation.DefaultOperationMethod;
 import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.internal.jaxb.referencing.SC_SingleCRS;
 import org.apache.sis.internal.jaxb.referencing.SC_DerivedCRSType;
 import org.apache.sis.internal.jaxb.referencing.CS_CoordinateSystem;
@@ -55,10 +54,8 @@
 import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.referencing.WKTKeywords;
 import org.apache.sis.io.wkt.Convention;
-import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.util.ComparisonMode;
-import org.apache.sis.util.Classes;
 
 // Branch-dependent imports
 import org.opengis.referencing.cs.ParametricCS;
@@ -97,7 +94,7 @@
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 0.7
+ * @version 1.3
  *
  * @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createDerivedCRS(String)
  *
@@ -567,18 +564,7 @@
             return WKTKeywords.Fitted_CS;
         } else {
             formatter.newLine();
-            formatter.append(new FormattableObject() {     // Format inside a "DefiningConversion" element.
-                @Override protected String formatTo(final Formatter formatter) {
-                    WKTUtilities.appendName(conversion, formatter, null);
-                    formatter.newLine();
-                    formatter.append(DefaultOperationMethod.castOrCopy(conversion.getMethod()));
-                    formatter.newLine();
-                    for (final GeneralParameterValue param : conversion.getParameterValues().values()) {
-                        WKTUtilities.append(param, formatter);
-                    }
-                    return WKTKeywords.DerivingConversion;
-                }
-            });
+            formatter.append(new ExplicitParameters(this, WKTKeywords.DerivingConversion));     // Format inside a "DefiningConversion" element.
             if (convention == Convention.INTERNAL || !isBaseCRS(formatter)) {
                 final CoordinateSystem cs = getCoordinateSystem();
                 formatCS(formatter, cs, ReferencingUtilities.getUnit(cs), isWKT1);
@@ -609,9 +595,7 @@
 
     /**
      * Returns the WKT 2 keyword for a {@code DerivedCRS} having the given base CRS and derived coordinate system.
-     * Note that an ambiguity exists if the given base CRS is a {@code GeodeticCRS}, as the result could be either
-     * {@code "GeodeticCRS"} or {@code "EngineeringCRS"}. The current implementation returns the former if the
-     * derived coordinate system is of the same kind than the base coordinate system.
+     * If the type can not be identifier, then this method returns {@code null}.
      */
     static String getType(final SingleCRS baseCRS, final CoordinateSystem derivedCS) {
         final Class<?> type;
@@ -626,14 +610,8 @@
         } else {
             return null;
         }
-        if (GeodeticCRS.class.isAssignableFrom(type)) {
-            if (Classes.implementSameInterfaces(derivedCS.getClass(),
-                    baseCRS.getCoordinateSystem().getClass(), CoordinateSystem.class))
-            {
-                return WKTKeywords.GeodeticCRS;
-            } else {
-                return WKTKeywords.EngineeringCRS;
-            }
+        if (GeodeticCRS.class.isAssignableFrom(type) && CoordinateSystems.isGeodetic(derivedCS)) {
+            return WKTKeywords.GeodeticCRS;
         } else if (VerticalCRS.class.isAssignableFrom(type) && derivedCS instanceof VerticalCS) {
             return WKTKeywords.VerticalCRS;
         } else if (TemporalCRS.class.isAssignableFrom(type) && derivedCS instanceof TimeCS) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultProjectedCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
index 04c86d3..42aaad4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/DefaultProjectedCRS.java
@@ -19,38 +19,25 @@
 import java.util.Map;
 import javax.measure.Unit;
 import javax.measure.quantity.Angle;
-import javax.measure.quantity.Length;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
-import org.opengis.parameter.ParameterValue;
-import org.opengis.parameter.GeneralParameterValue;
-import org.opengis.parameter.GeneralParameterDescriptor;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.cs.CartesianCS;
 import org.opengis.referencing.cs.CoordinateSystem;                 // For javadoc
-import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.Projection;
 import org.opengis.geometry.MismatchedDimensionException;
-import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.cs.AxesConvention;
-import org.apache.sis.referencing.operation.DefaultOperationMethod;
 import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.internal.referencing.WKTKeywords;
 import org.apache.sis.internal.referencing.WKTUtilities;
-import org.apache.sis.internal.util.Constants;
-import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.io.wkt.Convention;
-import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.util.ComparisonMode;
-import org.apache.sis.util.logging.Logging;
-
-import static java.util.logging.Logger.getLogger;
 import static org.apache.sis.internal.referencing.WKTUtilities.toFormattable;
 
 
@@ -404,7 +391,7 @@
         formatter.newLine();
         formatter.append(toFormattable(baseCRS));
         formatter.newLine();
-        final Parameters p = new Parameters(this);
+        final ExplicitParameters p = new ExplicitParameters(this, WKTKeywords.Conversion);
         final boolean isBaseCRS;
         if (isWKT1) {
             p.append(formatter);                        // Format outside of any "Conversion" element.
@@ -426,76 +413,6 @@
                 : formatter.shortOrLong(WKTKeywords.ProjCRS, WKTKeywords.ProjectedCRS);
     }
 
-    /**
-     * Temporary object for formatting the projection method and parameters inside a {@code Conversion} element.
-     */
-    private static final class Parameters extends FormattableObject {
-        /** The conversion which specify the operation method and parameters. */
-        private final Conversion conversion;
-
-        /** Semi-major and semi-minor axis lengths. */
-        private final Ellipsoid ellipsoid;
-
-        /** Creates a new temporary {@code Conversion} elements for the parameters of the given CRS. */
-        Parameters(final DefaultProjectedCRS crs) {
-            conversion = crs.getConversionFromBase();
-            ellipsoid = crs.getDatum().getEllipsoid();
-        }
-
-        /** Formats this {@code Conversion} element. */
-        @Override protected String formatTo(final Formatter formatter) {
-            WKTUtilities.appendName(conversion, formatter, null);
-            formatter.newLine();
-            append(formatter);
-            return WKTKeywords.Conversion;
-        }
-
-        /** Formats this {@code Conversion} element without the conversion name. */
-        void append(final Formatter formatter) {
-            final Unit<Length> axisUnit = ellipsoid.getAxisUnit();
-            formatter.append(DefaultOperationMethod.castOrCopy(conversion.getMethod()));
-            formatter.newLine();
-            for (final GeneralParameterValue param : conversion.getParameterValues().values()) {
-                final GeneralParameterDescriptor desc = param.getDescriptor();
-                String name;
-                if (IdentifiedObjects.isHeuristicMatchForName(desc, name = Constants.SEMI_MAJOR) ||
-                    IdentifiedObjects.isHeuristicMatchForName(desc, name = Constants.SEMI_MINOR))
-                {
-                    /*
-                     * Do not format semi-major and semi-minor axis length in most cases,  since those
-                     * information are provided in the ellipsoid.  An exception to this rule occurs if
-                     * the lengths are different from the ones declared in the datum.
-                     */
-                    if (param instanceof ParameterValue<?>) {
-                        final double value;
-                        try {
-                            value = ((ParameterValue<?>) param).doubleValue(axisUnit);
-                        } catch (IllegalStateException e) {
-                            /*
-                             * May happen if the 'conversionFromBase' parameter group does not provide values
-                             * for "semi_major" or "semi_minor" axis length. This should not happen with SIS
-                             * implementation, but may happen with user-defined map projection implementations.
-                             * Since the intent of this check was to skip those parameters anyway, it is okay
-                             * for the purpose of WKT formatting if there are no parameters for axis lengths.
-                             */
-                            Logging.recoverableException(getLogger(Loggers.WKT), DefaultProjectedCRS.class, "formatTo", e);
-                            continue;
-                        }
-                        if (Double.isNaN(value)) {
-                            continue;
-                        }
-                        final double expected = (name == Constants.SEMI_MINOR)   // using '==' is okay here.
-                                ? ellipsoid.getSemiMinorAxis() : ellipsoid.getSemiMajorAxis();
-                        if (value == expected) {
-                            continue;
-                        }
-                    }
-                }
-                WKTUtilities.append(param, formatter);
-            }
-        }
-    }
-
 
 
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/ExplicitParameters.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/ExplicitParameters.java
new file mode 100644
index 0000000..61e0d41
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/crs/ExplicitParameters.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.crs;
+
+import org.opengis.parameter.ParameterValue;
+import org.opengis.parameter.GeneralParameterValue;
+import org.opengis.parameter.GeneralParameterDescriptor;
+import org.opengis.referencing.datum.Datum;
+import org.opengis.referencing.datum.GeodeticDatum;
+import org.opengis.referencing.datum.Ellipsoid;
+import org.opengis.referencing.operation.Conversion;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.operation.DefaultOperationMethod;
+import org.apache.sis.internal.referencing.WKTKeywords;
+import org.apache.sis.internal.referencing.WKTUtilities;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.io.wkt.FormattableObject;
+import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.util.logging.Logging;
+
+import static java.util.logging.Logger.getLogger;
+
+
+/**
+ * Temporary object for formatting the projection method and parameters inside a {@code Conversion} element.
+ * This object formats only the explicit parameters. Implicit parameters derived from source ellipsoid are omitted.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.3
+ * @since   0.6
+ * @module
+ */
+final class ExplicitParameters extends FormattableObject {
+    /**
+     * The conversion which specify the operation method and parameters.
+     */
+    private final Conversion conversion;
+
+    /**
+     * Semi-major and semi-minor axis lengths, or {@code null} if the datum is not geodetic.
+     */
+    private final Ellipsoid ellipsoid;
+
+    /**
+     * The keyword to be returned by {@link #formatTo(Formatter)}.
+     * Should be {@link WKTKeywords#Conversion} or {@link WKTKeywords#DerivingConversion}.
+     */
+    private final String keyword;
+
+    /**
+     * Creates a new temporary {@code Conversion} elements for the parameters of the given CRS.
+     */
+    ExplicitParameters(final AbstractDerivedCRS<?> crs, final String keyword) {
+        conversion = crs.getConversionFromBase();
+        final Datum datum = crs.getDatum();
+        ellipsoid = (datum instanceof GeodeticDatum) ? ((GeodeticDatum) datum).getEllipsoid() : null;
+        this.keyword = keyword;
+    }
+
+    /**
+     * Formats this {@code Conversion} element.
+     */
+    @Override
+    protected String formatTo(final Formatter formatter) {
+        WKTUtilities.appendName(conversion, formatter, null);
+        formatter.newLine();
+        append(formatter);
+        return keyword;
+    }
+
+    /**
+     * Formats this {@code Conversion} element without the conversion name.
+     */
+    void append(final Formatter formatter) {
+        formatter.append(DefaultOperationMethod.castOrCopy(conversion.getMethod()));
+        formatter.newLine();
+        for (final GeneralParameterValue param : conversion.getParameterValues().values()) {
+            final GeneralParameterDescriptor desc = param.getDescriptor();
+            if (ellipsoid != null) {
+                String name;
+                if (IdentifiedObjects.isHeuristicMatchForName(desc, name = Constants.SEMI_MAJOR) ||
+                    IdentifiedObjects.isHeuristicMatchForName(desc, name = Constants.SEMI_MINOR))
+                {
+                    /*
+                     * Do not format semi-major and semi-minor axis length in most cases,  since those
+                     * information are provided in the ellipsoid.  An exception to this rule occurs if
+                     * the lengths are different from the ones declared in the datum.
+                     */
+                    if (param instanceof ParameterValue<?>) {
+                        final double value;
+                        try {
+                            value = ((ParameterValue<?>) param).doubleValue(ellipsoid.getAxisUnit());
+                        } catch (IllegalStateException e) {
+                            /*
+                             * May happen if the 'conversionFromBase' parameter group does not provide values
+                             * for "semi_major" or "semi_minor" axis length. This should not happen with SIS
+                             * implementation, but may happen with user-defined map projection implementations.
+                             * Since the intent of this check was to skip those parameters anyway, it is okay
+                             * for the purpose of WKT formatting if there are no parameters for axis lengths.
+                             */
+                            Logging.recoverableException(getLogger(Loggers.WKT), DefaultProjectedCRS.class, "formatTo", e);
+                            continue;
+                        }
+                        if (Double.isNaN(value)) {
+                            continue;
+                        }
+                        final double expected = (name == Constants.SEMI_MINOR)   // using '==' is okay here.
+                                ? ellipsoid.getSemiMinorAxis() : ellipsoid.getSemiMajorAxis();
+                        if (value == expected) {
+                            continue;
+                        }
+                    }
+                }
+            }
+            WKTUtilities.append(param, formatter);
+        }
+    }
+}
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 b945ed4..1102f49 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
@@ -28,6 +28,7 @@
 import org.opengis.referencing.cs.RangeMeaning;
 import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.SphericalCS;
 import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
@@ -72,6 +73,20 @@
     }
 
     /**
+     * Returns whether the given coordinate system can be associated to a {@link org.opengis.referencing.crs.GeodeticCRS}.
+     * This is true for instances of {@link EllipsoidalCS}, {@link CartesianCS} and {@link SphericalCS},
+     * and false for all other types of coordinate system.
+     *
+     * @param  cs  the coordinate system to test (can be {@code null}).
+     * @return whether the given coordinate system can be associated to a geodetic CRS.
+     *
+     * @since 1.3
+     */
+    public static boolean isGeodetic(final CoordinateSystem cs) {
+        return (cs instanceof EllipsoidalCS) || (cs instanceof CartesianCS) || (cs instanceof SphericalCS);
+    }
+
+    /**
      * Returns an axis direction code from the given direction name.
      * Names are case-insensitive. They may be:
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/BursaWolfParameters.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/BursaWolfParameters.java
index 71cfec9..004fcdf 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/BursaWolfParameters.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/BursaWolfParameters.java
@@ -344,7 +344,7 @@
     }
 
     /**
-     * Returns {@code true} if the {@linkplain #targetDatum target datum} is equals (at least on computation purpose)
+     * Returns {@code true} if the {@linkplain #targetDatum target datum} is equal (at least on computation purpose)
      * to the WGS84 datum. If the datum is unspecified, then this method returns {@code true} since WGS84 is the only
      * datum supported by the WKT 1 format, and is what users often mean.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
index cac325a..19f8396 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
@@ -570,7 +570,7 @@
      * <p>This method is invoked by the default {@link #find(IdentifiedObject)} method implementation.
      * The caller iterates through the returned codes, instantiate the objects and compare them with
      * the specified one in order to determine which codes are really applicable.
-     * The iteration stops as soon as a match is found (in other words, if more than one object is equals
+     * The iteration stops as soon as a match is found (in other words, if more than one object is equal
      * to the specified one, then the {@code find(…)} method selects the first one in iteration order).</p>
      *
      * <h4>Default implementation</h4>
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectSet.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
index f1365b5..de5a619 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
@@ -476,7 +476,7 @@
      * {@link FactoryException} (except the ones accepted as {@linkplain #isRecoverableFailure recoverable failures})
      * are thrown as if they were never wrapped into {@link BackingStoreException}.
      *
-     * @param  n  the number of object to resolve. If this number is equals or greater than {@link #size()}, then
+     * @param  n  the number of object to resolve. If this number is equal or greater than {@link #size()}, then
      *            this method ensures that all {@code IdentifiedObject} instances in this collection are created.
      * @throws FactoryException if an {@linkplain #createObject(String) object creation} failed.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 112e16d..01742ed 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -1489,6 +1489,12 @@
                                  * For a ProjectedCRS, the baseCRS is always geographic. So in theory we would not
                                  * need the `instanceof` check. However the EPSG dataset version 8.9 also uses the
                                  * "projected" type for CRS that are actually derived CRS. See EPSG:5820 and 5821.
+                                 *
+                                 * TODO: there is an ambiguity when the source CRS is geographic but the operation
+                                 * is nevertheless considered as not a map projection. It is the case of EPSG:5819.
+                                 * The problem is that the "COORD_REF_SYS_KIND" column still contains "Projected".
+                                 * We need to check if EPSG database 10+ has more specific information.
+                                 * See https://issues.apache.org/jira/browse/SIS-518
                                  */
                                 final Map<String, Object> properties = createProperties("Coordinate Reference System",
                                                                         name, epsg, area, scope, remarks, deprecated);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java
index e099d5e..7925511 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java
@@ -507,7 +507,7 @@
     }
 
     /**
-     * Replaces the text at the given position in the buffer if it is equals to the {@code expected} text.
+     * Replaces the text at the given position in the buffer if it is equal to the {@code expected} text.
      */
     private static boolean replaceIfEquals(final StringBuilder ansi, final int pos,
             final String expected, final String replacement)
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 71f2c44..d709744 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -524,8 +524,9 @@
          * set to Greenwich in EPSG dataset 8.9.  For safety, the SIS's DefaultGeodeticDatum class ensures that if the
          * prime meridians are not the same, then the target meridian must be Greenwich.
          */
-        final DefaultMathTransformFactory.Context context = ReferencingUtilities.createTransformContext(
-                sourceCRS, targetCRS, new MathTransformContext(sourceDatum, targetDatum));
+        final DefaultMathTransformFactory.Context context = new MathTransformContext(sourceDatum, targetDatum);
+        context.setSource(sourceCRS);
+        context.setTarget(targetCRS);
         /*
          * If both CRS use the same datum and the same prime meridian, then the coordinate operation is only axis
          * swapping, unit conversion or change of coordinate system type (Ellipsoidal ↔ Cartesian ↔ Spherical).
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index 5e82b9c..51ea821 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -982,7 +982,7 @@
             final MathTransformFactory mtFactory = factorySIS.getMathTransformFactory();
             if (mtFactory instanceof DefaultMathTransformFactory) {
                 MathTransform mt = ((DefaultMathTransformFactory) mtFactory).createParameterizedTransform(
-                        parameters, ReferencingUtilities.createTransformContext(sourceCRS, targetCRS, null));
+                        parameters, ReferencingUtilities.createTransformContext(sourceCRS, targetCRS));
                 return factorySIS.createSingleOperation(IdentifiedObjects.getProperties(operation),
                         sourceCRS, targetCRS, null, operation.getMethod(), mt);
             }
@@ -1104,7 +1104,7 @@
                         try {
                             mt = ((DefaultMathTransformFactory) mtFactory).createParameterizedTransform(
                                     ((SingleOperation) op).getParameterValues(),
-                                    ReferencingUtilities.createTransformContext(sourceCRS, targetCRS, null));
+                                    ReferencingUtilities.createTransformContext(sourceCRS, targetCRS));
                         } catch (InvalidGeodeticParameterException e) {
                             log(null, e);
                             break;
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 dcc444c..8cec2a3 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
@@ -256,10 +256,10 @@
                  */
                 final DefaultMathTransformFactory.Context context;
                 if (target instanceof GeneralDerivedCRS) {
-                    context = ReferencingUtilities.createTransformContext(source, null, null);
+                    context = ReferencingUtilities.createTransformContext(source, null);
                     context.setTarget(target.getCoordinateSystem());    // Using `target` would be unsafe here.
                 } else {
-                    context = ReferencingUtilities.createTransformContext(source, target, null);
+                    context = ReferencingUtilities.createTransformContext(source, target);
                 }
                 transform = ((DefaultMathTransformFactory) factory).createParameterizedTransform(parameters, context);
                 parameters = Parameters.unmodifiable(context.getCompletedParameters());
@@ -431,7 +431,7 @@
     }
 
     /**
-     * Ensures that the {@code actual} CRS uses a datum which is equals, ignoring metadata,
+     * Ensures that the {@code actual} CRS uses a datum which is equal, ignoring metadata,
      * to the datum of the {@code expected} CRS.
      *
      * @param  param     the parameter name, used only in case of error.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
index 3cd1f00..833d3f4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
@@ -557,7 +557,7 @@
          *   - Otherwise we have a datum change, which implies that we have a Transformation.
          *
          * In the case of Conversion, we can specialize one step more if the conversion is going from a geographic CRS
-         * to a projected CRS. It may seems that we should check if ProjectedCRS.getBaseCRS() is equals (ignoring meta
+         * to a projected CRS. It may seems that we should check if ProjectedCRS.getBaseCRS() is equal (ignoring meta
          * data) to source CRS. But we already checked the datum, which is the important part. The axis order and unit
          * could be different, which we want to allow.
          */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/MathTransformContext.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/MathTransformContext.java
index 1103ec4..defcd75 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/MathTransformContext.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/MathTransformContext.java
@@ -36,7 +36,7 @@
 /**
  * Information about the context in which a {@code MathTransform} is created.
  * This class performs the same normalization than the super-class (namely axis swapping and unit conversions),
- * with the addition of longitude rotation for supporting change of prime meridian.  This later change is not
+ * with the addition of longitude rotation for supporting change of prime meridian.  This latter change is not
  * applied by the super-class because prime meridian is part of geodetic datum, and the public math transform
  * factory know nothing about datum (on design, for separation of concerns).
  *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
index e9d8cae..19bc0d2 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
@@ -226,8 +226,10 @@
     }
 
     /**
-     * Converts the specified (θ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (θ,φ) coordinates and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The units of measurement are implementation-specific (see super-class javadoc).
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
index 2f331d4..039802a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AuthalicMercator.java
@@ -54,9 +54,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}
-     * (linear distance on a unit sphere). In addition, opportunistically computes the projection derivative
-     * if {@code derivate} is {@code true}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
+     * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AzimuthalEquidistant.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AzimuthalEquidistant.java
index ba8c3e8..500c481 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AzimuthalEquidistant.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AzimuthalEquidistant.java
@@ -157,7 +157,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate and stores the (<var>x</var>,<var>y</var>) result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
+     * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @param  srcPts    source point coordinate, as (<var>longitude</var>, <var>latitude</var>) in radians.
      * @param  srcOff    the offset of the single coordinate to be converted in the source array.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CassiniSoldner.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CassiniSoldner.java
index 10b3684..aba6518 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CassiniSoldner.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CassiniSoldner.java
@@ -233,8 +233,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @param  srcPts    the array containing the source point coordinate,
      *                   as (<var>longitude</var>, <var>latitude</var>) angles in <strong>radians</strong>.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
index 0b1eca0..2261170 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
@@ -230,9 +230,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}
-     * (linear distance on a unit sphere). In addition, opportunistically computes the projection derivative
-     * if {@code derivate} is {@code true}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
+     * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java
index 58cff5e..1ba7d95 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java
@@ -163,8 +163,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ)) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
index 0a6923d..459212d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
@@ -442,8 +442,10 @@
     }
 
     /**
-     * Converts the specified (θ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (θ,φ) coordinates and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The units of measurement are implementation-specific (see super-class javadoc).
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
index 90097ef..e0fed81 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -381,7 +381,7 @@
     @Override
     public MathTransform createMapProjection(final MathTransformFactory factory) throws FactoryException {
         NormalizedProjection kernel = this;
-subst:  if ((variant.spherical || eccentricity == 0) && getClass() == Mercator.class) {
+subst:  if (variant.spherical || (eccentricity == 0 && getClass() == Mercator.class)) {
             if (variant == Variant.AUXILIARY && eccentricity != 0) {
                 final int type = context.getValue(MercatorAuxiliarySphere.AUXILIARY_SPHERE_TYPE);
                 if (type == AuthalicMercator.TYPE) {
@@ -408,8 +408,9 @@
     }
 
     /**
-     * Converts the specified coordinate (implementation-specific units) and stores the result in {@code dstPts}.
+     * Projects the specified coordinates (implementation-specific units) and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ModifiedAzimuthalEquidistant.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ModifiedAzimuthalEquidistant.java
index 744aea5..0a50fe3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ModifiedAzimuthalEquidistant.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ModifiedAzimuthalEquidistant.java
@@ -155,7 +155,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate and stores the (<var>x</var>,<var>y</var>) result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinate (units in radians)
+     * and stores the (<var>x</var>,<var>y</var>) result in {@code dstPts}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
index f9c4d75..5285f2a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Mollweide.java
@@ -119,8 +119,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate and stores the (<var>x</var>,<var>y</var>) result in {@code dstPts}.
+     * Projects the specified (Λ,φ) coordinates and stores the (<var>x</var>,<var>y</var>) result in {@code dstPts}.
      * The units of measurement are implementation-specific (see super-class javadoc).
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
index 8b7e0c1..07946d5 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
@@ -690,9 +690,9 @@
      */
 
     /**
-     * Converts a single coordinate in {@code srcPts} at the given offset and stores the result
-     * in {@code dstPts} at the given offset. In addition, opportunistically computes the
-     * transform derivative if requested.
+     * Projects a single coordinate tuple in {@code srcPts} at the given offset
+     * and stores the result in {@code dstPts} at the given offset.
+     * In addition, opportunistically computes the transform derivative if requested.
      *
      * <h4>Normalization</h4>
      * The input coordinates are (<var>λ</var>,<var>φ</var>) (the variable names for <var>longitude</var> and
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueMercator.java
index 061f586..b3cc32a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueMercator.java
@@ -327,8 +327,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
index 615bad7..fcd61d7 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ObliqueStereographic.java
@@ -257,8 +257,10 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (Λ,φ) coordinates and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The units of measurement are implementation-specific (see super-class javadoc).
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
@@ -327,7 +329,7 @@
         final double cosφ = cos(φ);
         final double dχ_dφ = (1/cosφ - cosφ*eccentricitySquared/(1 - ℯsinφ*ℯsinφ)) * 2*n*sqrt(w) / (w + 1);
         /*
-         * Above ∂χ/∂φ is equals to 1 in the spherical case.
+         * Above ∂χ/∂φ is equal to 1 in the spherical case.
          * Remaining formulas below are the same than in the spherical case.
          */
         final double B2 = B * B;
@@ -360,7 +362,7 @@
         final double j = atan2(x, g - y) - i;
         /*
          * The conformal longitude is  Λ = j + 2i + Λ₀.  In the particular case of stereographic projection,
-         * the geodetic longitude λ is equals to Λ. Furthermore in Apache SIS implementation, Λ₀ is added by
+         * the geodetic longitude λ is equal to Λ. Furthermore in Apache SIS implementation, Λ₀ is added by
          * the denormalization matrix and shall not be handled here. The only remaining part is λ = j + 2i.
          */
         final double λ = j + 2*i;
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java
index 70979ec..95a2664 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java
@@ -145,8 +145,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate and stores the (<var>x</var>,<var>y</var>) result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates and stores the result in {@code dstPts}.
      * The units of measurement are implementation-specific (see super-class javadoc).
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java
index 0694632..ce77e48 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/PolarStereographic.java
@@ -317,8 +317,9 @@
     }
 
     /**
-     * Converts the specified (θ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Polyconic.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Polyconic.java
index 4170432..e19d6a0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Polyconic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Polyconic.java
@@ -168,9 +168,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}
-     * (linear distance on a unit sphere). In addition, opportunistically computes the projection derivative
-     * if {@code derivate} is {@code true}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
+     * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
index 7bab551..454920c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/SatelliteTracking.java
@@ -295,8 +295,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The units of measurement are implementation-specific (see super-class javadoc).
      * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * <p>The <var>y</var> axis lies along the central meridian λ₀, <var>y</var> increasing northerly, and
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Sinusoidal.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Sinusoidal.java
index 808d9e4..0912b32 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Sinusoidal.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Sinusoidal.java
@@ -17,6 +17,7 @@
 package org.apache.sis.referencing.operation.projection;
 
 import java.util.EnumMap;
+import java.util.regex.Pattern;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.referencing.operation.Matrix;
@@ -41,7 +42,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   1.0
  * @module
  */
@@ -52,16 +53,60 @@
     private static final long serialVersionUID = 7908925241331303236L;
 
     /**
+     * Variants of Sinusoidal projection. Those variants modify the way the projections are constructed
+     * (e.g. in the way parameters are interpreted), but formulas are basically the same after construction.
+     *
+     * <p>We do not provide such codes in public API because they duplicate the functionality of
+     * {@link OperationMethod} instances. We use them only for constructors convenience.</p>
+     */
+    private enum Variant implements ProjectionVariant {
+        // Declaration order matter. Patterns are matched in that order.
+
+        /** The <cite>"Pseudo sinusoidal equal-area"</cite> projection. */
+        PSEUDO(".*\\bPseudo.*");
+
+        /** Name pattern for this variant. */
+        private final Pattern operationName;
+
+        /** Creates a new enumeration value.  */
+        private Variant(final String operationName) {
+            this.operationName = Pattern.compile(operationName, Pattern.CASE_INSENSITIVE);
+        }
+
+        /** The expected name pattern of an operation method for this variant. */
+        @Override public Pattern getOperationNamePattern() {
+            return operationName;
+        }
+
+        /** EPSG identifier of an operation method for this variant. */
+        @Override public String getIdentifier() {
+            return null;
+        }
+    }
+
+    /**
+     * The type of sinusoidal projection. Possible values are:
+     * <ul>
+     *   <li>{@link Variant#PSEUDO} if this projection is the "Pseudo sinusoidal equal-area" case.</li>
+     *   <li>{@code null} for the standard case.</li>
+     * </ul>
+     *
+     * Other cases may be added in the future.
+     */
+    private final Variant variant;
+
+    /**
      * Work around for RFE #4093999 in Sun's bug database
      * ("Relax constraint on placement of this()/super() call in constructors").
      */
     @Workaround(library="JDK", version="1.8")
     private static Initializer initializer(final OperationMethod method, final Parameters parameters) {
+        final Variant variant = variant(method, Variant.values(), null);
         final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(ParameterRole.class);
         roles.put(ParameterRole.CENTRAL_MERIDIAN, CENTRAL_MERIDIAN);
         roles.put(ParameterRole.FALSE_EASTING,    FALSE_EASTING);
         roles.put(ParameterRole.FALSE_NORTHING,   FALSE_NORTHING);
-        return new Initializer(method, parameters, roles, null);
+        return new Initializer(method, parameters, roles, variant);
     }
 
     /**
@@ -76,7 +121,17 @@
      * @param parameters  the parameter values of the projection to create.
      */
     public Sinusoidal(final OperationMethod method, final Parameters parameters) {
-        super(initializer(method, parameters));
+        this(initializer(method, parameters));
+    }
+
+    /**
+     * Work around for RFE #4093999 in Sun's bug database
+     * ("Relax constraint on placement of this()/super() call in constructors").
+     */
+    @Workaround(library="JDK", version="1.7")
+    private Sinusoidal(final Initializer initializer) {
+        super(initializer);
+        variant = (Variant) initializer.variant;
     }
 
     /**
@@ -84,6 +139,7 @@
      */
     Sinusoidal(final Sinusoidal other) {
         super(other);
+        variant = other.variant;
     }
 
     /**
@@ -101,16 +157,16 @@
     @Override
     public MathTransform createMapProjection(final MathTransformFactory factory) throws FactoryException {
         Sinusoidal kernel = this;
-        if (eccentricity == 0 && getClass() == Sinusoidal.class) {
+        if ((eccentricity == 0 && getClass() == Sinusoidal.class) || variant == Variant.PSEUDO) {
             kernel = new Spherical(this);
         }
         return context.completeTransform(factory, kernel);
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}
-     * (linear distance on a unit sphere). In addition, opportunistically computes the projection derivative
-     * if {@code derivate} is {@code true}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
+     * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * @return the matrix of the projection derivative at the given source position,
      *         or {@code null} if the {@code derivate} argument is {@code false}.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
index 10ee421..2ad53aa 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/TransverseMercator.java
@@ -409,8 +409,9 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates (units in radians) and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     * The results must be multiplied by the denormalization matrix before to get linear distances.
      *
      * <h4>Accuracy and domain of validity</h4>
      * Projection errors depend on the difference ∆λ between longitude λ and the central meridian λ₀.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
index 146f574..524c6fd 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/ZonedGridSystem.java
@@ -165,7 +165,7 @@
     }
 
     /**
-     * Converts the specified (λ,φ) coordinate and stores the result in {@code dstPts}.
+     * Projects the specified (λ,φ) coordinates and stores the result in {@code dstPts}.
      * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
      * Note that the derivative does not contain zone prefix.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java
index f2ad345..0fa929c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/package-info.java
@@ -160,7 +160,7 @@
  * @author  Rémi Maréchal (Geomatys)
  * @author  Adrian Custer (Geomatys)
  * @author  Matthieu Bastianelli (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="https://mathworld.wolfram.com/MapProjection.html">Map projections on MathWorld</a>
  *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
index 8e3709d..fc1a854 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/AbstractMathTransform.java
@@ -785,7 +785,7 @@
      * The default implementation performs the following steps:
      *
      * <ul>
-     *   <li>Ensure that the {@code point} dimension is equals to this math transform
+     *   <li>Ensure that the {@code point} dimension is equal to this math transform
      *       {@linkplain #getSourceDimensions() source dimensions}.</li>
      *   <li>Copy the coordinate in a temporary array and pass that array to the
      *       {@link #transform(double[], int, double[], int, boolean)} method,
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
index 3e6e312..6766f5d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
@@ -38,6 +38,7 @@
 import org.opengis.parameter.InvalidParameterNameException;
 import org.opengis.parameter.InvalidParameterValueException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.cs.CartesianCS;
@@ -165,7 +166,7 @@
  * There is typically only one {@code MathTransformFactory} instance for the whole application.
  *
  * @author  Martin Desruisseaux (Geomatys, IRD)
- * @version 1.1
+ * @version 1.3
  *
  * @see MathTransformProvider
  * @see AbstractMathTransform
@@ -541,16 +542,17 @@
      *       coordinate systems are not {@linkplain AxesConvention#NORMALIZED normalized}.</li>
      * </ul>
      *
-     * By default this class does <strong>not</strong> handle change of
+     * This class does <strong>not</strong> handle change of
      * {@linkplain org.apache.sis.referencing.datum.DefaultGeodeticDatum#getPrimeMeridian() prime meridian}
      * or anything else related to datum. Datum changes have dedicated {@link OperationMethod},
      * for example <cite>"Longitude rotation"</cite> (EPSG:9601) for changing the prime meridian.
      *
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 0.7
+     * @version 1.3
      * @since   0.7
      * @module
      */
+    @SuppressWarnings("serial")         // Fields are not statically typed as Serializable.
     public static class Context implements Serializable {
         /**
          * For cross-version compatibility.
@@ -575,14 +577,14 @@
          *
          * @todo We could make this information public as a replacement of {@link #getLastMethodUsed()}.
          */
-        OperationMethod provider;
+        private OperationMethod provider;
 
         /**
          * The parameters actually used.
          *
          * @see #getCompletedParameters()
          */
-        ParameterValueGroup parameters;
+        private ParameterValueGroup parameters;
 
         /**
          * Creates a new context with all properties initialized to {@code null}.
@@ -615,13 +617,39 @@
          *
          * @param  cs         the coordinate system to set as the source, or {@code null}.
          * @param  ellipsoid  the ellipsoid associated to the given coordinate system, or {@code null}.
+         *
+         * @deprecated Replaced by {@link #setSource(GeodeticCRS)}.
          */
+        @Deprecated
         public void setSource(final EllipsoidalCS cs, final Ellipsoid ellipsoid) {
             sourceCS = cs;
             sourceEllipsoid = ellipsoid;
         }
 
         /**
+         * Sets the source coordinate system and related ellipsoid to the components of given CRS.
+         * The {@link Ellipsoid}, fetched from the geodetic datum, is often used together with an {@link EllipsoidalCS},
+         * but not necessarily. The geodetic CRS may also be associated with a spherical or Cartesian coordinate system,
+         * and the ellipsoid information may still be needed even with those non-ellipsoidal coordinate systems.
+         *
+         * <p><strong>This method is not for datum shifts.</strong>
+         * All datum information other than the ellipsoid are ignored.</p>
+         *
+         * @param  crs  the coordinate system and ellipsoid to set as the source, or {@code null}.
+         *
+         * @since 1.3
+         */
+        public void setSource(final GeodeticCRS crs) {
+            if (crs != null) {
+                sourceCS = crs.getCoordinateSystem();
+                sourceEllipsoid = crs.getDatum().getEllipsoid();
+            } else {
+                sourceCS = null;
+                sourceEllipsoid = null;
+            }
+        }
+
+        /**
          * Sets the target coordinate system to the given value.
          * The target ellipsoid is unconditionally set to {@code null}.
          *
@@ -640,13 +668,39 @@
          *
          * @param  cs         the coordinate system to set as the source, or {@code null}.
          * @param  ellipsoid  the ellipsoid associated to the given coordinate system, or {@code null}.
+         *
+         * @deprecated Replaced by {@link #setTarget(GeodeticCRS)}.
          */
+        @Deprecated
         public void setTarget(final EllipsoidalCS cs, final Ellipsoid ellipsoid) {
             targetCS = cs;
             targetEllipsoid = ellipsoid;
         }
 
         /**
+         * Sets the target coordinate system and related ellipsoid to the components of given CRS.
+         * The {@link Ellipsoid}, fetched from the geodetic datum, is often used together with an {@link EllipsoidalCS},
+         * but not necessarily. The geodetic CRS may also be associated with a spherical or Cartesian coordinate system,
+         * and the ellipsoid information may still be needed even with those non-ellipsoidal coordinate systems.
+         *
+         * <p><strong>This method is not for datum shifts.</strong>
+         * All datum information other than the ellipsoid are ignored.</p>
+         *
+         * @param  crs  the coordinate system and ellipsoid to set as the target, or {@code null}.
+         *
+         * @since 1.3
+         */
+        public void setTarget(final GeodeticCRS crs) {
+            if (crs != null) {
+                targetCS = crs.getCoordinateSystem();
+                targetEllipsoid = crs.getDatum().getEllipsoid();
+            } else {
+                targetCS = null;
+                targetEllipsoid = null;
+            }
+        }
+
+        /**
          * Returns the source coordinate system, or {@code null} if unspecified.
          *
          * @return the source coordinate system, or {@code null}.
@@ -981,39 +1035,35 @@
              * not a SIS implementation, use as a fallback whether ellipsoids are provided. This fallback
              * may be less reliable.
              */
-            int n;
+            final boolean sourceOnEllipsoid, targetOnEllipsoid;
             if (provider instanceof AbstractProvider) {
-                n = ((AbstractProvider) provider).getEllipsoidsMask();
+                final AbstractProvider p = (AbstractProvider) provider;
+                sourceOnEllipsoid = p.sourceOnEllipsoid;
+                targetOnEllipsoid = p.targetOnEllipsoid;
             } else {
-                n = 0;
-                if (sourceEllipsoid != null) n  = 1;
-                if (targetEllipsoid != null) n |= 2;
+                sourceOnEllipsoid = getSourceEllipsoid() != null;
+                targetOnEllipsoid = getTargetEllipsoid() != null;
             }
             /*
              * Set the ellipsoid axis-length parameter values. Those parameters may appear in the source ellipsoid,
              * in the target ellipsoid or in both ellipsoids.
              */
-            switch (n) {
-                case 0: return null;
-                case 1: return setEllipsoid(getSourceEllipsoid(), Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null);
-                case 2: return setEllipsoid(getTargetEllipsoid(), Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null);
-                case 3: {
-                    RuntimeException failure = null;
-                    if (sourceCS != null) try {
-                        ensureCompatibleParameters(true);
-                        final ParameterValue<?> p = parameters.parameter("dim");    // Really `parameters`, not `userParams`.
-                        if (p.getValue() == null) {
-                            p.setValue(sourceCS.getDimension());
-                        }
-                    } catch (IllegalArgumentException | IllegalStateException e) {
-                        failure = e;
-                    }
-                    failure = setEllipsoid(getSourceEllipsoid(), "src_semi_major", "src_semi_minor", false, failure);
-                    failure = setEllipsoid(getTargetEllipsoid(), "tgt_semi_major", "tgt_semi_minor", false, failure);
-                    return failure;
+            if (!(sourceOnEllipsoid | targetOnEllipsoid)) return null;
+            if (!targetOnEllipsoid) return setEllipsoid(getSourceEllipsoid(), Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null);
+            if (!sourceOnEllipsoid) return setEllipsoid(getTargetEllipsoid(), Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null);
+            RuntimeException failure = null;
+            if (sourceCS != null) try {
+                ensureCompatibleParameters(true);
+                final ParameterValue<?> p = parameters.parameter("dim");    // Really `parameters`, not `userParams`.
+                if (p.getValue() == null) {
+                    p.setValue(sourceCS.getDimension());
                 }
-                default: throw new AssertionError(n);
+            } catch (IllegalArgumentException | IllegalStateException e) {
+                failure = e;
             }
+            failure = setEllipsoid(getSourceEllipsoid(), "src_semi_major", "src_semi_minor", false, failure);
+            failure = setEllipsoid(getTargetEllipsoid(), "tgt_semi_major", "tgt_semi_minor", false, failure);
+            return failure;
         }
     }
 
@@ -1356,7 +1406,7 @@
         ArgumentChecks.ensureNonNull("baseCRS",    baseCRS);
         ArgumentChecks.ensureNonNull("parameters", parameters);
         ArgumentChecks.ensureNonNull("derivedCS",  derivedCS);
-        final Context context = ReferencingUtilities.createTransformContext(baseCRS, null, null);
+        final Context context = ReferencingUtilities.createTransformContext(baseCRS, null);
         context.setTarget(derivedCS);
         return createParameterizedTransform(parameters, context);
     }
@@ -1403,12 +1453,14 @@
                     final String operation;
                     if (isEllipsoidalSource) {
                         operation = GeographicToGeocentric.NAME;
-                        context.setSource(cs = (EllipsoidalCS) source, ellipsoid);
+                        context.setSource(cs = (EllipsoidalCS) source);
                         context.setTarget(target);
+                        context.sourceEllipsoid = ellipsoid;
                     } else {
                         operation = GeocentricToGeographic.NAME;
                         context.setSource(source);
-                        context.setTarget(cs = (EllipsoidalCS) target, ellipsoid);
+                        context.setTarget(cs = (EllipsoidalCS) target);
+                        context.targetEllipsoid = ellipsoid;
                     }
                     final ParameterValueGroup pg = getDefaultParameters(operation);
                     if (cs.getDimension() < 3) pg.parameter("dim").setValue(2);       // Apache SIS specific parameter.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
index 81b3bbe..ce4215e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
@@ -90,14 +90,13 @@
  * The units of measurements depend on how the {@code MathTransform} has been created:
  * <ul>
  *   <li>{@code EllipsoidToCentricTransform} instances created directly by the constructor expect (λ,φ) values
- *       in radians and compute (X,Y,Z) values in units of an ellipsoid having a semi-major axis length of 1.
- *       That constructor is reserved for subclasses only.</li>
+ *       in radians and compute (X,Y,Z) values in units of an ellipsoid having a semi-major axis length of 1.</li>
  *   <li>Transforms created by the {@link #createGeodeticConversion createGeodeticConversion(…)} static method expect
  *       (λ,φ) values in degrees and compute (X,Y,Z) values in units of the ellipsoid axes (usually metres).</li>
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -265,7 +264,7 @@
      *
      * @see #createGeodeticConversion(MathTransformFactory, double, double, Unit, boolean, TargetType)
      */
-    protected EllipsoidToCentricTransform(final double semiMajor, final double semiMinor,
+    public EllipsoidToCentricTransform(final double semiMajor, final double semiMinor,
             final Unit<Length> unit, final boolean withHeight, final TargetType target)
     {
         ArgumentChecks.ensureStrictlyPositive("semiMajor", semiMajor);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 65af836..73e125f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -375,9 +375,9 @@
      * <p>Invariants:</p>
      * <ul>
      *   <li>The {@linkplain AbstractMathTransform#getSourceDimensions() source dimensions} of the returned transform
-     *       is equals to the sum of the source dimensions of all given transforms.</li>
+     *       is equal to the sum of the source dimensions of all given transforms.</li>
      *   <li>The {@linkplain AbstractMathTransform#getTargetDimensions() target dimensions} of the returned transform
-     *       is equals to the sum of the target dimensions of all given transforms.</li>
+     *       is equal to the sum of the target dimensions of all given transforms.</li>
      * </ul>
      *
      * @param  components  the transforms to aggregate in a single transform, in the given order.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
index e8992a2..5a8cd86 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
@@ -62,8 +62,7 @@
  *
  * The units of measurements depend on how the {@code MathTransform} has been created:
  * <ul>
- *   <li>{@code MolodenskyTransform} instances created directly by the constructor work with angular values in radians.
- *       That constructor is reserved for subclasses only.</li>
+ *   <li>{@code MolodenskyTransform} instances created directly by the constructor work with angular values in radians.</li>
  *   <li>Transforms created by the {@link #createGeodeticTransformation createGeodeticTransformation(…)} static method
  *       work with angular values in degrees and heights in the same units than the <strong>source</strong> ellipsoid
  *       axes (usually metres).</li>
@@ -82,7 +81,7 @@
  * @author  Rueben Schulz (UBC)
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Rémi Maréchal (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -145,10 +144,10 @@
      *
      * @see #createGeodeticTransformation(MathTransformFactory, Ellipsoid, boolean, Ellipsoid, boolean, double, double, double, boolean)
      */
-    protected MolodenskyTransform(final Ellipsoid source, final boolean isSource3D,
-                                  final Ellipsoid target, final boolean isTarget3D,
-                                  final double tX, final double tY, final double tZ,
-                                  final boolean isAbridged)
+    public MolodenskyTransform(final Ellipsoid source, final boolean isSource3D,
+                               final Ellipsoid target, final boolean isTarget3D,
+                               final double tX, final double tY, final double tZ,
+                               final boolean isAbridged)
     {
         super(source, isSource3D, target, isTarget3D, tX, tY, tZ, null, isAbridged,
                 isAbridged ? AbridgedMolodensky.PARAMETERS : Molodensky.PARAMETERS);
diff --git a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
index 52f61ed..4c04e55 100644
--- a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
+++ b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
@@ -64,6 +64,7 @@
 org.apache.sis.internal.referencing.provider.AzimuthalEquidistantSpherical
 org.apache.sis.internal.referencing.provider.ZonedTransverseMercator
 org.apache.sis.internal.referencing.provider.Sinusoidal
+org.apache.sis.internal.referencing.provider.PseudoSinusoidal
 org.apache.sis.internal.referencing.provider.Polyconic
 org.apache.sis.internal.referencing.provider.Mollweide
 org.apache.sis.internal.referencing.provider.SouthPoleRotation
@@ -75,3 +76,5 @@
 org.apache.sis.internal.referencing.provider.Interpolation1D
 org.apache.sis.internal.referencing.provider.SatelliteTracking
 org.apache.sis.internal.referencing.provider.Wraparound
+org.apache.sis.internal.referencing.provider.GeocentricToTopocentric
+org.apache.sis.internal.referencing.provider.GeographicToTopocentric
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/EnvelopesTest.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/EnvelopesTest.java
index 5f4967b..3f17668 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/EnvelopesTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/EnvelopesTest.java
@@ -102,7 +102,7 @@
     }
 
     /**
-     * Asserts that the given envelope is equals to the expected value.
+     * Asserts that the given envelope is equal to the expected value.
      */
     @Override
     void assertGeometryEquals(GeneralEnvelope expected, GeneralEnvelope actual, double tolx, double toly) {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/GeneralEnvelopeTest.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/GeneralEnvelopeTest.java
index d2e44c1..ac572c7 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/GeneralEnvelopeTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/GeneralEnvelopeTest.java
@@ -85,7 +85,7 @@
     }
 
     /**
-     * Asserts that the given two-dimensional envelope is equals to the given rectangle.
+     * Asserts that the given two-dimensional envelope is equal to the given rectangle.
      * The {@code xLower} and {@code xUpper} arguments are the <var>x</var> coordinate values
      * for the lower and upper corners respectively. The actual {@code xmin} and {@code ymin}
      * values will be inferred from those corners.
@@ -129,7 +129,7 @@
     }
 
     /**
-     * Asserts that the intersection of the two following envelopes is equals to the given rectangle.
+     * Asserts that the intersection of the two following envelopes is equal to the given rectangle.
      * First, this method tests using the {@link Envelope2D} implementation. Then, it tests using the
      * {@link GeneralEnvelope} implementation.
      */
@@ -159,7 +159,7 @@
     }
 
     /**
-     * Asserts that the union of the two following envelopes is equals to the given rectangle.
+     * Asserts that the union of the two following envelopes is equal to the given rectangle.
      * First, this method tests using the {@link Envelope2D} implementation.
      * Then, it tests using the {@link GeneralEnvelope} implementation.
      *
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/Shapes2DTest.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/Shapes2DTest.java
index 8862db4..53a6556 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/Shapes2DTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/Shapes2DTest.java
@@ -79,7 +79,7 @@
     }
 
     /**
-     * Asserts that the given rectangle is equals to the expected value.
+     * Asserts that the given rectangle is equal to the expected value.
      */
     @Override
     void assertGeometryEquals(Rectangle2D expected, Rectangle2D actual, double tolx, double toly) {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java
index 0514d7f..27233c2 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java
@@ -80,7 +80,7 @@
     abstract boolean contains(G outer, G inner);
 
     /**
-     * Asserts that the given envelope or rectangle is equals to the expected value.
+     * Asserts that the given envelope or rectangle is equal to the expected value.
      */
     abstract void assertGeometryEquals(G expected, G actual, double tolx, double toly);
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/j2d/ShapeUtilitiesTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/j2d/ShapeUtilitiesTest.java
index a0653a9..efac0d6 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/j2d/ShapeUtilitiesTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/j2d/ShapeUtilitiesTest.java
@@ -44,7 +44,7 @@
     private static final double EPS = 1E-12;
 
     /**
-     * Asserts that the given point is equals to the given value.
+     * Asserts that the given point is equal to the given value.
      */
     private static void assertPointEquals(final double x, final double y, final Point2D point) {
         assertEquals(x, point.getX(), EPS);
@@ -95,7 +95,7 @@
 
     /**
      * Invokes {@code ShapeUtilities.fitParabol(x1, y1, px, py, x2, y2, horizontal)},
-     * then verifies that the control point of the returned curve is equals to {@code (cx, cy)}.
+     * then verifies that the control point of the returned curve is equal to {@code (cx, cy)}.
      */
     private static void assertParabolEquals(final double cx, final double cy,
                                             final double x1, final double y1,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProviderMock.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProviderMock.java
index 1d03d13..ea85948 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProviderMock.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProviderMock.java
@@ -18,6 +18,8 @@
 
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.operation.SingleOperation;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
@@ -33,7 +35,7 @@
  * <p>Subclasses may be promoted to a real operation if we implement their formulas in a future Apache SIS version.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -42,11 +44,13 @@
     /**
      * Creates a new mock provider.
      */
-    ProviderMock(final int sourceDimension,
-                 final int targetDimension,
-                 final ParameterDescriptorGroup parameters)
+    ProviderMock(final ParameterDescriptorGroup parameters,
+                 final int sourceDimension,
+                 final int targetDimension)
     {
-        super(sourceDimension, targetDimension, parameters);
+        super(SingleOperation.class, parameters,
+              CoordinateSystem.class, sourceDimension, false,
+              CoordinateSystem.class, targetDimension, false);
     }
 
     /**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
index fb24e65..12e85a3 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
@@ -36,7 +36,7 @@
  * Tests {@link Providers} and some consistency rules of all providers defined in this package.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -71,6 +71,8 @@
             GeocentricTranslation3D.class,
             GeographicToGeocentric.class,
             GeocentricToGeographic.class,
+            GeocentricToTopocentric.class,
+            GeographicToTopocentric.class,
             Geographic3Dto2D.class,
             Geographic2Dto3D.class,
             Molodensky.class,
@@ -114,6 +116,7 @@
             ZonedTransverseMercator.class,
             SatelliteTracking.class,
             Sinusoidal.class,
+            PseudoSinusoidal.class,
             Polyconic.class,
             Mollweide.class,
             SouthPoleRotation.class,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/SeismicBinGridMock.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/SeismicBinGridMock.java
index 3fc49f7..abe4383 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/SeismicBinGridMock.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/SeismicBinGridMock.java
@@ -65,6 +65,6 @@
      * Creates a new <cite>"Seismic bin grid transformation"</cite> operation method.
      */
     public SeismicBinGridMock() {
-        super(2, 2, PARAMETERS);
+        super(PARAMETERS, 2, 2);
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/TopocentricConversionMock.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/TopocentricConversionMock.java
deleted file mode 100644
index c46a448..0000000
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/TopocentricConversionMock.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.referencing.provider;
-
-import org.opengis.parameter.ParameterDescriptor;
-import org.opengis.parameter.ParameterDescriptorGroup;
-import org.apache.sis.parameter.ParameterBuilder;
-import org.apache.sis.measure.Units;
-
-
-/**
- * The provider for <cite>"Geographic/topocentric conversions"</cite> conversion (EPSG:9837).
- *
- * This conversion is not yet implemented in Apache SIS, but we need to at least accept the parameters
- * for a Well Known Text (WKT) parsing test in the {@link org.apache.sis.io.wkt.WKTParserTest} class.
- *
- * <p>This class may be promoted to a real operation if we implement the formulas in a future Apache SIS version.</p>
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since   0.6
- * @module
- */
-@SuppressWarnings("serial")
-public final strictfp class TopocentricConversionMock extends ProviderMock {
-    /**
-     * The group of all parameters expected by this coordinate operation.
-     */
-    private static final ParameterDescriptorGroup PARAMETERS;
-    static {
-        final ParameterBuilder builder = builder();
-        final ParameterDescriptor<?>[] parameters = {
-            createLatitude (builder.addIdentifier("8834").addName("Latitude of topocentric origin"), true),
-            createLongitude(builder.addIdentifier("8835").addName("Longitude of topocentric origin")),
-            builder.addIdentifier("8836").addName("Ellipsoidal height of topocentric origin").create(0, Units.METRE)
-        };
-        PARAMETERS = builder
-                .addIdentifier("9837")
-                .addName("Geographic/topocentric conversions")
-                .createGroup(parameters);
-    }
-
-    /**
-     * Creates a new <cite>"Geographic/topocentric conversions"</cite> operation method.
-     */
-    public TopocentricConversionMock() {
-        super(3, 3, PARAMETERS);
-    }
-}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index 4a106b9..78ca272 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -57,7 +57,7 @@
  * Tests {@link GeodeticObjectParser}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -972,6 +972,53 @@
     }
 
     /**
+     * Tests the parsing of a derived CRS from a WKT 2 string.
+     *
+     * @throws ParseException if the parsing failed.
+     */
+    @Test
+    public void testDerivedCRS() throws ParseException {
+        final DerivedCRS crs = parse(DerivedCRS.class,
+                "GeodCRS[“EPSG topocentric example B”,\n" +
+                "  BaseGeodCRS[“WGS 84”,\n" +
+                "    Datum[“World Geodetic System 1984”,\n" +
+                "      Ellipsoid[“WGS 84”, 6378137.0, 298.257223563, LengthUnit[“metre”, 1]]],\n" +
+                "      PrimeM[“Greenwich”, 0.0, AngleUnit[“degree”, 0.017453292519943295]]],\n" +
+                "  DerivingConversion[“EPSG topocentric example B”,\n" +
+                "    Method[“Geocentric/topocentric conversions”, Id[“EPSG”, 9836]],\n" +
+                "    Parameter[“Geocentric X of topocentric origin”, 3771793.97, LengthUnit[“metre”, 1], Id[“EPSG”, 8837]],\n" +
+                "    Parameter[“Geocentric Y of topocentric origin”,  140253.34, LengthUnit[“metre”, 1], Id[“EPSG”, 8838]],\n" +
+                "    Parameter[“Geocentric Z of topocentric origin”, 5124304.35, LengthUnit[“metre”, 1], Id[“EPSG”, 8839]]],\n" +
+                "  CS[Cartesian, 3],\n" +
+                "    Axis[“Topocentric East (U)”,  east],\n" +
+                "    Axis[“Topocentric North (V)”, north],\n" +
+                "    Axis[“Topocentric height (W)”, up],\n" +
+                "    LengthUnit[“metre”, 1],\n" +
+                "  Scope[“Example only - fictitious.”],\n" +
+                "  Id[“EPSG”, 5820, “9.9.1”, URI[“urn:ogc:def:crs:EPSG:9.9.1:5820”]]]");
+
+        assertNameAndIdentifierEqual("EPSG topocentric example B", 5820, crs);
+        assertNameAndIdentifierEqual("EPSG topocentric example B", 0, crs.getConversionFromBase());
+        CoordinateSystem cs = crs.getCoordinateSystem();
+        assertInstanceOf("coordinateSystem", CartesianCS.class, cs);
+        assertEquals("dimension", 3, cs.getDimension());
+        assertUnboundedAxisEquals("Topocentric East",   "U", AxisDirection.EAST,  Units.METRE, cs.getAxis(0));
+        assertUnboundedAxisEquals("Topocentric North",  "V", AxisDirection.NORTH, Units.METRE, cs.getAxis(1));
+        assertUnboundedAxisEquals("Topocentric height", "W", AxisDirection.UP,    Units.METRE, cs.getAxis(2));
+        /*
+         * The type of the coordinate system of the base CRS is not specified in the WKT.
+         * The parser should use the `AbstractProvider.sourceCSType` field for detecting
+         * that the expected type for “Geocentric/topocentric conversions” is Cartesian.
+         */
+        cs = crs.getBaseCRS().getCoordinateSystem();
+        assertInstanceOf("coordinateSystem", CartesianCS.class, cs);
+        assertEquals("dimension", 3, cs.getDimension());
+        assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_X, "X", AxisDirection.GEOCENTRIC_X, Units.METRE, cs.getAxis(0));
+        assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_Y, "Y", AxisDirection.GEOCENTRIC_Y, Units.METRE, cs.getAxis(1));
+        assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_Z, "Z", AxisDirection.GEOCENTRIC_Z, Units.METRE, cs.getAxis(2));
+    }
+
+    /**
      * Tests the parsing of an engineering CRS from a WKT 2 string.
      *
      * @throws ParseException if the parsing failed.
@@ -990,6 +1037,7 @@
         assertNameAndIdentifierEqual("A building-centred CRS", 0, crs);
         assertNameAndIdentifierEqual("Building reference point", 0, crs.getDatum());
         final CoordinateSystem cs = crs.getCoordinateSystem();
+        assertInstanceOf("coordinateSystem", CartesianCS.class, cs);
         assertEquals("dimension", 3, cs.getDimension());
 
         // Axis names are arbitrary and could change in future SIS versions.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/TransliteratorTest.java b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/TransliteratorTest.java
index c1f35d7..77e89b7 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/TransliteratorTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/TransliteratorTest.java
@@ -119,7 +119,7 @@
 
     /**
      * Asserts that the name of the given axis, after replacement by a short name,
-     * is equals to the expected string.
+     * is equal to the expected string.
      */
     private static void assertShortAxisNameEquals(final String expected, final CoordinateSystemAxisMock axis) {
         assertEquals("name", expected, Transliterator.DEFAULT.toShortAxisName(axis,
@@ -128,7 +128,7 @@
 
     /**
      * Asserts that the abbreviation of the given axis, after replacement of Greek letters,
-     * is equals to the expected string.
+     * is equal to the expected string.
      */
     private static void assertAbbreviationEquals(final String expected, final CoordinateSystemAxisMock axis) {
         assertEquals("abbreviation", expected, Transliterator.DEFAULT.toLatinAbbreviation(axis,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/parameter/TensorValuesTest.java b/core/sis-referencing/src/test/java/org/apache/sis/parameter/TensorValuesTest.java
index a47d554..f2fc84d 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/parameter/TensorValuesTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/parameter/TensorValuesTest.java
@@ -333,7 +333,7 @@
      * Tests {@link TensorParameters#ALPHANUM} formatting.
      * <ul>
      *   <li>Group name shall be {@code "Affine parametric transformation"}.</li>
-     *   <li>No {@code "num_row"} or {@code "num_col"} parameters if their value is equals to 3.</li>
+     *   <li>No {@code "num_row"} or {@code "num_col"} parameters if their value is equal to 3.</li>
      *   <li>Parameter names shall be of the form {@code "A0"}.</li>
      *   <li>Identifiers present, but only for A0-A2 and B0-B2.</li>
      * </ul>
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java
index 02722f4..b48ee80 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java
@@ -92,6 +92,8 @@
 
     /**
      * Tests {@link CRS#forCode(String)} with EPSG codes.
+     * The codes tested by this method shall be in the list of EPSG codes
+     * for which Apache SIS has hard-coded fallbacks to use if no EPSG database is available.
      *
      * @throws FactoryException if a CRS can not be constructed.
      *
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
index 3dcbcdf..57e6980 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/GeodeticCalculatorTest.java
@@ -75,7 +75,7 @@
     }
 
     /**
-     * Verifies that the given point is equals to the given latitude and longitude.
+     * Verifies that the given point is equal to the given latitude and longitude.
      *
      * @param φ  the expected latitude value, in degrees.
      * @param λ  the expected longitude value, in degrees.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultDerivedCRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultDerivedCRSTest.java
index 79afba8..a421004 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultDerivedCRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultDerivedCRSTest.java
@@ -44,7 +44,7 @@
  * Tests the {@link DefaultDerivedCRS} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.6
  * @module
  */
@@ -87,13 +87,13 @@
         assertEquals("Using consistent arguments but one less dimension.", WKTKeywords.GeodeticCRS,
                 DefaultDerivedCRS.getType(HardCodedCRS.GEOCENTRIC, HardCodedCS.CARTESIAN_2D));
 
-        assertEquals("Using different coordinate system type.", WKTKeywords.EngineeringCRS,
+        assertEquals("Using different coordinate system type.", WKTKeywords.GeodeticCRS,
                 DefaultDerivedCRS.getType(HardCodedCRS.GEOCENTRIC, HardCodedCS.SPHERICAL));
 
-        assertEquals("Using different coordinate system type.", WKTKeywords.EngineeringCRS,
+        assertEquals("Using different coordinate system type.", WKTKeywords.GeodeticCRS,
                 DefaultDerivedCRS.getType(HardCodedCRS.WGS84, HardCodedCS.CARTESIAN_2D));
 
-        assertEquals("Using illegal coordinate system type.", WKTKeywords.EngineeringCRS,
+        assertNull("Using illegal coordinate system type.",
                 DefaultDerivedCRS.getType(HardCodedCRS.WGS84, HardCodedCS.GRAVITY_RELATED_HEIGHT));
     }
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CoordinateSystemsTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CoordinateSystemsTest.java
index cc10dab..cc4dbc8 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CoordinateSystemsTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CoordinateSystemsTest.java
@@ -132,7 +132,7 @@
     }
 
     /**
-     * Asserts that the angle between the parsed directions is equals to the given value.
+     * Asserts that the angle between the parsed directions is equal to the given value.
      * This method tests also the angle by interchanging the axis directions.
      */
     private static void assertAngleEquals(final boolean isElevation, final double expected,
@@ -146,7 +146,7 @@
     }
 
     /**
-     * Asserts that the angle between the given directions is equals to the given value.
+     * Asserts that the angle between the given directions is equal to the given value.
      * This method tests also the angle by interchanging the given directions.
      */
     private static void assertAngleEquals(final boolean isElevation, final double expected,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/IdentifiedObjectFinderTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/IdentifiedObjectFinderTest.java
index 83e29ba..3baf8bd 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/IdentifiedObjectFinderTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/IdentifiedObjectFinderTest.java
@@ -89,7 +89,7 @@
                    finder.findSingleton(search));
 
         finder.setSearchDomain(IdentifiedObjectFinder.Domain.VALID_DATASET);
-        assertSame("A full scan should allow us to find WGS84, since it is equals ignoring metadata to CRS:84.",
+        assertSame("A full scan should allow us to find WGS84, since it is equal ignoring metadata to CRS:84.",
                    CRS84, finder.findSingleton(search));
     }
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/MatrixTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/MatrixTestCase.java
index 749ddb1..d1522ed 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/MatrixTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/MatrixTestCase.java
@@ -149,7 +149,7 @@
     }
 
     /**
-     * Verifies that the SIS matrix is equals to the JAMA one, up to the given tolerance value.
+     * Verifies that the SIS matrix is equal to the JAMA one, up to the given tolerance value.
      *
      * @param  expected   the JAMA matrix used as a reference implementation.
      * @param  actual     the SIS matrix to compare to JAMA.
@@ -177,7 +177,7 @@
     }
 
     /**
-     * Asserts that the given matrix is equals to the given expected values, up to the given tolerance threshold.
+     * Asserts that the given matrix is equal to the given expected values, up to the given tolerance threshold.
      * This method compares the elements values in two slightly redundant ways.
      */
     static void assertEqualsElements(final double[] expected, final int numRow, final int numCol,
@@ -190,7 +190,7 @@
     }
 
     /**
-     * Asserts that an element from the given matrix is equals to the expected value, using a relative threshold.
+     * Asserts that an element from the given matrix is equal to the expected value, using a relative threshold.
      */
     private static void assertEqualsRelative(final String message, final double expected,
             final MatrixSIS matrix, final int row, final int column)
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
index 7ee9d06..7b949b1 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
@@ -218,7 +218,7 @@
     }
 
     /**
-     * Transforms the given coordinates and verifies that the result is equals (within a positive delta)
+     * Transforms the given coordinates and verifies that the result is equal (within a positive delta)
      * to the expected ones. If the difference between an expected and actual coordinate value is greater
      * than the {@linkplain #tolerance tolerance} threshold, then the assertion fails.
      *
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformResultComparator.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformResultComparator.java
index ca59ed4..817b3f4 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformResultComparator.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/TransformResultComparator.java
@@ -61,7 +61,7 @@
     }
 
     /**
-     * Delegates to the tested implementation and verifies that the value is equals
+     * Delegates to the tested implementation and verifies that the value is equal
      * to the one provided by the reference implementation.
      */
     @Override
@@ -72,7 +72,7 @@
     }
 
     /**
-     * Delegates to the tested implementation and verifies that the value is equals
+     * Delegates to the tested implementation and verifies that the value is equal
      * to the one provided by the reference implementation.
      */
     @Override
@@ -83,7 +83,7 @@
     }
 
     /**
-     * Delegates to the tested implementation and verifies that the value is equals
+     * Delegates to the tested implementation and verifies that the value is equal
      * to the one provided by the reference implementation.
      */
     @Override
@@ -94,7 +94,7 @@
     }
 
     /**
-     * Delegates to the tested implementation and verifies that the value is equals
+     * Delegates to the tested implementation and verifies that the value is equal
      * to the one provided by the reference implementation.
      */
     @Override
@@ -105,7 +105,7 @@
     }
 
     /**
-     * Delegates to the tested implementation and verifies that the value is equals
+     * Delegates to the tested implementation and verifies that the value is equal
      * to the one provided by the reference implementation.
      */
     @Override
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java
index f8751f3..86404ec 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/report/HTMLGenerator.java
@@ -185,7 +185,7 @@
     }
 
     /**
-     * Closes the last HTML tag if it is equals to the given element, and opens a new tag on the same line.
+     * Closes the last HTML tag if it is equal to the given element, and opens a new tag on the same line.
      *
      * @param  tag  the HTML tag without brackets (e.g. {@code "h2"}).
      * @throws IOException if an error occurred while writing to the file.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/ReferencingAssert.java b/core/sis-referencing/src/test/java/org/apache/sis/test/ReferencingAssert.java
index b4f61f0..617af3a 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/ReferencingAssert.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/ReferencingAssert.java
@@ -132,7 +132,7 @@
     }
 
     /**
-     * Asserts that the tip of the unique alias of the given object is equals to the expected value.
+     * Asserts that the tip of the unique alias of the given object is equal to the expected value.
      * As a special case if the expected value is null, then this method verifies that the given object has no alias.
      *
      * @param expected  the expected alias, or {@code null} if we expect no alias.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/ConsistencyTest.java b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/ConsistencyTest.java
index fab5bee..437ca32 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/ConsistencyTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/ConsistencyTest.java
@@ -74,7 +74,7 @@
      */
     private static final Set<String> EXCLUDES = new HashSet<>(Arrays.asList(
         "CRS:1",            // Computer display: WKT parser alters the (i,j) axis names.
-        "EPSG:5819",        // EPSG topocentric example A: error while parsing WKT.
+        "EPSG:5819",        // EPSG topocentric example A: DerivedCRS wrongly handled as a ProjectedCRS. See SIS-518.
         "AUTO2:42001",      // This projection requires parameters, but we provide none.
         "AUTO2:42002",      // This projection requires parameters, but we provide none.
         "AUTO2:42003",      // This projection requires parameters, but we provide none.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/CoordinateReferenceSystemTest.java b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/CoordinateReferenceSystemTest.java
index f600d68..a9ca705 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/CoordinateReferenceSystemTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/CoordinateReferenceSystemTest.java
@@ -17,6 +17,11 @@
 package org.apache.sis.test.integration;
 
 import org.opengis.util.FactoryException;
+import org.opengis.referencing.cs.CartesianCS;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.crs.DerivedCRS;
+import org.opengis.referencing.crs.GeodeticCRS;
+import org.opengis.referencing.crs.GeneralDerivedCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.factory.TestFactorySource;
@@ -24,15 +29,15 @@
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.opengis.test.Assert.*;
 import static org.junit.Assume.assumeNotNull;
 
 
 /**
- * Advances CRS constructions requiring the EPSG geodetic dataset.
+ * Advanced CRS constructions requiring the EPSG geodetic dataset.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -55,4 +60,31 @@
         CoordinateReferenceSystem crs = CRS.forCode("urn:ogc:def:crs, crs:EPSG::27700, crs:EPSG::5701");
         assertSame("OSGB 1936 / British National Grid + ODN height", CRS.forCode("EPSG:7405"), crs);
     }
+
+    /**
+     * Tests creation of "EPSG topocentric example A/B". They are derived geodetic CRS.
+     *
+     * @throws FactoryException if an authority or a code is not recognized.
+     */
+    @Test
+    public void testDerivedCRS() throws FactoryException {
+        assumeNotNull(TestFactorySource.getSharedFactory());
+        CoordinateReferenceSystem crs = CRS.forCode("EPSG:5820");
+        assertInstanceOf("Derived CRS type",  DerivedCRS .class, crs);
+        assertInstanceOf("Derived CRS type",  GeodeticCRS.class, crs);
+        assertInstanceOf("CS of derived CRS", CartesianCS.class, crs.getCoordinateSystem());
+        assertInstanceOf("CS of base CRS",    CartesianCS.class, ((GeneralDerivedCRS) crs).getBaseCRS().getCoordinateSystem());
+        /*
+         * Some tests are disabled because `EPSGDataAccess` confuse this derived CRS
+         * with a projected CRS. We are waiting for upgrade to EPSG database 10+
+         * before to re-evaluate how to fix this issue.
+         *
+         * https://issues.apache.org/jira/browse/SIS-518
+         */
+        crs = CRS.forCode("EPSG:5819");
+//      assertInstanceOf("Derived CRS type",  DerivedCRS .class, crs);
+//      assertInstanceOf("Derived CRS type",  GeodeticCRS.class, crs);
+        assertInstanceOf("CS of derived CRS", CartesianCS.class, crs.getCoordinateSystem());
+        assertInstanceOf("CS of base CRS",    EllipsoidalCS.class, ((GeneralDerivedCRS) crs).getBaseCRS().getCoordinateSystem());
+    }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/package-info.java b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/package-info.java
index c6b7788..53f6cdc 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/package-info.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/package-info.java
@@ -23,7 +23,7 @@
  * environment variable.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.3
  * @since   0.4
  * @module
  */
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/widget/SwingAssertions.java b/core/sis-referencing/src/test/java/org/apache/sis/test/widget/SwingAssertions.java
index afbf9b4..ed2aee7 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/widget/SwingAssertions.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/widget/SwingAssertions.java
@@ -38,7 +38,7 @@
     }
 
     /**
-     * Ensures that a tree is equals to an other tree.
+     * Ensures that a tree is equal to an other tree.
      * This method invokes itself recursively for every child nodes.
      *
      * @param  expected  the expected tree, or {@code null}.
diff --git a/core/sis-referencing/src/test/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod b/core/sis-referencing/src/test/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
index b6d93bb..2fcd0f5 100644
--- a/core/sis-referencing/src/test/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
+++ b/core/sis-referencing/src/test/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
@@ -1,4 +1,3 @@
 # Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements;
 # and to You under the Apache License, Version 2.0.
-org.apache.sis.internal.referencing.provider.TopocentricConversionMock
 org.apache.sis.internal.referencing.provider.SeismicBinGridMock
diff --git a/core/sis-referencing/src/test/resources/org/apache/sis/referencing/crs/DerivedCRS.xml b/core/sis-referencing/src/test/resources/org/apache/sis/referencing/crs/DerivedCRS.xml
index 8e72f0f..5b76845 100644
--- a/core/sis-referencing/src/test/resources/org/apache/sis/referencing/crs/DerivedCRS.xml
+++ b/core/sis-referencing/src/test/resources/org/apache/sis/referencing/crs/DerivedCRS.xml
@@ -147,7 +147,7 @@
    <gml:derivedCRSType> is specific to <gml:DerivedCRS> and is not stored explicitly in Apache SIS
    implementation. Instead, we infer this value from the interface implemented by DefaultDerivedCRS.
   -->
-  <gml:derivedCRSType codeSpace="EPSG">engineering</gml:derivedCRSType>
+  <gml:derivedCRSType codeSpace="EPSG">geodetic</gml:derivedCRSType>
 
   <gml:coordinateSystem>
     <gml:CartesianCS gml:id="test-cs-derivedcs">
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java b/core/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java
index eb25d93..2d1a07e 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java
@@ -151,7 +151,7 @@
     }
 
     /**
-     * If {@code existing} or one of its children is equals to the given {@code converter},
+     * If {@code existing} or one of its children is equal to the given {@code converter},
      * returns it. Otherwise returns {@code null}.
      *
      * @param  <S>        the {@code converter} source class.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
index 2e86876..615fdf8 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
@@ -33,7 +33,6 @@
  *
  * <p>For example, all the following URIs are for the same object:</p>
  * <ul>
- *   <li>{@code "4326"} (codespace inferred by the caller)</li>
  *   <li>{@code "EPSG:4326"} (older format)</li>
  *   <li>{@code "EPSG::4326"} (often seen for similarity with URN below)</li>
  *   <li>{@code "urn:ogc:def:crs:EPSG::4326"} (version number is omitted)</li>
@@ -106,7 +105,7 @@
  * {@code "urn:ogc:def:crs,crs:EPSG:6.3:27700,crs:EPSG:6.3:5701"}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see org.apache.sis.internal.metadata.NameMeaning
  * @see <a href="https://portal.ogc.org/files/?artifact_id=24045">Definition identifier URNs in OGC namespace</a>
@@ -122,7 +121,7 @@
     public static final String PREFIX = "urn:ogc:def";
 
     /**
-     * The URN separator.
+     * The path separator in URN.
      */
     public static final char SEPARATOR = ':';
 
@@ -403,7 +402,7 @@
                         do {
                             /*
                              * Find indices of URI sub-component to parse. The sub-component will
-                             * go from 'splitAt' to 'next' exclusive ('splitAt' is exclusive too).
+                             * go from `splitAt` to `next` exclusive (`splitAt` is exclusive too).
                              */
                             int next = uri.indexOf(componentSeparator, splitAt+1);
                             hasMore = next >= 0 && next < componentsEnd;
@@ -468,7 +467,7 @@
      * @param  upper      index after the last character in {@code urn} to compare, ignoring whitespaces.
      * @return {@code true} if the given sub-region of {@code urn} match the given part.
      */
-    static boolean regionMatches(final String part, final String urn, int lower, int upper) {
+    public static boolean regionMatches(final String part, final String urn, int lower, int upper) {
         lower = skipLeadingWhitespaces (urn, lower, upper);
         upper = skipTrailingWhitespaces(urn, lower, upper);
         final int length = upper - lower;
@@ -493,39 +492,8 @@
     }
 
     /**
-     * Returns the substring of the given URN, ignoring whitespaces and version number if present.
-     * The substring is expected to contains at most one {@code ':'} character. If such separator
-     * character is present, then that character and everything before it are ignored. The ignored
-     * part should be the version number, but this is not verified.
-     *
-     * <p>If the remaining substring is empty or contains more {@code ':'} characters, then this method
-     * returns {@code null}. The presence of more {@code ':'} characters means that the code has parameters,
-     * (e.g. {@code "urn:ogc:def:crs:OGC:1.3:AUTO42003:1:-100:45"}) which are not handled by this method.</p>
-     *
-     * @param  urn        the URN from which to get the code.
-     * @param  fromIndex  index of the first character in {@code urn} to check.
-     * @return the code part of the URN, or {@code null} if empty or invalid.
-     */
-    private static String codeIgnoreVersion(final String urn, int fromIndex) {
-        final int length = urn.length();
-        fromIndex = skipLeadingWhitespaces(urn, fromIndex, length);
-        if (fromIndex >= length) {
-            return null;                            // Empty code.
-        }
-        final int s = urn.indexOf(SEPARATOR, fromIndex);
-        if (s >= 0) {
-            // Ignore the version number (actually everything up to the first ':').
-            fromIndex = skipLeadingWhitespaces(urn, s+1, length);
-            if (fromIndex >= length || urn.indexOf(SEPARATOR, fromIndex) >= 0) {
-                return null;    // Empty code, or the code is followed by parameters.
-            }
-        }
-        return urn.substring(fromIndex, skipTrailingWhitespaces(urn, fromIndex, length));
-    }
-
-    /**
      * Returns the code part of the given URI, provided that it matches the given object type and authority.
-     * This lightweight method is useful when:
+     * This method is useful when:
      *
      * <ul>
      *   <li>the URI is expected to have a specific <cite>object type</cite> and <cite>authority</cite>;</li>
@@ -536,77 +504,63 @@
      * This method accepts the following URI representations:
      *
      * <ul>
-     *   <li>Code alone, without any {@code ':'} character (e.g. {@code "4326"}).</li>
      *   <li>The given authority followed by the code (e.g. {@code "EPSG:4326"}).</li>
      *   <li>The URN form (e.g. {@code "urn:ogc:def:crs:EPSG::4326"}), ignoring version number.
      *       This method accepts also the former {@code "x-ogc"} in place of {@code "ogc"}.</li>
-     *   <li>The HTTP form (e.g. {@code "http://www.opengis.net/gml/srs/epsg.xml#4326"}).</li>
+     *   <li>The HTTP form (e.g. {@code "http://www.opengis.net/def/crs/EPSG/0/4326"}).</li>
+     *   <li>The GML form (e.g. {@code "http://www.opengis.net/gml/srs/epsg.xml#4326"}).</li>
      * </ul>
      *
      * @param  type       the expected object type (e.g. {@code "crs"}) in lower cases. See class javadoc for a list of types.
-     * @param  authority  the expected authority, typically {@code "epsg"}. See class javadoc for a list of authorities.
+     * @param  authority  the expected authority, typically {@code "EPSG"}. See class javadoc for a list of authorities.
      * @param  uri        the URI to parse.
      * @return the code part of the given URI, or {@code null} if the codespace does not match the given type
      *         and authority, the code is empty, or the code is followed by parameters.
      */
-    public static String codeOf(final String type, final String authority, final String uri) {
+    public static String codeOf(final String type, final String authority, final CharSequence uri) {
         ensureNonNull("type",      type);
         ensureNonNull("authority", authority);
-        ensureNonNull("uri",       uri);
-        /*
-         * Get the part before the first ':' character. If none, assume that the given URI is already the code.
-         * Otherwise the part may be either "http" or "urn" protocol, or the given authority (typically "EPSG").
-         * In the latter case, we return immediately the code after the authority part.
-         */
-        int upper = uri.indexOf(SEPARATOR);
-        if (upper < 0) {
-            return trimWhitespaces(uri);
-        }
-        int lower  = skipLeadingWhitespaces(uri, 0, upper);
-        int length = skipTrailingWhitespaces(uri, lower, upper) - lower;
-        if (length == authority.length() && uri.regionMatches(true, lower, authority, 0, length)) {
-            return codeIgnoreVersion(uri, upper+1);
-        }
-        /*
-         * Check for supported protocols: only "urn" and "http" at this time.
-         * All other protocols are rejected as unrecognized.
-         */
-        String part;
-        switch (length) {
-            case 3:  part = "urn";  break;
-            case 4:  part = "http"; break;
-            default: return null;
-        }
-        if (!uri.regionMatches(true, lower, part, 0, length)) {
-            return null;
-        }
-        if (length == 4) {
-            return codeForGML(type, authority, uri, upper+1, null);
-        }
-        /*
-         * At this point we have determined that the protocol is URN. The next parts after "urn"
-         * shall be "ogc" or "x-ogc", then "def", then the type and authority given in arguments.
-         */
-        for (int p=0; p!=4; p++) {
-            lower = upper + 1;
-            upper = uri.indexOf(SEPARATOR, lower);
-            if (upper < 0) {
-                return null;                                                    // No more parts.
+        final int length = uri.length();
+        int s = indexOf(uri, SEPARATOR, 0, length);
+        if (s >= 0) {
+            int from = skipLeadingWhitespaces(uri, 0, s);           // Start of authority part.
+            if (skipTrailingWhitespaces(uri, from, s) - from == authority.length()
+                      && CharSequences.regionMatches(uri, from, authority, true))
+            {
+                from = skipLeadingWhitespaces(uri, s+1, length);    // Start of code part.
+                if (from >= length) {
+                    return null;
+                }
+                /*
+                 * The substring is expected to contains zero or one more separator character.
+                 * If present, then the separator character and everything before it are ignored.
+                 * The ignored part should be the version number, but this is not verified.
+                 */
+                s = indexOf(uri, SEPARATOR, from, length);
+                if (s >= 0) {
+                    from = skipLeadingWhitespaces(uri, s+1, length);
+                    if (from >= length || indexOf(uri, SEPARATOR, from, length) >= 0) {
+                        /*
+                         * If the remaining substring contains more ':' characters, then it means that
+                         * the code has parameters, e.g. "urn:ogc:def:crs:OGC:1.3:AUTO42003:1:-100:45".
+                         */
+                        return null;
+                    }
+                }
+                return uri.subSequence(from, skipTrailingWhitespaces(uri, from, length)).toString();
             }
-            switch (p) {
-                // "ogc" is tested before "x-ogc" because more common.
-                case 0: if (regionMatches("ogc", uri, lower, upper)) continue;
-                        part = "x-ogc";   break;       // Fallback if the part is not "ogc".
-                case 1: part = "def";     break;
-                case 2: part = type;      break;
-                case 3: part = authority; break;
-                default: throw new AssertionError(p);
-            }
-            if (!regionMatches(part, uri, lower, upper)) {
-                return null;
+            final DefinitionURI def = parse(uri.toString());
+            if (def != null && def.parameters == null) {
+                if (type.equalsIgnoreCase(def.type) && authority.equalsIgnoreCase(def.authority)) {
+                    String code = def.code;
+                    if (code == null) {
+                        code = def.version;     // May happen with for example "EPSG:4326" instead of "EPSG::4326".
+                    }
+                    return code;
+                }
             }
         }
-        return codeIgnoreVersion(uri, upper+1);
+        return null;
     }
 
     /**
@@ -632,13 +586,13 @@
                 return null;
             }
             // TODO: For now do nothing since PATHS is a singleton. However if a future SIS version
-            //       defines more PATHS entries, then we should replace here the 'paths' reference by
+            //       defines more PATHS entries, then we should replace here the `paths` reference by
             //       a new Collections.singletonMap containing only the entry of interest.
         }
         for (final Map.Entry<String,String> entry : paths.entrySet()) {
             final String path = entry.getValue();
             if (url.regionMatches(true, lower, path, 0, path.length())) {
-                lower = CharSequences.skipLeadingWhitespaces(url, lower + path.length(), url.length());
+                lower = skipLeadingWhitespaces(url, lower + path.length(), url.length());
                 if (authority == null) {
                     authority = url.substring(lower, skipIdentifierPart(url, lower));
                 } else if (!url.regionMatches(true, lower, authority, 0, authority.length())) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DoubleDouble.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DoubleDouble.java
index 792ecb4..4b483d6 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DoubleDouble.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DoubleDouble.java
@@ -88,7 +88,7 @@
 
     /**
      * When computing <var>a</var> - <var>b</var> as a double-double (106 significand bits) value,
-     * if the amount of non-zero significand bits is equals or lower than {@code ZERO_THRESHOLD+1},
+     * if the amount of non-zero significand bits is equal or lower than {@code ZERO_THRESHOLD+1},
      * consider the result as zero.
      */
     private static final int ZERO_THRESHOLD = 2;
@@ -382,9 +382,9 @@
     }
 
     /**
-     * Returns {@code true} if this {@code DoubleDouble} is equals to zero.
+     * Returns {@code true} if this {@code DoubleDouble} is equal to zero.
      *
-     * @return {@code true} if this {@code DoubleDouble} is equals to zero.
+     * @return {@code true} if this {@code DoubleDouble} is equal to zero.
      */
     public boolean isZero() {
         return value == 0 && error == 0;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java
index be3fcda..620ef2b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/SetOfUnknownSize.java
@@ -68,8 +68,8 @@
 
     /**
      * Returns the number of elements in this set. The default implementation counts the number of elements
-     * returned by the {@link #iterator() iterator}. Subclasses are encouraged to cache this value if they
-     * know that the underlying storage is immutable.
+     * returned by the {@linkplain #iterator() iterator}. Subclasses are encouraged to cache this value
+     * if they know that the underlying storage is immutable.
      *
      * @return the number of elements in this set.
      */
@@ -181,7 +181,7 @@
          * iterate over the elements of this Set. The reason is that this Set may compute the values dynamically and
          * it is sometime difficult to ensure that this Set's iterator is fully consistent with the values recognized
          * by the contains(Object) method. For example the iterator may return "EPSG:4326" while the contains(Object)
-         * method may accept both "EPSG:4326" and "EPSG:4326". For this equals(Object) method, we consider the
+         * method may accept both "EPSG:4326" and "EPSG:4326". For this equal(Object) method, we consider the
          * contains(Object) method of the other Set as more reliable.
          */
         if (object == this) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java
deleted file mode 100644
index 01544a8..0000000
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.util;
-
-import java.util.List;
-import java.util.ArrayList;
-import org.apache.sis.util.Static;
-import org.apache.sis.util.resources.Errors;
-
-import static org.apache.sis.util.CharSequences.*;
-import static org.apache.sis.internal.util.DefinitionURI.regionMatches;
-
-
-/**
- * Utility methods related to x-paths. This is intended to be only a lightweight support;
- * this is not a replacement for {@link javax.xml.xpath} implementations. This is used as
- * a place where to centralize XPath handling for possible replacement by a more advanced
- * framework in the future.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since   0.4
- * @module
- */
-public final class XPaths extends Static {
-    /**
-     * The separator between path components.
-     */
-    public static final char SEPARATOR = '/';
-
-    /**
-     * Do not allow instantiation of this class.
-     */
-    private XPaths() {
-    }
-
-    /**
-     * If the given character sequences seems to be a URI, returns the presumed end of that URN.
-     * Otherwise returns -1.
-     * Examples:
-     * <ul>
-     *   <li>{@code "urn:ogc:def:uom:EPSG::9001"}</li>
-     *   <li>{@code "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"}</li>
-     * </ul>
-     *
-     * @param  uri     the URI candidate to verify.
-     * @param  offset  index of the first character to verify.
-     * @return index after the last character of the presumed URI, or -1 if this
-     *         method thinks that the given character sequence is not a URI.
-     */
-    public static int endOfURI(final CharSequence uri, int offset) {
-        boolean isURI = false;
-        int parenthesis = 0;
-        final int length = uri.length();
-scan:   while (offset < length) {
-            final int c = Character.codePointAt(uri, offset);
-            if (!Character.isLetterOrDigit(c)) {
-                switch (c) {
-                    case '#':                                           // Anchor in URL, presumed followed by xpointer.
-                    case ':': isURI |= (parenthesis == 0); break;       // Scheme or URN separator.
-                    case '_':
-                    case '-':                                           // Valid character in URL.
-                    case '%':                                           // Encoded character in URL.
-                    case '.':                                           // Domain name separator in URL.
-                    case SEPARATOR: break;                              // Path separator, but could also be division as in "m/s".
-                    case '(': parenthesis++; break;
-                    case ')': parenthesis--; break;
-                    default: {
-                        if (Character.isSpaceChar(c)) break;            // Not supposed to be valid, but be lenient.
-                        if (parenthesis != 0) break;
-                        break scan;                                     // Non-valid character outside parenthesis.
-                    }
-                }
-            }
-            offset += Character.charCount(c);
-        }
-        return isURI ? offset : -1;
-    }
-
-    /**
-     * Splits the given URL around the {@code '/'} separator, or returns {@code null} if there is no separator.
-     * By convention if the URL is absolute, then the leading {@code '/'} character is kept in the first element.
-     * For example {@code "/∗/property"} is splitted as two elements: {@code "/∗"} and {@code "property"}.
-     *
-     * <p>This method trims the whitespaces of components except the last one (the tip),
-     * for consistency with the case where this method returns {@code null}.</p>
-     *
-     * @param  xpath  the URL to split.
-     * @return the splitted URL with the heading separator kept in the first element, or {@code null}
-     *         if there is no separator. If non-null, the list always contains at least one element.
-     * @throws IllegalArgumentException if the XPath contains at least one empty component.
-     */
-    public static List<String> split(final String xpath) {
-        int next = xpath.indexOf(SEPARATOR);
-        if (next < 0) {
-            return null;
-        }
-        final List<String> components = new ArrayList<>(4);
-        int start = skipLeadingWhitespaces(xpath, 0, next);
-        if (start < next) {
-            // No leading '/' (the characters before it are a path element, added below).
-            components.add(xpath.substring(start, skipTrailingWhitespaces(xpath, start, next)));
-            start = ++next;
-        } else {
-            // Keep the `start` position on the leading '/'.
-            next++;
-        }
-        while ((next = xpath.indexOf(SEPARATOR, next)) >= 0) {
-            components.add(trimWhitespaces(xpath, start, next).toString());
-            start = ++next;
-        }
-        components.add(xpath.substring(start));         // No whitespace trimming.
-        if (components.stream().anyMatch(String::isEmpty)) {
-            throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
-        }
-        return components;
-    }
-
-    /**
-     * Parses a URL which contains a pointer to a XML fragment.
-     * The current implementation recognizes the following types:
-     *
-     * <ul>
-     *   <li>{@code uom} for Unit Of Measurement (example:
-     *       {@code "http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"})</li>
-     * </ul>
-     *
-     * @param  type  the object type.
-     * @param  url   the URL to parse.
-     * @return the reference, or {@code null} if none.
-     */
-    public static String xpointer(final String type, final String url) {
-        if (type.equals("uom")) {
-            final int f = url.indexOf('#');
-            if (f >= 1) {
-                /*
-                 * For now we accept any path as long as it ends with the "gmxUom.xml" file
-                 * because resources may be hosted on different servers, or the path may be
-                 * relative instead of absolute.
-                 */
-                int i = url.lastIndexOf('/', f-1) + 1;
-                if (regionMatches("gmxUom.xml", url, i, f) || regionMatches("ML_gmxUom.xml", url, i, f)) {
-                    /*
-                     * The fragment should typically be of the form "xpointer(//*[@gml:id='m'])".
-                     * However sometime we found no "xpointer", but directly the unit instead.
-                     */
-                    i = url.indexOf('(', f+1);
-                    if (i >= 0 && regionMatches("xpointer", url, f+1, i)) {
-                        i = url.indexOf("@gml:id=", i+1);
-                        if (i >= 0) {
-                            i = skipLeadingWhitespaces(url, i+8, url.length());     // 8 is the length of "@gml:id="
-                            final int c = url.charAt(i);
-                            if (c == '\'' || c == '"') {
-                                final int s = url.indexOf(c, ++i);
-                                if (s >= 0) {
-                                    return trimWhitespaces(url, i, s).toString();
-                                }
-                            }
-                        }
-                    } else {
-                        return trimWhitespaces(url, f+1, url.length()).toString();
-                    }
-                }
-            }
-        }
-        return null;
-    }
-}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java b/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
index 1b5c65a..c2b108f 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
@@ -275,7 +275,7 @@
             return true;
         }
 
-        /** Returns whether this vector in the given range is equals to the specified vector. */
+        /** Returns whether this vector in the given range is equal to the specified vector. */
         @Override boolean equals(final int lower, final int upper, final Vector other, final int otherOffset) {
             if (other instanceof Doubles) {
                 return JDK9.equals(array, lower, upper,
@@ -395,7 +395,7 @@
             return true;
         }
 
-        /** Returns whether this vector in the given range is equals to the specified vector. */
+        /** Returns whether this vector in the given range is equal to the specified vector. */
         @Override final boolean equals(final int lower, final int upper, final Vector other, final int otherOffset) {
             if (other.getClass() == getClass()) {
                 return JDK9.equals(array, lower, upper,
@@ -555,7 +555,7 @@
             return index;
         }
 
-        /** Returns whether this vector in the given range is equals to the specified vector. */
+        /** Returns whether this vector in the given range is equal to the specified vector. */
         @Override final boolean equals(final int lower, final int upper, final Vector other, final int otherOffset) {
             if (other.getClass() == getClass()) {
                 return JDK9.equals(array, lower, upper,
@@ -685,7 +685,7 @@
             return index;
         }
 
-        /** Returns whether this vector in the given range is equals to the specified vector. */
+        /** Returns whether this vector in the given range is equal to the specified vector. */
         @Override final boolean equals(final int lower, final int upper, final Vector other, final int otherOffset) {
             if (other.getClass() == getClass()) {
                 return JDK9.equals(array, lower, upper,
@@ -819,7 +819,7 @@
             return index;
         }
 
-        /** Returns whether this vector in the given range is equals to the specified vector. */
+        /** Returns whether this vector in the given range is equal to the specified vector. */
         @Override final boolean equals(final int lower, final int upper, final Vector other, final int otherOffset) {
             if (other.getClass() == getClass()) {
                 return JDK9.equals(array, lower, upper,
@@ -928,7 +928,7 @@
             return index;
         }
 
-        /** Returns whether this vector in the given range is equals to the specified vector. */
+        /** Returns whether this vector in the given range is equal to the specified vector. */
         @Override final boolean equals(int lower, final int upper, final Vector other, int otherOffset) {
             if (other.getClass() == getClass()) {
                 return JDK9.equals(array, lower, upper,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java b/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java
index 6e59a67..c3479f3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Fraction.java
@@ -92,7 +92,7 @@
      * will be smaller depending on the {@linkplain #denominator} required for representing that value.
      *
      * @param  value  the double-precision value to convert to a fraction.
-     * @return a fraction such as {@link #doubleValue()} is equals to the given value.
+     * @return a fraction such as {@link #doubleValue()} is equal to the given value.
      * @throws IllegalArgumentException if the given value can not be converted to a fraction.
      *
      * @since 1.0
@@ -210,7 +210,7 @@
 
     /**
      * Returns a fraction equivalent to {@code num} / {@code den} after simplification.
-     * If the simplified fraction is equals to {@code this}, then this method returns {@code this}.
+     * If the simplified fraction is equal to {@code this}, then this method returns {@code this}.
      *
      * <p>The arguments given to this method are the results of multiplications and additions of {@code int} values.
      * This method fails if any argument value is {@link Long#MIN_VALUE} because that value can not be made positive.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java b/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
index 1b8ca41..0cdee86 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/MathFunctions.java
@@ -212,7 +212,7 @@
      * the value is negative.
      *
      * @param  value  the value to truncate.
-     * @return the largest in magnitude (further from zero) integer value which is equals
+     * @return the largest in magnitude (further from zero) integer value which is equal
      *         or less in magnitude than the given value.
      */
     public static double truncate(final double value) {
@@ -385,8 +385,8 @@
      *
      * Special cases:
      * <ul>
-     *   <li>If <var>x</var> is equals or lower than -324, then the result is 0.</li>
-     *   <li>If <var>x</var> is equals or greater than 309, then the result is {@linkplain Double#POSITIVE_INFINITY positive infinity}.</li>
+     *   <li>If <var>x</var> is equal or lower than -324, then the result is 0.</li>
+     *   <li>If <var>x</var> is equal or greater than 309, then the result is {@linkplain Double#POSITIVE_INFINITY positive infinity}.</li>
      *   <li>If <var>x</var> is in the [0 … 18] range inclusive, then the result is exact.</li>
      *   <li>For all other <var>x</var> values, the result is the closest IEEE 754 approximation.</li>
      * </ul>
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
index 31556b5..2270ece 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
@@ -1663,7 +1663,7 @@
     }
 
     /**
-     * Returns {@code true} if this vector in the given range is equals to the specified vector.
+     * Returns {@code true} if this vector in the given range is equal to the specified vector.
      * NaN values are considered equal to all other NaN values, and -0.0 is different than +0.0.
      *
      * @param  lower        index of the first value to compare in this vector, inclusive.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java
index 47bb1b6..18e0436 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java
@@ -361,7 +361,7 @@
 
     /**
      * Returns the result of setting the origin of the scale of measurement to the given value.
-     * For example {@code CELSIUS = KELVIN.shift(273.15)} returns a unit where 0°C is equals to 273.15 K.
+     * For example {@code CELSIUS = KELVIN.shift(273.15)} returns a unit where 0°C is equal to 273.15 K.
      *
      * @param  offset  the value to add when converting from the new unit to this unit.
      * @return this unit offset by the specified value, or {@code this} if the given offset is zero.
@@ -385,7 +385,7 @@
 
     /**
      * Returns the result of multiplying this unit by the specified factor.
-     * For example {@code KILOMETRE = METRE.multiply(1000)} returns a unit where 1 km is equals to 1000 m.
+     * For example {@code KILOMETRE = METRE.multiply(1000)} returns a unit where 1 km is equal to 1000 m.
      *
      * @param  multiplier  the scale factor when converting from the new unit to this unit.
      * @return this unit scaled by the specified multiplier.
@@ -413,7 +413,7 @@
 
     /**
      * Returns the result of dividing this unit by an approximate divisor.
-     * For example {@code GRAM = KILOGRAM.divide(1000)} returns a unit where 1 g is equals to 0.001 kg.
+     * For example {@code GRAM = KILOGRAM.divide(1000)} returns a unit where 1 g is equal to 0.001 kg.
      *
      * @param  divisor  the inverse of the scale factor when converting from the new unit to this unit.
      * @return this unit divided by the specified divisor.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
index 3c47d62..f798ca6 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
@@ -42,7 +42,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -218,25 +218,23 @@
         Number v2 = u2.getConverterTo(s2).convert(q2.getValue());
         if (Numbers.isNaN(v2)) return q1;
         if (Numbers.isNaN(v1)) return q2;
-        /*
-         * If the two types are instances of `Scalar`, we can compare them directly. Otherwise convert the
-         * `Scalar` type (if any) to `Double` type, then convert again to the widest type of both values.
-         */
-        final boolean t1 = (v1 instanceof Scalar);
-        final boolean t2 = (v2 instanceof Scalar);
-        if (!(t1 & t2)) {
-            if (t1) v1 = v1.doubleValue();
-            if (t2) v2 = v2.doubleValue();
-            final Class<? extends Number> type = Numbers.widestClass(v1, v2);
-            v1 = Numbers.cast(v1, type);
-            v2 = Numbers.cast(v2, type);
-        }
-        /*
-         * Both v1 and v2 are instance of `Comparable<?>` because `Numbers.widestClass(…)`
-         * accepts only known number types such as `Integer`, `Float`, `BigDecimal`, etc.
-         */
-        @SuppressWarnings("unchecked")
-        final int c = ((Comparable) v1).compareTo((Comparable) v2);
+        final int c = compare(v1, v2);
         return (max ? c >= 0 : c <= 0) ? q1 : q2;
     }
+
+    /**
+     * Compares the two given number, without casting to {@code double} if we can avoid that cast.
+     * The intent is to avoid loosing precision for example by casting a {@code BigDecimal}.
+     */
+    @SuppressWarnings("unchecked")
+    private static int compare(final Number v1, final Number v2) {
+        if (v1 instanceof Comparable<?>) {
+            if (v1.getClass().isInstance(v2)) {
+                return ((Comparable) v1).compareTo(v2);
+            } else if (v2 instanceof Comparable<?> && v2.getClass().isInstance(v1)) {
+                return -((Comparable) v2).compareTo(v1);
+            }
+        }
+        return Double.compare(v1.doubleValue(), v2.doubleValue());
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java
index 9060ccf..8471563 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java
@@ -23,7 +23,13 @@
 import java.text.ParsePosition;
 import javax.measure.Quantity;
 import javax.measure.Unit;
+import javax.measure.format.ParserException;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.util.FinalFieldSetter;
+
+import static java.util.logging.Logger.getLogger;
 
 
 /**
@@ -86,7 +92,7 @@
 
     /**
      * Formats the specified quantity in the given buffer.
-     * The given object shall be an {@link Quantity} instance.
+     * The given object shall be a {@link Quantity} instance.
      *
      * @param  quantity    the quantity to format.
      * @param  toAppendTo  where to format the quantity.
@@ -107,16 +113,22 @@
      *
      * @param  source  the text, part of which should be parsed.
      * @param  pos     index and error index information.
-     * @return a unit parsed from the string, or {@code null} in case of error.
+     * @return a quantity parsed from the string, or {@code null} in case of error.
      */
     @Override
     public Object parseObject(final String source, final ParsePosition pos) {
+        final int start = pos.getIndex();
         final Number value = numberFormat.parse(source, pos);
         if (value != null) {
-            final Unit<?> unit = unitFormat.parse(source, pos);
-            if (unit != null) {
-                return Quantities.create(value.doubleValue(), unit);
+            try {
+                final Unit<?> unit = unitFormat.parse(source, pos);
+                if (unit != null) {
+                    return Quantities.create(value.doubleValue(), unit);
+                }
+            } catch (ParserException e) {
+                Logging.ignorableException(getLogger(Loggers.MEASURE), QuantityFormat.class, "parseObject", e);
             }
+            pos.setIndex(start);        // By `Format.parseObject(…)` method contract.
         }
         return null;
     }
@@ -130,16 +142,10 @@
     public QuantityFormat clone() {
         final QuantityFormat clone = (QuantityFormat) super.clone();
         try {
-            java.lang.reflect.Field field;
-            field = QuantityFormat.class.getField("numberFormat");
-            field.setAccessible(true);
-            field.set(clone, numberFormat.clone());
-
-            field = QuantityFormat.class.getField("unitFormat");
-            field.setAccessible(true);
-            field.set(clone, unitFormat.clone());
+            FinalFieldSetter.set(QuantityFormat.class, "numberFormat", "unitFormat",
+                                 clone, numberFormat.clone(), unitFormat.clone());
         } catch (ReflectiveOperationException e) {
-            throw new AssertionError(e);
+            throw FinalFieldSetter.cloneFailure(e);
         }
         return clone;
     }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
index adde620..6ba1731 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
@@ -665,7 +665,7 @@
      *
      * <ul>
      *   <li>If the range {@linkplain #isEmpty() is empty}, then this method returns "{@code {}}".</li>
-     *   <li>Otherwise if the minimal value is equals to the maximal value, then the string
+     *   <li>Otherwise if the minimal value is equal to the maximal value, then the string
      *       representation of that value is returned inside braces as in "{@code {value}}".</li>
      *   <li>Otherwise the string representation of the minimal and maximal values are formatted
      *       like "{@code [min … max]}" for inclusive endpoints or "{@code (min … max)}" for exclusive
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java
index 3e35eac..a6c4337 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java
@@ -55,7 +55,7 @@
  *
  * <ul>
  *   <li>If the range {@linkplain Range#isEmpty() is empty}, then the range is represented by "{@code {}}".</li>
- *   <li>Otherwise if the {@linkplain Range#getMinValue() minimal value} is equals to the
+ *   <li>Otherwise if the {@linkplain Range#getMinValue() minimal value} is equal to the
  *       {@linkplain Range#getMaxValue() maximal value}, then that single value is formatted
  *       inside braces as in "{@code {value}}".</li>
  *   <li>Otherwise the minimal and maximal values are formatted inside bracket or parenthesis,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/SexagesimalConverter.java b/core/sis-utility/src/main/java/org/apache/sis/measure/SexagesimalConverter.java
index bb87fd2..0e91221 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/SexagesimalConverter.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/SexagesimalConverter.java
@@ -20,7 +20,6 @@
 import javax.measure.quantity.Angle;
 import javax.measure.UnitConverter;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.math.MathFunctions;
 
@@ -40,7 +39,7 @@
  * This class and all inner classes are immutable, and thus inherently thread-safe.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.2
  * @since   0.3
  * @module
  */
@@ -280,7 +279,7 @@
                     if (min >= 0) deg++; else deg--;
                     min = 0;
                 } else {
-                    throw illegalField(angle, min, Vocabulary.Keys.AngularMinutes);
+                    throw illegalField(angle, min, 0);
                 }
             }
             if (sec <= -60 || sec >= 60) {                              // Do not enter for NaN
@@ -288,7 +287,7 @@
                     if (sec >= 0) min++; else min--;
                     sec = 0;
                 } else {
-                    throw illegalField(angle, sec, Vocabulary.Keys.AngularSeconds);
+                    throw illegalField(angle, sec, 1);
                 }
             }
             return (sec/60 + min)/60 + deg;
@@ -299,12 +298,11 @@
          *
          * @param  value  the user-supplied angle value.
          * @param  field  the value of the illegal field.
-         * @param  unit   the vocabulary key for the field (minutes or seconds).
+         * @param  unit   0 for minutes or 1 for seconds.
          * @return the exception to throw.
          */
-        private static IllegalArgumentException illegalField(final double value, final double field, final short unit) {
-            return new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentField_4,
-                    "angle", value, Vocabulary.format(unit), field));
+        private static IllegalArgumentException illegalField(final double value, final double field, final int unit) {
+            return new IllegalArgumentException(Errors.format(Errors.Keys.IllegalSexagesimalField_3, value, unit, field));
         }
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
index d4ddc59..0fdaec9 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/SystemUnit.java
@@ -320,7 +320,7 @@
     }
 
     /**
-     * Returns {@code true} if this unit is equals to the given unit ignoring name, symbol and EPSG code.
+     * Returns {@code true} if this unit is equal to the given unit ignoring name, symbol and EPSG code.
      * This method should always returns {@code true} if parameterized type has not been compromised with
      * raw types or unchecked casts.
      *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
index d5df26e..c9a3020 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
@@ -37,8 +37,6 @@
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.DefinitionURI;
 import org.apache.sis.internal.util.FinalFieldSetter;
-import org.apache.sis.internal.util.XPointer;
-import org.apache.sis.internal.util.XPaths;
 import org.apache.sis.math.Fraction;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.math.MathFunctions;
@@ -60,23 +58,22 @@
  * some symbols found in <cite>Well Known Text</cite> (WKT) definitions or in XML files.
  *
  * <h2>Parsing authority codes</h2>
- * As a special case, if a character sequence given to the {@link #parse(CharSequence)} method is of the
- * {@code "EPSG:####"} or {@code "urn:ogc:def:uom:EPSG::####"} form (ignoring case and whitespaces),
- * then {@code "####"} is parsed as an integer and forwarded to the {@link Units#valueOfEPSG(int)} method.
+ * If a character sequence given to the {@link #parse(CharSequence)} method is of the form {@code "EPSG:####"},
+ * {@code "urn:ogc:def:uom:EPSG::####"} or {@code "http://www.opengis.net/def/uom/EPSG/0/####"} (ignoring case
+ * and whitespaces around path separators), then {@code "####"} is parsed as an integer and forwarded to the
+ * {@link Units#valueOfEPSG(int)} method.
  *
- * <h2>NetCDF unit symbols</h2>
- * The attributes in netCDF files often merge the axis direction with the angular unit,
- * as in {@code "degrees_east"}, {@code "degrees_north"} or {@code "Degrees North"}.
- * This class ignores those suffixes and unconditionally returns {@link Units#DEGREE} for all axis directions.
- * In particular, the units for {@code "degrees_west"} and {@code "degrees_east"} do <strong>not</strong> have
- * opposite sign. It is caller responsibility to handle the direction of axes associated to netCDF units.
+ * <h2>Note on netCDF unit symbols</h2>
+ * In netCDF files, values of "unit" attribute are concatenations of an angular unit with an axis direction,
+ * as in {@code "degrees_east"} or {@code "degrees_north"}. This class ignores those suffixes and unconditionally
+ * returns {@link Units#DEGREE} for all axis directions.
  *
  * <h2>Multi-threading</h2>
  * {@code UnitFormat} is generally not thread-safe. If units need to be parsed or formatted in different threads,
  * each thread should have its own {@code UnitFormat} instance.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see Units#valueOf(String)
  *
@@ -90,6 +87,11 @@
     private static final long serialVersionUID = -3064428584419360693L;
 
     /**
+     * Whether the parsing of authority codes such as {@code "EPSG:9001"} is allowed.
+     */
+    private static final boolean PARSE_AUTHORITY_CODES = true;
+
+    /**
      * The unit name for degrees (not necessarily angular), to be handled in a special way.
      * Must contain only ASCII lower case letters ([a … z]).
      */
@@ -549,7 +551,7 @@
             nameToUnit = map;
         }
         /*
-         * The 'nameToUnit' map contains plural forms (declared in UnitAliases.properties),
+         * The `nameToUnit` map contains plural forms (declared in UnitAliases.properties),
          * but we make a special case for "degrees", "metres" and "meters" because they
          * appear in numerous places.
          */
@@ -677,7 +679,7 @@
          * have been created by SystemUnit.transform(…), in which case "Choice 3" above would have been executed.
          */
         final Unit<?> unscaled = unit.getSystemUnit();
-        @SuppressWarnings("unchecked")          // Both 'unit' and 'unscaled' are 'Unit<Q>'.
+        @SuppressWarnings("unchecked")          // Both `unit` and `unscaled` are `Unit<Q>`.
         final double scale = AbstractConverter.scale(unit.getConverterTo((Unit) unscaled));
         if (Double.isNaN(scale)) {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.NonRatioUnit_1,
@@ -733,7 +735,7 @@
          * Append the scale factor. If we can use a prefix (e.g. "km" instead of "1000⋅m"), we will do that.
          * Otherwise if the scale is a power of 10 and we are allowed to use Unicode symbols, we will write
          * for example 10⁵⋅m instead of 100000⋅m. If the scale is not a power of 10, or if we are requested
-         * to format UCUM symbol, then we fallback on the usual 'Double.toString(double)' representation.
+         * to format UCUM symbol, then we fallback on the usual `Double.toString(double)` representation.
          */
         if (scale != 1) {
             final char prefix = Prefixes.symbol(scale, prefixPower);
@@ -759,9 +761,9 @@
                     toAppendTo.append(text, 0, length);
                 }
                 /*
-                 * The 'formatComponents' method appends division symbol only, no multiplication symbol.
+                 * The `formatComponents` method appends division symbol only, no multiplication symbol.
                  * If we have formatted a scale factor and there is at least one component to multiply,
-                 * we need to append the multiplication symbol ourselves. Note that 'formatComponents'
+                 * we need to append the multiplication symbol ourselves. Note that `formatComponents`
                  * put numerators before denominators, so we are sure that the first term after the
                  * multiplication symbol is a numerator.
                  */
@@ -806,7 +808,7 @@
         /*
          * At this point, all numerators have been appended. Now append the denominators together.
          * For example pressure dimension is formatted as M∕(L⋅T²) no matter if 'M' was the first
-         * dimension in the given 'components' map or not.
+         * dimension in the given `components` map or not.
          */
         if (!deferred.isEmpty()) {
             toAppendTo.append(style.divide);
@@ -1112,22 +1114,14 @@
          */
         int end   = symbols.length();
         int start = CharSequences.skipLeadingWhitespaces(symbols, position.getIndex(), end);
-        int endOfURI = XPaths.endOfURI(symbols, start);
-        if (endOfURI >= 0) {
-            final String uom = symbols.subSequence(start, endOfURI).toString();
-            String code = DefinitionURI.codeOf("uom", Constants.EPSG, uom);
-            /*
-             * DefinitionURI.codeOf(…) returns 'uom' directly (provided that whitespaces were already trimmed)
-             * if no ':' character were found, in which case the string is assumed to be the code directly.
-             * This is the intended behavior for AuthorityFactory, but in the particular case of this method
-             * we want to try to parse as a xpointer before to give up.
-             */
-            if (code != null && code != uom) {
+        if (PARSE_AUTHORITY_CODES) {
+            final String code = DefinitionURI.codeOf("uom", Constants.EPSG, symbols);
+            if (code != null) {
                 NumberFormatException failure = null;
                 try {
                     final Unit<?> unit = Units.valueOfEPSG(Integer.parseInt(code));
                     if (unit != null) {
-                        position.setIndex(endOfURI);
+                        position.setIndex(end);
                         finish(position);
                         return unit;
                     }
@@ -1135,36 +1129,15 @@
                     failure = e;
                 }
                 throw (ParserException) new ParserException(Errors.format(Errors.Keys.UnknownUnit_1,
-                        Constants.EPSG + Constants.DEFAULT_SEPARATOR + code),
-                        symbols, start + Math.max(0, uom.lastIndexOf(code))).initCause(failure);
-            }
-            /*
-             * Not an EPSG code. Maybe it is a URI like this example:
-             * http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])
-             *
-             * If we find such 'uom' value, we could replace 'symbols' by that 'uom'. But it would cause a wrong
-             * error index to be reported in case of parsing failure. We will rather try to adjust the indices
-             * (and replace 'symbols' only in last resort).
-             */
-            code = XPointer.UOM.reference(uom);
-            if (code != null) {
-                final int base = start;
-                start = endOfURI - code.length();
-                do if (--start < base) {          // Should never happen (see above comment), but we are paranoiac.
-                    symbols = code;
-                    start = 0;
-                    break;
-                } while (!CharSequences.regionMatches(symbols, start, code));
-                end = start + code.length();
-            } else {
-                endOfURI = -1;
+                        Constants.EPSG + Constants.DEFAULT_SEPARATOR + code), symbols,
+                        start + Math.max(0, symbols.toString().lastIndexOf(code))).initCause(failure);
             }
         }
         /*
          * Split the unit around the multiplication and division operators and parse each term individually.
          * Note that exponentation need to be kept as part of a single unit symbol.
          *
-         * The 'start' variable is the index of the first character of the next unit term to parse.
+         * The `start` variable is the index of the first character of the next unit term to parse.
          */
         final Operation operation = new Operation(symbols);    // Enumeration value: NOOP, IMPLICIT, MULTIPLY, DIVIDE.
         Unit<?> unit = null;
@@ -1279,7 +1252,7 @@
              * between the previously parsed units and the next unit to parse. A special case is IMPLICIT, which is
              * a multiplication without explicit × symbol after the parenthesis. The implicit multiplication can be
              * overridden by an explicit × or / symbol, which is what happened if we reach this point (tip: look in
-             * the above 'switch' statement all cases that end with 'break', not 'break scan' or 'continue').
+             * the above `switch` statement all cases that end with `break`, not `break scan` or `continue`).
              */
             if (operation.code != Operation.IMPLICIT) {
                 unit = operation.apply(unit, parseTerm(symbols, start, i, operation), start);
@@ -1328,13 +1301,13 @@
             }
         }
         if (!(operation.finished = (component != null))) {
-            component = parseTerm(symbols, start, i, operation);            // May set 'operation.finished' flag.
+            component = parseTerm(symbols, start, i, operation);            // May set `operation.finished` flag.
         }
         if (operation.finished) {
             finish(position);           // For preventing interpretation of "degree minute" as "degree × minute".
         }
         unit = operation.apply(unit, component, start);
-        position.setIndex(endOfURI >= 0 ? endOfURI : i);
+        position.setIndex(i);
         return unit;
     }
 
@@ -1514,7 +1487,7 @@
                                 try {
                                     power = new Fraction(uom.substring(i));
                                 } catch (NumberFormatException e) {
-                                    // Should never happen unless the number is larger than 'int' capacity.
+                                    // Should never happen unless the number is larger than `int` capacity.
                                     throw (ParserException) new ParserException(Errors.format(
                                             Errors.Keys.UnknownUnit_1, uom), symbols, lower+i).initCause(e);
                                 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitServices.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitServices.java
index 386e114..0c615ea 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitServices.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitServices.java
@@ -46,7 +46,7 @@
  * without direct dependency. A {@code UnitServices} instance can be obtained by call to {@link #current()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.8
  * @module
  */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
index 0cad26f..090d7a8 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Units.java
@@ -1701,17 +1701,14 @@
      * and may change in future SIS versions.
      *
      * <h4>Parsing authority codes</h4>
-     * As a special case, if the given {@code uom} arguments is of the form {@code "EPSG:####"}
-     * or {@code "urn:ogc:def:uom:EPSG:####"} (ignoring case and whitespaces), then {@code "####"}
-     * is parsed as an integer and forwarded to the {@link #valueOfEPSG(int)} method.
+     * If the given {@code uom} arguments is of the form {@code "EPSG:####"}, {@code "urn:ogc:def:uom:EPSG:####"}
+     * or {@code "http://www.opengis.net/def/uom/EPSG/0/####"} (ignoring case and whitespaces around separators),
+     * then {@code "####"} is parsed as an integer and forwarded to the {@link #valueOfEPSG(int)} method.
      *
-     * <h4>NetCDF unit symbols</h4>
-     * The attributes in netCDF files often merge the axis direction with the angular unit,
-     * as in {@code "degrees_east"} or {@code "degrees_north"}. This {@code valueOf} method
-     * ignores those suffixes and unconditionally returns {@link #DEGREE} for all axis directions.
-     * In particular, the units for {@code "degrees_west"} and {@code "degrees_east"}
-     * do <strong>not</strong> have opposite sign.
-     * It is caller responsibility to handle the direction of axes associated to netCDF units.
+     * <h4>Note on netCDF unit symbols</h4>
+     * In netCDF files, values of "unit" attribute are concatenations of an angular unit with an axis direction,
+     * as in {@code "degrees_east"} or {@code "degrees_north"}. This {@code valueOf(…)} method ignores those suffixes
+     * and unconditionally returns {@link #DEGREE} for all axis directions.
      *
      * @param  uom  the symbol to parse, or {@code null}.
      * @return the parsed symbol, or {@code null} if {@code uom} was null.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
index 4240e1b..10f6d75 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
@@ -173,7 +173,7 @@
 
     /**
      * Makes sure that an array is non-null and non-empty. If the given {@code array} is null,
-     * then a {@link NullArgumentException} is thrown. Otherwise if the array length is equals
+     * then a {@link NullArgumentException} is thrown. Otherwise if the array length is equal
      * to 0, then an {@link IllegalArgumentException} is thrown.
      *
      * @param  name   the name of the argument to be checked. Used only if an exception is thrown.
@@ -331,7 +331,7 @@
     }
 
     /**
-     * Ensures that the given index is equals or greater than zero and lower than the given
+     * Ensures that the given index is equal or greater than zero and lower than the given
      * upper value. This method is designed for methods that expect an index value as the only
      * argument. For this reason, this method does not take the argument name.
      *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java b/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
index 80006a0..da114b3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
@@ -518,7 +518,7 @@
      * Returns the index of the first non-white character in the given range.
      * If the given range contains only space characters, then this method returns the index of the
      * first character after the given range, which is always equals or greater than {@code toIndex}.
-     * Note that this character may not exist if {@code toIndex} is equals to the text length.
+     * Note that this character may not exist if {@code toIndex} is equal to the text length.
      *
      * <p>Special cases:</p>
      * <ul>
@@ -2165,7 +2165,7 @@
      *
      * <p>This method is similar to {@link String#replace(CharSequence, CharSequence)} except that is accepts
      * arbitrary {@code CharSequence} objects. As of Java 10, another difference is that this method does not
-     * create a new {@code String} if {@code toSearch} is equals to {@code replaceBy}.</p>
+     * create a new {@code String} if {@code toSearch} is equal to {@code replaceBy}.</p>
      *
      * @param  text       the character sequence in which to perform the replacements, or {@code null}.
      * @param  toSearch   the string to replace.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/Emptiable.java b/core/sis-utility/src/main/java/org/apache/sis/util/Emptiable.java
index 6d3171e..a377a31 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/Emptiable.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/Emptiable.java
@@ -23,7 +23,7 @@
  * Some examples of emptiable classes are:
  *
  * <ul>
- *   <li>{@link org.apache.sis.measure.Range} when the lower bounds is equals to the upper bounds and at least
+ *   <li>{@link org.apache.sis.measure.Range} when the lower bounds is equal to the upper bounds and at least
  *       one bound is exclusive.</li>
  *   <li>{@link org.apache.sis.metadata.AbstractMetadata} when no property value has been given to the metadata,
  *       or all properties are themselves empty.</li>
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/RangeSet.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/RangeSet.java
index ad0b995..ce7df4d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/RangeSet.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/RangeSet.java
@@ -1402,7 +1402,7 @@
                 return -1;
             }
         } else if (!((index & 1) == 0 ? isMinIncluded : isMaxIncluded)) {
-            // The value is equals to an excluded endpoint.
+            // The value is equal to an excluded endpoint.
             return -1;
         }
         index /= 2;             // Round toward 0 (odd index are maximum values).
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java
index 8d71f62..3665317 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java
@@ -242,9 +242,9 @@
          *
          * <ul>
          *   <li>The given object is also a {@code Node}.</li>
-         *   <li>The list returned by {@link TreeTable#getColumns()} is equals for both nodes.</li>
+         *   <li>The list returned by {@link TreeTable#getColumns()} is equal for both nodes.</li>
          *   <li>The objects returned by {@link #getValue(TableColumn)} are equal for each column.</li>
-         *   <li>The list returned by {@linkplain #getChildren() children} is equals for both node.</li>
+         *   <li>The list returned by {@linkplain #getChildren() children} is equal for both node.</li>
          * </ul>
          *
          * The node returned by {@link #getParent()} shall <strong>not</strong> be taken in account.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakHashSet.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakHashSet.java
index fec31f2..a518ae3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakHashSet.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakHashSet.java
@@ -45,7 +45,7 @@
  * <h2>Optimizing memory use in factory implementations</h2>
  * The {@code WeakHashSet} class has a {@link #get(Object)} method that is not part of the
  * {@link java.util.Set} interface. This {@code get} method retrieves an entry from this set
- * that is equals to the supplied object. The {@link #unique(Object)} method combines a
+ * that is equal to the supplied object. The {@link #unique(Object)} method combines a
  * {@code get} followed by a {@code add} operation if the specified object was not in the set.
  * This is similar in spirit to the {@link String#intern()} method. The following example shows
  * a convenient way to use {@code WeakHashSet} as an internal pool of immutable objects:
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index 739c255..de7a003 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@ -340,12 +340,6 @@
         public static final short IllegalArgumentClass_3 = 43;
 
         /**
-         * Argument ‘{0}’ can not take the “{1}” value, because the ‘{2}’ field can not take the “{3}”
-         * value.
-         */
-        public static final short IllegalArgumentField_4 = 44;
-
-        /**
          * Argument ‘{0}’ can not take the “{1}” value.
          */
         public static final short IllegalArgumentValue_2 = 45;
@@ -436,6 +430,12 @@
         public static final short IllegalRange_2 = 60;
 
         /**
+         * Sexagesimal angle {0,number} is illegal because the {1,choice,0#minutes|1#seconds} field can
+         * not take the {2,number} value.
+         */
+        public static final short IllegalSexagesimalField_3 = 44;
+
+        /**
          * Value {1} for “{0}” is not a valid Unicode code point.
          */
         public static final short IllegalUnicodeCodePoint_2 = 61;
@@ -477,11 +477,6 @@
         public static final short InconsistentAttribute_2 = 67;
 
         /**
-         * Expected “{0}” namespace for “{1}”.
-         */
-        public static final short InconsistentNamespace_2 = 68;
-
-        /**
          * Inconsistent table columns.
          */
         public static final short InconsistentTableColumns = 69;
@@ -897,6 +892,11 @@
         public static final short UnexpectedFileFormat_2 = 139;
 
         /**
+         * The “{1}” name is not valid in this context, because the “{0}” namespace was expected.
+         */
+        public static final short UnexpectedNamespace_2 = 68;
+
+        /**
          * Parameter “{0}” was not expected.
          */
         public static final short UnexpectedParameter_1 = 140;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index c0b3168..ec0853a 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@ -79,7 +79,6 @@
 ForbiddenProperty_1               = Property \u201c{0}\u201d is not allowed.
 IllegalArgumentClass_2            = Argument \u2018{0}\u2019 can not be an instance of \u2018{1}\u2019.
 IllegalArgumentClass_3            = Argument \u2018{0}\u2019 can not be an instance of \u2018{2}\u2019. Expected an instance of \u2018{1}\u2019 or derived type.
-IllegalArgumentField_4            = Argument \u2018{0}\u2019 can not take the \u201c{1}\u201d value, because the \u2018{2}\u2019 field can not take the \u201c{3}\u201d value.
 IllegalArgumentValue_2            = Argument \u2018{0}\u2019 can not take the \u201c{1}\u201d value.
 IllegalBitsPattern_1              = Illegal bits pattern: {0}.
 IllegalCharacter_2                = The \u201c{1}\u201d character can not be used for \u201c{0}\u201d.
@@ -98,6 +97,7 @@
 IllegalPropertyValueClass_2       = Property \u201c{0}\u201d does not accept instances of \u2018{1}\u2019.
 IllegalPropertyValueClass_3       = Expected an instance of \u2018{1}\u2019 for the \u201c{0}\u201d property, but got an instance of \u2018{2}\u2019.
 IllegalRange_2                    = Range [{0} \u2026 {1}] is not valid.
+IllegalSexagesimalField_3         = Sexagesimal angle {0,number} is illegal because the {1,choice,0#minutes|1#seconds} field can not take the {2,number} value.
 IllegalUnicodeCodePoint_2         = Value {1} for \u201c{0}\u201d is not a valid Unicode code point.
 IllegalValueForProperty_2         = Illegal value for property \u201c{1}\u201d in \u201c{0}\u201d.
 IncompatibleFormat_2              = Can not use the {1} format with \u201c{0}\u201d.
@@ -106,7 +106,6 @@
 IncompatibleUnits_2               = Units \u201c{0}\u201d and \u201c{1}\u201d are incompatible.
 IncompatibleUnitDimension_5       = The \u201c{0}\u201d unit of measurement has dimension of \u2018{1}\u2019 ({2}). It is incompatible with dimension of \u2018{3}\u2019 ({4}).
 InconsistentAttribute_2           = Value \u201c{1}\u201d of attribute \u2018{0}\u2019 is inconsistent with other attributes.
-InconsistentNamespace_2           = Expected \u201c{0}\u201d namespace for \u201c{1}\u201d.
 InconsistentTableColumns          = Inconsistent table columns.
 InconsistentUnitsForCS_1          = Unit of measurement \u201c{0}\u201d is inconsistent with coordinate system axes.
 IndexOutOfBounds_1                = Index {0} is out of bounds.
@@ -190,6 +189,7 @@
 UnexpectedEndOfFile_1             = Unexpected end of file while reading \u201c{0}\u201d.
 UnexpectedEndOfString_1           = More characters were expected at the end of \u201c{0}\u201d.
 UnexpectedFileFormat_2            = File \u201c{1}\u201d seems to be encoded in an other format than {0}.
+UnexpectedNamespace_2             = The \u201c{1}\u201d name is not valid in this context, because the \u201c{0}\u201d namespace was expected.
 UnexpectedParameter_1             = Parameter \u201c{0}\u201d was not expected.
 UnexpectedProperty_2              = Property \u201c{1}\u201d was not expected in \u201c{0}\u201d.
 UnexpectedScaleFactorForUnit_2    = Unexpected scale factor {1,number} for unit of measurement \u201c{0}\u201d.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index b93ab77..305e55d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@ -76,7 +76,6 @@
 ForbiddenProperty_1               = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019est pas autoris\u00e9e.
 IllegalArgumentClass_2            = L\u2019argument \u2018{0}\u2019 ne peut pas \u00eatre de type \u2018{1}\u2019.
 IllegalArgumentClass_3            = L\u2019argument \u2018{0}\u2019 ne peut pas \u00eatre de type \u2018{2}\u2019. Une instance de \u2018{1}\u2019 ou d\u2019un type d\u00e9riv\u00e9 \u00e9tait attendue.
-IllegalArgumentField_4            = L\u2019argument \u2018{0}\u2019 n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb parce que le champs \u2018{2}\u2019 ne peut pas prendre la valeur \u00ab\u202f{3}\u202f\u00bb.
 IllegalArgumentValue_2            = L\u2019argument \u2018{0}\u2019 n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb.
 IllegalBitsPattern_1              = Pattern de bits invalide\u00a0: {0}.
 IllegalClass_2                    = La classe \u2018{1}\u2019 est ill\u00e9gale. Il doit s\u2019agir d\u2019une classe \u2018{0}\u2019 ou d\u00e9riv\u00e9e.
@@ -95,6 +94,7 @@
 IllegalPropertyValueClass_2       = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019accepte pas les valeurs de type \u2018{1}\u2019.
 IllegalPropertyValueClass_3       = Une instance \u2018{1}\u2019 \u00e9tait attendue pour la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb, mais la valeur donn\u00e9e est une instance de \u2018{2}\u2019.
 IllegalRange_2                    = La plage [{0} \u2026 {1}] n\u2019est pas valide.
+IllegalSexagesimalField_3         = L\u2019angle sexag\u00e9simal {0,number} est invalide parce que le champs des {1,choice,0#minutes|1#secondes} ne peut pas prendre la valeur {2,number}.
 IllegalUnicodeCodePoint_2         = La valeur {1} de \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un code Unicode valide.
 IllegalValueForProperty_2         = Valeur ill\u00e9gale pour la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb dans \u00ab\u202f{0}\u202f\u00bb.
 IncompatibleFormat_2              = Le format {1} ne s\u2019applique pas \u00e0 \u00ab\u202f{0}\u202f\u00bb.
@@ -103,7 +103,6 @@
 IncompatibleUnits_2               = Les unit\u00e9s \u00ab\u202f{0}\u202f\u00bb et \u00ab\u202f{1}\u202f\u00bb ne sont pas compatibles.
 IncompatibleUnitDimension_5       = L\u2019unit\u00e9 de mesure \u00ab\u202f{0}\u202f\u00bb a la dimension de \u2018{1}\u2019 ({2}). Elle est incompatible avec la dimension de \u2018{3}\u2019 ({4}).
 InconsistentAttribute_2           = La valeur \u00ab\u202f{1}\u202f\u00bb de l\u2019attribut \u2018{0}\u2019 n\u2019est pas coh\u00e9rente avec celles des autres attributs.
-InconsistentNamespace_2           = L\u2019espace de nom \u201c{0}\u201d \u00e9tait attendu pour \u201c{1}\u201d.
 InconsistentTableColumns          = Les colonnes des tables ne sont pas coh\u00e9rentes.
 InconsistentUnitsForCS_1          = L\u2019unit\u00e9 de mesure \u00ab\u202f{0}\u202f\u00bb n\u2019est pas coh\u00e9rente avec les axes du syst\u00e8me de coordonn\u00e9es.
 IndexOutOfBounds_1                = L\u2019index {0} est en dehors des limites permises.
@@ -186,6 +185,7 @@
 UnexpectedEndOfFile_1             = Fin de fichier inattendue lors de la lecture de \u00ab\u202f{0}\u202f\u00bb.
 UnexpectedEndOfString_1           = D\u2019autres caract\u00e8res \u00e9taient attendus \u00e0 la fin du texte \u00ab\u202f{0}\u202f\u00bb.
 UnexpectedFileFormat_2            = Le fichier \u00ab\u202f{1}\u202f\u00bb semble \u00eatre encod\u00e9 dans un autre format que {0}.
+UnexpectedNamespace_2             = Le nom \u201c{1}\u201d n\u2019est pas valide dans ce contexte, parce que l\u2019espace de nom \u201c{0}\u201d \u00e9tait attendu.
 UnexpectedParameter_1             = Le param\u00e8tre \u00ab\u202f{0}\u202f\u00bb est inattendu.
 UnexpectedProperty_2              = La propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb est inattendue dans \u00ab\u202f{0}\u202f\u00bb.
 UnexpectedScaleFactorForUnit_2    = Le facteur d\u2019\u00e9chelle {1,number} est inattendu pour l\u2019unit\u00e9 de mesure \u00ab\u202f{0}\u202f\u00bb.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index f7f2fe3..2bbe399 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -1155,6 +1155,11 @@
         public static final short SpatialRepresentation = 184;
 
         /**
+         * Sphere
+         */
+        public static final short Sphere = 271;
+
+        /**
          * Standard deviation
          */
         public static final short StandardDeviation = 185;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index 1ad786a..21efb2b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -234,6 +234,7 @@
 Source                  = Source
 SouthBound              = South bound
 SpatialRepresentation   = Spatial representation
+Sphere                  = Sphere
 StandardDeviation       = Standard deviation
 StartDate               = Start date
 StartPoint              = Start point
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 4bff86e..39059bf 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -241,6 +241,7 @@
 Source                  = Source
 SouthBound              = Limite sud
 SpatialRepresentation   = Repr\u00e9sentation spatiale
+Sphere                  = Sph\u00e8re
 StandardDeviation       = \u00c9cart type
 StartDate               = Date de d\u00e9part
 StartPoint              = Point de d\u00e9part
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/DefinitionURITest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/DefinitionURITest.java
index 22965fa..c4facb3 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/DefinitionURITest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/DefinitionURITest.java
@@ -27,7 +27,7 @@
  * Tests {@link DefinitionURI}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.4
  * @module
  */
@@ -188,11 +188,12 @@
     }
 
     /**
-     * Tests {@link DefinitionURI#codeOf(String, String, String)} with URI like {@code "EPSG:4326"}.
+     * Tests {@link DefinitionURI#codeOf(String, String, String)}
+     * with URI like {@code "EPSG:4326"}.
      */
     @Test
     public void testCodeOfEPSG() {
-        assertEquals("4326", DefinitionURI.codeOf("crs", "EPSG", "4326"));
+        assertNull  (        DefinitionURI.codeOf("crs", "EPSG", "4326"));
         assertEquals("4326", DefinitionURI.codeOf("crs", "EPSG", "EPSG:4326"));
         assertEquals("4326", DefinitionURI.codeOf("crs", "EPSG", "EPSG::4326"));
         assertNull  (        DefinitionURI.codeOf("crs", "EPSG", "EPSG:::4326"));
@@ -203,8 +204,8 @@
     }
 
     /**
-     * Tests {@link DefinitionURI#codeOf(String, String, String)} with URN like
-     * {@code "urn:ogc:def:crs:EPSG::4326"}.
+     * Tests {@link DefinitionURI#codeOf(String, String, String)}
+     * with URN like {@code "urn:ogc:def:crs:EPSG::4326"}.
      */
     @Test
     public void testCodeOfURN() {
@@ -214,16 +215,26 @@
         assertEquals("4326",  DefinitionURI.codeOf("crs", "EPSG", "urn:x-ogc:def:crs:EPSG::4326"));
         assertNull  (         DefinitionURI.codeOf("crs", "EPSG", "urn:n-ogc:def:crs:EPSG::4326"));
         assertEquals("4326",  DefinitionURI.codeOf("crs", "EPSG", " urn : ogc : def : crs : epsg : : 4326"));
-        assertNull  (         DefinitionURI.codeOf("crs", "EPSG", "urn:ogc:def:uom:EPSG:9102"));
-        assertEquals("9102",  DefinitionURI.codeOf("uom", "EPSG", "urn:ogc:def:uom:EPSG:9102"));
+        assertNull  (         DefinitionURI.codeOf("crs", "EPSG", "urn:ogc:def:uom:EPSG::9102"));
+        assertEquals("9102",  DefinitionURI.codeOf("uom", "EPSG", "urn:ogc:def:uom:EPSG::9102"));
         assertNull  (         DefinitionURI.codeOf("crs", "EPSG", "urn:ogc:def:crs:OGC:1.3:CRS84"));
         assertEquals("CRS84", DefinitionURI.codeOf("crs", "OGC",  "urn:ogc:def:crs:OGC:1.3:CRS84"));
         assertNull  (         DefinitionURI.codeOf("crs", "OGC",  "urn:ogc:def:crs:OGC:1.3:AUTO42003:1:-100:45"));
     }
 
     /**
-     * Tests {@link DefinitionURI#codeOf(String, String, String)} with URL like
-     * {@code "http://www.opengis.net/gml/srs/epsg.xml#4326"}.
+     * Tests {@link DefinitionURI#codeOf(String, String, String)}
+     * with URL like {@code "http://www.opengis.net/def/crs/EPSG/0/4326"}.
+     */
+    @Test
+    public void testCodeOfDefinitionServer() {
+        assertEquals("4326", DefinitionURI.codeOf("crs", "EPSG", "http://www.opengis.net/def/crs/EPSG/0/4326"));
+        assertEquals("9102", DefinitionURI.codeOf("uom", "EPSG", "http://www.opengis.net/def/uom/EPSG/0/9102"));
+    }
+
+    /**
+     * Tests {@link DefinitionURI#codeOf(String, String, String)}
+     * with URL like {@code "http://www.opengis.net/gml/srs/epsg.xml#4326"}.
      */
     @Test
     public void testCodeOfGML() {
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java
deleted file mode 100644
index fe27718..0000000
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.util;
-
-import org.apache.sis.util.Characters;
-import org.apache.sis.test.TestCase;
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-
-/**
- * Tests {@link XPaths}.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since   0.4
- * @module
- */
-public final strictfp class XPathsTest extends TestCase {
-    /**
-     * Tests the {@link XPaths#endOfURI(CharSequence, int)} method.
-     */
-    @Test
-    public void testEndOfURI() {
-        assertEquals(26, XPaths.endOfURI("urn:ogc:def:uom:EPSG::9001", 0));
-        assertEquals(80, XPaths.endOfURI("http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])", 0));
-        assertEquals(97, XPaths.endOfURI("http://schemas.opengis.net/iso/19139/20070417/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])", 0));
-        assertEquals(-1, XPaths.endOfURI("m/s", 0));
-        assertEquals(-1, XPaths.endOfURI("m.s", 0));
-        assertEquals(11, XPaths.endOfURI("EPSG" + Characters.NO_BREAK_SPACE + ": 9001", 0));
-    }
-
-    /**
-     * Tests {@link XPaths#split(String)}.
-     */
-    @Test
-    public void testSplit() {
-        assertNull(XPaths.split("property"));
-        assertArrayEquals(new String[] {"/property"},                    XPaths.split("/property").toArray());
-        assertArrayEquals(new String[] {"Feature", "property", "child"}, XPaths.split("Feature/property/child").toArray());
-        assertArrayEquals(new String[] {"/Feature", "property"},         XPaths.split("/Feature/property").toArray());
-    }
-}
diff --git a/core/sis-utility/src/test/java/org/apache/sis/io/AppenderTestCase.java b/core/sis-utility/src/test/java/org/apache/sis/io/AppenderTestCase.java
index 46a8d8c..9b5cbc8 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/io/AppenderTestCase.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/io/AppenderTestCase.java
@@ -67,7 +67,7 @@
     abstract void run(final String lineSeparator) throws IOException;
 
     /**
-     * Ensures that the buffer content is equals to the given string.
+     * Ensures that the buffer content is equal to the given string.
      *
      * @param  expected  the expected content.
      * @throws IOException should never happen since the tests will write in a buffer.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/SexagesimalConverterTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/SexagesimalConverterTest.java
index 041dfd5..fa5d2f3 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/SexagesimalConverterTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/SexagesimalConverterTest.java
@@ -30,7 +30,7 @@
  * Test the {@link SexagesimalConverter} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -93,6 +93,22 @@
     }
 
     /**
+     * Tests the error message on attempt to convert an illegal value.
+     */
+    @Test
+    public void testErrorMessage() {
+        final UnitConverter converter = DMS.getConverterTo(Units.DEGREE);
+        assertEquals(10.5, converter.convert(10.3), STRICT);
+        try {
+            converter.convert(10.7);
+            fail("Conversion of illegal value should not be allowed.");
+        } catch (IllegalArgumentException e) {
+            final String message = e.getMessage();
+            assertNotNull(message);     // Can not test message content because it is locale-sensitive.
+        }
+    }
+
+    /**
      * Tests the fix for rounding error in conversion of 46°57'8.66".
      * This fix is necessary for avoiding a 4 cm error with Apache SIS
      * construction of EPSG:2056 projected CRS.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
index e5a88a3..3e6e0e2 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java
@@ -39,7 +39,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -428,9 +428,9 @@
     @Test
     public void testParseEPSG() {
         final UnitFormat f = new UnitFormat(Locale.UK);
+        assertSame(Units.METRE,             f.parse("EPSG:9001"));
         assertSame(Units.METRE,             f.parse("urn:ogc:def:uom:EPSG::9001"));
         assertSame(Units.METRES_PER_SECOND, f.parse("urn:ogc:def:uom:EPSG::1026"));
-        assertSame(Units.METRE, f.parse("http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"));
     }
 
     /**
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java
index 81134e3..4554079 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/UnitsTest.java
@@ -35,15 +35,14 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
 @DependsOn({
     UnitFormatTest.class,
     SexagesimalConverterTest.class,
-    org.apache.sis.internal.util.DefinitionURITest.class,
-    org.apache.sis.internal.util.XPointerTest.class
+    org.apache.sis.internal.util.DefinitionURITest.class
 })
 public final strictfp class UnitsTest extends TestCase {
     /**
@@ -365,19 +364,23 @@
     }
 
     /**
+     * Tests {@link Units#valueOf(String)} with a URN syntax.
+     */
+    @Test
+    public void testValueOfURN() {
+        assertSame(METRE,  valueOf("EPSG:9001"));
+        assertSame(DEGREE, valueOf(" epsg : 9102"));
+        assertSame(DEGREE, valueOf("urn:ogc:def:uom:EPSG::9102"));
+    }
+
+    /**
      * Tests {@link Units#valueOfEPSG(int)} and {@link Units#valueOf(String)} with a {@code "EPSG:####"} syntax.
      */
     @Test
     public void testValueOfEPSG() {
-        assertSame(METRE,  valueOfEPSG(9001));
-        assertSame(DEGREE, valueOfEPSG(9102));              // Used in prime meridian and operation parameters.
-        assertSame(DEGREE, valueOfEPSG(9122));              // Used in coordinate system axes.
-        assertSame(METRE,  valueOf("EPSG:9001"));
-        assertSame(DEGREE, valueOf(" epsg : 9102"));
-        assertSame(DEGREE, valueOf("urn:ogc:def:uom:EPSG::9102"));
-        assertSame(METRE,  valueOf("http://www.isotc211.org/2005/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])"));
-        assertSame(METRE,  valueOf("gmxUom.xml#m"));
-
+        assertSame(METRE,          valueOfEPSG(9001));
+        assertSame(DEGREE,         valueOfEPSG(9102));      // Used in prime meridian and operation parameters.
+        assertSame(DEGREE,         valueOfEPSG(9122));      // Used in coordinate system axes.
         assertSame(TROPICAL_YEAR,  valueOfEPSG(1029));
         assertSame(SECOND,         valueOfEPSG(1040));
         assertSame(FOOT,           valueOfEPSG(9002));
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java b/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java
index 9a1b6d9..fc4fb73 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java
@@ -311,7 +311,7 @@
 
     /**
      * Serializes the given object in memory, deserializes it and ensures that the deserialized
-     * object is equals to the original one. This method does not write anything to the disk.
+     * object is equal to the original one. This method does not write anything to the disk.
      *
      * <p>If the serialization fails, then this method throws an {@link AssertionError}
      * as do the other JUnit assertion methods.</p>
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
index 4e81d87..bb83fbe 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java
@@ -25,7 +25,7 @@
  * All tests from the {@code sis-utility} module, in rough dependency order.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -87,8 +87,6 @@
 
     // GeoAPI most basic types.
     org.apache.sis.internal.util.DefinitionURITest.class,
-    org.apache.sis.internal.util.XPathsTest.class,
-    org.apache.sis.internal.util.XPointerTest.class,
     org.apache.sis.util.SimpleInternationalStringTest.class,
     org.apache.sis.util.DefaultInternationalStringTest.class,
     org.apache.sis.internal.util.LocalizedParseExceptionTest.class,
diff --git a/pom.xml b/pom.xml
index 1ec987c..39d7e26 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,7 +39,7 @@
   <parent>
     <groupId>org.apache</groupId>
     <artifactId>apache</artifactId>
-    <version>26</version>
+    <version>27</version>
   </parent>
 
 
@@ -444,7 +444,7 @@
       <dependency>
         <groupId>org.locationtech.jts</groupId>
         <artifactId>jts-core</artifactId>
-        <version>1.18.2</version>
+        <version>1.19.0</version>
         <optional>true</optional>
       </dependency>
       <dependency>
@@ -492,19 +492,19 @@
       <dependency>
         <groupId>org.hsqldb</groupId>
         <artifactId>hsqldb</artifactId>
-        <version>2.6.1</version>
+        <version>2.7.0</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>com.h2database</groupId>
         <artifactId>h2</artifactId>
-        <version>2.1.212</version>
+        <version>2.1.214</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>org.postgresql</groupId>
         <artifactId>postgresql</artifactId>
-        <version>42.3.5</version>
+        <version>42.4.1</version>
         <scope>test</scope>
       </dependency>
 
@@ -621,7 +621,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-enforcer-plugin</artifactId>
-        <version>3.0.0</version>
+        <version>3.1.0</version>
         <executions>
           <execution>
             <id>enforce</id>
@@ -683,7 +683,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
-        <version>3.0.0-M6</version>
+        <version>3.0.0-M7</version>
         <configuration>
           <trimStackTrace>false</trimStackTrace>
           <includes>
@@ -737,7 +737,7 @@
       <!-- Set "*-source-release.zip" filename prefix to "sis-*" instead of "parent-*" -->
       <plugin>
         <artifactId>maven-assembly-plugin</artifactId>
-        <version>3.3.0</version>
+        <version>3.4.2</version>
         <executions>
           <execution>
             <id>source-release-assembly</id>
@@ -968,7 +968,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-deploy-plugin</artifactId>
-          <version>3.0.0-M2</version>
+          <version>3.0.0</version>
         </plugin>
       </plugins>
     </pluginManagement>
diff --git a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index e81c9ef..8ec93cd 100644
--- a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -37,7 +37,7 @@
 import org.apache.sis.internal.netcdf.VariableRole;
 import org.apache.sis.internal.netcdf.Linearizer;
 import org.apache.sis.internal.netcdf.Node;
-import org.apache.sis.internal.referencing.provider.Sinusoidal;
+import org.apache.sis.internal.referencing.provider.PseudoSinusoidal;
 import org.apache.sis.internal.referencing.provider.Equirectangular;
 import org.apache.sis.internal.referencing.provider.PolarStereographicA;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
@@ -117,7 +117,7 @@
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="http://global.jaxa.jp/projects/sat/gcom_c/">SHIKISAI (GCOM-C) on JAXA</a>
  * @see <a href="https://en.wikipedia.org/wiki/Global_Change_Observation_Mission">GCOM on Wikipedia</a>
@@ -365,7 +365,7 @@
         final int s = name.indexOf(' ');
         final String code = (s >= 0) ? name.substring(0, s) : name;
         if (code.equalsIgnoreCase("EQA")) {
-            method = Sinusoidal.NAME;
+            method = PseudoSinusoidal.NAME;
         } else if (code.equalsIgnoreCase("EQR")) {
             method = Equirectangular.NAME;
         } else if (code.equalsIgnoreCase("PS")) {
diff --git a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/package-info.java b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/package-info.java
index 1381a2a..e0f420d 100644
--- a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/package-info.java
+++ b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/package-info.java
@@ -21,7 +21,7 @@
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.0
  * @module
  */
diff --git a/src/main/javadoc/sis.css b/src/main/javadoc/sis.css
index e9f2278..cc771d6 100644
--- a/src/main/javadoc/sis.css
+++ b/src/main/javadoc/sis.css
@@ -262,37 +262,3 @@
 :not(td) > div.block {
   text-align: justify;
 }
-
-
-
-/* ----------------------------------------------------------------------
- * End of SIS-specific part. The remaining of this file overwrite some
- * aspects of the Javadoc default stylesheet.
- */
-body, div.block {
-  color:       black;
-  font-family: Helvetica, Arial, sans-serif;
-}
-
-.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd {
-  color:       #353833;
-  font-family: Helvetica, Arial, sans-serif;
-  margin-left: 40px;
-}
-
-pre, div.block pre, code, tt, dt code, table tr td dt code {
-  font-family: Andale Mono, Courier New, monospace;
-}
-
-div.block pre, code, tt, dt code, table tr td dt code {
-  line-height: 1em;
-  font-size:   1em;
-}
-
-/* Appareance of links in the "Description" column of class and package summaries.
- * JDK style uses bold characters for the left column, which contains the class and
- * package names. But we do not want those bold characters to apply to the descriptions.
- */
-td.colLast div.block a:link {
-  font-weight: normal;
-}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 90c53e2..6995585 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -56,7 +56,9 @@
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.iso.DefaultNameSpace;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -65,6 +67,7 @@
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Thi Phuong Hao Nguyen (VNSC)
+ * @author  Alexis Manin (Geomatys)
  * @version 1.3
  * @since   0.8
  * @module
@@ -466,6 +469,7 @@
 
     /**
      * Returns the image at the given index. Images numbering starts at 1.
+     * If the given string has a scope (e.g. "filename:1"), then the scope
      *
      * @param  sequence  string representation of the image index, starting at 1.
      * @return image at the given index.
@@ -473,22 +477,50 @@
      */
     @Override
     public synchronized GridCoverageResource findResource(final String sequence) throws DataStoreException {
-        Exception cause;
-        int index;
-        try {
-            index = Integer.parseInt(sequence);
-            cause = null;
-        } catch (NumberFormatException e) {
-            index = 0;
-            cause = e;
-        }
-        if (index > 0) try {
-            GridCoverageResource image = reader().getImage(index - 1);
+        ArgumentChecks.ensureNonNull("sequence", sequence);
+        final int index = parseImageIndex(sequence);
+        if (index >= 0) try {
+            final GridCoverageResource image = reader().getImage(index - 1);
             if (image != null) return image;
         } catch (IOException e) {
             throw errorIO(e);
         }
-        throw new IllegalNameException(StoreUtilities.resourceNotFound(this, sequence), cause);
+        throw new IllegalNameException(StoreUtilities.resourceNotFound(this, sequence));
+    }
+
+    /**
+     * Validates input resource name and extracts the image index it should contain.
+     * The resource name may be of the form "1" or "filename:1". We verify that:
+     *
+     * <ul>
+     *   <li>Input tip (last name part) is a parsable integer.</li>
+     *   <li>If input provides more than a tip, all test before the tip matches this datastore namespace
+     *       (should be the name of the Geotiff file without its extension).</li>
+     * </ul>
+     *
+     * @param  sequence  a string representing the name of a resource present in this datastore.
+     * @return the index of the Geotiff image matching the requested resource.
+     *         There is no verification that the returned index is valid.
+     * @throws IllegalNameException if the argument use an invalid namespace or if the tip is not an integer.
+     */
+    private int parseImageIndex(String sequence) throws IllegalNameException {
+        final NameSpace namespace = namespace();
+        final String separator = DefaultNameSpace.getSeparator(namespace, false);
+        final int s = sequence.lastIndexOf(separator);
+        if (s >= 0) {
+            if (namespace != null) {
+                final String expected = namespace.name().toString();
+                if (!sequence.substring(0, s).equals(expected)) {
+                    throw new IllegalNameException(errors().getString(Errors.Keys.UnexpectedNamespace_2, expected, sequence));
+                }
+            }
+            sequence = sequence.substring(s + separator.length());
+        }
+        try {
+            return Integer.parseInt(sequence);
+        } catch (NumberFormatException e) {
+            throw new IllegalNameException(StoreUtilities.resourceNotFound(this, sequence), e);
+        }
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 001f40e..e5b35f9 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -459,6 +459,13 @@
     }
 
     /**
+     * Returns the image index used in the default identifier.
+     */
+    final String getImageIndex() {
+        return String.valueOf(index + 1);
+    }
+
+    /**
      * Returns the identifier in the namespace of the {@link GeoTiffStore}.
      * The first image has the sequence number "1", optionally customized.
      * If this image is an overview, then its namespace should be the name of the base image
@@ -477,8 +484,8 @@
                     // Should not happen because `setOverviewIdentifier(…)` should have been invoked.
                     return Optional.empty();
                 }
-                final String id = String.valueOf(index + 1);
-                final GenericName name = reader.nameFactory.createLocalName(reader.store.namespace(), id);
+                GenericName name = reader.nameFactory.createLocalName(reader.store.namespace(), getImageIndex());
+                name = name.toFullyQualifiedName();     // Because "1" alone is not very informative.
                 identifier = reader.store.customizer.customize(index, name);
                 if (identifier == null) identifier = name;
             }
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
index 8870d00..157a2d8 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
@@ -39,7 +39,7 @@
  * discard them (which save a little bit of space) when no longer needed.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -170,7 +170,11 @@
      * @throws DataStoreException if an error occurred while reading metadata from the data store.
      */
     void finish(final ImageFileDirectory image, final StoreListeners listeners) throws DataStoreException {
-        image.getIdentifier().ifPresent((id) -> addTitle(id.toString()));
+        image.getIdentifier().ifPresent((id) -> {
+            if (!image.getImageIndex().equals(id.tip().toString())) {
+                addTitle(id.toString());
+            }
+        });
         /*
          * Add information about the file format.
          *
diff --git a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java
index bd9e820..48ad7fb 100644
--- a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java
+++ b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/SelfConsistencyTest.java
@@ -16,15 +16,21 @@
  */
 package org.apache.sis.storage.geotiff;
 
+import java.util.List;
 import java.util.Optional;
 import java.nio.file.Path;
+import org.opengis.util.GenericName;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.test.OptionalTestData;
 import org.apache.sis.test.storage.CoverageReadConsistency;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
+import org.junit.Test;
 
+import static org.junit.Assert.*;
 import static org.junit.Assume.assumeNotNull;
 
 
@@ -36,7 +42,8 @@
  * a subset of data.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @author  Alexis Manin (Geomatys)
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -82,4 +89,32 @@
     public SelfConsistencyTest() throws DataStoreException {
         super(store.components().iterator().next());
     }
+
+    /**
+     * Verifies that {@link GeoTiffStore#findResource(String)} returns the resource when using
+     * either the full name or only its tip.
+     *
+     * @throws DataStoreException if an error occurred while reading the file.
+     */
+    @Test
+    public void findResourceByName() throws DataStoreException {
+        final List<GridCoverageResource> datasets = store.components();
+        assertFalse(datasets.isEmpty());
+        for (GridCoverageResource dataset : datasets) {
+            final GenericName name = dataset.getIdentifier()
+                    .orElseThrow(() -> new AssertionError("A component of the GeoTiff datastore is unnamed"));
+            GridCoverageResource foundResource = store.findResource(name.toString());
+            assertSame(dataset, foundResource);
+            foundResource = store.findResource(name.tip().toString());
+            assertSame(dataset, foundResource);
+        }
+        try {
+            final GridCoverageResource r = store.findResource("a_wrong_namespace:1");
+            fail("No dataset should be returned when user specify the wrong namespace. However, the datastore returned " + r);
+        } catch (IllegalNameException e) {
+            // Expected behaviour.
+            final String message = e.getMessage();
+            assertTrue(message, message.contains("a_wrong_namespace:1"));
+        }
+    }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
index 275c40b..5e73914 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -446,6 +446,7 @@
                         if (range.getMinDouble() >= 0 && range.getMaxDouble() > axis.getMaximumValue()) {
                             referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem)
                                                 .forConvention(AxesConvention.POSITIVE_RANGE);
+                            coordinateSystem = null;
                             break;
                         }
                     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java
new file mode 100644
index 0000000..80c0153
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf;
+
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
+import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.util.Utilities;
+
+
+/**
+ * Merges the CRS declared in grid mapping attributes with the CRS inferred from coordinate variables.
+ * The former (called "explicit CRS") may have map projection parameters that are difficult to infer
+ * from the coordinate variables. The latter (called "implicit CRS") have better name and all required
+ * dimensions, while the explicit CRS often has only the horizontal dimensions.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+final class CRSMerger extends GeodeticObjectBuilder {
+    /**
+     * Creates a new builder for the given netCDF reader.
+     */
+    CRSMerger(final Decoder decoder) {
+        super(decoder, decoder.listeners.getLocale());
+    }
+
+    /**
+     * Replaces the component starting at given index by the given component, possibly with adjusted longitude range.
+     * The implicit CRS has been inferred from coordinate variables, while the explicit CRS has been inferred from
+     * the grid mapping attributes. The explicit CRS is the one to use, but its longitude range is the default one
+     * because thar range depends on the coordinate variable, which was inspected by the implicit CRS.
+     *
+     * @param  implicit        the coordinate reference system in which to replace a component.
+     * @param  firstDimension  index of the first dimension to replace.
+     * @param  explicit        the component to insert in place of the CRS component at given index.
+     * @return a CRS with the component replaced.
+     * @throws FactoryException if the object creation failed.
+     */
+    @Override
+    public CoordinateReferenceSystem replaceComponent(final CoordinateReferenceSystem implicit,
+            final int firstDimension, CoordinateReferenceSystem explicit) throws FactoryException
+    {
+        final CoordinateSystem cs = implicit.getCoordinateSystem();
+        if (cs instanceof EllipsoidalCS) {
+            final int i = AxisDirections.indexOfColinear(cs, AxisDirection.EAST);
+            if (i >= 0 && cs.getAxis(i).getMinimumValue() >= 0) {       // The `i >= 0` check is paranoiac.
+                explicit = AbstractCRS.castOrCopy(explicit).forConvention(AxesConvention.POSITIVE_RANGE);
+            }
+        }
+        final CoordinateReferenceSystem result = super.replaceComponent(implicit, firstDimension, explicit);
+        return Utilities.equalsIgnoreMetadata(implicit, result) ? implicit : result;
+    }
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
index f14865a..67ea796 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
@@ -459,6 +459,10 @@
      *     <td>{@link Number} or {@code double[]}</td>
      *     <td>Map projection parameter values</td>
      *   </tr><tr>
+     *     <td>{@value CF#SEMI_MAJOR_AXIS} and {@value CF#SEMI_MINOR_AXIS}</td>
+     *     <td>{@link Number}</td>
+     *     <td>Ellipsoid axis lengths.</td>
+     *   </tr><tr>
      *     <td>{@value #TOWGS84}</td>
      *     <td>{@link BursaWolfParameters}</td>
      *     <td>Datum shift information.</td>
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index 71af070..0438456 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -48,7 +48,7 @@
  * if a variable dimensions should considered as bands instead of spatiotemporal dimensions.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see Decoder#getGridCandidates()
  *
@@ -144,21 +144,18 @@
      * This is the number of dimensions of the <em>grid</em>.
      * It should be equal to the size of {@link #getDimensions()} list.
      *
+     * <h4>Note on target dimensions</h4>
+     * A {@code getTargetDimensions()} method would return the number of dimensions of the
+     * <em>coordinate reference system</em>, which is the target of the <cite>"grid to CRS"</cite> conversion.
+     * However we do not provide that method because, while it should be equal to {@code getAxes(decoder).length},
+     * it sometime differs because {@link #getAxes(Decoder)} may exclude axis with zero dimensions.
+     * The latter method should be used as the authoritative one.
+     *
      * @return number of grid dimensions.
      */
     public abstract int getSourceDimensions();
 
     /**
-     * Returns the number of dimensions of target coordinates in the <cite>"grid to CRS"</cite> conversion.
-     * This is the number of dimensions of the <em>coordinate reference system</em>.
-     * It should be equal to the length of the array returned by {@link #getAxes(Decoder)},
-     * but caller should be robust to inconsistencies.
-     *
-     * @return number of CRS dimensions.
-     */
-    public abstract int getTargetDimensions();
-
-    /**
      * Returns the dimensions of this grid, in netCDF (reverse of "natural") order. Each element in the list
      * contains the number of cells in the dimension, together with implementation-specific information.
      * The list length should be equal to {@link #getSourceDimensions()}.
@@ -182,10 +179,8 @@
     protected abstract List<Dimension> getDimensions();
 
     /**
-     * Returns the axes of the coordinate reference system. The size of this array is expected equals to the
-     * value returned by {@link #getTargetDimensions()}, but the caller should be robust to inconsistencies.
-     * The axis order is CRS order (reverse of netCDF order) for consistency with the common practice in the
-     * {@code "coordinates"} attribute.
+     * Returns the axes of the coordinate reference system. The axis order is CRS order (reverse of netCDF order)
+     * for consistency with the common practice in the {@code "coordinates"} attribute.
      *
      * <p>This method returns a direct reference to the cached array; do not modify.</p>
      *
@@ -397,9 +392,9 @@
              * (the source) +1, and the number of rows is the number of dimensions in the CRS (the target) +1.
              * The order of dimensions in the transform is the reverse of the netCDF dimension order.
              */
-            int lastSrcDim = getSourceDimensions();                         // Will be decremented later, then kept final.
-            int lastTgtDim = getTargetDimensions();
-            final int[] deferred = new int[axes.length];                    // Indices of axes that have been deferred.
+            int lastSrcDim = getSourceDimensions();         // Will be decremented later, then kept final.
+            int lastTgtDim = axes.length;                   // Should be `getTargetDimensions()` but some axes may have been excluded.
+            final int[] deferred = new int[axes.length];    // Indices of axes that have been deferred.
             final List<MathTransform> nonLinears = new ArrayList<>(axes.length);
             final Matrix affine = Matrices.createZero(lastTgtDim + 1, lastSrcDim + 1);
             affine.setElement(lastTgtDim--, lastSrcDim--, 1);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
index 54c9149..aedeccc 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
@@ -21,9 +21,15 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Collections;
+import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.function.Supplier;
+import java.text.NumberFormat;
+import java.text.FieldPosition;
 import java.text.ParseException;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterValueGroup;
@@ -58,7 +64,6 @@
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
 import org.apache.sis.internal.referencing.provider.PseudoPlateCarree;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Constants;
@@ -80,7 +85,7 @@
  * which creates Coordinate Reference Systems by inspecting coordinate system axes.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
  *
@@ -89,8 +94,9 @@
  */
 final class GridMapping {
     /**
-     * The Coordinate Reference System, or {@code null} if none. This CRS can be constructed from Well Known Text
-     * or EPSG codes declared in {@code "spatial_ref"}, {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes.
+     * The Coordinate Reference System inferred from grid mapping attribute values, or {@code null} if none.
+     * This CRS may have been constructed from Well Known Text or EPSG codes declared in {@code "spatial_ref"},
+     * {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes.
      *
      * <div class="note"><b>Note:</b> this come from different information than the one used by {@link CRSBuilder},
      * which creates CRS by inspection of coordinate system axes.</div>
@@ -104,21 +110,27 @@
     private final MathTransform gridToCRS;
 
     /**
-     * Whether the {@link #crs} where defined by an EPSG code.
+     * Whether the {@link #crs} was defined by a WKT string.
      */
-    private final boolean isEPSG;
+    private final boolean isWKT;
 
     /**
      * Creates an instance for the given {@link #crs} and {@link #gridToCRS} values.
+     *
+     * @param  crs        CRS inferred from grid mapping attribute values, or {@code null} if none.
+     * @param  gridToCRS  transform from GDAL conventions, or {@code null} if none.
+     * @param  isWKT      wether the {@code crs} was defined by a WKT string.
      */
-    private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isEPSG) {
+    private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isWKT) {
         this.crs       = crs;
         this.gridToCRS = gridToCRS;
-        this.isEPSG    = isEPSG;
+        this.isWKT     = isWKT;
     }
 
     /**
      * Fetches grid geometry information from attributes associated to the given variable.
+     * This method should be invoked only once per variable, but may return a shared {@code GridMapping} instance
+     * for all variables because there is typically only one set of grid mapping attributes for the whole file.
      *
      * @param  variable  the variable for which to create a grid geometry.
      */
@@ -302,25 +314,49 @@
         final PrimeMeridian meridian;
         if (greenwichLongitude instanceof Number) {
             final double longitude = ((Number) greenwichLongitude).doubleValue();
-            final Map<String,?> properties = properties(definition, Convention.PRIME_MERIDIAN_NAME, null);
+            final Map<String,?> properties = properties(definition,
+                    Convention.PRIME_MERIDIAN_NAME, (longitude == 0) ? "Greenwich" : null);
             meridian = datumFactory.createPrimeMeridian(properties, longitude, Units.DEGREE);
             isSpecified = true;
         } else {
             meridian = defaultDefinitions.primeMeridian();
         }
         /*
-         * Ellipsoid built from "semi_major_axis", "semi_minor_axis", etc.
+         * Ellipsoid built from "semi_major_axis" and "semi_minor_axis" parameters. Note that it is okay
+         * to use the OGC name (e.g. "semi_major") instead of the netCDF name (e.g. ""semi_major_axis").
+         * The Apache SIS implementation of parameter value group understands the aliases. Using the OGC
+         * names is safer because they should be understood by most map projection implementations.
          */
         Ellipsoid ellipsoid;
         try {
-            final double semiMajor = parameters.parameter(Constants.SEMI_MAJOR).doubleValue();
-            final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, null);
-            if (parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue()) {
-                final double ivf = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
-                ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, ivf, Units.METRE);
+            final ParameterValue<?> p = parameters.parameter(Constants.SEMI_MAJOR);
+            final Unit<Length> axisUnit = p.getUnit().asType(Length.class);
+            final double  semiMajor = p.doubleValue();
+            final double  secondDefiningParameter;
+            final boolean isSphere;
+            final boolean isIvfDefinitive = parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue();
+            if (isIvfDefinitive) {
+                secondDefiningParameter = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
+                isSphere = (secondDefiningParameter == 0) || Double.isInfinite(secondDefiningParameter);
             } else {
-                final double semiMinor = parameters.parameter(Constants.SEMI_MINOR).doubleValue();
-                ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, semiMinor, Units.METRE);
+                secondDefiningParameter = parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit);
+                isSphere = secondDefiningParameter == semiMajor;
+            }
+            final Supplier<Object> fallback = () -> {           // Default ellipsoid name if not specified.
+                final Locale  locale = decoder.listeners.getLocale();
+                final NumberFormat f = NumberFormat.getNumberInstance(locale);
+                f.setMaximumFractionDigits(5);      // Centimetric precision.
+                final double km = axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor);
+                final StringBuffer b = new StringBuffer()
+                        .append(Vocabulary.getResources(locale).getString(isSphere ? Vocabulary.Keys.Sphere : Vocabulary.Keys.Ellipsoid))
+                        .append(isSphere ? " R=" : " a=");
+                return f.format(km, b, new FieldPosition(0)).append(" km").toString();
+            };
+            final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, fallback);
+            if (isIvfDefinitive) {
+                ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, secondDefiningParameter, axisUnit);
+            } else {
+                ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, secondDefiningParameter, axisUnit);
             }
             isSpecified = true;
         } catch (ParameterNotFoundException | IllegalStateException e) {
@@ -368,13 +404,15 @@
     private static Map<String,Object> properties(final Map<String,Object> definition, final String nameAttribute, final Object fallback) {
         Object name = definition.remove(nameAttribute);
         if (name == null) {
-            if (fallback instanceof IdentifiedObject) {
-                name = ((IdentifiedObject) fallback).getName();
-            } else if (fallback != null) {
-                name = fallback.toString();
-            } else {
-                name = Vocabulary.format(Vocabulary.Keys.Unnamed);
+            if (fallback == null) {
                 // Note: IdentifiedObject.name does not accept InternationalString.
+                name = Vocabulary.format(Vocabulary.Keys.Unnamed);
+            } else if (fallback instanceof IdentifiedObject) {
+                name = ((IdentifiedObject) fallback).getName();
+            } else if (fallback instanceof Supplier<?>) {
+                name = ((Supplier<?>) fallback).get();
+            } else {
+                name = fallback.toString();
             }
         }
         return Collections.singletonMap(IdentifiedObject.NAME_KEY, name);
@@ -421,7 +459,7 @@
         } catch (ParseException | NumberFormatException e) {
             canNotCreate(mapping, message, e);
         }
-        return new GridMapping(crs, gridToCRS, false);
+        return new GridMapping(crs, gridToCRS, wkt != null);
     }
 
     /**
@@ -441,14 +479,13 @@
      * @return whether this method found grid geometry attributes.
      */
     private static GridMapping parseNonStandard(final Node variable) {
-        boolean isEPSG = false;
         String code = variable.getAttributeAsString("ESRI_pe_string");
-        if (code == null) {
+        final boolean isWKT = (code != null);
+        if (!isWKT) {
             code = variable.getAttributeAsString("EPSG_code");
             if (code == null) {
                 return null;
             }
-            isEPSG = true;
         }
         /*
          * The Coordinate Reference System stored in those attributes often use the GeoTIFF flavor of EPSG codes,
@@ -459,16 +496,16 @@
          */
         CoordinateReferenceSystem crs;
         try {
-            if (isEPSG) {
-                crs = CRS.forCode(Constants.EPSG + ':' + isEPSG);
-            } else {
+            if (isWKT) {
                 crs = createFromWKT(variable, code);
+            } else {
+                crs = CRS.forCode(Constants.EPSG + ':' + code);
             }
         } catch (FactoryException | ParseException | ClassCastException e) {
             canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e);
             crs = null;
         }
-        return new GridMapping(crs, null, isEPSG);
+        return new GridMapping(crs, null, isWKT);
     }
 
     /**
@@ -509,10 +546,12 @@
     }
 
     /**
-     * Creates a new grid geometry for the given extent.
-     * This method should be invoked only when no existing {@link GridGeometry} can be used as template.
+     * Creates a new grid geometry with the extent of the given variable and a potentially null CRS.
+     * This method should be invoked only as a fallback when no existing {@link GridGeometry} can be used.
+     * The CRS and "grid to CRS" transform are null, unless some partial information was found for example
+     * as WKT string.
      */
-    GridGeometry createGridCRS(final Variable variable) {
+    final GridGeometry createGridCRS(final Variable variable) {
         final List<Dimension> dimensions = variable.getGridDimensions();
         final long[] upper = new long[dimensions.size()];
         for (int i=0; i<upper.length; i++) {
@@ -523,42 +562,48 @@
     }
 
     /**
-     * Creates the grid geometry from the {@link #crs} and {@link #gridToCRS} field,
-     * completing missing information with the given template.
+     * Creates the grid geometry from the {@link #crs} and {@link #gridToCRS} fields,
+     * completing missing information with the implicit grid geometry derived from coordinate variables.
+     * For example {@code GridMapping} may contain information only about the horizontal dimensions, so
+     * the given {@code implicit} geometry is used for completing with vertical and temporal dimensions.
      *
      * @param  variable  the variable for which to create a grid geometry.
-     * @param  template  template to use for completing missing information.
+     * @param  implicit  template to use for completing missing information.
      * @param  anchor    whether we computed "grid to CRS" transform relative to pixel center or pixel corner.
      * @return the grid geometry with modified CRS and "grid to CRS" transform, or {@code null} in case of failure.
      */
-    GridGeometry adaptGridCRS(final Variable variable, final GridGeometry template, final PixelInCell anchor) {
-        CoordinateReferenceSystem givenCRS = crs;
+    final GridGeometry adaptGridCRS(final Variable variable, final GridGeometry implicit, final PixelInCell anchor) {
+        /*
+         * The CRS and grid geometry built from grid mapping attributes are called "explicit" in this method.
+         * This is by contrast with CRS derived from coordinate variables, which is only implicit.
+         */
+        CoordinateReferenceSystem explicitCRS = crs;
         int firstAffectedCoordinate = 0;
         boolean isSameGrid = true;
-        if (template.isDefined(GridGeometry.CRS)) {
-            final CoordinateReferenceSystem templateCRS = template.getCoordinateReferenceSystem();
-            if (givenCRS == null) {
-                givenCRS = templateCRS;
+        if (implicit.isDefined(GridGeometry.CRS)) {
+            final CoordinateReferenceSystem implicitCRS = implicit.getCoordinateReferenceSystem();
+            if (explicitCRS == null) {
+                explicitCRS = implicitCRS;
             } else {
                 /*
-                 * The CRS built by Grid may have a different axis order than the CRS specified by grid mapping attributes.
-                 * Check which axis order seems to fit, then replace grid CRS by given CRS (potentially with swapped axes).
-                 * This is where the potential difference between EPSG axis order and grid axis order is handled. If we can
-                 * not find where to substitute the CRS, assume that the given CRS describes the first dimensions. We have
-                 * no guarantees that this later assumption is right, but it seems to match common practice.
+                 * The CRS built by the `Grid` class (based on an inspection of coordinate variables)
+                 * may have a different axis order than the CRS specified by grid mapping attributes
+                 * (the CRS built by this class). This block checks which axis order seems to fit,
+                 * then potentially replaces `Grid` implicit CRS by `GridMapping` explicit CRS.
+                 *
+                 * This is where the potential difference between EPSG axis order and grid axis order is handled.
+                 * If we can not find which component to replace, assume that grid mapping describes the first dimensions.
+                 * We have no guarantees that this latter assumption is right, but it seems to match common practice.
                  */
-                final CoordinateSystem cs = templateCRS.getCoordinateSystem();
-                CoordinateSystem subCS = givenCRS.getCoordinateSystem();
-                firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, subCS);
+                final CoordinateSystem cs = implicitCRS.getCoordinateSystem();
+                firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem());
                 if (firstAffectedCoordinate < 0) {
-                    givenCRS = AbstractCRS.castOrCopy(givenCRS).forConvention(AxesConvention.RIGHT_HANDED);
-                    subCS = givenCRS.getCoordinateSystem();
-                    firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, subCS);
+                    explicitCRS = AbstractCRS.castOrCopy(explicitCRS).forConvention(AxesConvention.RIGHT_HANDED);
+                    firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem());
                     if (firstAffectedCoordinate < 0) {
                         firstAffectedCoordinate = 0;
-                        if (!isEPSG) {
-                            givenCRS = crs;                             // If specified by WKT, use the given CRS verbatim.
-                            subCS = givenCRS.getCoordinateSystem();
+                        if (isWKT && crs != null) {
+                            explicitCRS = crs;                         // If specified by WKT, use the CRS verbatim.
                         }
                     }
                 }
@@ -567,15 +612,15 @@
                  * axis order. If the grid CRS contains more axes (for example elevation or time axis), we try to keep them.
                  */
                 try {
-                    givenCRS = new GeodeticObjectBuilder(variable.decoder, variable.decoder.listeners.getLocale())
-                                                .replaceComponent(templateCRS, firstAffectedCoordinate, givenCRS);
+                    explicitCRS = new CRSMerger(variable.decoder)
+                            .replaceComponent(implicitCRS, firstAffectedCoordinate, explicitCRS);
                 } catch (FactoryException e) {
                     canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e);
                     return null;
                 }
-                isSameGrid = templateCRS.equals(givenCRS);
+                isSameGrid = implicitCRS.equals(explicitCRS);
                 if (isSameGrid) {
-                    givenCRS = templateCRS;                                 // Keep existing instance if appropriate.
+                    explicitCRS = implicitCRS;                                 // Keep existing instance if appropriate.
                 }
             }
         }
@@ -585,31 +630,31 @@
          * then we need to perform selection in target dimensions (not source dimensions) because the first affected
          * coordinate computed above is in CRS dimension, which is the target of "grid to CRS" transform.
          */
-        MathTransform givenG2C = gridToCRS;
-        if (template.isDefined(GridGeometry.GRID_TO_CRS)) {
-            final MathTransform templateG2C = template.getGridToCRS(anchor);
-            if (givenG2C == null) {
-                givenG2C = templateG2C;
+        MathTransform explicitG2C = gridToCRS;
+        if (implicit.isDefined(GridGeometry.GRID_TO_CRS)) {
+            final MathTransform implicitG2C = implicit.getGridToCRS(anchor);
+            if (explicitG2C == null) {
+                explicitG2C = implicitG2C;
             } else try {
                 int count = 0;
                 MathTransform[] components = new MathTransform[3];
-                final TransformSeparator sep = new TransformSeparator(templateG2C, variable.decoder.getMathTransformFactory());
+                final TransformSeparator sep = new TransformSeparator(implicitG2C, variable.decoder.getMathTransformFactory());
                 if (firstAffectedCoordinate != 0) {
                     sep.addTargetDimensionRange(0, firstAffectedCoordinate);
                     components[count++] = sep.separate();
                     sep.clear();
                 }
-                components[count++] = givenG2C;
-                final int next = firstAffectedCoordinate + givenG2C.getTargetDimensions();
-                final int upper = templateG2C.getTargetDimensions();
+                components[count++] = explicitG2C;
+                final int next = firstAffectedCoordinate + explicitG2C.getTargetDimensions();
+                final int upper = implicitG2C.getTargetDimensions();
                 if (next != upper) {
                     sep.addTargetDimensionRange(next, upper);
                     components[count++] = sep.separate();
                 }
                 components = ArraysExt.resize(components, count);
-                givenG2C = MathTransforms.compound(components);
-                if (templateG2C.equals(givenG2C)) {
-                    givenG2C = templateG2C;                                 // Keep using existing instance if appropriate.
+                explicitG2C = MathTransforms.compound(components);
+                if (implicitG2C.equals(explicitG2C)) {
+                    explicitG2C = implicitG2C;                                 // Keep using existing instance if appropriate.
                 } else {
                     isSameGrid = false;
                 }
@@ -623,9 +668,9 @@
          * If any of them have changed, create the new grid geometry.
          */
         if (isSameGrid) {
-            return template;
+            return implicit;
         } else {
-            return new GridGeometry(template.getExtent(), anchor, givenG2C, givenCRS);
+            return new GridGeometry(implicit.getExtent(), anchor, explicitG2C, explicitCRS);
         }
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index 6bbb896..4305b22 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -48,13 +48,15 @@
 import ucar.nc2.constants.CDM;                      // We use only String constants.
 import ucar.nc2.constants.CF;
 
+import static org.apache.sis.internal.storage.StoreUtilities.ALLOW_LAST_RESORT_STATISTICS;
+
 
 /**
  * A netCDF variable created by {@link Decoder}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -851,6 +853,12 @@
         NumberRange<?> range = decoder.convention().validRange(this);
         if (range == null) {
             range = getRangeFallback();
+            if (ALLOW_LAST_RESORT_STATISTICS && range == null) try {
+                range = read().range();
+            } catch (DataStoreException | IOException e) {
+                // It should be a fatal error, but maybe the user wants only metadata.
+                error(Variable.class, "getValidRange", e, Errors.Keys.CanNotRead_1, getName());
+            }
         }
         return range;
     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
index 27c97fd..f41c864 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridInfo.java
@@ -42,7 +42,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -120,16 +120,15 @@
         return domain.length;
     }
 
-    /**
-     * Returns the number of dimensions of target coordinates in the <cite>"grid to CRS"</cite> conversion.
-     * This is the number of dimensions of the <em>coordinate reference system</em>.
-     * It should be equal to the size of the array returned by {@link #getAxes(Decoder)},
-     * but caller should be robust to inconsistencies.
+    /*
+     * A `getTargetDimensions()` method would be like below, but is
+     * excluded because `getAxes(…).length` is the authoritative value:
+     *
+     *     @Override
+     *     public int getTargetDimensions() {
+     *         return range.length;
+     *     }
      */
-    @Override
-    public int getTargetDimensions() {
-        return range.length;
-    }
 
     /**
      * Returns the dimensions of this grid, in netCDF (reverse of "natural") order.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/package-info.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/package-info.java
index 2319110..c44733a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/package-info.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/package-info.java
@@ -30,7 +30,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.3
  * @module
  */
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
index 2d0cde9..2eb9b2a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GridWrapper.java
@@ -45,7 +45,7 @@
  * Many netCDF variables may be associated to the same {@code GridWrapper} instance.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -193,16 +193,15 @@
         return netcdfCS.getRankDomain();
     }
 
-    /**
-     * Returns the number of dimensions of target coordinates in the <cite>"grid to CRS"</cite> conversion.
-     * This is the number of dimensions of the <em>coordinate reference system</em>.
-     * It should be equal to the length of the array returned by {@link #getAxes(Decoder)},
-     * but caller should be robust to inconsistencies.
+    /*
+     * A `getTargetDimensions()` method would be like below, but is
+     * excluded because `getAxes(…).length` is the authoritative value:
+     *
+     *     @Override
+     *     public int getTargetDimensions() {
+     *         return netcdfCS.getRankRange();
+     *     }
      */
-    @Override
-    public int getTargetDimensions() {
-        return netcdfCS.getRankRange();
-    }
 
     /**
      * Returns the dimensions of this grid, in netCDF (reverse of "natural") order.
@@ -257,9 +256,9 @@
          * In this method, `sourceDim` and `targetDim` are relative to "grid to CRS" conversion.
          * So `sourceDim` is the grid (domain) dimension and `targetDim` is the CRS (range) dimension.
          */
+        int axisCount = 0;
         int targetDim = range.size();
         final Axis[] axes = new Axis[targetDim];
-        final int lastDim = targetDim - 1;
         while (--targetDim >= 0) {
             final CoordinateAxis axis = range.get(targetDim);
             final Variable wrapper = ((DecoderWrapper) decoder).getWrapperFor(axis);
@@ -307,9 +306,11 @@
                  * package, we can proceed as if the dimension does not exist (`i` not incremented).
                  */
             }
-            axes[lastDim - targetDim] = new Axis(abbreviation, axis.getPositive(),
-                    ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), wrapper);
+            if (i != 0) {   // Variables with 0 dimensions sometime happen.
+                axes[axisCount++] = new Axis(abbreviation, axis.getPositive(),
+                        ArraysExt.resize(indices, i), ArraysExt.resize(sizes, i), wrapper);
+            }
         }
-        return axes;
+        return ArraysExt.resize(axes, axisCount);
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index 47a2980..d797390 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -113,7 +113,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -685,17 +685,13 @@
     /**
      * Adds information about axes and cell geometry.
      * This is the {@code <mdb:spatialRepresentationInfo>} element in XML.
+     * We work on grid axes instead of Coordinate Reference System axes because
+     * {@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize} seems to imply that.
      *
      * @param  cs  the grid geometry (related to the netCDF coordinate system).
      * @throws ArithmeticException if the size of an axis exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
      */
-    private void addSpatialRepresentationInfo(final Grid cs) throws IOException, DataStoreException {
-        /*
-         * We work on grid axes instead of Coordinate Reference System axes because
-         * `metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize`
-         * seems to imply that.
-         */
-        final Axis[] axes = cs.getAxes(decoder);
+    private void addSpatialRepresentationInfo(final Axis[] axes) throws IOException, DataStoreException {
         for (int i=0; i<axes.length; i++) {
             final Axis axis = axes[i];
             /*
@@ -1060,10 +1056,11 @@
          * is built from the netCDF CoordinateSystem objects.
          */
         for (final Grid cs : decoder.getGridCandidates()) {
-            if (cs.getSourceDimensions() >= Grid.MIN_DIMENSION &&
-                cs.getTargetDimensions() >= Grid.MIN_DIMENSION)
-            {
-                addSpatialRepresentationInfo(cs);
+            if (cs.getSourceDimensions() >= Grid.MIN_DIMENSION) {
+                final Axis[] axes = cs.getAxes(decoder);
+                if (axes.length >= Grid.MIN_DIMENSION) {
+                    addSpatialRepresentationInfo(axes);
+                }
             }
         }
         setISOStandards(hasGridCoverages);
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
index f9f487d..7f14447 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
@@ -63,7 +63,7 @@
     }
 
     /**
-     * Tests {@link Grid#getSourceDimensions()} and {@link Grid#getTargetDimensions()}.
+     * Tests {@link Grid#getSourceDimensions()} and {@code Grid.getTargetDimensions()}.
      *
      * @throws IOException if an I/O error occurred while opening the file.
      * @throws DataStoreException if a logical error occurred.
@@ -72,12 +72,12 @@
     public void testDimensions() throws IOException, DataStoreException {
         Grid geometry = getSingleton(filter(selectDataset(TestData.NETCDF_2D_GEOGRAPHIC).getGridCandidates()));
         assertEquals("getSourceDimensions()", 2, geometry.getSourceDimensions());
-        assertEquals("getTargetDimensions()", 2, geometry.getTargetDimensions());
+        assertEquals("getTargetDimensions()", 2, geometry.getAxes(decoder()).length);
 
         final int n = includeRuntimeDimension ? 5 : 4;
         geometry = getSingleton(filter(selectDataset(TestData.NETCDF_4D_PROJECTED).getGridCandidates()));
         assertEquals("getSourceDimensions()", 4, geometry.getSourceDimensions());
-        assertEquals("getTargetDimensions()", n, geometry.getTargetDimensions());
+        assertEquals("getTargetDimensions()", n, geometry.getAxes(decoder()).length);
     }
 
     /**
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
index bd2adec..f6d8c9f 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
@@ -213,7 +213,7 @@
     }
 
     /**
-     * Asserts that the textual value of the named attribute is equals to the expected value.
+     * Asserts that the textual value of the named attribute is equal to the expected value.
      * The {@link #selectDataset(TestData)} method must be invoked at least once before this method.
      *
      * @param  expected       the expected attribute value.
@@ -225,7 +225,7 @@
     }
 
     /**
-     * Asserts that the numeric value of the named attribute is equals to the expected value.
+     * Asserts that the numeric value of the named attribute is equal to the expected value.
      * The {@link #selectDataset(TestData)} method must be invoked at least once before this method.
      *
      * @param  expected       the expected attribute value.
@@ -237,7 +237,7 @@
     }
 
     /**
-     * Asserts that the temporal value of the named attribute is equals to the expected value.
+     * Asserts that the temporal value of the named attribute is equal to the expected value.
      * The {@link #selectDataset(TestData)} method must be invoked at least once before this method.
      *
      * @param  expected       the expected attribute value.
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java
index 3ed8762..e084c7b 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/GridInfoTest.java
@@ -34,7 +34,7 @@
  * passed.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -70,7 +70,7 @@
         final Grid[] copy = new Grid[geometries.length];
         int count = 0;
         for (final Grid geometry : geometries) {
-            if (geometry.getSourceDimensions() != 1 || geometry.getTargetDimensions() != 1) {
+            if (geometry.getSourceDimensions() != 1) {
                 copy[count++] = geometry;
             }
         }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
index 3abf514..2c8f587 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
@@ -731,7 +731,7 @@
      */
     protected final void log(final LogRecord record) {
         record.setSourceClassName(SQLStore.class.getName());
-        record.setSourceMethodName("components");                // Main public API trigging the database analysis.
+        record.setSourceMethodName("components");                // Main public API triggering the database analysis.
         record.setLoggerName(Modules.SQL);
         listeners.warning(record);
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
index cd8fd65..eaeb355 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
@@ -62,12 +62,23 @@
  * Some methods may also move in public API if we feel confident enough.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
 public final class StoreUtilities extends Static {
     /**
+     * Whether to allow computation of statistics when no minimum/maximum values can be determined.
+     * This is a costly operation because it requires loading all data, so any code enabled by this
+     * flag should be executed in last resort only.
+     *
+     * <p>This flag can be set to {@code true} for exploring data that we can not visualize otherwise.
+     * But it should generally stay to {@code false}, because otherwise browsing resource metadata can
+     * become as costly (slow and high memory usage) as visualizing the full raster.</p>
+     */
+    public static final boolean ALLOW_LAST_RESORT_STATISTICS = false;
+
+    /**
      * Logger for the {@value Modules#STORAGE} module. This is used when no more specific logger is available,
      * or if the more specific logger is not appropriate (e.g. because the log message come from base class).
      */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java
index 9d19f6f..fca3055 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/MovingFeatureBuilder.java
@@ -229,7 +229,7 @@
             }
             /*
              * At this point we have a non-empty valid sequence of coordinate values. If the first point of current
-             * vector is equals to the last point of previous vector, assume that they form a continuous polyline.
+             * vector is equal to the last point of previous vector, assume that they form a continuous polyline.
              */
             if (previous != null) {
                 if (equals(previous, v, dimension)) {
@@ -280,7 +280,7 @@
     }
 
     /**
-     * Returns {@code true} if the last coordinate of the {@code previous} vector is equals to the first
+     * Returns {@code true} if the last coordinate of the {@code previous} vector is equal to the first
      * coordinate of the {@code next} vector.
      *
      * @param previous   the previous vector.
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 084d67f..adb49da 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
@@ -311,7 +311,7 @@
     /**
      * Moves the reader position to beginning of file, if possible. We try to use the mark defined by the constructor,
      * which is set after the last header line. If the mark is no longer valid, then we have to create a new line reader.
-     * In this later case, we have to skip the header lines (i.e. we reproduce the constructor loop, but without parsing
+     * In this latter case, we have to skip the header lines (i.e. we reproduce the constructor loop, but without parsing
      * metadata).
      *
      * @todo Not yet used. This is planned for a future version of {@link #features(boolean)} method implementation.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
index 8c2f9e5..887d162 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
@@ -26,7 +26,6 @@
 import javax.imageio.ImageReader;
 import javax.imageio.ImageReadParam;
 import javax.imageio.ImageTypeSpecifier;
-import org.opengis.util.LocalName;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.apache.sis.image.ImageProcessor;
@@ -58,7 +57,7 @@
  * A single image in a {@link WorldFileStore}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -89,7 +88,7 @@
      *
      * @see #getIdentifier()
      */
-    private LocalName identifier;
+    private GenericName identifier;
 
     /**
      * The grid geometry of this resource. The grid extent is the image size.
@@ -188,7 +187,7 @@
                 if (store.suffix != null) {
                     filename = IOUtilities.filenameWithoutExtension(filename);
                 }
-                identifier = Names.createLocalName(filename, null, id);
+                identifier = Names.createLocalName(filename, null, id).toFullyQualifiedName();
             }
             return Optional.of(identifier);
         }
@@ -362,7 +361,7 @@
     final void dispose() {
         if (identifier != null) {
             // For information purpose but not really used.
-            store.identifiers.put(identifier.toString(), Boolean.FALSE);
+            store.identifiers.put(identifier.tip().toString(), Boolean.FALSE);
         }
         store            = null;
         identifier       = null;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
index 84244c6..4fd5592 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
@@ -682,7 +682,7 @@
 
     /**
      * Writes fully the buffer content from its position to its limit.
-     * After this method call, the buffer position is equals to its limit.
+     * After this method call, the buffer position is equal to its limit.
      *
      * @throws IOException if an error occurred while writing to the channel.
      */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java
index d07eeac..a110dc7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/package-info.java
@@ -25,7 +25,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.4
  * @module
  */
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
index 1f4c1c5..ac413bf 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
@@ -43,7 +43,7 @@
  * Tests {@link WorldFileStore} and {@link WorldFileStoreProvider}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -126,7 +126,7 @@
             try (WorldFileStore source = provider.open(testData())) {
                 assertFalse(source instanceof WritableStore);
                 final GridCoverageResource resource = getSingleton(source.components());
-                assertEquals("identifier", "1", resource.getIdentifier().get().toString());
+                assertEquals("identifier", "gradient:1", resource.getIdentifier().get().toString());
                 /*
                  * Above `resource` is the content of "gradient.png" file.
                  * Write the resource in a new file using a different format.
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
index 5a42216..6c86ccb 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/ChannelDataOutputTest.java
@@ -113,7 +113,7 @@
     }
 
     /**
-     * Asserts that the content of {@link #testedStream} is equals to the content of {@link #referenceStream}.
+     * Asserts that the content of {@link #testedStream} is equal to the content of {@link #referenceStream}.
      * This method closes the reference stream before to perform the comparison.
      */
     final void assertStreamContentEquals() throws IOException {
diff --git a/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/ReaderTest.java b/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/ReaderTest.java
index e53176b..da119f8 100644
--- a/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/ReaderTest.java
+++ b/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/ReaderTest.java
@@ -122,7 +122,7 @@
     }
 
     /**
-     * Asserts that the string value of {@code actual} is equals to the expected value.
+     * Asserts that the string value of {@code actual} is equal to the expected value.
      *
      * @param  expected  the expected value (can be {@code null}).
      * @param  actual    the actual value (can be {@code null}).