/* * 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.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.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import org.geotools.io.LineFormat; import org.geotools.factory.GeoTools; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Descriptions; import org.geotools.resources.i18n.DescriptionKeys; import org.geotools.image.io.metadata.ImageGeometry; import org.geotools.image.io.metadata.GeographicMetadata; /** * 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): * * <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> * * 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: * * <blockquote><pre> * 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 * </pre>(...etc...) * </blockquote> * * From this decoder point of view, the two first columns (longitude and latitude) * 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. * * @since 2.1 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class TextRecordImageReader extends TextImageReader { /** * Petit facteur de tolérance servant à tenir compte des erreurs d'arrondissement. */ private static final float EPS = 1E-5f; /** * Intervalle (en nombre d'octets) entre les rapports de progrès. */ private static final int PROGRESS_INTERVAL = 4096; /** * Lorsque la lecture se fait par-dessus une image {@link BufferedReader} existante, * indique s'il faut effacer la région dans laquelle sera placée l'image avant de la * lire. La valeur {@code false} permettra de conserver les anciens pixels dans * les régions ou le fichier ne définit pas de nouvelles valeurs. */ private static final boolean CLEAR = true; /** * Données des images, ou {@code null} si aucune lecture n'a encore été * faite. Chaque élément contient les données de l'image à l'index correspondant * (i.e. l'élément {@code data[0]} contient les données de l'image #0, * {@code data[1]} contient les données de l'image #1, etc.). Des éléments * de ce tableau peuvent être nuls si les données des images correspondantes * ne sont pas retenues après chaque lecture (c'est-à-dire si * <code>{@link #seekForwardOnly}==true</code>). */ private RecordList[] data; /** * Index de la prochaine image à lire. Cet index n'est pas nécessairement * égal à la longueur du tableau {@link #data}. Il peut être aussi bien * plus petit que plus grand. */ private int nextImageIndex; /** * Nombre moyen de caractères par données (incluant les espaces et les codes * de fin de ligne). Cette information n'est qu'à titre indicative, mais son * exactitude peut aider à accelerer la lecture et rendre les rapport des * progrès plus précis. Elle sera automatiquement mise à jour en fonction * des lignes lues. */ private float expectedDatumLength = 10.4f; /** * Constructs a new image reader. * * @param provider the provider that is invoking this constructor, or {@code null} if none. */ public TextRecordImageReader(final ImageReaderSpi 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}. 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. * @throws IOException If an error occurs reading the from the input source. */ protected int getColumnX(final int imageIndex) throws IOException { return (originatingProvider instanceof Spi) ? ((Spi)originatingProvider).xColumn : 0; } /** * Invokes {@link #getColumnX} and checks the result. */ private int getCheckedColumnX(final int imageIndex) throws IOException { final int xColumn = getColumnX(imageIndex); if (xColumn < 0) { throw new IllegalStateException(Errors.format( ErrorKeys.NEGATIVE_COLUMN_$2, "x", xColumn)); } return xColumn; } /** * Returns the column number for <var>x</var> values. The default implementation returns * {@link TextRecordImageReader.Spi#yColumn}. 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. * @throws IOException If an error occurs reading the from the input source. */ protected int getColumnY(final int imageIndex) throws IOException { return (originatingProvider instanceof Spi) ? ((Spi)originatingProvider).yColumn : 1; } /** * Invokes {@link #getColumnY} and checks the result. */ private int getCheckedColumnY(final int imageIndex) throws IOException { final int yColumn = getColumnY(imageIndex); if (yColumn < 0) { throw new IllegalStateException(Errors.format( ErrorKeys.NEGATIVE_COLUMN_$2, "y", yColumn)); } return yColumn; } /** * Retourne le numéro de colonne dans laquelle se trouvent les données de la * bande spécifiée. L'implémentation par défaut retourne {@code band}+1 * ou 2 si la bande est plus grand ou égal à {@link #getColumnX} et/ou * {@link #getColumnY}. Cette implémentation devrait convenir pour des données * se trouvant aussi bien avant qu'après les colonnes <var>x</var> * et <var>y</var>, même si ces dernières ne sont pas consécutives. * * @param imageIndex Index de l'image à lire. * @param band Bande de l'image à lire. * @return Numéro de colonne des données de l'image. * @throws IOException si l'opération nécessitait une lecture du fichier (par exemple * des informations inscrites dans un en-tête) et que cette lecture a échouée. */ private int getColumn(final int imageIndex, int band) throws IOException { final int xColumn = getCheckedColumnX(imageIndex); final int yColumn = getCheckedColumnY(imageIndex); if (band >= Math.min(xColumn, yColumn)) band++; if (band >= Math.max(xColumn, yColumn)) band++; return band; } /** * Set the input source. It should be one of the following object, in preference order: * {@link java.io.File}, {@link java.net.URL}, {@link java.io.BufferedReader}. * {@link java.io.Reader}, {@link java.io.InputStream} or * {@link javax.imageio.stream.ImageInputStream}. */ @Override public void setInput(final Object input, final boolean seekForwardOnly, final boolean ignoreMetadata) { clear(); super.setInput(input, seekForwardOnly, ignoreMetadata); } /** * Returns the number of bands available for the specified image. * * @param imageIndex The image index. * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumBands(final int imageIndex) throws IOException { return getRecords(imageIndex).getColumnCount() - (getCheckedColumnX(imageIndex) == getCheckedColumnY(imageIndex) ? 1 : 2); } /** * 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 { return getRecords(imageIndex).getPointCount(getCheckedColumnX(imageIndex), getGridTolerance()); } /** * Returns the height in pixels of the given image within the input source. * * @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 { return getRecords(imageIndex).getPointCount(getCheckedColumnY(imageIndex), getGridTolerance()); } /** * 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) { return null; } final GeographicMetadata metadata = new GeographicMetadata(this); final ImageGeometry geometry = metadata.getGeometry(); /* * Computes the smallest bounding box containing the full image in user coordinates. * This implementation searchs 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 float tolerance = getGridTolerance(); final RecordList records = getRecords(imageIndex); final int xColumn = getCheckedColumnX(imageIndex); final int yColumn = getCheckedColumnY(imageIndex); final int width = records.getPointCount(xColumn, tolerance); final int height = records.getPointCount(yColumn, tolerance); final double xmin = records.getMinimum(xColumn); final double ymin = records.getMinimum(yColumn); final double xmax = records.getMaximum(xColumn); final double ymax = records.getMaximum(yColumn); geometry.setOrdinateRange(0, xmin, xmax); geometry.setGridRange(0, 0, width-1); geometry.setOrdinateRange(1, ymin, ymax); geometry.setGridRange(1, 0, height-1); geometry.setPixelOrientation("center"); /* * Now adds the valid range of sample values for each band. */ final int numBands = records.getColumnCount() - (xColumn == yColumn ? 1 : 2); for (int band=0; band<numBands; band++) { final int column = getColumn(imageIndex, band); metadata.getBand(band).setValidRange(records.getMinimum(column), records.getMaximum(column)); } return metadata; } /** * Rounds the specified values. This method is invoked automatically by the {@link #read read} * method while reading an image. It provides a place where to fix rounding errors in latitude * and longitude coordinates. For example if longitudes have a step 1/6° but are written with * only 3 decimal digits, then we get {@linkplain #getColumnX x} values like {@code 10.000}, * {@code 10.167}, {@code 10.333}, <cite>etc.</cite>, 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>". 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. * <p> * The default implementation do nothing. * * @param values The values to round in place. */ protected void round(double[] values) { } /** * Retourne les données de l'image à l'index spécifié. Si cette image avait déjà été lue, ses * données seront retournées immédiatement. Sinon, cette image sera lue ainsi que toutes les * images qui précèdent {@code imageIndex} et qui n'avaient pas encore été lues. Que ces * images précédentes soient mémorisées ou oubliées dépend de {@link #seekForwardOnly}. * * @param imageIndex Index de l'image à lire. * @return Les données de l'image. Cette méthode ne retourne jamais {@code null}. * @throws IOException si une erreur est survenue lors de la lecture du flot, * ou si des nombres n'étaient pas correctement formatés dans le flot. * @throws IndexOutOfBoundsException si l'index spécifié est en dehors des * limites permises ou si aucune image n'a été conservée à cet index. */ private RecordList getRecords(final int imageIndex) throws IOException { clearAbortRequest(); checkImageIndex(imageIndex); if (imageIndex >= nextImageIndex) { processImageStarted(imageIndex); final BufferedReader reader = getReader(); final long origine = getStreamPosition(reader); final long length = getStreamLength(nextImageIndex, imageIndex+1); long nextProgressPosition = (origine>=0 && length>0) ? 0 : Long.MAX_VALUE; for (; nextImageIndex<=imageIndex; nextImageIndex++) { /* * Réduit la consommation de mémoire des images précédentes. On ne réduit * pas celle de l'image courante, puisque la plupart du temps le tableau * sera bientôt détruit de toute façon. */ if (seekForwardOnly) { minIndex=nextImageIndex; } if (nextImageIndex!=0 && data!=null) { final RecordList records = data[nextImageIndex-1]; if (records != null) { if (seekForwardOnly) { data[nextImageIndex-1]=null; } else { records.trimToSize(); } } } /* * Procède à la lecture de chacune des lignes de données. Que ces lignes * soient mémorisées ou pas dépend de l'image que l'on est en train de * décoder ainsi que de la valeur de {@link #seekForwardOnly}. */ double[] values = null; RecordList records = null; final boolean keep = (nextImageIndex==imageIndex) || !seekForwardOnly; final int xColumn = getCheckedColumnX(nextImageIndex); final int yColumn = getCheckedColumnY(nextImageIndex); final double padValue = getPadValue (nextImageIndex); final LineFormat lineFormat = getLineFormat (nextImageIndex); try { String line; while ((line=reader.readLine()) != null) { if (isComment(line) || lineFormat.setLine(line) == 0) { continue; } values = lineFormat.getValues(values); for (int i=0; i<values.length; i++) { if (i!=xColumn && i!=yColumn && values[i]==padValue) { values[i] = Double.NaN; } } round(values); if (keep) { if (records == null) { final int expectedLineCount = Math.max(8, Math.min(65536, Math.round(length / (expectedDatumLength*values.length)))); records = new RecordList(values.length, expectedLineCount); } records.add(values); } final long position = getStreamPosition(reader) - origine; if (position >= nextProgressPosition) { processImageProgress(position * (100f/length)); nextProgressPosition = position + PROGRESS_INTERVAL; if (abortRequested()) { processReadAborted(); return records; } } } } catch (ParseException exception) { throw new IIOException(getPositionString(exception.getLocalizedMessage()), exception); } /* * Après la lecture d'une image, vérifie s'il y avait un nombre suffisant de lignes. * Une exception sera lancée si l'image ne contenait pas au moins deux lignes. On * ajustera ensuite le nombre moyens de caractères par données. */ if (records != null) { final int lineCount = records.getLineCount(); if (lineCount<2) { throw new IIOException(getPositionString(Errors.format( ErrorKeys.FILE_HAS_TOO_FEW_DATA))); } if (data == null) { data = new RecordList[imageIndex+1]; } else if (data.length <= imageIndex) { data = XArray.resize(data, imageIndex+1); } data[nextImageIndex] = records; final float meanDatumLength = (getStreamPosition(reader)-origine) / (float)records.getDataCount(); if (meanDatumLength>0) expectedDatumLength = meanDatumLength; } } processImageComplete(); } /* * Une fois les lectures terminées, retourne les données de l'image * demandée. Une exception sera lancée si ces données n'ont pas été * conservées. */ if (data != null && imageIndex < data.length) { final RecordList 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 complete 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. */ public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { final float tolerance = getGridTolerance(); final int xColumn = getCheckedColumnX(imageIndex); final int yColumn = getCheckedColumnY(imageIndex); final RecordList records = getRecords(imageIndex); final int width = records.getPointCount(xColumn, tolerance); final int height = records.getPointCount(yColumn, tolerance); final int numSrcBands = records.getColumnCount() - (xColumn==yColumn ? 1 : 2); /* * Extracts user's parameters */ final int[] srcBands; final int[] dstBands; final int sourceXSubsampling; final int sourceYSubsampling; final int subsamplingXOffset; final int subsamplingYOffset; final int destinationXOffset; final int destinationYOffset; if (param != null) { srcBands = param.getSourceBands(); dstBands = 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 { srcBands = null; dstBands = null; sourceXSubsampling = 1; sourceYSubsampling = 1; subsamplingXOffset = 0; subsamplingYOffset = 0; destinationXOffset = 0; destinationYOffset = 0; } /* * Initializes... */ final int numDstBands = (dstBands!=null) ? dstBands.length : (srcBands!=null) ? srcBands.length : numSrcBands; final BufferedImage image = getDestination(imageIndex, param, width, height, null); // TODO 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 sourceXMax = srcRegion.width + sourceXMin; final int sourceYMax = srcRegion.height + sourceYMin; final WritableRaster raster = image.getRaster(); final int rasterWidth = raster.getWidth(); final int rasterHeigth = raster.getHeight(); final int columnCount = records.getColumnCount(); 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 minX = dstRegion.x; final int minY = dstRegion.y; final int maxX = dstRegion.width + minX; final int maxY = dstRegion.height + minY; for (int b=(dstBands!=null) ? dstBands.length : numDstBands; --b>=0;) { final int band = (dstBands!=null) ? dstBands[b] : b; for (int y=minY; y<maxY; y++) { for (int x=minX; 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] = getColumn(imageIndex, srcBands!=null ? srcBands[i] : i); } for (int i=0; i<dataCount; i+=columnCount) { /* * On convertit maintenant la coordonnée (x,y) logique en coordonnée pixel. Cette * coordonnée pixel se réfère à l'image "source"; elle ne se réfère pas encore à * l'image destination. Elle doit obligatoirement être entière. Plus loin, nous * tiendrons compte du "subsampling". */ final double fx = (data[i+xColumn]-xmin)*scaleX; // (fx,fy) may be NaN: Use final double fy = (ymax-data[i+yColumn])*scaleY; // "!(abs(...)<=tolerance)". int x = (int)Math.round(fx); // This conversion is not the same than int y = (int)Math.round(fy); // getTransform(), but it should be ok. if (!(Math.abs(x-fx)<=tolerance)) {fireBadCoordinate(data[i+xColumn]); continue;} if (!(Math.abs(y-fy)<=tolerance)) {fireBadCoordinate(data[i+yColumn]); continue;} if (x>=sourceXMin && x<sourceXMax && y>=sourceYMin && y<sourceYMax) { x -= subsamplingXOffset; y -= subsamplingYOffset; if ((x % sourceXSubsampling)==0 && (y % sourceYSubsampling)==0) { x = x/sourceXSubsampling + (destinationXOffset-sourceXMin); y = y/sourceYSubsampling + (destinationYOffset-sourceYMin); if (x<rasterWidth && y<rasterHeigth) { for (int j=0; j<columns.length; j++) { raster.setSample(x, y, (dstBands!=null ? dstBands[j] : j), data[i+columns[j]]); } } } } } return image; } /** * Prévient qu'une coordonnée est mauvaise. Cette méthode est appelée lors de la lecture * s'il a été détecté qu'une coordonnée est en dehors des limites prévues, ou qu'elle ne * correspond pas à des coordonnées pixels entières. */ private void fireBadCoordinate(final float coordinate) { processWarningOccurred(getPositionString(Errors.format(ErrorKeys.BAD_COORDINATE_$1, coordinate))); } /** * Supprime les données de toutes les images * qui avait été conservées en mémoire. */ private void clear() { data = null; nextImageIndex = 0; expectedDatumLength = 10.4f; } /** * Restores the {@code TextRecordImageReader} to its initial state. */ @Override public void reset() { clear(); super.reset(); } /** * Service provider interface (SPI) for {@link TextRecordImageReader}s. This SPI provides * necessary implementation for creating default {@link TextRecordImageReader} 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 CLSImageReaderSpi extends TextRecordImageReader.Spi { * public CLSImageReaderSpi() { * {@link #names names} = new String[] {"CLS"}; * {@link #MIMETypes MIMETypes} = new String[] {"text/x-records-CLS"}; * {@link #vendorName vendorName} = "Institut de Recherche pour le Développement"; * {@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 TextRecordImageReader} * 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 TextRecordImageReader} configuration. */ private static final String[] NAMES = {"records"}; /** * The mime types for the default {@link TextRecordImageReader} configuration. */ private static final String[] MIME_TYPES = {"text/x-records"}; /** * 0-based column number for <var>x</var> values. The default value is 0. * * @see TextRecordImageReader#getColumnX * @see TextRecordImageReader#parseLine */ protected int xColumn; /** * 0-based column number for <var>y</var> values. The default value is 1. * * @see TextRecordImageReader#getColumnY * @see TextRecordImageReader#parseLine */ protected int yColumn; /** * A tolerance factor during decoding, between 0 and 1. During decoding, * the image reader compute 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}. 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 "records"}</li> * <li>{@link #MIMETypes} = {@code "text/x-records"}</li> * <li>{@link #pluginClassName} = {@code "org.geotools.image.io.text.TextRecordImageReader"}</li> * <li>{@link #vendorName} = {@code "Geotools"}</li> * <li>{@link #xColumn} = {@code 0}</li> * <li>{@link #yColumn} = {@code 1}</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.TextRecordImageReader"; vendorName = "GeoTools"; version = GeoTools.getVersion().toString(); 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. */ public String getDescription(final Locale locale) { return Descriptions.getResources(locale).getString(DescriptionKeys.CODEC_GRID); } /** * 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. */ public ImageReader createReaderInstance(final Object extension) throws IOException { return new TextRecordImageReader(this); } /** * Returns {@code true} if the specified row length is valid. The default implementation * returns {@code true} if the row seems "short", where "short" 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 >= (xColumn == yColumn ? 2 : 3) && count <= 10; } } }