/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.image.io.text; import java.io.*; // Many imports, including some for javadoc only. import java.nio.charset.Charset; import java.net.URL; import java.net.URLConnection; import java.util.Locale; import java.text.NumberFormat; import java.text.DecimalFormat; import java.text.FieldPosition; import java.awt.image.DataBuffer; import javax.imageio.IIOImage; import javax.imageio.ImageWriteParam; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.stream.ImageOutputStream; import javax.media.jai.iterator.RectIter; import org.geotools.image.io.StreamImageWriter; /** * Base class for text image encoders. "Text images" are usually ASCII files * containing pixel values (often geophysical values, like sea level anomalies). * * @since 2.4 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public abstract class TextImageWriter extends StreamImageWriter { /** * Maximum number of digits to be allowed by {@link #createNumberFormat}. */ private static final int MAXIMUM_DIGITS = 12; /** * Do not force the formatting of fraction digits if the sample values are equal or * greater than 1E+6. */ private static final double NODIGITS_THRESHOLD = 1E+6; /** * Number of digits to check after the last one, as 10<sup>-n</sup>. The default value is * 1E-1. This means that if the digit immediately after the last one is 0, we will consider * that we have reached the intented precision. * * For adjusting the number of digits to check after the last one, just put this number * as a negative power in place of the "-1" above. */ private static final double DELTA_THRESHOLD = 1E-1; /** * The maximum value found during the last call to {@link #createNumberFormat}. */ private double maximum; /** * {@link #output} as a writer, or {@code null} if none. * * @see #getWriter */ private BufferedWriter writer; /** * Constructs a {@code TextImageWriter}. * * @param originatingProvider The {@code ImageWriterSpi} that * is constructing this object, or {@code null}. */ protected TextImageWriter(final ImageWriterSpi provider) { super(provider); } /** * Returns the locale to use for encoding values, or {@code null} for the * {@linkplain Locale#getDefault default}. The default implementation returns the * {@linkplain Spi#locale locale} specified to the {@link Spi} object given to this * {@code TextImageWriter} constructor. Subclasses can override this method if they * want to specify the data locale in some other way. * <p> * <b>Note:</b> This locale should not be confused with {@link #getLocale}. * * @param parameters The write parameters, or {@code null} for the defaults. * @return The locale to use for parsing numbers in the image file. * * @see Spi#locale */ protected Locale getDataLocale(final ImageWriteParam parameters) { return (originatingProvider instanceof Spi) ? ((Spi)originatingProvider).locale : null; } /** * Returns the character set to use for encoding the string to the output stream. * The default implementation returns the {@linkplain Spi#charset character set} * specified to the {@link Spi} object given to this {@code TextImageWriter} constructor. * Subclasses can override this method if they want to specify the character encoding in * some other way. * * @param parameters The write parameters, or {@code null} for the defaults. * @return The character encoding, or {@code null} for the platform default encoding. * @throws IOException If reading from the output stream failed. * * @see Spi#charset */ protected Charset getCharset(final ImageWriteParam parameters) throws IOException { return (originatingProvider instanceof Spi) ? ((Spi)originatingProvider).charset : null; } /** * Returns the line separator to use when writing to the output stream. The default * implementation returns the {@linkplain Spi#lineSeparator line separator} specified * to the {@link Spi} object given to this {@code TextImageWriter} constructor. Subclasses * can override this method if they want to specify the line separator in some other way. * * @param parameters The write parameters, or {@code null} for the defaults. * @return The line separator to use for writting the image. * * @see Spi#lineSeparator */ protected String getLineSeparator(final ImageWriteParam parameters) { if (originatingProvider instanceof Spi) { final String lineSeparator = ((Spi)originatingProvider).lineSeparator; if (lineSeparator != null) { return lineSeparator; } } return System.getProperty("line.separator", "\n"); } /** * Returns the {@linkplain #output output} as an {@linkplain BufferedWriter buffered writer}. * If the output is already a buffered writer, it is returned unchanged. Otherwise this method * creates a new {@linkplain BufferedWriter buffered writer} from various output types * including {@link File}, {@link URL}, {@link URLConnection}, {@link Writer}, * {@link OutputStream} and {@link ImageOutputStream}. * <p> * This method creates a new {@linkplain BufferedWriter writer} only when first invoked. * All subsequent calls will returns the same instance. Consequently, the returned writer * should never be closed by the caller. It may be {@linkplain #close closed} automatically * when {@link #setOutput setOutput(Object)}, {@link #reset() reset()} or {@link #dispose() * dispose()} methods are invoked. * * @param parameters The write parameters, or {@code null} for the defaults. * @return {@link #getOutput} as a {@link BufferedWriter}. * @throws IllegalStateException if the {@linkplain #output output} is not set. * @throws IOException If the output stream can't be created for an other reason. * * @see #getOutput * @see #getOutputStream */ protected BufferedWriter getWriter(final ImageWriteParam parameters) throws IllegalStateException, IOException { if (writer == null) { final Object output = getOutput(); if (output instanceof BufferedWriter) { writer = (BufferedWriter) output; closeOnReset = null; // We don't own the underlying writer, so don't close it. } else if (output instanceof Writer) { writer = new BufferedWriter((Writer) output); closeOnReset = null; // We don't own the underlying writer, so don't close it. } else { final OutputStream stream = getOutputStream(); final Charset charset = getCharset(parameters); writer = new BufferedWriter((charset != null) ? new OutputStreamWriter(stream, charset) : new OutputStreamWriter(stream)); if (closeOnReset == stream) { closeOnReset = writer; } } } return writer; } /** * Returns a number format to be used for formatting the sample values in the given image. * * @param image The image or raster to be written. * @param parameters The write parameters, or {@code null} if the whole image will be written. * @return A number format appropriate for the given image. */ protected strictfp NumberFormat createNumberFormat(final IIOImage image, final ImageWriteParam parameters) { final Locale locale = getDataLocale(parameters); final int type = image.hasRaster() ? image.getRaster().getTransferType() : image.getRenderedImage().getSampleModel().getDataType(); if (type != DataBuffer.TYPE_FLOAT && type != DataBuffer.TYPE_DOUBLE) { maximum = Short.MAX_VALUE; // TODO: This is not really accurate... return (locale != null) ? NumberFormat.getIntegerInstance(locale) : NumberFormat.getIntegerInstance(); } int digits = 0; double multiple = 1; maximum = Double.NEGATIVE_INFINITY; final RectIter iterator = createRectIter(image, parameters); if (!iterator.finishedBands()) do { if (!iterator.finishedLines()) do { if (!iterator.finishedPixels()) do { final double value = Math.abs(iterator.getSampleDouble()); if (Double.isInfinite(value)) { continue; } // Following code is NaN tolerant - no need for explicit check. if (value > maximum) { maximum = value; } while (true) { double scaled = value * multiple; if (type == DataBuffer.TYPE_FLOAT) { scaled = (float) scaled; // Drops the extra digits. } // Condition below uses '!' in order to cath NaN values. if (!(StrictMath.abs(scaled - StrictMath.rint(scaled)) >= DELTA_THRESHOLD)) { break; } if (++digits > MAXIMUM_DIGITS) { return NumberFormat.getNumberInstance(locale); } multiple *= 10; } } while (!iterator.nextPixelDone()); iterator.startPixels(); } while (!iterator.nextLineDone()); iterator.startLines(); } while (!iterator.nextBandDone()); /* * 'digits' should now be the exact number of fraction digits to format. However the above * algorithm do not work if all values are smaller (in absolute value) to DELTA_THRESHOLD, * in which case 'digits' is still set to 0. In such case it is better to keep the default * format unchanged, since it should be generic enough. */ final NumberFormat format = (locale != null) ? NumberFormat.getNumberInstance(locale) : NumberFormat.getNumberInstance(); if (digits != 0 || maximum >= DELTA_THRESHOLD) { format.setMaximumFractionDigits(digits); if (maximum < NODIGITS_THRESHOLD) { format.setMinimumFractionDigits(digits); } } return format; } /** * Returns the expected position of the fraction part for numbers to be formatted using the * given format. This method should be invoked after {@link #createNumberFormat}, but the * given format doesn't need to be the instance returned by the later. * * @param format The format to be used for formatting numbers. * @return The expected position of the fraction part. * * @todo I don't really kwon what to do if the format is not an instance of * {@link DecimalFormat}... */ protected FieldPosition getExpectedFractionPosition(final NumberFormat format) { int width = Math.max((int) Math.floor(Math.log10(maximum)) + 1, format.getMinimumIntegerDigits()); int digits = Math.min(format.getMaximumFractionDigits(), MAXIMUM_DIGITS); if (format instanceof DecimalFormat) { final DecimalFormat decimal = (DecimalFormat) format; if (digits>0 || decimal.isDecimalSeparatorAlwaysShown()) { width++; } width += Math.max(decimal.getNegativePrefix().length(), decimal.getPositivePrefix().length()); digits += Math.max(decimal.getNegativeSuffix().length(), decimal.getPositiveSuffix().length()); } final FieldPosition position = new FieldPosition(NumberFormat.FRACTION_FIELD); position.setBeginIndex(width); position.setEndIndex(width += digits); // 'width' is now the full width. We don't do anything with it at this time, // but maybe in some future version... return position; } /** * Closes the writer created by {@link #getWriter()}. This method does nothing if * the writer is the {@linkplain #output output} instance given by the user rather * than a writer created by this class from a {@link File} or {@link URL} output. * * @see #closeOnReset */ @Override protected void close() throws IOException { writer = null; super.close(); } /** * Service provider interface (SPI) for {@link TextImageWriter}s. This SPI provides a * convenient way to control the {@link TextImageWriter} character encoding: the * {@link #charset} field. For example, many {@code Spi} subclasses will put the * following line in their constructor: * * <blockquote><pre> * {@link #charset} = Charset.forName("ISO-LATIN-1"); // ISO Latin Alphabet No. 1 (ISO-8859-1) * </pre></blockquote> * * @since 2.4 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public static abstract class Spi extends StreamImageWriter.Spi { /** * List of legal output types for {@link TextImageWriter}. */ private static final Class[] INPUT_TYPES = new Class[] { File.class, URL.class, URLConnection.class, Writer.class, OutputStream.class, ImageOutputStream.class, String.class // To be interpreted as file path. }; /** * Default list of file extensions. */ private static final String[] EXTENSIONS = new String[] { "txt", "TXT", "asc", "ASC", "dat", "DAT" }; /** * Character encoding, or {@code null} for the default. This field is initially * {@code null}. A value shall be set by subclasses if the files to be encoded * use some specific character encoding. * * @see TextImageWriter#getCharset */ protected Charset charset; /** * The locale for numbers formatting. For example {@link Locale#US} means that * numbers are expected to use dot as decimal separator. This field is initially * {@code null}, which means that default locale should be used. * * @see TextImageWriter#getDataLocale */ protected Locale locale; /** * The line separator to use, or {@code null} for the system default. * * @see TextImageWriter#getLineSeparator */ protected String lineSeparator; /** * Constructs a quasi-blank {@code TextImageWriter.Spi}. It is up to the subclass to * initialize instance variables in order to provide working versions of all methods. * This constructor provides the following defaults: * * <ul> * <li>{@link #outputTypes} = {{@link File}, {@link URL}, {@link URLConnection}, * {@link Writer}, {@link OutputStream}, {@link ImageOutputStream}, {@link String}}</li> * * <li>{@link #suffixes} = {{@code "txt"}, {@code "asc"}, {@code "dat"}} * (lowercases and uppercases)</li> * </ul> * * For efficienty reasons, the above fields are initialized to shared arrays. Subclasses * can assign new arrays, but should not modify the default array content. */ public Spi() { outputTypes = INPUT_TYPES; suffixes = EXTENSIONS; } } }