/*
* 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.util.Arrays;
import javax.imageio.IIOException;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.resources.Vocabulary;
/**
* List of data records in an image. One instance of this class is created by
* {@link TextRecordImageReader} for every image in a file. A {@code TextRecordList}
* contains a list of records where each record contains data for one pixel. A record
* contains usually the following information:
* <p>
* <ul>
* <li>Pixel's x and y coordinate.</li>
* <li>Pixel's values for each band.</li>
* </li>
* <p>
* Those information can appear in arbitrary columns, providing that the column order stay
* the same for every record in a particular {@code TextRecordList} instance. Records can appear
* in arbitrary order.
* <p>
* Data are floating point value ({@code float} type). Current implementation expects pixels
* distributed on a regular grid. The grid interval will be automatically computed when needed.
* The interval computation should be accurate even if there is missing and/or duplicated records.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.08
*
* @since 3.08 (derived from 1.2)
* @module
*/
final class TextRecordList {
/**
* The minimal values found in every columns.
*/
private final double[] min;
/**
* The maximal values found in every columns.
*/
private final double[] max;
/**
* The number of points in the axis represented by the given column. The sentinel value 0
* means that the number of points for an individual column has not yet been computed.
* This information is typically computed only for {@link #xColumn} and {@link #yColumn}.
*/
private final int[] pointCount;
/**
* A flat array of every records read so far, or {@code null} if the read process has
* not yet been started. Each row in this matrix has a length of {@link #columnCount}.
*/
private float[] data;
/**
* The column of <var>x</var> or <var>y</var> values.
*/
final int xColumn, yColumn;
/**
* Number of columns in each record.
*/
final int columnCount;
/**
* Index of the first element to write in the {@code #data} array. This is always a multiple
* of {@link #columnCount} and is incremented after each call to {@link #add(double[])}.
*/
private int upper;
/**
* Tolerance factor when determining if the interval is constant between
* values in consecutive records.
*/
private final float gridTolerance;
/**
* Creates a new {@code TextRecordList} initialized to the given line.
*
* @param firstLine The first line having been read.
* @param expectedLineCount Number of expected lines. This information is approximative,
* but array allocations will be reduced if an exact value is provided.
*/
public TextRecordList(final double[] firstLine, final int expectedLineCount,
final int xColumn, final int yColumn, final float gridTolerance)
{
min = firstLine.clone();
max = firstLine.clone();
this.xColumn = xColumn;
this.yColumn = yColumn;
columnCount = firstLine.length;
for (int i=0; i<firstLine.length; i++) {
if (Double.isNaN(firstLine[i])) {
min[i] = Double.POSITIVE_INFINITY;
max[i] = Double.NEGATIVE_INFINITY;
}
}
data = new float [columnCount * expectedLineCount];
pointCount = new int[firstLine.length];
this.gridTolerance = gridTolerance;
}
/**
* Adds the given line. If the given line is shorter than expected, then the missing values
* are assumed to be NaN. If the given line is longer than the expected length, then the
* extra values are ignored.
*/
public void add(final double[] line) {
final int limit = Math.min(columnCount, line.length);
final int nextUpper = upper + columnCount;
if (nextUpper >= data.length) {
data = Arrays.copyOf(data, nextUpper * 2);
}
for (int i=0; i<limit; i++) {
final double value = line[i];
if (value < min[i]) min[i] = value;
if (value > max[i]) max[i] = value;
data[upper + i] = (float) value;
}
Arrays.fill(data, upper + limit, nextUpper, Float.NaN);
upper = nextUpper;
}
/**
* Trims the internal array to the minimal size needed for holding all data. This method
* should be invoked only when reading is finished and the array will be kept for a while
* because {@link TextRecordImageReader#seekForwardOnly} is {@code false}.
*/
public void trimToSize() {
data = ArraysExt.resize(data, upper);
}
/**
* Returns a direct reference to internal data. <strong>Do not modify the values</strong>.
* Valid index range from 0 inclusive to {@link #getDataCount} exclusive.
*/
final float[] getData() {
return data;
}
/**
* Returns the length of valid element in the array returned by {@link #getData()}.
*/
final int getDataCount() {
return upper;
}
/**
* Returns the number of line read so far.
*/
public int getLineCount() {
assert (upper % columnCount) == 0;
return upper / columnCount;
}
/**
* Returns the number of bands. This is the number of columns not
* counting the longitude or latitude columns.
*/
public int getNumBands() {
return columnCount - (xColumn == yColumn ? 1 : 2);
}
/**
* Returns the column number where to get the data for the given band. In most typical
* cases, this method just add 2 to the given value in order to skip the longitude and
* latitude columns. However this method is robust to the cases where the longitude and
* latitude columns are not in their usual place.
*
* @param band Index of the band to read.
* @return Index of the column in a record to read.
*/
public int getColumnForBand(int band) {
if (band >= xColumn) band++;
if (band >= yColumn) band++;
return band;
}
/**
* Returns the minimal value in the given column.
*/
public double getMinimum(final int column) {
final double value = min[column];
return value <= max[column] ? value : Double.NaN;
}
/**
* Returns the maximal value in the given column.
*/
public double getMaximum(final int column) {
final double value = max[column];
return min[column] <= value ? value : Double.NaN;
}
/**
* Returns the interval between the values in the given column. This method checks if
* the interval is constant, with a tolerance factor given by the {@link #gridTolerance}
* value.
*
* @param column Column for which the interval is desired.
* @throws IIOException If the interval is not constant in the given column.
*/
private float getInterval(final int column) throws IIOException {
/*
* Copies the values of the given column in a temporary array and sort them.
*/
int count = 0;
final float[] array = new float[getLineCount()];
for (int i=column; i<upper; i += columnCount) {
array[count++] = data[i];
}
assert count == array.length;
Arrays.sort(array);
/*
* Removes duplicates values. When duplicates are found, they range
* from 'lower' to 'upper' inclusive ('upper' is inclusive too!).
*/
int upper = count-1;
int lower = count;
while (--lower >= 1) {
if (array[upper] != array[lower-1]) {
if (upper != lower) {
System.arraycopy(array, upper, array, lower, count-upper);
final int oldCount = count;
count -= (upper-lower);
Arrays.fill(array, count, oldCount, Float.NaN); // For safety.
}
upper = lower-1;
}
}
if (upper != lower) {
System.arraycopy(array, upper, array, lower, count-upper);
final int oldCount = count;
count -= (upper-lower);
Arrays.fill(array, count, oldCount, Float.NaN); // For safety.
}
/*
* Searches the smallest interval between two records. Next, checks if the interval
* between every consecutive records is a multiple of that value. This algorithm
* allow the check to be tolerant to missing records.
*/
float delta = Float.POSITIVE_INFINITY;
for (int i=1; i<count; i++) {
final float d = array[i] - array[i-1];
assert d > 0;
if (d < delta) {
delta = d;
}
}
for (int i=1; i<count; i++) {
final float e = (array[i] - array[i-1]) / delta;
final float re = (float) Math.rint(e);
if (Math.abs(e - re) > re*gridTolerance) {
throw new IIOException(Errors.format(Errors.Keys.NotAGrid));
}
}
return delta;
}
/**
* Returns the number of distinct values in the given columns, as if no records were missing.
* This method assumes that the interval between values in the given column is constant.
* <p>
* This method can be invoked only when the image reading is finished, because it caches
* the value for future reuse (this method is typically invoked more than once for the
* same column).
*
* @param column Column for which the number of points is desired.
* @throws IIOException If the interval is not constant in the given column.
*/
public int getPointCount(final int column) throws IIOException {
int n = pointCount[column];
if (n == 0) {
n = (int) Math.round((getMaximum(column) - getMinimum(column)) / getInterval(column)) +1;
pointCount[column] = n;
}
return n;
}
/**
* Returns a string representation of this {@code TextRecordList} for debugging purpose.
*/
@Override
public String toString() {
final int oldX = pointCount[xColumn];
final int oldY = pointCount[yColumn];
Object xCount, yCount;
try {
xCount = getPointCount(xColumn);
} catch (IIOException exception) {
xCount = exception.getLocalizedMessage();
}
try {
yCount = getPointCount(yColumn);
} catch (IIOException exception) {
yCount = exception.getLocalizedMessage();
}
/*
* Resets the counts to the old value, because this method may be invoked in the debugger
* while the reading is still under progress. In such case we want to reset the 0 value for
* forcing getPointCount(...) to compute the new value when it will be requested so.
*/
pointCount[xColumn] = oldX;
pointCount[yColumn] = oldY;
return Vocabulary.format(Vocabulary.Keys.PointCountInGrid_3, upper, xCount, yCount);
}
}