sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: First draft of GridGeometry.getCoordinateReferenceSystem() method, except for the temporal axis and projected CRS.
Date Thu, 15 Nov 2018 15:51:59 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new ee56b82  First draft of GridGeometry.getCoordinateReferenceSystem() method, except for the temporal axis and projected CRS.
ee56b82 is described below

commit ee56b82af867aa48f7cb29e226af3683d4d442fd
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Nov 15 16:51:12 2018 +0100

    First draft of GridGeometry.getCoordinateReferenceSystem() method, except for the temporal axis and projected CRS.
---
 .../apache/sis/util/logging/QuietLogRecord.java    |   2 +-
 .../sis/util/resources/IndexedResourceBundle.java  |   5 +
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  44 +-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java | 442 +++++++++++++++++++++
 .../org/apache/sis/internal/netcdf/Decoder.java    |  17 +-
 .../apache/sis/internal/netcdf/GridGeometry.java   |  81 ++--
 .../apache/sis/internal/netcdf/NamedElement.java   |  50 +++
 .../org/apache/sis/internal/netcdf/Resources.java  |  12 +
 .../sis/internal/netcdf/Resources.properties       |   2 +
 .../sis/internal/netcdf/Resources_fr.properties    |   2 +
 .../org/apache/sis/internal/netcdf/Variable.java   |  34 +-
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |   6 +-
 .../sis/internal/netcdf/impl/GridGeometryInfo.java |   9 +-
 .../sis/internal/netcdf/impl/VariableInfo.java     |   3 +-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |   4 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |  34 +-
 .../sis/storage/netcdf/MetadataReaderTest.java     |   1 +
 17 files changed, 643 insertions(+), 105 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java
index fb2cb4e..1b225fe 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java
@@ -21,7 +21,7 @@ import java.util.logging.LogRecord;
 
 
 /**
- * A log record to be logged without stack trace, unless the user specified it explicitely.
+ * A log record to be logged without stack trace, unless the user specified it explicitly.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 0.8
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
index 2bc56a9..b5cdd19 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
@@ -30,6 +30,7 @@ import java.util.ResourceBundle;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.lang.reflect.Modifier;
+import javax.measure.Unit;
 import org.opengis.util.CodeList;
 import org.opengis.util.ControlledVocabulary;
 import org.opengis.util.InternationalString;
@@ -425,6 +426,10 @@ public class IndexedResourceBundle extends ResourceBundle implements Localized {
             } else if (element instanceof Range<?>) {
                 final Range<?> range = (Range<?>) element;
                 replacement = new RangeFormat(getLocale(), range.getElementType()).format(range);
+            } else if (element instanceof Unit<?>) {
+                String s = element.toString();
+                if (s.isEmpty()) s = "1";
+                replacement = s;
             }
             /*
              * No need to check for Numbers or Dates instances, since they are
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 bc6a54a..c8383d7 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
@@ -33,6 +33,7 @@ import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.measure.Units;
 import ucar.nc2.constants.CDM;
 import ucar.nc2.constants.CF;
 
@@ -74,6 +75,7 @@ public final class Axis extends NamedElement {
      * </ul>
      *
      * @see AxisDirections#fromAbbreviation(char)
+     * @see CRSBuilder#dispatch(List, Axis)
      */
     public final char abbreviation;
 
