/* * 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.geom.AffineTransform; import java.awt.geom.Dimension2D; import java.awt.image.DataBuffer; import java.util.Locale; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.Charset; import java.util.Map; import java.util.List; import java.util.LinkedHashMap; import javax.imageio.IIOImage; import javax.imageio.IIOException; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageWriterSpi; import javax.media.jai.iterator.RectIter; import org.opengis.coverage.grid.RectifiedGrid; import org.opengis.metadata.spatial.Georectified; import org.opengis.metadata.spatial.PixelOrientation; import org.apache.sis.util.CharSequences; import org.geotoolkit.image.ImageDimension; import org.geotoolkit.image.io.TextImageWriter; import org.geotoolkit.image.io.ImageMetadataException; import org.geotoolkit.image.io.metadata.MetadataHelper; import org.geotoolkit.image.io.metadata.SampleDimension; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.internal.image.io.Warnings; import org.geotoolkit.resources.Errors; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; /** * Writer for the ASCII Grid format. As the "ASCII" name implies, the data file are written 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. The default implementation * writes only the header attribute defined below: * <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 allowed. </td> * </tr> * <tr> * <td> {@code DX} and {@code DY} </td> * <td> Floating point </td> * <td> Forbidden if {@linkplain #setStrictCellSize strict cell size} * has been set to {@code false} </td> * </tr> * <tr> * <td> {@code NODATA_VALUE} </td> * <td> Floating point </td> * <td> Optional, default to -9999 </td> * </tr> * </table> * <p> * The {@code DX} and {@code DY} attributes are non-standard, but recognized by the GDAL library * and Golden Surfer as <a href="http://www.gdal.org/frmt_various.html#AAIGrid">documented here</a>. * The default {@code AsciiGridWriter} behavior is to use those parameters if the image has rectangular * pixels, unless <code>{@linkplain #setStrictCellSize(boolean) setStrictCellSize}(true)</code> is * invoked. * * @author Martin Desruisseaux (Geomatys) * @version 3.08 * * @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 AsciiGridReader * * @since 3.08 (derived from 3.07) * @module */ public class AsciiGridWriter extends TextImageWriter { /** * The default fill value. This is part of the ASCII grid format specification. */ private static final String DEFAULT_FILL = "-9999"; /** * {@code true} if attempts to write an image with non-square pixels should throw an * exception, or {@code false} for allowing the use of the {@code DX} and {@code DY} * in such case. */ private boolean strictCellSize; /** * Constructs a new image writer. * * @param provider The {@link ImageWriterSpi} that is constructing this object, or {@code null}. */ protected AsciiGridWriter(final Spi provider) { super(provider); } /** * Sets whatever the policy about the {@code CELLSIZE} attribute is to be strict. * If {@code true}, attempts to write an image with non-square pixels will throw an * exception. If {@code false} (the default) and an image has rectangular pixels, then * the {@code DX} and {@code DY} attributes will be used instead of {@code CELLSIZE} * and a warning will be emitted. * <p> * The {@code DX} and {@code DY} attributes are non-standard, but recognized by the GDAL * library and Golden Surfer. The default value is {@code false}, thus allowing creation * of non-standard ASCII grid file. * * @param strict {@code true} if attempts to write an image with non-square pixels should * throw an exception, or {@code false} for emitting a warning instead. */ public void setStrictCellSize(final boolean strict) { strictCellSize = strict; } /** * Returns the value set by the last call to {@link #setStrictCellSize(boolean)}. * The default value is {@code false}. * * @return {@code true} if attempts to write an image with non-square pixels should * throw an exception, or {@code false} for emitting a warning instead. */ public boolean getStrictCellSize() { return strictCellSize; } /** * Fills the given {@code header} map with values extracted from the given image metadata. * The {@code "NCOLS"} and {@code "NROWS"} attributes are already defined when this method * is invoked. This method is responsible for filling the remaining attributes. * * @param metadata The metadata. * @param header The map in which to store the (<var>key</var>, <var>value</var>) pairs * to be written. * @return The fill value, or {@code Double#NaN} if none. * @throws IOException If the metadata can not be prepared. */ private String prepareHeader(final SpatialMetadata metadata, final Map<String,String> header, final ImageWriteParam param) throws IOException { final MetadataHelper helper = new MetadataHelper(this); final Georectified spatialRp = metadata.getInstanceForType(Georectified .class); final RectifiedGrid domain = metadata.getInstanceForType(RectifiedGrid.class); final PixelOrientation ptInPixel = (spatialRp != null) ? spatialRp.getPointInPixel() : null; final AffineTransform gridToCRS = helper.getAffineTransform(domain, param); String xll = "XLLCORNER"; String yll = "YLLCORNER"; // Test UPPER_LEFT corder, not LOWER_LEFT, because the Y axis has been // reverted (i.e. the corresponding value in OffsetVectors is negative). if (ptInPixel != null && !ptInPixel.equals(PixelOrientation.UPPER_LEFT)) { if (ptInPixel.equals(PixelOrientation.CENTER)) { xll = "XLLCENTER"; yll = "YLLCENTER"; } else if (ptInPixel.equals(PixelOrientation.valueOf("UPPER"))) { yll = "YLLCENTER"; } else if (ptInPixel.equals(PixelOrientation.valueOf("LEFT"))) { xll = "XLLCENTER"; } else { throw new ImageMetadataException(Warnings.message(this, Errors.Keys.IllegalParameterValue_2, "pointInPixel", ptInPixel)); } } header.put(xll, String.valueOf(gridToCRS.getTranslateX())); header.put(yll, String.valueOf(gridToCRS.getTranslateY())); /* * Use the CELLSIZE attribute if the pixels are square, or the DX, DY attibutes * if they are rectangular and we are allowed to use those non-standard attributes. */ try { header.put("CELLSIZE", String.valueOf(helper.getCellSize(gridToCRS))); } catch (IIOException e) { final Dimension2D size; if (strictCellSize || (size = helper.getCellDimension(gridToCRS)) == null) { throw e; } Warnings.log(this, null, AsciiGridWriter.class, "writeHeader", e); header.put("DX", String.valueOf(size.getWidth())); header.put("DY", String.valueOf(size.getHeight())); } /* * Get the fill sample value, which is optional. The default defined by * the ASCII grid format is -9999. */ String fillValue = DEFAULT_FILL; final List<SampleDimension> dimensions = metadata.getListForType(SampleDimension.class); if (!isNullOrEmpty(dimensions)) { final SampleDimension dim = dimensions.get(0); if (dim != null) { final double[] fillValues = dim.getFillSampleValues(); if (fillValues != null && fillValues.length != 0) { final double value = fillValues[0]; if (!Double.isNaN(value)) { fillValue = CharSequences.trimFractionalPart(String.valueOf(value)).toString(); header.put("NODATA_VALUE", fillValue); } } } } return fillValue; } /** * Invoked by the {@link #write write} method for appending the header to the output * stream. Subclasses can override this method in order to modify the header content. * The {@code header} map given in argument can be freely modified. * * @param header The content of the header to be written. * @param out The streal where to write the header. * @throws IOException If an error occurred while writing the header. * * @todo Overriding not yet allowed. We are waiting to see if this API is really appropriate. */ private void writeHeader(final Map<String,String> header, final BufferedWriter out) throws IOException { int length = 0; for (final String key : header.keySet()) { final int lg = key.length(); if (lg > length) { length = lg; } } boolean first = true; // Do not write the line separator for the first line. for (final Map.Entry<String,String> entry : header.entrySet()) { if (!first) { out.write('\n'); } first = false; final String key = entry.getKey(); out.write(key); out.append(CharSequences.spaces(2 + Math.max(0, length - key.length()))); out.write(entry.getValue()); } // We intentionally omit the line separator for the last line, // because the write(...) method below will add it itself. } /** * Appends a complete image stream containing a single image. * * @param streamMetadata The stream metadata (ignored in default implementation). * @param image The image or raster to be written. * @param parameters The write parameters, or null if the whole image will be written. * @throws IOException If an error occurred while writing to the stream. */ @Override public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam parameters) throws IOException { processImageStarted(); final BufferedWriter out = getWriter(parameters); final ImageDimension size = computeSize(image, parameters); /* * Write the header. */ final Map<String,String> header = new LinkedHashMap<>(8); header.put("NCOLS", String.valueOf(size.width )); header.put("NROWS", String.valueOf(size.height)); final SpatialMetadata metadata = convertImageMetadata(image.getMetadata(), null, parameters); String fillValue = DEFAULT_FILL; if (metadata != null) { fillValue = prepareHeader(metadata, header, parameters); } writeHeader(header, out); /* * Write the pixel values. */ final RectIter iterator = createRectIter(image, parameters); final int dataType = getSampleModel(image, parameters).getDataType(); final float progressScale = 100f / size.getNumSampleValues(); int numSampleValues = 0, nextProgress = 0; boolean moreBands = false; if (!iterator.finishedBands()) do { if (moreBands) { out.write('\n'); // Separate bands by a blank line. } if (!iterator.finishedLines()) do { if (numSampleValues >= nextProgress) { // Informs about progress only every 2000 numbers. processImageProgress(progressScale * numSampleValues); nextProgress = numSampleValues + 2000; } if (abortRequested()) { processWriteAborted(); return; } char separator = '\n'; if (!iterator.finishedPixels()) do { final String value; switch (dataType) { case DataBuffer.TYPE_DOUBLE: { final double v = iterator.getSampleDouble(); value = Double.isNaN(v) ? fillValue : Double.toString(v); break; } case DataBuffer.TYPE_FLOAT: { final float v = iterator.getSampleFloat(); value = Float.isNaN(v) ? fillValue : Float.toString(v); break; } default: { value = Integer.toString(iterator.getSample()); break; } case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_BYTE: { value = Integer.toString(iterator.getSample() & 0x7FFFFFFF); break; } } out.write(separator); out.write(value); separator = ' '; } while (!iterator.nextPixelDone()); numSampleValues += size.width; iterator.startPixels(); } while (!iterator.nextLineDone()); iterator.startLines(); moreBands = true; } while (!iterator.nextBandDone()); out.write('\n'); out.flush(); processImageComplete(); } /** * Service provider interface (SPI) for {@code AsciiGridWriter}s. This SPI provides * the necessary implementation for creating default {@link AsciiGridWriter}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.AsciiGridWriter"} </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> {@link #lineSeparator}  </td><td> {@code "\n"} </td></tr> * <tr><td colspan="2" align="center">See * {@linkplain org.geotoolkit.image.io.TextImageWriter.Spi super-class javadoc} for remaining fields</td></tr> * </table> * * @author Martin Desruisseaux (Geomatys) * @version 3.08 * * @see AsciiGridReader.Spi * * @since 3.08 (derived from 3.07) * @module */ public static class Spi extends TextImageWriter.Spi { /** * The provider of the corresponding image reader. */ private static final String[] READERS = {"org.geotoolkit.image.io.plugin.AsciiGridReader$Spi"}; /** * Constructs a default {@code AsciiGridWriter.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 above fields are initialized to shared arrays. * Subclasses can assign new arrays, but should not modify the default array content. */ public Spi() { names = AsciiGridReader.Spi.NAMES; MIMETypes = AsciiGridReader.Spi.MIME_TYPES; pluginClassName = "org.geotoolkit.image.io.plugin.AsciiGridWriter"; readerSpiNames = READERS; locale = Locale.US; charset = Charset.forName("US-ASCII"); lineSeparator = "\n"; 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 ImageWriter} implementation associated * with this service provider. * * @param extension An optional extension object, which may be null. * @return An image writer instance. * @throws IOException if the attempt to instantiate the writer fails. */ @Override public ImageWriter createWriterInstance(final Object extension) throws IOException { return new AsciiGridWriter(this); } } }