/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.image.io.plugin; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.WritableRaster; import java.io.File; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.imageio.IIOException; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.media.jai.iterator.RectIterFactory; import javax.media.jai.iterator.WritableRectIter; import com.sun.media.imageio.stream.RawImageInputStream; import org.opengis.metadata.spatial.PixelOrientation; import org.geotoolkit.image.io.TextImageReader; import org.geotoolkit.image.io.SampleConverter; import org.geotoolkit.image.io.ImageMetadataException; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.apache.sis.internal.storage.ChannelImageInputStream; import org.geotoolkit.internal.image.io.DataTypes; import org.geotoolkit.internal.image.io.DimensionAccessor; import org.geotoolkit.internal.image.io.GridDomainAccessor; import org.geotoolkit.internal.image.io.Warnings; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.resources.Errors; /** * Reader for the ASCII Grid format. As the "ASCII" name implies, the data files are read in * US-ASCII character encoding no matter what the {@link Spi#charset} value is. In addition, * the US locale is enforced no matter what the {@link Spi#locale} value is, with a tolerance * for the decimal separator character which can be either {@code '.'} or {@code ','}. * <p> * ASCII grid files contains a header before the actual data. The header contains (<var>key</var> * <var>value</var>) pairs, one pair per line and using the space as the separator between key and * value. The valid keys are listed in table below (see the * <a href="http://daac.ornl.gov/MODIS/ASCII_Grid_Format_Description.html">ASCII Grid Format * Description</a> for more details). Note that Geotk adds some extensions to the standard * ASCII grid format: * <p> * <ul> * <li>{@linkplain #isComment(String) Comment lines} and empty lines are ignored.</li> * <li>The {@code '='} and {@code ':'} characters can be used as a separator between * the keys and the values.</li> * <li>The {@code CELLSIZE} attribute can be substituted by the {@code DX} and {@code DY} * attributes. {@code DX}/{@code DY} are not standard, but can be produced by the GDAL * library. See <a href="http://www.gdal.org/frmt_various.html#AAIGrid">GDAL notes</a> * for more information.</li> * <li>The {@code "MIN_VALUE"} and {@code "MAX_VALUE"} attributes are Geotk extensions * not defined in the ASCII Grid standard. While optional, they are quite convenient * for setting the color space.</li> * </ul> * <p> * Subclasses can add their own (<var>key</var>, <var>value</var>) pairs, or modify the * ones defined below, by overriding the {@link #processHeader(Map)} method. * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"> * <th>Keyword</th> * <th>Value type</th> * <th>Obligation</th> * </tr> * <tr> * <td> {@code NCOLS} </td> * <td> Integer </td> * <td> Mandatory </td> * </tr> * <tr> * <td> {@code NROWS} </td> * <td> Integer </td> * <td> Mandatory </td> * </tr> * <tr> * <td> {@code XLLCORNER} or {@code XLLCENTER} </td> * <td> Floating point </td> * <td> Mandatory </td> * </tr> * <tr> * <td> {@code YLLCORNER} or {@code YLLCENTER} </td> * <td> Floating point </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> * </tr> * <tr> * <td> {@code DX} and {@code DY} </td> * <td> Floating point </td> * <td> Accepted but non-standard </td> * </tr> * <tr> * <td> {@code NODATA_VALUE} </td> * <td> Floating point </td> * <td> Optional </td> * </tr> * <tr> * <td> {@code MIN_VALUE} </td> * <td> Floating point </td> * <td> Optional - this is a Geotk extension </td> * </tr> * <tr> * <td> {@code MAX_VALUE} </td> * <td> Floating point </td> * <td> Optional - this is a Geotk extension </td> * </tr> * <tr> * <td> {@code BINARY_TYPE} </td> * <td> String </td> * <td> Optional - this is a Geotk extension </td> * </tr> * </table> * <p> * {@code BINARY_TYPE} is a Geotk extension provided for performance only. If this attribute * is provided and if the input is a {@link java.io.File}, {@link java.net.URL} or * {@link java.net.URI}, then {@code AsciiGridReader} will looks for a file of the same name * with the {@code ".raw"} extension. If this file is found, then the data in the ASCII file * will be ignored (they can be non-existent) and the RAW file will be read instead, which is * usually much faster. The value of the {@code BINARY_TYPE} attribute specify the data type: * {@code BYTE}, {@code SHORT}, {@code USHORT}, {@code INT}, {@code FLOAT} or {@code DOUBLE}. * * @author Martin Desruisseaux (Geomatys) * @version 3.12 * * @see <a href="http://daac.ornl.gov/MODIS/ASCII_Grid_Format_Description.html">ASCII Grid Format Description</a> * @see <a href="http://en.wikipedia.org/wiki/ESRI_grid">ESRI Grid on Wikipedia</a> * @see AsciiGridWriter * * @since 3.08 (derived from 3.07) * @module * * @todo The current implementation ignores the <code>seekForwardOnly</code> parameter. * It processes as if that parameter was always set to <code>true</code>. */ public class AsciiGridReader extends TextImageReader { /** * The size of the NIO direct buffer to create. */ private static final int BUFFER_SIZE = 16 * 1024; /** * {@code true} if the header has been read. */ private boolean headerValid; /** * The {@code NCOLS} and {@code NROWS} attributes read from the header. * Those values are valid only if {@link #headerValid} is {@code true}. */ private int width, height; /** * The {@code XLLCORNER | XLLCENTER} and {@code YLLCORNER | YLLCENTER} attributes read * from the header. Those values are valid only if {@link #headerValid} is {@code true}. */ private double xll, yll; /** * {@code true} if the {@link #xll} and {@link #yll} values are determined from the * {@code XLLCENTER} and {@code YLLCENTER} attributes, or {@code false} if they are * determined from the {@code XLLCORNER} and {@code YLLCORNER} attributes. */ private boolean xCenter, yCenter; /** * The {@code CELLSIZE} attribute, or the {@code DX} and {@code DY} attributes. * This value is valid only if {@link #headerValid} is {@code true}. */ private double scaleX, scaleY; /** * The optional {@code NODATA_VALUE} attribute, or {@code NaN} if none. * This value is valid only if {@link #headerValid} is {@code true}. */ private double fillValue; /** * The minimum and maximum values, or infinities if they are not specified. */ private double minValue, maxValue; /** * If a binary type has been specified, the corresponding {@link DataBuffer} * constant. Otherwise {@link DataBuffer#TYPE_UNDEFINED}. This information is * valid only if {@link headerValid} is {@code true}. */ private int binaryType; /** * The image reader for reading binary images, or {@code null} if not needed. * This is used only if {@link #binaryType} is defined. */ private transient ImageReader binaryReader; /** * The buffer used for data transfer. This is created only when first needed. * If more than one image is read with the same reader, this buffer will be * recycled for each image. */ private transient ByteBuffer buffer; /** * Constructs a new image reader. * * @param provider The {@link ImageReaderSpi} that is constructing this object, or {@code null}. */ protected AsciiGridReader(final Spi provider) { super(provider); } /** * Parses the given string as a double value. Before to parse, this method replaces the * {@code ','} character by {@code '.'} in order to take in account the ASCII-Grid format * created by localized version of ESRI softwares. * * @param value The value to parse. * @return The value as a {@code double}. * @throws NumberFormatException If the given string can not be parsed as a {@code double}. */ private static double parseDouble(final String value) throws NumberFormatException { return Double.parseDouble(value.replace(',', '.')); } /** * Reads the header, if it was not already read. If successful, then all the instance * variables declared in this class should be assigned to their final value. * * @throws IOException If an error occurred while reading the header. */ private void ensureHeaderRead() throws IOException { if (!headerValid) { xCenter = true; yCenter = true; final Map<String,String> header = readHeader(); processHeader(header); String key = null; try { width = Integer.parseInt(ensureDefined(key = "NCOLS", header.remove(key))); height = Integer.parseInt(ensureDefined(key = "NROWS", header.remove(key))); String value = header.remove(key = "CELLSIZE"); if (value != null) { scaleX = scaleY = parseDouble(value); } else { // If missing, declare that CELLSIZE is missing since DX and DY are not standard. scaleX = parseDouble(ensureDefined("CELLSIZE", header.remove(key = "DX"))); scaleY = parseDouble(ensureDefined("CELLSIZE", header.remove(key = "DY"))); } value = header.remove(key = "NODATA_VALUE"); fillValue = (value != null) ? parseDouble(value) : super.getPadValue(0); value = header.remove(key = "MIN_VALUE"); minValue = (value != null) ? parseDouble(value) : Double.NEGATIVE_INFINITY; value = header.remove(key = "MAX_VALUE"); maxValue = (value != null) ? parseDouble(value) : Double.POSITIVE_INFINITY; value = header.remove(key = "XLLCENTER"); if (value == null) { value = header.remove(key = "XLLCORNER"); xCenter = false; } xll = parseDouble(ensureDefined(key, value)); value = header.remove(key = "YLLCENTER"); if (value == null) { value = header.remove(key = "YLLCORNER"); yCenter = false; } yll = parseDouble(ensureDefined(key, value)); } catch (NumberFormatException cause) { throw new IIOException(Warnings.message(this, Errors.Keys.UnparsableNumber_1, key), cause); } /* * The binary format, which is a Geotk extension. */ binaryType = DataBuffer.TYPE_UNDEFINED; String value = header.remove("BINARY_TYPE"); if (value != null) { binaryType = DataTypes.getDataBufferType(value); if (binaryType == DataBuffer.TYPE_UNDEFINED) { Warnings.log(this, null, AsciiGridReader.class, "readHeader", Errors.Keys.IllegalParameterValue_2, "BINARY_TYPE", value); } } headerValid = true; /* * We should not have any entry left. */ for (final String extra : header.keySet()) { Warnings.log(this, null, AsciiGridReader.class, "readHeader", Errors.Keys.UnknownParameter_1, extra); } } } /** * Ensures that the value is non-null. Otherwise an exception is thrown using the given name. * * @param name The name of the properties to be tested. * @param value The value of the properties. * @return The given value, guaranteed to be non-null. * @throws IIOException If the given value was null. */ private String ensureDefined(final String name, final String value) throws IIOException { if (value == null || value.isEmpty()) { throw new ImageMetadataException(Warnings.message(this, Errors.Keys.NoParameter_1, name)); } return value; } /** * Reads the header from the {@linkplain #getChannel() channel}. The given buffer is used for * transferring data. The file encoding is assumed ASCII. The {@linkplain #isComment(String) * comment lines} are skipped. The separator between keys and values is assumed any of space, * {@code ':'} or {@code '='} character. The scan stop at the first line which seems to * contains a number. * * @return The header as a new, modifiable, map. * @throws IOException If an error occurred while reading. */ private Map<String,String> readHeader() throws IOException { final ReadableByteChannel channel = getChannel(); final StringBuilder stbuff = new StringBuilder(32); final Map<String,String> header = new HashMap<>(); ByteBuffer buffer = this.buffer; if (buffer == null) { this.buffer = buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); } buffer.clear(); buffer.limit(buffer.position()); // For forcing a filling of the buffer. readHeader: while (true) { final int startPos = buffer.position(); boolean skipWhitespaces = true; /* * Reads exactly one line. The buffer will be filled inside the loop. * This allow us to fill it again if only a few bytes have been read * during the first call to channel.read(buffer). */ readLine: while (true) { if (!buffer.hasRemaining()) { final int pos = buffer.position(); final int capacity = buffer.capacity(); if (pos >= capacity) { throw new ImageMetadataException(Errors.format( Errors.Keys.UnexpectedHeaderLength_1, capacity)); } /* * Arbitrary read a block of 512 bytes for starting, because the header is * typically small (less than 256 characters). If 512 bytes is not enough, * we will read more bytes up to the buffer capacity. */ buffer.limit(Math.min(capacity, Math.max(512, 2*pos))); if (channel.read(buffer) < 0) { if (skipWhitespaces) { break readHeader; } else { break readLine; } } buffer.flip().position(pos); } char c = (char) (buffer.get() & 0xFF); switch (c) { case '\n': break readLine; case '\r': { // Skip the "\n" part in "\r\n" (if any). if (buffer.hasRemaining()) { c = (char) (buffer.get() & 0xFF); if (c != '\n') { buffer.position(buffer.position() - 1); } } break readLine; } } if (skipWhitespaces) { // If the first non-blank character seems to be part of a number, stop. if (c >= '+' && c <= '9') { // Include +,-./ and digits buffer.position(startPos); break readHeader; } if (c > ' ') { skipWhitespaces = false; } } stbuff.append(c); } // At this point, a line has been read. Add it to the buffer. String line = stbuff.toString().trim(); stbuff.setLength(0); if (!line.isEmpty() && !isComment(line)) { String key = line; String value = null; final int length = line.length(); for (int i=0; i<length; i++) { char c = line.charAt(i); if (c <= ' ' || c == ':' || c == '=') { key = line.substring(0, i).toUpperCase(Locale.US); // Skip the whitespaces, if any. while (c <= ' ' && ++i <= length) { c = line.charAt(i); if (c == ':' || c == '=') { i++; // Skip the separator. break; } } value = line.substring(i).trim(); break; } } final Object old = header.put(key, value); if (old != null && !old.equals(value)) { throw new ImageMetadataException(Errors.format( Errors.Keys.ValueAlreadyDefined_1, key)); } } } return header; } /** * Returns the width in pixels of the given image within the input source. * * @param imageIndex the index of the image to be queried. * @return Image width. * @throws IOException If an error occurs reading the width information * from the input source. */ @Override public int getWidth(int imageIndex) throws IOException { checkImageIndex(imageIndex); ensureHeaderRead(); return width; } /** * Returns the height in pixels of the given image within the input source. * * @param imageIndex the index of the image to be queried. * @return Image height. * @throws IOException If an error occurs reading the width information * from the input source. */ @Override public int getHeight(int imageIndex) throws IOException { checkImageIndex(imageIndex); ensureHeaderRead(); return height; } /** * Returns the data type which most closely represents the "raw" internal data of the image. * If a {@code "BINARY_TYPE"} attribute is presents in the header, then the code corresponding * to that attribute is returned. Otherwise {@link DataBuffer#TYPE_FLOAT} is returned. * * @param imageIndex The index of the image to be queried. * @return The data type ({@link DataBuffer#TYPE_FLOAT} by default). * @throws IOException If an error occurs reading the format information from the input source. */ @Override protected int getRawDataType(int imageIndex) throws IOException { checkImageIndex(imageIndex); ensureHeaderRead(); return (binaryType != DataBuffer.TYPE_UNDEFINED) ? binaryType : DataBuffer.TYPE_FLOAT; } /** * Returns metadata associated with the given image. * * @param imageIndex The image index. * @return The metadata, or {@code null} if none. * @throws IOException If an error occurs reading the data information from the input source. */ @Override protected SpatialMetadata createMetadata(final int imageIndex) throws IOException { if (imageIndex < 0) { // Stream metadata. return null; } ensureHeaderRead(); final PixelOrientation po; if (xCenter) { po = yCenter ? PixelOrientation.CENTER : PixelOrientation.valueOf("UPPER"); } else { po = yCenter ? PixelOrientation.valueOf("LEFT") : PixelOrientation.UPPER_LEFT; // We really want UPPER_LEFT, not LOWER_LEFT in the above condition, because // we are reverting the direction of the y axis in the computation of origin // and offset vectors. } final double[] origin = new double[] {xll, yll + scaleX * (height - (yCenter ? 1 : 0))}; final double[] bounds = new double[] {xll + scaleY * (width - (xCenter ? 1 : 0)), yll}; final SpatialMetadata metadata = new SpatialMetadata(false, this, null); final GridDomainAccessor domain = new GridDomainAccessor(metadata); domain.setOrigin(origin); domain.addOffsetVector(scaleX, 0); domain.addOffsetVector(0, -scaleY); domain.setLimits(new int[2], new int[] {width-1, height-1}); domain.setSpatialRepresentation(origin, bounds, null, po); final boolean hasRange = !Double.isInfinite(minValue) && !Double.isInfinite(maxValue); final boolean hasFill = !Double.isNaN(fillValue); if (hasRange || hasFill) { final DimensionAccessor dimensions = new DimensionAccessor(metadata); dimensions.selectChild(dimensions.appendChild()); if (hasRange) dimensions.setValueRange(minValue, maxValue); if (hasFill) dimensions.setFillSampleValues(fillValue); } return metadata; } /** * Invoked automatically after the (<var>key</var>, <var>value</var>) pairs in the header * have been read. Subclasses can override this method in order to modify the map passed * in argument. They can freely add, remove of modify values. * <p> * Keys shall be upper-case, and the mandatory attributes defined in the * <a href="#skip-navbar_top">class javadoc</a> shall be present in the {@code header} map * after the completion of this method. * <p> * The default implementation does nothing. * * @param header A modifiable map of (<var>key</var>, <var>value</var>) pairs. * @throws IOException If an error occurred while processing the header values. */ protected void processHeader(final Map<String,String> header) throws IOException { } /** * Reads the image indexed by {@code imageIndex}. * * @param imageIndex The index of the image to be retrieved. * @param param Parameters used to control the reading process, or null. * @return The desired portion of the image. * @throws IOException if an input operation failed. */ @Override public BufferedImage read(int imageIndex, final ImageReadParam param) throws IOException { clearAbortRequest(); checkImageIndex(imageIndex); processImageStarted(imageIndex); ensureHeaderRead(); if (binaryType != DataBuffer.TYPE_UNDEFINED) { /* * Optional Geotk extension: if a binary file is present, reads * that file instead than the ASCII file. This is much faster. */ final BufferedImage image = readBinary(imageIndex, param); if (image != null) { return image; } } /* * Parameters check. */ final int numSrcBands = 1; // To be modified in a future version if we support multi-bands. /* * Extract user's parameters. */ final int[] sourceBands; // To be used in a future version if we support multi-bands. final int[] destinationBands; final int sourceXSubsampling; final int sourceYSubsampling; if (param != null) { sourceBands = param.getSourceBands(); destinationBands = param.getDestinationBands(); sourceXSubsampling = param.getSourceXSubsampling(); sourceYSubsampling = param.getSourceYSubsampling(); } else { sourceBands = null; destinationBands = null; sourceXSubsampling = 1; sourceYSubsampling = 1; } final int width = this.width; final int height = this.height; final int numDstBands = (destinationBands != null) ? destinationBands.length : numSrcBands; final SampleConverter[] converters = new SampleConverter[numDstBands]; final BufferedImage image = getDestination(imageIndex, param, width, height, converters); final WritableRaster raster = image.getRaster(); checkReadParamBandSettings(param, numSrcBands, raster.getNumBands()); final Rectangle srcRegion = new Rectangle(); final Rectangle dstRegion = new Rectangle(); computeRegions(param, width, height, image, srcRegion, dstRegion); final WritableRectIter iter = RectIterFactory.createWritable(raster, dstRegion); final int dstBand = (destinationBands != null) ? destinationBands[0] : 0; for (int i=dstBand; --i>=0;) { if (iter.nextBandDone()) { throw new IIOException(Errors.format(Errors.Keys.IllegalBandNumber_1, dstBand)); } } if (!iter.finishedBands() && !iter.finishedLines() && !iter.finishedPixels()) { final int dataType = raster.getSampleModel().getDataType(); final char[] charBuffer = new char[48]; // Arbitrary length limit for a sample value. final ByteBuffer buffer = this.buffer; final ReadableByteChannel channel = getChannel(); final SampleConverter converter = converters[0]; /* * Before to start reading, set the 'minIndex' in order to prevent new attempt * to read an image from this point. * * TODO: We should mark the position instead if 'seekForwardOnly' is false. */ minIndex = imageIndex + 1; /* * At this point we have all the metadata needed for reading the sample values. * The (x,y) index below are relative to the source region to read, not to the * source image. */ final float progressScale = 100f / ((srcRegion.x + srcRegion.height) * width); int sy = 1 + srcRegion.y; loop: for (int y=0; /* stop condition inside */; y++) { if (abortRequested()) { processReadAborted(); return image; } boolean isValid = (--sy == 0); if (isValid) { sy = sourceYSubsampling; } int sx = 1 + srcRegion.x; for (int x=0; x<width; x++) { /* * Skip whitespaces or EOL (if any), then copy the next character in the * string buffer until the next space. If we are outside the region to be * read, those characters will be discarded immediately except in case of * error. */ int nChar = 0; while (true) { if (!buffer.hasRemaining()) { processImageProgress((y*width + x) * progressScale); buffer.clear(); if (channel.read(buffer) < 0) { throw new EOFException(Errors.format(Errors.Keys.EndOfDataFile)); } buffer.flip(); } char c = (char) (buffer.get() & 0xFF); if (c > ' ') { if (nChar >= charBuffer.length) { throw new IIOException(Warnings.message(this, Errors.Keys.IllegalParameterValue_2, "cell(" + x + ',' + y + ')', String.valueOf(charBuffer))); } if (c == ',') c = '.'; charBuffer[nChar++] = c; } else if (nChar != 0) { break; } } /* * At this point the sample values is available as a string. * Process only if we need to parse that string. */ if (isValid && --sx == 0) { sx = sourceXSubsampling; final String value = new String(charBuffer, 0, nChar); try { switch (dataType) { case DataBuffer.TYPE_DOUBLE: { iter.setSample(converter.convert(Double.parseDouble(value))); break; } case DataBuffer.TYPE_FLOAT: { iter.setSample(converter.convert(Float.parseFloat(value))); break; } default: { iter.setSample(converter.convert(Integer.parseInt(value))); break; } } } catch (NumberFormatException cause) { throw new IIOException(Warnings.message(this, Errors.Keys.UnparsableNumber_1, value), cause); } /* * Move to the next pixel in the destination image. The reading process * will stop when we have reached the last pixel (which may be sooner * than the end of the current row in the input image). */ if (iter.nextPixelDone()) { if (iter.nextLineDone()) { break loop; } iter.startPixels(); isValid = false; } } } /* * At this point we finished to parse a line. 'isValid' should always be false. * If not, then 'dstRegion' computation was probably inaccurate. */ assert !isValid : dstRegion; } } processImageComplete(); return image; } /** * Reads the binary file associated with the ASCII file. This is a Geotk extension * enabled only if the {@code "BINARY_TYPE"} attribute is present. * <p> * Note that this method reuses the existing {@linkplain #buffer}. Consequently, if this * method returns a non-null image, then any previous content of the buffer is lost. If * this method returns {@code null}, then the previous content still valid. * * @param input The file, URL or URI to the binary file. * @param param The parameter of the image to be read. * @return The image, or {@code null} if this method can not process. * @throws IOException If an error occurred while reading the binary file. */ private BufferedImage readBinary(final int imageIndex, final ImageReadParam param) throws IOException { Object binaryInput = IOUtilities.changeExtension(input, "raw"); if (binaryInput == null || binaryInput == input) { // The input type is unknown, or the extension is already "raw". return null; } /* * The binary file is optional. In the particular case of File input, * we perform a test cheaper than the attempt to open the connection. * We also check for the existence of the RAW image reader before to * attempt to open the connection. */ if (binaryInput instanceof File) { final File file = (File) binaryInput; if (!file.isFile() || !file.canRead()) { return null; } } ImageReader binaryReader = this.binaryReader; if (binaryReader == null) { this.binaryReader = binaryReader = new RawReader(null); } final InputStream binaryStream; try { binaryStream = IOUtilities.open(binaryInput); } catch (IOException e) { Warnings.log(this, null, AsciiGridReader.class, "readBinary", e); return null; } /* * At this point we have successfully opened a connection to the binary stream. * Make the buffer empty before to use it. Now we are not allowed to return null * anymore since we have destroyed the previous buffer content. */ final BufferedImage image; try { buffer.clear().limit(0); final ImageInputStream in = new ChannelImageInputStream(IOUtilities.filename(input), Channels.newChannel(binaryStream), buffer, true); try (RawImageInputStream rawStream = new RawImageInputStream(in, getRawImageType(imageIndex), new long[1], new Dimension[] {new Dimension(width, height)})) { binaryReader.setInput(rawStream, true, true); image = binaryReader.read(imageIndex, param); } } finally { binaryStream.close(); binaryReader.reset(); } return image; } /** * The reader to use for decoding the RAW file that may be provided together with the * ASCII image file. This is created by {@link AsciiGridReader#readBinary} only if needed. * * @author Martin Desruisseaux (Geomatys) * @version 3.07 * * @since 3.07 * @module */ private final class RawReader extends RawImageReader { /** * Creates a new reader. The provider is set to the ASCII grid reader provider. */ RawReader(final Spi provider) { super(provider); } /** * Delegates to the stream metadata of the enclosing {@link AsciiGridReader}. * This is provided only for completness with {@link #getImageMetadata(int)}. */ @Override public SpatialMetadata getStreamMetadata() throws IOException { return AsciiGridReader.this.getStreamMetadata(); } /** * Delegates to the image metadata of the enclosing {@link AsciiGridReader}. * This is necessary for allowing {@link SpatialImageReader#getDestination} * to build the same color model than what it would have done if we were * reading with the normal ASCII reader. */ @Override public SpatialMetadata getImageMetadata(final int imageIndex) throws IOException { return AsciiGridReader.this.getImageMetadata(imageIndex); } /** * Forwards to the enclosing image reader. Note: we do not forward * {@code processImageStarted()} because {@link AsciiGridReader#read} * has already sent this notification. */ @Override protected void processImageComplete() { AsciiGridReader.this.processImageComplete(); } /** * Forwards to the enclosing image reader. */ @Override protected void processImageProgress(final float percentageDone) { AsciiGridReader.this.processImageProgress(percentageDone); } /** * Forwards to the enclosing image reader. */ @Override protected void processReadAborted() { AsciiGridReader.this.processReadAborted(); } } /** * Closes the input stream created by this reader as documented in the * {@linkplain org.geotoolkit.image.io.StreamImageReader#close() super-class method}. * If an input stream was created for reading the data from a RAW file, it is also closed. */ @Override protected void close() throws IOException { headerValid = false; super.close(); // First in order to make sure that it is always executed. final ImageReader br = binaryReader; if (br != null) { br.reset(); } } /** * Allows any resources held by this reader to be released. */ @Override public void dispose() { buffer = null; final ImageReader br = binaryReader; if (br != null) { binaryReader = null; br.dispose(); } super.dispose(); } /** * Service provider interface (SPI) for {@code AsciiGridReader}s. This SPI provides * the necessary implementation for creating default {@link AsciiGridReader}s using * US locale and ASCII character set. The {@linkplain #locale locale} and * {@linkplain #charset charset} fields are ignored by the default implementation. * <p> * The default constructor initializes the fields to the values listed below. * Users wanting different values should create a subclass of {@code Spi} and * set the desired values in their constructor. * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"><th>Field</th><th>Value</th></tr> * <tr><td> {@link #names}  </td><td> {@code "ascii-grid"} </td></tr> * <tr><td> {@link #MIMETypes}  </td><td> {@code "text/plain"}, {@code "text/x-ascii-grid"} </td></tr> * <tr><td> {@link #pluginClassName}  </td><td> {@code "org.geotoolkit.image.io.plugin.AsciiGridReader"} </td></tr> * <tr><td> {@link #vendorName}  </td><td> {@code "Geotoolkit.org"} </td></tr> * <tr><td> {@link #version}  </td><td> Value of {@link org.geotoolkit.util.Version#GEOTOOLKIT} </td></tr> * <tr><td> {@link #locale}  </td><td> {@link Locale#US} </td></tr> * <tr><td> {@link #charset}  </td><td> {@code "US-ASCII"} </td></tr> * <tr><td colspan="2" align="center">See * {@linkplain org.geotoolkit.image.io.TextImageReader.Spi super-class javadoc} for remaining fields</td></tr> * </table> * * @author Martin Desruisseaux (Geomatys) * @version 3.08 * * @see AsciiGridWriter.Spi * * @since 3.08 (derived from 3.07) * @module */ public static class Spi extends TextImageReader.Spi { /** * The format names for the default {@link AsciiGridReader} configuration. */ static final String[] NAMES = {"ASCII-Grid", "ascii-grid"}; /** * The file suffixes. This replace the {@link TextImageReader.Spi#SUFFIXES} declared * in the parent class. */ static final String[] SUFFIXES = {"asc", "ASC", "grd", "GRD", "agr", "AGR"}; /** * The mime types for the default {@link AsciiGridReader} configuration. */ static final String[] MIME_TYPES = {"text/plain", "text/x-ascii-grid"}; /** * The provider of the corresponding image writer. */ private static final String[] WRITERS = {"org.geotoolkit.image.io.plugin.AsciiGridWriter$Spi"}; /** * Constructs a default {@code AsciiGridReader.Spi}. The fields are initialized as * documented in the <a href="#skip-navbar_top">class javadoc</a>. Subclasses can * modify those values if desired. * <p> * For efficiency reasons, the fields are initialized to shared arrays. * Subclasses can assign new arrays, but should not modify the default array content. */ public Spi() { names = NAMES; suffixes = SUFFIXES; MIMETypes = MIME_TYPES; pluginClassName = "org.geotoolkit.image.io.plugin.AsciiGridReader"; writerSpiNames = WRITERS; locale = Locale.US; charset = Charset.forName("US-ASCII"); nativeStreamMetadataFormatName = null; // No stream metadata. } /** * Returns a brief, human-readable description of this service provider * and its associated implementation. The resulting string should be * localized for the supplied locale, if possible. * * @param locale A Locale for which the return value should be localized. * @return A String containing a description of this service provider. */ @Override public String getDescription(final Locale locale) { return "ASCII grid"; } /** * Returns an instance of the {@code ImageReader} implementation associated * with this service provider. * * @param extension An optional extension object, which may be null. * @return An image reader instance. * @throws IOException if the attempt to instantiate the reader fails. */ @Override public ImageReader createReaderInstance(final Object extension) throws IOException { return new AsciiGridReader(this); } /** * Returns {@code true} if the given set of keywords contains at least the * {@code "NCOLS"} and {@code "NROWS"} elements. */ @Override protected boolean isValidHeader(final Set<String> keywords) { return keywords.contains("NCOLS") && keywords.contains("NROWS") && (keywords.contains("XLLCORNER") || keywords.contains("XLLCENTER")) && (keywords.contains("YLLCORNER") || keywords.contains("YLLCENTER")) && (keywords.contains("CELLSIZE") || (keywords.contains("DX") && keywords.contains("DY"))); } /** * Returns {@code true} unconditionally, because ASCII grid files don't require lines * of same length. This method returns {@code true} even if there is no data at all, * because those data can be stored in a separated RAW file (this behavior is a Geotk * extension) */ @Override protected boolean isValidContent(final double[][] rows) { return true; } } }