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: Parse missing values and fill values in netCDF files.
Date Thu, 06 Dec 2018 22:19:48 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 f2bee90  Parse missing values and fill values in netCDF files.
f2bee90 is described below

commit f2bee909eecb8cb6a155666ee25e7d172172f098
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Dec 6 23:19:31 2018 +0100

    Parse missing values and fill values in netCDF files.
---
 .../org/apache/sis/coverage/SampleDimension.java   |  20 +++
 .../org/apache/sis/coverage/SampleRangeFormat.java | 144 +++++++++++++++------
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../org/apache/sis/internal/netcdf/Variable.java   |  86 +++++++++++-
 .../sis/internal/netcdf/impl/VariableInfo.java     |  55 ++++++--
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  44 ++++++-
 .../apache/sis/storage/netcdf/GridResource.java    | 119 ++++++++++-------
 9 files changed, 376 insertions(+), 99 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index b1ba2f5..3d57af1 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -628,11 +628,31 @@ public class SampleDimension implements Serializable {
          */
         public Builder addQuantitative(CharSequence name, NumberRange<?> samples, MathTransform1D
toUnits, Unit<?> units) {
             ArgumentChecks.ensureNonNull("toUnits", toUnits);
+            if (units != null && toUnits.isIdentity() && samples != null
&& !(samples instanceof MeasurementRange<?>)) {
+                samples = new MeasurementRange<>(samples, units);
+            }
             categories.add(new Category(name, samples, toUnits, units, padValues));
             return this;
         }
 
         /**
+         * Returns {@code true} if the given range intersect the range of at least one category
previously added.
+         * This method can be invoked before to add a new category for checking if it would
cause a range collision.
+         *
+         * @param  minimum  minimal value of the range to test, inclusive.
+         * @param  maximum  maximal value of the range to test, inclusive.
+         * @return whether the given range intersects at least one previously added range.
+         */
+        public boolean intersect(final double minimum, final double maximum) {
+            for (final Category category : categories) {
+                if (maximum >= category.minimum && minimum <= category.maximum)
{
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
          * Creates a new sample with the properties defined to this builder.
          *
          * @return the sample dimension.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
index d8b7fa9..a8ca87e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
@@ -20,11 +20,14 @@ import java.util.List;
 import java.util.Locale;
 import java.text.NumberFormat;
 import java.io.IOException;
+import java.text.DecimalFormat;
 import org.opengis.util.InternationalString;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.measure.Range;
 import org.apache.sis.measure.RangeFormat;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.util.resources.Vocabulary;
 
 
@@ -50,6 +53,18 @@ final class SampleRangeFormat extends RangeFormat {
     private int ndigits;
 
     /**
+     * {@code true} if the range of sample values is different than the range of real values,
or
+     * if there is qualitative categories. If {@code false}, then we can omit the "Samples"
column.
+     */
+    private boolean hasPackedValues;
+
+    /**
+     * Whether {@link #prepare(List)} found at least one quantitative category.
+     * If {@code false}, then we can omit the "Measures" column.
+     */
+    private boolean hasQuantitative;
+
+    /**
      * The localize resources for table header. Words will be "Values", "Measures" and "Name".
      */
     private final Vocabulary words;
@@ -68,40 +83,70 @@ final class SampleRangeFormat extends RangeFormat {
      * Computes the smallest number of fraction digits necessary to resolve all quantitative
values.
      * This method assumes that real values in the range {@code Category.converted.range}
are stored
      * as integer sample values in the range {@code Category.range}.
-     *
-     * @return {@code true} if at least one quantitative category has been found.
      */
-    private boolean prepare(final List<Category> categories) {
-        ndigits = 0;
-        boolean hasQuantitative = false;
+    private void prepare(final List<Category> categories) {
+        ndigits         = 0;
+        hasPackedValues = false;
+        hasQuantitative = false;
         for (final Category category : categories) {
             final Category converted = category.converted;
-            final double increment = (converted.maximum - converted.minimum)
-                                   / ( category.maximum -  category.minimum);
+            final boolean  isPacked  = (category.minimum != converted.minimum)
+                                     | (category.maximum != converted.maximum);
+            hasPackedValues |= isPacked;
+            /*
+             * If the sample values are already real values, pretend that they are packed
in bytes.
+             * The intent is only to compute an arbitrary number of fraction digits.
+             */
+            final double range = isPacked ? ( category.maximum -  category.minimum) : 255;
+            final double increment =        (converted.maximum - converted.minimum) / range;
             if (!Double.isNaN(increment)) {
                 hasQuantitative = true;
-                final int n = 1 - Numerics.toExp10(Math.getExponent(increment));
+                final int n = -Numerics.toExp10(Math.getExponent(increment));
                 if (n > ndigits) {
                     ndigits = n;
-                    if (n >= MAX_DIGITS) {
-                        ndigits = MAX_DIGITS;
-                        break;
-                    }
                 }
             }
         }
-        return hasQuantitative;
+        if (ndigits >= MAX_DIGITS) {
+            ndigits = MAX_DIGITS;
+        }
     }
 
     /**
      * Formats a sample value or a range of sample value.
      * The value should have been fetched by {@link Category#getRangeLabel()}.
+     * This method applies the following rules:
+     *
+     * <ul>
+     *   <li>If the value is a number, check if we should use scientific notation.</li>
+     *   <li>If the value is a range, discard the unit of measurement if any.
+     *       We do that because the range may be repeated in the "Measure" column with units.</li>
+     * </ul>
      */
-    private String formatSample(final Object value) {
+    private String formatSample(Object value) {
         if (value instanceof Number) {
-            return elementFormat.format(value).concat(" ");
+            final double m = Math.abs(((Number) value).doubleValue());
+            final String text;
+            if ((m >= 1E+9 || m < 1E-4) && elementFormat instanceof DecimalFormat)
{
+                final DecimalFormat df = (DecimalFormat) elementFormat;
+                final String pattern = df.toPattern();
+                df.applyPattern("0.######E00");
+                text = df.format(value);
+                df.applyPattern(pattern);
+            } else {
+                text = elementFormat.format(value);
+            }
+            return text.concat(" ");
         } else if (value instanceof Range<?>) {
-            return format(value);
+            if (value instanceof MeasurementRange<?>) {
+                /*
+                 * Probably the same range than the one to be formatted in the "Measure"
column.
+                 * Format it in the same way (same number of fraction digits) but without
units.
+                 */
+                return formatMeasure(new NumberRange<>((MeasurementRange<?>)
value));
+            } else {
+                return format(value);
+            }
         } else {
             return String.valueOf(value);
         }
@@ -111,22 +156,22 @@ final class SampleRangeFormat extends RangeFormat {
      * Formats a range of measurements. There is usually only zero or one range of measurement
per {@link SampleDimension},
      * but {@code SampleRangeFormat} is not restricted to that limit. The number of fraction
digits to use should have been
      * computed by {@link #prepare(List)} before to call this method.
+     *
+     * @return the range to write, or {@code null} if the given {@code range} argument was
null.
      */
     private String formatMeasure(final Range<?> range) {
         if (range == null) {
-            return "";
+            return null;
         }
         final NumberFormat nf = (NumberFormat) elementFormat;
         final int min = nf.getMinimumFractionDigits();
         final int max = nf.getMaximumFractionDigits();
-        try {
-            nf.setMinimumFractionDigits(ndigits);
-            nf.setMaximumFractionDigits(ndigits);
-            return format(range);
-        } finally {
-            nf.setMinimumFractionDigits(min);
-            nf.setMaximumFractionDigits(max);
-        }
+        nf.setMinimumFractionDigits(ndigits);
+        nf.setMaximumFractionDigits(ndigits);
+        final String text = format(range);
+        nf.setMinimumFractionDigits(min);
+        nf.setMaximumFractionDigits(max);
+        return text;
     }
 
     /**
@@ -147,33 +192,58 @@ final class SampleRangeFormat extends RangeFormat {
 
     /**
      * Formats a string representation of the given list of categories.
+     * This method formats a table like below:
+     *
+     * {@preformat text
+     *   ┌────────────┬────────────────┬─────────────┐
+     *   │   Values   │    Measures    │    Name     │
+     *   ├────────────┼────────────────┼─────────────┤
+     *   │         0  │ NaN #0         │ No data     │
+     *   │         1  │ NaN #1         │ Clouds      │
+     *   │         5  │ NaN #5         │ Lands       │
+     *   │ [10 … 200) │ [6.0 … 25.0)°C │ Temperature │
+     *   └────────────┴────────────────┴─────────────┘
+     * }
      *
      * @param title       caption for the table.
      * @param categories  the list of categories to format.
      * @param out         where to write the category table.
      */
     void format(final InternationalString title, final CategoryList categories, final Appendable
out) throws IOException {
+        prepare(categories);
         final String lineSeparator = System.lineSeparator();
         out.append(title.toString(getLocale())).append(lineSeparator);
-        final TableAppender table  = new TableAppender(out, " │ ");
-        final boolean hasQuantitative = prepare(categories);
+        /*
+         * Write table header: │ Values │ Measures │ name │
+         */
+        final TableAppender table = new TableAppender(out, " │ ");
         table.appendHorizontalSeparator();
         table.setCellAlignment(TableAppender.ALIGN_CENTER);
-        table.append(words.getString(Vocabulary.Keys.Values)).nextColumn();
-        if (hasQuantitative) {
-            table.append(words.getString(Vocabulary.Keys.Measures)).nextColumn();
-        }
-        table.append(words.getString(Vocabulary.Keys.Name)).nextLine();
+        if (hasPackedValues) table.append(words.getString(Vocabulary.Keys.Values))  .nextColumn();
+        if (hasQuantitative) table.append(words.getString(Vocabulary.Keys.Measures)).nextColumn();
+        /* Unconditional  */ table.append(words.getString(Vocabulary.Keys.Name))    .nextLine();
         table.appendHorizontalSeparator();
         for (final Category category : categories) {
-            table.setCellAlignment(TableAppender.ALIGN_RIGHT);
-            table.append(formatSample(category.getRangeLabel()));
-            table.nextColumn();
-            if (hasQuantitative) {
-                table.append(formatMeasure(category.converted.range));
+            /*
+             * "Sample values" column. Omitted if all values are already real values.
+             */
+            if (hasPackedValues) {
+                table.setCellAlignment(TableAppender.ALIGN_RIGHT);
+                table.append(formatSample(category.getRangeLabel()));
                 table.nextColumn();
             }
             table.setCellAlignment(TableAppender.ALIGN_LEFT);
+            /*
+             * "Real values" column. Omitted if no category has a transfer function.
+             */
+            if (hasQuantitative) {
+                String text = formatMeasure(category.converted.range);            // Example:
[6.0 … 25.0)°C
+                if (text == null) {
+                    text = String.valueOf(category.converted.getRangeLabel());    // Example:
NaN #0
+                }
+                table.append(text);
+                table.nextColumn();
+            }
             table.append(category.name.toString(getLocale()));
             table.nextLine();
         }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index a42b856..4b13da5 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -347,6 +347,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short File = 144;
 
         /**
+         * Fill value
+         */
+        public static final short FillValue = 159;
+
+        /**
          * Geocentric
          */
         public static final short Geocentric = 42;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index b2c7a9e..e93052d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -72,6 +72,7 @@ EntryCount_1            = {0} entr{0,choice,0#y|2#ies}
 Envelope                = Envelope
 Exit                    = Exit
 File                    = File
+FillValue               = Fill value
 Geocentric              = Geocentric
 GeocentricRadius        = Geocentric radius
 GeocentricConversion    = Geocentric conversion
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 47dffd8..6c739ab 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -79,6 +79,7 @@ EndDate                 = Date de fin
 Envelope                = Enveloppe
 Exit                    = Quitter
 File                    = Fichier
+FillValue               = Valeur de remplissage
 Geocentric              = G\u00e9ocentrique
 GeocentricRadius        = Rayon g\u00e9ocentrique
 GeocentricConversion    = Conversion g\u00e9ocentrique
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 c79fbd4..087c651 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
@@ -24,9 +24,11 @@ import java.awt.image.DataBuffer;
 import java.time.Instant;
 import javax.measure.Unit;
 import org.opengis.referencing.operation.Matrix;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.math.Vector;
 import org.apache.sis.math.DecimalFunctions;
-import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.resources.Errors;
 
@@ -42,6 +44,18 @@ import org.apache.sis.util.resources.Errors;
  */
 public abstract class Variable extends NamedElement {
     /**
+     * Names of attributes where to fetch minimum and maximum sample values, in preference
order.
+     *
+     * @see #getValidValues()
+     */
+    private static final String[] RANGE_ATTRIBUTES = {
+        "valid_range",      // Expected "reasonable" range for variable.
+        "actual_range",     // Actual data range for variable.
+        "valid_min",        // Fallback if "valid_range" is not specified.
+        "valid_max"
+    };
+
+    /**
      * The pattern to use for parsing temporal units of the form "days since 1970-01-01 00:00:00".
      *
      * @see #parseUnit(String)
@@ -182,6 +196,8 @@ public abstract class Variable extends NamedElement {
      * Returns the variable data type.
      *
      * @return the variable data type, or {@link DataType#UNKNOWN} if unknown.
+     *
+     * @see #getAttributeType(String)
      */
     public abstract DataType getDataType();
 
@@ -294,12 +310,21 @@ public abstract class Variable extends NamedElement {
      * Returns the names of all attributes associated to this variable.
      *
      * @return names of all attributes associated to this variable.
-     *
-     * @todo Remove this method if it still not used.
      */
     public abstract Collection<String> getAttributeNames();
 
     /**
+     * Returns the numeric type of the attribute of the given name, or {@code null}
+     * if the given attribute is not found or its value is not numeric.
+     *
+     * @param  attributeName  the name of the attribute for which to get the type.
+     * @return type of the given attribute, or {@code null} if none or not numeric.
+     *
+     * @see #getDataType()
+     */
+    public abstract Class<? extends Number> getAttributeType(String attributeName);
+
+    /**
      * Returns the sequence of values for the given attribute, or an empty array if none.
      * The elements will be of class {@link String} if {@code numeric} is {@code false},
      * or {@link Number} if {@code numeric} is {@code true}. Some elements may be null
@@ -366,6 +391,61 @@ public abstract class Variable extends NamedElement {
     }
 
     /**
+     * Returns the range of valid values, or {@code null} if unknown.
+     * The range of values is taken from the following properties, in precedence order:
+     *
+     * <ol>
+     *   <li>{@code "valid_range"}  — expected "reasonable" range for variable.</li>
+     *   <li>{@code "actual_range"} — actual data range for variable.</li>
+     *   <li>{@code "valid_min"}    — ignored if {@code "valid_range"} is present,
as specified in UCAR documentation.</li>
+     *   <li>{@code "valid_max"}    — idem.</li>
+     * </ol>
+     *
+     * @return the range of valid values, or {@code null} if unknown.
+     */
+    public NumberRange<?> getValidValues() {
+        Number minimum = null;
+        Number maximum = null;
+        Class<? extends Number> type = null;
+        for (final String attribute : RANGE_ATTRIBUTES) {
+            for (final Object element : getAttributeValues(attribute, true)) {
+                if (element instanceof Number) {
+                    Number value = (Number) element;
+                    if (element instanceof Float) {
+                        final float fp = (Float) element;
+                        if      (fp == +Float.MAX_VALUE) value = Float.POSITIVE_INFINITY;
+                        else if (fp == -Float.MAX_VALUE) value = Float.NEGATIVE_INFINITY;
+                    } else if (element instanceof Double) {
+                        final double fp = (Double) element;
+                        if      (fp == +Double.MAX_VALUE) value = Double.POSITIVE_INFINITY;
+                        else if (fp == -Double.MAX_VALUE) value = Double.NEGATIVE_INFINITY;
+                    }
+                    type = Numbers.widestClass(type, value.getClass());
+                    minimum = Numbers.cast(minimum, type);
+                    maximum = Numbers.cast(maximum, type);
+                    value   = Numbers.cast(value,   type);
+                    if (!attribute.endsWith("max") && (minimum == null || compare(value,
minimum) < 0)) minimum = value;
+                    if (!attribute.endsWith("min") && (maximum == null || compare(value,
maximum) > 0)) maximum = value;
+                }
+            }
+            if (minimum != null && maximum != null) {
+                @SuppressWarnings({"unchecked", "rawtypes"})
+                final NumberRange<?> range = new NumberRange(type, minimum, true, maximum,
true);
+                return range;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Compares two numbers which shall be of the same class.
+     */
+    @SuppressWarnings("unchecked")
+    private static int compare(final Number n1, final Number n2) {
+        return ((Comparable) n1).compareTo((Comparable) n2);
+    }
+
+    /**
      * Whether {@link #read()} invoked {@link Vector#compress(double)} on the returned vector.
      * This information is used for avoiding to do twice some potentially costly operations.
      *
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 ba20c10..95394ae 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
@@ -196,14 +196,14 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo>
{
                  final WarningListeners<?>   listeners) throws DataStoreContentException
     {
         super(listeners);
-        final Object isUnsigned = attributes.get(CDM.UNSIGNED);
-        if (isUnsigned != null) {
-            dataType = dataType.unsigned(booleanValue(isUnsigned));
-        }
         this.name       = name;
         this.dimensions = dimensions;
         this.attributes = attributes;
-        this.dataType   = dataType;
+        final Object isUnsigned = getAttributeValue(CDM.UNSIGNED, "_unsigned");
+        if (isUnsigned != null) {
+            dataType = dataType.unsigned(booleanValue(isUnsigned));
+        }
+        this.dataType = dataType;
         /*
          * The 'size' value is provided in the netCDF files, but doesn't need to be stored
since it
          * is redundant with the dimension lengths and is not large enough for big variables
anyway.
@@ -515,6 +515,25 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo>
{
     }
 
     /**
+     * Returns the numeric type of the attribute of the given name, or {@code null}
+     * if the given attribute is not found or its value is not numeric.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public Class<? extends Number> getAttributeType(final String attributeName) {
+        final Object value = getAttributeValue(attributeName);
+        if (value != null) {
+            Class<?> type = value.getClass();
+            final Class<?> c = type.getComponentType();
+            if (c != null) type = c;
+            if (Number.class.isAssignableFrom(type)) {
+                return (Class<? extends Number>) type;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the value of the given attribute, or {@code null} if none.
      * This method should be invoked only for hard-coded names that mix lower-case and upper-case
letters.
      *
@@ -523,7 +542,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo>
{
      * @return variable attribute value of the given name, or {@code null} if none.
      */
     private Object getAttributeValue(final String attributeName, final String lowerCase)
{
-        Object value = attributes.get(attributeName);
+        Object value = getAttributeValue(attributeName);
         if (value == null) {
             value = attributes.get(lowerCase);
         }
@@ -591,19 +610,35 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo>
{
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     static Number[] numberValues(final Object value) {
         if (value != null) {
+            if (value instanceof Number) {
+                return new Number[] {(Number) value};
+            }
             if (value.getClass().isArray()) {
                 final Number[] values = new Number[Array.getLength(value)];
                 for (int i=0; i<values.length; i++) {
                     final Object element = Array.get(value, i);
+                    final Number n;
                     if (element instanceof Number) {
-                        values[i] = (Number) element;
+                        n = (Number) element;
+                    } else if (element instanceof String) {
+                        final String t = (String) element;
+                        try {
+                            if (t.indexOf('.') >= 0) {
+                                n = Double.valueOf(t);
+                            } else {
+                                n = Long.valueOf(t);
+                            }
+                        } catch (NumberFormatException e) {
+                            // TODO: log warning. See also Decoder.parseNumber(String).
+                            continue;
+                        }
+                    } else {
+                        continue;
                     }
+                    values[i] = n;
                 }
                 return values;
             }
-            if (value instanceof Number) {
-                return new Number[] {(Number) value};
-            }
         }
         return EMPTY;
     }
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 16e8119..799aea7 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
@@ -30,6 +30,8 @@ import ucar.nc2.VariableIF;
 import ucar.nc2.dataset.Enhancements;
 import ucar.nc2.dataset.VariableEnhanced;
 import ucar.nc2.dataset.CoordinateAxis1D;
+import ucar.nc2.dataset.CoordinateSystem;
+import ucar.nc2.dataset.EnhanceScaleMissing;
 import ucar.nc2.units.SimpleUnit;
 import ucar.nc2.units.DateUnit;
 import org.opengis.referencing.operation.Matrix;
@@ -41,8 +43,8 @@ import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
-import ucar.nc2.dataset.CoordinateSystem;
 
 
 /**
@@ -167,6 +169,8 @@ final class VariableWrapper extends Variable {
     /**
      * Returns the variable data type.
      * This method may return {@code UNKNOWN} if the datatype is unknown.
+     *
+     * @see #getAttributeType(String)
      */
     @Override
     public DataType getDataType() {
@@ -259,6 +263,28 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Returns the numeric type of the attribute of the given name, or {@code null}
+     * if the given attribute is not found or its value is not numeric.
+     *
+     * @see #getDataType()
+     */
+    @Override
+    public Class<? extends Number> getAttributeType(final String attributeName) {
+        final Attribute attribute = raw.findAttributeIgnoreCase(attributeName);
+        if (attribute != null) {
+            switch (attribute.getDataType()) {
+                case BYTE:   return Byte.class;
+                case SHORT:  return Short.class;
+                case INT:    return Integer.class;
+                case LONG:   return Long.class;
+                case FLOAT:  return Float.class;
+                case DOUBLE: return Double.class;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the sequence of values for the given attribute, or an empty array if none.
      * The elements will be of class {@link String} if {@code numeric} is {@code false},
      * or {@link Number} if {@code numeric} is {@code true}.
@@ -304,6 +330,22 @@ final class VariableWrapper extends Variable {
     }
 
     /**
+     * Returns the minimum and maximum values as determined by the UCAR library.
+     * If that library has not seen valid range, then fallbacks on Apache SIS.
+     */
+    @Override
+    public NumberRange<?> getValidValues() {
+        if (variable instanceof EnhanceScaleMissing) {
+            final EnhanceScaleMissing ev = (EnhanceScaleMissing) variable;
+            if (ev.hasInvalidData()) {
+                return NumberRange.create(ev.getValidMin(), true, ev.getValidMax(), true);
+            }
+        }
+        return super.getValidValues();
+
+    }
+
+    /**
      * Whether {@link #read()} invokes {@link Vector#compress(double)} on the returned vector.
      *
      * @return {@code false}.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
index 6eee206..1cc0b41 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
@@ -22,6 +22,8 @@ import java.util.Collections;
 import java.io.IOException;
 import java.nio.file.Path;
 import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.referencing.operation.MathTransform1D;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.netcdf.Decoder;
@@ -34,6 +36,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.resources.Vocabulary;
 import ucar.nc2.constants.CDM;                      // We use only String constants.
 
 
@@ -49,13 +52,13 @@ import ucar.nc2.constants.CDM;                      // We use only String
consta
  */
 final class GridResource extends AbstractGridResource implements ResourceOnFileSystem {
     /**
-     * Names of attributes where to fetch minimum and maximum sample values, in preference
order.
+     * Names of attributes where to fetch missing values, in preference order.
+     * The union of all "no data" values will be stored, but the category name
+     * will be inferred from the first attribute declaring the "no data" value.
      */
-    private static final String[] RANGE_ATTRIBUTES = {
-        "valid_range",      // Expected "reasonable" range for variable.
-        "actual_range",     // Actual data range for variable.
-        "valid_min",        // Fallback if "valid_range" is not specified.
-        "valid_max"
+    private static final String[] NODATA_ATTRIBUTES = {
+        CDM.MISSING_VALUE,
+        CDM.FILL_VALUE
     };
 
     /**
@@ -144,58 +147,78 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
     @Override
     public List<SampleDimension> getSampleDimensions() {
         if (definition == null) {
-            /*
-             * Gets minimum and maximum. If a "valid_range" attribute is present, it has
precedence
-             * over "valid_min" and "valid_max" as specified in the UCAR documentation.
-             */
-            Number minimum = null;
-            Number maximum = null;
-            Class<? extends Number> type = null;
-            for (final String attribute : RANGE_ATTRIBUTES) {
-                for (final Object element : data.getAttributeValues(attribute, true)) {
-                    if (element instanceof Number) {
-                        Number value = (Number) element;
-                        if (element instanceof Float) {
-                            final float fp = (Float) element;
-                            if      (fp == +Float.MAX_VALUE) value = Float.POSITIVE_INFINITY;
-                            else if (fp == -Float.MAX_VALUE) value = Float.NEGATIVE_INFINITY;
-                        } else if (element instanceof Double) {
-                            final double fp = (Double) element;
-                            if      (fp == +Double.MAX_VALUE) value = Double.POSITIVE_INFINITY;
-                            else if (fp == -Double.MAX_VALUE) value = Double.NEGATIVE_INFINITY;
+            final SampleDimension.Builder builder = new SampleDimension.Builder();
+            NumberRange<?> range = data.getValidValues();
+            if (range != null) {
+                /*
+                 * If scale_factor and/or add_offset variable attributes are present, then
this is
+                 * a "packed" variable. Otherwise the transfer function is the identity transform.
+                 */
+                final TransferFunction tr = new TransferFunction();
+                final double scale  = data.getAttributeAsNumber(CDM.SCALE_FACTOR);
+                final double offset = data.getAttributeAsNumber(CDM.ADD_OFFSET);
+                if (!Double.isNaN(scale))  tr.setScale (scale);
+                if (!Double.isNaN(offset)) tr.setOffset(offset);
+                final MathTransform1D mt = tr.getTransform();
+                if (!mt.isIdentity()) {
+                    /*
+                     * Heuristic rule defined in UCAR documentation (see EnhanceScaleMissing
interface):
+                     * if the type of the range is equals to the type of the scale, and the
type of the
+                     * data is not wider, then assume that the minimum and maximum are real
values.
+                     */
+                    final int dataType  = data.getDataType().number;
+                    final int rangeType = Numbers.getEnumConstant(range.getElementType());
+                    if (rangeType >= dataType &&
+                        rangeType >= Math.max(Numbers.getEnumConstant(data.getAttributeType(CDM.SCALE_FACTOR)),
+                                              Numbers.getEnumConstant(data.getAttributeType(CDM.ADD_OFFSET))))
+                    {
+                        final boolean isMinIncluded = range.isMinIncluded();
+                        final boolean isMaxIncluded = range.isMaxIncluded();
+                        double minimum = (range.getMinDouble() - offset) / scale;
+                        double maximum = (range.getMaxDouble() - offset) / scale;
+                        if (maximum > minimum) {
+                            final double swap = maximum;
+                            maximum = minimum;
+                            minimum = swap;
+                        }
+                        if (dataType < Numbers.FLOAT && minimum >= Long.MIN_VALUE
&& maximum <= Long.MAX_VALUE) {
+                            range = NumberRange.create(Math.round(minimum), isMinIncluded,
Math.round(maximum), isMaxIncluded);
+                        } else {
+                            range = NumberRange.create(minimum, isMinIncluded, maximum, isMaxIncluded);
                         }
-                        type = Numbers.widestClass(type, value.getClass());
-                        minimum = Numbers.cast(minimum, type);
-                        maximum = Numbers.cast(maximum, type);
-                        value   = Numbers.cast(value,   type);
-                        if (minimum == null || compare(value, minimum) < 0) minimum =
value;
-                        if (maximum == null || compare(value, maximum) > 0) maximum =
value;
                     }
                 }
-                if (minimum != null && maximum != null) break;
+                builder.addQuantitative(data.getName(), range, mt, data.getUnit());
             }
-            @SuppressWarnings({"unchecked", "rawtypes"})
-            final NumberRange<?> range = new NumberRange(type, minimum, true, maximum,
true);
             /*
-             * Conversion from sample values to real values. If no scale factor and offset
are specified,
-             * then the default will be the identity transform.
+             * Adds the "no data" or "fill value" as qualitative categories.
              */
-            final TransferFunction tr = new TransferFunction();
-            double scale  = data.getAttributeAsNumber(CDM.SCALE_FACTOR);
-            double offset = data.getAttributeAsNumber(CDM.ADD_OFFSET);
-            if (!Double.isNaN(scale))  tr.setScale (scale);
-            if (!Double.isNaN(offset)) tr.setOffset(offset);
-            definition = new SampleDimension.Builder()
-                    .addQuantitative(data.getName(), range, tr.getTransform(), data.getUnit()).build();
+            for (final String attribute : NODATA_ATTRIBUTES) {
+                InternationalString name = null;
+                for (final Object value : data.getAttributeValues(attribute, true)) {
+                    if (value instanceof Number) {
+                        final Number n = (Number) value;
+                        final double fp = n.doubleValue();
+                        if (!builder.intersect(fp, fp)) {
+                            if (name == null) {
+                                short key = Vocabulary.Keys.Nodata;
+                                if (CDM.FILL_VALUE.equalsIgnoreCase(attribute)) {
+                                    key = Vocabulary.Keys.FillValue;
+                                }
+                                name = Vocabulary.formatInternational(key);
+                            }
+                            @SuppressWarnings({"unchecked", "rawtypes"})
+                            NumberRange<?> r = new NumberRange(value.getClass(), n,
true, n, true);
+                            builder.addQualitative(name, r);
+                        }
+                    }
+                }
+            }
+            definition = builder.build();
         }
         return Collections.singletonList(definition);
     }
 
-    @SuppressWarnings("unchecked")
-    private static int compare(final Number n1, final Number n2) {
-        return ((Comparable) n1).compareTo((Comparable) n2);
-    }
-
     /**
      * Gets the paths to files used by this resource, or an empty array if unknown.
      */


Mime
View raw message