sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/02: Consolidation of parsing of "grid_mapping" attributes in netCDF file: - Change order of some methods in Convention class for grouping "domain" or "range" aspects together. - Take in account the "latitude_longitude" pseudo-projection special case. - Add a Convention.defaultHorizontalCRS(boolean) method in replacement of CRSBuilder.DEFAULT constant. - Replace some in-line strings for attribute name by constants.
Date Sat, 27 Apr 2019 16:14:18 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

commit 5d0d7e7334bfd18fb4e16e44094c697ef0e4902d
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Apr 27 18:09:04 2019 +0200

    Consolidation of parsing of "grid_mapping" attributes in netCDF file:
    - Change order of some methods in Convention class for grouping "domain" or "range" aspects together.
    - Take in account the "latitude_longitude" pseudo-projection special case.
    - Add a Convention.defaultHorizontalCRS(boolean) method in replacement of CRSBuilder.DEFAULT constant.
    - Replace some in-line strings for attribute name by constants.
---
 .../internal/referencing/ReferencingUtilities.java |  18 +
 .../referencing/provider/PseudoPlateCarree.java    |  13 +-
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   | 203 ++++++-----
 .../apache/sis/internal/earth/netcdf/GCOM_W.java   |  36 +-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |   2 +-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |  64 ++--
 .../org/apache/sis/internal/netcdf/Convention.java | 384 ++++++++++++---------
 .../apache/sis/internal/netcdf/GridMapping.java    | 170 ++++-----
 8 files changed, 516 insertions(+), 374 deletions(-)

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 a5441de..06f6233 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
@@ -40,6 +40,7 @@ import org.opengis.referencing.datum.VerticalDatumType;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.opengis.util.FactoryException;
 import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.util.Constants;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.CharSequences;
@@ -458,6 +459,23 @@ public final class ReferencingUtilities extends Static {
     }
 
     /**
+     * Returns the a coordinate system for map projections with (easting, northing) axes in metres.
+     * This coordinate system is identified by EPSG:{@value Constants#EPSG_PROJECTED_CS}.
+     *
+     * @param  factory the factory to use for creating the coordinate system, or {@code null} for the default.
+     * @return a coordinate system with (easting, northing) axes in metres.
+     * @throws FactoryException if an error occurred while creating the coordinate system.
+     *
+     * @since 1.0
+     */
+    public static CartesianCS standardProjectedCS(CSAuthorityFactory factory) throws FactoryException {
+        if (factory == null) {
+            factory = DefaultFactories.forBuildin(CSAuthorityFactory.class);
+        }
+        return factory.createCartesianCS(String.valueOf(Constants.EPSG_PROJECTED_CS));
+    }
+
+    /**
      * Returns the mapping between parameter identifiers and parameter names as defined by the given authority.
      * This method assumes that the identifiers of all parameters defined by that authority are numeric.
      * Examples of authorities defining numeric parameters are EPSG and GeoTIFF.
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 42d8fec..75e4961 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
@@ -22,12 +22,16 @@ import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.metadata.iso.citation.Citations;
 
 
 /**
- * The <cite>"Pseudo Plate Carrée"</cite> pseudo-projection (EPSG:9825). This is only the identity transform;
- * even the semi-major and semi-minor axis lengths are fixed to 1. We do not declare that operation method as
- * a {@link org.opengis.referencing.operation.Projection} because axis units are degrees.
+ * The <cite>"Pseudo Plate Carrée"</cite> pseudo-projection (EPSG:9825). This is only the identity transform.
+ * The semi-major and semi-minor axis lengths are ignored (they could be fixed to 1) but nevertheless declared
+ * for allowing netCDF file encoding to declare the ellipsoid in pseudo-projection parameters.
+ *
+ * <p>We do not declare that operation method as a {@link org.opengis.referencing.operation.Projection} because
+ * axis units are degrees.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
@@ -48,7 +52,8 @@ public final class PseudoPlateCarree extends AbstractProvider {
      * The group of all parameters expected by this coordinate operation.
      */
     private static final ParameterDescriptorGroup PARAMETERS = builder()
-            .addName("Pseudo Plate Carree").addIdentifier("9825").createGroup();
+            .addName("Pseudo Plate Carree").addIdentifier("9825")
+            .addName(Citations.NETCDF, "latitude_longitude").createGroupForMapProjection();
 
     /**
      * Constructs a new provider.
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index 0993fc0..477e799 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -218,32 +218,6 @@ public final class GCOM_C extends Convention {
     }
 
     /**
-     * Returns the attribute-specified name of the dimension at the given index, or {@code null} if unspecified.
-     * See {@link Convention#nameOfDimension(Variable, int)} for a more detailed explanation of this information.
-     * The implementation in this class fixes a typo found in some {@code "Dim1"} attribute values and generates
-     * the values when they are known to be missing.
-     *
-     * @param  dataOrAxis  the variable for which to get the attribute-specified name of the dimension.
-     * @param  index       zero-based index of the dimension for which to get the name.
-     * @return dimension name as specified by attributes, or {@code null} if none.
-     */
-    @Override
-    public String nameOfDimension(final Variable dataOrAxis, final int index) {
-        String name = super.nameOfDimension(dataOrAxis, index);
-        if (name == null) {
-            if ("QA_flag".equals(dataOrAxis.getName())) {       // Missing Dim0 and Dim1 for this variable in GCOM-C version 1.00.
-                switch (index) {
-                    case 0: name = "Line grids";  break;
-                    case 1: name = "Pixel grids"; break;
-                }
-            }
-        } else if ("Piexl grids".equalsIgnoreCase(name)) {      // Typo in GCOM-C version 1.00.
-            name = "Pixel grids";
-        }
-        return name;
-    }
-
-    /**
      * Returns whether the given variable is used as a coordinate system axis, a coverage or something else.
      *
      * @param  variable  the variable for which to get the role, or {@code null}.
@@ -265,65 +239,43 @@ public final class GCOM_C extends Convention {
         return role;
     }
 
-    /**
-     * Returns the range of valid values, or {@code null} if unknown.
-     *
-     * @param  data  the variable to get valid range of values for.
-     * @return the range of valid values, or {@code null} if unknown.
-     */
-    @Override
-    public NumberRange<?> validRange(final Variable data) {
-        NumberRange<?> range = super.validRange(data);
-        if (range == null) {
-            final double min = data.getAttributeAsNumber("Minimum_valid_DN");
-            final double max = data.getAttributeAsNumber("Maximum_valid_DN");
-            if (Double.isFinite(min) && Double.isFinite(max)) {
-                range = NumberRange.createBestFit(min, true, max, true);
-            }
-        }
-        return range;
-    }
+
+    // ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+    // │                                      COVERAGE DOMAIN                                       │
+    // └────────────────────────────────────────────────────────────────────────────────────────────┘
+
 
     /**
-     * Returns all no-data values declared for the given variable, or an empty map if none.
-     * The map keys are the no-data values (pad sample values or missing sample values).
-     * The map values are {@link String} instances containing the description of the no-data value.
+     * Returns the attribute-specified name of the dimension at the given index, or {@code null} if unspecified.
+     * See {@link Convention#nameOfDimension(Variable, int)} for a more detailed explanation of this information.
+     * The implementation in this class fixes a typo found in some {@code "Dim1"} attribute values and generates
+     * the values when they are known to be missing.
      *
-     * @param  data  the variable for which to get no-data values.
-     * @return no-data values with textual descriptions.
+     * @param  dataOrAxis  the variable for which to get the attribute-specified name of the dimension.
+     * @param  index       zero-based index of the dimension for which to get the name.
+     * @return dimension name as specified by attributes, or {@code null} if none.
      */
     @Override
