nom.tam.fits.AsciiTable Maven / Gradle / Ivy
package nom.tam.fits;
/*-
* #%L
* nom.tam.fits
* %%
* Copyright (C) 1996 - 2024 nom-tam-fits
* %%
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
* #L%
*/
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
import nom.tam.fits.header.Bitpix;
import nom.tam.fits.header.IFitsHeader;
import nom.tam.fits.header.Standard;
import nom.tam.util.ArrayDataInput;
import nom.tam.util.ArrayDataOutput;
import nom.tam.util.ArrayFuncs;
import nom.tam.util.ByteFormatter;
import nom.tam.util.ByteParser;
import nom.tam.util.Cursor;
import nom.tam.util.FormatException;
import static nom.tam.fits.header.Standard.NAXIS1;
import static nom.tam.fits.header.Standard.NAXIS2;
import static nom.tam.fits.header.Standard.TBCOLn;
import static nom.tam.fits.header.Standard.TDMAXn;
import static nom.tam.fits.header.Standard.TDMINn;
import static nom.tam.fits.header.Standard.TFIELDS;
import static nom.tam.fits.header.Standard.TFORMn;
import static nom.tam.fits.header.Standard.TLMAXn;
import static nom.tam.fits.header.Standard.TLMINn;
import static nom.tam.fits.header.Standard.TNULLn;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* ASCII table data. ASCII tables are meant for human readability without any special tools. However, they are far less
* flexible or compact than {@link BinaryTable}. As such, users are generally discouraged from using this type of table
* to represent FITS table data. This class only supports scalar entries of type int
, long
,
* float
, double
, or else String
types.
*
* @see AsciiTableHDU
* @see BinaryTable
*/
@SuppressWarnings("deprecation")
public class AsciiTable extends AbstractTableData {
private static final int MAX_INTEGER_LENGTH = 10;
private static final int FLOAT_MAX_LENGTH = 16;
private static final int LONG_MAX_LENGTH = 20;
private static final int INT_MAX_LENGTH = 10;
private static final int DOUBLE_MAX_LENGTH = 24;
/** Whether I10 columns should be treated as int
provided that defined limits allow for it. */
private static boolean isI10PreferInt = true;
// private static final Logger LOG = Logger.getLogger(AsciiTable.class.getName());
/** The number of rows in the table */
private int nRows;
/** The number of fields in the table */
private int nFields;
/** The number of bytes in a row */
private int rowLen;
/** The null string for the field */
private String[] nulls;
/** The type of data in the field */
private Class>[] types;
/** The offset from the beginning of the row at which the field starts */
private int[] offsets;
/** The number of bytes in the field */
private int[] lengths;
/** The byte buffer used to read/write the ASCII table */
private byte[] buffer;
/** Markers indicating fields that are null */
private boolean[] isNull;
/** Column names */
private String[] names;
/**
* An array of arrays giving the data in the table in binary numbers
*/
private Object[] data;
/**
* The parser used to convert from buffer to data.
*/
private ByteParser bp;
/** The actual stream used to input data */
private ArrayDataInput currInput;
/** Create an empty ASCII table */
public AsciiTable() {
data = new Object[0];
buffer = null;
nFields = 0;
nRows = 0;
rowLen = 0;
types = new Class[0];
lengths = new int[0];
offsets = new int[0];
nulls = new String[0];
names = new String[0];
}
/**
* Creates an ASCII table given a header. For tables that contain integer-valued columns of format I10
,
* the {@link #setI10PreferInt(boolean)} mayb be used to control whether to treat them as int
or as
* long
values (the latter is the default).
*
* @param hdr The header describing the table
*
* @throws FitsException if the operation failed
*
* @deprecated (for internal use) Visibility may be reduced to the package level in the future.
*/
public AsciiTable(Header hdr) throws FitsException {
this(hdr, isI10PreferInt);
}
/**
*
* Create an ASCII table given a header, with custom integer handling support.
*
*
* The preferInt
parameter controls how columns with format "I10
" are handled; this is
* tricky because some, but not all, integers that can be represented in 10 characters can be represented as 32-bit
* integers. Setting it true
may make it more likely to avoid unexpected type changes during
* round-tripping, but it also means that some (large number) data in I10 columns may be impossible to read.
*
*
* @param hdr The header describing the table
* @param preferInt if true
, format "I10" columns will be assumed int.class
,
* provided TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if
* false
, I10 columns that have no clear indication of data range will be
* assumed long.class
.
*
* @throws FitsException if the operation failed
*
* @deprecated Use {@link #setI10PreferInt(boolean)} instead prior to reading ASCII tables.
*/
public AsciiTable(Header hdr, boolean preferInt) throws FitsException {
String ext = hdr.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
if (!ext.equalsIgnoreCase(Standard.XTENSION_ASCIITABLE)) {
throw new FitsException("Not an ASCII table header (XTENSION = " + hdr.getStringValue(Standard.XTENSION) + ")");
}
nRows = hdr.getIntValue(NAXIS2);
nFields = hdr.getIntValue(TFIELDS);
rowLen = hdr.getIntValue(NAXIS1);
types = new Class[nFields];
offsets = new int[nFields];
lengths = new int[nFields];
nulls = new String[nFields];
names = new String[nFields];
for (int i = 0; i < nFields; i++) {
names[i] = hdr.getStringValue(Standard.TTYPEn.n(i + 1), TableHDU.getDefaultColumnName(i));
offsets[i] = hdr.getIntValue(TBCOLn.n(i + 1)) - 1;
String s = hdr.getStringValue(TFORMn.n(i + 1));
if (offsets[i] < 0 || s == null) {
throw new FitsException("Invalid Specification for column:" + (i + 1));
}
s = s.trim();
char c = s.charAt(0);
s = s.substring(1);
if (s.indexOf('.') > 0) {
s = s.substring(0, s.indexOf('.'));
}
lengths[i] = Integer.parseInt(s);
switch (c) {
case 'A':
types[i] = String.class;
break;
case 'I':
if (lengths[i] == MAX_INTEGER_LENGTH) {
types[i] = guessI10Type(i, hdr, preferInt);
} else {
types[i] = lengths[i] > MAX_INTEGER_LENGTH ? long.class : int.class;
}
break;
case 'F':
case 'E':
types[i] = float.class;
break;
case 'D':
types[i] = double.class;
break;
default:
throw new FitsException("could not parse column type of ascii table");
}
nulls[i] = hdr.getStringValue(TNULLn.n(i + 1));
if (nulls[i] != null) {
nulls[i] = nulls[i].trim();
}
}
}
/**
* Creates an ASCII table from existing data in column-major format order.
*
* @param columns The data for scalar-valued columns. Each column must be an array of int[]
,
* long[]
, float[]
, double[]
, or else
* String[]
, containing the same number of elements in each column (the
* number of rows).
*
* @return a new ASCII table with the data. The tables data may be partially independent from the
* argument. Modifications to the table data, or that to the argument have undefined
* effect on the other object. If it is important to decouple them, you can use a
* {@link ArrayFuncs#deepClone(Object)} of your original data as an argument.
*
* @throws FitsException if the argument is not a suitable representation of FITS data in columns
*
* @see BinaryTable#fromColumnMajor(Object[])
*
* @since 1.19
*/
public static AsciiTable fromColumnMajor(Object[] columns) throws FitsException {
AsciiTable t = new AsciiTable();
for (int i = 0; i < columns.length; i++) {
try {
t.addColumn(columns[i]);
} catch (Exception e) {
throw new FitsException("col[" + i + "]: " + e.getMessage(), e);
}
}
return t;
}
void setColumnName(int col, String value)
throws IllegalArgumentException, IndexOutOfBoundsException, HeaderCardException {
HeaderCard.validateChars(value);
names[col] = value;
}
/**
* Checks if the integer value of a specific key requires long
value type to store.
*
* @param h the header
* @param key the keyword to check
*
* @return true
if the keyword exists and has an integer value that is outside the range of
* int
. Otherwise false
*
* @see #guessI10Type(int, Header, boolean)
*/
private boolean requiresLong(Header h, IFitsHeader key, Long dft) {
long l = h.getLongValue(key, dft);
if (l == dft) {
return false;
}
return (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE);
}
/**
* Guesses what type of values to use to return I10 type table values. Depending on the range of represented values
* I10 may fit into int
types, or else require long
type arrays. Therefore, the method
* checks for the presence of standard column limit keywords TLMINn/TLMAXn and TDMINn/TDMAXn and if these exist and
* are outside of the range of an int
then the call will return long.class
. If the header
* does not define the data limits (fully), it will return the class the caller prefers. Otherwise (data limits were
* defined and fit into the int
range) int.class
will be returned.
*
* @param col the 0-based table column index
* @param h the header
* @param preferInt whether we prefer int.class
over long.class
in case the header does
* not provide us with a clue.
*
* @return long.class
if the data requires long or we prefer it. Othwerwise
* int.class
*
* @see #AsciiTable(Header, boolean)
*/
private Class> guessI10Type(int col, Header h, boolean preferInt) {
col++;
if (requiresLong(h, TLMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TLMAXn.n(col), Long.MIN_VALUE)
|| requiresLong(h, TDMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TDMAXn.n(col), Long.MIN_VALUE)) {
return long.class;
}
if ((h.containsKey(TLMINn.n(col)) || h.containsKey(TDMINn.n(col))) //
&& (h.containsKey(TLMAXn.n(col)) || h.containsKey(TDMAXn.n(col)))) {
// There are keywords defining both min/max values, and none of them require long types...
return int.class;
}
return preferInt ? int.class : long.class;
}
/**
* Return the data type in the specified column, such as int.class
or String.class
.
*
* @param col The 0-based column index
*
* @return the class of data in the specified column.
*
* @since 1.16
*/
public final Class> getColumnType(int col) {
return types[col];
}
int addColInfo(int col, Cursor iter) {
String tform = null;
if (types[col] == String.class) {
tform = "A" + lengths[col];
} else if (types[col] == int.class || types[col] == long.class) {
tform = "I" + lengths[col];
} else if (types[col] == float.class) {
tform = "E" + lengths[col] + ".0";
} else if (types[col] == double.class) {
tform = "D" + lengths[col] + ".0";
}
Standard.context(AsciiTable.class);
if (names[col] != null) {
iter.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), names[col]));
}
iter.add(HeaderCard.create(Standard.TFORMn.n(col + 1), tform));
iter.add(HeaderCard.create(Standard.TBCOLn.n(col + 1), offsets[col] + 1));
Standard.context(null);
return lengths[col];
}
@Override
public int addColumn(Object newCol) throws FitsException, IllegalArgumentException {
if (newCol == null) {
throw new FitsException("data is null");
}
if (!newCol.getClass().isArray()) {
throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName());
}
int maxLen = 1;
if (newCol instanceof String[]) {
String[] sa = (String[]) newCol;
for (String element : sa) {
if (element != null && element.length() > maxLen) {
maxLen = element.length();
}
}
} else if (newCol instanceof double[]) {
maxLen = DOUBLE_MAX_LENGTH;
} else if (newCol instanceof int[]) {
maxLen = INT_MAX_LENGTH;
} else if (newCol instanceof long[]) {
maxLen = LONG_MAX_LENGTH;
} else if (newCol instanceof float[]) {
maxLen = FLOAT_MAX_LENGTH;
} else {
throw new FitsException(
"No AsciiTable support for elements of " + newCol.getClass().getComponentType().getName());
}
addColumn(newCol, maxLen);
// Invalidate the buffer
buffer = null;
return nFields;
}
/**
* Adds an ASCII table column with the specified ASCII text width for storing its elements.
*
* @param newCol The new column data, which must be one of: int[]
,
* long[]
, float[]
, double[]
, or else
* String[]
. If the table already contains data, the length of the
* array must match the number of rows already contained in the table.
* @param width the ASCII text width of the for the column entries (without the string
* termination).
*
* @return the number of columns after this one is added.
*
* @throws IllegalArgumentException if the column data is not an array or the specified text width
is
* ≤1.
* @throws FitsException if the column us of an unsupported data type or if the number of entries does
* not match the number of rows already contained in the table.
*
* @see #addColumn(Object)
*/
public int addColumn(Object newCol, int width) throws FitsException, IllegalArgumentException {
if (width < 1) {
throw new IllegalArgumentException("Illegal ASCII column width: " + width);
}
if (!newCol.getClass().isArray()) {
throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName());
}
if (nFields > 0 && Array.getLength(newCol) != nRows) {
throw new FitsException(
"Mismatched number of rows: expected " + nRows + ", got " + Array.getLength(newCol) + "rows.");
}
if (nFields == 0) {
nRows = Array.getLength(newCol);
}
Class> type = ArrayFuncs.getBaseClass(newCol);
if (type != int.class && type != long.class && type != float.class && type != double.class
&& type != String.class) {
throw new FitsException("No AsciiTable support for elements of " + type.getName());
}
data = Arrays.copyOf(data, nFields + 1);
offsets = Arrays.copyOf(offsets, nFields + 1);
lengths = Arrays.copyOf(lengths, nFields + 1);
types = Arrays.copyOf(types, nFields + 1);
nulls = Arrays.copyOf(nulls, nFields + 1);
names = Arrays.copyOf(names, nFields + 1);
data[nFields] = newCol;
offsets[nFields] = rowLen + 1;
lengths[nFields] = width;
types[nFields] = ArrayFuncs.getBaseClass(newCol);
names[nFields] = TableHDU.getDefaultColumnName(nFields);
rowLen += width + 1;
if (isNull != null) {
boolean[] newIsNull = new boolean[nRows * (nFields + 1)];
// Fix the null pointers.
int add = 0;
for (int i = 0; i < isNull.length; i++) {
if (i % nFields == 0) {
add++;
}
if (isNull[i]) {
newIsNull[i + add] = true;
}
}
isNull = newIsNull;
}
nFields++;
// Invalidate the buffer
buffer = null;
return nFields;
}
/**
* Beware that adding rows to ASCII tables may be very inefficient. Avoid addding more than a few rows if you can.
*/
@Override
public int addRow(Object[] newRow) throws FitsException {
try {
// If there are no fields, then this is the
// first row. We need to add in each of the columns
// to get the descriptors set up.
if (nFields == 0) {
for (Object element : newRow) {
addColumn(element);
}
} else {
for (int i = 0; i < nFields; i++) {
Object o = ArrayFuncs.newInstance(types[i], nRows + 1);
System.arraycopy(data[i], 0, o, 0, nRows);
System.arraycopy(newRow[i], 0, o, nRows, 1);
data[i] = o;
}
nRows++;
}
// Invalidate the buffer
buffer = null;
return nRows;
} catch (Exception e) {
throw new FitsException("Error adding row:" + e.getMessage(), e);
}
}
@Override
public void deleteColumns(int start, int len) throws FitsException {
ensureData();
Object[] newData = new Object[nFields - len];
int[] newOffsets = new int[nFields - len];
int[] newLengths = new int[nFields - len];
Class>[] newTypes = new Class[nFields - len];
String[] newNulls = new String[nFields - len];
// Copy in the initial stuff...
System.arraycopy(data, 0, newData, 0, start);
// Don't do the offsets here.
System.arraycopy(lengths, 0, newLengths, 0, start);
System.arraycopy(types, 0, newTypes, 0, start);
System.arraycopy(nulls, 0, newNulls, 0, start);
// Copy in the final
System.arraycopy(data, start + len, newData, start, nFields - start - len);
// Don't do the offsets here.
System.arraycopy(lengths, start + len, newLengths, start, nFields - start - len);
System.arraycopy(types, start + len, newTypes, start, nFields - start - len);
System.arraycopy(nulls, start + len, newNulls, start, nFields - start - len);
for (int i = start; i < start + len; i++) {
rowLen -= lengths[i] + 1;
}
data = newData;
offsets = newOffsets;
lengths = newLengths;
types = newTypes;
nulls = newNulls;
if (isNull != null) {
boolean found = false;
boolean[] newIsNull = new boolean[nRows * (nFields - len)];
for (int i = 0; i < nRows; i++) {
int oldOff = nFields * i;
int newOff = (nFields - len) * i;
for (int col = 0; col < start; col++) {
newIsNull[newOff + col] = isNull[oldOff + col];
found = found || isNull[oldOff + col];
}
for (int col = start + len; col < nFields; col++) {
newIsNull[newOff + col - len] = isNull[oldOff + col];
found = found || isNull[oldOff + col];
}
}
if (found) {
isNull = newIsNull;
} else {
isNull = null;
}
}
// Invalidate the buffer
buffer = null;
nFields -= len;
}
/**
* Beware that repeatedly deleting rows from ASCII tables may be very inefficient. Avoid calling this more than once
* (or a few times) if you can.
*/
@Override
public void deleteRows(int start, int len) throws FitsException {
if (nRows == 0 || start < 0 || start >= nRows || len <= 0) {
return;
}
if (start + len > nRows) {
len = nRows - start;
}
ensureData();
for (int i = 0; i < nFields; i++) {
try {
Object o = ArrayFuncs.newInstance(types[i], nRows - len);
System.arraycopy(data[i], 0, o, 0, start);
System.arraycopy(data[i], start + len, o, start, nRows - len - start);
data[i] = o;
} catch (Exception e) {
throw new FitsException("Error deleting row: " + e.getMessage(), e);
}
}
nRows -= len;
}
@Override
protected void loadData(ArrayDataInput in) throws IOException, FitsException {
currInput = in;
if (buffer == null) {
getBuffer((long) nRows * rowLen, 0);
}
data = new Object[nFields];
for (int i = 0; i < nFields; i++) {
data[i] = ArrayFuncs.newInstance(types[i], nRows);
}
bp.setOffset(0);
int rowOffset;
for (int i = 0; i < nRows; i++) {
rowOffset = rowLen * i;
for (int j = 0; j < nFields; j++) {
try {
if (!extractElement(rowOffset + offsets[j], lengths[j], data, j, i, nulls[j])) {
if (isNull == null) {
isNull = new boolean[nRows * nFields];
}
isNull[j + i * nFields] = true;
}
} catch (ArrayIndexOutOfBoundsException e) {
throw new FitsException("not enough data: " + e, e);
}
}
}
}
@Override
public void read(ArrayDataInput in) throws FitsException {
currInput = in;
super.read(in);
}
/**
* Move an element from the buffer into a data array.
*
* @param offset The offset within buffer at which the element starts.
* @param length The number of bytes in the buffer for the element.
* @param array An array of objects, each of which is a simple array.
* @param col Which element of array is to be modified?
* @param row Which index into that element is to be modified?
* @param nullFld What string signifies a null element?
*
* @throws FitsException if the operation failed
*/
private boolean extractElement(int offset, int length, Object[] array, int col, int row, String nullFld)
throws FitsException {
bp.setOffset(offset);
if (nullFld != null) {
String s = bp.getString(length);
if (s.trim().equals(nullFld)) {
return false;
}
bp.skip(-length);
}
try {
if (array[col] instanceof String[]) {
((String[]) array[col])[row] = bp.getString(length);
} else if (array[col] instanceof int[]) {
((int[]) array[col])[row] = bp.getInt(length);
} else if (array[col] instanceof float[]) {
((float[]) array[col])[row] = bp.getFloat(length);
} else if (array[col] instanceof double[]) {
((double[]) array[col])[row] = bp.getDouble(length);
} else if (array[col] instanceof long[]) {
((long[]) array[col])[row] = bp.getLong(length);
} else {
throw new FitsException("Invalid type for ASCII table conversion:" + array[col]);
}
} catch (FormatException e) {
throw new FitsException("Error parsing data at row,col:" + row + "," + col + " ", e);
}
return true;
}
@Override
protected void fillHeader(Header h) {
h.deleteKey(Standard.SIMPLE);
h.deleteKey(Standard.EXTEND);
Standard.context(AsciiTable.class);
Cursor c = h.iterator();
c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_ASCIITABLE));
c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue()));
c.add(HeaderCard.create(Standard.NAXIS, 2));
c.add(HeaderCard.create(Standard.NAXIS1, rowLen));
c.add(HeaderCard.create(Standard.NAXIS2, nRows));
c.add(HeaderCard.create(Standard.PCOUNT, 0));
c.add(HeaderCard.create(Standard.GCOUNT, 1));
c.add(HeaderCard.create(Standard.TFIELDS, nFields));
for (int i = 0; i < nFields; i++) {
addColInfo(i, c);
}
Standard.context(null);
}
/**
* Read some data into the buffer.
*/
private void getBuffer(long size, long offset) throws IOException, FitsException {
if (currInput == null) {
throw new IOException("No stream open to read");
}
if (size > Integer.MAX_VALUE) {
throw new FitsException("Cannot read ASCII table > 2 GB");
}
buffer = new byte[(int) size];
if (offset != 0) {
FitsUtil.reposition(currInput, offset);
}
currInput.readFully(buffer);
bp = new ByteParser(buffer);
}
/**
*
* Returns the data for a particular column in as a flattened 1D array of elements. See {@link #addColumn(Object)}
* for more information about the format of data elements in general.
*
*
* @param col The 0-based column index.
*
* @return an array of primitives (for scalar columns), or else an Object[]
array.
*
* @throws FitsException if the table could not be accessed
*
* @see #setColumn(int, Object)
* @see #getElement(int, int)
* @see #getNCols()
*/
@Override
public Object getColumn(int col) throws FitsException {
ensureData();
return data[col];
}
@Override
@SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intended exposure of mutable data")
protected Object[] getCurrentData() {
return data;
}
@Override
public Object[] getData() throws FitsException {
return (Object[]) super.getData();
}
@Override
public Object getElement(int row, int col) throws FitsException {
if (data != null) {
return singleElement(row, col);
}
return parseSingleElement(row, col);
}
@Override
public int getNCols() {
return nFields;
}
@Override
public int getNRows() {
return nRows;
}
@Override
public Object[] getRow(int row) throws FitsException {
if (data != null) {
return singleRow(row);
}
return parseSingleRow(row);
}
/**
* Get the number of bytes in a row
*
* @return The number of bytes for a single row in the table.
*/
public int getRowLen() {
return rowLen;
}
@Override
protected long getTrueSize() {
return (long) nRows * rowLen;
}
/**
* Checks if an element is null
.
*
* @param row The 0-based row
* @param col The 0-based column
*
* @return if the given element has been nulled.
*/
public boolean isNull(int row, int col) {
if (isNull != null) {
return isNull[row * nFields + col];
}
return false;
}
/**
* Read a single element from the table. This returns an array of dimension 1.
*
* @throws FitsException if the operation failed
*/
private Object parseSingleElement(int row, int col) throws FitsException {
Object[] res = new Object[1];
try {
getBuffer(lengths[col], getFileOffset() + (long) row * (long) rowLen + offsets[col]);
} catch (IOException e) {
buffer = null;
throw new FitsException("Unable to read element", e);
}
res[0] = ArrayFuncs.newInstance(types[col], 1);
boolean success = extractElement(0, lengths[col], res, 0, 0, nulls[col]);
buffer = null;
return success ? res[0] : null;
}
/**
* Read a single row from the table. This returns a set of arrays of dimension 1.
*
* @throws FitsException if the operation failed
*/
private Object[] parseSingleRow(int row) throws FitsException {
Object[] res = new Object[nFields];
try {
getBuffer(rowLen, getFileOffset() + (long) row * (long) rowLen);
} catch (IOException e) {
throw new FitsException("Unable to read row", e);
}
for (int i = 0; i < nFields; i++) {
res[i] = ArrayFuncs.newInstance(types[i], 1);
if (!extractElement(offsets[i], lengths[i], res, i, 0, nulls[i])) {
res[i] = null;
}
}
// Invalidate buffer for future use.
buffer = null;
return res;
}
@Override
public void setColumn(int col, Object newData) throws FitsException {
ensureData();
if (col < 0 || col >= nFields || newData.getClass() != data[col].getClass()
|| Array.getLength(newData) != Array.getLength(data[col])) {
throw new FitsException("Invalid column/column mismatch:" + col);
}
data[col] = newData;
// Invalidate the buffer.
buffer = null;
}
@Override
public void setElement(int row, int col, Object newData) throws FitsException {
ensureData();
try {
System.arraycopy(newData, 0, data[col], row, 1);
} catch (Exception e) {
throw new FitsException("Incompatible element:" + row + "," + col, e);
}
setNull(row, col, false);
// Invalidate the buffer
buffer = null;
}
/**
* Mark (or unmark) an element as null. Note that if this FITS file is latter written out, a TNULL keyword needs to
* be defined in the corresponding header. This routine does not add an element for String columns.
*
* @param row The 0-based row.
* @param col The 0-based column.
* @param flag True if the element is to be set to null.
*/
public void setNull(int row, int col, boolean flag) {
if (flag) {
if (isNull == null) {
isNull = new boolean[nRows * nFields];
}
isNull[col + row * nFields] = true;
} else if (isNull != null) {
isNull[col + row * nFields] = false;
}
// Invalidate the buffer
buffer = null;
}
/**
* Set the null string for a columns. This is not a public method since we want users to call the method in
* AsciiTableHDU and update the header also.
*/
void setNullString(int col, String newNull) {
if (col >= 0 && col < nulls.length) {
nulls[col] = newNull;
}
}
@Override
public void setRow(int row, Object[] newData) throws FitsException {
if (row < 0 || row > nRows) {
throw new FitsException("Invalid row in setRow");
}
ensureData();
for (int i = 0; i < nFields; i++) {
try {
System.arraycopy(newData[i], 0, data[i], row, 1);
} catch (Exception e) {
throw new FitsException("Unable to modify row: incompatible data:" + row, e);
}
setNull(row, i, false);
}
// Invalidate the buffer
buffer = null;
}
/**
* Extract a single element from a table. This returns an array of length 1.
*/
private Object singleElement(int row, int col) {
Object res = null;
if (isNull == null || !isNull[row * nFields + col]) {
res = ArrayFuncs.newInstance(types[col], 1);
System.arraycopy(data[col], row, res, 0, 1);
}
return res;
}
/**
* Extract a single row from a table. This returns an array of Objects each of which is an array of length 1.
*/
private Object[] singleRow(int row) {
Object[] res = new Object[nFields];
for (int i = 0; i < nFields; i++) {
if (isNull == null || !isNull[row * nFields + i]) {
res[i] = ArrayFuncs.newInstance(types[i], 1);
System.arraycopy(data[i], row, res[i], 0, 1);
}
}
return res;
}
/**
* @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a
* new HDU and editing the header as necessary to incorporate custom entries. May be removed from
* the API in the future.
*/
@Override
public void updateAfterDelete(int oldNCol, Header hdr) throws FitsException {
int offset = 0;
for (int i = 0; i < nFields; i++) {
offsets[i] = offset;
hdr.addValue(TBCOLn.n(i + 1), offset + 1);
offset += lengths[i] + 1;
}
for (int i = nFields; i < oldNCol; i++) {
hdr.deleteKey(TBCOLn.n(i + 1));
}
hdr.addValue(NAXIS1, rowLen);
}
@Override
public void write(ArrayDataOutput str) throws FitsException {
// Make sure we have the data in hand.
if (str != currInput) {
ensureData();
}
// If buffer is still around we can just reuse it,
// since nothing we've done has invalidated it.
if (data == null) {
throw new FitsException("Attempt to write undefined ASCII Table");
}
if ((long) nRows * rowLen > Integer.MAX_VALUE) {
throw new FitsException("Cannot write ASCII table > 2 GB");
}
buffer = new byte[nRows * rowLen];
bp = new ByteParser(buffer);
for (int i = 0; i < buffer.length; i++) {
buffer[i] = (byte) ' ';
}
ByteFormatter bf = new ByteFormatter();
for (int i = 0; i < nRows; i++) {
for (int j = 0; j < nFields; j++) {
int offset = i * rowLen + offsets[j];
int len = lengths[j];
if (isNull != null && isNull[i * nFields + j]) {
if (nulls[j] == null) {
throw new FitsException("No null value set when needed");
}
bf.format(nulls[j], buffer, offset, len);
} else if (types[j] == String.class) {
String[] s = (String[]) data[j];
bf.format(s[i], buffer, offset, len);
} else if (types[j] == int.class) {
int[] ia = (int[]) data[j];
bf.format(ia[i], buffer, offset, len);
} else if (types[j] == float.class) {
float[] fa = (float[]) data[j];
bf.format(fa[i], buffer, offset, len);
} else if (types[j] == double.class) {
double[] da = (double[]) data[j];
bf.format(da[i], buffer, offset, len);
} else if (types[j] == long.class) {
long[] la = (long[]) data[j];
bf.format(la[i], buffer, offset, len);
}
}
}
// Now write the buffer.
try {
str.write(buffer);
FitsUtil.pad(str, buffer.length, (byte) ' ');
} catch (IOException e) {
throw new FitsException("Error writing ASCII Table data", e);
}
}
@Override
public AsciiTableHDU toHDU() {
Header h = new Header();
fillHeader(h);
return new AsciiTableHDU(h, this);
}
/**
*
* Controls how columns with format "I10
" are handled; this is tricky because some, but not all,
* integers that can be represented in 10 characters form 32-bit integers. Setting it true
may make it
* more likely to avoid unexpected type changes during round-tripping, but it also means that some values in I10
* columns may be impossible to read. The default behavior is to assume true
, and thus to treat I10
* columns as int
values.
*
*
* @param value if true
, format "I10" columns will be assumed int.class
, provided
* TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if false
, I10 columns
* that have no clear indication of data range will be assumed long.class
.
*
* @since 1.19
*
* @see AsciiTable#isI10PreferInt()
*/
public static void setI10PreferInt(boolean value) {
isI10PreferInt = value;
}
/**
* Checks if I10 columns should be treated as containing 32-bit int
values, rather than 64-bit
* long
values, when possible.
*
* @return true
if I10 columns should be treated as containing 32-bit int
values,
* otherwise false
.
*
* @since 1.19
*
* @see #setI10PreferInt(boolean)
*/
public static boolean isI10PreferInt() {
return isI10PreferInt;
}
}