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 97524f8379a6e1e80765793a875bd611230fd438
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Jan 11 22:37:23 2022 +0100

    Make exception messages more informative for some errors happening at 
netCDF reading time.
    It requires some code for making possible to transfer information, but this 
is for error handling only.
---
 .../apache/sis/internal/referencing/Resources.java |   9 +-
 .../sis/internal/referencing/Resources.properties  |   3 +-
 .../internal/referencing/Resources_fr.properties   |   3 +-
 .../referencing/factory/FactoryDataException.java  |  13 ++-
 .../sis/referencing/factory/package-info.java      |   2 +-
 .../operation/builder/LinearTransformBuilder.java  |  11 +-
 .../operation/builder/LocalizationGridBuilder.java |   2 +-
 .../builder/LocalizationGridException.java         | 114 +++++++++++++++++++++
 .../main/java/org/apache/sis/util/Exceptions.java  |   5 +-
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  16 +++
 .../org/apache/sis/internal/netcdf/Dimension.java  |  16 ++-
 .../java/org/apache/sis/internal/netcdf/Grid.java  |  12 ++-
 .../org/apache/sis/internal/netcdf/Linearizer.java |  46 +++++++--
 .../org/apache/sis/internal/netcdf/Variable.java   |  24 +++--
 .../sis/internal/netcdf/ucar/DimensionWrapper.java |  13 ++-
 15 files changed, 257 insertions(+), 32 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
