You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/11/29 23:22:22 UTC

[sis] branch geoapi-4.0 updated (a24f3de -> 5b8c29b)

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

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


    from a24f3de  More stable behavior of the tree of resources when the second resource is added after the first one.
     new d58bf37  Subsampling computed by `GridDerivation.subgrid(GridGeometry)` must be integer (it was not when the given grid geometry has only a "grid to CRS" transform). Better determination of which grid between source and target must have a CRS.
     new 5b8c29b  Allow netCDF reader to distinguish "discrete coverage" from "continuous coverage". In GCOM-C case, the "QA_flag" (data quality) variable is a discrete coverage.

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


Summary of changes:
 .../java/org/apache/sis/coverage/Category.java     |  8 +--
 .../org/apache/sis/coverage/SampleDimension.java   | 10 ++--
 .../coverage/grid/CoordinateOperationFinder.java   | 65 ++++++++++------------
 .../apache/sis/coverage/grid/GridDerivation.java   | 22 ++++----
 .../org/apache/sis/coverage/grid/GridGeometry.java |  1 +
 .../sis/coverage/grid/ResampledGridCoverage.java   |  1 +
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   | 16 ++++--
 .../apache/sis/internal/netcdf/RasterResource.java | 10 +++-
 .../org/apache/sis/internal/netcdf/Variable.java   |  2 +
 .../apache/sis/internal/netcdf/VariableRole.java   | 27 ++++++++-
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |  6 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |  2 +-
 12 files changed, 103 insertions(+), 67 deletions(-)

[sis] 01/02: Subsampling computed by `GridDerivation.subgrid(GridGeometry)` must be integer (it was not when the given grid geometry has only a "grid to CRS" transform). Better determination of which grid between source and target must have a CRS.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit d58bf3769ea80723c2b24802da01a4933bea847d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Nov 29 23:01:23 2021 +0100

    Subsampling computed by `GridDerivation.subgrid(GridGeometry)` must be integer
    (it was not when the given grid geometry has only a "grid to CRS" transform).
    Better determination of which grid between source and target must have a CRS.
---
 .../coverage/grid/CoordinateOperationFinder.java   | 65 ++++++++++------------
 .../apache/sis/coverage/grid/GridDerivation.java   | 22 ++++----
 .../org/apache/sis/coverage/grid/GridGeometry.java |  1 +
 .../sis/coverage/grid/ResampledGridCoverage.java   |  1 +
 4 files changed, 43 insertions(+), 46 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
index 1608b6c..df88dbb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
@@ -17,6 +17,7 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Arrays;
+import java.util.Objects;
 import java.util.function.Supplier;
 import javax.measure.Quantity;
 import javax.measure.quantity.Length;
