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 <[email protected]>
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));
}
}
}