index b3e451d..36943bc 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
@@ -44,7 +44,7 @@ public final class Resources extends IndexedResourceBundle {
      * pools of compiled classes.
      *
      * @author  Martin Desruisseaux (IRD, Geomatys)
-     * @since   0.8
+     * @since   1.2
      * @module
      */
     public static final class Keys extends KeyConstants {
@@ -189,7 +189,7 @@ public final class Resources extends IndexedResourceBundle {
         public static final short ConstantProjParameterValue_1 = 12;
 
         /**
-         * Coordinate conversion of transformation from system “{0}” to “{1}” 
has not been found.
+         * Coordinate operation from system “{0}” to “{1}” has not been found.
          */
         public static final short CoordinateOperationNotFound_2 = 13;
 
@@ -247,6 +247,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short GeodeticDataBase_4 = 18;
 
         /**
+         * The grid spans {0}° of longitude, which may be too wide for the 
“{1}” domain.
+         */
+        public static final short GridLongitudeSpanTooWide_2 = 102;
+
+        /**
          * More than one service provider of type ‘{0}’ are declared for 
“{1}”. Only the first provider
          * (an instance of ‘{2}’) will be used.
          */
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
index 59a3282..17b98e8 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
@@ -66,7 +66,7 @@ CanNotTransformEnvelopeToGeodetic = Can not transform 
envelope to a geodetic ref
 CanNotTransformGeometry           = Can not transform the given geometry.
 CanNotUseGeodeticParameters_2     = Can not use the {0} geodetic parameters. 
Caused by: {1}
 ColinearAxisDirections_2          = Axis directions {0} and {1} are colinear.
-CoordinateOperationNotFound_2     = Coordinate conversion of transformation 
from system \u201c{0}\u201d to \u201c{1}\u201d has not been found.
+CoordinateOperationNotFound_2     = Coordinate operation from system 
\u201c{0}\u201d to \u201c{1}\u201d has not been found.
 DatumChangesDirectory_1           = Datum shift files are searched in the 
\u201c{0}\u201d directory.
 DatumOriginShallBeDate            = Origin of temporal datum shall be a date.
 DuplicatedParameterName_4         = Name or alias for parameter 
\u201c{0}\u201d at index {1} conflict with name \u201c{2}\u201d at index {3}.
@@ -74,6 +74,7 @@ DuplicatedSpatialComponents_1     = Compound coordinate 
reference systems can no
 EllipsoidalHeightNotAllowed_1     = Compound coordinate reference systems 
should not contain ellipsoidal height. Use a three-dimensional 
{0,choice,0#geographic|1#projected} system instead.
 FileNotFound_2                    = Can not find {0} file named 
\u201c{1}\u201d.
 FileNotReadable_2                 = Can not parse \u201c{1}\u201d as a file in 
the {0} format.
+GridLongitudeSpanTooWide_2        = The grid spans {0}\u00b0 of longitude, 
which may be too wide for the \u201c{1}\u201d domain.
 IllegalAxisDirection_2            = Coordinate system of class \u2018{0}\u2019 
can not have axis in the {1} direction.
 IllegalOperationDimension_3       = Dimensions of \u201c{0}\u201d operation 
can not be ({1} \u2192 {2}).
 IllegalOperationForValueClass_1   = This operation can not be applied to 
values of class \u2018{0}\u2019.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
index 6ef6d37..e81f00e 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
@@ -71,7 +71,7 @@ CanNotTransformEnvelopeToGeodetic = Ne peut pas transformer 
l\u2019enveloppe ver
 CanNotTransformGeometry           = Ne peut pas transformer la 
g\u00e9om\u00e9trie donn\u00e9e.
 CanNotUseGeodeticParameters_2     = Ne peut pas utiliser les param\u00e8tres 
g\u00e9od\u00e9siques {0}. La cause est\u202f: {1}
 ColinearAxisDirections_2          = Les directions d\u2019axes {0} et {1} sont 
colin\u00e9aires.
-CoordinateOperationNotFound_2     = La conversion ou transformation des 
coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers 
\u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e.
+CoordinateOperationNotFound_2     = L\u2019op\u00e9ration sur les 
coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers 
\u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e.
 DatumChangesDirectory_1           = Les fichiers de changements de 
r\u00e9f\u00e9rentiel sont cherch\u00e9s dans le dossier 
\u00ab\u202f{0}\u202f\u00bb.
 DatumOriginShallBeDate            = L\u2019origine d\u2019un 
r\u00e9f\u00e9rentiel temporel doit \u00eatre une date.
 DuplicatedParameterName_4         = Le nom ou un alias pour le param\u00e8tre 
\u00ab\u202f{0}\u202f\u00bb \u00e0 l\u2019index {1} duplique le nom 
\u00ab\u202f{2}\u202f\u00bb \u00e0 l\u2019index {3}.
@@ -79,6 +79,7 @@ DuplicatedSpatialComponents_1     = Un syst\u00e8me de 
r\u00e9f\u00e9rence des c
 EllipsoidalHeightNotAllowed_1     = Un syst\u00e8me de r\u00e9f\u00e9rence des 
coordonn\u00e9es ne devrait pas contenir une hauteur ellipso\u00efdale. 
Utilisez plut\u00f4t un syst\u00e8me 
{0,choice,0#g\u00e9ographique|1#projet\u00e9} \u00e0 trois dimensions.
 FileNotFound_2                    = Ne peut pas trouver le fichier {0} 
nomm\u00e9 \u00ab\u202f{1}\u202f\u00bb.
 FileNotReadable_2                 = Ne peut pas lire 
\u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}.
+GridLongitudeSpanTooWide_2        = La grille s\u2019\u00e9tend sur {0}\u00b0 
de longitude, ce qui peut \u00eatre trop pour le domaine de 
\u00ab\u202f{1}\u202f\u00bb.
 IllegalAxisDirection_2            = Les syst\u00e8mes de coordonn\u00e9es de 
classe \u2018{0}\u2019 ne peuvent pas avoir d\u2019axe dans la direction 
\u00ab\u202f{1}\u202f\u00bb.
 IllegalOperationDimension_3       = Les dimensions de l\u2019op\u00e9ration 
\u00ab\u202f{0}\u202f\u00bb ne peuvent pas \u00eatre ({1} \u2192 {2}).
 IllegalOperationForValueClass_1   = Cette op\u00e9ration ne peut pas 
s\u2019appliquer aux valeurs de classe \u2018{0}\u2019.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/FactoryDataException.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/FactoryDataException.java
index 873387b..2cd38c8 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/FactoryDataException.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/FactoryDataException.java
@@ -26,7 +26,7 @@ import org.opengis.util.FactoryException;
  * an EPSG database record containing a null value in a column where nulls 
should not have been allowed.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.2
  * @since   0.7
  * @module
  */
@@ -52,6 +52,17 @@ public class FactoryDataException extends FactoryException {
     }
 
     /**
+     * Constructs an exception with the specified cause.
+     *
+     * @param cause  the cause, saved for later retrieval by the {@link 
#getCause()} method.
+     *
+     * @since 1.2
+     */
+    public FactoryDataException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
      * Construct an exception with the specified detail message and cause.
      *
      * @param  message  the detail message, saved for later retrieval by the 
{@link #getMessage()} method.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java
index affc539..0c2c551 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java
@@ -56,7 +56,7 @@
  * </table>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.2
  * @since   0.6
  * @module
  */
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
index 0f5838f..36ee404 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
@@ -270,7 +270,7 @@ public class LinearTransformBuilder extends 
TransformBuilder {
         try {
             return (LinearTransform) Linearizer.approximate(gridToCRS, domain);
         } catch (TransformException e) {
-            throw new FactoryException(e);
+            throw new LocalizationGridException(e);
         }
     }
 
@@ -1467,10 +1467,13 @@ search:         for (int j=domain(); --j >= 0;) {
                 }
                 /*
                  * Finished to try all transforms. If all of them failed, wrap 
the `TransformException`.
+                 * We use a sub-type of `FactoryException` which allows 
callers to add their own information.
+                 * For example the caller may know that the grid was possibly 
out of CRS domain of validity
+                 * and wanted to try anyway (it can be difficult to predict in 
advance if it will work).
                  */
                 if (bestTransform == null) {
-                    throw new 
FactoryException(Resources.format(Resources.Keys.CanNotLinearizeLocalizationGrid),
-                                               
ProjectedTransformTry.getError(linearizers));
+                    throw new 
LocalizationGridException(Resources.format(Resources.Keys.CanNotLinearizeLocalizationGrid),
+                                                        
ProjectedTransformTry.getError(linearizers));
                 }
                 if (needTargetReplace) {
                     transformedArrays = 
appliedLinearizer.replaceTransformed(targets, transformedArrays);
@@ -1547,7 +1550,7 @@ search:         for (int j=domain(); --j >= 0;) {
                     break;
                 }
                 default: {
-                    throw new 
FactoryException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, 
sourceDim));
+                    throw new 
InvalidGeodeticParameterException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1,
 sourceDim));
                 }
             }
             correlations[j] = c;
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
index 73383a1..aaedbe2 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
@@ -713,7 +713,7 @@ public class LocalizationGridBuilder extends 
TransformBuilder {
                         step = 
InterpolatedTransform.createGeodeticTransformation(nonNull(factory), shifts);
                     }
                 } catch (TransformException e) {
-                    throw new FactoryException(e);                             
             // Should never happen.
+                    throw new LocalizationGridException(e);                    
             // Should never happen.
                 }
             }
             /*
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridException.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridException.java
new file mode 100644
index 0000000..a74c47a
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridException.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.builder;
+
+import org.opengis.util.InternationalString;
+import org.apache.sis.referencing.factory.FactoryDataException;
+
+
+/**
+ * Thrown when a localization grid can not be computed, presumably because of 
a problem with grid data.
+ * It may be because some grid coordinates are out of CRS domain of validity, 
causing either
+ * {@link org.opengis.referencing.operation.MathTransform} to be thrown or 
{@link Double#NaN}
+ * coordinate values to be computed.
+ *
+ * <h2>Additional information on exception cause</h2>
+ * It is sometime difficult to determine the root cause of this exception.
+ * For example grid points slightly outside the CRS domain of validity will 
not necessarily cause a failure.
+ * A strategy can be to try to build the grid anyway, and in case of failure 
declare that the grid was maybe
+ * too far from CRS domain of validity. Because the potential causes are 
better known by the code that wants
+ * a localization grid instead of the {@link LocalizationGridBuilder} class, 
{@code LocalizationGridException}
+ * provides a {@link #setPotentialCause(CharSequence)} method for allowing 
top-level code to attach additional
+ * information to this exception.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ */
+public class LocalizationGridException extends FactoryDataException {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = -9069664783475360076L;
+
+    /**
+     * Additional information about what may be the cause of this exception.
+     * Example: <cite>"The grid spans more than 180° of longitude"</cite>,
+     * which may be a cause of map projection failures.
+     *
+     * @see #getPotentialCause()
+     */
+    private CharSequence potentialCause;
+
+    /**
+     * Construct an exception with no detail message.
+     */
+    public LocalizationGridException() {
+    }
+
+    /**
+     * Constructs an exception with the specified detail message.
+     *
+     * @param message  the detail message, saved for later retrieval by the 
{@link #getMessage()} method.
+     */
+    public LocalizationGridException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs an exception with the specified cause.
+     *
+     * @param cause  the cause, saved for later retrieval by the {@link 
#getCause()} method.
+     */
+    public LocalizationGridException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs an exception with the specified detail message and cause.
+     * The cause is the exception thrown in the underlying database
+     * (e.g. {@link java.io.IOException} or {@link java.sql.SQLException}).
+     *
+     * @param message  the detail message, saved for later retrieval by the 
{@link #getMessage()} method.
+     * @param cause    the cause, saved for later retrieval by the {@link 
#getCause()} method.
+     */
+    public LocalizationGridException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Specifies additional information about what may be the cause of this 
exception.
+     * Example: <cite>"The grid spans more than 180° of longitude"</cite>,
+     * which may be a cause of map projection failures.
+     *
+     * @param  details  a potential cause, or {@code null} if none.
+     *         The type should be {@link String} or {@link 
InternationalString}.
+     */
+    public synchronized void setPotentialCause(CharSequence details) {
+        potentialCause = details;
+    }
+
+    /**
+     * Returns the value given to the last call of {@link 
#setPotentialCause(CharSequence)}.
+     *
+     * @return potential cause, or {@code null} if none.
+     *         The type should be {@link String} or {@link 
InternationalString}.
+     */
+    public synchronized CharSequence getPotentialCause() {
+        return potentialCause;
+    }
+}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
index 1ed6359..13a90a3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
@@ -108,7 +108,10 @@ public final class Exceptions extends Static {
      *                   will be happened after the provided message.
      * @return a new exception with the given message, or the given exception 
if the exception
      *         class does not provide public {@code Exception(String)} 
constructor.
+     *
+     * @deprecated To be removed with no replacement.
      */
+    @Deprecated
     @SuppressWarnings("unchecked")
     public static <T extends Throwable> T setMessage(final T exception, String 
message, final boolean append) {
         if (append) {
@@ -155,7 +158,7 @@ public final class Exceptions extends Static {
     }
 
     /**
-     * Returns a string which contain the given message on the first line, 
followed by the
+     * Returns a string which contains the given message on the first line, 
followed by the
      * {@linkplain #getLocalizedMessage(Throwable, Locale) localized message} 
of the given exception
      * on the next line. If the exception has a {@linkplain 
Throwable#getCause() causes}, then
      * the class name and the localized message of the cause are formatted on 
the next line
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
index a14466f..0b5c1bf 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
@@ -39,6 +39,7 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.metadata.content.TransferFunctionType;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.referencing.operation.builder.LocalizationGridException;
 import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -864,6 +865,21 @@ public final class Axis extends NamedElement {
                 tr = new GridCacheValue(linearizers, grid, factory);
                 tr = keyLocal.cache(decoder, tr);
             }
+        } catch (LocalizationGridException ex) {
+            /*
+             * Complete the exception with a possible failure cause before to 
propagate the exception.
+             * Example: "The grid spans more than 180° of longitude", which 
may cause transform errors.
+             * The possible causes are known only by the linearizer, which is 
why we could not set it
+             * at `LocalizationGridException` construction time.
+             */
+            for (final Linearizer linearizer : linearizers) {
+                final CharSequence reason = 
linearizer.getPotentialCause(coordinates);
+                if (reason != null) {
+                    ex.setPotentialCause(reason);
+                    break;          // Take the cause of the linearizer that 
had the highest priority.
+                }
+            }
+            throw ex;
         } finally {
             handler.putAndUnlock(tr);
         }
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java
index 26caff7..f04687f 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Dimension.java
@@ -32,7 +32,7 @@ import org.apache.sis.util.resources.Vocabulary;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.2
  * @since   1.0
  * @module
  */
@@ -71,6 +71,20 @@ public abstract class Dimension extends NamedElement {
     protected abstract boolean isUnlimited();
 
     /**
+     * Returns a dimension with its index decremented by 1. This method is 
invoked for trailing dimensions
+     * after a previous dimension has been removed from a list. This is useful 
only for subclasses that
+     * need to know the index of this dimension in a list of dimensions.
+     *
+     * <p>Note: this method may be removed in a future version if we do not 
need to store index anymore.
+     * See <a href="https://github.com/Unidata/netcdf-java/issues/951";>Issue 
#951 on netcdf-java</a>.</p>
+     *
+     * @return a dimension equals to this one but with its list index (if any) 
decremented.
+     */
+    protected Dimension decrementIndex() {
+        return this;
+    }
+
+    /**
      * Writes in the given buffer the length of this dimension between bracket.
      * The length may be unknown (represented by {@code '?'}).
      */
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index d19b8ce..374d625 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -30,6 +30,7 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.builder.LocalizationGridException;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.IllegalGridGeometryException;
@@ -47,7 +48,7 @@ import org.apache.sis.util.ArraysExt;
  * if a variable dimensions should considered as bands instead of 
spatiotemporal dimensions.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see Decoder#getGrids()
  *
@@ -563,6 +564,13 @@ findFree:       for (int srcDim : 
axis.gridDimensionIndices) {
      * @param  key     one of {@link Resources.Keys#CanNotCreateCRS_3} or 
{@link Resources.Keys#CanNotCreateGridGeometry_3}.
      */
     private void canNotCreate(final Decoder decoder, final String caller, 
final short key, final Exception ex) {
-        warning(decoder.listeners, Grid.class, caller, ex, null, key, 
decoder.getFilename(), getName(), ex.getLocalizedMessage());
+        CharSequence message = null;
+        if (ex instanceof LocalizationGridException) {
+            message = ((LocalizationGridException) ex).getPotentialCause();
+        }
+        if (message == null) {
+            message = ex.getLocalizedMessage();
+        }
+        warning(decoder.listeners, Grid.class, caller, ex, null, key, 
decoder.getFilename(), getName(), message);
     }
 }
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
index 48ff2d6..d02505b 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
@@ -21,6 +21,7 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
 import org.opengis.geometry.Envelope;
+import org.opengis.util.InternationalString;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.cs.CoordinateSystem;
@@ -161,6 +162,14 @@ public final class Linearizer {
     private boolean axisSwap;
 
     /**
+     * The image span in degrees of longitude, or 0 if not computed.
+     * This is used for giving a hint about why a projection may have failed.
+     *
+     * @see #getPotentialCause()
+     */
+    private float longitudeSpan;
+
+    /**
      * Creates a new linearizer working on the specified datum.
      *
      * @param  datum  the datum to use. Should be consistent with {@link 
Convention#defaultHorizontalCRS(boolean)}.
@@ -197,6 +206,23 @@ public final class Linearizer {
     }
 
     /**
+     * If this linearizer can give a probable reason why it failed to compute 
the localization grid, returns that reason.
+     * Otherwise returns {@code null}.
+     *
+     * @param  owner  for fetching localized resources.
+     * @return potential error cause, or {@code null} if unknown.
+     */
+    final InternationalString getPotentialCause(final Node owner) {
+        if (longitudeSpan >= 180 - 6) {         // 180° of longitude minus a 
UTM zone width.
+            final String name = IdentifiedObjects.getDisplayName(targetCRS, 
owner.getLocale());
+            return 
org.apache.sis.internal.referencing.Resources.formatInternational(
+                   
org.apache.sis.internal.referencing.Resources.Keys.GridLongitudeSpanTooWide_2,
+                   longitudeSpan, (name != null) ? name : type);
+        }
+        return null;
+    }
+
+    /**
      * Returns a string representation for debugging purposes.
      */
     @Override
@@ -231,26 +257,32 @@ public final class Linearizer {
              */
             case UNIVERSAL: {
                 final Envelope bounds = grid.getSourceEnvelope(false);
-                double x, y, ymin, ymax;
+                double x, y, xmin, xmax, ymin, ymax;
                 {   // For keeping `median` variable local.
                     final double[] median = grid.getControlPoint(
                             (int) Math.round(bounds.getMedian(0)),
                             (int) Math.round(bounds.getMedian(1)));
-                    x = median[xdim];
-                    y = median[ydim];
-                    ymin = ymax = y;
+                    x = median[xdim]; xmin = xmax = x;
+                    y = median[ydim]; ymin = ymax = y;
                 }
                 final int[] gc = new int[SOURCE_DIMENSION];
                 for (int i=0; i<4; i++) {
                     for (int d=0; d<SOURCE_DIMENSION; d++) {
                         gc[d] = (int) Math.round(((i & (1 << d)) == 0) ? 
bounds.getMinimum(d) : bounds.getMaximum(d));
                     }
-                    final double yp = grid.getControlPoint(gc[0], gc[1])[ydim];
-                    if (yp < ymin) ymin = yp;
-                    if (yp > ymax) ymax = yp;
+                    final double[] cp = grid.getControlPoint(gc[0], gc[1]);
+                    double c = cp[xdim];
+                    if (c < xmin) xmin = c;
+                    if (c > xmax) xmax = c;
+                    c = cp[ydim];
+                    if (c < ymin) ymin = c;
+                    if (c > ymax) ymax = c;
                 }
+                longitudeSpan = (float) (xmax - xmin);      // For providing a 
hint in case of failure.
                 /*
                  * If the image is far from equator, replace the middle point 
by a point close to pole.
+                 * The intend is to avoid using UTM projection for latitudes 
such as 89°N, because a single
+                 * NaN in transformed coordinates is enough for blocking 
creation of the localization grid.
                  */
                      if (ymin >= +Type.POLAR_THRESHOLD) y = ymax;
                 else if (ymax <= -Type.POLAR_THRESHOLD) y = ymin;
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index 55b301a..9de8f12 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -390,7 +390,7 @@ public abstract class Variable extends Node {
     public final Unit<?> getUnit() {
         if (!unitParsed) {
             unitParsed = true;                          // Set first for 
avoiding to report errors many times.
-            final String symbols = getUnitsString();
+            String symbols = getUnitsString();
             Exception error = null;
             if (symbols != null) try {
                 unit = parseUnit(symbols);
@@ -402,6 +402,9 @@ public abstract class Variable extends Node {
             } catch (ParserException ex) {
                 if (error == null) error = ex;
                 else error.addSuppressed(ex);
+                if (symbols == null) {
+                    symbols = ex.getParsedString();
+                }
             }
             if (error != null) {
                 error(Variable.class, "getUnit", error, 
Errors.Keys.CanNotAssignUnitToVariable_2, getName(), symbols);
@@ -581,8 +584,8 @@ public abstract class Variable extends Node {
         }
         /*
          * Get all dimensions of this variable in netCDF order, then replace 
them by dimensions from an axis variable.
-         * If we are in the situation #1 documented in javadoc, 'isIncomplete' 
will be 'false' after execution of this
-         * loop and all dimensions should be the same than the values returned 
by 'Variable.getGridDimensions()'.
+         * If we are in the situation #1 documented in javadoc, `isIncomplete` 
will be `false` after execution of this
+         * loop and all dimensions should be the same than the values returned 
by `Variable.getGridDimensions()`.
          */
         boolean isIncomplete = false;
         final List<Dimension> fromVariable = getGridDimensions();
@@ -605,7 +608,7 @@ public abstract class Variable extends Node {
                     }
                     /*
                      * The first time that we find a label that may allow us 
to associate this variable dimension with a
-                     * grid dimension, build a map of all labels associated to 
dimensions. We reuse the existing 'domain'
+                     * grid dimension, build a map of all labels associated to 
dimensions. We reuse the existing `domain`
                      * map; there is no confusion since the keys are not of 
the same class.
                      */
                     if (isIncomplete) {
@@ -702,10 +705,10 @@ public abstract class Variable extends Node {
                         Dimension expected = toKeep.get(i);
                         expected = 
adjustment.gridToVariable.getOrDefault(expected, expected);
                         /*
-                         * At this point, 'expected' is a dimension of the 
variable that we expect to find at
-                         * current index 'i'. If we do not find that 
dimension, then the unexpected dimension
+                         * At this point, `expected` is a dimension of the 
variable that we expect to find at
+                         * current index `i`. If we do not find that 
dimension, then the unexpected dimension
                          * is assumed to be a band. We usually remove at most 
one element. If removal results
-                         * in a list too short, it would be a bug in the way 
we computed 'toKeep'.
+                         * in a list too short, it would be a bug in the way 
we computed `toKeep`.
                          */
                         while (!expected.equals(dimensions.get(i))) {
                             if (!copied) {
@@ -716,17 +719,20 @@ public abstract class Variable extends Node {
                              * It is possible that we never reach this point 
if the unexpected dimension is last.
                              * However in such case the dimension to declare 
is the last one in netCDF order,
                              * which corresponds to the first dimension (i.e. 
dimension 0) in "natural" order.
-                             * Since the 'bandDimension' field is initialized 
to zero, its value is correct.
+                             * Since the `bandDimension` field is initialized 
to zero, its value is correct.
                              */
                             bandDimension = dataDimension - 1 - i;          // 
Convert netCDF order to "natural" order.
                             dimensions.remove(i);
+                            for (int j = dimensions.size(); --j >= i;) {
+                                dimensions.set(j, 
dimensions.get(j).decrementIndex());
+                            }
                             if (dimensions.size() < numToKeep) {
                                 throw new InternalDataStoreException();     // 
Should not happen (see above comment).
                             }
                         }
                     }
                     /*
-                     * At this point 'dimensions' may still be longer than 
'toKeep' but it does not matter.
+                     * At this point `dimensions` may still be longer than 
`toKeep` but it does not matter.
                      * We only need that for any index i < numToKeep, 
dimensions.get(i) corresponds to the
                      * dimension at the same index in the grid.
                      */
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
index caa7c54..579e24e 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DimensionWrapper.java
@@ -69,7 +69,7 @@ final class DimensionWrapper extends 
org.apache.sis.internal.netcdf.Dimension {
      * dimensions that are private to a variable, because those dimensions may 
be unnamed.
      * Consequently value -1 should be used only for shared dimensions.
      *
-     * <a href="https://github.com/Unidata/netcdf-java/issues/951";>Issue #951 
on netcdf-java</a>
+     * @see <a href="https://github.com/Unidata/netcdf-java/issues/951";>Issue 
#951 on netcdf-java</a>
      */
     private final int index;
 
@@ -109,6 +109,17 @@ final class DimensionWrapper extends 
org.apache.sis.internal.netcdf.Dimension {
     }
 
     /**
+     * Returns a dimension with its index decremented by 1. This method is 
invoked for trailing dimensions
+     * after a previous dimension has been removed from a list.
+     *
+     * @return a dimension equals to this one but with its list index (if any) 
decremented.
+     */
+    @Override
+    protected org.apache.sis.internal.netcdf.Dimension decrementIndex() {
+        return new DimensionWrapper(netcdf, index - 1);
+    }
+
+    /**
      * Returns {@code true} if the given object represents the same dimension 
than this object.
      * If the dimension is shared, then it has a unique name and {@link 
Dimension#equals(Object)}
      * can distinguish dimensions based on their name. But if the dimension is 
private to a variable,

Reply via email to