sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Make a bigger effort to use the EPSG definitions for CoordinateSystem with different axis order and for CoordinateReferenceSystem with unknown datum.
Date Sat, 22 Dec 2018 17:19:01 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new b09bc6a  Make a bigger effort to use the EPSG definitions for CoordinateSystem with different axis order and for CoordinateReferenceSystem with unknown datum.
b09bc6a is described below

commit b09bc6a336849c93e2e944b75c6357d8e5d96733
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Dec 22 18:18:10 2018 +0100

    Make a bigger effort to use the EPSG definitions for CoordinateSystem with different axis order and for CoordinateReferenceSystem with unknown datum.
---
 .../referencing/ReferencingFactoryContainer.java   | 102 +++++++++++--
 .../org/apache/sis/referencing/cs/AbstractCS.java  |  70 ++++++++-
 .../java/org/apache/sis/referencing/cs/Codes.java  |  58 +++++---
 .../sis/referencing/cs/CoordinateSystems.java      | 142 +++++++++++++-----
 .../sis/referencing/cs/DefaultCartesianCS.java     |   8 +
 .../sis/referencing/cs/DefaultEllipsoidalCS.java   |   8 +
 .../org/apache/sis/referencing/CommonCRSTest.java  |  10 +-
 .../referencing/crs/DefaultGeographicCRSTest.java  |   8 +-
 .../org/apache/sis/referencing/cs/CodesTest.java   |  10 +-
 .../referencing/cs/DefaultEllipsoidalCSTest.java   |   4 +-
 .../org/apache/sis/referencing/cs/HardCodedCS.java |   2 +-
 .../operation/matrix/GeneralMatrixTest.java        |   2 +-
 .../referencing/operation/matrix/Matrix2Test.java  |   2 +-
 .../referencing/operation/matrix/Matrix4Test.java  |   2 +-
 .../org/apache/sis/internal/util/Constants.java    |  16 +-
 .../org/apache/sis/internal/util/DoubleDouble.java |   4 +-
 .../apache/sis/internal/util/DoubleDoubleTest.java |   4 +-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |  56 ++-----
 .../org/apache/sis/internal/netcdf/CRSBuilder.java | 162 ++++++++++++++++++---
 19 files changed, 514 insertions(+), 156 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java
index fc1e018..10144fc 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingFactoryContainer.java
@@ -16,12 +16,20 @@
  */
 package org.apache.sis.internal.referencing;
 
+import org.opengis.util.FactoryException;
 import org.opengis.referencing.cs.CSFactory;
+import org.opengis.referencing.cs.CSAuthorityFactory;
 import org.opengis.referencing.crs.CRSFactory;
+import org.opengis.referencing.crs.CRSAuthorityFactory;
 import org.opengis.referencing.datum.DatumFactory;
