/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-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.Point;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferFloat;
import java.util.Arrays;
import java.util.Locale;
import java.util.Collections;
import java.text.ParseException;
import java.io.IOException;
import java.io.BufferedReader;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.io.LineFormat;
import org.geotoolkit.resources.Descriptions;
import org.geotoolkit.image.io.TextImageReader;
import org.geotoolkit.image.io.SampleConverter;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.internal.image.io.DimensionAccessor;
/**
* 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.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.08
*
* @see TextMatrixImageWriter
*
* @since 3.08 (derived from 1.2)
* @module
*/
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
* or {@link #completed} is {@code true}.
*/
private int width;
/**
* The image height. If only a part of the image has been read (typically only the first line
* in order to determine the value of {@link #width}), then this is the number of lines read
* so far. This field is the actual image height only when {@link #completed} is true.
*/
private int height;
/**
* The expected height, or 0 if unknown. 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. Note that this field may be {@code true} while the
* {@link #data} are {@code null} if the {@link #width} and {@link #height} fields
* are still valids.
*/
private boolean completed;
/**
* Constructs a new image reader.
*
* @param provider The {@link ImageReaderSpi} that is constructing this object, or {@code null}.
*/
protected TextMatrixImageReader(final Spi provider) {
super(provider);
}
/**
* Loads data. No subsampling is performed, and the pad value is not replaced by NaN.
* This method sets the {@link #width} fields (or ensure that every row have a length
* equals to {@code width} if at least one row was already read) and increment the
* {@link #height} field by the amount of row read.
* <p>
* Once this method complete, the {@link #completed} field is set to the value of
* {@code all} parameter.
*
* @param imageIndex the index of the image to be read.
* @param all {@code true} to read all data, or {@code false} to read only one 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);
}
// If some rows were already read, a non-null array
// will force the next rows to have the same length.
float[] values = (data != null) ? new float[width] : null;
// If some data was already read, the offset where to continue. Otherwise 0.
int offset = width * height;
final BufferedReader input = getReader();
final LineFormat format = getLineFormat(imageIndex);
String line; while ((line = input.readLine()) != null) {
if (isComment(line)) {
continue;
}
try {
format.setLine(line);
values = format.getValues(values);
} catch (ParseException exception) {
throw new IIOException(getPositionString(exception.getLocalizedMessage()), exception);
}
final int newOffset = offset + (width = values.length);
/*
* Try to guess the expected height after the first line, then try to allocate
* the right amout of memory. If the guess is not accurate, the amount of memory
* will be adjusted as needed.
*/
if (data == null) {
final long streamLength = getStreamLength();
if (streamLength >= 0) {
final int length = line.length() + 1; // Add 1 for the EOL character.
expectedHeight = (int) ((streamLength + length/2) / length);
}
data = new float[Math.max(1024, width * expectedHeight)];
} else if (newOffset > data.length) {
data = Arrays.copyOf(data, newOffset * 2);
}
System.arraycopy(values, 0, data, offset, width);
offset = newOffset;
height++;
if (!all) {
return false;
}
/*
* Update progress.
*/
if (height <= expectedHeight) {
processImageProgress(height * 100f / expectedHeight);
}
if (abortRequested()) {
processReadAborted();
return true;
}
}
data = ArraysExt.resize(data, offset);
expectedHeight = height;
completed = true;
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.
*/
@Override
public int getWidth(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
/*
* The line below use && instead of || because we don't need the complete
* set of data. The first line is enough, which is indicated by a non-null
* data array with 'completed' set to false.
*/
if (data == null && !completed) {
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.
*/
@Override
public int getHeight(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
if (!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
protected SpatialMetadata createMetadata(final int imageIndex) throws IOException {
if (imageIndex >= 0) {
if (data == null || !completed) {
if (load(imageIndex, true)) {
return null;
}
}
final float padValue = (float) getPadValue(imageIndex);
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 != padValue) {
if (value < minimum) minimum = value;
if (value > maximum) maximum = value;
}
}
final SpatialMetadata metadata = new SpatialMetadata(false, this, null);
final DimensionAccessor accessor = new DimensionAccessor(metadata);
accessor.selectChild(accessor.appendChild());
if (minimum < maximum) {
accessor.setValueRange(minimum, maximum);
}
if (!Float.isNaN(padValue)) {
accessor.setFillSampleValues(padValue);
}
return metadata;
}
return super.createMetadata(imageIndex);
}
/**
* 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(final int imageIndex, final ImageReadParam param) throws IOException {
/*
* Parameters check.
*/
final int numSrcBands = 1; // To be modified in a future version if we support multi-bands.
final int numDstBands = 1;
checkImageIndex(imageIndex);
checkReadParamBandSettings(param, numSrcBands, numDstBands);
/*
* 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;
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;
}
final int dstBand;
if (destinationBands == null) {
dstBand = 0;
} else {
dstBand = destinationBands[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;
}
}
final float[] data = this.data;
final int width = this.width;
final int height = this.height;
/*
* Get the converter of sample values. In most cases, it will
* just replace pad value (e.g. -9999) by NaN value.
*/
final SampleConverter[] converters = new SampleConverter[numDstBands];
final ImageTypeSpecifier type = getImageType(imageIndex, param, converters);
final SampleConverter converter = converters[0];
/*
* If a direct mapping is possible, perform it. If a direct mapping is performed,
* we will need to set the data array to null (and consequently force a new data
* loading if the image is requested again) because the user could have modified
* the sample values.
*/
if (isDirect && (param == null || param.getDestination() == null) &&
type.getSampleModel().getDataType() == DataBuffer.TYPE_FLOAT)
{
if (!SampleConverter.IDENTITY.equals(converter)) {
for (int i=0; i<data.length; i++) {
data[i] = converter.convert(data[i]);
}
}
final SampleModel model = type.getSampleModel(width, height);
final DataBuffer buffer = new DataBufferFloat(data, data.length);
final WritableRaster raster = Raster.createWritableRaster(model, buffer, null);
this.data = null; // See the above block comment.
minIndex = imageIndex + 1;
return new BufferedImage(type.getColorModel(), raster, false, null);
}
/*
* Copy data into a new image.
*/
final BufferedImage image = getDestination(param, Collections.singleton(type).iterator(), width, height);
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;
final int offset = srcY * width;
int srcX = srcRegion.x;
for (int x=dstXMin; x<dstXMax; x++) {
assert srcX < srcRegion.x + srcRegion.width;
final float value = converter.convert(data[offset + 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.
*
* @throws IOException If an error occurred while closing the reader.
*/
@Override
protected void close() throws IOException {
completed = false;
data = null;
width = 0;
height = 0;
expectedHeight = 0;
super.close();
}
/**
* Service provider interface (SPI) for {@code TextMatrixImageReader}s. This SPI provides
* the necessary implementation for creating default {@link TextMatrixImageReader} using
* default locale and character set. The default constructor initializes the fields to the
* values listed below:
* <p>
* <table border="1" cellspacing="0">
* <tr bgcolor="lightblue"><th>Field</th><th>Value</th></tr>
* <tr><td> {@link #names} </td><td> {@code "matrix"} </td></tr>
* <tr><td> {@link #MIMETypes} </td><td> {@code "text/plain"}, {@code "text/x-matrix"} </td></tr>
* <tr><td> {@link #pluginClassName} </td><td> {@code "org.geotoolkit.image.io.plugin.TextMatrixImageReader"} </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 colspan="2" align="center">See
* {@linkplain org.geotoolkit.image.io.TextImageReader.Spi super-class javadoc} for remaining fields</td></tr>
* </table>
* <p>
* Subclasses can set some fields at construction time
* in order to tune the reader to a particular environment, e.g.:
*
* {@preformat java
* public final class MyCustomSpi extends TextMatrixImageReader.Spi {
* public MyCustomSpi() {
* names = new String[] {"myformat"};
* MIMETypes = new String[] {"text/plain"};
* vendorName = "Foo inc.";
* version = "1.0";
* locale = Locale.US;
* charset = Charset.forName("ISO-8859-1"); // ISO-LATIN-1
* padValue = -9999;
* }
* }
* }
*
* {@note fields <code>vendorName</code> and <code>version</code> 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}.
*
* @author Martin Desruisseaux (IRD)
* @version 3.08
*
* @see TextMatrixImageWriter.Spi
*
* @since 3.08 (derived from 2.1)
* @module
*/
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 = {"text/plain", "text/x-matrix"};
/**
* The provider of the corresponding image writer.
*/
private static final String[] WRITERS = {"org.geotoolkit.image.io.plugin.TextMatrixImageWriter$Spi"};
/**
* Constructs a default {@code TextMatrixImageReader.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 = NAMES;
MIMETypes = MIME_TYPES;
pluginClassName = "org.geotoolkit.image.io.plugin.TextMatrixImageReader";
writerSpiNames = WRITERS;
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 Descriptions.getResources(locale).getString(Descriptions.Keys.CodecMatrix);
}
/**
* 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 TextMatrixImageReader(this);
}
}
}