@@ -74,7 +75,7 @@ import org.apache.sis.internal.util.Numerics;
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -234,6 +235,18 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
     }
 
     /**
+     * Verifies whether the presence of a CRS considered mandatory, unless the CRS of opposite grid
+     * is also missing.
+     *
+     * @param  rs  {@code true} is source CRS is mandatory, {@code false} if target CRS is mandatory.
+     */
+    final void verifyPresenceOfCRS(final boolean rs) {
+        if ((rs ? target : source).isDefined(GridGeometry.CRS)) {
+            Objects.requireNonNull((rs ? source : target).getCoordinateReferenceSystem());
+        }
+    }
+
+    /**
      * Sets whether operations will be between cell centers or cell corners.
      * This method must be invoked before any other method in this class.
      * The {@link PixelInCell#CELL_CORNER} value should be used first
@@ -277,28 +290,12 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
     }
 
     /**
-     * Returns the CRS of the source grid geometry. If neither the source and target grid geometry
-     * define a CRS, then this method returns {@code null}.
-     *
-     * @throws IncompleteGridGeometryException if the target grid geometry has a CRS but the source
-     *         grid geometry has none. Note that the converse is allowed, in which case the target
-     *         CRS is assumed the same than the source.
-     */
-    private CoordinateReferenceSystem getSourceCRS() {
-        return source.isDefined(GridGeometry.CRS) ||
-               target.isDefined(GridGeometry.CRS) ? source.getCoordinateReferenceSystem() : null;
-    }
-
-    /**
      * Returns the target of the "corner to CRS" transform.
      * May be {@code null} if the neither the source and target grid geometry define a CRS.
-     *
-     * @throws IncompleteGridGeometryException if the target grid geometry has a CRS but the source
-     *         grid geometry has none. Note that the converse is allowed, in which case the target
-     *         CRS is assumed the same than the source.
      */
     final CoordinateReferenceSystem getTargetCRS() {
-        return (changeOfCRS != null) ? changeOfCRS.getTargetCRS() : getSourceCRS();
+        return (changeOfCRS != null) ? changeOfCRS.getTargetCRS() :
+                source.isDefined(GridGeometry.CRS) ? source.getCoordinateReferenceSystem() : null;
     }
 
     /**
@@ -322,23 +319,21 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
                 if (sourceEnvelope != null && targetEnvelope != null) {
                     changeOfCRS = Envelopes.findOperation(sourceEnvelope, targetEnvelope);
                 }
-                if (changeOfCRS == null && target.isDefined(GridGeometry.CRS)) {
-                    final CoordinateReferenceSystem sourceCRS = getSourceCRS();
-                    if (sourceCRS != null) {
-                        /*
-                         * Unconditionally create operation even if CRS are the same. A non-null operation trig
-                         * the check for wraparound axes, which is necessary even if the transform is identity.
-                         */
-                        DefaultGeographicBoundingBox areaOfInterest = null;
-                        if (sourceEnvelope != null || targetEnvelope != null) try {
-                            areaOfInterest = new DefaultGeographicBoundingBox();
-                            areaOfInterest.setBounds(targetEnvelope != null ? targetEnvelope : sourceEnvelope);
-                        } catch (TransformException e) {
-                            areaOfInterest = null;
-                            recoverableException("changeOfCRS", e);
-                        }
-                        changeOfCRS = CRS.findOperation(sourceCRS, target.getCoordinateReferenceSystem(), areaOfInterest);
+                if (changeOfCRS == null && source.isDefined(GridGeometry.CRS) && target.isDefined(GridGeometry.CRS)) {
+                    final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem();
+                    /*
+                     * Unconditionally create operation even if CRS are the same. A non-null operation trig
+                     * the check for wraparound axes, which is necessary even if the transform is identity.
+                     */
+                    DefaultGeographicBoundingBox areaOfInterest = null;
+                    if (sourceEnvelope != null || targetEnvelope != null) try {
+                        areaOfInterest = new DefaultGeographicBoundingBox();
+                        areaOfInterest.setBounds(targetEnvelope != null ? targetEnvelope : sourceEnvelope);
+                    } catch (TransformException e) {
+                        areaOfInterest = null;
+                        recoverableException("changeOfCRS", e);
                     }
+                    changeOfCRS = CRS.findOperation(sourceCRS, target.getCoordinateReferenceSystem(), areaOfInterest);
                 }
             } catch (BackingStoreException e) {                         // May be thrown by getConstantCoordinates().
                 throw e.unwrapOrRethrow(TransformException.class);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index 9041ca6..0822d14 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -500,25 +500,25 @@ public class GridDerivation {
             if (base.equals(areaOfInterest)) {
                 return this;
             }
-            if (areaOfInterest.extent == null && areaOfInterest.resolution != null) {
-                return subgrid(areaOfInterest.envelope, areaOfInterest.resolution);
-            }
             final MathTransform mapCenters;
-            final GridExtent domain = areaOfInterest.getExtent();       // May throw IncompleteGridGeometryException.
+            final GridExtent domain = areaOfInterest.extent;
+            final CoordinateOperationFinder finder = new CoordinateOperationFinder(areaOfInterest, base);
+            finder.verifyPresenceOfCRS(false);
             try {
-                final CoordinateOperationFinder finder = new CoordinateOperationFinder(areaOfInterest, base);
-                final MathTransform mapCorners = finder.gridToGrid();
+                final MathTransform mapCorners = (domain != null) ? finder.gridToGrid() : null;
                 finder.setAnchor(PixelInCell.CELL_CENTER);
                 finder.nowraparound();
                 mapCenters = finder.gridToGrid();                               // We will use only the scale factors.
-                setBaseExtentClipped(domain.toCRS(mapCorners, mapCenters, null));
+                if (domain != null) {
+                    setBaseExtentClipped(domain.toCRS(mapCorners, mapCenters, null));
+                    if (baseExtent != base.extent && baseExtent.equals(domain)) {
+                        baseExtent = domain;                                    // Share common instance.
+                    }
+                }
                 subGridSetter = "subgrid";
             } catch (FactoryException | TransformException e) {
                 throw new IllegalGridGeometryException(e, "areaOfInterest");
             }
-            if (baseExtent != base.extent && baseExtent.equals(areaOfInterest.extent)) {
-                baseExtent = areaOfInterest.extent;                                         // Share common instance.
-            }
             // The `domain` extent must be the source of the `mapCenters` transform.
             scales = GridGeometry.resolution(mapCenters, domain);
         }
@@ -540,7 +540,7 @@ public class GridDerivation {
     }
 
     /**
-     * Requests a grid geometry over a sub-envelope and optionally with a different a coarser resolution.
+     * Requests a grid geometry over a sub-envelope and optionally with a coarser resolution.
      * The given envelope does not need to be expressed in the same coordinate reference system (CRS)
      * than {@linkplain GridGeometry#getCoordinateReferenceSystem() the CRS of the base grid geometry};
      * coordinate conversions or transformations will be applied as needed.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 720fd8d..25eddfd 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1365,6 +1365,7 @@ public class GridGeometry implements LenientComparable, Serializable {
          * effort for using `WraparoundTransform` only if needed (contrarily to `gridToCRS(…)`).
          */
         final CoordinateOperationFinder finder = new CoordinateOperationFinder(target, this);
+        finder.verifyPresenceOfCRS(false);
         finder.setAnchor(anchor);
         final MathTransform tr;
         try {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index dc01606..758678b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -282,6 +282,7 @@ final class ResampledGridCoverage extends GridCoverage {
     {
         final GridGeometry sourceGG = source.getGridGeometry();
         final CoordinateOperationFinder changeOfCRS = new CoordinateOperationFinder(sourceGG, target);
+        changeOfCRS.verifyPresenceOfCRS(true);
         /*
          * Compute the transform from source pixels to target CRS (to be completed to target pixels later).
          * The following lines may throw IncompleteGridGeometryException, which is desired because if that

[sis] 02/02: Allow netCDF reader to distinguish "discrete coverage" from "continuous coverage". In GCOM-C case, the "QA_flag" (data quality) variable is a discrete coverage.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 5b8c29b65aef1d3543a37d6158a0026a61170437
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Nov 30 00:18:46 2021 +0100

    Allow netCDF reader to distinguish "discrete coverage" from "continuous coverage".
    In GCOM-C case, the "QA_flag" (data quality) variable is a discrete coverage.
---
 .../java/org/apache/sis/coverage/Category.java     |  8 +++----
 .../org/apache/sis/coverage/SampleDimension.java   | 10 ++++----
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   | 16 +++++++++----
 .../apache/sis/internal/netcdf/RasterResource.java | 10 +++++---
 .../org/apache/sis/internal/netcdf/Variable.java   |  2 ++
 .../apache/sis/internal/netcdf/VariableRole.java   | 27 +++++++++++++++++++---
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |  6 +++--
 .../apache/sis/storage/netcdf/MetadataReader.java  |  2 +-
 8 files changed, 60 insertions(+), 21 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
index 8d21e70..019c4ea 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
@@ -227,14 +227,14 @@ public class Category implements Serializable {
         ArgumentChecks.ensureNonNull("samples", samples);
         if (units != null) {
             ArgumentChecks.ensureNonNull("toUnits", toUnits);
-            // The converse is not true: we allow 'units' to be null even if 'toUnits' is non-null.
+            // The converse is not true: we allow `units` to be null even if `toUnits` is non-null.
         }
         this.name = Types.toInternationalString(name);
         final double  minimum = samples.getMinDouble(true);
         final double  maximum = samples.getMaxDouble(true);
         final boolean isNaN   = Double.isNaN(minimum);
         /*
-         * Following arguments check uses '!' in comparison in order to reject NaN values in quantitative category.
+         * Following arguments check uses `!` in comparison in order to reject NaN values in quantitative category.
          * For qualitative category, NaN is accepted provided that it is the same NaN for both ends of the range.
          */
         if (!(minimum <= maximum)) {
@@ -301,7 +301,7 @@ public class Category implements Serializable {
         name       = original.name;
         toConverse = Objects.requireNonNull(toSamples);
         /*
-         * Compute 'minimum' and 'maximum' (which must be real numbers) using the conversion from samples
+         * Compute `minimum` and `maximum` (which must be real numbers) using the conversion from samples
          * to real values. To be strict, we should use some numerical algorithm for finding a function's
          * minimum and maximum. For linear and logarithmic functions, minimum and maximum are always at
          * the bounding input values, so we are using a very simple algorithm for now.
@@ -449,7 +449,7 @@ public class Category implements Serializable {
     public Optional<MathTransform1D> getTransferFunction() {
         /*
          * Note: if this method is invoked on "real values category", then we need to return
-         * the identity transform instead of 'toConverse'. This is done by ConvertedCategory.
+         * the identity transform instead of `toConverse`. This is done by ConvertedCategory.
          */
         if (converse.isConvertedQualitative()) {
             return Optional.empty();
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
index ce4e432..819634f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -1033,7 +1033,7 @@ public class SampleDimension implements Serializable {
          * @param  name     the category name as a {@link String} or {@link InternationalString} object.
          * @param  minimum  the minimum value (inclusive) in the given units.
          * @param  maximum  the maximum value (inclusive) in the given units.
-         * @param  units    the units of measurement.
+         * @param  units    the units of measurement, or {@code null} if unknown or not applicable.
          * @return {@code this}, for method call chaining.
          * @throws IllegalArgumentException if a value is NaN or if {@code minimum} is greater than {@code maximum}.
          */
@@ -1051,7 +1051,7 @@ public class SampleDimension implements Serializable {
          * @param  name     the category name as a {@link String} or {@link InternationalString} object.
          * @param  minimum  the minimum value (inclusive) in the given units.
          * @param  maximum  the maximum value (inclusive) in the given units.
-         * @param  units    the units of measurement.
+         * @param  units    the units of measurement, or {@code null} if unknown or not applicable.
          * @return {@code this}, for method call chaining.
          * @throws IllegalArgumentException if a value is NaN or if {@code minimum} is greater than {@code maximum}.
          */
@@ -1075,7 +1075,8 @@ public class SampleDimension implements Serializable {
          * @param  upper   the upper sample value, exclusive.
          * @param  scale   the scale value which is multiplied to sample values for the category. Must be different than zero.
          * @param  offset  the offset value to add to sample values for this category.
-         * @param  units   the units of measurement of values after conversion by the scale factor and offset.
+         * @param  units   the units of measurement of values after conversion by the scale factor and offset,
+         *                 or {@code null} if unknown or not applicable.
          * @return {@code this}, for method call chaining.
          * @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper},
          *         or if {@code scale} or {@code offset} are not real numbers, or if {@code scale} is zero.
@@ -1100,7 +1101,8 @@ public class SampleDimension implements Serializable {
          * @param  samples  the minimum and maximum sample values in the category. Element class is usually
          *                  {@link Integer}, but {@link Float} and {@link Double} types are accepted as well.
          * @param  toUnits  the transfer function from sample values to real values in the specified units.
-         * @param  units    the units of measurement of values after conversion by the transfer function.
+         * @param  units    the units of measurement of values after conversion by the transfer function,
+         *                  or {@code null} if unknown or not applicable.
          * @return {@code this}, for method call chaining.
          * @throws ClassCastException if the range element class is not a {@link Number} subclass.
          * @throws IllegalArgumentException if the range is invalid.
diff --git a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index ad4aebb..9df14da 100644
--- a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -113,7 +113,7 @@ import ucar.nc2.constants.CF;
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see <a href="http://global.jaxa.jp/projects/sat/gcom_c/">SHIKISAI (GCOM-C) on JAXA</a>
  * @see <a href="https://en.wikipedia.org/wiki/Global_Change_Observation_Mission">GCOM on Wikipedia</a>
@@ -128,6 +128,11 @@ public final class GCOM_C extends Convention {
     private static final Pattern SENTINEL_VALUE = Pattern.compile(".*\\bGCOM-C\\b.*");
 
     /**
+     * Name of the variable storing data quality flags.
+     */
+    private static final String QA_FLAG = "QA_flag";
+
+    /**
      * Mapping from ACDD or CF-Convention attribute names to names of attributes used by GCOM-C.
      * This map does not include attributes for geographic extent because the "Lower_left_latitude",
      * "Lower_left_longitude", "Lower_right_latitude", <i>etc.</i> attributes are difficult to use.
@@ -238,7 +243,7 @@ public final class GCOM_C extends Convention {
      */
     @Override
     public VariableRole roleOf(final Variable variable) {
-        VariableRole role = super.roleOf(variable);
+        final VariableRole role = super.roleOf(variable);
         if (role == VariableRole.COVERAGE) {
             /*
              * Exclude (for now) some variables associated to longitude and latitude: Obs_time, Sensor_zenith, Solar_zenith.
@@ -246,7 +251,10 @@ public final class GCOM_C extends Convention {
              */
             final String group = variable.getGroupName();
             if (GEOMETRY_DATA.equalsIgnoreCase(group)) {
-                role = VariableRole.OTHER;
+                return VariableRole.OTHER;
+            }
+            if (QA_FLAG.equals(variable.getName())) {
+                return VariableRole.DISCRETE_COVERAGE;
             }
         }
         return role;
@@ -272,7 +280,7 @@ public final class GCOM_C extends Convention {
     public String nameOfDimension(final Variable dataOrAxis, final int index) {
         String name = super.nameOfDimension(dataOrAxis, index);
         if (name == null) {
-            if ("QA_flag".equals(dataOrAxis.getName())) {
+            if (QA_FLAG.equals(dataOrAxis.getName())) {
                 /*
                  * The "QA_flag" variable is missing "Dim0" and "Dim1" attribute in GCOM-C version 1.00.
                  * However not all GCOM-C files use a localization grid. We use the presence of spatial
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 7fa5889..7001f2e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -210,7 +210,7 @@ public final class RasterResource extends AbstractGridResource implements Resour
         final Map<GenericName,List<RasterResource>> byName = new HashMap<>();   // For detecting name collisions.
         for (int i=0; i<variables.length; i++) {
             final Variable variable = variables[i];
-            if (variable == null || variable.getRole() != VariableRole.COVERAGE) {
+            if (!VariableRole.isCoverage(variable)) {
                 continue;                                                   // Skip variables that are not grid coverages.
             }
             final GridGeometry grid = variable.getGridGeometry();
@@ -268,7 +268,7 @@ public final class RasterResource extends AbstractGridResource implements Resour
                         int suffixLength = name.length() - suffixStart;
                         for (int j=i; ++j < variables.length;) {
                             final Variable candidate = variables[j];
-                            if (candidate == null || candidate.getRole() != VariableRole.COVERAGE) {
+                            if (!VariableRole.isCoverage(candidate)) {
                                 variables[j] = null;                                // For avoiding to revisit that variable again.
                                 continue;
                             }
@@ -504,7 +504,11 @@ public final class RasterResource extends AbstractGridResource implements Resour
             } else {
                 String name = band.getDescription();
                 if (name == null) name = band.getName();
-                builder.addQuantitative(name, range, mt, band.getUnit());
+                if (band.getRole() == VariableRole.DISCRETE_COVERAGE) {
+                    builder.addQualitative(name, range);
+                } else {
+                    builder.addQuantitative(name, range, mt, band.getUnit());
+                }
             }
         } catch (TransformException e) {
             /*
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 e71bfa5..55b301a 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
@@ -933,6 +933,8 @@ public abstract class Variable extends Node {
 
     /**
      * Builds the function converting values from their packed formats in the variable to "real" values.
+     * This method is invoked in contexts where a transfer function is assumed to exist. Consequently it
+     * shall never return {@code null}, but may return the identity function.
      */
     final TransferFunction getTransferFunction() {
         return decoder.convention().transferFunction(this);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/VariableRole.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/VariableRole.java
index 2c37ac8..97a092d 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/VariableRole.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/VariableRole.java
@@ -21,7 +21,7 @@ package org.apache.sis.internal.netcdf;
  * Specifies whether a variable is used as a coordinate system axis, a coverage or other purpose.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.0
  * @module
  */
@@ -32,11 +32,18 @@ public enum VariableRole {
     AXIS,
 
     /**
-     * The variable is a grid coverage.
+     * The variable is a continuous grid coverage.
+     * Interpolation between cells is allowed.
      */
     COVERAGE,
 
     /**
+     * The variable is a discrete grid coverage, for example data quality masks.
+     * Interpolation between cells is not allowed.
+     */
+    DISCRETE_COVERAGE,
+
+    /**
      * The variable is a property of a feature.
      */
     FEATURE,
@@ -50,5 +57,19 @@ public enum VariableRole {
     /**
      * Unidentified kind of variable.
      */
-    OTHER
+    OTHER;
+
+    /**
+     * Returns {@code true} if the role of the given variable is {@link #COVERAGE} or {@link #DISCRETE_COVERAGE}.
+     *
+     * @param  candidate  the variable for which to check the role, or {@code null}.
+     * @return whether the given variable is non-null and its role is a continuous or discrete coverage.
+     */
+    public static boolean isCoverage(final Variable candidate) {
+        if (candidate != null) {
+            final VariableRole role = candidate.getRole();
+            return (role == COVERAGE || role == DISCRETE_COVERAGE);
+        }
+        return false;
+    }
 }
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 6452e53..e0d9792 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
@@ -74,7 +74,7 @@ import org.apache.sis.math.Vector;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see <a href="http://portal.opengeospatial.org/files/?artifact_id=43734">NetCDF Classic and 64-bit Offset Format (1.0)</a>
  *
@@ -974,7 +974,9 @@ public final class ChannelDecoder extends Decoder {
             final Map<DimensionInfo, List<VariableInfo>> dimToAxes = new IdentityHashMap<>();
             for (final VariableInfo variable : variables) {
                 switch (variable.getRole()) {
-                    case COVERAGE: {
+                    case COVERAGE:
+                    case DISCRETE_COVERAGE:
+                    {
                         // If Convention.roleOf(…) overwrote the value computed by VariableInfo,
                         // remember the new value for avoiding to ask again in next loops.
                         variable.isCoordinateSystemAxis = false;
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 7bd6ae3..be567fd 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
@@ -882,7 +882,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
     private void addContentInfo() {
         final Map<List<String>, List<Variable>> contents = new HashMap<>(4);
         for (final Variable variable : decoder.getVariables()) {
-            if (variable.getRole() == VariableRole.COVERAGE) {
+            if (VariableRole.isCoverage(variable)) {
                 final List<org.apache.sis.internal.netcdf.Dimension> dimensions = variable.getGridDimensions();
                 final String[] names = new String[dimensions.size()];
                 for (int i=0; i<names.length; i++) {