-    public Map<Number,Object> nodataValues(final Variable data) {
-        final Map<Number, Object> pads = super.nodataValues(data);
-        for (String name : NO_DATA) {
-            final double value = data.getAttributeAsNumber(name);
-            if (Double.isFinite(value)) {
-                if (name.endsWith(SUFFIX)) {
-                    name = name.substring(0, name.length() - SUFFIX.length());
+    public String nameOfDimension(final Variable dataOrAxis, final int index) {
+        String name = super.nameOfDimension(dataOrAxis, index);
+        if (name == null) {
+            if ("QA_flag".equals(dataOrAxis.getName())) {
+                /*
+                 * The "QA_flag" variable is missing "Dim0" and "Dim1" attribute in GCOM-C version 1.00.
+                 * However not all GCOM-C files use a localization grid. We use the presence of spatial
+                 * resolution attribute as a sentinel value for now.
+                 */
+                if (dataOrAxis.getAttributeType("Spatial_resolution") != null) {
+                    switch (index) {
+                        case 0: name = "Line grids";  break;
+                        case 1: name = "Pixel grids"; break;
+                    }
                 }
-                pads.put(value, name.replace('_', ' '));
             }
+        } else if ("Piexl grids".equalsIgnoreCase(name)) {      // Typo in GCOM-C version 1.00.
+            name = "Pixel grids";
         }
-        return pads;
-    }
-
-    /**
-     * Builds the function converting values from their packed formats in the variable to "real" values.
-     * This method is invoked only if {@link #validRange(Variable)} returned a non-null value.
-     *
-     * @param  data  the variable from which to determine the transfer function.
-     * @return a transfer function built from the attributes defined in the given variable.
-     */
-    @Override
-    public TransferFunction transferFunction(final Variable data) {
-        final TransferFunction tr = super.transferFunction(data);
-        if (tr.isIdentity()) {
-            final double slope  = data.getAttributeAsNumber("Slope");
-            final double offset = data.getAttributeAsNumber("Offset");
-            if (Double.isFinite(slope))  tr.setScale (slope);
-            if (Double.isFinite(offset)) tr.setOffset(offset);
-        }
-        return tr;
+        return name;
     }
 
     /**
@@ -346,10 +298,10 @@ public final class GCOM_C extends Convention {
      * @return name of nodes that may contain the grid mapping, or an empty set if none.
      */
     @Override
-    public Set<String> gridMapping(final Variable data) {
+    public Set<String> nameOfMappingNode(final Variable data) {
         final Set<String> names = new LinkedHashSet<>(4);
         names.add(GEOMETRY_DATA);
-        names.addAll(super.gridMapping(data));              // Fallback if geometry data does not exist.
+        names.addAll(super.nameOfMappingNode(data));            // Fallback if geometry data does not exist.
         return names;
     }
 
@@ -394,9 +346,8 @@ public final class GCOM_C extends Convention {
             return super.projection(node);
         }
         final Map<String,Object> definition = new HashMap<>(4);
-        definition.put(BASE_CRS, CommonCRS.SPHERE.geographic());
         definition.put("grid_mapping_name", method);
-        definition.put("conversion_name", name);
+        definition.put(CONVERSION_NAME, name);
         return definition;
     }
 
@@ -420,18 +371,18 @@ public final class GCOM_C extends Convention {
      * This method is invoked after call to {@link #projection(Node)} resulted in creation of a projected CRS.
      * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} shall have (latitude, longitude) axes in degrees.
      *
-     * @param  node  the same node than the one given to {@link #projection(Node)}.
-     * @param  crs   the projected coordinate reference system created from the information given by {@code node}.
+     * @param  node       the same node than the one given to {@link #projection(Node)}.
+     * @param  baseToCRS  conversion from (latitude, longitude) in degrees to the projected CRS.
      * @return the "grid corner to CRS" transform, or {@code null} if none or unknown.
      * @throws TransformException if a coordinate operation was required but failed.
      */
     @Override
-    public MathTransform gridToCRS(final Node node, final ProjectedCRS crs) throws TransformException {
+    public MathTransform gridToCRS(final Node node, final MathTransform baseToCRS) throws TransformException {
         final double[] corners = new double[CORNERS.length];
         for (int i=0; i<corners.length; i++) {
             corners[i] = node.getAttributeAsNumber(CORNERS[i]);
         }
-        crs.getConversionFromBase().getMathTransform().transform(corners, 0, corners, 0, corners.length / 2);
+        baseToCRS.transform(corners, 0, corners, 0, corners.length / 2);
         /*
          * Compute spans of data (typically in metres) as the average of the spans on both sides
          * (width as length of top and bottom edges, height as length of left and right edges).
@@ -452,6 +403,86 @@ public final class GCOM_C extends Convention {
             m.m12 = corners[1];
             return MathTransforms.linear(m);
         }
-        return super.gridToCRS(node, crs);
+        return super.gridToCRS(node, baseToCRS);
+    }
+
+    /**
+     * Returns the default prime meridian, ellipsoid, datum or CRS to use if no information is found in the netCDF file.
+     * While GCOM documentation said that the datum is WGS 84, we have found that the map projection applied use spherical
+     * formulas.
+     *
+     * @param  spherical  ignored, since we assume a sphere in all cases.
+     * @return information about geodetic objects to use if no explicit information is found in the file.
+     */
+    @Override
+    public CommonCRS defaultHorizontalCRS(final boolean spherical) {
+        return CommonCRS.SPHERE;
+    }
+
+
+    // ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+    // │                                       COVERAGE RANGE                                       │
+    // └────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+    /**
+     * Returns the range of valid values, or {@code null} if unknown.
+     *
+     * @param  data  the variable to get valid range of values for.
+     * @return the range of valid values, or {@code null} if unknown.
+     */
+    @Override
+    public NumberRange<?> validRange(final Variable data) {
+        NumberRange<?> range = super.validRange(data);
+        if (range == null) {
+            final double min = data.getAttributeAsNumber("Minimum_valid_DN");
+            final double max = data.getAttributeAsNumber("Maximum_valid_DN");
+            if (Double.isFinite(min) && Double.isFinite(max)) {
+                range = NumberRange.createBestFit(min, true, max, true);
+            }
+        }
+        return range;
+    }
+
+    /**
+     * Returns all no-data values declared for the given variable, or an empty map if none.
+     * The map keys are the no-data values (pad sample values or missing sample values).
+     * The map values are {@link String} instances containing the description of the no-data value.
+     *
+     * @param  data  the variable for which to get no-data values.
+     * @return no-data values with textual descriptions.
+     */
+    @Override
+    public Map<Number,Object> nodataValues(final Variable data) {
+        final Map<Number, Object> pads = super.nodataValues(data);
+        for (String name : NO_DATA) {
+            final double value = data.getAttributeAsNumber(name);
+            if (Double.isFinite(value)) {
+                if (name.endsWith(SUFFIX)) {
+                    name = name.substring(0, name.length() - SUFFIX.length());
+                }
+                pads.put(value, name.replace('_', ' '));
+            }
+        }
+        return pads;
+    }
+
+    /**
+     * Builds the function converting values from their packed formats in the variable to "real" values.
+     * This method is invoked only if {@link #validRange(Variable)} returned a non-null value.
+     *
+     * @param  data  the variable from which to determine the transfer function.
+     * @return a transfer function built from the attributes defined in the given variable.
+     */
+    @Override
+    public TransferFunction transferFunction(final Variable data) {
+        final TransferFunction tr = super.transferFunction(data);
+        if (tr.isIdentity()) {
+            final double slope  = data.getAttributeAsNumber("Slope");
+            final double offset = data.getAttributeAsNumber("Offset");
+            if (Double.isFinite(slope))  tr.setScale (slope);
+            if (Double.isFinite(offset)) tr.setOffset(offset);
+        }
+        return tr;
     }
 }
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
index 9c53ad6..233dc84 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
@@ -169,6 +169,30 @@ public final class GCOM_W extends Convention {
         return role;
     }
 
+
+    // ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+    // │                                      COVERAGE DOMAIN                                       │
+    // └────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+    /**
+     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
+     * localization grid more linear.
+     *
+     * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
+     * @return enumeration of two-dimensional non-linear transforms to try.
+     */
+    @Override
+    public Set<Linearizer> linearizers(final Decoder decoder) {
+        return Collections.singleton(Linearizer.GROUND_TRACK);
+    }
+
+
+    // ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+    // │                                       COVERAGE RANGE                                       │
+    // └────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
     /**
      * Returns no-data values declared for the given variable.
      *
@@ -207,16 +231,4 @@ public final class GCOM_W extends Convention {
         }
         return tr;
     }
-
-    /**
-     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
-     * localization grid more linear.
-     *
-     * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
-     * @return enumeration of two-dimensional non-linear transforms to try.
-     */
-    @Override
-    public Set<Linearizer> linearizers(final Decoder decoder) {
-        return Collections.singleton(Linearizer.GROUND_TRACK);
-    }
 }
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 86741d6..01e5957 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
@@ -1373,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 = getCSAuthorityFactory().createCartesianCS(String.valueOf(Constants.EPSG_PROJECTED_CS));
+                CartesianCS cs = ReferencingUtilities.standardProjectedCS(getCSAuthorityFactory());
                 if (!Units.METRE.equals(linearUnit)) {
                     cs = replaceLinearUnit(cs, linearUnit);
                 }
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 a214e3f..d3d6b60 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
@@ -45,6 +45,7 @@ import org.apache.sis.referencing.crs.AbstractCRS;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
 import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
 import org.apache.sis.internal.referencing.provider.Equirectangular;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.util.TemporalUtilities;
 import org.apache.sis.storage.DataStoreContentException;
@@ -79,16 +80,6 @@ import org.apache.sis.math.Vector;
  */
 abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
     /**
-     * The coordinate reference system which is presumed the basis of datum on netCDF files.
-     * Note: if this default is changed, search also for "GRS 1980" strings in this class.
-     *
-     * <div class="note"><b>Note:</b> we use GRS 1980 instead than WGS 84 because the CRS name
-     * clearly said "Unknown datum based upon the GRS 1980 ellipsoid" and for consistency with
-     * {@link CommonCRS#SPHERE}, which also use GRS 1980.</div>
-     */
-    static final CommonCRS DEFAULT = CommonCRS.GRS1980;
-
-    /**
      * The type of datum as a GeoAPI sub-interface of {@link Datum}.
      * Used for verifying the type of cached datum at {@link #datumIndex}.
      */
@@ -290,8 +281,9 @@ previous:   for (int i=components.size(); --i >= 0;) {
         datum = datumType.cast(decoder.datumCache[datumIndex]);         // Should be before 'setPredefinedComponents' call.
         setPredefinedComponents(decoder);
         /*
-         * If 'setPredefinedComponents(decoder)' offers a datum, we will used it as-is. Otherwise create the datum now.
-         * Datum are often not defined in netCDF files, so we use EPSG::6019 — "Not specified (based on GRS 1980 ellipsoid)".
+         * If `setPredefinedComponents(decoder)` offers a datum, we will used it as-is. Otherwise create the datum now.
+         * Datum are often not defined in netCDF files, so the above `setPredefinedComponents` method call may have set
+         * EPSG::6019 — "Not specified (based on GRS 1980 ellipsoid)". If not, we build a similar name.
          */
         if (datum == null) {
             // Not localized because stored as a String, possibly exported in WKT or GML, and 'datumBase' is in English.
@@ -454,6 +446,11 @@ previous:   for (int i=components.size(); --i >= 0;) {
      */
     private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> {
         /**
+         * The coordinate reference system which is presumed the basis of datum on netCDF files.
+         */
+        protected CommonCRS defaultCRS;
+
+        /**
          * Whether the coordinate system has longitude before latitude.
          * This flag is set as a side-effect of {@link #isPredefinedCS(Unit)} method call.
          */
@@ -469,11 +466,18 @@ previous:   for (int i=components.size(); --i >= 0;) {
         }
 
         /**
-         * Creates a {@link GeodeticDatum} for <cite>"Unknown datum based on GRS 1980"</cite>.
+         * Initializes this builder before {@link #build(Decoder)} execution.
+         */
+        @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
+            defaultCRS = decoder.convention().defaultHorizontalCRS(false);
+        }
+
+        /**
+         * Creates a {@link GeodeticDatum} for <cite>"Unknown datum presumably based on GRS 1980"</cite>.
          * This method is invoked only if {@link #setPredefinedComponents(Decoder)} failed to create a datum.
          */
         @Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
-            final GeodeticDatum template = DEFAULT.datum();
+            final GeodeticDatum template = defaultCRS.datum();
             datum = factory.createGeodeticDatum(properties, template.getEllipsoid(), template.getPrimeMeridian());
         }
 
@@ -519,8 +523,9 @@ previous:   for (int i=components.size(); --i >= 0;) {
          * matching the axes defined in the netCDF file.
          */
         @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
+            super.setPredefinedComponents(decoder);
             if (isPredefinedCS(Units.DEGREE)) {
-                GeocentricCRS crs = DEFAULT.spherical();
+                GeocentricCRS crs = defaultCRS.spherical();
                 if (isLongitudeFirst) {
                     crs = DefaultGeocentricCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
                 }
@@ -528,7 +533,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
                 coordinateSystem = (SphericalCS) crs.getCoordinateSystem();
                 datum            = crs.getDatum();
             } else {
-                datum = DEFAULT.datum();
+                datum = defaultCRS.datum();
             }
         }
 
@@ -570,23 +575,24 @@ previous:   for (int i=components.size(); --i >= 0;) {
          * to predefined objects matching the axes defined in the netCDF file.
          */
         @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
+            super.setPredefinedComponents(decoder);
             if (isPredefinedCS(Units.DEGREE)) {
                 GeographicCRS crs;
                 if (is3D()) {
-                    crs = DEFAULT.geographic3D();
+                    crs = defaultCRS.geographic3D();
                     if (isLongitudeFirst) {
                         crs = DefaultGeographicCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
                     }
                 } else if (isLongitudeFirst) {
-                    crs = DEFAULT.normalizedGeographic();
+                    crs = defaultCRS.normalizedGeographic();
                 } else {
-                    crs = DEFAULT.geographic();
+                    crs = defaultCRS.geographic();
                 }
                 referenceSystem  = crs;
                 coordinateSystem = crs.getCoordinateSystem();
                 datum            = crs.getDatum();
             } else {
-                datum = DEFAULT.datum();
+                datum = defaultCRS.datum();
                 final Integer epsg = epsgCandidateCS(Units.DEGREE);
                 if (epsg != null) try {
                     coordinateSystem = decoder.getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
@@ -629,14 +635,13 @@ previous:   for (int i=components.size(); --i >= 0;) {
      */
     private static final class Projected extends Geodetic<CartesianCS> {
         /**
-         * The spherical variant of {@link CRSBuilder#DEFAULT}.
-         * Currently based upon the GRS 1980 Authalic Sphere.
+         * The spherical variant of {@link #defaultCRS}.
          */
-        private static final CommonCRS SPHERICAL = CommonCRS.SPHERE;
+        private CommonCRS sphericalDatum;
 
         /**
          * Defining conversion for "Not specified (presumed Plate Carrée)". This conversion use spherical formulas.
-         * Consequently it should be used with {@link #SPHERICAL} instead of {@link CommonCRS#DEFAULT}.
+         * Consequently it should be used with {@link #sphericalDatum} instead of {@link #defaultCRS}.
          */
         private static final Conversion UNKNOWN_PROJECTION;
         static {
@@ -663,9 +668,11 @@ previous:   for (int i=components.size(); --i >= 0;) {
          * to predefined objects matching the axes defined in the netCDF file.
          */
         @Override void setPredefinedComponents(final Decoder decoder) throws FactoryException {
-            datum = SPHERICAL.datum();
+            super.setPredefinedComponents(decoder);
+            sphericalDatum = decoder.convention().defaultHorizontalCRS(true);
+            datum = sphericalDatum.datum();
             if (isPredefinedCS(Units.METRE)) {
-                coordinateSystem = CommonCRS.WGS84.universal(0,0).getCoordinateSystem();
+                coordinateSystem = ReferencingUtilities.standardProjectedCS(decoder.getCSAuthorityFactory());
             }
         }
 
@@ -684,10 +691,11 @@ previous:   for (int i=components.size(); --i >= 0;) {
 
         /**
          * Creates the coordinate reference system from datum and coordinate system computed in previous steps.
-         * The datum for this method is based upon the GRS 1980 Authalic Sphere.
+         * The datum for this method is based on a sphere.
          */
         @Override void createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
-            GeographicCRS baseCRS = (coordinateSystem.getDimension() >= 3) ? SPHERICAL.geographic3D() : SPHERICAL.geographic();
+            final boolean is3D = (coordinateSystem.getDimension() >= 3);
+            GeographicCRS baseCRS = is3D ? sphericalDatum.geographic3D() : sphericalDatum.geographic();
             if (!baseCRS.getDatum().equals(datum)) {
                 baseCRS = factory.createGeographicCRS(properties, datum, baseCRS.getCoordinateSystem());
             }
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 3a5939f..96d32cf 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
@@ -230,6 +230,12 @@ public class Convention {
         return VariableRole.OTHER;
     }
 
+
+    // ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+    // │                                      COVERAGE DOMAIN                                       │
+    // └────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
     /**
      * Returns the names of the variables containing data for all dimension of a variable.
      * Each netCDF variable can have an arbitrary number of dimensions identified by their name.
@@ -308,7 +314,7 @@ public class Convention {
      * This value may be different than 1 if the localization grid is smaller than the data grid,
      * as documented in the {@link #nameOfDimension(Variable, int)}.
      *
-     * <p>Default implementation returns the inverse of {@code "resampling_interval"} attribute value.
+     * <p>Default implementation returns the {@code "resampling_interval"} attribute value.
      * This feature is an extension to CF-conventions.</p>
      *
      * @param  axis  the axis for which to get the "grid indices to data indices" scale factor.
@@ -319,6 +325,217 @@ public class Convention {
     }
 
     /**
+     * Returns an enumeration of two-dimensional non-linear transforms (usually map projections) that may result
+     * in more linear localization grids. The enumerated transforms will be tested in "trials and errors" and the
+     * one resulting in best {@linkplain org.apache.sis.math.Plane#fit linear regression correlation coefficients}
+     * will be selected.
+     *
+     * <p>Default implementation returns an empty set.</p>
+     *
+     * @param  decoder  the netCDF file for which to get linearizer candidates.
+     * @return enumeration of two-dimensional non-linear transforms to try on the localization grid.
+     */
+    public Set<Linearizer> linearizers(final Decoder decoder) {
+        return Collections.emptySet();
+    }
+
+    /**
+     * Returns the name of nodes (variables or groups) that may define the map projection parameters.
+     * The variables or groups will be inspected in the order they are declared in the returned set.
+     * For each string in the set, {@link Decoder#findNode(String)} is invoked and the return value
+     * (if non-null) is given to {@link #projection(Node)} until a non-null map is obtained.
+     *
+     * <div class="note"><b>API note:</b>
+     * this method name is singular because even if a set is returned, in the end only one value is used.</div>
+     *
+     * The default implementation returns the value of {@link CF#GRID_MAPPING} attribute, or an empty set
+     * if the given variable does not contain that attribute. Subclasses may override for example if grid
+     * mapping information are hard-coded in a particular node for a specific product.
+     *
+     * @param  data  the variable for which to get the grid mapping node.
+     * @return name of nodes that may contain the grid mapping, or an empty set if none.
+     */
+    public Set<String> nameOfMappingNode(final Variable data) {
+        final String mapping = data.getAttributeAsString(CF.GRID_MAPPING);
+        return (mapping != null) ? Collections.singleton(mapping) : Collections.emptySet();
+    }
+
+    /**
+     * The {@value} attribute name from CF-convention, defined here because not yet provided in {@link CF}.
+     * Associated value shall be an instance of {@link Number}. This field may be removed in a future SIS
+     * version if this constant become defined in {@link ucar.nc2.constants}.
+     */
+    protected static final String LONGITUDE_OF_PRIME_MERIDIAN = "longitude_of_prime_meridian";
+
+    /**
+     * The {@value} attribute name from CF-convention, defined here because not yet provided in {@link CF}.
+     * Associated value shall be an instance of {@link String}. This field may be removed in a future SIS
+     * version if this constant become defined in {@link ucar.nc2.constants}.
+     */
+    protected static final String ELLIPSOID_NAME      = "reference_ellipsoid_name",
+                                  PRIME_MERIDIAN_NAME = "prime_meridian_name",
+                                  GEODETIC_DATUM_NAME = "horizontal_datum_name",
+                                  GEOGRAPHIC_CRS_NAME = "geographic_crs_name",
+                                  PROJECTED_CRS_NAME  = "projected_crs_name";
+
+    /**
+     * The {@value} attribute name, not yet part of CF-convention.
+     */
+    protected static final String CONVERSION_NAME = "conversion_name";
+
+    /**
+     * The {@value} attribute name from CF-convention, defined here because not yet provided in {@link CF}.
+     * Associated value shall be an instance of {@link BursaWolfParameters}.
+     */
+    protected static final String TOWGS84 = "towgs84";
+
+    /**
+     * Returns the map projection defined by the given node. The given {@code node} argument is one of the nodes
+     * named by {@link #nameOfMappingNode(Variable)} (typically a variable referenced by {@value CF#GRID_MAPPING}
+     * attribute on the data variable), or if no grid mapping attribute is found {@code node} may be directly the
+     * data variable (not a CF-compliant approach, but found in practice). If non-null, the returned map contains
+     * the following information ({@value CF#GRID_MAPPING_NAME} is mandatory, all other entries are optional):
+     *
+     * <table class="sis">
+     *   <caption>Content of the returned map</caption>
+     *   <tr>
+     *     <th>Key</th>
+     *     <th>Value type</th>
+     *     <th>Description</th>
+     *   </tr><tr>
+     *     <td>{@value CF#GRID_MAPPING_NAME}</td>
+     *     <td>{@link String}</td>
+     *     <td>Operation method name <strong>(mandatory)</strong></td>
+     *   </tr><tr>
+     *     <td>{@code "*_name"}</td>
+     *     <td>{@link String}</td>
+     *     <td>Name of a component (datum, base CRS, …)</td>
+     *   </tr><tr>
+     *     <td>{@value #LONGITUDE_OF_PRIME_MERIDIAN}</td>
+     *     <td>{@link Number}</td>
+     *     <td>Value in degrees relative to reference meridian.</td>
+     *   </tr><tr>
+     *     <td>(projection-dependent)</td>
+     *     <td>{@link Number} or {@code double[]}</td>
+     *     <td>Map projection parameter values</td>
+     *   </tr><tr>
+     *     <td>{@value #TOWGS84}</td>
+     *     <td>{@link BursaWolfParameters}</td>
+     *     <td>Datum shift information.</td>
+     *   </tr>
+     * </table>
+     *
+     * The returned map must be modifiable for allowing callers to modify its content.
+     *
+     * @param  node  the {@value CF#GRID_MAPPING} variable (preferred) or the data variable (as a fallback) from which to read attributes.
+     * @return the map projection definition as a modifiable map, or {@code null} if none.
+     *
+     * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
+     */
+    public Map<String,Object> projection(final Node node) {
+        final String method = node.getAttributeAsString(CF.GRID_MAPPING_NAME);
+        if (method == null) {
+            return null;
+        }
+        final Map<String,Object> definition = new HashMap<>();
+        definition.put(CF.GRID_MAPPING_NAME, method);
+        for (final String name : node.getAttributeNames()) {
+            final String ln = name.toLowerCase(Locale.US);
+            final Object value;
+            if (ln.endsWith("_name")) {
+                value = node.getAttributeAsString(name);
+                if (value == null) continue;
+                break;
+            } else switch (ln) {
+                case CF.GRID_MAPPING_NAME: continue;        // Already stored.
+                case TOWGS84: {
+                    /*
+                     * Conversion to WGS 84 datum may be specified as Bursa-Wolf parameters. Encoding this information
+                     * with the CRS is deprecated (the hard-coded WGS84 target datum is not always suitable) but still
+                     * a common practice as of 2019. We require at least the 3 translation parameters.
+                     */
+                    final Object[] values = node.getAttributeValues(name, true);
+                    if (values.length < 3) continue;
+                    final BursaWolfParameters bp = new BursaWolfParameters(CommonCRS.WGS84.datum(), null);
+                    bp.setValues(Vector.create(values, false).doubleValues());
+                    value = bp;
+                    break;
+                }
+                case "crs_wkt": {
+                    /*
+                     * CF-Convention said that even if a WKT definition is provided, other attributes shall be present
+                     * and have precedence over the WKT definition. Consequently purpose of WKT in netCDF files is not
+                     * obvious (except for CompoundCRS). We ignore them for now.
+                     */
+                    continue;
+                }
+                default: {
+                    /*
+                     * Assume that all map projection parameters in netCDF files are numbers or array of numbers.
+                     */
+                    final Object[] values = node.getAttributeValues(name, true);
+                    switch (values.length) {
+                        case 0:  continue;                       // Attribute not found or not numeric.
+                        case 1:  value = values[0]; break;       // This is the usual case.
+                        default: value = Vector.create(values, false).doubleValues(); break;
+                    }
+                    break;
+                }
+            }
+            if (definition.putIfAbsent(name, value) != null) {
+                node.error(Convention.class, "projection", null, Errors.Keys.DuplicatedIdentifier_1, name);
+            }
+        }
+        return definition;
+    }
+
+    /**
+     * Returns the <cite>grid to CRS</cite> transform for the given node. This method is invoked after call
+     * to {@link #projection(Node)} method resulted in creation of a projected coordinate reference system.
+     * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} is fixed to (latitude, longitude) axes in degrees,
+     * but the projected CRS axes may have any order and units. In the particular case of "latitude_longitude"
+     * pseudo-projection, the "projected" CRS is actually a {@link GeographicCRS} instance.
+     * The returned transform, if non-null, shall map cell corners.
+     *
+     * <div class="note"><b>API notes:</b>
+     * <ul>
+     *   <li>We do not provide a {@link ProjectedCRS} argument because of the {@code "latitude_longitude"} special case.</li>
+     *   <li>Base CRS axis order is (latitude, longitude) for increasing the chances to have a CRS identified by EPSG.</li>
+     * </ul></div>
+     *
+     * The default implementation returns {@code null}.
+     *
+     * @param  node       the same node than the one given to {@link #projection(Node)}.
+     * @param  baseToCRS  conversion from (latitude, longitude) in degrees to the projected CRS.
+     * @return the <cite>grid corner to CRS</cite> transform, or {@code null} if none or unknown.
+     * @throws TransformException if a coordinate operation was required but failed.
+     */
+    public MathTransform gridToCRS(final Node node, final MathTransform baseToCRS) throws TransformException {
+        return null;
+    }
+
+    /**
+     * Returns an identification of default geodetic components to use if no corresponding information is found in the
+     * netCDF file. The default implementation returns <cite>"Unknown datum based upon the GRS 1980 ellipsoid"</cite>.
+     * Note that the GRS 1980 ellipsoid is close to WGS 84 ellipsoid.
+     *
+     * <div class="note"><b>Maintenance note:</b>
+     * if this default is changed, search also for "GRS 1980" strings in {@link CRSBuilder} class.</div>
+     *
+     * @param  spherical  whether to restrict the ellipsoid to a sphere.
+     * @return information about geodetic objects to use if no explicit information is found in the file.
+     */
+    public CommonCRS defaultHorizontalCRS(final boolean spherical) {
+        return spherical ? CommonCRS.SPHERE : CommonCRS.GRS1980;
+    }
+
+
+    // ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+    // │                                       COVERAGE RANGE                                       │
+    // └────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+    /**
      * Returns the range of valid values, or {@code null} if unknown.
      * The default implementation takes the range of values from the following properties, in precedence order:
      *
@@ -462,169 +679,4 @@ public class Convention {
         if (!Double.isNaN(offset)) tr.setOffset(offset);
         return tr;
     }
-
-    /**
-     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
-     * localization grid more linear. Default implementation returns an empty set. If this method is overridden,
-     * the enumerated transforms will be tested in "trials and errors" and the one resulting in best correlation
-     * coefficients will be selected.
-     *
-     * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
-     * @return enumeration of two-dimensional non-linear transforms to try.
-     */
-    public Set<Linearizer> linearizers(final Decoder decoder) {
-        return Collections.emptySet();
-    }
-
-    /**
-     * Returns the name of nodes (variables or groups) that may define the map projection parameters.
-     * The variables or groups will be inspected in the order they are declared in the returned set.
-     * For each string in the set, {@link Decoder#findNode(String)} is invoked and the return value
-     * (if non-null) is given to {@link #projection(Node)}.
-     *
-     * <p>The default implementation returns the value of {@link CF#GRID_MAPPING}, or an empty set
-     * if the given variable does not contain that attribute. Subclasses may override for example
-     * if grid mapping information are hard-coded in a particular node for a specific product.</p>
-     *
-     * @param  data  the variable for which to get the grid mapping node.
-     * @return name of nodes that may contain the grid mapping, or an empty set if none.
-     */
-    public Set<String> gridMapping(final Variable data) {
-        final String mapping = data.getAttributeAsString(CF.GRID_MAPPING);
-        return (mapping != null) ? Collections.singleton(mapping) : Collections.emptySet();
-    }
-
-    /**
-     * Key associated to {@link GeographicCRS} value in the map returned by {@link #projection(Node)}.
-     * The geographic CRS associated to this key <strong>must</strong> have (latitude, longitude) axes
-     * in degrees.
-     */
-    protected static final String BASE_CRS = "base_crs";
-
-    /**
-     * Returns the map projection defined by the given node. The given {@code node} argument is one of the nodes
-     * named by {@link #gridMapping(Variable)} (typically a variable named by {@value CF#GRID_MAPPING} attribute),
-     * or if no grid mapping attribute is found {@code node} may be directly the data variable (non CF-compliant,
-     * but found in practice). If non-null, the returned map contains the following information:
-     *
-     * <table class="sis">
-     *   <caption>Content of the returned map</caption>
-     *   <tr>
-     *     <th>Key</th>
-     *     <th>Value type</th>
-     *     <th>Description</th>
-     *     <th>Default value</th>
-     *   </tr><tr>
-     *     <td>{@value CF#GRID_MAPPING_NAME}</td>
-     *     <td>{@link String}</td>
-     *     <td>Operation method name</td>
-     *     <td>Value of {@value CF#GRID_MAPPING_NAME} attribute.</td>
-     *   </tr><tr>
-     *     <td>{@value #BASE_CRS}</td>
-     *     <td>{@link GeographicCRS}</td>
-     *     <td>Base CRS of the map projection</td>
-     *     <td>Unknown datum based upon the GRS 1980 ellipsoid.</td>
-     *   </tr><tr>
-     *     <td>{@code "*_name"}</td>
-     *     <td>{@link String}</td>
-     *     <td>Name of a component (datum, base CRS, …)</td>
-     *     <td>Attribute values found on grid mapping variable.</td>
-     *   </tr><tr>
-     *     <td>(projection-dependent)</td>
-     *     <td>{@link Number} or {@code double[]}</td>
-     *     <td>Map projection parameter values</td>
-     *     <td>Attribute values found on grid mapping variable.</td>
-     *   </tr><tr>
-     *     <td>{@code "towgs84"}</td>
-     *     <td>{@link BursaWolfParameters}</td>
-     *     <td>Datum shift information.</td>
-     *     <td>Built from {@code "towgs84"} attribute values.</td>
-     *   </tr>
-     * </table>
-     *
-     * The {@value CF#GRID_MAPPING_NAME} entry is mandatory. All other entries are optional.
-     *
-     * <p>Subclasses can override this method for example in order to override the {@value #BASE_CRS} attribute
-     * if they know that a particular product is based on "World Geodetic System 1984" or other datum.
-     * The returned map must be modifiable for allowing callers to modify its content.</p>
-     *
-     * @param  node  the {@value CF#GRID_MAPPING} variable (preferred) or the data variable (as a fallback) from which to read attributes.
-     * @return the map projection definition as a modifiable map, or {@code null} if none.
-     *
-     * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
-     */
-    public Map<String,Object> projection(final Node node) {
-        final String method = node.getAttributeAsString(CF.GRID_MAPPING_NAME);
-        if (method == null) {
-            return null;
-        }
-        final Map<String,Object> definition = new HashMap<>();
-        definition.put(CF.GRID_MAPPING_NAME, method);
-        for (final String name : node.getAttributeNames()) {
-            final String ln = name.toLowerCase(Locale.US);
-            final Object value;
-            if (ln.endsWith("_name")) {
-                value = node.getAttributeAsString(name);
-                if (value == null) continue;
-                break;
-            } else switch (ln) {
-                case CF.GRID_MAPPING_NAME: continue;        // Already stored.
-                case "towgs84": {
-                    /*
-                     * Conversion to WGS 84 datum may be specified as Bursa-Wolf parameters. Encoding this information
-                     * with the CRS is deprecated (the hard-coded WGS84 target datum is not always suitable) but still
-                     * a common practice as of 2019. We require at least the 3 translation parameters.
-                     */
-                    final Object[] values = node.getAttributeValues(name, true);
-                    if (values.length < 3) continue;
-                    final BursaWolfParameters bp = new BursaWolfParameters(CommonCRS.WGS84.datum(), null);
-                    bp.setValues(Vector.create(values, false).doubleValues());
-                    value = bp;
-                    break;
-                }
-                case "crs_wkt": {
-                    /*
-                     * CF-Convention said that even if a WKT definition is provided, other attributes shall be present
-                     * and have precedence over the WKT definition. Consequently purpose of WKT in netCDF files is not
-                     * obvious (except for CompoundCRS). We ignore them for now.
-                     */
-                    continue;
-                }
-                default: {
-                    /*
-                     * Assume that all map projection parameters in netCDF files are numbers or array of numbers.
-                     */
-                    final Object[] values = node.getAttributeValues(name, true);
-                    switch (values.length) {
-                        case 0:  continue;                       // Attribute not found or not numeric.
-                        case 1:  value = values[0]; break;       // This is the usual case.
-                        default: value = Vector.create(values, false).doubleValues(); break;
-                    }
-                    break;
-                }
-            }
-            if (definition.putIfAbsent(name, value) != null) {
-                node.error(Convention.class, "projection", null, Errors.Keys.DuplicatedIdentifier_1, name);
-            }
-        }
-        return definition;
-    }
-
-    /**
-     * Returns the <cite>grid to CRS</cite> transform for the given node. This method is invoked after call
-     * to {@link #projection(Node)} method resulted in creation of a projected coordinate reference system.
-     * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} shall have (latitude, longitude) axes in degrees.
-     * The projected CRS axes may have any order and units.
-     * The returned transform, if non-null, shall map cell corners.
-     *
-     * <p>The default implementation returns {@code null}.</p>
-     *
-     * @param  node  the same node than the one given to {@link #projection(Node)}.
-     * @param  crs   the projected coordinate reference system created from the information given by {@code node}.
-     * @return the "grid corner to CRS" transform, or {@code null} if none or unknown.
-     * @throws TransformException if a coordinate operation was required but failed.
-     */
-    public MathTransform gridToCRS(final Node node, final ProjectedCRS crs) throws TransformException {
-        return null;
-    }
 }
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 e7d28bc..b6b9d6f 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
@@ -29,7 +29,6 @@ import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.cs.CartesianCS;
 import org.opengis.referencing.cs.CoordinateSystem;
-import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -50,6 +49,7 @@ import org.apache.sis.referencing.crs.AbstractCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
 import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
@@ -57,6 +57,8 @@ import org.apache.sis.internal.metadata.AxisDirections;
 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.ReferencingUtilities;
+import org.apache.sis.internal.referencing.provider.PseudoPlateCarree;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.util.resources.Vocabulary;
@@ -121,7 +123,7 @@ final class GridMapping {
      */
     static GridMapping forVariable(final Variable variable) {
         final Map<Object,GridMapping> gridMapping = variable.decoder.gridMapping;
-        for (final String name : variable.decoder.convention().gridMapping(variable)) {
+        for (final String name : variable.decoder.convention().nameOfMappingNode(variable)) {
             GridMapping gm = gridMapping.get(name);
             if (gm != null) {
                 return gm;
@@ -160,7 +162,7 @@ final class GridMapping {
      * Otherwise returns {@code null}. The given {@code node} argument is typically a dummy variable referenced by value
      * of the {@value CF#GRID_MAPPING} attribute on the real data variable (as required by CF-conventions), but may also
      * be something else (the data variable itself, or a group, <i>etc.</i>). That node, together with the attributes to
-     * be parsed, depend on the {@link Convention} instance.
+     * be parsed, depends on the {@link Convention} instance.
      *
      * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
      */
@@ -171,7 +173,7 @@ final class GridMapping {
              * Fetch now numerical values that are not map projection parameters.
              * This step needs to be done before to try to set parameter values.
              */
-            final Object greenwichLongitude = definition.remove("longitude_of_prime_meridian");
+            final Object greenwichLongitude = definition.remove(Convention.LONGITUDE_OF_PRIME_MERIDIAN);
             /*
              * Prepare the block of projection parameters. The set of legal parameter depends on the map projection.
              * We assume that all numerical values are map projection parameters; character sequences (assumed to be
@@ -180,7 +182,7 @@ final class GridMapping {
              * the redundant parameters like "inverse_flattening" and "earth_radius".
              */
             final CoordinateOperationFactory opFactory = node.decoder.getCoordinateOperationFactory();
-            final OperationMethod method = opFactory.getOperationMethod((String) definition.get(CF.GRID_MAPPING_NAME));
+            final OperationMethod method = opFactory.getOperationMethod((String) definition.remove(CF.GRID_MAPPING_NAME));
             final ParameterValueGroup parameters = method.getParameters().createValue();
             for (final Map.Entry<String,Object> entry : definition.entrySet()) {
                 final String name  = entry.getKey();
@@ -197,82 +199,27 @@ final class GridMapping {
              * But if those information are provided, then we use them for building the geodetic reference frame.
              * Otherwise a default reference frame will be used.
              */
-            final CRSFactory crsFactory = node.decoder.getCRSFactory();
-            GeographicCRS baseCRS = (GeographicCRS) definition.get(Convention.BASE_CRS);
-            if (baseCRS == null) {
-                final DatumFactory datumFactory = node.decoder.getDatumFactory();
-                final CommonCRS defaultDefinitions = CRSBuilder.DEFAULT;
-                boolean isSpecified = false;
-                /*
-                 * Prime meridian built from "longitude_of_prime_meridian".
-                 */
-                final PrimeMeridian meridian;
-                if (greenwichLongitude instanceof Number) {
-                    final double longitude = ((Number) greenwichLongitude).doubleValue();
-                    final Map<String,?> properties = properties(definition, "prime_meridian_name", 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 ellipsoid;
-                try {
-                    final double semiMajor = parameters.parameter(Constants.SEMI_MAJOR).doubleValue();
-                    final Map<String,?> properties = properties(definition, "reference_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);
-                    } else {
-                        final double semiMinor = parameters.parameter(Constants.SEMI_MINOR).doubleValue();
-                        ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, semiMinor, Units.METRE);
-                    }
-                    isSpecified = true;
-                } catch (ParameterNotFoundException | IllegalStateException e) {
-                    // Ignore - may be normal if the map projection is not an Apache SIS implementation.
-                    ellipsoid = defaultDefinitions.ellipsoid();
-                }
-                /*
-                 * Geodetic datum built from "towgs84" and above properties.
-                 */
-                final Object bursaWolf = definition.remove("towgs84");
-                final GeodeticDatum datum;
-                if (isSpecified | bursaWolf != null) {
-                    Map<String,Object> properties = properties(definition, "horizontal_datum_name", ellipsoid);
-                    if (bursaWolf instanceof BursaWolfParameters) {
-                        properties = new HashMap<>(properties);
-                        properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf);
-                        isSpecified = true;
-                    }
-                    datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian);
-                } else {
-                    datum = defaultDefinitions.datum();
-                }
-                /*
-                 * Geographic CRS form all above properties.
-                 */
-                if (isSpecified) {
-                    final Map<String,?> properties = properties(definition, "geographic_coordinate_system_name", datum);
-                    baseCRS = crsFactory.createGeographicCRS(properties, datum, defaultDefinitions.geographic().getCoordinateSystem());
-                } else {
-                    baseCRS = defaultDefinitions.geographic();
-                }
+            final GeographicCRS baseCRS = createBaseCRS(node.decoder, parameters, definition, greenwichLongitude);
+            final MathTransform baseToCRS;
+            final CoordinateReferenceSystem crs;
+            if (method instanceof PseudoPlateCarree) {
+                // Only swap axis order from (latitude, longitude) to (longitude, latitude).
+                baseToCRS = MathTransforms.linear(new Matrix3(0, 1, 0, 1, 0, 0, 0, 0, 1));
+                crs = baseCRS;
+            } else {
+                Map<String,?> properties = properties(definition, Convention.CONVERSION_NAME, node.getName());
+                final Conversion conversion = opFactory.createDefiningConversion(properties, method, parameters);
+                final CartesianCS cs = ReferencingUtilities.standardProjectedCS(node.decoder.getCSAuthorityFactory());
+                properties = properties(definition, Convention.PROJECTED_CRS_NAME, conversion);
+                final ProjectedCRS p = node.decoder.getCRSFactory().createProjectedCRS(properties, baseCRS, conversion, cs);
+                baseToCRS = p.getConversionFromBase().getMathTransform();
+                crs = p;
             }
             /*
-             * Create defining conversion from the parameters and the projected CRS.
-             */
-            Map<String,?> properties = properties(definition, "conversion_name", node.getName());
-            final Conversion conversion = opFactory.createDefiningConversion(properties, method, parameters);
-            final CartesianCS cs = CommonCRS.WGS84.universal(0,0).getCoordinateSystem();                     // TODO
-            properties = properties(definition, "projected_coordinate_system_name", conversion);
-            final ProjectedCRS crs = crsFactory.createProjectedCRS(properties, baseCRS, conversion, cs);
-            /*
-             * Build the "grid to CRS" if present. This is not present in CF-convention,
+             * Build the "grid to CRS" if present. This is not defined by CF-convention,
              * but may be present in some non-CF conventions.
              */
-            final MathTransform gridToCRS = node.decoder.convention().gridToCRS(node, crs);
+            final MathTransform gridToCRS = node.decoder.convention().gridToCRS(node, baseToCRS);
             return new GridMapping(crs, gridToCRS, false);
         } catch (ClassCastException | IllegalArgumentException | FactoryException | TransformException e) {
             canNotCreate(node, Resources.Keys.CanNotCreateCRS_3, e);
@@ -281,6 +228,75 @@ final class GridMapping {
     }
 
     /**
+     * Creates the geographic CRS from axis length specified in the given map projection parameters.
+     * The returned CRS will always have (latitude, longitude) axes in that order and in degrees.
+     */
+    private static GeographicCRS createBaseCRS(final Decoder decoder, final ParameterValueGroup parameters,
+            final Map<String,Object> definition, final Object greenwichLongitude) throws FactoryException
+    {
+        final DatumFactory datumFactory = decoder.getDatumFactory();
+        final CommonCRS defaultDefinitions = decoder.convention().defaultHorizontalCRS(false);
+        boolean isSpecified = false;
+        /*
+         * Prime meridian built from "longitude_of_prime_meridian".
+         */
+        final PrimeMeridian meridian;
+        if (greenwichLongitude instanceof Number) {
+            final double longitude = ((Number) greenwichLongitude).doubleValue();
+            final Map<String,?> properties = properties(definition, Convention.PRIME_MERIDIAN_NAME, 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 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);
+            } else {
+                final double semiMinor = parameters.parameter(Constants.SEMI_MINOR).doubleValue();
+                ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, semiMinor, Units.METRE);
+            }
+            isSpecified = true;
+        } catch (ParameterNotFoundException | IllegalStateException e) {
+            // Ignore - may be normal if the map projection is not an Apache SIS implementation.
+            ellipsoid = defaultDefinitions.ellipsoid();
+        }
+        /*
+         * Geodetic datum built from "towgs84" and above properties.
+         */
+        final Object bursaWolf = definition.remove(Convention.TOWGS84);
+        final GeodeticDatum datum;
+        if (isSpecified | bursaWolf != null) {
+            Map<String,Object> properties = properties(definition, Convention.GEODETIC_DATUM_NAME, ellipsoid);
+            if (bursaWolf instanceof BursaWolfParameters) {
+                properties = new HashMap<>(properties);
+                properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf);
+                isSpecified = true;
+            }
+            datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian);
+        } else {
+            datum = defaultDefinitions.datum();
+        }
+        /*
+         * Geographic CRS from all above properties.
+         */
+        if (isSpecified) {
+            final Map<String,?> properties = properties(definition, Convention.GEOGRAPHIC_CRS_NAME, datum);
+            return decoder.getCRSFactory().createGeographicCRS(properties, datum,
+                    defaultDefinitions.geographic().getCoordinateSystem());
+        } else {
+            return defaultDefinitions.geographic();
+        }
+    }
+
+    /**
      * Returns the {@code properties} argument value to give to the factory methods of geodetic objects.
      * The returned map contains at least an entry for {@value IdentifiedObject#NAME_KEY} with the name
      * fetched from the value of the attribute named {@code nameAttribute}.


Mime
View raw message