/*
* 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.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Locale;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ServiceRegistry;
import org.geotoolkit.io.LineFormat;
import org.geotoolkit.resources.Errors;
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;
import org.geotoolkit.internal.image.io.GridDomainAccessor;
import org.geotoolkit.internal.io.LineReader;
/**
* Image decoder for text files storing pixel values as records.
* Such text files use one line (record) by pixel. Each line contains
* at least 3 columns (in arbitrary order):
* <p>
* <ul>
* <li>Pixel's <var>x</var> coordinate.</li>
* <li>Pixel's <var>y</var> coordinate.</li>
* <li>An arbitrary number of pixel values.</li>
* </ul>
* <p>
* For example, some Sea Level Anomaly (SLA) files contains rows of longitude
* (degrees), latitude (degrees), SLA (cm), East/West current (cm/s) and
* North/South current (cm/s), as below:
*
* {@preformat text
* 45.1250 -29.8750 -7.28 10.3483 -0.3164
* 45.1250 -29.6250 -4.97 11.8847 3.6192
* 45.1250 -29.3750 -2.91 3.7900 3.0858
* 45.1250 -29.1250 -3.48 -5.1833 -5.0759
* 45.1250 -28.8750 -4.36 -1.8129 -16.3689
* 45.1250 -28.6250 -3.91 7.5577 -24.6801
* (...etc...)
* }
*
* From this decoder point of view, the two first columns (<var>longitude</var> and <var>latitude</var>)
* are pixel's logical coordinate (<var>x</var>,<var>y</var>), while the three last
* columns are three image's bands. The whole file contains only one image, unless
* {@link #getNumImages} has been overridden. All (<var>x</var>,<var>y</var>)
* coordinates belong to pixel's center. This decoder will automatically translate
* (<var>x</var>,<var>y</var>) coordinates from logical space to pixel space.
* <p>
* By default, {@code TextRecordImageReader} assumes that <var>x</var> and
* <var>y</var> coordinates appear in column #0 and 1 respectively. It also assumes
* that numeric values are encoded using current defaults {@link java.nio.charset.Charset}
* and {@link java.util.Locale}, and that there is no pad value. The easiest way to change
* the default setting is to create a {@link Spi} subclass. There is no need to subclass
* {@code TextRecordImageReader}, unless you want more control on the decoding process.
*
* {@section Example}
* The text of the left side is an extract of a list of (<var>longitude</var>, <var>latitude</var>,
* <var>elevation of the ocean floor</var>) records. The image on the right side is the image
* produced by {@code TextRecordImageReader} when reading such file.
*
* <table cellpadding='24'>
* <tr valign="top"><td><pre>
* # Longitude Latitude Altitude
* 59.9000 -30.0000 -3022
* 59.9333 -30.0000 -3194
* 59.9667 -30.0000 -3888
* 60.0000 -30.0000 -3888
* 45.0000 -29.9667 -2502
* 45.0333 -29.9667 -2502
* 45.0667 -29.9667 -2576
* 45.1000 -29.9667 -2576
* 45.1333 -29.9667 -2624
* 45.1667 -29.9667 -2690
* 45.2000 -29.9667 -2690
* 45.2333 -29.9667 -2692
* 45.2667 -29.9667 -2606
* 45.3000 -29.9667 -2606
* 45.3333 -29.9667 -2528</pre>etc...</td>
* <td><img src="doc-files/Sandwell.jpeg"></td>
* </tr></table>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.08
*
* @since 3.08 (derived from 1.2)
* @module
*/
public class TextRecordImageReader extends TextImageReader {
/**
* Small factor for working around rounding error.
*/
private static final float EPS = 1E-5f;
/**
* Interval in bytes between calls to {@link #processImageProgress(float)}.
*/
private static final int PROGRESS_INTERVAL = 4096;
/**
* If {@code true}, then {@link BufferedReader} are filled with {@code NaN} values before
* to read the pixels. If {@code false}, then pixel values in location not defined by the
* file will keep their old value.
*/
private static final boolean CLEAR = true;
/**
* The data for all images, {@code null} if no data have been read yet. The length of this
* array is the number of images. Each element is the data of the image at the corresponding
* index.
* <p>
* if {@link #seekForwardOnly} is {@code true}, then the element are cleared to {@code null}
* when the reader advance to the next image.
*/
private TextRecordList[] data;
/**
* Index of the next image to read.
*/
private int nextImageIndex;
/**
* Constructs a new image reader.
*
* @param provider The {@link ImageReaderSpi} that is constructing this object, or {@code null}.
*/
public TextRecordImageReader(final Spi provider) {
super(provider);
}
/**
* Returns the grid tolerance (epsilon) value.
*/
private float getGridTolerance() {
return (originatingProvider instanceof Spi) ? ((Spi) originatingProvider).gridTolerance : EPS;
}
/**
* Returns the column number for <var>x</var> values. The default implementation returns
* {@link TextRecordImageReader.Spi#xColumn}, or 0 if no service prodiver were specified
* to the constructor. Subclasses should override this method if this information should
* be obtained in an other way.
*
* @param imageIndex The index of the image to be queried.
* @return The column number for <var>x</var> values.
* @throws IOException If an error occurs while reading the from the input source.
*/
protected int getColumnX(final int imageIndex) throws IOException {
return (originatingProvider instanceof Spi) ? ((Spi) originatingProvider).xColumn : 0;
}
/**
* Returns the column number for <var>y</var> values. The default implementation returns
* {@link TextRecordImageReader.Spi#yColumn}, or 1 if no service prodiver were specified
* to the constructor. Subclasses should override this method if this information should
* be obtained in an other way.
*
* @param imageIndex The index of the image to be queried.
* @return The column number for <var>y</var> values.
* @throws IOException If an error occurs while reading the from the input source.
*/
protected int getColumnY(final int imageIndex) throws IOException {
return (originatingProvider instanceof Spi) ? ((Spi) originatingProvider).yColumn : 1;
}
/**
* Ensures that the given value is positive. This is used for checking the values returned
* by {@link #getColumnX(int)} and {@link #getColumnY(int)}. In case of negative number, we
* throw an {@link IIOException} instead of {@link IllegalArgumentException} because this
* is not a method argument provided by the user.
*/
private static void ensurePositive(final String name, final int column) throws IIOException {
if (column < 0) {
throw new IIOException(Errors.format(Errors.Keys.NegativeColumn_2, name, column));
}
}
/**
* Returns the number of bands available for the specified image. The default
* implementation reads the image immediately and counts the number of columns
* after the geodetic coordinate columns.
*
* @param imageIndex The image index.
* @throws IOException if an error occurs while reading the information from the input source.
*/
@Override
public int getNumBands(final int imageIndex) throws IOException {
return getRecords(imageIndex, false).getNumBands();
}
/**
* Returns the width in pixels of the given image within the input source.
* Invoking this method forces the reading of the whole image.
*
* @param imageIndex the index of the image to be queried.
* @return Image width.
* @throws IOException If an error occurs while reading the width information
* from the input source.
*/
@Override
public int getWidth(final int imageIndex) throws IOException {
final TextRecordList records = getRecords(imageIndex, false);
return records.getPointCount(records.xColumn);
}
/**
* Returns the height in pixels of the given image within the input source.
* Invoking this method forces the reading of the whole image.
*
* @param imageIndex the index of the image to be queried.
* @return Image height.
* @throws IOException If an error occurs while reading the height information
* from the input source.
*/
@Override
public int getHeight(final int imageIndex) throws IOException {
final TextRecordList records = getRecords(imageIndex, false);
return records.getPointCount(records.yColumn);
}
/**
* Returns an approximation of the length of the stream portion from the first given image
* to the second given image (inclusive). This implementation assumes that every image have
* the same length. If the length can not be computed, then this method returns -1.
* <p>
* Many text formats store only one image. Getting the actual number of images is possible
* by costly since it typically require parsing the file until some separator are found.
* Consequently this method uses the actual number of images only if it is cheap to obtain,
* otherwise it assumes that the stream contains only one image.
* <p>
* The returned information is approximative and should be used only for the purpose
* of updating progress listeners.
*
* @param fromImage The index of the first image (inclusive).
* @param toIndex The index of the last image (inclusive) for which to measure the stream length.
* @return The number of bytes in the given portion of the stream, or -1 if unknown.
* @throws IOException If an error occurred while reading the stream.
*
* @see #getStreamLength()
*
* @since 3.08
*/
private long getStreamLength(final int fromImage, final int toImage) throws IOException {
long length = getStreamLength();
if (length > 0) {
final int numImages = getNumImages(false);
if (numImages > 0) {
length = length * (toImage - fromImage + 1) / numImages;
}
}
return length;
}
/**
* Retourne la position du flot spécifié, ou {@code -1} si cette position est
* inconnue. Note: la position retournée est <strong>approximative</strong>.
* Elle est utile pour afficher un rapport des progrès, mais sans plus.
*
* @param reader Flot dont on veut connaître la position.
* @return Position approximative du flot, ou {@code -1}
* si cette position n'a pas pu être obtenue.
* @throws IOException si l'opération a échouée.
*/
private static long getStreamPosition(final Reader reader) throws IOException {
return (reader instanceof LineReader) ? ((LineReader) reader).getPosition() : -1;
}
/**
* 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) {
// Stream metadata.
return null;
}
final SpatialMetadata metadata = new SpatialMetadata(false, this, null);
/*
* Computes the smallest bounding box containing the full image in user coordinates.
* This implementation searches for minimum and maximum values in x and y columns as
* returned by getColumnX() and getColumnY(). Reminder: xmax and ymax are INCLUSIVE
* in the code below, as well as (width-1) and (height-1).
*/
final TextRecordList records = getRecords(imageIndex, false);
final int xColumn = records.xColumn;
final int yColumn = records.yColumn;
final int width = records.getPointCount(xColumn);
final int height = records.getPointCount(yColumn);
final double xmin = records.getMinimum(xColumn);
final double ymin = records.getMinimum(yColumn);
final double xmax = records.getMaximum(xColumn);
final double ymax = records.getMaximum(yColumn);
final double padValue = getPadValue(imageIndex);
final GridDomainAccessor domain = new GridDomainAccessor(metadata);
// Note: the swapping of ymax and ymin below is intentional,
// since values on the y axis are increasing downward.
domain.setAll(xmin, ymax, xmax, ymin, width, height, true, null);
/*
* Now adds the valid range of sample values for each band.
*/
final DimensionAccessor dimensions = new DimensionAccessor(metadata);
final int numBands = records.getNumBands();
for (int band=0; band<numBands; band++) {
final int column = records.getColumnForBand(band);
dimensions.selectChild(dimensions.appendChild());
dimensions.setValueRange(records.getMinimum(column), records.getMaximum(column));
dimensions.setFillSampleValues(padValue);
}
return metadata;
}
/**
* Invoked during {@link #read read} operation for rounding a record of values. The default
* implementation does nothing. The main purpose of this method is to give to subclasses an
* opportunity to fix rounding errors in latitude and longitude coordinates.
*
* {@section Example}
* Assume that the longitudes in a file are likely to have an interval of 1/6° but are written
* with only 3 decimal digits. In such case, the {@linkplain #getColumnX x} values look like
* {@code 10.000}, {@code 10.167}, {@code 10.333}, <i>etc.</i>, which can leads to an
* error of 0.001° in longitude. This error may cause {@code TextRecordImageReader} to fails
* validation tests and throws an {@link javax.imageio.IIOException}: "<cite>Points dont seem
* to be distributed on a regular grid</cite>".
* <p>
* A work around is to multiply the <var>x</var> and <var>y</var> coordinates by 6, round to
* the nearest integer and divide them by 6 as in the code below (which is doing the same
* process to latitude):
*
* {@preformat java
* int xColumn = getColumnX();
* int yColumn = getColumnY();
* values[xColumn] = XMath.roundIfAlmostInteger(values[xColumn] * 6, 3) / 6;
* values[yColumn] = XMath.roundIfAlmostInteger(values[yColumn] * 6, 3) / 6;
* }
*
* @param values The values to round in place.
*/
protected void round(double[] values) {
}
/**
* Returns the records for the image at the given index. If this image has already been read
* in a previous call to this method, then the cached {@code TextRecordList} will be returned
* immediately. Otherwise the records will be read from the input source. This will force
* the loading of every images before the given {@code imageIndex} that has not yet been
* read. Those previous images will be discarded immediately if {@link #seekForwardOnly}
* is {@code true}.
*
* @param imageIndex The image index.
* @param allowCancel {@code false} if cancelation should throw an exception.
* @return The list of records for the requested image (never {@code null}).
* @throws IOException if an error occurred while reading, including badly formatted numbers.
* @throws IndexOutOfBoundsException If the given image index is outside the range of index
* that this method can process.
*/
private TextRecordList getRecords(final int imageIndex, final boolean allowCancel) throws IOException {
clearAbortRequest();
checkImageIndex(imageIndex);
if (imageIndex >= nextImageIndex) {
processImageStarted(imageIndex);
final BufferedReader reader = getReader();
final long streamOrigin = getStreamPosition(reader);
final long streamLength = getStreamLength(nextImageIndex, imageIndex);
for (; nextImageIndex <= imageIndex; nextImageIndex++) {
/*
* If there is some image before this one, trim their internal array in order to
* reduce memory usage. Note that the image to be read below will not be trimmed
* at the end of this method because in typical usage, it will not be keept (i.e.
* we usually load only one image, or if we want many images we typically set
* seekForwardOnly to true).
*/
if (seekForwardOnly) {
minIndex = nextImageIndex;
}
if (nextImageIndex != 0 && data != null) {
final TextRecordList records = data[nextImageIndex-1];
if (records != null) {
if (seekForwardOnly) {
data[nextImageIndex-1] = null;
} else {
records.trimToSize();
}
}
}
/*
* Parse the line read from the input source. Those lines will be immediately
* discarded if seekForwardOnly is true and the current image index is lower
* than the requested one (because we need to parse previous images before to
* reach the requested one).
*/
int linePosition = 0;
int nextProgress = 1;
double[] values = null;
TextRecordList records = null;
final boolean memorize = (nextImageIndex == imageIndex) || !seekForwardOnly;
final int xColumn = getColumnX (nextImageIndex); ensurePositive("x", xColumn);
final int yColumn = getColumnY (nextImageIndex); ensurePositive("y", yColumn);
final double padValue = getPadValue (nextImageIndex);
final LineFormat lineFormat = getLineFormat(nextImageIndex);
String line;
while ((line = reader.readLine()) != null) {
if (abortRequested()) {
processReadAborted();
if (!allowCancel || records == null) {
throw new IIOException(Errors.format(Errors.Keys.CanceledOperation));
}
return records;
}
linePosition++;
if (isComment(line)) {
continue;
}
try {
if (lineFormat.setLine(line) == 0) {
continue;
}
values = lineFormat.getValues(values);
} catch (ParseException exception) {
throw new IIOException(getPositionString(exception.getLocalizedMessage()), exception);
}
/*
* Modify (if needed) the values of the record we just read, replacing pad
* values by NaN and fixing the geodetic coordinates for rounding errors
* (if the user overridden the 'round' method).
*/
for (int i=0; i<values.length; i++) {
if (i != xColumn && i != yColumn && values[i] == padValue) {
values[i] = Double.NaN;
}
}
round(values);
if (memorize) {
if (records == null) {
records = new TextRecordList(values,
Math.max(8, (int) (streamLength / (line.length() + 1)) + 1),
xColumn, yColumn, getGridTolerance());
}
records.add(values);
}
/*
* Report progress.
*/
if (linePosition >= nextProgress) {
final long position = getStreamPosition(reader) - streamOrigin;
processImageProgress(position * (100f / streamLength));
nextProgress += (int) ((long) PROGRESS_INTERVAL * linePosition / position);
}
}
/*
* At this point, we have finished reading the image.
* Ensure that we have enough data (2 records is a minimum).
*/
if (records != null) {
final int lineCount = records.getLineCount();
if (lineCount < 2) {
throw new IIOException(getPositionString(Errors.format(
Errors.Keys.FileHasTooFewData)));
}
if (data == null) {
data = new TextRecordList[imageIndex+1];
} else if (data.length <= imageIndex) {
data = Arrays.copyOf(data, imageIndex*2);
}
data[nextImageIndex] = records;
}
}
processImageComplete();
}
/*
* Following should never be null if checkImageIndex(int) did its work properly.
* We check nevertheless as a safety (user could have overridden checkImageIndex(int)
* for instance).
*/
if (data != null && imageIndex < data.length) {
final TextRecordList records = data[imageIndex];
if (records != null) {
return records;
}
}
throw new IndexOutOfBoundsException(String.valueOf(imageIndex));
}
/**
* Reads the image indexed by {@code imageIndex} and returns it as a buffered image.
*
* @param imageIndex The index of the image to be retrieved.
* @param param Parameters used to control the reading process, or {@code null}.
* @return The desired portion of the image.
* @throws IOException If an error occurs during reading.
*/
@Override
public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
final TextRecordList records = getRecords(imageIndex, true);
final int xColumn = records.xColumn;
final int yColumn = records.yColumn;
final int width = records.getPointCount(xColumn);
final int height = records.getPointCount(yColumn);
final int numSrcBands = records.getNumBands();
/*
* Extracts user's parameters
*/
final int[] srcBands;
final int[] dstBands;
final int sourceXSubsampling;
final int sourceYSubsampling;
if (param != null) {
srcBands = param.getSourceBands();
dstBands = param.getDestinationBands();
sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = param.getSourceYSubsampling();
} else {
srcBands = null;
dstBands = null;
sourceXSubsampling = 1;
sourceYSubsampling = 1;
}
/*
* Initializes...
*/
final int numDstBands = (dstBands != null) ? dstBands.length :
(srcBands != null) ? srcBands.length : numSrcBands;
final SampleConverter[] converters = new SampleConverter[numDstBands];
final BufferedImage image = getDestination(imageIndex, param, width, height, converters);
checkReadParamBandSettings(param, numSrcBands, image.getSampleModel().getNumBands());
final Rectangle srcRegion = new Rectangle();
final Rectangle dstRegion = new Rectangle();
computeRegions(param, width, height, image, srcRegion, dstRegion);
final int sourceXMin = srcRegion.x;
final int sourceYMin = srcRegion.y;
final int sourceWidth = srcRegion.width;
final int sourceHeight = srcRegion.height;
final int destinationXOffset = dstRegion.x;
final int destinationYOffset = dstRegion.y;
final WritableRaster raster = image.getRaster();
final int columnCount = records.columnCount;
final int dataCount = records.getDataCount();
final float[] data = records.getData();
final double xmin = records.getMinimum(xColumn);
final double ymin = records.getMinimum(yColumn);
final double xmax = records.getMaximum(xColumn);
final double ymax = records.getMaximum(yColumn);
final double scaleX = (width -1) / (xmax - xmin);
final double scaleY = (height-1) / (ymax - ymin);
/*
* Clears the image area. All values are set to NaN.
*/
if (CLEAR) {
final int maxX = dstRegion.width + destinationXOffset;
final int maxY = dstRegion.height + destinationYOffset;
for (int b = (dstBands != null) ? dstBands.length : numDstBands; --b>=0;) {
final int band = (dstBands != null) ? dstBands[b] : b;
for (int y=destinationYOffset; y<maxY; y++) {
for (int x=destinationXOffset; x<maxX; x++) {
raster.setSample(x, y, band, Float.NaN);
}
}
}
}
/*
* Computes column numbers corresponding to source bands,
* and start storing values into the image.
*/
final int[] columns = new int[(srcBands!=null) ? srcBands.length : numDstBands];
for (int i=0; i<columns.length; i++) {
columns[i] = records.getColumnForBand(srcBands != null ? srcBands[i] : i);
}
for (int i=0; i<dataCount; i+=columnCount) {
/*
* Converts the (x,y) geodetic coordinate into pixel coordinate into the source image.
* Then convert the result from a pixel coordinate in the source image to a coordinate
* in the destination image. Note that the conversion from geodetic to pixel coordinates
* assume that the y axis is increasing downward, as often with images.
*/
int x = (int) Math.round((data[i+xColumn] - xmin) * scaleX) - sourceXMin;
if (x >= 0 && x < sourceWidth && (x % sourceXSubsampling) == 0) {
int y = (int) Math.round((ymax - data[i+yColumn]) * scaleY) - sourceYMin;
if (y >= 0 && y < sourceHeight && (y % sourceYSubsampling) == 0) {
x = x / sourceXSubsampling + destinationXOffset;
y = y / sourceYSubsampling + destinationYOffset;
for (int j=0; j<columns.length; j++) {
final int db = (dstBands != null ? dstBands[j] : j);
raster.setSample(x, y, db, converters[j].convert(data[i+columns[j]]));
}
}
}
}
return image;
}
/**
* {@inheritDoc}
*/
@Override
protected void close() throws IOException {
data = null;
nextImageIndex = 0;
super.close();
}
/**
* Service provider interface (SPI) for {@code TextRecordImageReader}s. This SPI provides
* necessary implementation for creating default {@link TextRecordImageReader} 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 "records"} </td></tr>
* <tr><td> {@link #MIMETypes} </td><td> {@code "text/plain"} </td></tr>
* <tr><td> {@link #pluginClassName} </td><td> {@code "org.geotoolkit.image.io.plugin.TextRecordImageReader"} </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 #xColumn} </td><td> {@code 0} </td></tr>
* <tr><td> {@link #yColumn} </td><td> {@code 1} </td></tr>
* <tr><td> {@link #gridTolerance} </td><td> May vary. </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 CLSImageReaderSpi extends TextRecordImageReader.Spi {
* public CLSImageReaderSpi() {
* names = new String[] {"CLS"};
* MIMETypes = new String[] {"text/x-records-CLS"};
* vendorName = "Institut de Recherche pour le Développement";
* 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 TextRecordImageReader}
* and {@code Spi}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.08
*
* @since 3.08 (derived from 2.1)
* @module
*/
public static class Spi extends TextImageReader.Spi {
/**
* The format names for the default {@link TextRecordImageReader} configuration.
*/
private static final String[] NAMES = {"records"};
/**
* The mime types for the default {@link TextRecordImageReader} configuration.
*/
private static final String[] MIME_TYPES = {"text/plain"};
/**
* 0-based column number for <var>x</var> values. The default value is 0.
*
* @see TextRecordImageReader#getColumnX
*/
protected int xColumn;
/**
* 0-based column number for <var>y</var> values. The default value is 1.
*
* @see TextRecordImageReader#getColumnY
*/
protected int yColumn;
/**
* A tolerance factor during decoding, between 0 and 1. During decoding, the image reader
* computes cell's width and height (i.e. the smallest non-null difference between ordinates
* in a given column: <var>x</var> for cell's width and <var>y</var> for cell's height).
* Then, it checks if every coordinate points fall on a grid having this cell's size. If
* a point depart from more than {@code gridTolerance} percent of cell's width or height,
* an exception is thrown.
* <p>
* {@code gridTolerance} should be a small number like {@code 1E-5f}
* or {@code 1E-3f}. The later is more tolerant than the former.
*/
protected float gridTolerance = EPS;
/**
* Constructs a default {@code TextRecordImageReader.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.TextRecordImageReader";
xColumn = 0;
yColumn = 1;
gridTolerance = EPS;
}
/**
* 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.CodecGrid);
}
/**
* Returns an instance of the 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 TextRecordImageReader(this);
}
/**
* Returns {@code true} if the content of the first few rows seems valid, or {@code false}
* otherwise. The default implementation performs the same check than the
* {@linkplain org.geotoolkit.image.io.TextImageReader.Spi#isValidContent(double[][])
* super-class}, and additionaly checks if the (<var>x</var>, <var>y</var>) values seem
* distributed on a regular grid.
*
* @param rows The first few rows.
* @return {@code true} if the given rows seem to have a valid content.
*/
@Override
protected boolean isValidContent(final double[][] rows) {
/*
* The 12 lines limit is arbitrary and may change in future version.
* We ask for a minimal amount of lines in order to have reasonable
* chances to determine if the data are distributed on a regular grid.
* This limit should be safe if the average line length is lower than
* 80 characters (usually they are about 15 characters).
*/
if (rows.length < 12 || !super.isValidContent(rows)) {
return false;
}
final TextRecordList records = new TextRecordList(rows[0], rows.length, xColumn, yColumn, gridTolerance);
for (int i=1; i<rows.length; i++) {
records.add(rows[i]);
}
try {
records.getPointCount(records.xColumn);
records.getPointCount(records.yColumn);
} catch (IIOException e) {
return false;
}
return true;
}
/**
* Invoked by {@link #isValidColumnCount(int)} for determining if the given number of
* columns is valid. Note that this is the number of columns in the data file, not to
* be confused with the image width.
*
* @since 3.07
*/
@Override
protected boolean isValidColumnCount(final int count) {
return count >= (xColumn == yColumn ? 2 : 3);
}
/**
* Invoked when this Service Provider is registered. This method
* {@linkplain ServiceRegistry#setOrdering(Class, Object, Object) sets the ordering}
* of this {@code TextRecordImageReader.Spi} before {@link TextMatrixImageReader.Spi},
* because the later is generic enough for claiming to be able to read records file.
*
* @param registry The registry where is service is registered.
* @param category The category for which this service is registered.
*/
@Override
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
super.onRegistration(registry, category);
if (category.equals(ImageReaderSpi.class)) {
final TextMatrixImageReader.Spi matrixProvider =
registry.getServiceProviderByClass(TextMatrixImageReader.Spi.class);
if (matrixProvider != null) {
registry.setOrdering(ImageReaderSpi.class, this, matrixProvider);
}
}
}
}
}