/* * 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.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferFloat; import java.awt.image.Raster; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.BufferedReader; import java.io.IOException; import java.text.ParseException; import java.util.Locale; import javax.imageio.IIOException; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.metadata.IIOMetadata; import org.geotools.io.LineFormat; import org.geotools.factory.GeoTools; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Descriptions; import org.geotools.resources.i18n.DescriptionKeys; import org.geotools.image.io.metadata.GeographicMetadata; /** * An image decoder for matrix of floating-point numbers. The default implementation creates * rasters of {@link DataBuffer#TYPE_FLOAT}. An easy way to change this type is to overwrite * the {@link #getRawDataType} method. * * @since 2.4 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class TextMatrixImageReader extends TextImageReader { /** * The matrix data loaded by {@link #load} method. */ private float[] data; /** * The image width. This number is valid only if {@link #data} is non-null. */ private int width; /** * The image height. This number is valid only if {@link #completed} is true. */ private int height; /** * The expected height, or 0 if unknow. This number * has no signification if {@link #data} is null. */ private int expectedHeight; /** * {@code true} if {@link #data} contains all data, or {@code false} * if {@link #data} contains only the first line. This field has no * signification if {@link #data} is null. */ private boolean completed; /** * Constructs a new image reader. * * @param provider the provider that is invoking this constructor, or {@code null} if none. */ protected TextMatrixImageReader(final ImageReaderSpi provider) { super(provider); } /** * Load data. No subsampling is performed. * * @param imageIndex the index of the image to be read. * @param all {@code true} to read all data, or {@code false} to read only the first line. * @return {@code true} if reading has been aborted. * @throws IOException If an error occurs reading the width information from the input source. */ private boolean load(final int imageIndex, final boolean all) throws IOException { clearAbortRequest(); if (all) { processImageStarted(imageIndex); } float[] values = (data != null) ? new float[width] : null; int offset = width*height; final BufferedReader input = getReader(); final LineFormat format = getLineFormat(imageIndex); final float padValue = (float)getPadValue(imageIndex); String line; while ((line=input.readLine())!=null) { if (isComment(line)) { continue; } try { format.setLine(line); values = format.getValues(values); for (int i=values.length; --i>=0;) { if (values[i] == padValue) { values[i] = Float.NaN; } } } catch (ParseException exception) { throw new IIOException(getPositionString(exception.getLocalizedMessage()), exception); } if (data == null) { data = new float[1024]; } final int newOffset = offset + (width=values.length); if (newOffset > data.length) { data = XArray.resize(data, newOffset+Math.min(newOffset, 65536)); } System.arraycopy(values, 0, data, offset, width); offset = newOffset; height++; /* * If only one line was requested, try to guess the expected height. */ if (!all) { final long streamLength = getStreamLength(imageIndex, imageIndex+1); if (streamLength >= 0) { expectedHeight = (int) (streamLength / (line.length() + 1)); } break; } /* * Update progress. */ if (height <= expectedHeight) { processImageProgress(height*100f/expectedHeight); if (abortRequested()) { processReadAborted(); return true; } } } if ((completed = all) == true) { data = XArray.resize(data, offset); expectedHeight = height; } if (all) { processImageComplete(); } return false; } /** * 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. */ public int getWidth(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (data == null) { load(imageIndex, false); } return width; } /** * Returns the height in pixels of the given image within the input source. * Calling this method may force loading of full image. * * @param imageIndex the index of the image to be queried. * @return Image height. * @throws IOException If an error occurs reading the height information * from the input source. */ public int getHeight(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (data == null || !completed) { load(imageIndex, true); } return height; } /** * Returns metadata associated with the given image. * Calling this method may force loading of full 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 public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (!ignoreMetadata) { if (data == null || !completed) { load(imageIndex, true); } float minimum = Float.POSITIVE_INFINITY; float maximum = Float.NEGATIVE_INFINITY; for (int i=0; i<data.length; i++) { final float value = data[i]; if (value < minimum) minimum = value; if (value > maximum) maximum = value; } if (minimum < maximum) { final GeographicMetadata metadata = new GeographicMetadata(this); metadata.getBand(0).setValidRange(minimum, maximum); return metadata; } } return null; } /** * 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. */ public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { /* * Parameters check. */ final int numSrcBands = 1; final int numDstBands = 1; checkImageIndex(imageIndex); checkReadParamBandSettings(param, numSrcBands, numDstBands); /* * Extract user's parameters. */ final int[] sourceBands; final int[] destinationBands; final int sourceXSubsampling; final int sourceYSubsampling; final int subsamplingXOffset; final int subsamplingYOffset; final int destinationXOffset; final int destinationYOffset; if (param != null) { sourceBands = param.getSourceBands(); destinationBands = param.getDestinationBands(); final Point offset = param.getDestinationOffset(); sourceXSubsampling = param.getSourceXSubsampling(); sourceYSubsampling = param.getSourceYSubsampling(); subsamplingXOffset = param.getSubsamplingXOffset(); subsamplingYOffset = param.getSubsamplingYOffset(); destinationXOffset = offset.x; destinationYOffset = offset.y; } else { sourceBands = null; destinationBands = null; sourceXSubsampling = 1; sourceYSubsampling = 1; subsamplingXOffset = 0; subsamplingYOffset = 0; destinationXOffset = 0; destinationYOffset = 0; } /* * Compute source region and check for possible optimization. */ final Rectangle srcRegion = getSourceRegion(param, width, height); final boolean isDirect = sourceXSubsampling==1 && sourceYSubsampling==1 && subsamplingXOffset==0 && subsamplingYOffset==0 && destinationXOffset==0 && destinationYOffset==0 && srcRegion.x ==0 && srcRegion.width ==width && srcRegion.y ==0 && srcRegion.height==height; /* * Read data if it was not already done. */ if (data == null || !completed) { if (load(imageIndex, true)) { return null; } } /* * If a direct mapping is possible, perform it. */ if (isDirect && (param==null || param.getDestination()==null)) { final ImageTypeSpecifier type = getRawImageType(imageIndex, param, null); // TODO: use SampleConverter final SampleModel model = type.getSampleModel().createCompatibleSampleModel(width,height); final DataBuffer buffer = new DataBufferFloat(data, data.length); final WritableRaster raster = Raster.createWritableRaster(model, buffer, null); return new BufferedImage(type.getColorModel(), raster, false, null); } /* * Copy data into a new image. */ final int dstBand = 0; final BufferedImage image = getDestination(imageIndex, param, width, height, null); // TODO final WritableRaster dstRaster = image.getRaster(); final Rectangle dstRegion = new Rectangle(); computeRegions(param, width, height, image, srcRegion, dstRegion); final int dstXMin = dstRegion.x; final int dstYMin = dstRegion.y; final int dstXMax = dstRegion.width + dstXMin; final int dstYMax = dstRegion.height + dstYMin; int srcY = srcRegion.y; for (int y=dstYMin; y<dstYMax; y++) { assert(srcY < srcRegion.y+srcRegion.height); int srcX = srcRegion.x; for (int x=dstXMin; x<dstXMax; x++) { assert(srcX < srcRegion.x+srcRegion.width); final float value = data[srcY*width+srcX]; dstRaster.setSample(x, y, dstBand, value); srcX += sourceXSubsampling; } srcY += sourceYSubsampling; } return image; } /** * Closes the input stream and disposes the resources that was specific to that stream. */ @Override public void close() throws IOException { completed = false; data = null; width = 0; height = 0; expectedHeight = 0; super.close(); } /** * Service provider interface (SPI) for {@link TextMatrixImageReader}s. This SPI provides * the necessary implementation for creating default {@link TextMatrixImageReader} using * default locale and character set. Subclasses can set some fields at construction time * in order to tune the reader to a particular environment, e.g.: * * <blockquote><pre> * public final class MyCustomSpi extends TextMatrixImageReader.Spi { * public MyCustomSpi() { * {@link #names names} = new String[] {"myformat"}; * {@link #MIMETypes MIMETypes} = new String[] {"text/plain"}; * {@link #vendorName vendorName} = "Foo inc."; * {@link #version version} = "1.0"; * {@link #locale locale} = Locale.US; * {@link #charset charset} = Charset.forName("ISO-LATIN-1"); * {@link #padValue padValue} = 9999; * } * } * </pre></blockquote> * * (Note: fields {@code vendorName} and {@code version} are only informatives). * There is no need to override any method in this example. However, developers * can gain more control by creating subclasses of {@link TextMatrixImageReader} * and {@code Spi}. * * @since 2.1 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public static class Spi extends TextImageReader.Spi { /** * The format names for the default {@link TextMatrixImageReader} configuration. */ static final String[] NAMES = {"matrix"}; /** * The mime types for the default {@link TextMatrixImageReader} configuration. */ static final String[] MIME_TYPES = {"application/matrix", "text/plain"}; /** * Constructs a default {@code TextMatrixImageReader.Spi}. This constructor * provides the following defaults in addition to the defaults defined in the * {@linkplain TextImageReader.Spi#Spi super-class constructor}: * * <ul> * <li>{@link #names} = {@code "matrix"}</li> * <li>{@link #MIMETypes} = {@code "text/x-matrix"}</li> * <li>{@link #pluginClassName} = {@code "org.geotools.image.io.text.TextMatrixImageReader"}</li> * <li>{@link #vendorName} = {@code "Geotools"}</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() { names = NAMES; MIMETypes = MIME_TYPES; pluginClassName = "org.geotools.image.io.text.TextMatrixImageReader"; vendorName = "GeoTools"; version = GeoTools.getVersion().toString(); } /** * 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. */ public String getDescription(final Locale locale) { return Descriptions.getResources(locale).getString(DescriptionKeys.CODEC_MATRIX); } /** * 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. */ public ImageReader createReaderInstance(final Object extension) throws IOException { return new TextMatrixImageReader(this); } /** * Returns {@code true} if the specified row length is valid. The default implementation * returns {@code true} if the row seems "long", where "long" is arbitrary fixed to 10 * columns. This is an arbitrary choice, which is why this method is not public. It may * be changed in any future Geotools version. */ @Override boolean isValidColumnCount(final int count) { return count > 10; } } }