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

[sis] branch geoapi-4.0 updated (ac6a4fdb02 -> dc743d4a4d)

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

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


    from ac6a4fdb02 Allow to fetch statistics in last resort if min/max are not specified in a netCDF file. This is disabled by default, because this operation is potentially costly.
     new 553f9f4f87 Avoid an `AssertionError` when sub-classing a `Builder`	subclass. Allow subclassing of `GeodeticObjectBuilder`.
     new 61e556777c Construct a better CRS from grid mapping attributes: - More informative ellipsoid/datum name than "Unnamed". - If the CRS inferred from coordinate variables has 0°…360° longitude range,   apply that range to the CRS created from grid mapping attributes.
     new dc743d4a4d More robust check for wraparound in `GridEvaluator` when the given point has less dimensions than the grid geometry.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  |   1 +
 .../apache/sis/coverage/grid/GridEvaluator.java    |  37 +++-
 .../referencing/GeodeticObjectBuilder.java         |  18 +-
 .../java/org/apache/sis/referencing/Builder.java   |   6 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |   1 +
 .../org/apache/sis/internal/netcdf/CRSMerger.java  |  76 +++++++++
 .../apache/sis/internal/netcdf/GridMapping.java    | 188 +++++++++++++--------
 10 files changed, 246 insertions(+), 88 deletions(-)
 create mode 100644 storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java


[sis] 01/03: Avoid an `AssertionError` when sub-classing a `Builder` subclass. Allow subclassing of `GeodeticObjectBuilder`.

Posted by de...@apache.org.
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 553f9f4f8708a56ba72c1d5a6e310a98648825ea
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 3 16:47:04 2022 +0200

    Avoid an `AssertionError` when sub-classing a `Builder` subclass.
    Allow subclassing of `GeodeticObjectBuilder`.
---
 .../internal/referencing/GeodeticObjectBuilder.java    | 18 +++++++++++-------
 .../main/java/org/apache/sis/referencing/Builder.java  |  6 ++++--
 2 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java
index 30f63540a5..62ef1b36ee 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/GeodeticObjectBuilder.java
@@ -23,6 +23,7 @@ import java.util.Locale;
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
 import javax.measure.quantity.Length;
+import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterNotFoundException;
@@ -63,11 +64,11 @@ import org.apache.sis.referencing.IdentifiedObjects;
  * However this class may move in a public package later if we feel confident that its API is mature enough.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.6
  * @module
  */