+import org.opengis.referencing.datum.DatumAuthorityFactory;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
+import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
+import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
 import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.referencing.CRS;
 
 
 /**
@@ -33,11 +41,20 @@ import org.apache.sis.internal.system.DefaultFactories;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @version 1.0
- * @since   1.0
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
+ *
+ * @since 1.0
  * @module
  */
 public class ReferencingFactoryContainer {
     /**
+     * The factory for creating coordinate reference systems from authority codes.
+     * If null, then a default factory will be created only when first needed.
+     */
+    private CRSAuthorityFactory crsAuthorityFactory;
+
+    /**
      * The {@linkplain org.opengis.referencing.datum.Datum datum} factory.
      * If null, then a default factory will be created only when first needed.
      */
@@ -74,27 +91,62 @@ public class ReferencingFactoryContainer {
     }
 
     /**
-     * Returns the factory for creating datum, prime meridians and ellipsoids.
+     * Returns the factory for creating coordinate reference systems from authority codes.
+     * Currently only EPSG codes are supported.
      *
-     * @return the Datum factory (never {@code null}).
+     * @return the Coordinate Reference System authority factory (never {@code null}).
+     * @throws FactoryException if the authority factory can not be obtained.
      */
-    public final DatumFactory getDatumFactory() {
-        if (datumFactory == null) {
-            datumFactory = DefaultFactories.forBuildin(DatumFactory.class);
+    public final CRSAuthorityFactory getCRSAuthorityFactory() throws FactoryException {
+        if (crsAuthorityFactory == null) {
+            crsAuthorityFactory = CRS.getAuthorityFactory(Constants.EPSG);
         }
-        return datumFactory;
+        return crsAuthorityFactory;
     }
 
     /**
-     * Returns the factory for creating coordinate systems and their axes.
+     * Returns the factory for creating coordinate systems from authority codes.
+     * Currently only EPSG codes are supported.
      *
-     * @return the Coordinate System factory (never {@code null}).
+     * @return the Coordinate System authority factory (never {@code null}).
+     * @throws FactoryException if the authority factory can not be obtained.
      */
-    public final CSFactory getCSFactory() {
-        if (csFactory == null) {
-            csFactory = DefaultFactories.forBuildin(CSFactory.class);
+    public final CSAuthorityFactory getCSAuthorityFactory() throws FactoryException {
+        final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+        if (factory instanceof CSAuthorityFactory) {                    // This is the case for SIS implementation.
+            return (CSAuthorityFactory) factory;
         }
-        return csFactory;
+        throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+    }
+
+    /**
+     * Returns the factory for creating datum from authority codes.
+     * Currently only EPSG codes are supported.
+     *
+     * @return the Datum authority factory (never {@code null}).
+     * @throws FactoryException if the authority factory can not be obtained.
+     */
+    public final DatumAuthorityFactory getDatumAuthorityFactory() throws FactoryException {
+        final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+        if (factory instanceof DatumAuthorityFactory) {                 // This is the case for SIS implementation.
+            return (DatumAuthorityFactory) factory;
+        }
+        throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+    }
+
+    /**
+     * Returns the factory for creating coordinate operations from authority codes.
+     * Currently only EPSG codes are supported.
+     *
+     * @return the Coordinate Operation authority factory (never {@code null}).
+     * @throws FactoryException if the authority factory can not be obtained.
+     */
+    public final CoordinateOperationAuthorityFactory getCoordinateOperationAuthorityFactory() throws FactoryException {
+        final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+        if (factory instanceof CoordinateOperationAuthorityFactory) {       // This is the case for SIS implementation.
+            return (CoordinateOperationAuthorityFactory) factory;
+        }
+        throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
     }
 
     /**
@@ -110,6 +162,30 @@ public class ReferencingFactoryContainer {
     }
 
     /**
+     * Returns the factory for creating coordinate systems and their axes.
+     *
+     * @return the Coordinate System factory (never {@code null}).
+     */
+    public final CSFactory getCSFactory() {
+        if (csFactory == null) {
+            csFactory = DefaultFactories.forBuildin(CSFactory.class);
+        }
+        return csFactory;
+    }
+
+    /**
+     * Returns the factory for creating datum, prime meridians and ellipsoids.
+     *
+     * @return the Datum factory (never {@code null}).
+     */
+    public final DatumFactory getDatumFactory() {
+        if (datumFactory == null) {
+            datumFactory = DefaultFactories.forBuildin(DatumFactory.class);
+        }
+        return datumFactory;
+    }
+
+    /**
      * Returns the factory for fetching operation methods and creating defining conversions.
      * This is needed only for user-defined projected coordinate reference system.
      * The factory is fetched when first needed.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java
index d53e6c1..2e0de29 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/AbstractCS.java
@@ -24,24 +24,33 @@ import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlSeeAlso;
+import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.Identifier;
 import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.cs.CSAuthorityFactory;
+import org.opengis.referencing.AuthorityFactory;
+import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.AbstractIdentifiedObject;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.CRS;
 import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.metadata.AxisDirections;
 import org.apache.sis.internal.metadata.WKTKeywords;
 import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.internal.util.Constants;
 import org.apache.sis.io.wkt.ElementKind;
 import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 
 import static org.apache.sis.util.ArgumentChecks.*;
-import static org.apache.sis.util.Utilities.deepEquals;
 
 
 /**
@@ -64,7 +73,7 @@ import static org.apache.sis.util.Utilities.deepEquals;
  * objects and passed between threads without synchronization.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see DefaultCoordinateSystemAxis
  * @see org.apache.sis.referencing.crs.AbstractCRS
@@ -339,8 +348,17 @@ public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSy
         if (cs == null) {
             cs = Normalizer.forConvention(this, convention);
             if (cs == null) {
-                cs = this;  // This coordinate system is already normalized.
+                cs = this;                                                          // This coordinate system is already normalized.
+            } else if (convention != AxesConvention.POSITIVE_RANGE &&
+                    IdentifiedObjects.getIdentifier(this, Citations.EPSG) != null)  // See resolveEPSG(…) for purpose of this check.
+            {
+                cs = cs.resolveEPSG(false);
             }
+            /*
+             * It happen often that the CRS created by RIGHT_HANDED, CONVENTIONALLY_ORIENTED
+             * and NORMALIZED are the same. If this is the case, sharing the same instance
+             * not only save memory but can also make future comparisons faster.
+             */
             for (final AbstractCS existing : derived.values()) {
                 if (cs.equals(existing)) {
                     cs = existing;
@@ -368,6 +386,50 @@ public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSy
     }
 
     /**
+     * Verify if we can get a coordinate system from the EPSG database with the same axes.
+     * Such CS gives more information (better name and remarks). This is a "would be nice"
+     * feature; if we fail, we keep the CS built by {@link Normalizer}.
+     *
+     * <p>Subclasses need to override this method in order to change the {@code enabled}
+     * argument value from {@code false} to {@code true}. This is required because we do
+     * not want to enable this resolving process to every CS types.</p>
+     *
+     * @param  enabled  if {@code false}, do nothing.
+     * @return the resolved CS, or {@code this} if none.
+     */
+    AbstractCS resolveEPSG(final boolean enabled) {
+        if (enabled) {
+            final Integer epsg = CoordinateSystems.getEpsgCode(axes);
+            if (epsg != null) try {
+                final AuthorityFactory factory = CRS.getAuthorityFactory(Constants.EPSG);
+                if (factory instanceof CSAuthorityFactory) {
+                    final CoordinateSystem fromDB = ((CSAuthorityFactory) factory).createCoordinateSystem(epsg.toString());
+                    if (fromDB instanceof AbstractCS) {
+                        /*
+                         * We should compare axes strictly using Arrays.equals(…). However axes in different order
+                         * get different codes in EPSG database, which may them not strictly equal. We would need
+                         * another comparison mode ignoring only the authority code. We don't add this complexity
+                         * for now, and rather rely on the check for EPSG code done by the caller. If the original
+                         * CS was an EPSG object, then we assume that we still want an EPSG object here.
+                         */
+                        if (Utilities.equalsIgnoreMetadata(axes, ((AbstractCS) fromDB).axes)) {
+                            return (AbstractCS) fromDB;
+                        }
+                    }
+                }
+            } catch (FactoryException e) {
+                /*
+                 * NoSuchAuthorityCodeException may happen if factory is EPSGFactoryFallback.
+                 * Other exceptions would probably be more serious errors, but it still non-fatal
+                 * for this method since we can continue with what Normalizer created.
+                 */
+                Logging.recoverableException(Logging.getLogger(Modules.REFERENCING), getClass(), "forConvention", e);
+            }
+        }
+        return this;
+    }
+
+    /**
      * Convenience method for implementations of {@link #createForAxes(Map, CoordinateSystemAxis[])}
      * when the resulting coordinate system would have an unexpected number of dimensions.
      *
@@ -419,7 +481,7 @@ public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSy
                 }
                 if (mode != ComparisonMode.ALLOW_VARIANT) {
                     for (int i=0; i<dimension; i++) {
-                        if (!deepEquals(getAxis(i), that.getAxis(i), mode)) {
+                        if (!Utilities.deepEquals(getAxis(i), that.getAxis(i), mode)) {
                             return false;
                         }
                     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Codes.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Codes.java
index e043117..5ff01a4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Codes.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/Codes.java
@@ -35,7 +35,7 @@ import static org.apache.sis.internal.util.Constants.EPSG_PROJECTED_CS;
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -50,6 +50,7 @@ final class Codes {
 
     /**
      * EPSG code of the target unit of measurement.
+     * Applies only to the two first axes. The third axis, if any, is assumed in metres.
      */
     final short unit;
 
@@ -89,6 +90,9 @@ final class Codes {
 
     /**
      * Returns the EPSG code for the given axis directions and unit of measurement, or 0 if none.
+     *
+     * @param  unit  the unit of the two first axis. The third axis, if any, is assumed in metres.
+     * @param  directions  axis directions to look for.
      */
     static short lookup(final Unit<?> unit, final AxisDirection[] directions) {
         final Integer uc = Units.getEpsgCode(unit, true);
@@ -130,47 +134,59 @@ final class Codes {
     /**
      * All hard-coded EPSG codes known to this class.
      */
-    private static final Map<Codes,Codes> EPSG = new HashMap<>(20);
+    private static final Map<Codes,Codes> EPSG = new HashMap<>(31);
     static {
         final AxisDirection[] directions = new AxisDirection[] {AxisDirection.EAST, AxisDirection.NORTH};
+        final int addVertical = AxisDirection.UP.ordinal() << (2 * Byte.SIZE);
         int packed = pack(directions);
         short unit = EPSG_METRE;
 loop:   for (int i=0; ; i++) {
             final short epsg;
+            short to3D = 0;
             switch (i) {
-                case  0: epsg = EPSG_PROJECTED_CS;              break;      //  Cartesian   [E,N] in metres
-                case  1: epsg = 1039; unit = 9002;              break;      //  Cartesian   [E,N] in feet
-                case  2: epsg = 4497; unit = 9003;              break;      //  Cartesian   [E,N] in US survey feet
-                case  3: epsg = 4403; unit = 9005;              break;      //  Cartesian   [E,N] in Clarke feet
-                case  4: epsg = 6424; unit = EPSG_AXIS_DEGREES; break;      //  Ellipsoidal [E,N] in degrees
-                case  5: epsg = 6425; unit = 9105;              break;      //  Ellipsoidal [E,N] in gradians
-                case  6: epsg = 6429; unit = 9101;              break;      //  Ellipsoidal [E,N] in radians
+                case  0: epsg = EPSG_PROJECTED_CS;                           break;      //  Cartesian   [E,N] in metres
+                case  1: epsg = 1039;              unit = 9002;              break;      //  Cartesian   [E,N] in feet
+                case  2: epsg = 4497;              unit = 9003;              break;      //  Cartesian   [E,N] in US survey feet
+                case  3: epsg = 4403;              unit = 9005;              break;      //  Cartesian   [E,N] in Clarke feet
+                case  4: epsg = 6424; to3D = 6426; unit = EPSG_AXIS_DEGREES; break;      //  Ellipsoidal [E,N] in degrees
+                case  5: epsg = 6425; to3D = 6427; unit = 9105;              break;      //  Ellipsoidal [E,N] in grads
+                case  6: epsg = 6429; to3D = 6431; unit = 9101;              break;      //  Ellipsoidal [E,N] in radians
                 case  7: ArraysExt.swap(directions, 0, 1);
                          packed = pack(directions);
-                         epsg = 4500; unit = EPSG_METRE;        break;      //  Cartesian   [N,E] in metres
-                case  8: epsg = 1029; unit = 9002;              break;      //  Cartesian   [N,E] in feet
-                case  9: epsg = 4502; unit = 9005;              break;      //  Cartesian   [N,E] in Clarke feet
-                case 10: epsg = 6422; unit = EPSG_AXIS_DEGREES; break;      //  Ellipsoidal [N,E] in degrees
-                case 11: epsg = 6403; unit = 9105;              break;      //  Ellipsoidal [N,E] in gradians
-                case 12: epsg = 6428; unit = 9101;              break;      //  Ellipsoidal [N,E] in radians
+                         epsg = 4500;              unit = EPSG_METRE;        break;      //  Cartesian   [N,E] in metres
+                case  8: epsg = 1029;              unit = 9002;              break;      //  Cartesian   [N,E] in feet
+                case  9: epsg = 4502;              unit = 9005;              break;      //  Cartesian   [N,E] in Clarke feet
+                case 10: epsg = 6422; to3D = 6423; unit = EPSG_AXIS_DEGREES; break;      //  Ellipsoidal [N,E] in degrees
+                case 11: epsg = 6403; to3D = 6421; unit = 9105;              break;      //  Ellipsoidal [N,E] in grads
+                case 12: epsg = 6428; to3D = 6430; unit = 9101;              break;      //  Ellipsoidal [N,E] in radians
                 case 13: directions[1] = AxisDirection.WEST;
                          packed = pack(directions);
-                         epsg = 4501; unit = EPSG_METRE;        break;      //  Cartesian [N,W] in metres
+                         epsg = 4501; unit = EPSG_METRE;                     break;      //  Cartesian [N,W] in metres
                 case 14: ArraysExt.swap(directions, 0, 1);
                          packed = pack(directions);
-                         epsg = 4491; break;                                //  Cartesian [W,N] in metres
+                         epsg = 4491; break;                                             //  Cartesian [W,N] in metres
                 case 15: directions[1] = AxisDirection.SOUTH;
                          packed = pack(directions);
-                         epsg = 6503; break;                                //  Cartesian [W,S] in metres
+                         epsg = 6503; break;                                             //  Cartesian [W,S] in metres
                 case 16: ArraysExt.swap(directions, 0, 1);
                          packed = pack(directions);
-                         epsg = 6501; break;                                //  Cartesian [S,W] in metres
+                         epsg = 6501; break;                                             //  Cartesian [S,W] in metres
                 default: break loop;
             }
-            final Codes m = new Codes(packed, unit, epsg);
-            if (packed == 0 || EPSG.put(m, m) != null) {
+            Codes m = new Codes(packed, unit, epsg);
+            if (EPSG.put(m, m) != null) {
                 throw new AssertionError(m.epsg);
             }
+            /*
+             * Add three-dimensional cases. The vertical axis is in metres
+             * (this is encoded in CodesTest.VERTICAL_UNIT constant).
+             */
+            if (to3D != 0) {
+                m = new Codes(packed | addVertical, unit, to3D);
+                if (EPSG.put(m, m) != null) {
+                    throw new AssertionError(m.epsg);
+                }
+            }
         }
     }
 }
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 1d777b4..555cba6 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
@@ -19,28 +19,32 @@ package org.apache.sis.referencing.cs;
 import java.util.Arrays;
 import java.util.Objects;
 import javax.measure.Unit;
+import javax.measure.UnitConverter;
 import javax.measure.quantity.Length;
 import javax.measure.IncommensurableException;
+import org.opengis.referencing.cs.RangeMeaning;
 import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.measure.Angle;
+import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.ElevationAngle;
 import org.apache.sis.measure.Units;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.NullArgumentException;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.DoubleDouble;
 import org.apache.sis.internal.metadata.AxisDirections;
+import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
-import static org.apache.sis.util.ArgumentChecks.ensureNonNullElement;
-
 
 /**
  * Utility methods working on {@link CoordinateSystem} objects and their axes.
@@ -49,7 +53,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNullElement;
  * between two coordinate systems.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -78,7 +82,7 @@ public final class CoordinateSystems extends Static {
      * @throws IllegalArgumentException if the given name is not a known axis direction.
      */
     public static AxisDirection parseAxisDirection(String name) throws IllegalArgumentException {
-        ensureNonNull("name", name);
+        ArgumentChecks.ensureNonNull("name", name);
         name = CharSequences.trimWhitespaces(name);
         AxisDirection candidate = AxisDirections.valueOf(name);
         if (candidate != null) {
@@ -172,8 +176,8 @@ public final class CoordinateSystems extends Static {
      *         {@code null} if this value can not be computed.
      */
     public static Angle angle(final AxisDirection source, final AxisDirection target) {
-        ensureNonNull("source", source);
-        ensureNonNull("target", target);
+        ArgumentChecks.ensureNonNull("source", source);
+        ArgumentChecks.ensureNonNull("target", target);
         /*
          * Check for NORTH, SOUTH, EAST, EAST-NORTH-EAST, etc.
          * Checked first because this is the most common case.
@@ -270,8 +274,8 @@ public final class CoordinateSystems extends Static {
                                           final CoordinateSystem targetCS)
             throws IllegalArgumentException, IncommensurableException
     {
-        ensureNonNull("sourceCS", sourceCS);
-        ensureNonNull("targetCS", targetCS);
+        ArgumentChecks.ensureNonNull("sourceCS", sourceCS);
+        ArgumentChecks.ensureNonNull("targetCS", targetCS);
         if (!Classes.implementSameInterfaces(sourceCS.getClass(), targetCS.getClass(), CoordinateSystem.class)) {
             throw new IllegalArgumentException(Resources.format(Resources.Keys.IncompatibleCoordinateSystemTypes));
         }
@@ -382,7 +386,7 @@ public final class CoordinateSystems extends Static {
      * @since 0.6
      */
     public static CoordinateSystem replaceAxes(final CoordinateSystem cs, final AxisFilter filter) {
-        ensureNonNull("filter", filter);
+        ArgumentChecks.ensureNonNull("filter", filter);
         if (cs != null) {
             final CoordinateSystem newCS;
             if (filter instanceof AxesConvention) {
@@ -425,7 +429,7 @@ public final class CoordinateSystems extends Static {
      * @since 0.7
      */
     public static CoordinateSystem replaceLinearUnit(final CoordinateSystem cs, final Unit<Length> newUnit) {
-        ensureNonNull("newUnit", newUnit);
+        ArgumentChecks.ensureNonNull("newUnit", newUnit);
         return CoordinateSystems.replaceAxes(cs, new AxisFilter() {
             @Override public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
                 return Units.isLinear(unit) ? newUnit : unit;
@@ -456,7 +460,7 @@ public final class CoordinateSystems extends Static {
      * @since 0.7
      */
     public static CoordinateSystem replaceAngularUnit(final CoordinateSystem cs, final Unit<javax.measure.quantity.Angle> newUnit) {
-        ensureNonNull("newUnit", newUnit);
+        ArgumentChecks.ensureNonNull("newUnit", newUnit);
         return CoordinateSystems.replaceAxes(cs, new AxisFilter() {
             @Override public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
                 return Units.isAngular(unit) ? newUnit : unit;
@@ -476,18 +480,21 @@ public final class CoordinateSystems extends Static {
      * @since 0.8
      */
     public static AxisDirection[] getAxisDirections(final CoordinateSystem cs) {
-        ensureNonNull("cs", cs);
+        ArgumentChecks.ensureNonNull("cs", cs);
         final AxisDirection[] directions = new AxisDirection[cs.getDimension()];
         for (int i=0; i<directions.length; i++) {
             final CoordinateSystemAxis axis = cs.getAxis(i);
-            ensureNonNullElement("cs", i, cs);
-            ensureNonNullElement("cs[#].direction", i, directions[i] = axis.getDirection());
+            ArgumentChecks.ensureNonNullElement("cs", i, cs);
+            ArgumentChecks.ensureNonNullElement("cs[#].direction", i, directions[i] = axis.getDirection());
         }
         return directions;
     }
 
     /**
-     * Returns the EPSG code of a coordinate system using the given unit and axis directions.
+     * Returns the EPSG code of a coordinate system using the units and directions of given axes.
+     * This method ignores axis metadata (names, abbreviation, identifiers, remarks, <i>etc.</i>).
+     * The axis minimum and maximum values are checked only if the
+     * {@linkplain CoordinateSystemAxis#getRangeMeaning() range meaning} is "wraparound".
      * If no suitable coordinate system is known to Apache SIS, then this method returns {@code null}.
      *
      * <p>Current implementation uses a hard-coded list of known coordinate systems;
@@ -496,27 +503,88 @@ public final class CoordinateSystems extends Static {
      *
      * <table>
      *   <caption>Known coordinate systems (CS)</caption>
-     *   <tr><th>EPSG</th> <th>CS type</th> <th colspan="2">Axis directions</th> <th>Unit</th></tr>
-     *   <tr><td>6424</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td>degree</td></tr>
-     *   <tr><td>6422</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td>degree</td></tr>
-     *   <tr><td>6425</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td>grads</td></tr>
-     *   <tr><td>6403</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td>grads</td></tr>
-     *   <tr><td>6429</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td>radian</td></tr>
-     *   <tr><td>6428</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td>radian</td></tr>
-     *   <tr><td>4400</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td>metre</td></tr>
-     *   <tr><td>4500</td> <td>Cartesian</td>   <td>north</td> <td>east</td>  <td>metre</td></tr>
-     *   <tr><td>4491</td> <td>Cartesian</td>   <td>west</td>  <td>north</td> <td>metre</td></tr>
-     *   <tr><td>4501</td> <td>Cartesian</td>   <td>north</td> <td>west</td>  <td>metre</td></tr>
-     *   <tr><td>6503</td> <td>Cartesian</td>   <td>west</td>  <td>south</td> <td>metre</td></tr>
-     *   <tr><td>6501</td> <td>Cartesian</td>   <td>south</td> <td>west</td>  <td>metre</td></tr>
-     *   <tr><td>1039</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td>foot</td></tr>
-     *   <tr><td>1029</td> <td>Cartesian</td>   <td>north</td> <td>east</td>  <td>foot</td></tr>
-     *   <tr><td>4403</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td>Clarke’s foot</td></tr>
-     *   <tr><td>4502</td> <td>Cartesian</td>   <td>north</td> <td>east</td>  <td>Clarke’s foot</td></tr>
-     *   <tr><td>4497</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td>US survey foot</td></tr>
+     *   <tr><th>EPSG</th> <th>CS type</th> <th colspan="3">Axis directions</th> <th>Horizontal unit</th></tr>
+     *   <tr><td>6424</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td></td>   <td>degree</td></tr>
+     *   <tr><td>6422</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td></td>   <td>degree</td></tr>
+     *   <tr><td>6425</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td></td>   <td>grads</td></tr>
+     *   <tr><td>6403</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td></td>   <td>grads</td></tr>
+     *   <tr><td>6429</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td></td>   <td>radian</td></tr>
+     *   <tr><td>6428</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td></td>   <td>radian</td></tr>
+     *   <tr><td>6426</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td>up</td> <td>degree</td></tr>
+     *   <tr><td>6423</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td>up</td> <td>degree</td></tr>
+     *   <tr><td>6427</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td>up</td> <td>grads</td></tr>
+     *   <tr><td>6421</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td>up</td> <td>grads</td></tr>
+     *   <tr><td>6431</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> <td>up</td> <td>radian</td></tr>
+     *   <tr><td>6430</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  <td>up</td> <td>radian</td></tr>
+     *   <tr><td>4400</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td></td>   <td>metre</td></tr>
+     *   <tr><td>4500</td> <td>Cartesian</td>   <td>north</td> <td>east</td>  <td></td>   <td>metre</td></tr>
+     *   <tr><td>4491</td> <td>Cartesian</td>   <td>west</td>  <td>north</td> <td></td>   <td>metre</td></tr>
+     *   <tr><td>4501</td> <td>Cartesian</td>   <td>north</td> <td>west</td>  <td></td>   <td>metre</td></tr>
+     *   <tr><td>6503</td> <td>Cartesian</td>   <td>west</td>  <td>south</td> <td></td>   <td>metre</td></tr>
+     *   <tr><td>6501</td> <td>Cartesian</td>   <td>south</td> <td>west</td>  <td></td>   <td>metre</td></tr>
+     *   <tr><td>1039</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td></td>   <td>foot</td></tr>
+     *   <tr><td>1029</td> <td>Cartesian</td>   <td>north</td> <td>east</td>  <td></td>   <td>foot</td></tr>
+     *   <tr><td>4403</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td></td>   <td>Clarke’s foot</td></tr>
+     *   <tr><td>4502</td> <td>Cartesian</td>   <td>north</td> <td>east</td>  <td></td>   <td>Clarke’s foot</td></tr>
+     *   <tr><td>4497</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> <td></td>   <td>US survey foot</td></tr>
      * </table>
      *
-     * @param  unit        desired unit of measurement.
+     * @param  axes  axes for which a coordinate system EPSG code is desired.
+     * @return EPSG codes for a coordinate system using the given axes (ignoring metadata), or {@code null} if unknown
+     *         to this method. Note that a null value does not mean that a more  extensive search in the EPSG database
+     *         would not find a matching coordinate system.
+     *
+     * @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createCoordinateSystem(String)
+     *
+     * @since 1.0
+     */
+    @SuppressWarnings("fallthrough")
+    public static Integer getEpsgCode(final CoordinateSystemAxis... axes) {
+        ArgumentChecks.ensureNonNull("axes", axes);
+forDim: switch (axes.length) {
+            case 3: {
+                if (!Units.METRE.equals(axes[2].getUnit())) break;      // Restriction in our hard-coded list of codes.
+                // Fall through
+            }
+            case 2: {
+                final Unit<?> unit = axes[0].getUnit();
+                if (unit != null && unit.equals(axes[1].getUnit())) {
+                    final AxisDirection[] directions = new AxisDirection[axes.length];
+                    for (int i=0; i<directions.length; i++) {
+                        final CoordinateSystemAxis axis = axes[i];
+                        ArgumentChecks.ensureNonNullElement("axes", i, axis);
+                        directions[i] = axis.getDirection();
+                        if (RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning()) && Units.isAngular(unit)) try {
+                            final UnitConverter uc = unit.getConverterToAny(Units.DEGREE);
+                            final double min = uc.convert(axis.getMinimumValue());
+                            final double max = uc.convert(axis.getMaximumValue());
+                            if ((min > Double.NEGATIVE_INFINITY && Math.abs(min - Longitude.MIN_VALUE) > Formulas.ANGULAR_TOLERANCE) ||
+                                (max < Double.POSITIVE_INFINITY && Math.abs(max - Longitude.MAX_VALUE) > Formulas.ANGULAR_TOLERANCE))
+                            {
+                                break forDim;
+                            }
+                        } catch (IncommensurableException e) {      // Should never happen since we checked that units are angular.
+                            Logging.unexpectedException(Logging.getLogger(Modules.REFERENCING), CoordinateSystems.class, "getEpsgCode", e);
+                            break forDim;
+                        }
+                    }
+                    return getEpsgCode(unit, directions);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the EPSG code of a coordinate system using the given unit and axis directions.
+     * If no suitable coordinate system is known to Apache SIS, then this method returns {@code null}.
+     *
+     * <p>Current implementation uses a hard-coded list of known coordinate systems;
+     * it does not yet scan the EPSG database (this may change in future Apache SIS version).
+     * The current list of known coordinate systems is documented in {@link #getEpsgCode(CoordinateSystemAxis...)}.</p>
+     *
+     * @param  unit        desired unit of measurement. For three-dimensional ellipsoidal coordinate system,
+     *                     this is the unit for the horizontal axes only; the vertical axis is in metres.
      * @param  directions  desired axis directions.
      * @return EPSG codes for a coordinate system using the given axis directions and unit of measurement,
      *         or {@code null} if unknown to this method. Note that a null value does not mean that a more
@@ -528,8 +596,8 @@ public final class CoordinateSystems extends Static {
      * @since 0.8
      */
     public static Integer getEpsgCode(final Unit<?> unit, final AxisDirection... directions) {
-        ensureNonNull("unit", unit);
-        ensureNonNull("directions", directions);
+        ArgumentChecks.ensureNonNull("unit", unit);
+        ArgumentChecks.ensureNonNull("directions", directions);
         final int code = Codes.lookup(unit, directions);
         return (code != 0) ? code : null;
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java
index f4f4896..b3d5a26 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultCartesianCS.java
@@ -242,6 +242,14 @@ public class DefaultCartesianCS extends DefaultAffineCS implements CartesianCS {
         }
     }
 
+    /**
+     * Overridden for enabling the search of equivalent CS in the EPSG database.
+     */
+    @Override
+    final AbstractCS resolveEPSG(final boolean enabled) {
+        return super.resolveEPSG(true);
+    }
+
 
 
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java
index edaa12e..4e7b8d0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCS.java
@@ -252,6 +252,14 @@ public class DefaultEllipsoidalCS extends AbstractCS implements EllipsoidalCS {
         }
     }
 
+    /**
+     * Overridden for enabling the search of equivalent CS in the EPSG database.
+     */
+    @Override
+    final AbstractCS resolveEPSG(final boolean enabled) {
+        return super.resolveEPSG(true);
+    }
+
 
 
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java
index 5a22731..d67f0ef 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java
@@ -43,7 +43,7 @@ import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.opengis.test.Assert.*;
+import static org.apache.sis.test.MetadataAssert.*;
 import static org.apache.sis.test.TestUtilities.*;
 
 
@@ -141,11 +141,13 @@ public final strictfp class CommonCRSTest extends TestCase {
         final GeographicCRS normalized = CommonCRS.WGS84.normalizedGeographic();
         Validators.validate(normalized);
         assertSame(geographic.getDatum(), normalized.getDatum());
-
+        /*
+         * Compare axes. Note that axes in different order have different EPSG codes.
+         */
         final CoordinateSystem φλ = geographic.getCoordinateSystem();
         final CoordinateSystem λφ = normalized.getCoordinateSystem();
-        assertSame("Longitude", φλ.getAxis(1), λφ.getAxis(0));
-        assertSame("Latitude",  φλ.getAxis(0), λφ.getAxis(1));
+        assertEqualsIgnoreMetadata(φλ.getAxis(1), λφ.getAxis(0));       // Longitude
+        assertEqualsIgnoreMetadata(φλ.getAxis(0), λφ.getAxis(1));       // Latitude
         assertSame("Cached value", normalized, CommonCRS.WGS84.normalizedGeographic());
     }
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
index 3f16b8b..3497269 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java
@@ -37,7 +37,7 @@ import static org.apache.sis.test.TestUtilities.getSingleton;
  * Tests the {@link DefaultGeographicCRS} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -80,9 +80,9 @@ public final strictfp class DefaultGeographicCRSTest extends TestCase {
         assertNotSame(crs, normalized);
         final EllipsoidalCS cs = normalized.getCoordinateSystem();
         final EllipsoidalCS ref = crs.getCoordinateSystem();
-        assertSame("longitude", ref.getAxis(1), cs.getAxis(0));
-        assertSame("latitude",  ref.getAxis(0), cs.getAxis(1));
-        assertSame("height",    ref.getAxis(2), cs.getAxis(2));
+        assertEqualsIgnoreMetadata(ref.getAxis(1), cs.getAxis(0));      // EPSG codes differ because of different axis order.
+        assertEqualsIgnoreMetadata(ref.getAxis(0), cs.getAxis(1));
+        assertEqualsIgnoreMetadata(ref.getAxis(2), cs.getAxis(2));
     }
 
     /**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CodesTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CodesTest.java
index 4ae7222..b79ace6 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CodesTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/CodesTest.java
@@ -23,6 +23,7 @@ import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CSAuthorityFactory;
 import org.apache.sis.referencing.factory.TestFactorySource;
+import org.apache.sis.measure.Units;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -33,12 +34,17 @@ import static org.junit.Assert.*;
  * Compares the {@link Codes} elements with the EPSG geodetic database.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
 public final strictfp class CodesTest extends TestCase {
     /**
+     * The unit of measurement of the vertical axis.
+     */
+    private static final Unit<?> VERTICAL_UNIT = Units.METRE;
+
+    /**
      * Compares the axis directions and units with EPSG definitions.
      *
      * @throws Exception if an error occurred while fetching the codes or querying the database.
@@ -54,7 +60,7 @@ public final strictfp class CodesTest extends TestCase {
             final Unit<?> unit = cs.getAxis(0).getUnit();
             final AxisDirection[] directions = new AxisDirection[cs.getDimension()];
             for (int i=0; i<directions.length; i++) {
-                assertEquals(unit, cs.getAxis(i).getUnit());
+                assertEquals(i < 2 ? unit : VERTICAL_UNIT, cs.getAxis(i).getUnit());
                 directions[i] = cs.getAxis(i).getDirection();
             }
             assertEquals("Codes.lookpup(…)", c.epsg, Codes.lookup(unit, directions));
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCSTest.java
index 0dbc617..a3bc398 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCSTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/DefaultEllipsoidalCSTest.java
@@ -70,11 +70,11 @@ public final strictfp class DefaultEllipsoidalCSTest extends TestCase {
     }
 
     /**
-     * Tests the {@link DefaultEllipsoidalCS#forConvention(AxesConvention)} method with gradians units.
+     * Tests the {@link DefaultEllipsoidalCS#forConvention(AxesConvention)} method with grads units.
      */
     @Test
     @DependsOnMethod("testShiftLongitudeRange")
-    public void testShiftLongitudeRangeGradians() {
+    public void testShiftLongitudeRangeGrads() {
         final DefaultEllipsoidalCS cs = HardCodedCS.ELLIPSOIDAL_gon;
         CoordinateSystemAxis axis = cs.getAxis(0);
         assertEquals("longitude.minimumValue", -200, axis.getMinimumValue(), STRICT);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java
index 305471b..efb6957 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/cs/HardCodedCS.java
@@ -73,7 +73,7 @@ public final strictfp class HardCodedCS {
      * A two-dimensional ellipsoidal CS with
      * <var>{@linkplain HardCodedAxes#LONGITUDE_gon longitude}</var>,
      * <var>{@linkplain HardCodedAxes#LATITUDE_gon latitude}</var>
-     * axes (without "Geodetic" prefix) in gradians.
+     * axes (without "Geodetic" prefix) in grads.
      *
      * <p>This coordinate system is used for testing unit conversions without axes swapping.</p>
      */
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/GeneralMatrixTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/GeneralMatrixTest.java
index bb4d88f..8d3d883 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/GeneralMatrixTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/GeneralMatrixTest.java
@@ -71,7 +71,7 @@ public final strictfp class GeneralMatrixTest extends MatrixTestCase {
         testGetExtendedElements(new GeneralMatrix(2, 2, new double[] {
                 StrictMath.PI / 180,            // Degrees to radians
                 180 / StrictMath.PI,            // Radians to degrees
-                0.9,                            // Gradians to degrees
+                0.9,                            // Grads to degrees
                 0.1234567}));                   // Random value with no special meaning.
     }
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix2Test.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix2Test.java
index 13c345a..974d3b0 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix2Test.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix2Test.java
@@ -76,7 +76,7 @@ public final strictfp class Matrix2Test extends MatrixTestCase {
         GeneralMatrixTest.testGetExtendedElements(new Matrix2(
                 StrictMath.PI / 180,        // Degrees to radians
                 180 / StrictMath.PI,        // Radians to degrees
-                0.9,                        // Gradians to degrees
+                0.9,                        // Grads to degrees
                 0.1234567));                // Random value with no special meaning.
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java
index a30109b..3632436 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/matrix/Matrix4Test.java
@@ -125,7 +125,7 @@ public final strictfp class Matrix4Test extends MatrixTestCase {
         final double parisMeridian = 2 + (20 + 13.82/60)/60;            // Paris meridian: 2°20'13.82"
         final double toRadians = StrictMath.PI / 180;
         /*
-         * Gradians to degrees with a Prime Meridian shift
+         * Grads to degrees with a Prime Meridian shift
          * and a random conversion factor for z values.
          */
         final Matrix4 step1 = new Matrix4(
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
index 7d7fa26..d089889 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
@@ -31,7 +31,7 @@ import org.apache.sis.util.Static;
  * creates itself the instance to be tested.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.5
  * @module
  */
@@ -241,6 +241,20 @@ public final class Constants extends Static {
     public static final short EPSG_ANTARCTIC_POLAR_STEREOGRAPHIC = 3031;
 
     /**
+     * EPSG code of "Unknown datum based upon the WGS 84 ellipsoid".
+     * This is a two-dimensional geographic CRS.
+     * Note that the EPSG database defines unknown CRS for many other ellipsoids.
+     * For now only the WGS 84 case is used by Apache SIS.
+     */
+    public static final short EPSG_UNKNOWN_CRS = 4030;
+
+    /**
+     * EPSG code of "Not specified (based upon the WGS 84 ellipsoid)".
+     * This is a geodetic datum.
+     */
+    public static final short EPSG_UNKNOWN_DATUM = 6030;
+
+    /**
      * Do not allow instantiation of this class.
      */
     private Constants() {
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 8cc0e6e..4edd814 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
@@ -136,9 +136,9 @@ public final class DoubleDouble extends Number {
          0.0002777777777777777777777777777777778,   // Second to degrees
          0.002777777777777777777777777777777778,    // 1/360°
          0.01666666666666666666666666666666667,     // Minute to degrees
-         0.01745329251994329576923690768488613,     // Degrees to radians
+         0.01745329251994329576923690768488613,     // Degree to radians
          0.785398163397448309615660845819876,       // π/4
-         1.111111111111111111111111111111111,       // Gradian to degrees
+         1.111111111111111111111111111111111,       // Grad to degrees
          1.414213562373095048801688724209698,       // √2
          1.570796326794896619231321691639751,       // π/2
          2.356194490192344928846982537459627,       // π * 3/4
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/DoubleDoubleTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/DoubleDoubleTest.java
index 3aa1bb5..8d10ced 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/DoubleDoubleTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/DoubleDoubleTest.java
@@ -327,8 +327,8 @@ public final strictfp class DoubleDoubleTest extends TestCase {
          "1.8288",                                      // Fathom to metres
         "20.1168",                                      // Chain to metres
          "2.54",                                        // Inch to centimetres
-         "0.9",                                         // Degrees to gradians
-         "1.111111111111111111111111111111111",         // Gradian to degrees
+         "0.9",                                         // Degree to grads
+         "1.111111111111111111111111111111111",         // Grad to degrees
          "0.002777777777777777777777777777777778",      // 1/360°
          "0.0002777777777777777777777777777777778",     // Second to degrees
          "0.01666666666666666666666666666666667",       // Minute to degrees
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index c3d55ae..f0e849d 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -68,13 +68,11 @@ import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 import org.apache.sis.metadata.iso.citation.Citations;
-import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
-import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.util.iso.DefaultNameSpace;
 import org.apache.sis.util.resources.Errors;
@@ -201,19 +199,6 @@ final class CRSBuilder extends ReferencingFactoryContainer {
     private final Map<Short,Object> geoKeys;
 
     /**
-     * Factory for creating geodetic objects from EPSG codes, or {@code null} if not yet fetched.
-     * The EPSG code for a complete CRS definition can be stored in a single {@link GeoKeys}.
-     *
-     * <div class="note"><b>Note:</b> we do not yet split this field into 3 separated fields for datums,
-     * coordinate systems and coordinate reference systems objects because it is not needed with Apache SIS
-     * implementation of those factories. However we may revisit this choice if we want to let the user specify
-     * his own factories.</div>
-     *
-     * @see #epsgFactory()
-     */
-    private GeodeticAuthorityFactory epsgFactory;
-
-    /**
      * Name of the last object created. This is used by {@link #properties(Object)} for reusing existing instance
      * if possible. This is useful in GeoTIFF files since the same name is used for different geodetic components,
      * for example the datum and the ellipsoid.
@@ -264,20 +249,6 @@ final class CRSBuilder extends ReferencingFactoryContainer {
     }
 
     /**
-     * Returns the factory for creating geodetic objects from EPSG codes.
-     * The factory is fetched when first needed.
-     *
-     * @return the EPSG factory (never {@code null}).
-     * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
-     */
-    private GeodeticAuthorityFactory epsgFactory() throws FactoryException {
-        if (epsgFactory == null) {
-            epsgFactory = (GeodeticAuthorityFactory) CRS.getAuthorityFactory(Constants.EPSG);
-        }
-        return epsgFactory;
-    }
-
-    /**
      * Returns a map with the given name associated to {@value org.opengis.referencing.IdentifiedObject#NAME_KEY}.
      * The given name shall be either an instance of {@link String} or {@link Identifier}.
      * This is an helper method for creating geodetic objects with {@link #getCRSFactory()}.
@@ -748,7 +719,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
     private CartesianCS replaceLinearUnit(final CartesianCS cs, final Unit<Length> unit) throws FactoryException {
         final Integer epsg = CoordinateSystems.getEpsgCode(unit, CoordinateSystems.getAxisDirections(cs));
         if (epsg != null) try {
-            return epsgFactory().createCartesianCS(epsg.toString());
+            return getCSAuthorityFactory().createCartesianCS(epsg.toString());
         } catch (NoSuchAuthorityCodeException e) {
             reader.owner.warning(null, e);
         }
@@ -766,7 +737,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
     private EllipsoidalCS replaceAngularUnit(final EllipsoidalCS cs, final Unit<Angle> unit) throws FactoryException {
         final Integer epsg = CoordinateSystems.getEpsgCode(unit, CoordinateSystems.getAxisDirections(cs));
         if (epsg != null) try {
-            return epsgFactory().createEllipsoidalCS(epsg.toString());
+            return getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
         } catch (NoSuchAuthorityCodeException e) {
             reader.owner.warning(null, e);
         }
@@ -816,7 +787,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also provide the scale value, we will verify that the value
                  * is consistent with what we would expect for a unit of the given EPSG code.
                  */
-                final Unit<Q> unit = epsgFactory().createUnit(String.valueOf(epsg)).asType(quantity);
+                final Unit<Q> unit = getCSAuthorityFactory().createUnit(String.valueOf(epsg)).asType(quantity);
                 if (scaleKey != 0) {
                     final double scale = getAsDouble(scaleKey);
                     if (!Double.isNaN(scale)) {
@@ -878,7 +849,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also provide the longitude value, verify that the value is consistent
                  * with what we would expect for a prime meridian of the given EPSG code.
                  */
-                final PrimeMeridian pm = epsgFactory().createPrimeMeridian(String.valueOf(epsg));
+                final PrimeMeridian pm = getDatumAuthorityFactory().createPrimeMeridian(String.valueOf(epsg));
                 verify(pm, unit);
                 return pm;
             }
@@ -956,7 +927,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also provide defining parameter values, verify that those values
                  * are consistent with what we would expect for an ellipsoid of the given EPSG code.
                  */
-                final Ellipsoid ellipsoid = epsgFactory().createEllipsoid(String.valueOf(epsg));
+                final Ellipsoid ellipsoid = getDatumAuthorityFactory().createEllipsoid(String.valueOf(epsg));
                 verify(ellipsoid, unit);
                 return ellipsoid;
             }
@@ -1045,7 +1016,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also defines the components, verify that those components are
                  * consistent with what we would expect for a datum of the given EPSG code.
                  */
-                final GeodeticDatum datum = epsgFactory().createGeodeticDatum(String.valueOf(epsg));
+                final GeodeticDatum datum = getDatumAuthorityFactory().createGeodeticDatum(String.valueOf(epsg));
                 verify(datum, angularUnit, linearUnit);
                 return datum;
             }
@@ -1207,7 +1178,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also defines the components, verify that those components are consistent
                  * with what we would expect for a CRS of the given EPSG code.
                  */
-                GeographicCRS crs = epsgFactory().createGeographicCRS(String.valueOf(epsg));
+                GeographicCRS crs = getCRSAuthorityFactory().createGeographicCRS(String.valueOf(epsg));
                 if (rightHanded) {
                     crs = DefaultGeographicCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
                 }
@@ -1277,7 +1248,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also defines the components, verify that those components are consistent
                  * with what we would expect for a CRS of the given EPSG code.
                  */
-                final GeocentricCRS crs = epsgFactory().createGeocentricCRS(String.valueOf(epsg));
+                final GeocentricCRS crs = getCRSAuthorityFactory().createGeocentricCRS(String.valueOf(epsg));
                 verify(crs);
                 return crs;
             }
@@ -1402,7 +1373,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                 final Unit<Angle>   angularUnit = createUnit(GeoKeys.AngularUnits, GeoKeys.AngularUnitSize, Angle.class, Units.DEGREE);
                 final GeographicCRS baseCRS     = createGeographicCRS(false, angularUnit);
                 final Conversion    projection  = createConversion(name, angularUnit, linearUnit);
-                CartesianCS cs = epsgFactory().createCartesianCS(String.valueOf(Constants.EPSG_PROJECTED_CS));
+                CartesianCS cs = getCSAuthorityFactory().createCartesianCS(String.valueOf(Constants.EPSG_PROJECTED_CS));
                 if (!Units.METRE.equals(linearUnit)) {
                     cs = replaceLinearUnit(cs, linearUnit);
                 }
@@ -1416,7 +1387,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also defines the components, verify that those components are consistent
                  * with what we would expect for a CRS of the given EPSG code.
                  */
-                final ProjectedCRS crs = epsgFactory().createProjectedCRS(String.valueOf(epsg));
+                final ProjectedCRS crs = getCRSAuthorityFactory().createProjectedCRS(String.valueOf(epsg));
                 verify(crs);
                 return crs;
             }
@@ -1527,7 +1498,8 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                  * But if the file also defines the components, verify that those components are
                  * consistent with what we would expect for a conversion of the given EPSG code.
                  */
-                final Conversion projection = (Conversion) epsgFactory().createCoordinateOperation(String.valueOf(epsg));
+                final Conversion projection = (Conversion)
+                        getCoordinateOperationAuthorityFactory().createCoordinateOperation(String.valueOf(epsg));
                 verify(projection, angularUnit, linearUnit);
                 return projection;
             }
@@ -1603,7 +1575,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                 throw new NoSuchElementException(missingValue(GeoKeys.VerticalDatum));
             }
             default: {
-                return epsgFactory().createVerticalDatum(String.valueOf(epsg));
+                return getDatumAuthorityFactory().createVerticalDatum(String.valueOf(epsg));
             }
         }
     }
@@ -1644,7 +1616,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                 return getCRSFactory().createVerticalCRS(properties(name), datum, cs);
             }
             default: {
-                return epsgFactory().createVerticalCRS(String.valueOf(epsg));
+                return getCRSAuthorityFactory().createVerticalCRS(String.valueOf(epsg));
             }
         }
     }
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 b614187..0b3e209 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
@@ -22,6 +22,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.StringJoiner;
 import java.util.function.Supplier;
+import java.util.logging.Level;
 import java.time.Instant;
 import javax.measure.Unit;
 import org.opengis.util.FactoryException;
@@ -29,12 +30,16 @@ import org.opengis.referencing.cs.*;
 import org.opengis.referencing.datum.*;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.CRSFactory;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.cs.DefaultSphericalCS;
 import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.internal.util.TemporalUtilities;
+import org.apache.sis.internal.util.Constants;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.measure.Units;
 
@@ -43,9 +48,17 @@ import org.apache.sis.measure.Units;
  * Temporary object for building a coordinate reference system from the variables in a netCDF file.
  * Different instances are required for the geographic, vertical and temporal components of a CRS,
  * or if a netCDF file uses different CRS for different variables.
+ * This builder is used as below:
  *
- * <p>The builder type is inferred from axes. The axes are identified by their abbreviations,
- * which is a {@linkplain Axis#abbreviation controlled vocabulary} for this implementation.</p>
+ * <ol>
+ *   <li>Invoke {@link #dispatch(List, Axis)} for all axes in a grid.
+ *       Builders for CRS components will added in the given list.</li>
+ *   <li>Invoke {@link #build(Decoder)} on each builder prepared in above step.</li>
+ *   <li>Assemble the CRS components created in above step in a {@code CompoundCRS}.</li>
+ * </ol>
+ *
+ * The builder type is inferred from axes. The axes are identified by their abbreviations,
+ * which is a {@linkplain Axis#abbreviation controlled vocabulary} for this implementation.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
@@ -94,6 +107,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
      * The axes to use for creating the coordinate reference system.
      * They are information about netCDF axes, not yet ISO 19111 axes.
      * The axis are listed in "natural" order (reverse of netCDF order).
+     * Only the {@link #dimension} first elements are valid.
      */
     private Axis[] axes;
 
@@ -108,7 +122,22 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     CS coordinateSystem;
 
     /**
+     * The coordinate reference system that may have been create by {@link #candidate(Decoder)}.
+     */
+    SingleCRS candidateCRS;
+
+    /**
+     * Non-fatal exceptions that may occur while building the coordinate reference system.
+     * The same exception may be repeated many time, in which case we will report only the
+     * first one.
+     *
+     * @see #recoverableException(NoSuchAuthorityCodeException)
+     */
+    private NoSuchAuthorityCodeException warnings;
+
+    /**
      * Creates a new CRS builder based on datum of the given type.
+     * This constructor is invoked indirectly by {@link #dispatch(List, Axis)}.
      *
      * @param  datumType   the type of datum as a GeoAPI sub-interface of {@link Datum}.
      * @param  datumBase   name of the datum on which the CRS is presumed to be based, or {@code ""}.
@@ -116,7 +145,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
      * @param  minDim      minimum number of dimensions (usually 1, 2 or 3).
      * @param  maxDim      maximum number of dimensions (usually 1, 2 or 3).
      */
-    CRSBuilder(final Class<D> datumType, final String datumBase, final byte datumIndex, final byte minDim, final byte maxDim) {
+    private CRSBuilder(final Class<D> datumType, final String datumBase, final byte datumIndex, final byte minDim, final byte maxDim) {
         this.datumType  = datumType;
         this.datumBase  = datumBase;
         this.datumIndex = datumIndex;
@@ -237,20 +266,28 @@ previous:   for (int i=components.size(); --i >= 0;) {
             throw new DataStoreContentException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4,
                     axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", ")));
         }
-        datum = datumType.cast(decoder.datumCache[datumIndex]);
+        /*
+         * If the subclass can offer coordinate system and datum candidates based on a brief inspection of axes,
+         * set the datum, CS and CRS field values to those candidate. Those values do not need to be exact; they
+         * will be overwritten later if they do not match the netCDF file content.
+         */
+        datum = datumType.cast(decoder.datumCache[datumIndex]);         // Should be initialized before 'candidate' call.
+        candidate(decoder);
+        /*
+         * If 'candidate(decoder)' offers a datum, we will used it as-is. Otherwise create the datum now.
+         * Datum are often not defined in netCDF files, so it will usually be EPSG::6030 — "Not specified
+         * (based on WGS 84 ellipsoid)".
+         */
         if (datum == null) {
             // Not localized because stored as a String, possibly exported in WKT or GML, and 'datumBase' is in English.
             createDatum(decoder.getDatumFactory(), properties("Unknown datum presumably based upon ".concat(datumBase)));
-            decoder.datumCache[datumIndex] = datum;
         }
+        decoder.datumCache[datumIndex] = datum;
         /*
          * Verify if a pre-defined coordinate system can be used. This is often the case, for example
          * the EPSG::6424 coordinate system can be used for (longitude, latitude) axes in degrees.
          * Using a pre-defined CS allows us to get more complete definitions (minimum and maximum values, etc.).
-         *
-         * TODO: verify minimum and maximum longitude values for making sure we have a -180 … 180° range.
          */
-        candidateCS();
         if (coordinateSystem != null) {
             for (int i=dimension; --i >= 0;) {
                 final Axis expected = axes[i];
@@ -260,6 +297,10 @@ previous:   for (int i=components.size(); --i >= 0;) {
                 }
             }
         }
+        /*
+         * If 'candidate(decoder)' did not proposed a coordinate system, or if it proposed a CS but its
+         * axes do not match the axes in the netCDF file, then create a new coordinate system here.
+         */
         final Map<String,?> properties;
         if (coordinateSystem == null) {
             // Fallback if the coordinate system is not common.
@@ -276,7 +317,25 @@ previous:   for (int i=components.size(); --i >= 0;) {
         } else {
             properties = properties(NamedElement.listNames(axes, dimension, " "));
         }
-        return createCRS(decoder.getCRSFactory(), properties);
+        /*
+         * Creates the coordinate reference system using current value of 'datum' and 'coordinateSystem' fields.
+         * TODO: verify minimum and maximum longitude values for making sure we have a -180 … 180° range.
+         */
+        final SingleCRS crs = createCRS(decoder.getCRSFactory(), properties);
+        if (warnings != null) {
+            decoder.listeners.warning(Level.FINE, null, warnings);
+        }
+        return crs;
+    }
+
+    /**
+     * Reports a non-fatal exception that may occur when processing the value returned by {@link #epsgCandidate(Unit)}.
+     * In order to avoid repeating the same warning many times, this method collects the warnings together and reports
+     * them in a single log record after we finished creating the CRS.
+     */
+    final void recoverableException(final NoSuchAuthorityCodeException e) {
+        if (warnings == null) warnings = e;
+        else warnings.addSuppressed(e);
     }
 
     /**
@@ -289,12 +348,34 @@ previous:   for (int i=components.size(); --i >= 0;) {
     }
 
     /**
+     * Returns the EPSG code of a possible coordinate system from EPSG database. This method proceed by brief
+     * inspection of axis directions and units; there is no guarantees that the coordinate system returned by
+     * this method match the axes defined in the netCDF file. It is caller's responsibility to verify.
+     * This is a helper method for {@link #candidate(Decoder)} implementations.
+     *
+     * @param  defaultUnit  the unit to use if unit definition is missing in the netCDF file.
+     * @return EPSG code of a CS candidate, or {@code null} if none.
+     */
+    final Integer epsgCandidate(final Unit<?> defaultUnit) {
+        Unit<?> unit = getFirstAxis().getUnit();
+        if (unit == null) unit = defaultUnit;
+        final AxisDirection[] directions = new AxisDirection[dimension];
+        for (int i=0; i<directions.length; i++) {
+            directions[i] = axes[i].direction;
+        }
+        return CoordinateSystems.getEpsgCode(unit, directions);
+    }
+
+    /**
      * If a brief inspection of unit and direction of the {@linkplain #getFirstAxis() first axis} suggests
      * that a predefined coordinate system could be used, sets the {@link #coordinateSystem} field to that CS.
      * The coordinate system does not need to be a full match since all axes will be verified by the caller.
      * This method is invoked before to fallback on {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
+     *
+     * <p>This method may opportunistically set the {@link #datum} and {@link #candidateCRS} fields if it can
+     * propose a CRS candidate instead than only a CS candidate.</p>
      */
-    abstract void candidateCS();
+    abstract void candidate(Decoder decoder) throws FactoryException;
 
     /**
      * Creates the datum for the coordinate reference system to build. The datum are generally not specified in netCDF files.
@@ -354,7 +435,8 @@ previous:   for (int i=components.size(); --i >= 0;) {
          */
         final boolean isPredefined(final Unit<?> expected) {
             final Axis axis = getFirstAxis();
-            if (expected.equals(axis.getUnit())) {
+            final Unit<?> unit = axis.getUnit();
+            if (unit == null || expected.equals(unit)) {
                 isLongitudeFirst = AxisDirection.EAST.equals(axis.direction);
                 if (isLongitudeFirst || AxisDirection.NORTH.equals(axis.direction)) {
                     return true;
@@ -362,6 +444,19 @@ previous:   for (int i=components.size(); --i >= 0;) {
             }
             return false;
         }
+
+        /**
+         * If the {@link #datum} field is not already set, initialize it to "Not specified (based upon the WGS 84 ellipsoid)".
+         * Subclasses should override this method for setting also the {@link #coordinateSystem} and {@link #candidateCRS}
+         * fields if possible.
+         */
+        @Override void candidate(final Decoder decoder) throws FactoryException {
+            if (datum == null) try {
+                datum = decoder.getDatumAuthorityFactory().createGeodeticDatum(String.valueOf(Constants.EPSG_UNKNOWN_DATUM));
+            } catch (NoSuchAuthorityCodeException e) {
+                recoverableException(e);
+            }
+        }
     }
 
     /**
@@ -374,7 +469,15 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
-        @Override void candidateCS() {
+        @Override void candidate(final Decoder decoder) throws FactoryException {
+            super.candidate(decoder);
+            final Integer epsg = epsgCandidate(Units.DEGREE);
+            if (epsg != null) try {
+                coordinateSystem = decoder.getCSAuthorityFactory().createSphericalCS(epsg.toString());
+                return;
+            } catch (NoSuchAuthorityCodeException e) {
+                recoverableException(e);
+            }
             if (isPredefined(Units.DEGREE)) {
                 coordinateSystem = (SphericalCS) DEFAULT.spherical().getCoordinateSystem();
                 if (isLongitudeFirst) {
@@ -404,13 +507,36 @@ previous:   for (int i=components.size(); --i >= 0;) {
             super((byte) 2);
         }
 
+        /** Tries to creates the coordinate system from EPSG code. */
+        private boolean tryEPSG(final Decoder decoder) throws FactoryException {
+            super.candidate(decoder);               // Initialize the datum.
+            final Integer epsg = epsgCandidate(Units.DEGREE);
+            if (epsg != null) try {
+                coordinateSystem = decoder.getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
+                return true;
+            } catch (NoSuchAuthorityCodeException e) {
+                recoverableException(e);
+            }
+            return false;
+        }
+
         /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
-        @Override void candidateCS() {
+        @Override void candidate(final Decoder decoder) throws FactoryException {
             if (isPredefined(Units.DEGREE)) {
-                coordinateSystem = (is3D() ? DEFAULT.geographic3D() : DEFAULT.geographic()).getCoordinateSystem();
+                if (!is3D()) {
+                    GeographicCRS crs = decoder.getCRSAuthorityFactory().createGeographicCRS(String.valueOf(Constants.EPSG_UNKNOWN_CRS));
+                    coordinateSystem = crs.getCoordinateSystem();
+                    datum = crs.getDatum();
+                    candidateCRS = crs;
+                } else if (!tryEPSG(decoder)) {
+                    coordinateSystem = DEFAULT.geographic3D().getCoordinateSystem();
+                }
                 if (isLongitudeFirst) {
                     coordinateSystem = DefaultEllipsoidalCS.castOrCopy(coordinateSystem).forConvention(AxesConvention.RIGHT_HANDED);
+                    candidateCRS = null;
                 }
+            } else {
+                tryEPSG(decoder);
             }
         }
 
@@ -439,7 +565,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
-        @Override void candidateCS() {
+        @Override void candidate(final Decoder decoder) {
             if (isPredefined(Units.METRE)) {
                 coordinateSystem = DEFAULT.universal(0,0).getCoordinateSystem();
             }
@@ -471,7 +597,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
-        @Override void candidateCS() {
+        @Override void candidate(final Decoder decoder) {
             final Axis axis = getFirstAxis();
             final Unit<?> unit = axis.getUnit();
             final CommonCRS.Vertical predefined;
@@ -516,7 +642,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /** Possibly sets {@link #coordinateSystem} to a predefined CS matching the axes defined in the netCDF file. */
-        @Override void candidateCS() {
+        @Override void candidate(final Decoder decoder) {
             final Axis axis = getFirstAxis();
             final Unit<?> unit = axis.getUnit();
             final CommonCRS.Temporal predefined;
@@ -568,7 +694,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /** No-op since we have no predefined engineering CRS. */
-        @Override void candidateCS() {
+        @Override void candidate(final Decoder decoder) {
         }
 
         /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>. */


Mime
View raw message