@@ -91,7 +93,7 @@ public final class Axis extends NamedElement {
      * for ISO 19115 {@code metadata/spatialRepresentationInfo/axisDimensionProperties/dimensionSize}
      * metadata property.
      */
-    public final int[] sourceDimensions;
+    final int[] sourceDimensions;
 
     /**
      * The number of cell elements along the source grid dimensions. The length of this array shall be
@@ -104,7 +106,7 @@ public final class Axis extends NamedElement {
      * Values of coordinates on this axis for given grid indices. This variables is often one-dimensional,
      * but can also be two-dimensional.
      */
-    private final Variable coordinates;
+    final Variable coordinates;
 
     /**
      * Constructs a new axis associated to an arbitrary number of grid dimension.
@@ -156,7 +158,7 @@ public final class Axis extends NamedElement {
             }
         }
         if (!isConsistent) {
-            axis.warning(owner.getClass(), "getAxes",               // Caller of this constructor.
+            axis.warning(GridGeometry.class, "getAxes",             // Caller of this constructor.
                          Resources.Keys.AmbiguousAxisDirection_4, axis.getFilename(), axis.getName(), dir, check);
             if (isSigned) {
                 if (AxisDirections.isOpposite(dir)) {
@@ -218,27 +220,12 @@ public final class Axis extends NamedElement {
     }
 
     /**
-     * Creates ISO 19111 axes from the information stored in given netCDF axes.
-     *
-     * @param  axes     the axes to convert to ISO data structure.
-     * @param  factory  the factory to use for creating the coordinate system axis.
-     * @return the ISO axes.
-     */
-    static CoordinateSystemAxis[] toISO(final List<Axis> axes, final CSFactory factory) throws FactoryException {
-        final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[axes.size()];
-        for (int i=0; i<iso.length; i++) {
-            iso[i] = axes.get(i).toISO(factory);
-        }
-        return iso;
-    }
-
-    /**
      * Creates an ISO 19111 axis from the information stored in this netCDF axis.
      *
      * @param  factory  the factory to use for creating the coordinate system axis.
      * @return the ISO axis.
      */
-    private CoordinateSystemAxis toISO(final CSFactory factory) throws FactoryException {
+    final CoordinateSystemAxis toISO(final CSFactory factory) throws FactoryException {
         /*
          * The axis name is stored without namespace, because the variable name in a netCDF file can be anything;
          * this is not controlled vocabulary. However the standard name, if any, is stored with "NetCDF" namespace
@@ -272,12 +259,21 @@ public final class Axis extends NamedElement {
             properties.put(CoordinateSystemAxis.ALIAS_KEY, aliases.toArray(new GenericName[aliases.size()]));
         }
         /*
-         * Axis abbreviation, direction and unit of measurement are mandatory.
-         * If any of them is null, creation of CoordinateSystemAxis is likely
-         * to fail with an InvalidGeodeticParameterException. But we let the
-         * factory to choose, in case users specify their own factory.
+         * Axis abbreviation, direction and unit of measurement are mandatory. If any of them is null,
+         * creation of CoordinateSystemAxis is likely to fail with an InvalidGeodeticParameterException.
+         * We provide default values for the most well-accepted values and leave other values to null.
+         * Those null values can be accepted if users specify their own factory.
          */
-        final Unit<?> unit = coordinates.getUnit();
+        Unit<?> unit = coordinates.getUnit();
+        if (unit == null) {
+            switch (abbreviation) {
+                /*
+                 * TODO: consider moving those default values in a separated class,
+                 * for example a netCDF-specific CSFactory, for allowing users to override.
+                 */
+                case 'λ': case 'φ': unit = Units.DEGREE; break;
+            }
+        }
         final String abbr;
         if (abbreviation != 0) {
             abbr = Character.toString(abbreviation).intern();
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
new file mode 100644
index 0000000..243b2ab
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -0,0 +1,442 @@
+/*
+ * 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 java.util.Map;
+import java.util.List;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.StringJoiner;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.cs.*;
+import org.opengis.referencing.datum.*;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.CRSFactory;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.storage.DataStoreContentException;
+
+
+/**
+ * Temporary object for building a coordinate reference system from the variables in a netCDF file.
+ * Different instances are required for the geographic, vertical and temporal components of a CRS,
+ * or if a netCDF file uses different CRS for different variables.
+ *
+ * <p>The builder type is inferred from axes. The axes are identified by their abbreviations,
+ * which is a {@linkplain Axis#abbreviation controlled vocabulary} for this implementation.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> {
+    /**
+     * The type of datum as a GeoAPI sub-interface of {@link Datum}.
+     * Used for verifying the type of cached datum at {@link #datumIndex}.
+     */
+    private final Class<D> datumType;
+
+    /**
+     * Name of the datum on which the CRS is presumed to be based, or {@code null}. This is used
+     * for building a datum name like <cite>"Unknown datum presumably based on WGS 84"</cite>.
+     */
+    private final String datumBase;
+
+    /**
+     * Index of the cached datum in a {@code Datum[]} array, from 0 inclusive to {@value #DATUM_CACHE_SIZE} exclusive.
+     * The datum type at that index must be an instance of {@link #datumType}. We cache only the datum because they do
+     * not depend on the netCDF file content in the common case where the CRS is not explicitly specified.
+     */
+    private final byte datumIndex;
+
+    /**
+     * Specify the range of valid number of dimensions, inclusive.
+     * The {@link #dimension} value shall be in that range.
+     */
+    private final byte minDim, maxDim;
+
+    /**
+     * Number of valid elements in the {@link #axes} array. The count should not be larger than 3,
+     * even if the netCDF file has more axes, because each {@code CRSBuilder} is only for a subset.
+     */
+    private byte dimension;
+
+    /**
+     * The axes to use for creating the coordinate reference system.
+     * They are information about netCDF axes, not yet ISO 19111 axes.
+     */
+    private Axis[] axes;
+
+    /**
+     * The datum created by {@link #createDatum(DatumFactory, Map)}.
+     */
+    D datum;
+
+    /**
+     * The coordinate system created by {@link #createCS(CSFactory, Map, CoordinateSystemAxis[])}.
+     */
+    CS coordinateSystem;
+
+    /**
+     * Creates a new CRS builder based on datum of the given type.
+     *
+     * @param  datumType   the type of datum as a GeoAPI sub-interface of {@link Datum}.
+     * @param  datumBase   name of the datum on which the CRS is presumed to be based, or {@code null}.
+     * @param  datumIndex  index of the cached datum in a {@code Datum[]} array.
+     * @param  minDim      minimum number of dimensions (usually 1, 2 or 3).
+     * @param  maxDim      maximum number of dimensions (usually 1, 2 or 3).
+     */
+    CRSBuilder(final Class<D> datumType, final String datumBase, final byte datumIndex, final byte minDim, final byte maxDim) {
+        this.datumType  = datumType;
+        this.datumBase  = datumBase;
+        this.datumIndex = datumIndex;
+        this.minDim     = minDim;
+        this.maxDim     = maxDim;
+        this.axes       = new Axis[3];
+    }
+
+    /**
+     * Dispatches the given axis to a {@code CRSBuilder} appropriate for the axis type. The axis type is determined
+     * from {@link Axis#abbreviation}, taken as a controlled vocabulary. If no suitable {@code CRSBuilder} is found
+     * in the given list, then a new one will be created and added to the list.
+     *
+     * @param  components  the list of builder where to dispatch the axis. May be modified by this method.
+     * @param  axis        the axis to add to a builder in the given list.
+     * @throws DataStoreContentException if the given axis can not be added in a builder.
+     */
+    @SuppressWarnings("fallthrough")
+    public static void dispatch(final List<CRSBuilder<?,?>> components, final Axis axis) throws DataStoreContentException {
+        final Class<? extends CRSBuilder<?,?>> addTo;
+        int alternative = -1;
+        switch (axis.abbreviation) {
+            case 'h': for (int i=components.size(); --i >= 0;) {        // Can apply to either Geographic or Projected.
+                          if (components.get(i) instanceof Projected) {
+                              alternative = i;
+                              break;
+                          }
+                      }                    // Fallthrough
+            case 'λ': case 'φ':            addTo = Geographic.class;  break;
+            case 'θ': case 'Ω': case 'r':  addTo = Spherical.class;   break;
+            case 'E': case 'N':            addTo = Projected.class;   break;
+            case 'H': case 'D':            addTo = Vertical.class;    break;
+            case 't':                      addTo = Temporal.class;    break;
+            default:                       addTo = Engineering.class; break;
+        }
+        /*
+         * If a builder of 'addTo' class already exists, add the axis in the existing builder.
+         * We should have at most one builder of each class. But if we nevertheless have more,
+         * add to the most recently used builder. If there is no builder, create a new one.
+         */
+        for (int i=components.size(); --i >= 0;) {
+            final CRSBuilder<?,?> builder = components.get(i);
+            if (addTo.isInstance(builder) || i == alternative) {
+                builder.add(axis);
+                return;
+            }
+        }
+        final CRSBuilder<?,?> builder;
+        try {
+            builder = addTo.getConstructor((Class<?>[]) null).newInstance((Object[]) null);
+        } catch (ReflectiveOperationException e) {
+            throw new AssertionError(e);                            // Should never happen.
+        }
+        /*
+         * Before to add the axis to a newly created builder, verify if we wrongly associated
+         * the ellipsoidal height to Geographic builder before. The issue is that ellipsoidal
+         * height can be associated to either Geographic or Projected CRS.  If we do not have
+         * more information, our first bet is Geographic. If our bet appears to be wrong, the
+         * block below fixes it.
+         */
+        if (addTo == Projected.class) {
+previous:   for (int i=components.size(); --i >= 0;) {
+                final CRSBuilder<?,?> replace = components.get(i);
+                for (final Axis a : replace.axes) {
+                    if (a.abbreviation != 'h') {
+                        continue previous;                  // Not a lonely ellipsoidal height in a Geographic CRS.
+                    }
+                }
+                for (final Axis a : replace.axes) {         // Should have exactly one element, but we are paranoiac.
+                    builder.add(a);
+                }
+                components.remove(i);
+                break;
+            }
+        }
+        builder.add(axis);
+        components.add(builder);            // Add only after we ensured that the builder contains at least one axis.
+    }
+
+    /**
+     * Adds an axis for the coordinate reference system to build. Adding more than 3 axes is usually an error,
+     * but this method nevertheless stores those extraneous axis references for building an error message later.
+     *
+     * @param  axis  the axis to add.
+     * @throws DataStoreContentException if the given axis can not be added in this builder.
+     */
+    private void add(final Axis axis) throws DataStoreContentException {
+        if (dimension == Byte.MAX_VALUE) {
+            throw new DataStoreContentException(Errors.getResources(axes[0].coordinates.getLocale())
+                    .getString(Errors.Keys.ExcessiveListSize_2, "axes", (short) (Byte.MAX_VALUE + 1)));
+        }
+        if (dimension >= axes.length) {
+            axes = Arrays.copyOf(axes, dimension * 2);        // Should not happen (see method javadoc).
+        }
+        axes[dimension++] = axis;
+    }
+
+    /**
+     * Creates the coordinate reference system.
+     * This method can be invoked after all axes have been dispatched.
+     *
+     * @param  decoder  the decoder of the netCDF from which the CRS are constructed.
+     */
+    public final SingleCRS build(final Decoder decoder) throws FactoryException, DataStoreContentException {
+        if (dimension < minDim || dimension > maxDim) {
+            final Variable axis = axes[0].coordinates;
+            throw new DataStoreContentException(axis.resources().getString(Resources.Keys.UnexpectedAxisCount_4,
+                    axis.getFilename(), getClass().getSimpleName(), dimension, NamedElement.listNames(axes, dimension, ", ")));
+        }
+        datum = datumType.cast(decoder.datumCache[datumIndex]);
+        if (datum == null) {
+            createDatum(decoder.getDatumFactory(), properties("Unknown datum presumably based on ".concat(datumBase)));
+            decoder.datumCache[datumIndex] = datum;
+        }
+        final StringJoiner joiner = new StringJoiner(" ");
+        final CSFactory csFactory = decoder.getCSFactory();
+        final CoordinateSystemAxis[] iso = new CoordinateSystemAxis[dimension];
+        for (int i=0; i<iso.length; i++) {
+            final Axis axis = axes[i];
+            joiner.add(axis.getName());
+            iso[i] = axis.toISO(csFactory);
+        }
+        final Map<String,?> properties = properties(joiner.toString());
+        createCS(csFactory, properties, iso);
+        return createCRS(decoder.getCRSFactory(), properties);
+    }
+
+    /**
+     * Returns the properties to give to factory {@code create} methods.
+     *
+     * @param  name  name of the geodetic object (datum, coordinate system, …) to create.
+     */
+    private static Map<String,?> properties(final String name) {
+        return Collections.singletonMap(GeodeticDatum.NAME_KEY, name);
+    }
+
+    /**
+     * Creates the datum for the coordinate reference system to build. The datum are generally not specified in netCDF files.
+     * To make that clearer, this method builds datum with names like <cite>"Unknown datum presumably based on WGS 84"</cite>.
+     * The newly created datum is assigned to the {@link #datum} field.
+     *
+     * @param  factory     the factory to use for creating the datum.
+     * @param  properties  contains the name of the datum to create.
+     */
+    abstract void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException;
+
+    /**
+     * Creates the coordinate system from the given axes. This method is invoked only after we
+     * verified that the number of axes is inside the {@link #minDim} … {@link #maxDim} range.
+     * The newly created coordinate system is assigned to the {@link #coordinateSystem} field.
+     *
+     * @param  factory     the factory to use for creating the coordinate system.
+     * @param  properties  contains the name of the coordinate system to create.
+     * @param  axes        the axes of the coordinate system.
+     */
+    abstract void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException;
+
+    /**
+     * Creates the coordinate reference system from the values in {@link #datum} and {@link #coordinateSystem} fields.
+     *
+     * @param  factory     the factory to use for creating the coordinate reference system.
+     * @param  properties  contains the name of the coordinate reference system to create.
+     */
+    abstract SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException;
+
+    /**
+     * Base classes of {@link Spherical}, {@link Geographic} and {@link Projected} builders.
+     * They all have in common to be based on a {@link GeodeticDatum}.
+     */
+    private abstract static class Geodetic<CS extends CoordinateSystem> extends CRSBuilder<GeodeticDatum, CS> {
+        /** For subclasses constructors. */
+        Geodetic(final byte minDim) {
+            super(GeodeticDatum.class, "WGS 84", (byte) 0, minDim, (byte) 3);
+        }
+
+        /** Creates a {@link GeodeticDatum} for <cite>"Unknown datum based on WGS 84"</cite>. */
+        @Override final void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
+            final GeodeticDatum template = CommonCRS.WGS84.datum();
+            datum = factory.createGeodeticDatum(properties, template.getEllipsoid(), template.getPrimeMeridian());
+        }
+    }
+
+    /**
+     * Builder for geocentric CRS with (θ,Ω,r) axes.
+     */
+    private static final class Spherical extends Geodetic<SphericalCS> {
+        /** Creates a new builder (invoked by reflection). */
+        public Spherical() {
+            super((byte) 3);
+        }
+
+        /** Creates the three-dimensional {@link SphericalCS} from given axes. */
+        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
+            coordinateSystem = factory.createSphericalCS(properties, axes[0], axes[1], axes[2]);
+        }
+
+        /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
+        @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            return factory.createGeocentricCRS(properties, datum, coordinateSystem);
+        }
+    };
+
+    /**
+     * Geographic CRS with (λ,φ,h) axes.
+     * The height, if present, is ellipsoidal height.
+     */
+    private static final class Geographic extends Geodetic<EllipsoidalCS> {
+        /** Creates a new builder (invoked by reflection). */
+        public Geographic() {
+            super((byte) 2);
+        }
+
+        /** Creates the two- or three-dimensional {@link EllipsoidalCS} from given axes. */
+        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
+            if (axes.length > 2) {
+                coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]);
+            } else {
+                coordinateSystem = factory.createEllipsoidalCS(properties, axes[0], axes[1]);
+            }
+        }
+
+        /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
+        @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            return factory.createGeographicCRS(properties, datum, coordinateSystem);
+        }
+    };
+
+    /**
+     * Projected CRS with (E,N,h) axes.
+     */
+    private static final class Projected extends Geodetic<CartesianCS> {
+        /** Creates a new builder (invoked by reflection). */
+        public Projected() {
+            super((byte) 2);
+        }
+
+        /** Creates the two- or three-dimensional {@link CartesianCS} from given axes. */
+        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
+            if (axes.length > 2) {
+                coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1], axes[2]);
+            } else {
+                coordinateSystem = factory.createCartesianCS(properties, axes[0], axes[1]);
+            }
+        }
+
+        /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
+        @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            throw new UnsupportedOperationException();  // TODO
+        }
+    };
+
+    /**
+     * Vertical CRS with (H) or (D) axis.
+     * Used for mean sea level (not for ellipsoidal height).
+     */
+    private static final class Vertical extends CRSBuilder<VerticalDatum, VerticalCS> {
+        /** Creates a new builder (invoked by reflection). */
+        public Vertical() {
+            super(VerticalDatum.class, "Mean Sea Level", (byte) 1, (byte) 1, (byte) 1);
+        }
+
+        /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on Mean Sea Level"</cite>. */
+        @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
+            datum = factory.createVerticalDatum(properties, VerticalDatumType.GEOIDAL);
+        }
+
+        /** Creates the one-dimensional {@link VerticalCS} from given axes. */
+        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
+            coordinateSystem = factory.createVerticalCS(properties, axes[0]);
+        }
+
+        /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
+        @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            return factory.createVerticalCRS(properties, datum, coordinateSystem);
+        }
+    };
+
+    /**
+     * Temporal CRS with (t) axis. Its datum need to be built
+     * in a special way since it contains the time origin.
+     */
+    private static final class Temporal extends CRSBuilder<TemporalDatum, TimeCS> {
+        /** Creates a new builder (invoked by reflection). */
+        public Temporal() {
+            super(TemporalDatum.class, null, (byte) 2, (byte) 1, (byte) 1);
+        }
+
+        /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on …"</cite>. */
+        @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
+            throw new UnsupportedOperationException();  // TODO
+        }
+
+         /** Creates the one-dimensional {@link TimeCS} from given axes. */
+        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
+            throw new UnsupportedOperationException();  // TODO
+        }
+
+        /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
+        @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            return factory.createTemporalCRS(properties, datum, coordinateSystem);
+        }
+    };
+
+    /**
+     * Unknown CRS with (x,y,z) axes.
+     */
+    private static final class Engineering extends CRSBuilder<EngineeringDatum, AffineCS> {
+        /** Creates a new builder (invoked by reflection). */
+        public Engineering() {
+            super(EngineeringDatum.class, "affine coordinate system", (byte) 3, (byte) 2, (byte) 3);
+        }
+
+        /** Creates a {@link VerticalDatum} for <cite>"Unknown datum based on affine coordinate system"</cite>. */
+        @Override void createDatum(DatumFactory factory, Map<String,?> properties) throws FactoryException {
+            datum = factory.createEngineeringDatum(properties);
+        }
+
+         /** Creates two- or three-dimensional {@link AffineCS} from given axes. */
+        @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException {
+            if (axes.length > 2) {
+                coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]);
+            } else {
+                coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1]);
+            }
+        }
+
+        /** Creates the coordinate reference system from datum and coordinate system computed in previous steps. */
+        @Override SingleCRS createCRS(CRSFactory factory, Map<String,?> properties) throws FactoryException {
+            return factory.createEngineeringCRS(properties, datum, coordinateSystem);
+        }
+    };
+
+    /**
+     * Maximal {@link #datumIndex} value +1. The maximal value can be seen in the call to {@code super(…)} constructor
+     * in the last inner class defined above.
+     */
+    static final int DATUM_CACHE_SIZE = 4;
+}
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 7256bc3..96ae179 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -23,11 +23,13 @@ import java.io.Closeable;
 import java.io.IOException;
 import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