-public final class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> {
+public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> {
     /**
      * The geodetic datum, or {@code null} if none.
      */
@@ -517,8 +518,7 @@ public final class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder>
     /**
      * Replaces the component starting at given index by the given component. This method can be used for replacing
      * e.g. the horizontal component of a CRS, or the vertical component, <i>etc.</i>. If a new compound CRS needs
-     * to be created and a {@linkplain #addName(org.opengis.util.GenericName) name has been specified}, that name
-     * will be used.
+     * to be created and a {@linkplain #addName(GenericName) name has been specified}, that name will be used.
      *
      * <h4>Limitations</h4>
      * Current implementation can replace exactly one component of {@link CompoundCRS}.
@@ -538,7 +538,11 @@ public final class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder>
         final int srcDim = ReferencingUtilities.getDimension(source);
         final int repDim = ReferencingUtilities.getDimension(replacement);
         if (firstDimension == 0 && srcDim == repDim) {
-            return replacement;
+            /*
+             * conceptually return the replacement. But returning the original instance if applicable
+             * allows the caller to detect that a compound CRS does not need to be replaced.
+             */
+            return source.equals(replacement) ? source : replacement;
         }
         ArgumentChecks.ensureValidIndex(srcDim - repDim, firstDimension);
         if (source instanceof CompoundCRS) {
@@ -556,8 +560,8 @@ public final class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder>
                     Object ids   = properties.remove(IdentifiedObject.IDENTIFIERS_KEY);
                     final CoordinateReferenceSystem nc = replaceComponent(c, firstDimension - lower, replacement);
                     /*
-                     * Restore the names and identifiers before to create the final CompoundCRS. If no name was specified,
-                     * reuse the primary name of existing CRS but not the identifiers.
+                     * Restore the names and identifiers before to create the final CompoundCRS.
+                     * If no name was specified, reuse the primary name of existing CRS but not the identifiers.
                      */
                     if (name == null) {
                         name = source.getName();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java
index 81e054b5c2..50eb32159a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java
@@ -42,7 +42,7 @@ import static org.apache.sis.util.ArgumentChecks.*;
 
 
 /**
- * Base class of builders for various kind of {@link IdentifiedObject}. This class provides convenience methods
+ * Base class of builders for various kinds of {@link IdentifiedObject}. This class provides convenience methods
  * for filling the {@link #properties} map to be given to an {@link org.opengis.referencing.ObjectFactory}.
  * The main properties are:
  *
@@ -230,7 +230,7 @@ public abstract class Builder<B extends Builder<B>> {
      * Verifies that {@code B} in {@code <B extends Builder<B>} is the expected class.
      * This method is for assertion purposes only.
      */
-    private static boolean verifyParameterizedType(final Class<?> expected) {
+    private static boolean verifyParameterizedType(Class<?> expected) {
         for (Class<?> c = expected; c != null; c = c.getSuperclass()) {
             Type type = c.getGenericSuperclass();
             if (type instanceof ParameterizedType) {
@@ -240,6 +240,8 @@ public abstract class Builder<B extends Builder<B>> {
                     if (type == expected) return true;
                     throw new AssertionError(type);
                 }
+            } else {
+                expected = c.getSuperclass();
             }
         }
         return false;


[sis] 03/03: More robust check for wraparound in `GridEvaluator` when the given point has less dimensions than the grid geometry.

Posted by de...@apache.org.
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 dc743d4a4d9a75ba2f9d6ee9ee070912eab2afb8
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 3 18:40:56 2022 +0200

    More robust check for wraparound in `GridEvaluator` when the given point has less dimensions than the grid geometry.
---
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  |  1 +
 .../apache/sis/coverage/grid/GridEvaluator.java    | 37 ++++++++++++++++++----
 2 files changed, 32 insertions(+), 6 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
index 8a8a1e0c2a..d618731a48 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/ValuesUnderCursor.java
@@ -318,6 +318,7 @@ public abstract class ValuesUnderCursor {
             }
             evaluator = coverage.forConvertedValues(true).evaluator();
             evaluator.setNullIfOutside(true);
+            evaluator.setWraparoundEnabled(true);
             canvas(property).ifPresent((c) -> setSlice(c.getSliceExtent()));
             if (previous != null && bands.equals(previous.getSampleDimensions())) {
                 // Same configuration than previous coverage.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
index 5e22a9d539..a13ca901d3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java
@@ -183,7 +183,7 @@ public class GridEvaluator implements GridCoverage.Evaluator {
     /**
      * Returns the default slice where to perform evaluation, or an empty map if unspecified.
      * Keys are dimensions from 0 inclusive to {@link GridGeometry#getDimension()} exclusive,
-     * and values are the grid coordinate of the slice in that dimension.
+     * and values are the grid coordinates of the slice in the dimension specified by the key.
      *
      * <p>This information allows to invoke {@link #apply(DirectPosition)} with for example two-dimensional points
      * even if the underlying coverage is three-dimensional. The missing coordinate values are replaced by the
@@ -204,7 +204,7 @@ public class GridEvaluator implements GridCoverage.Evaluator {
 
     /**
      * Sets the default slice where to perform evaluation when the points do not have enough dimensions.
-     * A {@code null} argument restore the default value, which is to infer the slice from the coverage
+     * A {@code null} argument restores the default value, which is to infer the slice from the coverage
      * grid geometry.
      *
      * @param  slice  the default slice where to perform evaluation, or an empty map if none.
@@ -214,6 +214,7 @@ public class GridEvaluator implements GridCoverage.Evaluator {
      *
      * @since 1.3
      */
+    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
     public void setDefaultSlice(Map<Integer,Long> slice) {
         if (!Objects.equals(this.slice, slice)) {
             if (slice != null) {
@@ -475,7 +476,6 @@ public class GridEvaluator implements GridCoverage.Evaluator {
          * If most cases, the work of this method ends here. The remaining code in this method
          * is for handling wraparound axes. If a coordinate is outside the coverage extent,
          * check if a wraparound on some axes would bring the coordinates inside the extent.
-         * The first step is to get the point closest to the extent.
          */
         long axes = wraparoundAxes;
         if (axes != 0) {
@@ -487,6 +487,12 @@ public class GridEvaluator implements GridCoverage.Evaluator {
                 final double c = coordinates[i];
                 double border;
                 if (c < (border = wraparoundExtent[j++]) || c > (border = wraparoundExtent[j])) {
+                    /*
+                     * Detected that the point is outside the grid extent along an axis where wraparound is possible.
+                     * The first time that we find such axis, expand the coordinates array for storing two points.
+                     * The two points will have the same coordinates, except on all axes where the point is outside.
+                     * On those axes, the coordinate of the first point is set to the closest border of the grid.
+                     */
                     if (outsideAxes == 0) {
                         final int n = coordinates.length;
                         coordinates = Arrays.copyOf(coordinates, 2*Math.max(n, gridToWraparound.getTargetDimensions()));
@@ -502,7 +508,10 @@ public class GridEvaluator implements GridCoverage.Evaluator {
             /*
              * If a coordinate was found outside the grid, transform to a CRS where we can apply shift.
              * It may be the same CRS than the coverage CRS or the source CRS, but not necessarily.
-             * Current version does not try to optimize by checking if `point` argument can be reused.
+             * For example if the CRS is projected, then we need to use a geographic intermediate CRS.
+             * In the common case where the source CRS is already geographic, the second point in the
+             * `coordinates` array after `transform(…)` will contain the same coordinates as `point`,
+             * but potentially with more dimensions.
              */
             if (outsideAxes != 0) {
                 gridToWraparound.transform(coordinates, 0, coordinates, 0, 2);
@@ -515,7 +524,7 @@ public class GridEvaluator implements GridCoverage.Evaluator {
                          * then round that shift to an integer amount of periods. Modify the original
                          * coordinate by applying that modified translation.
                          */
-                        final int oi = i + s;
+                        final int oi = i + s;                       // Index of original coordinates.
                         double shift = coordinates[i] - coordinates[oi];
                         shift = Math.copySign(Math.ceil(Math.abs(shift) / period), shift) * period;
                         coordinates[oi] += shift;
@@ -536,7 +545,23 @@ public class GridEvaluator implements GridCoverage.Evaluator {
                     }
                     outsideAxes &= ~(1L << i);
                 } while (outsideAxes != 0);
-                System.arraycopy(coordinates, 0, position.coordinates, 0, position.coordinates.length);
+                /*
+                 * Copy shifted coordinate values to the final `FractionalGridCoordinates`, except the NaN values.
+                 * NaN values may exist if the given `point` has less dimensions than the grid geometry, in which
+                 * case missing values have been replaced by `slice` values in the `target` array but not in the
+                 * `coordinates` array. We want to keep the `slice` values in the `target` array.
+                 *
+                 * TODO: to be strict, we should skip the copy only if `slice.containsKey(i)` is true, because it
+                 * could happen that a transform resulted in NaN values in other dimensions. But that check would
+                 * be costly, so we avoid it for now.
+                 */
+                final double[] target = position.coordinates;
+                for (int i = target.length; --i >= 0;) {
+                    final double value = coordinates[i];
+                    if (!Double.isNaN(value)) {
+                        target[i] = value;
+                    }
+                }
             }
         }
         return position;


[sis] 02/03: Construct a better CRS from grid mapping attributes: - More informative ellipsoid/datum name than "Unnamed". - If the CRS inferred from coordinate variables has 0°…360° longitude range, apply that range to the CRS created from grid mapping attributes.

Posted by de...@apache.org.
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 61e556777c68ad48f41bd776e95034eec51e2117
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 3 16:48:14 2022 +0200

    Construct a better CRS from grid mapping attributes:
    - More informative ellipsoid/datum name than "Unnamed".
    - If the CRS inferred from coordinate variables has 0°…360° longitude range,
      apply that range to the CRS created from grid mapping attributes.
---
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |   1 +
 .../org/apache/sis/internal/netcdf/CRSMerger.java  |  76 +++++++++
 .../apache/sis/internal/netcdf/GridMapping.java    | 188 +++++++++++++--------
 6 files changed, 199 insertions(+), 73 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index f7f2fe3b5b..2bbe39973d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -1154,6 +1154,11 @@ public final class Vocabulary extends IndexedResourceBundle {
          */
         public static final short SpatialRepresentation = 184;
 
+        /**
+         * Sphere
+         */
+        public static final short Sphere = 271;
+
         /**
          * Standard deviation
          */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index 1ad786ab8e..21efb2b707 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -234,6 +234,7 @@ Slowness                = Slowness
 Source                  = Source
 SouthBound              = South bound
 SpatialRepresentation   = Spatial representation
+Sphere                  = Sphere
 StandardDeviation       = Standard deviation
 StartDate               = Start date
 StartPoint              = Start point
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 4bff86e19e..39059bff17 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -241,6 +241,7 @@ Slowness                = Lenteur
 Source                  = Source
 SouthBound              = Limite sud
 SpatialRepresentation   = Repr\u00e9sentation spatiale
+Sphere                  = Sph\u00e8re
 StandardDeviation       = \u00c9cart type
 StartDate               = Date de d\u00e9part
 StartPoint              = Point de d\u00e9part
diff --git a/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 275c40bad2..5e73914d95 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -446,6 +446,7 @@ previous:   for (int i=components.size(); --i >= 0;) {
                         if (range.getMinDouble() >= 0 && range.getMaxDouble() > axis.getMaximumValue()) {
                             referenceSystem = (SingleCRS) AbstractCRS.castOrCopy(referenceSystem)
                                                 .forConvention(AxesConvention.POSITIVE_RANGE);
+                            coordinateSystem = null;
                             break;
                         }
                     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java
new file mode 100644
index 0000000000..80c0153d54
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSMerger.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf;
+
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
+import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.util.Utilities;
+
+
+/**
+ * Merges the CRS declared in grid mapping attributes with the CRS inferred from coordinate variables.
+ * The former (called "explicit CRS") may have map projection parameters that are difficult to infer
+ * from the coordinate variables. The latter (called "implicit CRS") have better name and all required
+ * dimensions, while the explicit CRS often has only the horizontal dimensions.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+final class CRSMerger extends GeodeticObjectBuilder {
+    /**
+     * Creates a new builder for the given netCDF reader.
+     */
+    CRSMerger(final Decoder decoder) {
+        super(decoder, decoder.listeners.getLocale());
+    }
+
+    /**
+     * Replaces the component starting at given index by the given component, possibly with adjusted longitude range.
+     * The implicit CRS has been inferred from coordinate variables, while the explicit CRS has been inferred from
+     * the grid mapping attributes. The explicit CRS is the one to use, but its longitude range is the default one
+     * because thar range depends on the coordinate variable, which was inspected by the implicit CRS.
+     *
+     * @param  implicit        the coordinate reference system in which to replace a component.
+     * @param  firstDimension  index of the first dimension to replace.
+     * @param  explicit        the component to insert in place of the CRS component at given index.
+     * @return a CRS with the component replaced.
+     * @throws FactoryException if the object creation failed.
+     */
+    @Override
+    public CoordinateReferenceSystem replaceComponent(final CoordinateReferenceSystem implicit,
+            final int firstDimension, CoordinateReferenceSystem explicit) throws FactoryException
+    {
+        final CoordinateSystem cs = implicit.getCoordinateSystem();
+        if (cs instanceof EllipsoidalCS) {
+            final int i = AxisDirections.indexOfColinear(cs, AxisDirection.EAST);
+            if (i >= 0 && cs.getAxis(i).getMinimumValue() >= 0) {       // The `i >= 0` check is paranoiac.
+                explicit = AbstractCRS.castOrCopy(explicit).forConvention(AxesConvention.POSITIVE_RANGE);
+            }
+        }
+        final CoordinateReferenceSystem result = super.replaceComponent(implicit, firstDimension, explicit);
+        return Utilities.equalsIgnoreMetadata(implicit, result) ? implicit : result;
+    }
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
index 839e69c98b..aedeccc301 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridMapping.java
@@ -21,9 +21,15 @@ import java.util.List;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Collections;
+import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.function.Supplier;
+import java.text.NumberFormat;
+import java.text.FieldPosition;
 import java.text.ParseException;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterValueGroup;
@@ -58,7 +64,6 @@ import org.apache.sis.internal.referencing.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.GeodeticObjectBuilder;
 import org.apache.sis.internal.referencing.provider.PseudoPlateCarree;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Constants;
@@ -80,7 +85,7 @@ import ucar.nc2.constants.CF;
  * which creates Coordinate Reference Systems by inspecting coordinate system axes.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  *
  * @see <a href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
  *
@@ -89,8 +94,9 @@ import ucar.nc2.constants.CF;
  */
 final class GridMapping {
     /**
-     * The Coordinate Reference System, or {@code null} if none. This CRS can be constructed from Well Known Text
-     * or EPSG codes declared in {@code "spatial_ref"}, {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes.
+     * The Coordinate Reference System inferred from grid mapping attribute values, or {@code null} if none.
+     * This CRS may have been constructed from Well Known Text or EPSG codes declared in {@code "spatial_ref"},
+     * {@code "ESRI_pe_string"} or {@code "EPSG_code"} attributes.
      *
      * <div class="note"><b>Note:</b> this come from different information than the one used by {@link CRSBuilder},
      * which creates CRS by inspection of coordinate system axes.</div>
@@ -104,21 +110,27 @@ final class GridMapping {
     private final MathTransform gridToCRS;
 
     /**
-     * Whether the {@link #crs} where defined by an EPSG code.
+     * Whether the {@link #crs} was defined by a WKT string.
      */
-    private final boolean isEPSG;
+    private final boolean isWKT;
 
     /**
      * Creates an instance for the given {@link #crs} and {@link #gridToCRS} values.
+     *
+     * @param  crs        CRS inferred from grid mapping attribute values, or {@code null} if none.
+     * @param  gridToCRS  transform from GDAL conventions, or {@code null} if none.
+     * @param  isWKT      wether the {@code crs} was defined by a WKT string.
      */
-    private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isEPSG) {
+    private GridMapping(final CoordinateReferenceSystem crs, final MathTransform gridToCRS, final boolean isWKT) {
         this.crs       = crs;
         this.gridToCRS = gridToCRS;
-        this.isEPSG    = isEPSG;
+        this.isWKT     = isWKT;
     }
 
     /**
      * Fetches grid geometry information from attributes associated to the given variable.
+     * This method should be invoked only once per variable, but may return a shared {@code GridMapping} instance
+     * for all variables because there is typically only one set of grid mapping attributes for the whole file.
      *
      * @param  variable  the variable for which to create a grid geometry.
      */
@@ -302,7 +314,8 @@ final class GridMapping {
         final PrimeMeridian meridian;
         if (greenwichLongitude instanceof Number) {
             final double longitude = ((Number) greenwichLongitude).doubleValue();
-            final Map<String,?> properties = properties(definition, Convention.PRIME_MERIDIAN_NAME, null);
+            final Map<String,?> properties = properties(definition,
+                    Convention.PRIME_MERIDIAN_NAME, (longitude == 0) ? "Greenwich" : null);
             meridian = datumFactory.createPrimeMeridian(properties, longitude, Units.DEGREE);
             isSpecified = true;
         } else {
@@ -316,14 +329,34 @@ final class GridMapping {
          */
         Ellipsoid ellipsoid;
         try {
-            final double semiMajor = parameters.parameter(Constants.SEMI_MAJOR).doubleValue();
-            final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, null);
-            if (parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue()) {
-                final double ivf = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
-                ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, ivf, Units.METRE);
+            final ParameterValue<?> p = parameters.parameter(Constants.SEMI_MAJOR);
+            final Unit<Length> axisUnit = p.getUnit().asType(Length.class);
+            final double  semiMajor = p.doubleValue();
+            final double  secondDefiningParameter;
+            final boolean isSphere;
+            final boolean isIvfDefinitive = parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue();
+            if (isIvfDefinitive) {
+                secondDefiningParameter = parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
+                isSphere = (secondDefiningParameter == 0) || Double.isInfinite(secondDefiningParameter);
+            } else {
+                secondDefiningParameter = parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit);
+                isSphere = secondDefiningParameter == semiMajor;
+            }
+            final Supplier<Object> fallback = () -> {           // Default ellipsoid name if not specified.
+                final Locale  locale = decoder.listeners.getLocale();
+                final NumberFormat f = NumberFormat.getNumberInstance(locale);
+                f.setMaximumFractionDigits(5);      // Centimetric precision.
+                final double km = axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor);
+                final StringBuffer b = new StringBuffer()
+                        .append(Vocabulary.getResources(locale).getString(isSphere ? Vocabulary.Keys.Sphere : Vocabulary.Keys.Ellipsoid))
+                        .append(isSphere ? " R=" : " a=");
+                return f.format(km, b, new FieldPosition(0)).append(" km").toString();
+            };
+            final Map<String,?> properties = properties(definition, Convention.ELLIPSOID_NAME, fallback);
+            if (isIvfDefinitive) {
+                ellipsoid = datumFactory.createFlattenedSphere(properties, semiMajor, secondDefiningParameter, axisUnit);
             } else {
-                final double semiMinor = parameters.parameter(Constants.SEMI_MINOR).doubleValue();
-                ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, semiMinor, Units.METRE);
+                ellipsoid = datumFactory.createEllipsoid(properties, semiMajor, secondDefiningParameter, axisUnit);
             }
             isSpecified = true;
         } catch (ParameterNotFoundException | IllegalStateException e) {
@@ -371,13 +404,15 @@ final class GridMapping {
     private static Map<String,Object> properties(final Map<String,Object> definition, final String nameAttribute, final Object fallback) {
         Object name = definition.remove(nameAttribute);
         if (name == null) {
-            if (fallback instanceof IdentifiedObject) {
+            if (fallback == null) {
+                // Note: IdentifiedObject.name does not accept InternationalString.
+                name = Vocabulary.format(Vocabulary.Keys.Unnamed);
+            } else if (fallback instanceof IdentifiedObject) {
                 name = ((IdentifiedObject) fallback).getName();
-            } else if (fallback != null) {
-                name = fallback.toString();
+            } else if (fallback instanceof Supplier<?>) {
+                name = ((Supplier<?>) fallback).get();
             } else {
-                name = Vocabulary.format(Vocabulary.Keys.Unnamed);
-                // Note: IdentifiedObject.name does not accept InternationalString.
+                name = fallback.toString();
             }
         }
         return Collections.singletonMap(IdentifiedObject.NAME_KEY, name);
@@ -424,7 +459,7 @@ final class GridMapping {
         } catch (ParseException | NumberFormatException e) {
             canNotCreate(mapping, message, e);
         }
-        return new GridMapping(crs, gridToCRS, false);
+        return new GridMapping(crs, gridToCRS, wkt != null);
     }
 
     /**
@@ -444,14 +479,13 @@ final class GridMapping {
      * @return whether this method found grid geometry attributes.
      */
     private static GridMapping parseNonStandard(final Node variable) {
-        boolean isEPSG = false;
         String code = variable.getAttributeAsString("ESRI_pe_string");
-        if (code == null) {
+        final boolean isWKT = (code != null);
+        if (!isWKT) {
             code = variable.getAttributeAsString("EPSG_code");
             if (code == null) {
                 return null;
             }
-            isEPSG = true;
         }
         /*
          * The Coordinate Reference System stored in those attributes often use the GeoTIFF flavor of EPSG codes,
@@ -462,16 +496,16 @@ final class GridMapping {
          */
         CoordinateReferenceSystem crs;
         try {
-            if (isEPSG) {
-                crs = CRS.forCode(Constants.EPSG + ':' + isEPSG);
-            } else {
+            if (isWKT) {
                 crs = createFromWKT(variable, code);
+            } else {
+                crs = CRS.forCode(Constants.EPSG + ':' + code);
             }
         } catch (FactoryException | ParseException | ClassCastException e) {
             canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e);
             crs = null;
         }
-        return new GridMapping(crs, null, isEPSG);
+        return new GridMapping(crs, null, isWKT);
     }
 
     /**
@@ -512,10 +546,12 @@ final class GridMapping {
     }
 
     /**
-     * Creates a new grid geometry for the given extent.
-     * This method should be invoked only when no existing {@link GridGeometry} can be used as template.
+     * Creates a new grid geometry with the extent of the given variable and a potentially null CRS.
+     * This method should be invoked only as a fallback when no existing {@link GridGeometry} can be used.
+     * The CRS and "grid to CRS" transform are null, unless some partial information was found for example
+     * as WKT string.
      */
-    GridGeometry createGridCRS(final Variable variable) {
+    final GridGeometry createGridCRS(final Variable variable) {
         final List<Dimension> dimensions = variable.getGridDimensions();
         final long[] upper = new long[dimensions.size()];
         for (int i=0; i<upper.length; i++) {
@@ -526,42 +562,48 @@ final class GridMapping {
     }
 
     /**
-     * Creates the grid geometry from the {@link #crs} and {@link #gridToCRS} field,
-     * completing missing information with the given template.
+     * Creates the grid geometry from the {@link #crs} and {@link #gridToCRS} fields,
+     * completing missing information with the implicit grid geometry derived from coordinate variables.
+     * For example {@code GridMapping} may contain information only about the horizontal dimensions, so
+     * the given {@code implicit} geometry is used for completing with vertical and temporal dimensions.
      *
      * @param  variable  the variable for which to create a grid geometry.
-     * @param  template  template to use for completing missing information.
+     * @param  implicit  template to use for completing missing information.
      * @param  anchor    whether we computed "grid to CRS" transform relative to pixel center or pixel corner.
      * @return the grid geometry with modified CRS and "grid to CRS" transform, or {@code null} in case of failure.
      */
-    GridGeometry adaptGridCRS(final Variable variable, final GridGeometry template, final PixelInCell anchor) {
-        CoordinateReferenceSystem givenCRS = crs;
+    final GridGeometry adaptGridCRS(final Variable variable, final GridGeometry implicit, final PixelInCell anchor) {
+        /*
+         * The CRS and grid geometry built from grid mapping attributes are called "explicit" in this method.
+         * This is by contrast with CRS derived from coordinate variables, which is only implicit.
+         */
+        CoordinateReferenceSystem explicitCRS = crs;
         int firstAffectedCoordinate = 0;
         boolean isSameGrid = true;
-        if (template.isDefined(GridGeometry.CRS)) {
-            final CoordinateReferenceSystem templateCRS = template.getCoordinateReferenceSystem();
-            if (givenCRS == null) {
-                givenCRS = templateCRS;
+        if (implicit.isDefined(GridGeometry.CRS)) {
+            final CoordinateReferenceSystem implicitCRS = implicit.getCoordinateReferenceSystem();
+            if (explicitCRS == null) {
+                explicitCRS = implicitCRS;
             } else {
                 /*
-                 * The CRS built by Grid may have a different axis order than the CRS specified by grid mapping attributes.
-                 * Check which axis order seems to fit, then replace grid CRS by given CRS (potentially with swapped axes).
-                 * This is where the potential difference between EPSG axis order and grid axis order is handled. If we can
-                 * not find where to substitute the CRS, assume that the given CRS describes the first dimensions. We have
-                 * no guarantees that this later assumption is right, but it seems to match common practice.
+                 * The CRS built by the `Grid` class (based on an inspection of coordinate variables)
+                 * may have a different axis order than the CRS specified by grid mapping attributes
+                 * (the CRS built by this class). This block checks which axis order seems to fit,
+                 * then potentially replaces `Grid` implicit CRS by `GridMapping` explicit CRS.
+                 *
+                 * This is where the potential difference between EPSG axis order and grid axis order is handled.
+                 * If we can not find which component to replace, assume that grid mapping describes the first dimensions.
+                 * We have no guarantees that this latter assumption is right, but it seems to match common practice.
                  */
-                final CoordinateSystem cs = templateCRS.getCoordinateSystem();
-                CoordinateSystem subCS = givenCRS.getCoordinateSystem();
-                firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, subCS);
+                final CoordinateSystem cs = implicitCRS.getCoordinateSystem();
+                firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem());
                 if (firstAffectedCoordinate < 0) {
-                    givenCRS = AbstractCRS.castOrCopy(givenCRS).forConvention(AxesConvention.RIGHT_HANDED);
-                    subCS = givenCRS.getCoordinateSystem();
-                    firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, subCS);
+                    explicitCRS = AbstractCRS.castOrCopy(explicitCRS).forConvention(AxesConvention.RIGHT_HANDED);
+                    firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem());
                     if (firstAffectedCoordinate < 0) {
                         firstAffectedCoordinate = 0;
-                        if (!isEPSG) {
-                            givenCRS = crs;                             // If specified by WKT, use the given CRS verbatim.
-                            subCS = givenCRS.getCoordinateSystem();
+                        if (isWKT && crs != null) {
+                            explicitCRS = crs;                         // If specified by WKT, use the CRS verbatim.
                         }
                     }
                 }
@@ -570,15 +612,15 @@ final class GridMapping {
                  * axis order. If the grid CRS contains more axes (for example elevation or time axis), we try to keep them.
                  */
                 try {
-                    givenCRS = new GeodeticObjectBuilder(variable.decoder, variable.decoder.listeners.getLocale())
-                                                .replaceComponent(templateCRS, firstAffectedCoordinate, givenCRS);
+                    explicitCRS = new CRSMerger(variable.decoder)
+                            .replaceComponent(implicitCRS, firstAffectedCoordinate, explicitCRS);
                 } catch (FactoryException e) {
                     canNotCreate(variable, Resources.Keys.CanNotCreateCRS_3, e);
                     return null;
                 }
-                isSameGrid = templateCRS.equals(givenCRS);
+                isSameGrid = implicitCRS.equals(explicitCRS);
                 if (isSameGrid) {
-                    givenCRS = templateCRS;                                 // Keep existing instance if appropriate.
+                    explicitCRS = implicitCRS;                                 // Keep existing instance if appropriate.
                 }
             }
         }
@@ -588,31 +630,31 @@ final class GridMapping {
          * then we need to perform selection in target dimensions (not source dimensions) because the first affected
          * coordinate computed above is in CRS dimension, which is the target of "grid to CRS" transform.
          */
-        MathTransform givenG2C = gridToCRS;
-        if (template.isDefined(GridGeometry.GRID_TO_CRS)) {
-            final MathTransform templateG2C = template.getGridToCRS(anchor);
-            if (givenG2C == null) {
-                givenG2C = templateG2C;
+        MathTransform explicitG2C = gridToCRS;
+        if (implicit.isDefined(GridGeometry.GRID_TO_CRS)) {
+            final MathTransform implicitG2C = implicit.getGridToCRS(anchor);
+            if (explicitG2C == null) {
+                explicitG2C = implicitG2C;
             } else try {
                 int count = 0;
                 MathTransform[] components = new MathTransform[3];
-                final TransformSeparator sep = new TransformSeparator(templateG2C, variable.decoder.getMathTransformFactory());
+                final TransformSeparator sep = new TransformSeparator(implicitG2C, variable.decoder.getMathTransformFactory());
                 if (firstAffectedCoordinate != 0) {
                     sep.addTargetDimensionRange(0, firstAffectedCoordinate);
                     components[count++] = sep.separate();
                     sep.clear();
                 }
-                components[count++] = givenG2C;
-                final int next = firstAffectedCoordinate + givenG2C.getTargetDimensions();
-                final int upper = templateG2C.getTargetDimensions();
+                components[count++] = explicitG2C;
+                final int next = firstAffectedCoordinate + explicitG2C.getTargetDimensions();
+                final int upper = implicitG2C.getTargetDimensions();
                 if (next != upper) {
                     sep.addTargetDimensionRange(next, upper);
                     components[count++] = sep.separate();
                 }
                 components = ArraysExt.resize(components, count);
-                givenG2C = MathTransforms.compound(components);
-                if (templateG2C.equals(givenG2C)) {
-                    givenG2C = templateG2C;                                 // Keep using existing instance if appropriate.
+                explicitG2C = MathTransforms.compound(components);
+                if (implicitG2C.equals(explicitG2C)) {
+                    explicitG2C = implicitG2C;                                 // Keep using existing instance if appropriate.
                 } else {
                     isSameGrid = false;
                 }
@@ -626,9 +668,9 @@ final class GridMapping {
          * If any of them have changed, create the new grid geometry.
          */
         if (isSameGrid) {
-            return template;
+            return implicit;
         } else {
-            return new GridGeometry(template.getExtent(), anchor, givenG2C, givenCRS);
+            return new GridGeometry(implicit.getExtent(), anchor, explicitG2C, explicitCRS);
         }
     }
 }