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 b26d88a663 Store information about the "no data" value used in an ASCII Grid file. It will be needed for re-exporting data in ASCII Grid again. b26d88a663 is described below commit b26d88a66305ee513e3d11bf55ad66777b3e3d90 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Apr 6 01:27:15 2022 +0200 Store information about the "no data" value used in an ASCII Grid file. It will be needed for re-exporting data in ASCII Grid again. --- .../org/apache/sis/coverage/SampleDimension.java | 34 +++++++++++++++--- .../apache/sis/internal/storage/ascii/Store.java | 40 +++++++++++++++------- .../sis/internal/storage/ascii/StoreTest.java | 22 +++++++++++- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java index a8063f678e..9aad5130a0 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java @@ -549,6 +549,32 @@ public class SampleDimension implements Serializable { * @module */ public static class Builder { + /** + * The default name used for quantitative categories. + * + * @see #addQuantitative(CharSequence, NumberRange, MathTransform1D, Unit) + */ + private static final InternationalString DATA = Vocabulary.formatInternational(Vocabulary.Keys.Data); + + /** + * The default name used for qualitative categories. + * + * @see #addQualitative(CharSequence, NumberRange) + */ + private static final InternationalString NODATA = Vocabulary.formatInternational(Vocabulary.Keys.Nodata); + + /** + * The default name used for background. + * The difference between "no data" and "fill value" is that "no data" is used when a value + * is inside the coverage domain of validity but missing, for example because of clouds. + * By contrast the fill value is used for values outside the coverage domain of validity + * when the empty space must be filled with something. It happens for example when the + * coverage is rotated inside the rectangular bounds of the rendered image. + * + * @see #setBackground(CharSequence, Number) + */ + private static final InternationalString FILL_VALUE = Vocabulary.formatInternational(Vocabulary.Keys.FillValue); + /** * Identification for this sample dimension. */ @@ -741,7 +767,7 @@ public class SampleDimension implements Serializable { public Builder setBackground(CharSequence name, Number sample) { ArgumentChecks.ensureNonNull("sample", sample); if (name == null) { - name = Vocabulary.formatInternational(Vocabulary.Keys.FillValue); + name = FILL_VALUE; } final NumberRange<?> samples = range(sample.getClass(), sample, sample); // Use of `getMinValue()` below shall be consistent with ToNaN.remove(Category). @@ -931,7 +957,7 @@ public class SampleDimension implements Serializable { */ public Builder addQualitative(CharSequence name, final NumberRange<?> samples) { if (name == null) { - name = Vocabulary.formatInternational(Vocabulary.Keys.Nodata); + name = NODATA; } add(new Category(name, samples, null, null, toNaN)); return this; @@ -992,7 +1018,7 @@ public class SampleDimension implements Serializable { throw new IllegalArgumentException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, "NaN #" + ordinal)); } if (name == null) { - name = Vocabulary.formatInternational(Vocabulary.Keys.Nodata); + name = NODATA; } add(new Category(name, samples, null, null, (v) -> ordinal)); return this; @@ -1139,7 +1165,7 @@ public class SampleDimension implements Serializable { public Builder addQuantitative(CharSequence name, NumberRange<?> samples, MathTransform1D toUnits, Unit<?> units) { ArgumentChecks.ensureNonNull("toUnits", toUnits); if (name == null) { - name = Vocabulary.formatInternational(Vocabulary.Keys.Data); + name = DATA; } add(new Category(name, samples, toUnits, units, toNaN)); return this; diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java index d67738f34f..411490d10b 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java @@ -99,6 +99,11 @@ final class Store extends PRJDataStore implements GridCoverageResource { "DX", "DY" }; + /** + * The default no-data value. This is part of the ASCII Grid format specification. + */ + private static final double DEFAULT_NODATA = -9999; + /** * The object to use for reading data, or {@code null} if the channel has been closed. * Note that a null value does not necessarily means that the store is closed, because @@ -116,13 +121,14 @@ final class Store extends PRJDataStore implements GridCoverageResource { * The optional {@code NODATA_VALUE} attribute, or {@code NaN} if none. * This value is valid only if {@link #gridGeometry} is non-null. */ - private double fillValue; + private double nodataValue; /** - * The {@link #fillValue} as a text. This is useful when the fill value - * can not be parsed as a {@code double} value, for example {@code "N/A"}. + * The {@link #nodataValue} as a text. This is useful when the fill value + * can not be parsed as a {@code double} value, for example {@code "NULL"}, + * {@code "N/A"}, {@code "NA"}, {@code "mv"}, {@code "!"} or {@code "-"}. */ - private String fillText; + private String nodataText; /** * The image size together with the "grid to CRS" transform. @@ -152,7 +158,6 @@ final class Store extends PRJDataStore implements GridCoverageResource { */ public Store(final StoreProvider provider, final StorageConnector connector) throws DataStoreException { super(provider, connector); - fillValue = Double.NaN; input = new CharactersView(connector.commit(ChannelDataInput.class, StoreProvider.NAME), null); listeners.useWarningEventsOnly(); } @@ -219,11 +224,15 @@ cellsize: if (value != null) { * This reader accepts a value both as text and as a floating point. * The intent is to accept unparsable texts such as "NULL". */ - fillText = header.remove(key = NODATA_VALUE); - if (fillText != null) try { - fillValue = Double.parseDouble(fillText); + nodataText = header.remove(key = NODATA_VALUE); + if (nodataText != null) try { + nodataValue = Double.parseDouble(nodataText); } catch (NumberFormatException e) { + nodataValue = Double.NaN; listeners.warning(messageForProperty(Errors.Keys.IllegalValueForProperty_2, key), e); + } else { + nodataValue = DEFAULT_NODATA; + nodataText = "null"; // "NaN" is already understood by `parseDouble(String)`. } } catch (NumberFormatException e) { throw new DataStoreContentException(messageForProperty(Errors.Keys.IllegalValueForProperty_2, key), e); @@ -384,11 +393,11 @@ cellsize: if (value != null) { double value; try { value = Double.parseDouble(token); - if (value == fillValue) { + if (value == nodataValue) { value = Double.NaN; } } catch (NumberFormatException e) { - if (token.equalsIgnoreCase(fillText)) { + if (token.equalsIgnoreCase(nodataText)) { value = Double.NaN; } else { throw new DataStoreContentException(Resources.forLocale(getLocale()).getString( @@ -400,7 +409,8 @@ cellsize: if (value != null) { } /* * At this point we finished to read the full image. Close the channel now and build the sample dimension. - * The sample dimension does not contain NODATA_VALUE because we already converted them to NaN. + * We add a category for the NODATA_VALUE even if this value does not appear anymore in the `data` array + * (since we replaced it by NaN on-the-fly) because this information is needed by `WritableStore`. * * TODO: a future version could try to convert the image to integer values. * In this case only we may need to declare the NODATA_VALUE. @@ -413,8 +423,12 @@ cellsize: if (value != null) { minimum = 0; maximum = 1; } - final SampleDimension.Builder b = new SampleDimension.Builder().setName(filename); - final SampleDimension band = b.addQuantitative(null, minimum, maximum, null).build(); + final SampleDimension.Builder b = new SampleDimension.Builder(); + b.setName(filename).addQuantitative(null, minimum, maximum, null); + if (nodataValue < minimum || nodataValue > maximum) { + b.mapQualitative(null, nodataValue, Float.NaN); + } + final SampleDimension band = b.build().forConvertedValues(true); /* * Build the coverage last, because a non-null `coverage` field * is used for meaning that everything succeed. diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java index 4df0a60adc..2f8804f188 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java @@ -16,11 +16,13 @@ */ package org.apache.sis.internal.storage.ascii; +import java.util.List; import java.awt.image.Raster; import java.awt.image.RenderedImage; import org.opengis.metadata.Metadata; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.metadata.identification.Identification; +import org.apache.sis.coverage.Category; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.StorageConnector; @@ -48,7 +50,9 @@ public final strictfp class StoreTest extends TestCase { } /** - * Tests the metadata of the {@code "grid.asc"} file. + * Tests the metadata of the {@code "grid.asc"} file. This test reads only the header. + * It should not test sample dimensions or pixel values, because doing so read the full + * image and is the purpose of {@link #testRead()}. * * @throws DataStoreException if an error occurred while reading the file. */ @@ -73,6 +77,10 @@ public final strictfp class StoreTest extends TestCase { getSingleton(getSingleton(id.getExtents()).getGeographicElements()); assertEquals(-84, bbox.getSouthBoundLatitude(), 1); assertEquals(+85, bbox.getNorthBoundLatitude(), 1); + /* + * Verify that the metadata is cached. + */ + assertSame(metadata, store.getMetadata()); } } @@ -84,6 +92,14 @@ public final strictfp class StoreTest extends TestCase { @Test public void testRead() throws DataStoreException { try (Store store = open()) { + final List<Category> categories = getSingleton(store.getSampleDimensions()).getCategories(); + assertEquals(2, categories.size()); + assertEquals( -2, categories.get(0).getSampleRange().getMinDouble(), 1); + assertEquals( 30, categories.get(0).getSampleRange().getMaxDouble(), 1); + assertEquals(-9999, categories.get(1).forConvertedValues(false).getSampleRange().getMinDouble(), 0); + /* + * Check sample values. + */ final GridCoverage coverage = store.read(null, null); final RenderedImage image = coverage.render(null); assertEquals(10, image.getWidth()); @@ -94,6 +110,10 @@ public final strictfp class StoreTest extends TestCase { assertEquals(Float.NaN, tile.getSampleFloat(9, 19, 0), 0f); assertEquals( -1.075f, tile.getSampleFloat(0, 19, 0), 0f); assertEquals( 27.039f, tile.getSampleFloat(4, 10, 0), 0f); + /* + * Verify that the coverage is cached. + */ + assertSame(coverage, store.read(null, null)); } } }