+import org.opengis.referencing.datum.Datum;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
 
 
 /**
@@ -41,7 +43,13 @@ import org.apache.sis.internal.system.DefaultFactories;
  * @since   0.3
  * @module
  */
-public abstract class Decoder implements Closeable {
+public abstract class Decoder extends ReferencingFactoryContainer implements Closeable {
+    /**
+     * The format name to use in error message. We use lower-case "n" because it seems to be what the netCDF community uses.
+     * By contrast, {@code NetcdfStoreProvider} uses upper-case "N" because it is considered at the beginning of sentences.
+     */
+    public static final String FORMAT_NAME = "netCDF";
+
     /**
      * The data store identifier created from the global attributes, or {@code null} if none.
      * Defined as a namespace for use as the scope of children resources (the variables).
@@ -62,6 +70,12 @@ public abstract class Decoder implements Closeable {
     public final GeometryLibrary geomlib;
 
     /**
+     * The geodetic datum, created when first needed. The datum are generally not specified in netCDF files.
+     * To make that clearer, we will build datum with names like "Unknown datum presumably based on WGS 84".
+     */
+    final Datum[] datumCache;
+
+    /**
      * Where to send the warnings.
      */
     public final WarningListeners<DataStore> listeners;
@@ -83,6 +97,7 @@ public abstract class Decoder implements Closeable {
         this.geomlib     = geomlib;
         this.listeners   = listeners;
         this.nameFactory = DefaultFactories.forBuildin(NameFactory.class);
+        this.datumCache  = new Datum[CRSBuilder.DATUM_CACHE_SIZE];
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridGeometry.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridGeometry.java
index 61a7d25..8ced0ec 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridGeometry.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridGeometry.java
@@ -16,18 +16,16 @@
  */
 package org.apache.sis.internal.netcdf;
 
-import java.util.Map;
-import java.util.HashMap;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.io.IOException;
 import org.opengis.util.FactoryException;
-import org.opengis.referencing.cs.CSFactory;
-import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.cs.CoordinateSystem;
-import org.opengis.referencing.cs.CoordinateSystemAxis;
-import org.opengis.referencing.crs.CRSFactory;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.NullArgumentException;
 
 
 /**
@@ -50,6 +48,19 @@ public abstract class GridGeometry extends NamedElement {
     private Axis[] axes;
 
     /**
+     * The coordinate reference system, created when first needed.
+     * May be {@code null} even after we attempted to create it.
+     *
+     * @see #getCoordinateReferenceSystem(Decoder)
+     */
+    private CoordinateReferenceSystem crs;
+
+    /**
+     * Whether we determined the {@link #crs} value, which may be {@code null}.
+     */
+    private boolean isCRSDetermined;
+
+    /**
      * Constructs a new grid geometry information.
      */
     protected GridGeometry() {
@@ -120,41 +131,37 @@ public abstract class GridGeometry extends NamedElement {
     protected abstract double coordinateForAxis(Variable axis, int j, int i) throws IOException, DataStoreException;
 
     /**
-     * Creates the coordinate reference system.
+     * Returns the coordinate reference system, or {@code null} if none.
+     * This method creates the CRS the first time it is invoked and cache the result.
      *
-     * @param  csFactory   the factory to use for creating coordinate systems.
-     * @param  crsFactory  the factory to use for creating coordinate reference systems.
+     * @param   decoder  the decoder for which CRS are constructed.
+     * @return  the CRS for this grid geometry, or {@code null}.
+     * @throws  IOException if an I/O operation was necessary but failed.
+     * @throws  DataStoreException if the CRS can not be constructed.
      */
-    final void createCRS(final CSFactory csFactory, final CRSFactory crsFactory)
-            throws IOException, DataStoreException, FactoryException
-    {
-        final List<Axis> spherical   = new ArrayList<>();       // Spherical latitude, longitude and radius.
-        final List<Axis> ellipsoidal = new ArrayList<>();       // Geodetic latitude and longitude.
-        final List<Axis> projected   = new ArrayList<>();       // Easting and northing.
-        final List<Axis> compound    = new ArrayList<>();       // Geoidal height and/or time.
-        final List<Axis> engineering = new ArrayList<>();       // Everything else.
-        for (final Axis axis : getAxes()) {
-            final List<Axis> addTo;
-            switch (axis.abbreviation) {
-                case 'E': case 'N':            addTo = projected;   break;
-                case 'λ': case 'φ':            addTo = ellipsoidal; break;
-                case 'θ': case 'Ω': case 'r':  addTo = spherical;   break;
-                case 'H': case 'D': case 't':  addTo = compound;    break;
-                case 'h': projected.add(axis); addTo = ellipsoidal; break;  // Can be ellipsoidal or projected.
-                default:                       addTo = engineering; break;
+    public final CoordinateReferenceSystem getCoordinateReferenceSystem(final Decoder decoder) throws IOException, DataStoreException {
+        if (!isCRSDetermined) try {
+            final List<CRSBuilder<?,?>> builders = new ArrayList<>();
+            final Axis[] axes = getAxes();
+            for (int i=axes.length; --i >= 0;) {                // NetCDF order is reverse of "natural" order.
+                CRSBuilder.dispatch(builders, axes[i]);
             }
-            addTo.add(axis);
-        }
-        final Map<String,Object> properties = new HashMap<>(4);
-        properties.put(CoordinateSystem.NAME_KEY, getName());
-        if (!ellipsoidal.isEmpty()) {
-            final CoordinateSystemAxis[] axes = Axis.toISO(ellipsoidal, csFactory);
-            final EllipsoidalCS cs;
-            switch (axes.length) {
-                case 2: cs = csFactory.createEllipsoidalCS(properties, axes[0], axes[1]);          break;
-                case 3: cs = csFactory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]); break;
-                default: // TODO
+            final SingleCRS[] components = new SingleCRS[builders.size()];
+            for (int i=0; i < components.length; i++) {
+                components[i] = builders.get(i).build(decoder);
+            }
+            switch (components.length) {
+                case 0:  break;                                 // Leave 'crs' to null.
+                case 1:  crs = components[0]; break;
+                default: crs = decoder.getCRSFactory().createCompoundCRS(
+                                        Collections.singletonMap(CoordinateSystem.NAME_KEY, getName()), components);
             }
+            isCRSDetermined = true;
+        } catch (FactoryException | NullArgumentException ex) {
+            // TODO: avoid reporting the full exception stack trace (maybe leverage QuietLogRecord).
+            warning(decoder.listeners, GridGeometry.class, "getCoordinateReferenceSystem", ex, null,
+                    Resources.Keys.CanNotCreateCRS_3, decoder.getFilename(), getName(), ex.getLocalizedMessage());
         }
+        return crs;
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
index c835476..bf0c9ba 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
@@ -16,8 +16,14 @@
  */
 package org.apache.sis.internal.netcdf;
 
+import java.util.StringJoiner;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
 import org.apache.sis.util.Characters;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.resources.IndexedResourceBundle;
 
 
 /**
@@ -44,6 +50,23 @@ public abstract class NamedElement {
     public abstract String getName();
 
     /**
+     * Creates a name for a {@code NamedElement} made of other components.
+     * Current implementation returns a separated list of component names.
+     *
+     * @param  components  the component of the named object.
+     * @param  count       number of valid elements in the {@code components} array.
+     * @param  delimiter   the separator between component names.
+     * @return a name for an object composed of the given components.
+     */
+    protected static String listNames(final NamedElement[] components, final int count, final String delimiter) {
+        final StringJoiner joiner = new StringJoiner(delimiter);
+        for (int i=0; i<count; i++) {
+            joiner.add(components[i].getName());
+        }
+        return joiner.toString();
+    }
+
+    /**
      * Returns {@code true} if the given names are considered equals for the purpose of netCDF decoder.
      * Two names are considered similar if they are equal ignoring case and characters that are not valid
      * for an Unicode identifier.
@@ -57,6 +80,33 @@ public abstract class NamedElement {
     }
 
     /**
+     * Reports a warning to the specified listeners.
+     *
+     * @param  listeners  the listeners where to report the warning.
+     * @param  caller     the caller class to report, preferably a public class.
+     * @param  method     the caller method to report, preferable a public method.
+     * @param  exception  the exception that occurred, or {@code null} if none.
+     * @param  resources  the resources bundle for {@code key} and {@code arguments}, or {@code null} for {@link Resources}.
+     * @param  key        one or {@link Resources.Keys} constants.
+     * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
+     */
+    static void warning(final WarningListeners<?> listeners, final Class<?> caller, final String method,
+            final Exception exception, IndexedResourceBundle resources, final short key, final Object... arguments)
+    {
+        if (resources == null) {
+            resources = Resources.forLocale(listeners.getLocale());
+        }
+        final LogRecord record = resources.getLogRecord(Level.WARNING, key, arguments);
+        record.setLoggerName(Modules.NETCDF);
+        record.setSourceClassName(caller.getCanonicalName());
+        record.setSourceMethodName(method);
+        if (exception != null) {
+            record.setThrown(exception);
+        }
+        listeners.warning(record);
+    }
+
+    /**
      * Returns a string representation of this element. Current implementation returns only the element class and name.
      *
      * @return string representation of this element for debugging purposes.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index ba8e2a9..98e4cc2 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -70,6 +70,12 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotComputeVariablePosition_2 = 6;
 
         /**
+         * Can not create the Coordinate Reference System for grid geometry “{1}” in the “{0}” netCDF
+         * file. The reason is: {2}
+         */
+        public static final short CanNotCreateCRS_3 = 11;
+
+        /**
          * Can not use UCAR library for netCDF format. Fallback on Apache SIS implementation.
          */
         public static final short CanNotUseUCAR = 4;
@@ -90,6 +96,12 @@ public final class Resources extends IndexedResourceBundle {
         public static final short MismatchedVariableSize_3 = 8;
 
         /**
+         * Reference system of type ‘{1}’ can not have {2} axes. The axes found in the “{0}” netCDF
+         * file are: {3}.
+         */
+        public static final short UnexpectedAxisCount_4 = 10;
+
+        /**
          * Variable “{1}” in file “{0}” has a dimension “{3}” while we expected “{2}”.
          */
         public static final short UnexpectedDimensionForVariable_4 = 2;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index e5e0e0c..246bccc 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -21,10 +21,12 @@
 #
 AmbiguousAxisDirection_4          = NetCDF file \u201c{0}\u201d provides an ambiguous axis direction for variable \u201c{1}\u201d. It could be {2}\u00a0or {3}.
 CanNotComputeVariablePosition_2   = Can not compute data location for \u201c{1}\u201d variable in the \u201c{0}\u201d netCDF file.
+CanNotCreateCRS_3                 = Can not create the Coordinate Reference System for grid geometry \u201c{1}\u201d in the \u201c{0}\u201d netCDF file. The reason is: {2}
 CanNotUseUCAR                     = Can not use UCAR library for netCDF format. Fallback on Apache SIS implementation.
 DimensionNotFound_3               = Dimension \u201c{2}\u201d declared by attribute \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
 DuplicatedReference_2             = Duplicated reference to \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 MismatchedVariableSize_3          = The declared size of variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d is {2} bytes greater than expected.
+UnexpectedAxisCount_4             = Reference system of type \u2018{1}\u2019 can not have {2}\u00a0axes. The axes found in the \u201c{0}\u201d netCDF file are: {3}.
 UnexpectedDimensionForVariable_4  = Variable \u201c{1}\u201d in file \u201c{0}\u201d has a dimension \u201c{3}\u201d while we expected \u201c{2}\u201d.
 UnsupportedDataType_3             = NetCDF file \u201c{0}\u201d uses unsupported data type {2} for variable \u201c{1}\u201d.
 VariableNotFound_2                = Variable \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index 9731b7d..407e89b 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -26,10 +26,12 @@
 #
 AmbiguousAxisDirection_4          = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb fournit une direction d\u2019axe ambigu\u00eb pour la variable \u00ab\u202f{1}\u202f\u00bb. Elle pourrait \u00eatre {2}\u00a0ou {3}.
 CanNotComputeVariablePosition_2   = Ne peut pas calculer la position des donn\u00e9es de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
+CanNotCreateCRS_3                 = Ne peut pas cr\u00e9er le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es pour la g\u00e9om\u00e9trie de grille \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb. La raison est\u2008: {2}
 CanNotUseUCAR                     = Ne peut pas utiliser la biblioth\u00e8que de l\u2019UCAR pour le format netCDF. L\u2019impl\u00e9mentation de Apache SIS sera utilis\u00e9e \u00e0 la place.
 DimensionNotFound_3               = La dimension \u00ab\u202f{2}\u202f\u00bb d\u00e9clar\u00e9e par l\u2019attribut \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
 DuplicatedReference_2             = R\u00e9f\u00e9rence vers \u00ab\u202f{1}\u202f\u00bb dupliqu\u00e9e dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 MismatchedVariableSize_3          = La longueur d\u00e9clar\u00e9e de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb d\u00e9passe de {2} octets la valeur attendue.
+UnexpectedAxisCount_4             = Les syst\u00e8mes de r\u00e9f\u00e9rence de type \u2018{1}\u2019 ne peuvent pas avoir {2}\u00a0axes. Les axes trouv\u00e9s dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb sont\u2008: {3}.
 UnexpectedDimensionForVariable_4  = La variable \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb a une dimension \u00ab\u202f{3}\u202f\u00bb alors qu\u2019on attendait \u00ab\u202f{2}\u202f\u00bb.
 UnsupportedDataType_3             = Le fichier netCDF \u00ab\u202f{0}\u202f\u00bb utilise un type de donn\u00e9es non-support\u00e9 {2} pour la variable \u00ab\u202f{1}\u202f\u00bb.
 VariableNotFound_2                = La variable \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
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 3ba3ce4..06d168e 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
@@ -16,16 +16,14 @@
  */
 package org.apache.sis.internal.netcdf;
 
+import java.util.Locale;
 import java.util.Collection;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
 import java.io.IOException;
 import java.awt.image.DataBuffer;
 import javax.measure.Unit;
 import javax.measure.format.ParserException;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
-import org.apache.sis.internal.system.Modules;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.resources.Errors;
@@ -75,7 +73,7 @@ public abstract class Variable extends NamedElement {
 
     /**
      * Returns the name of the netCDF file containing this variable, or {@code null} if unknown.
-     * This is used for information purpose only.
+     * This is used for information purpose or error message formatting only.
      *
      * @return name of the netCDF file containing this variable, or {@code null} if unknown.
      */
@@ -97,7 +95,8 @@ public abstract class Variable extends NamedElement {
     public abstract String getDescription();
 
     /**
-     * Returns the unit of measurement as a string, or {@code null} if none.
+     * Returns the unit of measurement as a string, or {@code null} or an empty string if none.
+     * The empty string can not be used for meaning "dimensionless unit"; some text is required.
      *
      * <p>Note: the UCAR library has its own API for handling units (e.g. {@link ucar.nc2.units.SimpleUnit}).
      * However as of November 2018, this API does not allow us to identify the quantity type except for some
@@ -118,11 +117,11 @@ public abstract class Variable extends NamedElement {
         if (!unitParsed) {
             unitParsed = true;                          // Set first for avoiding to report errors many times.
             final String symbols = getUnitsString();
-            if (symbols != null) try {
+            if (symbols != null && !symbols.isEmpty()) try {
                 unit = Units.valueOf(symbols);
-            } catch (ParserException e) {
-                listeners.warning(Errors.getResources(listeners.getLocale())
-                        .getString(Errors.Keys.CanNotAssignUnitToVariable_2, getName(), symbols), e);
+            } catch (ParserException ex) {
+                warning(listeners, Variable.class, "getUnit", ex, Errors.getResources(listeners.getLocale()),
+                        Errors.Keys.CanNotAssignUnitToVariable_2, getName(), symbols);
             }
         }
         return unit;
@@ -308,12 +307,19 @@ public abstract class Variable extends NamedElement {
     public abstract Vector read(int[] areaLower, int[] areaUpper, int[] subsampling) throws IOException, DataStoreException;
 
     /**
+     * Returns the locale to use for warnings and error messages.
+     */
+    final Locale getLocale() {
+        return listeners.getLocale();
+    }
+
+    /**
      * Returns the resources to use for warnings or error messages.
      *
-     * @return the resources for the locales specified by the given argument.
+     * @return the resources for the locales specified to the decoder.
      */
     protected final Resources resources() {
-        return Resources.forLocale(listeners.getLocale());
+        return Resources.forLocale(getLocale());
     }
 
     /**
@@ -325,11 +331,7 @@ public abstract class Variable extends NamedElement {
      * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
      */
     protected final void warning(final Class<?> caller, final String method, final short key, final Object... arguments) {
-        final LogRecord record = resources().getLogRecord(Level.WARNING, key, arguments);
-        record.setLoggerName(Modules.NETCDF);
-        record.setSourceClassName(caller.getCanonicalName());
-        record.setSourceMethodName(method);
-        listeners.warning(record);
+        warning(listeners, caller, method, null, null, key, arguments);
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
index 47c18b1..d8ad414 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
@@ -243,7 +243,7 @@ public final class ChannelDecoder extends Decoder {
          */
         int version = input.readInt();
         if ((version & 0xFFFFFF00) != MAGIC_NUMBER) {
-            throw new DataStoreContentException(errors().getString(Errors.Keys.UnexpectedFileFormat_2, "netCDF", getFilename()));
+            throw new DataStoreContentException(errors().getString(Errors.Keys.UnexpectedFileFormat_2, FORMAT_NAME, getFilename()));
         }
         /*
          * Check the version number.
@@ -252,7 +252,7 @@ public final class ChannelDecoder extends Decoder {
         switch (version) {
             case 1:  is64bits = false; break;
             case 2:  is64bits = true;  break;
-            default: throw new DataStoreContentException(errors().getString(Errors.Keys.UnsupportedFormatVersion_2, "netCDF", version));
+            default: throw new DataStoreContentException(errors().getString(Errors.Keys.UnsupportedFormatVersion_2, FORMAT_NAME, version));
             // If more cases are added, remember to increment the MAX_VERSION constant.
         }
         numrecs = input.readInt();
@@ -355,7 +355,7 @@ public final class ChannelDecoder extends Decoder {
      * that the file should be a netCDF one, but we found some inconsistency or unknown tags.
      */
     private DataStoreContentException malformedHeader() {
-        return new DataStoreContentException(listeners.getLocale(), "netCDF", getFilename(), null);
+        return new DataStoreContentException(listeners.getLocale(), FORMAT_NAME, getFilename(), null);
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridGeometryInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridGeometryInfo.java
index dc16c70..116c43e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridGeometryInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/GridGeometryInfo.java
@@ -111,14 +111,7 @@ final class GridGeometryInfo extends GridGeometry {
      */
     @Override
     public String getName() {
-        final StringBuilder buffer = new StringBuilder();
-        for (final VariableInfo variable : range) {
-            if (buffer.length() != 0) {
-                buffer.append(' ');
-            }
-            buffer.append(variable.getName());
-        }
-        return buffer.toString();
+        return listNames(range, range.length, " ");
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
index d11d376..6a0de7f 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
@@ -24,6 +24,7 @@ import java.lang.reflect.Array;
 import ucar.nc2.constants.CF;
 import ucar.nc2.constants.CDM;
 import ucar.nc2.constants._Coordinate;
+import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.DataType;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.netcdf.Resources;
@@ -204,7 +205,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
                     offsetToNextRecord = Math.multiplyExact(offsetToNextRecord, dim.length());
                 } else if (i != 0) {
                     // Unlimited dimension, if any, must be first in a netCDF 3 classic format.
-                    throw new DataStoreContentException(listeners.getLocale(), "netCDF", input.filename, null);
+                    throw new DataStoreContentException(listeners.getLocale(), Decoder.FORMAT_NAME, input.filename, null);
                 }
             }
             reader = new HyperRectangleReader(dataType.number, input, offset);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
index 9d2496e..3a2aaf7 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
@@ -105,7 +105,9 @@ final class VariableWrapper extends Variable {
     }
 
     /**
-     * Returns the unit of measurement as a string, or {@code null} if none.
+     * Returns the unit of measurement as a string, or an empty strong if none.
+     * Note that the UCAR library represents missing unit by an empty string,
+     * which is ambiguous with dimensionless unit.
      */
     @Override
     protected String getUnitsString() {
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index e56ff50..b2130cc 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -47,6 +47,7 @@ import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.metadata.constraint.Restriction;
 import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.crs.VerticalCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.logging.WarningListeners;
@@ -67,6 +68,7 @@ import org.apache.sis.internal.metadata.AxisDirections;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.referencing.CRS;
 import org.apache.sis.measure.Units;
 
 // The following dependency is used only for static final String constants.
@@ -147,13 +149,6 @@ final class MetadataReader extends MetadataBuilder {
     private static final char QUOTE = '"';
 
     /**
-     * The vertical coordinate reference system to be given to the object created by {@link #addExtent()}.
-     *
-     * @todo Should be set to {@code CommonCRS.MEAN_SEA_LEVEL}.
-     */
-    private static final VerticalCRS VERTICAL_CRS = null;
-
-    /**
      * The source of netCDF attributes from which to infer ISO metadata.
      * This source is set at construction time.
      *
@@ -186,6 +181,12 @@ final class MetadataReader extends MetadataBuilder {
     private transient Responsibility pointOfContact;
 
     /**
+     * The vertical coordinate reference system to be given to the object created by {@link #addExtent()}.
+     * This is set to the first vertical CRS found.
+     */
+    private VerticalCRS verticalCRS;
+
+    /**
      * Creates a new <cite>netCDF to ISO</cite> mapper for the given source.
      *
      * @param  decoder  the source of netCDF attributes.
@@ -553,7 +554,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
 
     /**
      * Adds a {@code DataIdentification/Citation} element if at least one of the required attributes is non-null.
-     * This method will initialize the {@link #pointOfContact} field, than reuse it if non-null and suitable.
+     * This method will initialize the {@link #pointOfContact} field, then reuses it if non-null and suitable.
      *
      * <p>This method opportunistically collects the name of all publishers.
      * Those names are useful to {@link #addIdentificationInfo(Set)}.</p>
@@ -687,9 +688,9 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
                     stringValue(GEOSPATIAL_BOUNDS + "_crs"), stringValue(GEOSPATIAL_BOUNDS + "_vertical_crs")));
         }
         try {
-            setFormat("NetCDF");
+            setFormat(NetcdfStoreProvider.NAME);
         } catch (MetadataStoreException e) {
-            addFormatName("NetCDF");
+            addFormatName(NetcdfStoreProvider.NAME);
             warning(e);
         }
     }
@@ -756,6 +757,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
     /**
      * Adds the extent declared in the current group. For more consistent results, the caller should restrict
      * the {@linkplain Decoder#setSearchPath search path} to a single group before invoking this method.
+     * The {@link #verticalCRS} field should have been set before to invoke this method.
      *
      * @return {@code true} if at least one numerical value has been added.
      */
@@ -776,7 +778,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
          * If at least one vertical coordinate is available, add a VerticalExtent.
          */
         if (fillExtent(VERTICAL, Units.METRE, null, extent, 0)) {
-            addVerticalExtent(extent[0], extent[1], VERTICAL_CRS);
+            addVerticalExtent(extent[0], extent[1], verticalCRS);
             hasExtent = true;
         }
         /*
@@ -1018,9 +1020,15 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
      * @throws ArithmeticException if the size of an axis exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
      */
     public Metadata read() throws IOException, DataStoreException {
+        for (final GridGeometry cs : decoder.getGridGeometries()) {
+            final CoordinateReferenceSystem crs = cs.getCoordinateReferenceSystem(decoder);
+            addReferenceSystem(crs);
+            if (verticalCRS == null) {
+                verticalCRS = CRS.getVerticalComponent(crs, false);
+            }
+        }
         addResourceScope(ScopeCode.DATASET, null);
-        Set<InternationalString> publisher = addCitation();
-        addIdentificationInfo(publisher);
+        addIdentificationInfo(addCitation());
         for (final String service : SERVICES) {
             final String name = stringValue(service);
             if (name != null) {
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
index 4ccd6d7..779a47c 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
@@ -101,6 +101,7 @@ public final strictfp class MetadataReaderTest extends TestCase {
     static void compareToExpected(final Metadata actual) {
         final ContentVerifier verifier = new ContentVerifier();
         verifier.addPropertyToIgnore(Metadata.class, "metadataStandard");
+        verifier.addPropertyToIgnore(Metadata.class, "referenceSystemInfo");
         verifier.addMetadataToVerify(actual);
         verifier.assertMetadataEquals(
             // Hard-coded


Mime
View raw message