This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 19a871140542ea8cbdfa1db53913d67255a1f8ba Author: Martin Desruisseaux <[email protected]> AuthorDate: Mon Jan 21 19:33:07 2019 +0100 Replace the null value in Category.range by a range containing NaN values. The intent is to remove the Category.minimum/maximum fields in a future commit. --- .../java/org/apache/sis/coverage/Category.java | 78 +++++++++++----------- .../java/org/apache/sis/coverage/CategoryList.java | 29 ++++---- .../org/apache/sis/coverage/ConvertedCategory.java | 2 +- .../org/apache/sis/coverage/SampleDimension.java | 31 +++------ .../org/apache/sis/coverage/SampleRangeFormat.java | 9 ++- .../org/apache/sis/coverage/CategoryListTest.java | 2 +- .../java/org/apache/sis/coverage/CategoryTest.java | 13 +++- .../java/org/apache/sis/measure/NumberRange.java | 22 +++++- .../main/java/org/apache/sis/measure/Range.java | 5 +- .../apache/sis/storage/netcdf/GridResource.java | 6 +- 10 files changed, 104 insertions(+), 93 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 7f94c5b..5b81052 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 @@ -118,19 +118,17 @@ public class Category implements Serializable { final double minimum, maximum; /** - * The [{@linkplain #minimum} … {@linkplain #maximum}] range of values, or {@code null} if that range would - * contain {@link Float#NaN} bounds. This is partially redundant with the minimum and maximum fields, except - * for the following differences: + * The [minimum … maximum] range of values in this category (never {@code null}). Notes: * * <ul> - * <li>This field is {@code null} if the minimum and maximum values are NaN (converted qualitative category).</li> + * <li>The minimum and maximum values may be one of the {@linkplain Float#isNaN() NaN} values (see below).</li> * <li>The value type may be different than {@link Double} (typically {@link Integer}).</li> * <li>The bounds may be exclusive instead than inclusive.</li> * <li>The range may be an instance of {@link MeasurementRange} if the {@link #toConverse} is identity * and the units of measurement are known.</li> * </ul> * - * The range is null if this category is a qualitative category converted to real values. + * The range may be {@code NaN} 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: * @@ -258,7 +256,7 @@ public class Category implements Serializable { } } if (isNaN) { - range = null; + range = samples; converse = this; toConverse = identity(); } else try { @@ -345,7 +343,12 @@ public class Category implements Serializable { if (isQuantitative) { range = new ConvertedRange(extremums, minIncluded, maxIncluded, units); } else { - range = null; + final float min = (float) minimum; + if (Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(min)) { + range = NumberRange.create(Float.class, min); + } else { + range = NumberRange.create(Double.class, minimum); + } } } @@ -368,6 +371,15 @@ public class Category implements Serializable { } /** + * Returns {@code true} if this category is a qualitative category that has been converted to "real values". + * In such case, the real values are {@link Float#isNaN()} numbers. If {@code false}, then this category is + * either a quantitative category or a qualitative category that has not been converted to "real values". + */ + final boolean isConvertedQualitative() { + return Double.isNaN(range.getMinDouble()); + } + + /** * Returns {@code true} if this category is quantitative. A quantitative category has a * {@linkplain #getTransferFunction() transfer function} mapping sample values to values * in some units of measurement. By contrast, a qualitative category maps sample values @@ -378,7 +390,7 @@ public class Category implements Serializable { * {@code false} if this category is qualitative. */ public boolean isQuantitative() { - return converted().range != null; + return !converted().isConvertedQualitative(); } /** @@ -388,39 +400,16 @@ public class Category implements Serializable { * are already real values and the range may be an instance of {@link MeasurementRange} * (i.e. a number range with units of measurement). * + * <p>This method never returns {@code null}, but may return an {@linkplain NumberRange#isBounded() unbounded range} + * or a range containing a singleton {@link Double#NaN} value. The {@code NaN} values happen if this range is derived + * from a "no data" value converted to "real value" by the {@linkplain #getTransferFunction() transfer function}.</p> + * * @return the range of sample values in this category. * * @see SampleDimension#getSampleRange() */ - @SuppressWarnings({"unchecked", "rawtypes"}) public NumberRange<?> getSampleRange() { - 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. + return range; } /** @@ -432,8 +421,13 @@ public class Category implements Serializable { * @see SampleDimension#getMeasurementRange() */ public Optional<MeasurementRange<?>> getMeasurementRange() { - // A ClassCastException below would be a bug in our constructor. - return Optional.ofNullable((MeasurementRange<?>) converted().range); + final NumberRange<?> mr = converted().range; + if (Double.isNaN(mr.getMinDouble())) { + return Optional.empty(); + } else { + // A ClassCastException below would be a bug in our constructor. + return Optional.of((MeasurementRange<?>) mr); + } } /** @@ -464,7 +458,11 @@ public class Category implements Serializable { * 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. */ - return (converse.range != null) ? Optional.of(toConverse) : Optional.empty(); + if (converse.isConvertedQualitative()) { + return Optional.empty(); + } else { + return Optional.of(toConverse); + } } /** @@ -497,7 +495,7 @@ public class Category implements Serializable { } if (object != null && getClass().equals(object.getClass())) { final Category that = (Category) object; - return name.equals(that.name) && Objects.equals(range, that.range) && + return name.equals(that.name) && range.equals(that.range) && Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(that.minimum) && Double.doubleToRawLongBits(maximum) == Double.doubleToRawLongBits(that.maximum) && toConverse.equals(that.toConverse); diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java index f008311..b9e4e4b 100644 --- a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java +++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java @@ -86,7 +86,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform * The category to use if {@link #search(double)} is invoked with a sample value greater than all ranges in this list. * This is usually a reference to the last category to have a range of real values. A {@code null} value means that no * extrapolation should be used. By extension, a {@code null} value also means that {@link #search(double)} should not - * try to find any fallback at all if the requested sample value does not fall in a category range. + * try to find any fallback if the requested sample value does not fall in a category range. * * <p>There is no explicit extrapolation field for values less than all ranges in this list because the extrapolation * to use in such case is {@code categories[0]}.</p> @@ -131,7 +131,8 @@ final class CategoryList extends AbstractList<Category> implements MathTransform /** * Constructs a category list using the specified array of categories. - * The {@code categories} array should contain at least one element. + * The {@code categories} array should contain at least one element, + * otherwise the {@link #EMPTY} constant should be used. * * @param categories the list of categories. May be empty, but can not be null. * This array is not cloned and is modified in-place. @@ -172,17 +173,16 @@ final class CategoryList extends AbstractList<Category> implements MathTransform category.name, category.getRangeLabel())); } } - final NumberRange<?> extent = category.range; - if (extent != null) { + if (!category.isConvertedQualitative()) { /* - * Initialize with the union of ranges at index 0 and index i. In most cases, it will cover the whole range - * so all future calls to 'range.unionAny(extent)' will be no-op. The 'categories[0].range' field should not - * be null because categories with null ranges are sorted last (because their 'minimum' field is NaN). + * Initialize with the union of ranges at index 0 and index i. In most cases, the result will cover the whole + * range so all future calls to 'range.unionAny(…)' will be no-op. The 'categories[0].range' field should not + * be NaN because categories with NaN ranges are sorted last. */ if (range == null) { range = categories[0].range; } - range = range.unionAny(extent); + range = range.unionAny(category.range); } } this.range = range; @@ -203,10 +203,11 @@ final class CategoryList extends AbstractList<Category> implements MathTransform boolean hasQuantitative = false; final Category[] convertedCategories = new Category[categories.length]; for (int i=0; i < convertedCategories.length; i++) { - final Category category = categories[i]; - hasConversion |= (category != category.converse); - hasQuantitative |= (category.converse.range != null); - convertedCategories[i] = category.converse; + final Category category = categories[i]; + final Category converted = category.converse; + hasConversion |= (category != converted); + hasQuantitative |= !converted.isConvertedQualitative(); + convertedCategories[i] = converted; } if (hasQuantitative) { converse = hasConversion ? new CategoryList(convertedCategories, this) : this; @@ -457,6 +458,10 @@ final class CategoryList extends AbstractList<Category> implements MathTransform piece.transform(srcPts, srcOff, dstPts, stepOff, count); } } + /* + * If extrapolation may have happened, verify that transformed values are in expected ranges. + * Values out of range will be clamped. + */ if (extrapolation != null) { dstOff = srcOff + srcToDst; final Category converse = category.converse; 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 00c9007..fcfed80 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 @@ -69,6 +69,6 @@ final class ConvertedCategory extends Category { */ @Override public Optional<MathTransform1D> getTransferFunction() { - return (range != null) ? Optional.of(identity()) : Optional.empty(); + return isConvertedQualitative() ? Optional.empty() : Optional.of(identity()); } } 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 8bf397f..074dc32 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 @@ -134,7 +134,7 @@ public class SampleDimension implements Serializable { * @param original the original sample dimension for packed values. * @param bc category of the background value in original sample dimension, or {@code null}. */ - private SampleDimension(final SampleDimension original, Category bc) { + private SampleDimension(final SampleDimension original, final Category bc) { converse = original; name = original.name; categories = original.categories.converse; @@ -143,13 +143,7 @@ public class SampleDimension implements Serializable { if (bc == null) { background = null; } else { - bc = bc.converse; - final NumberRange<?> range = bc.range; - if (range != null) { - background = range.getMinValue(); - } else { - background = (float) bc.minimum; - } + background = bc.converse.range.getMinValue(); } } @@ -263,8 +257,9 @@ public class SampleDimension implements Serializable { Class<? extends Number> widestClass = Byte.class; int count = 0; for (final Category category : categories) { - final NumberRange<?> range = category.range; - if (range != null && !category.isQuantitative()) { + final Category converted = category.converted(); + if (category != converted && converted.isConvertedQualitative()) { + final NumberRange<?> range = category.range; if (!range.isBounded()) { throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range)); } @@ -950,20 +945,12 @@ public class SampleDimension implements Serializable { } /** - * Returns {@code true} if the given range intersects the range of a previously added category. - * This method can be invoked before to add a new category for checking if it would cause a range collision. + * Returns an unmodifiable view of the list of categories added so far. * - * @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. + * @return an unmodifiable view of the current category list. */ - public boolean rangeCollides(final double minimum, final double maximum) { - for (final Category category : categories) { - if (maximum >= category.minimum && minimum <= category.maximum) { - return true; - } - } - return false; + public List<Category> categories() { + return Collections.unmodifiableList(categories); } /** 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 4a1e1df..ccf4712 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 @@ -160,9 +160,6 @@ final class SampleRangeFormat extends RangeFormat { * @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 null; - } final NumberFormat nf = (NumberFormat) elementFormat; final int min = nf.getMinimumFractionDigits(); final int max = nf.getMaximumFractionDigits(); @@ -226,9 +223,11 @@ final class SampleRangeFormat extends RangeFormat { */ if (hasQuantitative) { final Category converted = category.converted(); - String text = formatMeasure(converted.range); // Example: [6.0 … 25.0)°C - if (text == null) { + final String text; + if (converted.isConvertedQualitative()) { text = String.valueOf(converted.getRangeLabel()); // Example: NaN #0 + } else { + text = formatMeasure(converted.getSampleRange()); // Example: [6.0 … 25.0)°C } table.append(text); table.nextColumn(); diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java index 211a53a..2e8bc25 100644 --- a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java +++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java @@ -63,7 +63,7 @@ public final strictfp class CategoryListTest extends TestCase { */ private static void assertNotConverted(final CategoryList categories) { for (final Category c : categories) { - assertNotNull(c.range); + assertFalse("isNaN", Double.isNaN(c.range.getMinDouble())); assertFalse(c.range instanceof ConvertedRange); } } 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 11b659b..b0056f3 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 @@ -44,6 +44,15 @@ public final strictfp class CategoryTest extends TestCase { static final double EPS = 1E-9; /** + * Asserts that the given range contains NaN values. + */ + private static void assertNaN(final String message, final NumberRange<?> range) { + final double value = range.getMinDouble(); + assertTrue(message, Double.isNaN(value)); + assertEquals(message, Double.doubleToRawLongBits(value), Double.doubleToRawLongBits(range.getMaxDouble())); + } + + /** * Checks if a {@link Comparable} is a number identical to the supplied integer value. */ private static void assertBoundEquals(final String message, final int expected, final Comparable<?> actual) { @@ -107,7 +116,7 @@ public final strictfp class CategoryTest extends TestCase { assertEquals ("name", "Random", String.valueOf(converse.getName())); assertTrue ("minimum", Double.isNaN(converse.minimum)); assertTrue ("maximum", Double.isNaN(converse.maximum)); - assertNull ("range", converse.range); + assertNaN ("range", converse.range); assertNotNull("sampleRange", converse.getSampleRange()); assertFalse ("measurementRange", category.getMeasurementRange().isPresent()); assertFalse ("toConverse.isIdentity", converse.toConverse.isIdentity()); @@ -231,7 +240,7 @@ public final strictfp class CategoryTest extends TestCase { 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); + assertNaN ("sampleRange", category.range); assertEquals("range.minValue", Float.NaN, range.getMinValue()); assertEquals("range.maxValue", Float.NaN, range.getMaxValue()); assertFalse ("measurementRange", category.getMeasurementRange().isPresent()); diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java index 7091dd9..1aad501 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java +++ b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java @@ -122,6 +122,22 @@ public class NumberRange<E extends Number & Comparable<? super E>> extends Range } /** + * Constructs a range containing a single value of the given type. + * The given value is used as the minimum and maximum values, inclusive. + * + * @param <N> compile-time value of {@code type}. + * @param type the element type, usually one of {@link Byte}, {@link Short}, + * {@link Integer}, {@link Long}, {@link Float} or {@link Double}. + * @param value the value, or {@code null} for creating an unbounded range. + * @return a range containing the given value as its inclusive minimum and maximum. + * + * @since 1.0 + */ + public static <N extends Number & Comparable<? super N>> NumberRange<N> create(final Class<N> type, final N value) { + return unique(new NumberRange<>(type, value, true, value, true)); + } + + /** * Constructs a range of {@code byte} values. * This method may return a shared instance, at implementation choice. * @@ -592,12 +608,12 @@ public class NumberRange<E extends Number & Comparable<? super E>> extends Range } /** - * Returns {@code true} if the supplied range is fully contained within this range. + * Returns {@code true} if the supplied range intersects this range. * This method converts {@code this} or the given argument to the widest numeric type, * then delegates to {@link #intersects(Range)}. * - * @param range the range to check for inclusion in this range. - * @return {@code true} if the given range is included in this range. + * @param range the range to check for intersection with this range. + * @return {@code true} if the given range intersects this range. * @throws IllegalArgumentException if the given range can not be converted to a valid type * through widening conversion, or if the units of measurement are not convertible. */ diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java index bb653dd..32ae059 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java +++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java @@ -587,10 +587,11 @@ public class Range<E extends Comparable<? super E>> implements CheckedContainer< * <ul> * <li>are both {@linkplain #isEmpty() empty}, or</li> * <li>have equal {@linkplain #getMinValue() minimum} and {@linkplain #getMaxValue() maximum} values - * with equal inclusive/exclusive flags.</li> + * with equal inclusive/exclusive flags. Note that numbers in {@link Float} or {@link Double} + * wrappers consider all {@code NaN} values as equal.</li> * </ul> * - * Note that subclasses may add other requirements, for example on units of measurement. + * Subclasses may add other requirements, for example on units of measurement. * * @param object the object to compare with this range for equality. * @return {@code true} if the given object is equal to this range. 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 53c410a..06c1d7c 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 @@ -327,11 +327,7 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS 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; - } + n = entry.getKey(); // Should be real number, made unique by the HashMap. } 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.
