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 3bb589a947992e343cbd3884f826afa52ec4677a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Apr 2 19:44:50 2022 +0200 Complete the ASCII Grid reader implementation. https://issues.apache.org/jira/browse/SIS-540 --- .../org/apache/sis/internal/storage/Resources.java | 5 + .../sis/internal/storage/Resources.properties | 1 + .../sis/internal/storage/Resources_fr.properties | 1 + .../sis/internal/storage/ascii/CharactersView.java | 49 +++--- .../apache/sis/internal/storage/ascii/Store.java | 190 ++++++++++++++------- .../sis/internal/storage/ascii/StoreProvider.java | 45 ++++- .../sis/internal/storage/ascii/package-info.java | 36 +++- .../org.apache.sis.storage.DataStoreProvider | 1 + 8 files changed, 231 insertions(+), 97 deletions(-) diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java index fdfa50c..0e50eb9 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java @@ -115,6 +115,11 @@ public final class Resources extends IndexedResourceBundle { public static final short CanNotReadFile_4 = 3; /** + * Can not read pixel at ({0}, {1}) indices in the “{2}” file. + */ + public static final short CanNotReadPixel_3 = 68; + + /** * Can not remove resource “{1}” from aggregate “{0}”. */ public static final short CanNotRemoveResource_2 = 49; diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties index 7a50e42..821d1cc 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties @@ -30,6 +30,7 @@ CanNotReadDirectory_1 = Can not read \u201c{0}\u201d directory. CanNotReadFile_2 = Can not read \u201c{1}\u201d as a file in the {0} format. CanNotReadFile_3 = Can not read line {2} of \u201c{1}\u201d as part of a file in the {0} format. CanNotReadFile_4 = Can not read after column {3} of line {2} of \u201c{1}\u201d as part of a file in the {0} format. +CanNotReadPixel_3 = Can not read pixel at ({0}, {1}) indices in the \u201c{2}\u201d file. CanNotRemoveResource_2 = Can not remove resource \u201c{1}\u201d from aggregate \u201c{0}\u201d. CanNotRenderImage_1 = Can not render an image for the \u201c{0}\u201d coverage. CanNotStoreResourceType_2 = Can not save resources of type \u2018{1}\u2019 in a \u201c{0}\u201d store. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties index 0b19b1f..d04def0 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties @@ -35,6 +35,7 @@ CanNotReadDirectory_1 = Ne peut pas lire le r\u00e9pertoire \u00ab\u CanNotReadFile_2 = Ne peut pas lire \u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}. CanNotReadFile_3 = Ne peut pas lire la ligne {2} de \u00ab\u202f{1}\u202f\u00bb comme une partie d\u2019un fichier au format {0}. CanNotReadFile_4 = Ne peut pas lire apr\u00e8s la colonne {3} de la ligne {2} de \u00ab\u202f{1}\u202f\u00bb comme une partie d\u2019un fichier au format {0}. +CanNotReadPixel_3 = Ne peut pas lire le pixel aux indices ({0}, {1}) dans le fichier \u00ab\u202f{2}\u202f\u00bb. CanNotRemoveResource_2 = Ne peut pas supprimer la ressource \u00ab\u202f{1}\u202f\u00bb de l\u2019agr\u00e9gat \u00ab\u202f{0}\u202f\u00bb. CanNotRenderImage_1 = Ne peut pas produire une image pour la couverture de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb. CanNotStoreResourceType_2 = Ne peut pas enregistrer des ressources de type \u2018{1}\u2019 dans un entrep\u00f4t de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java index ef5cfa6..e3c66ed 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java @@ -47,7 +47,9 @@ final class CharactersView implements CharSequence { private static final char SPACE = ' '; /** - * The object to use for reading data, or {@code null} if this store has been closed. + * The object to use for reading data, or {@code null} if unavailable. + * This is null during {@linkplain StoreProvider#probeContent probe} operation. + * Shall never be null when this instance is the {@link Store#input} instance. */ final ChannelDataInput input; @@ -72,11 +74,15 @@ final class CharactersView implements CharSequence { /** * Creates a new sequence of characters. * - * @param input the source of bytes. + * @param input the source of bytes, or {@code null} if unavailable. + * @oaram buffer the buffer, or {@code null} for {@code input.buffer}. */ - CharactersView(final ChannelDataInput input) { + CharactersView(final ChannelDataInput input, ByteBuffer buffer) { + if (buffer == null) { + buffer = input.buffer; + } this.input = input; - this.buffer = input.buffer; + this.buffer = buffer; this.direct = buffer.hasArray(); this.array = direct ? buffer.array() : new byte[80]; } @@ -134,6 +140,19 @@ final class CharactersView implements CharSequence { } /** + * Reads the next byte as an unsigned value. + */ + private int readByte() throws IOException { + if (!buffer.hasRemaining()) { + if (input == null) { + throw new EOFException(); + } + input.ensureBufferContains(Byte.BYTES); + } + return Byte.toUnsignedInt(buffer.get()); + } + + /** * Skips all character until the end of line. * This is used for skipping a comment line in the header. * This method can be invoked after {@link #readToken()}. @@ -146,9 +165,9 @@ final class CharactersView implements CharSequence { private boolean skipLine(final boolean stopAtToken) throws IOException { buffer.position(buffer.position() - 1); // For checking if the space that we skipped was CR/LF. boolean eol = false; - byte c; + int c; do { - c = input.readByte(); + c = readByte(); eol = (c == '\r' || c == '\n'); } while (!(eol || (stopAtToken && c > SPACE))); @@ -156,19 +175,6 @@ final class CharactersView implements CharSequence { } /** - * Skips leading white spaces, carriage returns or control characters, then skips the non-white characters. - * This method is used for skipping a sample value without parsing the number when a subsampling is applied. - * - * @throws EOFException if the channel has reached the end of stream. - * @throws IOException if an other kind of error occurred while reading. - */ - @SuppressWarnings("empty-statement") - final void skipToken() throws IOException { - while (input.readByte() <= SPACE); - while (input.readByte() > SPACE); - } - - /** * Skips leading white spaces, carriage returns or control characters, then reads and returns the next * sequence of non-white characters. After this method call, the buffer position is on the first white * character after the token. @@ -180,11 +186,14 @@ final class CharactersView implements CharSequence { */ @SuppressWarnings("empty-statement") final String readToken() throws IOException, DataStoreContentException { - while (input.readByte() <= SPACE); + while (readByte() <= SPACE); int start = buffer.position() - 1; int c; do { if (!buffer.hasRemaining()) { + if (input == null) { + throw new EOFException(); + } buffer.position(start); final int current = buffer.limit() - start; if (current >= buffer.capacity()) { 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 68d4178..4b119fe 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 @@ -18,7 +18,6 @@ package org.apache.sis.internal.storage.ascii; import java.util.Map; import java.util.List; -import java.util.Collections; import java.util.Optional; import java.util.StringJoiner; import java.io.IOException; @@ -34,6 +33,8 @@ import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.grid.GridCoverageBuilder; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.image.PlanarImage; +import org.apache.sis.math.Statistics; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreClosedException; @@ -43,13 +44,13 @@ import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.internal.storage.MetadataBuilder; import org.apache.sis.internal.storage.PRJDataStore; import org.apache.sis.internal.storage.RangeArgument; +import org.apache.sis.internal.storage.Resources; import org.apache.sis.internal.storage.io.ChannelDataInput; import org.apache.sis.metadata.iso.DefaultMetadata; import org.apache.sis.metadata.sql.MetadataStoreException; import org.apache.sis.referencing.operation.matrix.Matrix3; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.util.resources.Errors; -import org.apache.sis.util.resources.Vocabulary; /** @@ -58,9 +59,16 @@ import org.apache.sis.util.resources.Vocabulary; * one pair per line and using spaces as separator between keys and values. * The package javadoc lists the recognized keywords. * - * If we allow subclasses in a future version, - * subclasses can add their own (<var>key</var>, <var>value</var>) pairs or modify - * the existing ones by overriding the {@link #processHeader(Map)} method. + * <h2>Possible evolutions</h2> + * If we allow subclasses in a future version, we could add a {@code processHeader(Map)} method + * that subclasses can override for processing their own (<var>key</var>, <var>value</var>) pairs + * or for modifying the values of existing pairs. + * + * <h2>Limitations</h2> + * Current implementation loads and caches the full image no matter the subregion or subsampling + * specified to the {@code read(…)} method. The image is loaded by {@link #getSampleDimensions()} + * call too, because there is no other way to build a reliable sample dimension. + * Even the data type can not be determined for sure without loading the full image. * * @author Martin Desruisseaux (Geomatys) * @version 1.2 @@ -69,7 +77,32 @@ import org.apache.sis.util.resources.Vocabulary; */ final class Store extends PRJDataStore implements GridCoverageResource { /** - * The object to use for reading data, or {@code null} if this store has been closed. + * Keys of elements expected in the header. Must be in upper-case letters. + */ + static final String + NCOLS = "NCOLS", NROWS = "NROWS", + XLLCORNER = "XLLCORNER", YLLCORNER = "YLLCORNER", + XLLCENTER = "XLLCENTER", YLLCENTER = "YLLCENTER", + CELLSIZE = "CELLSIZE", NODATA_VALUE = "NODATA_VALUE"; + + /** + * Alternatives names for {@value #CELLSIZE} when the pixels are not squares. + * Those names are not part of the format defined by ESRI. + * Various implementations use different names. + * + * <p>Names at even indices are for the <var>x</var> axis + * and names at odd indices are for the <var>y</var> axis.</p> + */ + static final String[] CELLSIZES = { + "XCELLSIZE", "YCELLSIZE", + "XDIM", "YDIM", + "DX", "DY" + }; + + /** + * 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 + * it may have finished to read fully the {@linkplain #coverage}. */ private CharactersView input; @@ -99,11 +132,6 @@ final class Store extends PRJDataStore implements GridCoverageResource { private GridGeometry gridGeometry; /** - * Description of the single band contained in the ASCII Grid file. - */ - private SampleDimension band; - - /** * The metadata object, or {@code null} if not yet created. */ private DefaultMetadata metadata; @@ -125,7 +153,7 @@ 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)); + input = new CharactersView(connector.commit(ChannelDataInput.class, StoreProvider.NAME), null); listeners.useWarningEventsOnly(); } @@ -144,59 +172,61 @@ final class Store extends PRJDataStore implements GridCoverageResource { PixelInCell anchor = PixelInCell.CELL_CORNER; String key = null; // Used for error message if an exception is thrown. try { - width = Integer.parseInt(headerValue(header, key = "NCOLS")); - height = Integer.parseInt(headerValue(header, key = "NROWS")); + width = Integer.parseInt(getHeaderValue(header, key = NCOLS)); + height = Integer.parseInt(getHeaderValue(header, key = NROWS)); /* * The ESRI ASCII Grid format has only a "CELLSIZE" property for both axes. * The "DX" and "DY" properties are GDAL extensions and considered optional. * If the de-facto standard "CELLSIZE" property exists, "DX" and "DY" will * be considered unexpected. */ - String value = header.remove(key = "CELLSIZE"); - if (value != null) { - gridToCRS.m00 = gridToCRS.m11 = Double.parseDouble(value); + String value = header.remove(key = CELLSIZE); +cellsize: if (value != null) { + gridToCRS.m11 = -(gridToCRS.m00 = Double.parseDouble(value)); } else { int def = 0; - value = header.remove(key = "DX"); if (value != null) {gridToCRS.m00 = Double.parseDouble(value); def |= 1;} - value = header.remove(key = "DY"); if (value != null) {gridToCRS.m11 = Double.parseDouble(value); def |= 2;} - if (def != 3) { - // Report "CELLSIZE" as the missing property because it is the de-facto standard one. - throw new DataStoreContentException(illegalValue(Errors.Keys.MissingValueForProperty_2, "CELLSIZE")); + for (int i=0; i < CELLSIZES.length;) { + value = header.remove(key = CELLSIZES[i++]); if (value != null) {gridToCRS.m00 = Double.parseDouble(value); def |= 1;} + value = header.remove(key = CELLSIZES[i++]); if (value != null) {gridToCRS.m11 = -Double.parseDouble(value); def |= 2;} + if (def == 3) break cellsize; } + // Report "CELLSIZE" as the missing property because it is the de-facto standard one. + throw new DataStoreContentException(messageForProperty(Errors.Keys.MissingValueForProperty_2, CELLSIZE)); } /* * Lower-left coordinates is specified either by CENTER or CORNER property. * If both are missing, the error message reports that CORNER is missing. */ - value = header.remove(key = "XLLCENTER"); + value = header.remove(key = XLLCENTER); final boolean xCenter = (value != null); if (!xCenter) { - value = headerValue(header, key = "XLLCORNER"); + value = getHeaderValue(header, key = XLLCORNER); } gridToCRS.m02 = Double.parseDouble(value); - value = header.remove(key = "YLLCENTER"); + value = header.remove(key = YLLCENTER); final boolean yCenter = (value != null); if (!yCenter) { - value = headerValue(header, key = "YLLCORNER"); + value = getHeaderValue(header, key = YLLCORNER); } - gridToCRS.m12 = Double.parseDouble(value); + gridToCRS.m12 = Double.parseDouble(value) - gridToCRS.m11 * height; if (xCenter & yCenter) { anchor = PixelInCell.CELL_CENTER; } else if (xCenter != yCenter) { - gridToCRS.convertBefore(xCenter ? 0 : 1, null, 0.5); + gridToCRS.convertBefore(xCenter ? 0 : 1, null, -0.5); } /* * "No data" value is an optional property. Default value is NaN. - * This reader accepts a value specified as text. + * 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"); + fillText = header.remove(key = NODATA_VALUE); if (fillText != null) try { fillValue = Double.parseDouble(fillText); } catch (NumberFormatException e) { - listeners.warning(illegalValue(Errors.Keys.IllegalValueForProperty_2, key), e); + listeners.warning(messageForProperty(Errors.Keys.IllegalValueForProperty_2, key), e); } } catch (NumberFormatException e) { - throw new DataStoreContentException(illegalValue(Errors.Keys.IllegalValueForProperty_2, key), e); + throw new DataStoreContentException(messageForProperty(Errors.Keys.IllegalValueForProperty_2, key), e); } /* * Read the auxiliary PRJ file after we finished parsing the header file. @@ -205,13 +235,13 @@ final class Store extends PRJDataStore implements GridCoverageResource { readPRJ(); gridGeometry = new GridGeometry(new GridExtent(width, height), anchor, MathTransforms.linear(gridToCRS), crs); /* - * If there is any unprocessed properties, log warnings about them. + * If there is any unprocessed properties, log a warning about them. + * We list all properties in a single message. */ if (!header.isEmpty()) { final StringJoiner joiner = new StringJoiner(", "); header.keySet().forEach(joiner::add); - listeners.warning(Errors.getResources(getLocale()).getString( - Errors.Keys.UnexpectedProperty_2, input.input.filename, joiner.toString())); + listeners.warning(messageForProperty(Errors.Keys.UnexpectedProperty_2, joiner.toString())); } } catch (DataStoreException e) { closeOnError(e); @@ -229,30 +259,33 @@ final class Store extends PRJDataStore implements GridCoverageResource { * @param key key of the header property which was requested. * @return the message to use in the exception to be thrown or the warning to be logged. */ - private String illegalValue(final short rk, final String key) { + private String messageForProperty(final short rk, final String key) { return Errors.getResources(getLocale()).getString(rk, input.input.filename, key); } /** * Gets a value from the header map and ensures that it is non-null. + * The entry is removed from the {@code header} map for making easy + * to see if there is any unknown key left. * - * @param header map of (key, value) pair from the header. + * @param header map of (key, value) pairs from the header. * @param key the name of the properties to get. * @return the value, guaranteed to be non-null. * @throws DataStoreException if the value was null. */ - private String headerValue(final Map<String,String> header, final String key) throws DataStoreException { + private String getHeaderValue(final Map<String,String> header, final String key) throws DataStoreException { final String value = header.remove(key); if (value == null) { - throw new DataStoreContentException(illegalValue(Errors.Keys.MissingValueForProperty_2, key)); + throw new DataStoreContentException(messageForProperty(Errors.Keys.MissingValueForProperty_2, key)); } return value; } /** - * Returns the metadata associated to the ASII grid file, or {@code null} if none. + * Returns the metadata associated to the ASII grid file. + * The returned object contains only the metadata that can be computed without reading the whole image. * - * @return the metadata associated to the CSV file, or {@code null} if none. + * @return the metadata associated to the ASCII grid file. * @throws DataStoreException if an error occurred during the parsing process. */ @Override @@ -273,6 +306,12 @@ final class Store extends PRJDataStore implements GridCoverageResource { } catch (TransformException e) { throw new DataStoreReferencingException(getLocale(), StoreProvider.NAME, getDisplayName(), null).initCause(e); } + /* + * Do not add the sample dimension, because in current version computing the sample dimension + * requires loading the full image. Even if the `band` field is already computed and could be + * used opportunistically, we do not use it in order to keep a deterministic behavior + * (we do not want the metadata to vary depending on the order in which methods are invoked). + */ addTitleOrIdentifier(builder); builder.setISOStandards(false); metadata = builder.buildAndFreeze(); @@ -281,7 +320,7 @@ final class Store extends PRJDataStore implements GridCoverageResource { } /** - * Returns the spatiotemporal extent of CSV data in coordinate reference system of the CSV file. + * Returns the spatiotemporal extent of the ASCII grid file. * * @return the spatiotemporal resource extent. * @throws DataStoreException if an error occurred while computing the envelope. @@ -308,20 +347,23 @@ final class Store extends PRJDataStore implements GridCoverageResource { * Returns the ranges of sample values together with the conversion from samples to real values. * ASCII Grid files always contain a single band. * + * <p>In current implementation, fetching the sample dimension requires loading the full coverage because + * the ASCII Grid format provides no way to infer a reasonable {@code SampleDimension} from only the header. + * Even determining the type (integer or floating point values) requires parsing all values.</p> + * * @return ranges of sample values together with their mapping to "real values". * @throws DataStoreException if an error occurred while reading definitions from the underlying data store. */ @Override - public synchronized List<SampleDimension> getSampleDimensions() throws DataStoreException { - readHeader(); - if (band == null) { - read(null, null); - } - return Collections.singletonList(band); + public List<SampleDimension> getSampleDimensions() throws DataStoreException { + return read(null, null).getSampleDimensions(); } /** - * Loads the data. If a non-null grid geometry is specified, then this method may return a sub-sampled image. + * Loads the data if not already done and closes the channel. In current implementation the image is always + * fully loaded and cached. The given domain is ignored. We do that in order to have determinist and stable + * values for the sample range and for the data type. Loading the full image is reasonable if ASCII Grid + * files contain only small images, which is usually the case given how inefficient this format is. * * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. * @param range shall be either 0 or an containing only 0. @@ -333,46 +375,60 @@ final class Store extends PRJDataStore implements GridCoverageResource { RangeArgument.validate(1, range, listeners); if (coverage == null) try { readHeader(); - final CharactersView input = input(); + final CharactersView view = input(); + final String filename = view.input.filename; + final Statistics stats = new Statistics(filename); final double[] data = new double[width * height]; - double minimum = Double.POSITIVE_INFINITY; - double maximum = Double.NEGATIVE_INFINITY; for (int i=0; i < data.length; i++) { - final String token = input.readToken(); + final String token = view.readToken(); double value; try { value = Double.parseDouble(token); if (value == fillValue) { value = Double.NaN; - } else { - if (value < minimum) minimum = value; - if (value > maximum) maximum = value; } } catch (NumberFormatException e) { - if (token.equals(fillText)) { + if (token.equalsIgnoreCase(fillText)) { value = Double.NaN; } else { - throw new DataStoreContentException(e); + throw new DataStoreContentException(Resources.forLocale(getLocale()).getString( + Resources.Keys.CanNotReadPixel_3, i % width, i / width, filename), e); } } data[i] = value; + stats.accept(value); // Need to invoke even for NaN values (because we count them). } + /* + * 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. + * + * 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. + */ + input = null; + view.input.channel.close(); + double minimum = stats.minimum(); + double maximum = stats.maximum(); if (!(minimum <= maximum)) { minimum = 0; maximum = 1; } - final SampleDimension.Builder b = new SampleDimension.Builder(); - if (!Double.isNaN(fillValue)) { - b.setBackground(null, fillValue); - } - b.addQuantitative(Vocabulary.formatInternational(Vocabulary.Keys.Values), minimum, maximum, null); - band = b.build(); + final SampleDimension.Builder b = new SampleDimension.Builder().setName(filename); + final SampleDimension band = b.addQuantitative(null, minimum, maximum, null).build(); + /* + * Build the coverage last, because a non-null `coverage` field + * is used for meaning that everything succeed. + */ coverage = new GridCoverageBuilder() .addRange(band) .setDomain(gridGeometry) .setValues(new DataBufferDouble(data, data.length), null) + .addImageProperty(PlanarImage.STATISTICS_KEY, new Statistics[] {stats}) .build(); - } catch (IOException e) { + } catch (DataStoreException e) { + closeOnError(e); + throw e; + } catch (Exception e) { closeOnError(e); throw new DataStoreException(e); } @@ -398,7 +454,9 @@ final class Store extends PRJDataStore implements GridCoverageResource { @Override public synchronized void close() throws DataStoreException { final CharactersView view = input; - input = null; // Cleared first in case of failure. + input = null; // Cleared first in case of failure. + gridGeometry = null; + coverage = null; if (view != null) try { view.input.channel.close(); } catch (IOException e) { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java index 8dc8957..86bca8e 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java @@ -16,6 +16,9 @@ */ package org.apache.sis.internal.storage.ascii; +import java.util.Map; +import java.nio.ByteBuffer; +import java.io.EOFException; import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.ProbeResult; @@ -40,7 +43,7 @@ import org.apache.sis.internal.storage.PRJDataStore; * @module */ @StoreMetadata(formatName = StoreProvider.NAME, - fileSuffixes = {"asc", "grd", "agr"}, + fileSuffixes = {"asc", "grd", "agr", "aig"}, capabilities = Capability.READ, resourceTypes = GridCoverageResource.class) public final class StoreProvider extends PRJDataStore.Provider { @@ -75,7 +78,45 @@ public final class StoreProvider extends PRJDataStore.Provider { */ @Override public ProbeResult probeContent(StorageConnector connector) throws DataStoreException { - throw new UnsupportedOperationException("Not supported yet."); + return probeContent(connector, ByteBuffer.class, (buffer) -> { + /* + * Quick check if all characters are US-ASCII. + */ + buffer.mark(); + while (buffer.hasRemaining()) { + if (buffer.get() < 0) { + return ProbeResult.UNSUPPORTED_STORAGE; + } + } + buffer.reset(); + /* + * Try to parse the header and check if we can find the expected keywords. + */ + final CharactersView view = new CharactersView(null, buffer); + try { + final Map<String, String> header = view.readHeader(); + if (header.containsKey(Store.NROWS) && header.containsKey(Store.NCOLS) && + (header.containsKey(Store.XLLCORNER) || header.containsKey(Store.XLLCENTER)) && + (header.containsKey(Store.YLLCORNER) || header.containsKey(Store.YLLCENTER))) + { +cellsize: if (!header.containsKey(Store.CELLSIZE)) { + int def = 0; + for (int i=0; i < Store.CELLSIZES.length;) { + if (header.containsKey(Store.CELLSIZES[i++])) def |= 1; + if (header.containsKey(Store.CELLSIZES[i++])) def |= 2; + if (def == 3) break cellsize; + } + return ProbeResult.UNSUPPORTED_STORAGE; + } + return new ProbeResult(true, "text/plain", null); + } + } catch (EOFException e) { + return ProbeResult.INSUFFICIENT_BYTES; + } catch (DataStoreException e) { + // Ignore and return `UNSUPPORTED_STORAGE`. + } + return ProbeResult.UNSUPPORTED_STORAGE; + }); } /** diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java index a20a161..a4399ed 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java @@ -39,37 +39,47 @@ * </tr> * <tr> * <td>{@code NCOLS}</td> - * <td>Integer</td> + * <td>{@link java.lang.Integer}</td> * <td>Mandatory</td> * </tr> * <tr> * <td>{@code NROWS}</td> - * <td>Integer</td> + * <td>{@link java.lang.Integer}</td> * <td>Mandatory</td> * </tr> * <tr> * <td>{@code XLLCORNER} or {@code XLLCENTER}</td> - * <td>Floating point</td> + * <td>{@link java.lang.Double}</td> * <td>Mandatory</td> * </tr> * <tr> * <td>{@code YLLCORNER} or {@code YLLCENTER}</td> - * <td>Floating point</td> + * <td>{@link java.lang.Double}</td> * <td>Mandatory</td> * </tr> * <tr> * <td>{@code CELLSIZE}</td> - * <td>Floating point</td> - * <td>Mandatory, unless {@code DX} and {@code DY} are present</td> + * <td>{@link java.lang.Double}</td> + * <td>Mandatory, unless an alternative below is present</td> + * </tr> + * <tr> + * <td>{@code XCELLSIZE} and {@code YCELLSIZE}</td> + * <td>{@link java.lang.Double}</td> + * <td>Non-standard alternative to {@code CELLSIZE}</td> + * </tr> + * <tr> + * <td>{@code XDIM} and {@code YDIM}</td> + * <td>{@link java.lang.Double}</td> + * <td>Non-standard alternative to {@code CELLSIZE}</td> * </tr> * <tr> * <td>{@code DX} and {@code DY}</td> - * <td>Floating point</td> - * <td>Accepted but non-standard</td> + * <td>{@link java.lang.Double}</td> + * <td>Non-standard alternative to {@code CELLSIZE}</td> * </tr> * <tr> * <td>{@code NODATA_VALUE}</td> - * <td>Floating point</td> + * <td>{@link java.lang.Double}</td> * <td>Optional</td> * </tr> * </table> @@ -87,6 +97,14 @@ * <li>Lines in the header starting with {@code '#'} are ignored as comment lines.</li> * </ul> * + * <h2>Limitations</h2> + * Current implementation loads and caches the full image no matter the subregion or subsampling + * specified to the {@code read(…)} method. The image is loaded by {@code getSampleDimensions()} + * call too, because there is no other way to build a reliable sample dimension. + * Even the data type can not be determined for sure without loading the full image. + * Loading the full image is reasonable if ASCII Grid files contain only small images, + * which is usually the case given how inefficient this format is. + * * @author Martin Desruisseaux (Geomatys) * @version 1.2 * @since 1.2 diff --git a/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider index 821d25b..7e1430d 100644 --- a/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider +++ b/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider @@ -1,4 +1,5 @@ org.apache.sis.internal.storage.xml.StoreProvider org.apache.sis.internal.storage.wkt.StoreProvider org.apache.sis.internal.storage.csv.StoreProvider +org.apache.sis.internal.storage.ascii.StoreProvider org.apache.sis.internal.storage.folder.StoreProvider