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 2018/12/26 18:36:19 UTC
[sis] branch geoapi-4.0 updated: Replace fill/missing values by NaN
in netCDF files when there is no offset and scale factor. The intent is to
make easier to build a SampleDimension for such data.
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 3ebe785 Replace fill/missing values by NaN in netCDF files when there is no offset and scale factor. The intent is to make easier to build a SampleDimension for such data.
3ebe785 is described below
commit 3ebe7855c1da8ae43e62726953f103233cafea81
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Dec 26 19:35:22 2018 +0100
Replace fill/missing values by NaN in netCDF files when there is no offset and scale factor.
The intent is to make easier to build a SampleDimension for such data.
---
.../java/org/apache/sis/coverage/Category.java | 84 +++++---
.../org/apache/sis/coverage/ConvertedCategory.java | 16 --
.../org/apache/sis/coverage/SampleDimension.java | 41 +++-
.../java/org/apache/sis/coverage/CategoryTest.java | 21 ++
.../sis/geometry/AbstractDirectPosition.java | 8 +-
.../org/apache/sis/geometry/AbstractEnvelope.java | 6 +-
.../org/apache/sis/geometry/ArrayEnvelope.java | 3 +-
.../org/apache/sis/geometry/DirectPosition2D.java | 4 +-
.../apache/sis/geometry/GeneralDirectPosition.java | 4 +-
.../operation/transform/MathTransformTestCase.java | 4 +-
.../transform/PassThroughTransformTest.java | 3 +-
.../org/apache/sis/internal/util/Numerics.java | 91 --------
.../main/java/org/apache/sis/math/ArrayVector.java | 3 +-
.../main/java/org/apache/sis/util/ArraysExt.java | 231 ++++++++++++++++++---
.../org/apache/sis/internal/util/NumericsTest.java | 22 --
.../java/org/apache/sis/util/ArraysExtTest.java | 24 ++-
.../org/apache/sis/internal/netcdf/Variable.java | 83 +++++++-
.../sis/internal/netcdf/impl/VariableInfo.java | 10 +-
.../sis/internal/netcdf/ucar/VariableWrapper.java | 17 +-
.../apache/sis/storage/netcdf/GridResource.java | 42 ++--
20 files changed, 473 insertions(+), 244 deletions(-)
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
index f7c16a5..1f0a7e3 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
@@ -133,6 +133,7 @@ public class Category implements Serializable {
* The range is null if this category is a qualitative category converted to real values.
* Those categories are characterized by two apparently contradictory properties,
* and are implemented using {@link Float#NaN} values:
+ *
* <ul>
* <li>This category is member of a {@code SampleDimension} having an identity
* {@linkplain SampleDimension#getTransferFunction() transfer function}.</li>
@@ -192,13 +193,14 @@ public class Category implements Serializable {
*
* @param name the category name (mandatory).
* @param samples the minimum and maximum sample values (mandatory).
- * @param toUnits the conversion from sample values to real values,
- * or {@code null} for constructing a qualitative category.
+ * @param toUnits the conversion from sample values to real values (possibly identity), or {@code null}
+ * for constructing a qualitative category. Mandatory if {@code units} is non-null.
* @param units the units of measurement, or {@code null} if not applicable.
* This is the target units after conversion by {@code toUnits}.
* @param toNaN mapping from sample values to ordinal values to be supplied to {@link MathFunctions#toNanFloat(int)}.
- * That mapping is used only if {@code toUnits} is {@code null}. That mapping is responsible to ensure that
- * there is no ordinal value collision between different categories in the same {@link SampleDimension}.
+ * That mapping is used only if {@code toUnits} is {@code null} and {@code samples} are not NaN values.
+ * That mapping is responsible to ensure that there is no ordinal value collision between different categories
+ * in the same {@link SampleDimension}.
* The input is a real number in the {@code samples} range and the output shall be a unique value between
* {@value MathFunctions#MIN_NAN_ORDINAL} and {@value MathFunctions#MAX_NAN_ORDINAL} inclusive.
*/
@@ -207,21 +209,33 @@ public class Category implements Serializable {
{
ArgumentChecks.ensureNonEmpty("name", name);
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.
+ }
this.name = Types.toInternationalString(name);
this.minimum = samples.getMinDouble(true);
this.maximum = samples.getMaxDouble(true);
+ final boolean isNaN = Double.isNaN(minimum);
/*
- * Following arguments check uses '!' in comparison in order to reject NaN values.
+ * 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) || (minimum == Double.NEGATIVE_INFINITY) || (maximum == Double.POSITIVE_INFINITY)) {
- throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalCategoryRange_2, name, samples));
+ if (!(minimum <= maximum)) {
+ if (toUnits != null || !isNaN || Double.doubleToRawLongBits(minimum) != Double.doubleToRawLongBits(maximum)) {
+ throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalCategoryRange_2, name, samples));
+ }
}
- /*
- * Creates the transform doing the inverse conversion (from real values to sample values).
- * This transform is assigned to a new Category object with its own minimum and maximum values.
- * Those minimum and maximum may be NaN if this category is a qualitative category.
- */
- try {
+ if (isNaN) {
+ range = null;
+ converse = this;
+ toConverse = identity();
+ } else try {
+ /*
+ * Creates the transform doing the inverse conversion (from real values to sample values).
+ * This transform is assigned to a new Category object with its own minimum and maximum values.
+ * Those minimum and maximum may be NaN if this category is a qualitative category.
+ */
final MathTransform1D toSamples;
if (toUnits != null) {
toConverse = toUnits;
@@ -235,15 +249,15 @@ public class Category implements Serializable {
}
toSamples = toUnits.inverse();
} else {
- ArgumentChecks.ensureNonNull("toNaN", toNaN);
- final int ordinal = toNaN.applyAsInt(minimum);
/*
* For qualitative category, the transfer function maps to NaN while the inverse function maps back
* to some value in the [minimum … maximum] range. We chose the value closest to positive zero.
*/
- toConverse = (MathTransform1D) MathTransforms.linear(0, MathFunctions.toNanFloat(ordinal));
+ ArgumentChecks.ensureNonNull("toNaN", toNaN);
final double value = (minimum > 0) ? minimum : (maximum <= 0) ? maximum : 0d;
- toSamples = (MathTransform1D) MathTransforms.linear(0, value);
+ final float nan = MathFunctions.toNanFloat(toNaN.applyAsInt(value));
+ toConverse = (MathTransform1D) MathTransforms.linear(0, nan);
+ toSamples = (MathTransform1D) MathTransforms.linear(0, value);
}
range = samples;
converse = new ConvertedCategory(this, toSamples, toUnits != null, units);
@@ -347,10 +361,35 @@ public class Category implements Serializable {
*
* @see SampleDimension#getSampleRange()
*/
+ @SuppressWarnings({"unchecked", "rawtypes"})
public NumberRange<?> getSampleRange() {
- // Same assumption than in 'isQuantitative()'.
- assert range != null : this;
- return range;
+ if (range != null) {
+ return range;
+ }
+ /*
+ * The range can be null only if the minimum and maximum are NaN. This may be the case if NaN were
+ * given explicitly to the constructor or if this category is an instance of ConvertedCategory for
+ * qualitative category. In the later case, the NaN are the result of converting the sample values.
+ * We favor the Float type because values should be NaN produced by MathFunctions.toNanFloat(int).
+ * The minimum and maximum are usually the same value, but not necessarily.
+ */
+ final float min = (float) minimum;
+ final float max = (float) maximum;
+ final Number v1, v2;
+ final Class<?> type;
+ if (Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(min) &&
+ Double.doubleToRawLongBits(maximum) == Double.doubleToRawLongBits(max))
+ {
+ v1 = min;
+ v2 = (Float.floatToRawIntBits(min) == Float.floatToRawIntBits(max)) ? v1 : max;
+ type = Float.class;
+ } else {
+ v1 = minimum;
+ v2 = (Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(maximum)) ? v1 : maximum;
+ type = Double.class;
+ }
+ return new NumberRange(type, v1, true, v2, true);
+ // Do not use NumberRange.create(float, …) because it rejects NaN values.
}
/**
@@ -391,12 +430,9 @@ public class Category implements Serializable {
*/
public Optional<MathTransform1D> getTransferFunction() {
/*
- * This implementation assumes that this method will always be invoked on the instance
- * created for sample values, never on the instance created by the private constructor.
- * If this method was invoked on "real values category", then we would need to return
+ * Note: if this method is invoked on "real values category", then we need to return
* the identity transform instead than 'toConverse'. This is done by ConvertedCategory.
*/
- assert range != null : this;
return (converse.range != null) ? Optional.of(toConverse) : Optional.empty();
}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
index f73cec7..00c9007 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
@@ -20,7 +20,6 @@ import java.util.Optional;
import javax.measure.Unit;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.measure.NumberRange;
/**
@@ -65,21 +64,6 @@ final class ConvertedCategory extends Category {
}
/**
- * Returns the range of value, which is the same as {@link #getMeasurementRange()} unless the values are NaN.
- */
- @Override
- public NumberRange<?> getSampleRange() {
- if (range != null) {
- return range;
- }
- Float min = (float) minimum; // Should be NaN produced by MathFunctions.toNanFloat(int).
- Float max = (float) maximum;
- if (max.equals(min)) max = min;
- // Do not use NumberRange.create(float, …) because it rejects NaN values.
- return new NumberRange<>(Float.class, min, true, max, true);
- }
-
- /**
* Returns the <cite>transfer function</cite> from sample values to real values in units of measurement.
* The function is absent if this category is not a {@linkplain #isQuantitative() quantitative} category.
*/
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 fce8b7b..834585d 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
@@ -586,16 +586,39 @@ public class SampleDimension implements Serializable {
* Creates a range for the given minimum and maximum values. We use the static factory methods instead
* than the {@link NumberRange} constructor for sharing existing range instances. This is also a way
* to ensure that the number type is one of the primitive wrappers.
+ *
+ * <p>This method is invoked for qualitative categories only. For that reason, it accepts NaN values.</p>
*/
- private static NumberRange<?> range(final Class<?> type, final Number minimum, final Number maximum) {
+ private static NumberRange<?> range(final Class<?> type, Number minimum, Number maximum) {
switch (Numbers.getEnumConstant(type)) {
- case Numbers.BYTE: return NumberRange.create(minimum.byteValue(), true, maximum.byteValue(), true);
- case Numbers.SHORT: return NumberRange.create(minimum.shortValue(), true, maximum.shortValue(), true);
- case Numbers.INTEGER: return NumberRange.create(minimum.intValue(), true, maximum.intValue(), true);
- case Numbers.LONG: return NumberRange.create(minimum.longValue(), true, maximum.longValue(), true);
- case Numbers.FLOAT: return NumberRange.create(minimum.floatValue(), true, maximum.floatValue(), true);
- default: return NumberRange.create(minimum.doubleValue(), true, maximum.doubleValue(), true);
+ case Numbers.BYTE: return NumberRange.create(minimum.byteValue(), true, maximum.byteValue(), true);
+ case Numbers.SHORT: return NumberRange.create(minimum.shortValue(), true, maximum.shortValue(), true);
+ case Numbers.INTEGER: return NumberRange.create(minimum.intValue(), true, maximum.intValue(), true);
+ case Numbers.LONG: return NumberRange.create(minimum.longValue(), true, maximum.longValue(), true);
+ case Numbers.FLOAT: {
+ final float min = minimum.floatValue();
+ final float max = maximum.floatValue();
+ if (!Float.isNaN(min) || !Float.isNaN(max)) { // Let 'create' throws an exception if only one value is NaN.
+ return NumberRange.create(min, true, max, true);
+ }
+ if (minimum.getClass() != Float.class) minimum = min;
+ if (maximum.getClass() != Float.class) maximum = max;
+ break;
+ }
+ default: {
+ final double min = minimum.doubleValue();
+ final double max = maximum.doubleValue();
+ if (!Double.isNaN(min) || !Double.isNaN(max)) { // Let 'create' throws an exception if only one value is NaN.
+ return NumberRange.create(min, true, max, true);
+ }
+ if (minimum.getClass() != Double.class) minimum = min;
+ if (maximum.getClass() != Double.class) maximum = max;
+ break;
+ }
}
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ final NumberRange<?> samples = new NumberRange(type, minimum, true, maximum, true);
+ return samples;
}
/**
@@ -606,10 +629,10 @@ public class SampleDimension implements Serializable {
*
* @param name the category name as a {@link String} or {@link InternationalString} object,
* or {@code null} for a default "fill value" name.
- * @param sample the background value. Can not be NaN.
+ * @param sample the background value.
* @return {@code this}, for method call chaining.
*/
- public Builder setBackground(CharSequence name, final Number sample) {
+ public Builder setBackground(CharSequence name, Number sample) {
ArgumentChecks.ensureNonNull("sample", sample);
if (name == null) {
name = Vocabulary.formatInternational(Vocabulary.Keys.FillValue);
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
index df2db15..11b659b 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
@@ -218,4 +218,25 @@ public final strictfp class CategoryTest extends TestCase {
assertTrue ("isQuantitative", category.isQuantitative());
}
}
+
+ /**
+ * Tests a category with a NaN value.
+ */
+ @Test
+ public void testCategoryNaN() {
+ final Category category = new Category("NaN", new NumberRange<>(Float.class, Float.NaN, true, Float.NaN, true), null, null, null);
+ final NumberRange<?> range = category.getSampleRange();
+ assertSame ("converse", category, category.converse);
+ assertEquals("name", "NaN", String.valueOf(category.name));
+ assertEquals("name", "NaN", String.valueOf(category.getName()));
+ assertEquals("minimum", Double.NaN, category.minimum, STRICT);
+ assertEquals("maximum", Double.NaN, category.maximum, STRICT);
+ assertNull ("sampleRange", category.range);
+ assertEquals("range.minValue", Float.NaN, range.getMinValue());
+ assertEquals("range.maxValue", Float.NaN, range.getMaxValue());
+ assertFalse ("measurementRange", category.getMeasurementRange().isPresent());
+ assertFalse ("transferFunction", category.getTransferFunction().isPresent());
+ assertTrue ("toConverse.isIdentity", category.toConverse.isIdentity());
+ assertFalse ("isQuantitative", category.isQuantitative());
+ }
}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java
index 2136a8e..3573944 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractDirectPosition.java
@@ -252,12 +252,12 @@ public abstract class AbstractDirectPosition extends FormattableObject implement
* (WKT) format.
*
* @param position the position to format.
- * @param isSimplePrecision {@code true} if every ordinate values can be casted to {@code float}.
+ * @param isSinglePrecision {@code true} if every ordinate values can be casted to {@code float}.
* @return the point as a {@code POINT} in WKT format.
*
- * @see Numerics#isSimplePrecision(double[])
+ * @see Numerics#isSinglePrecision(double[])
*/
- static String toString(final DirectPosition position, final boolean isSimplePrecision) {
+ static String toString(final DirectPosition position, final boolean isSinglePrecision) {
final StringBuilder buffer = new StringBuilder(32).append("POINT");
final int dimension = position.getDimension();
if (dimension == 0) {
@@ -267,7 +267,7 @@ public abstract class AbstractDirectPosition extends FormattableObject implement
for (int i=0; i<dimension; i++) {
buffer.append(separator);
final double ordinate = position.getOrdinate(i);
- if (isSimplePrecision) {
+ if (isSinglePrecision) {
buffer.append((float) ordinate);
} else {
buffer.append(ordinate);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java
index abdcf54..d34990e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/AbstractEnvelope.java
@@ -1152,14 +1152,14 @@ public abstract class AbstractEnvelope extends FormattableObject implements Enve
* methods for formatting a {@code BOX} element from an envelope.
*
* @param envelope the envelope to format.
- * @param isSimplePrecision {@code true} if every lower and upper corner values can be casted to {@code float}.
+ * @param isSinglePrecision {@code true} if every lower and upper corner values can be casted to {@code float}.
* @return this envelope as a {@code BOX} or {@code BOX3D} (most typical dimensions) element.
*
* @see GeneralEnvelope#GeneralEnvelope(CharSequence)
* @see CoordinateFormat
* @see org.apache.sis.io.wkt
*/
- static String toString(final Envelope envelope, final boolean isSimplePrecision) {
+ static String toString(final Envelope envelope, final boolean isSinglePrecision) {
final int dimension = envelope.getDimension();
final StringBuilder buffer = new StringBuilder(64).append("BOX");
if (dimension != 2) {
@@ -1175,7 +1175,7 @@ public abstract class AbstractEnvelope extends FormattableObject implements Enve
for (int i=0; i<dimension; i++) {
buffer.append(i == 0 && !isUpper ? '(' : ' ');
final double ordinate = (isUpper ? upperCorner : lowerCorner).getOrdinate(i);
- if (isSimplePrecision) {
+ if (isSinglePrecision) {
buffer.append((float) ordinate);
} else {
buffer.append(ordinate);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/ArrayEnvelope.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/ArrayEnvelope.java
index e5beb1e..14bbab6 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/ArrayEnvelope.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/ArrayEnvelope.java
@@ -37,7 +37,6 @@ import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
-import org.apache.sis.internal.util.Numerics;
import static org.apache.sis.util.ArgumentChecks.*;
import static org.apache.sis.math.MathFunctions.isNegative;
@@ -594,6 +593,6 @@ scanNumber: while ((i += Character.charCount(c)) < length) {
*/
@Override
public String toString() {
- return toString(this, Numerics.isSimplePrecision(ordinates));
+ return toString(this, ArraysExt.isSinglePrecision(ordinates));
}
}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/DirectPosition2D.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/DirectPosition2D.java
index c3ed407..8152d6e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/DirectPosition2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/DirectPosition2D.java
@@ -22,8 +22,8 @@ import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.AxisDirection;
-import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArraysExt;
import static java.lang.Double.doubleToLongBits;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
@@ -292,7 +292,7 @@ public class DirectPosition2D extends Point2D.Double implements DirectPosition,
*/
@Override
public String toString() {
- return AbstractDirectPosition.toString(this, Numerics.isSimplePrecision(x, y));
+ return AbstractDirectPosition.toString(this, ArraysExt.isSinglePrecision(x, y));
}
/**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralDirectPosition.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralDirectPosition.java
index 2d85188..4a81c2f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralDirectPosition.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralDirectPosition.java
@@ -30,8 +30,8 @@ import java.security.PrivilegedAction;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArraysExt;
import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
@@ -283,7 +283,7 @@ public class GeneralDirectPosition extends AbstractDirectPosition implements Ser
*/
@Override
public String toString() {
- return toString(this, Numerics.isSimplePrecision(ordinates));
+ return toString(this, ArraysExt.isSinglePrecision(ordinates));
}
/**
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
index ed6cc93..dc2be7b 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
@@ -32,10 +32,10 @@ import org.apache.sis.parameter.Parameterized;
import org.apache.sis.measure.Longitude;
import org.apache.sis.util.Debug;
import org.apache.sis.util.Classes;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.io.TableAppender;
import org.apache.sis.io.wkt.Convention;
import org.apache.sis.io.wkt.FormattableObject;
-import org.apache.sis.internal.util.Numerics;
import static java.lang.StrictMath.*;
// Test imports
@@ -243,7 +243,7 @@ public abstract strictfp class MathTransformTestCase extends TransformTestCase {
* But in Apache SIS, we want to verify consistency for all math transforms. A previous version had
* a bug with the Google projection which was unnoticed because of lack of this consistency check.
*/
- final float[] asFloats = Numerics.copyAsFloats(coordinates);
+ final float[] asFloats = ArraysExt.copyAsFloats(coordinates);
final float[] result = verifyConsistency(asFloats);
for (int i=0; i<coordinates.length; i++) {
assertEquals("Detected change in source coordinates.", (float) coordinates[i], asFloats[i], 0f); // Paranoiac check.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
index 66ceb84..2b6398b 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
@@ -26,7 +26,6 @@ import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.Matrix3;
-import org.apache.sis.internal.util.Numerics;
import org.apache.sis.util.ArraysExt;
// Test imports
@@ -241,7 +240,7 @@ public final strictfp class PassThroughTransformTest extends MathTransformTestCa
/*
* Verify the consistency between different 'transform(…)' methods.
*/
- final float[] sourceAsFloat = Numerics.copyAsFloats(passthroughData);
+ final float[] sourceAsFloat = ArraysExt.copyAsFloats(passthroughData);
final float[] targetAsFloat = verifyConsistency(sourceAsFloat);
assertEquals("Unexpected length of transformed array.", expectedData.length, targetAsFloat.length);
/*
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
index 757e519..8090ef6 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
@@ -20,7 +20,6 @@ import java.util.Map;
import java.util.HashMap;
import org.apache.sis.util.Debug;
import org.apache.sis.util.Static;
-import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.math.DecimalFunctions;
import org.opengis.referencing.operation.Matrix; // For javadoc
@@ -208,96 +207,6 @@ public final class Numerics extends Static {
}
/**
- * Returns {@code true} if every values in the given {@code double} array could be casted to the
- * {@code float} type without precision lost. This method treats all {@code NaN} values as equal.
- *
- * @param values the value to test for their precision.
- * @return {@code true} if every values can be casted to the {@code float} type without precision lost.
- */
- public static boolean isSimplePrecision(final double... values) {
- for (final double value : values) {
- if (Double.doubleToLongBits(value) != Double.doubleToLongBits((float) value)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Returns a copy of the given array where each value has been casted to the {@code float} type,
- * but only if this cast is lossless. If any cast causes data loss, then this method returns {@code null}.
- *
- * @param data the array to copy.
- * @return a copy of the given array with values casted to the {@code float} type,
- * or {@code null} if the cast would cause data lost.
- */
- public static float[] copyAsFloatsIfLossless(final double[] data) {
- /*
- * Before to allocate a new array, performs a quick sampling of a few values.
- * Basically the first value, the last value, a value in the middle and a few others.
- */
- int i = data.length - 1;
- if (i < 0) {
- return ArraysExt.EMPTY_FLOAT;
- }
- for (;;) {
- final double d = data[i];
- if (Double.doubleToRawLongBits(d) != Double.doubleToRawLongBits((float) d)) {
- return null;
- }
- if (i == 0) break;
- i >>>= 1;
- }
- /*
- * At this point the quick sampling found no data loss. We can now allocate the array,
- * but we will still need to check for each value, which may interrupt the copy at any time.
- */
- final float[] result = new float[data.length];
- for (i = data.length; --i >= 0;) {
- final double d = data[i];
- final float f = (float) d;
- if (Double.doubleToRawLongBits(d) != Double.doubleToRawLongBits(f)) {
- return null;
- }
- result[i] = f;
- }
- return result;
- }
-
- /**
- * Returns a copy of the given array where each value has been casted to the {@code float} type.
- *
- * @param data the array to copy, or {@code null}.
- * @return a copy of the given array with values casted to the {@code float} type,
- * or {@code null} if the given array was null.
- */
- public static float[] copyAsFloats(final double[] data) {
- if (data == null) return null;
- final float[] result = new float[data.length];
- for (int i=0; i<data.length; i++) {
- result[i] = (float) data[i];
- }
- return result;
- }
-
- /**
- * Returns a copy of the given array where each value has been
- * {@linkplain Math#round(double) rounded} to the {@code int} type.
- *
- * @param data the array to copy, or {@code null}.
- * @return a copy of the given array with values rounded to the {@code int} type,
- * or {@code null} if the given array was null.
- */
- public static int[] copyAsInts(final double[] data) {
- if (data == null) return null;
- final int[] result = new int[data.length];
- for (int i=0; i<data.length; i++) {
- result[i] = Math.toIntExact(Math.round(data[i]));
- }
- return result;
- }
-
- /**
* Returns {@code true} if the given floats are equals. Positive and negative zero are
* considered different, while a NaN value is considered equal to all other NaN values.
*
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java b/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
index ed42e74..1ea8354 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/ArrayVector.java
@@ -28,6 +28,7 @@ import java.util.Arrays;
import java.util.Optional;
import java.util.function.IntSupplier;
import org.apache.sis.util.Numbers;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.internal.util.Numerics;
@@ -300,7 +301,7 @@ abstract class ArrayVector<E extends Number> extends Vector implements CheckedCo
/** Returns a copy of current data as a floating point array. */
@Override public float[] floatValues() {
- return Numerics.copyAsFloats(array);
+ return ArraysExt.copyAsFloats(array);
}
/** Applies hash code contract specified {@link Vector#hashCode()}. */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
index 3d91f73..51c80b0 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArraysExt.java
@@ -1703,6 +1703,203 @@ public final class ArraysExt extends Static {
}
/**
+ * Replaces all occurrences of the given value by the given replacement.
+ * This method compares the values using {@link Double#doubleToRawLongBits(double)}:
+ *
+ * <ul>
+ * <li>Positive zero is considered different then negative zero.</li>
+ * <li>The {@linkplain org.apache.sis.math.MathFunctions#toNanFloat(int) various
+ * possible NaN values} are considered different.</li>
+ * </ul>
+ *
+ * A common usage for this method is to replace pad values by {@link Double#NaN} in the
+ * sample values of a {@linkplain org.apache.sis.coverage.grid.GridCoverage grid coverage}.
+ * This method does nothing if the given array is {@code null} or if {@code search} is the
+ * same bits pattern than {@code replacement}.
+ *
+ * @param array the array where to perform the search and replace, or {@code null}.
+ * @param search the value to search.
+ * @param replacement the replacement.
+ *
+ * @since 1.0
+ */
+ public static void replace(final double[] array, final double search, final double replacement) {
+ if (array != null) {
+ final long bits = Double.doubleToRawLongBits(search);
+ if (bits != Double.doubleToRawLongBits(replacement)) {
+ for (int i=0; i<array.length; i++) {
+ if (Double.doubleToRawLongBits(array[i]) == bits) {
+ array[i] = replacement;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Replaces all occurrences of the given value by the given replacement.
+ * This method compares the values using {@link Float#floatToRawIntBits(float)}:
+ *
+ * <ul>
+ * <li>Positive zero is considered different then negative zero.</li>
+ * <li>The {@linkplain org.apache.sis.math.MathFunctions#toNanFloat(int) various
+ * possible NaN values} are considered different.</li>
+ * </ul>
+ *
+ * A common usage for this method is to replace pad values by {@link Float#NaN} in the
+ * sample values of a {@linkplain org.apache.sis.coverage.grid.GridCoverage grid coverage}.
+ * This method does nothing if the given array is {@code null} or if {@code search} is the
+ * same bits pattern than {@code replacement}.
+ *
+ * @param array the array where to perform the search and replace, or {@code null}.
+ * @param search the value to search.
+ * @param replacement the replacement.
+ *
+ * @since 1.0
+ */
+ public static void replace(final float[] array, final float search, final float replacement) {
+ if (array != null) {
+ final int bits = Float.floatToRawIntBits(search);
+ if (bits != Float.floatToRawIntBits(replacement)) {
+ for (int i=0; i<array.length; i++) {
+ if (Float.floatToRawIntBits(array[i]) == bits) {
+ array[i] = replacement;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a copy of the given array where each value has been casted to the {@code float} type.
+ * This method does not verify if the casts would cause data loss.
+ *
+ * @param data the array to copy, or {@code null}.
+ * @return a copy of the given array with values casted to the {@code float} type,
+ * or {@code null} if the given array was null.
+ *
+ * @since 1.0
+ */
+ public static float[] copyAsFloats(final double[] data) {
+ if (data == null) return null;
+ final float[] result = new float[data.length];
+ for (int i=0; i<data.length; i++) {
+ result[i] = (float) data[i];
+ }
+ return result;
+ }
+
+ /**
+ * Returns a copy of the given array where each value has been casted to the {@code float} type,
+ * but only if all casts are lossless. If any cast causes data loss, then this method returns {@code null}.
+ * This method is equivalent to the following code, but potentially more efficient:
+ *
+ * {@preformat java
+ * if (isSinglePrecision(data)) {
+ * return copyAsFloat(data);
+ * } else {
+ * return null;
+ * }
+ * }
+ *
+ * @param data the array to copy, or {@code null}.
+ * @return a copy of the given array with values casted to the {@code float} type, or
+ * {@code null} if the given array was null or if a cast would cause data lost.
+ *
+ * @since 1.0
+ */
+ public static float[] copyAsFloatsIfLossless(final double[] data) {
+ if (data == null) return null;
+ /*
+ * Before to allocate a new array, performs a quick sampling of a few values.
+ * Basically the first value, the last value, a value in the middle and a few others.
+ */
+ int i = data.length - 1;
+ if (i < 0) {
+ return ArraysExt.EMPTY_FLOAT;
+ }
+ for (;;) {
+ final double d = data[i];
+ if (Double.doubleToRawLongBits(d) != Double.doubleToRawLongBits((float) d)) {
+ return null;
+ }
+ if (i == 0) break;
+ i >>>= 1;
+ }
+ /*
+ * At this point the quick sampling found no data loss. We can now allocate the array,
+ * but we will still need to check for each value, which may interrupt the copy at any time.
+ */
+ final float[] result = new float[data.length];
+ for (i = data.length; --i >= 0;) {
+ final double d = data[i];
+ final float f = (float) d;
+ if (Double.doubleToRawLongBits(d) != Double.doubleToRawLongBits(f)) {
+ return null;
+ }
+ result[i] = f;
+ }
+ return result;
+ }
+
+ /**
+ * Returns {@code true} if every values in the given {@code double} array could be casted to the
+ * {@code float} type without data lost. If this method returns {@code true}, then the array can
+ * be converted to the {@code float[]} type with {@link #copyAsFloats(double[])} and the exact
+ * same {@code double} values can still be obtained by casting back each {@code float} value
+ * to {@code double}.
+ *
+ * @param values the values to test for their precision, or {@code null}.
+ * @return {@code true} if every values can be casted to the {@code float} type without data lost.
+ *
+ * @since 1.0
+ */
+ public static boolean isSinglePrecision(final double... values) {
+ if (values != null) {
+ for (final double value : values) {
+ if (Double.doubleToRawLongBits(value) != Double.doubleToRawLongBits((float) value)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns {@code true} if the specified array contains at least one {@link Double#NaN NaN} value.
+ *
+ * @param array the array to check, or {@code null}.
+ * @return {@code true} if the given array is non-null and contains at least one NaN value.
+ */
+ public static boolean hasNaN(final double[] array) {
+ if (array != null) {
+ for (int i=0; i<array.length; i++) {
+ if (Double.isNaN(array[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if the specified array contains at least one {@link Float#NaN NaN} value.
+ *
+ * @param array the array to check, or {@code null}.
+ * @return {@code true} if the given array is non-null and contains at least one NaN value.
+ */
+ public static boolean hasNaN(final float[] array) {
+ if (array != null) {
+ for (int i=0; i<array.length; i++) {
+ if (Float.isNaN(array[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
* Returns {@code true} if all values in the specified array are equal to the specified value,
* which may be {@code null}.
*
@@ -1780,40 +1977,6 @@ public final class ArraysExt extends Static {
}
/**
- * Returns {@code true} if the specified array contains at least one {@link Double#NaN NaN} value.
- *
- * @param array the array to check, or {@code null}.
- * @return {@code true} if the given array is non-null and contains at least one NaN value.
- */
- public static boolean hasNaN(final double[] array) {
- if (array != null) {
- for (int i=0; i<array.length; i++) {
- if (Double.isNaN(array[i])) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * Returns {@code true} if the specified array contains at least one {@link Float#NaN NaN} value.
- *
- * @param array the array to check, or {@code null}.
- * @return {@code true} if the given array is non-null and contains at least one NaN value.
- */
- public static boolean hasNaN(final float[] array) {
- if (array != null) {
- for (int i=0; i<array.length; i++) {
- if (Float.isNaN(array[i])) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
* Returns {@code true} if the specified array contains the specified value, ignoring case.
* This method should be used only for very small arrays.
*
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
index 6e880b8..a7c7284 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
@@ -84,28 +84,6 @@ public final strictfp class NumericsTest extends TestCase {
}
/**
- * Tests {@link Numerics#copyAsFloatsIfLossless(double[])}.
- */
- @Test
- public void testCopyAsFloatsIfLossless() {
- double[] array = {2, 0.5, 0.25, Double.NaN, Double.POSITIVE_INFINITY};
- float[] result = Numerics.copyAsFloatsIfLossless(array);
- assertNotNull(result);
- assertArrayEquals(new float[] {2f, 0.5f, 0.25f, Float.NaN, Float.POSITIVE_INFINITY}, result, 0f);
- array[3] = 0.3333333333333;
- assertNull(Numerics.copyAsFloatsIfLossless(array));
- }
-
- /**
- * Tests {@link Numerics#isSimplePrecision(double[])}.
- */
- @Test
- public void testIsSimplePrecision() {
- assertTrue (Numerics.isSimplePrecision(2, 0.5, 0.25, Double.NaN, Double.POSITIVE_INFINITY));
- assertFalse(Numerics.isSimplePrecision(2, 0.5, 1.0 / 3));
- }
-
- /**
* Tests {@link Numerics#isUnsignedInteger(String)}.
*/
@Test
diff --git a/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java b/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java
index bece8ac..295eb80 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/util/ArraysExtTest.java
@@ -27,7 +27,7 @@ import static org.junit.Assert.*;
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
- * @version 0.4
+ * @version 1.0
* @since 0.3
* @module
*/
@@ -259,4 +259,26 @@ public final strictfp class ArraysExtTest extends TestCase {
ArraysExt.swap(array, 1, 3);
assertArrayEquals(new char[] {4, 15, 12, 8, 18}, array);
}
+
+ /**
+ * Tests {@link ArraysExt#copyAsFloatsIfLossless(double[])}.
+ */
+ @Test
+ public void testCopyAsFloatsIfLossless() {
+ double[] array = {2, 0.5, 0.25, Double.NaN, Double.POSITIVE_INFINITY};
+ float[] result = ArraysExt.copyAsFloatsIfLossless(array);
+ assertNotNull(result);
+ assertArrayEquals(new float[] {2f, 0.5f, 0.25f, Float.NaN, Float.POSITIVE_INFINITY}, result, 0f);
+ array[3] = 0.3333333333333;
+ assertNull(ArraysExt.copyAsFloatsIfLossless(array));
+ }
+
+ /**
+ * Tests {@link ArraysExt#isSinglePrecision(double[])}.
+ */
+ @Test
+ public void testIsSinglePrecision() {
+ assertTrue (ArraysExt.isSinglePrecision(2, 0.5, 0.25, Double.NaN, Double.POSITIVE_INFINITY));
+ assertFalse(ArraysExt.isSinglePrecision(2, 0.5, 1.0 / 3));
+ }
}
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 2d4b8ff..9914d10 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
@@ -29,10 +29,13 @@ import org.opengis.referencing.operation.Matrix;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.math.Vector;
+import org.apache.sis.math.MathFunctions;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.Numbers;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.collection.WeakHashSet;
+import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.util.logging.WarningListeners;
import org.apache.sis.util.resources.Errors;
import ucar.nc2.constants.CDM; // We use only String constants.
@@ -112,6 +115,12 @@ public abstract class Variable extends NamedElement {
private boolean unitParsed;
/**
+ * All no-data values declared for this variable, or an empty map if none.
+ * This is computed by {@link #getNodataValues()} and cached for efficiency and stability.
+ */
+ private Map<Number,Integer> nodataValues;
+
+ /**
* Where to report warnings, if any.
*/
private final WarningListeners<?> listeners;
@@ -227,6 +236,29 @@ public abstract class Variable extends NamedElement {
}
/**
+ * Returns {@code true} if this variable contains data that are already in the unit of measurement represented by
+ * {@link #getUnit()}, except for the fill/missing values. If {@code true}, then replacing fill/missing values by
+ * {@code NaN} is the only action needed for having converted values.
+ *
+ * <p>This method is for detecting when {@link org.apache.sis.storage.netcdf.GridResource#getSampleDimensions()}
+ * should return sample dimensions for already converted values. But to be consistent with {@code SampleDimension}
+ * contract, it requires fill/missing values to be replaced by NaN. This is done by {@link #replaceNaN(Object)}.</p>
+ *
+ * @return whether this variable contains values in unit of measurement, ignoring fill and missing values.
+ */
+ public final boolean hasRealValues() {
+ final int n = getDataType().number;
+ if (n == Numbers.FLOAT | n == Numbers.DOUBLE) {
+ double c = getAttributeAsNumber(CDM.SCALE_FACTOR);
+ if (Double.isNaN(c) || c == 1) {
+ c = getAttributeAsNumber(CDM.ADD_OFFSET);
+ return Double.isNaN(c) || c == 0;
+ }
+ }
+ return false;
+ }
+
+ /**
* Returns the variable data type.
*
* @return the variable data type, or {@link DataType#UNKNOWN} if unknown.
@@ -508,20 +540,28 @@ public abstract class Variable extends NamedElement {
* <li>If bit 1 is set, then the value is a missing value.</li>
* </ul>
*
+ * Pad values are first in the map, followed by missing values.
* The same value may have more than one role.
+ * The map returned by this method shall be stable, i.e. two invocations of this method shall return the
+ * same entries in the same order. This is necessary for mapping "no data" values to the same NaN values,
+ * since their {@linkplain MathFunctions#toNanFloat(int) ordinal values} are based on order.
*
* @return pad/missing values with bitmask of their role.
*/
+ @SuppressWarnings("ReturnOfCollectionOrArrayField")
public final Map<Number,Integer> getNodataValues() {
- final Map<Number,Integer> pads = new LinkedHashMap<>();
- for (int i=0; i < NODATA_ATTRIBUTES.length; i++) {
- for (final Object value : getAttributeValues(NODATA_ATTRIBUTES[i], true)) {
- if (value instanceof Number) {
- pads.merge((Number) value, 1 << i, (v1, v2) -> v1 | v2);
+ if (nodataValues == null) {
+ final Map<Number,Integer> pads = new LinkedHashMap<>();
+ for (int i=0; i < NODATA_ATTRIBUTES.length; i++) {
+ for (final Object value : getAttributeValues(NODATA_ATTRIBUTES[i], true)) {
+ if (value instanceof Number) {
+ pads.merge((Number) value, 1 << i, (v1, v2) -> v1 | v2);
+ }
}
}
+ nodataValues = CollectionsExt.unmodifiableOrCopy(pads);
}
- return pads;
+ return nodataValues;
}
/**
@@ -563,7 +603,11 @@ public abstract class Variable extends NamedElement {
* (2,1,0) (2,1,1) (2,1,2) (2,1,3)
* }
*
- * This method may cache the returned vector, at implementation choice.
+ * If {@link #hasRealValues()} returns {@code true}, then this method shall
+ * {@linkplain #replaceNaN(Object) replace fill values and missing values by NaN values}.
+ * This method should cache the returned vector since this method may be invoked often.
+ * Because of caching, this method should not be invoked for large data array.
+ * Callers shall not modify the returned vector.
*
* @return the data as an array of a Java primitive type.
* @throws IOException if an error occurred while reading the data.
@@ -584,7 +628,8 @@ public abstract class Variable extends NamedElement {
* </ul>
*
* If the variable has more than one dimension, then the data are packed in a one-dimensional vector
- * in the same way than {@link #read()}.
+ * in the same way than {@link #read()}. If {@link #hasRealValues()} returns {@code true}, then this
+ * method shall {@linkplain #replaceNaN(Object) replace fill/missing values by NaN values}.
*
* @param area indices of cell values to read along each dimension, in "natural" order.
* @param subsampling sub-sampling along each dimension. 1 means no sub-sampling.
@@ -612,6 +657,28 @@ public abstract class Variable extends NamedElement {
}
/**
+ * Maybe replace fill values and missing values by {@code NaN} values in the given array.
+ * This method does nothing if {@link #hasRealValues()} returns {@code false}.
+ * The NaN values used by this method must be consistent with the NaN values declared in
+ * the sample dimensions created by {@link org.apache.sis.storage.netcdf.GridResource}.
+ *
+ * @param array the array in which to replace fill and missing values.
+ */
+ protected final void replaceNaN(final Object array) {
+ if (hasRealValues()) {
+ int ordinal = 0;
+ for (final Number value : getNodataValues().keySet()) {
+ final float pad = MathFunctions.toNanFloat(ordinal++); // Must be consistent with GridResource.createSampleDimension(…).
+ if (array instanceof float[]) {
+ ArraysExt.replace((float[]) array, value.floatValue(), pad);
+ } else if (array instanceof double[]) {
+ ArraysExt.replace((double[]) array, value.doubleValue(), pad);
+ }
+ }
+ }
+ }
+
+ /**
* Sets the scale and offset coefficients in the given "grid to CRS" transform if possible.
* Source and target dimensions given to this method are in "natural" order (reverse of netCDF order).
* This method is invoked only for variables that represent a coordinate system axis.
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 03203a6..6f8c96d 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
@@ -39,12 +39,12 @@ import org.apache.sis.internal.storage.io.ChannelDataInput;
import org.apache.sis.internal.storage.io.HyperRectangleReader;
import org.apache.sis.internal.storage.io.Region;
import org.apache.sis.internal.util.StandardDateFormat;
-import org.apache.sis.internal.util.Numerics;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.netcdf.AttributeNames;
import org.apache.sis.util.logging.WarningListeners;
import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Numbers;
import org.apache.sis.measure.Units;
import org.apache.sis.math.Vector;
@@ -698,6 +698,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
/**
* Reads all the data for this variable and returns them as an array of a Java primitive type.
* Multi-dimensional variables are flattened as a one-dimensional array (wrapped in a vector).
+ * Fill values/missing values are replaced by NaN if {@link #hasRealValues()} is {@code true}.
* The vector is cached and returned as-is in all future invocation of this method.
*
* @throws ArithmeticException if the size of the variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
@@ -720,6 +721,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
final Region region = new Region(upper, lower, upper, subsampling);
applyUnlimitedDimensionStride(region);
Object array = reader.read(region);
+ replaceNaN(array);
/*
* If we can convert a double[] array to a float[] array, we should do that before
* to invoke 'setValues(array)' - we can not rely on data.compress(tolerance). The
@@ -729,7 +731,7 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
* this assumption, we need to convert to float[] before createDecimalVector(…).
*/
if (array instanceof double[]) {
- final float[] copy = Numerics.copyAsFloatsIfLossless((double[]) array);
+ final float[] copy = ArraysExt.copyAsFloatsIfLossless((double[]) array);
if (copy != null) array = copy;
}
setValues(array);
@@ -803,7 +805,9 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
}
final Region region = new Region(size, lower, upper, subsampling);
applyUnlimitedDimensionStride(region);
- return Vector.create(reader.read(region), dataType.isUnsigned);
+ final Object array = reader.read(region);
+ replaceNaN(array);
+ return Vector.create(array, dataType.isUnsigned);
}
/**
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 bed9c0c..0d2d818 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
@@ -366,14 +366,14 @@ final class VariableWrapper extends Variable {
/**
* Reads all the data for this variable and returns them as an array of a Java primitive type.
* Multi-dimensional variables are flattened as a one-dimensional array (wrapped in a vector).
- * This method caches the returned vector.
+ * This method may replace fill/missing values by NaN values and caches the returned vector.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public Vector read() throws IOException {
if (values == null) {
final Array array = variable.read(); // May be already cached by the UCAR library.
- values = createDecimalVector(array.get1DJavaArray(array.getElementType()), variable.isUnsigned());
+ values = createDecimalVector(get1DJavaArray(array), variable.isUnsigned());
values = SHARED_VECTORS.unique(values);
}
return values;
@@ -405,7 +405,18 @@ final class VariableWrapper extends Variable {
} catch (InvalidRangeException e) {
throw new DataStoreException(e);
}
- return Vector.create(array.get1DJavaArray(array.getElementType()), variable.isUnsigned());
+ return Vector.create(get1DJavaArray(array), variable.isUnsigned());
+ }
+
+ /**
+ * Returns the one-dimensional Java array for the given UCAR array, avoiding copying if possible.
+ * If {@link #hasRealValues()} returns {@code true}, then this method replaces fill and missing
+ * values by {@code NaN} values.
+ */
+ private Object get1DJavaArray(final Array array) {
+ final Object data = array.get1DJavaArray(array.getElementType());
+ replaceNaN(data);
+ return data;
}
/**
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 8737d0e..88afe0b 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
@@ -43,6 +43,7 @@ import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreReferencingException;
import org.apache.sis.storage.Resource;
+import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.resources.Errors;
@@ -301,27 +302,38 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
/*
* Adds the "missing value" or "fill value" as qualitative categories.
* If a value has both roles, use "missing value" for category name.
+ * If the values are already real values, then the "no data" values
+ * have been replaced by NaN values by Variable.replaceNaN(Object).
+ * The qualitative categories constructed below must be consistent
+ * with the NaN values used by 'replaceNaN'.
*/
boolean setBackground = true;
+ int ordinal = data.hasRealValues() ? 0 : -1;
final InternationalString[] names = new InternationalString[2];
for (final Map.Entry<Number,Integer> entry : data.getNodataValues().entrySet()) {
- final Number n = entry.getKey();
- final double fp = n.doubleValue();
- if (!builder.rangeCollides(fp, fp)) {
- final int role = entry.getValue(); // Bit 0 set (value 1) = pad value, bit 1 set = missing value.
- final int i = (role == 1) ? 1 : 0; // i=1 if role is only pad value, i=0 otherwise.
- InternationalString name = names[i];
- if (name == null) {
- name = Vocabulary.formatInternational(i == 0 ? Vocabulary.Keys.MissingValue : Vocabulary.Keys.FillValue);
- names[i] = name;
- }
- if (setBackground & (role & 1) != 0) {
- setBackground = false; // Declare only one fill value.
- builder.setBackground(name, n);
- } else {
- builder.addQualitative(name, n, n);
+ final Number n;
+ if (ordinal >= 0) {
+ n = MathFunctions.toNanFloat(ordinal++); // Must be consistent with Variable.replaceNaN(Object).
+ } else {
+ n = entry.getKey();
+ final double fp = n.doubleValue();
+ if (builder.rangeCollides(fp, fp)) {
+ continue;
}
}
+ final int role = entry.getValue(); // Bit 0 set (value 1) = pad value, bit 1 set = missing value.
+ final int i = (role == 1) ? 1 : 0; // i=1 if role is only pad value, i=0 otherwise.
+ InternationalString name = names[i];
+ if (name == null) {
+ name = Vocabulary.formatInternational(i == 0 ? Vocabulary.Keys.MissingValue : Vocabulary.Keys.FillValue);
+ names[i] = name;
+ }
+ if (setBackground & (role & 1) != 0) {
+ setBackground = false; // Declare only one fill value.
+ builder.setBackground(name, n);
+ } else {
+ builder.addQualitative(name, n, n);
+ }
}
return builder.build();
}