From: W. Trevor King Date: Fri, 7 Oct 2011 04:02:15 +0000 (-0400) Subject: Begin versioning. X-Git-Tag: v1.06.0 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=17f866346e5d766cc1cf7864bb88e71ffd4b4d55;p=fits.git Begin versioning. --- a472a8dce77409d5b2088f190d5a3332a029332d diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..869a4e4 --- /dev/null +++ b/build.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/nom/tam/fits/AsciiTable.java b/src/nom/tam/fits/AsciiTable.java new file mode 100644 index 0000000..3d47c0e --- /dev/null +++ b/src/nom/tam/fits/AsciiTable.java @@ -0,0 +1,829 @@ +package nom.tam.fits; + +import nom.tam.util.*; +import java.lang.reflect.Array; +import java.io.IOException; +import java.io.EOFException; + +/** An ASCII table. */ +public class AsciiTable extends Data implements TableData { + + /** 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; + /** 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. + */ + ByteParser bp; + /** The actual stream used to input data */ + ArrayDataInput currInput; + + /** Create an ASCII table given a header */ + public AsciiTable(Header hdr) throws FitsException { + + 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]; + + for (int i = 0; i < nFields; i += 1) { + offsets[i] = hdr.getIntValue("TBCOL" + (i + 1)) - 1; + String s = hdr.getStringValue("TFORM" + (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] > 10) { + types[i] = long.class; + } else { + types[i] = int.class; + } + break; + case 'F': + case 'E': + types[i] = float.class; + break; + case 'D': + types[i] = double.class; + break; + } + + nulls[i] = hdr.getStringValue("TNULL" + (i + 1)); + if (nulls[i] != null) { + nulls[i] = nulls[i].trim(); + } + } + } + + /** 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]; + } + + /** Read in an ASCII table. Reading is deferred if + * we are reading from a random access device + */ + public void read(ArrayDataInput str) throws FitsException { + + setFileOffset(str); + currInput = str; + + if (str instanceof RandomAccess) { + + try { + str.skipBytes((long) nRows * rowLen); + } catch (IOException e) { + throw new FitsException("Error skipping data: " + e); + } + + } else { + try { + if ((long) rowLen * nRows > Integer.MAX_VALUE) { + throw new FitsException("Cannot read ASCII table > 2 GB"); + } + getBuffer(rowLen * nRows, 0); + } catch (IOException e) { + throw new FitsException("Error reading ASCII table:" + e); + } + } + + try { + str.skipBytes(FitsUtil.padding(nRows * rowLen)); + } catch (EOFException e) { + throw new PaddingException("EOF skipping padding after ASCII Table:" + e, this); + } catch (IOException e) { + throw new FitsException("Error skipping padding after ASCII Table:" + e); + } + } + + /** Read some data into the buffer. + */ + private void getBuffer(int size, long offset) throws IOException, FitsException { + + if (currInput == null) { + throw new IOException("No stream open to read"); + } + + buffer = new byte[size]; + if (offset != 0) { + FitsUtil.reposition(currInput, offset); + } + currInput.readFully(buffer); + bp = new ByteParser(buffer); + } + + /** Get the ASCII table information. + * This will actually do the read if it had previously been deferred + */ + public Object getData() throws FitsException { + + if (data == null) { + + data = new Object[nFields]; + + for (int i = 0; i < nFields; i += 1) { + data[i] = ArrayFuncs.newInstance(types[i], nRows); + } + + if (buffer == null) { + long newOffset = FitsUtil.findOffset(currInput); + try { + getBuffer(nRows * rowLen, fileOffset); + + } catch (IOException e) { + throw new FitsException("Error in deferred read -- file closed prematurely?:" + e); + } + FitsUtil.reposition(currInput, newOffset); + } + + bp.setOffset(0); + + int rowOffset; + for (int i = 0; i < nRows; i += 1) { + rowOffset = rowLen * i; + for (int j = 0; j < nFields; j += 1) { + 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; + } + } + } + } + + return data; + } + + /** 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? + */ + 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; + } + + /** Get a column of data */ + public Object getColumn(int col) throws FitsException { + if (data == null) { + getData(); + } + return data[col]; + } + + /** Get a row. If the data has not yet been read just + * read this row. + */ + public Object[] getRow(int row) throws FitsException { + + if (data != null) { + return singleRow(row); + } else { + return parseSingleRow(row); + } + } + + /** Get a single element as a one-d array. + * We return String's as arrays for consistency though + * they could be returned as a scalar. + */ + public Object getElement(int row, int col) throws FitsException { + if (data != null) { + return singleElement(row, col); + } else { + return parseSingleElement(row, col); + } + } + + /** 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 += 1) { + 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; + } + + /** 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; + } + + /** Read a single row from the table. This returns + * a set of arrays of dimension 1. + */ + private Object[] parseSingleRow(int row) throws FitsException { + + int offset = row * rowLen; + + Object[] res = new Object[nFields]; + + try { + getBuffer(rowLen, fileOffset + row * rowLen); + } catch (IOException e) { + throw new FitsException("Unable to read row"); + } + + for (int i = 0; i < nFields; i += 1) { + 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; + } + + /** Read a single element from the table. This returns + * an array of dimension 1. + */ + private Object parseSingleElement(int row, int col) throws FitsException { + + Object[] res = new Object[1]; + try { + getBuffer(lengths[col], fileOffset + row * rowLen + offsets[col]); + } catch (IOException e) { + buffer = null; + throw new FitsException("Unable to read element"); + } + res[0] = ArrayFuncs.newInstance(types[col], 1); + + if (extractElement(0, lengths[col], res, 0, 0, nulls[col])) { + buffer = null; + return res[0]; + + } else { + + buffer = null; + return null; + } + } + + /** Write the data to an output stream. + */ + public void write(ArrayDataOutput str) throws FitsException { + + // Make sure we have the data in hand. + + getData(); + // If buffer is still around we can just reuse it, + // since nothing we've done has invalidated it. + + if (buffer == null) { + + 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 += 1) { + buffer[i] = (byte) ' '; + } + + ByteFormatter bf = new ByteFormatter(); + bf.setTruncationThrow(false); + bf.setTruncateOnOverflow(true); + + for (int i = 0; i < nRows; i += 1) { + + for (int j = 0; j < nFields; j += 1) { + int offset = i * rowLen + offsets[j]; + int len = lengths[j]; + + try { + 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); + } + } + } catch (TruncationException e) { + System.err.println("Ignoring truncation error:" + i + "," + j); + } + } + } + } + + // 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"); + } + } + + /** Replace a column with new data. + */ + public void setColumn(int col, Object newData) throws FitsException { + if (data == null) { + getData(); + } + 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; + + } + + /** Modify a row in the table */ + public void setRow(int row, Object[] newData) throws FitsException { + if (row < 0 || row > nRows) { + throw new FitsException("Invalid row in setRow"); + } + + if (data == null) { + getData(); + } + for (int i = 0; i < nFields; i += 1) { + try { + System.arraycopy(newData[i], 0, data[i], row, 1); + } catch (Exception e) { + throw new FitsException("Unable to modify row: incompatible data:" + row); + } + setNull(row, i, false); + } + + // Invalidate the buffer + buffer = null; + + } + + /** Modify an element in the table */ + public void setElement(int row, int col, Object newData) throws FitsException { + + if (data == null) { + getData(); + } + try { + System.arraycopy(newData, 0, data[col], row, 1); + } catch (Exception e) { + throw new FitsException("Incompatible element:" + row + "," + col); + } + 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. + */ + 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; + } + + /** See if an element is null. + */ + public boolean isNull(int row, int col) { + if (isNull != null) { + return isNull[row * nFields + col]; + } else { + return false; + } + } + + /** Add a row to the table. Users should be cautious + * of calling this routine directly rather than the corresponding + * routine in AsciiTableHDU since this routine knows nothing + * of the FITS header modifications required. + */ + public int addColumn(Object newCol) throws FitsException { + int maxLen = 0; + if (newCol instanceof String[]) { + + String[] sa = (String[]) newCol; + for (int i = 0; i < sa.length; i += 1) { + if (sa[i] != null && sa[i].length() > maxLen) { + maxLen = sa[i].length(); + } + } + } else if (newCol instanceof double[]) { + maxLen = 24; + } else if (newCol instanceof int[]) { + maxLen = 10; + } else if (newCol instanceof long[]) { + maxLen = 20; + } else if (newCol instanceof float[]) { + maxLen = 16; + } + addColumn(newCol, maxLen); + + // Invalidate the buffer + buffer = null; + + return nFields; + } + + /** This version of addColumn allows the user to override + * the default length associated with each column type. + */ + public int addColumn(Object newCol, int length) throws FitsException { + + if (nFields > 0 && Array.getLength(newCol) != nRows) { + throw new FitsException("New column has different number of rows"); + } + + if (nFields == 0) { + nRows = Array.getLength(newCol); + } + + Object[] newData = new Object[nFields + 1]; + int[] newOffsets = new int[nFields + 1]; + int[] newLengths = new int[nFields + 1]; + Class[] newTypes = new Class[nFields + 1]; + String[] newNulls = new String[nFields + 1]; + + System.arraycopy(data, 0, newData, 0, nFields); + System.arraycopy(offsets, 0, newOffsets, 0, nFields); + System.arraycopy(lengths, 0, newLengths, 0, nFields); + System.arraycopy(types, 0, newTypes, 0, nFields); + System.arraycopy(nulls, 0, newNulls, 0, nFields); + + data = newData; + offsets = newOffsets; + lengths = newLengths; + types = newTypes; + nulls = newNulls; + + newData[nFields] = newCol; + offsets[nFields] = rowLen + 1; + lengths[nFields] = length; + types[nFields] = ArrayFuncs.getBaseClass(newCol); + + rowLen += length + 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 += 1) { + if (i % nFields == 0) { + add += 1; + } + if (isNull[i]) { + newIsNull[i + add] = true; + } + } + isNull = newIsNull; + } + nFields += 1; + + // Invalidate the buffer + buffer = null; + + return nFields; + } + + /** Add a row to the FITS table. */ + public int addRow(Object[] newRow) throws FitsException { + + // 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 (int i = 0; i < newRow.length; i += 1) { + addColumn(newRow[i]); + } + } else { + for (int i = 0; i < nFields; i += 1) { + try { + 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; + } catch (Exception e) { + throw new FitsException("Error adding row:" + e); + } + } + nRows += 1; + } + + // Invalidate the buffer + buffer = null; + + return nRows; + } + + /** Delete rows from a FITS table */ + 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; + } + getData(); + try { + for (int i = 0; i < nFields; i += 1) { + 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; + } + nRows -= len; + } catch (Exception e) { + throw new FitsException("Error deleting row:" + e); + } + } + + /** 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; + } + } + + /** Return the size of the data section */ + protected long getTrueSize() { + return (long) (nRows) * rowLen; + } + + /** Fill in a header with information that points to this + * data. + */ + public void fillHeader(Header hdr) { + + try { + hdr.setXtension("TABLE"); + hdr.setBitpix(8); + hdr.setNaxes(2); + hdr.setNaxis(1, rowLen); + hdr.setNaxis(2, nRows); + Cursor iter = hdr.iterator(); + iter.setKey("NAXIS2"); + iter.next(); + iter.add("PCOUNT", new HeaderCard("PCOUNT", 0,"ntf::asciitable:pcount:1")); + iter.add("GCOUNT", new HeaderCard("GCOUNT", 1, "ntf::asciitable:gcount:1")); + iter.add("TFIELDS", new HeaderCard("TFIELDS", nFields, "ntf::asciitable:tfields:1")); + + for (int i = 0; i < nFields; i += 1) { + addColInfo(i, iter); + } + + } catch (HeaderCardException e) { + System.err.println("ImpossibleException in fillHeader:" + e); + } + + } + + int addColInfo(int col, Cursor iter) throws HeaderCardException { + + 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"; + } + String key; + key = "TFORM" + (col + 1); + iter.add(key, new HeaderCard(key, tform, "ntf::asciitable:tformN:1")); + key = "TBCOL" + (col + 1); + iter.add(key, new HeaderCard(key, offsets[col] + 1, "ntf::asciitable:tbcolN:1")); + return lengths[col]; + } + + /** Get the number of rows in the table */ + public int getNRows() { + return nRows; + } + + /** Get the number of columns in the table */ + public int getNCols() { + return nFields; + } + + /** Get the number of bytes in a row */ + public int getRowLen() { + return rowLen; + } + + /** Delete columns from the table. + */ + public void deleteColumns(int start, int len) throws FitsException { + + getData(); + + 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 += 1) { + 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 += 1) { + int oldOff = nFields * i; + int newOff = (nFields - len) * i; + for (int col = 0; col < start; col += 1) { + newIsNull[newOff + col] = isNull[oldOff + col]; + found = found || isNull[oldOff + col]; + } + for (int col = start + len; col < nFields; col += 1) { + 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; + } + + /** This is called after we delete columns. The HDU + * doesn't know how to update the TBCOL entries. + */ + public void updateAfterDelete(int oldNCol, Header hdr) throws FitsException { + + int offset = 0; + for (int i = 0; i < nFields; i += 1) { + offsets[i] = offset; + hdr.addValue("TBCOL" + (i + 1), offset + 1, "ntf::asciitable:tbcolN:2"); + offset += lengths[i] + 1; + } + for (int i = nFields; i < oldNCol; i += 1) { + hdr.deleteKey("TBCOL" + (i + 1)); + } + + hdr.addValue("NAXIS1", rowLen, "ntf::asciitable:naxis1:1"); + } +} diff --git a/src/nom/tam/fits/AsciiTableHDU.java b/src/nom/tam/fits/AsciiTableHDU.java new file mode 100644 index 0000000..344c84a --- /dev/null +++ b/src/nom/tam/fits/AsciiTableHDU.java @@ -0,0 +1,211 @@ +package nom.tam.fits; + +import java.io.IOException; +import nom.tam.util.*; +import java.util.Iterator; + + +/* + * Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +/** + * FITS ASCII table header/data unit + */ +public class AsciiTableHDU extends TableHDU { + + /** Just a copy of myData with the correct type */ + AsciiTable data; + /** The standard column stems for an ASCII table. + * Note that TBCOL is not included here -- it needs to + * be handled specially since it does not simply shift. + */ + private String[] keyStems = {"TFORM", "TZERO", "TNULL", "TTYPE", "TUNIT"}; + + /** + * Create an ascii table header/data unit. + * @param header the template specifying the ascii table. + * @param data the FITS data structure containing the table data. + * @exception FitsException if there was a problem with the header. + */ + public AsciiTableHDU(Header h, Data d) { + super((TableData) d); + myHeader = h; + data = (AsciiTable) d; + myData = d; + } + + /** + * Check that this is a valid ascii table header. + * @param header to validate. + * @return true if this is an ascii table header. + */ + public static boolean isHeader(Header header) { + return header.getStringValue("XTENSION").trim().equals("TABLE"); + } + + /** + * Check that this HDU has a valid header. + * @return true if this HDU has a valid header. + */ + public boolean isHeader() { + return isHeader(myHeader); + } + + /** Check if this data is usable as an ASCII table. + */ + public static boolean isData(Object o) { + + if (o instanceof Object[]) { + Object[] oo = (Object[]) o; + for (int i = 0; i < oo.length; i += 1) { + if (oo[i] instanceof String[] + || oo[i] instanceof int[] + || oo[i] instanceof long[] + || oo[i] instanceof float[] + || oo[i] instanceof double[]) { + continue; + } + return false; + } + return true; + } else { + return false; + } + } + + /** + * Create a Data object to correspond to the header description. + * @return An unfilled Data object which can be used to read + * in the data for this HDU. + * @exception FitsException if the Data object could not be created + * from this HDU's Header + */ + public static Data manufactureData(Header hdr) throws FitsException { + return new AsciiTable(hdr); + } + + /** Create an empty data structure corresponding to the input header. + */ + public Data manufactureData() throws FitsException { + return manufactureData(myHeader); + } + + /** Create a header to match the input data. */ + public static Header manufactureHeader(Data d) throws FitsException { + Header hdr = new Header(); + d.fillHeader(hdr); + Iterator iter = hdr.iterator(); + return hdr; + } + + /** Create a ASCII table data structure from an array of objects + * representing the columns. + */ + public static Data encapsulate(Object o) throws FitsException { + + Object[] oo = (Object[]) o; + AsciiTable d = new AsciiTable(); + for (int i = 0; i < oo.length; i += 1) { + d.addColumn(oo[i]); + } + return d; + } + + /** + * Skip the ASCII table and throw an exception. + * @param stream the stream from which the data is read. + */ + public void readData(ArrayDataInput stream) + throws FitsException { + myData.read(stream); + } + + /** Mark an entry as null. + */ + public void setNull(int row, int col, boolean flag) { + + if (flag) { + String nullStr = myHeader.getStringValue("TNULL" + (col + 1)); + if (nullStr == null) { + setNullString(col, "NULL"); + } + } + data.setNull(row, col, flag); + } + + /** See if an element is null */ + public boolean isNull(int row, int col) { + return data.isNull(row, col); + } + + /** Set the null string for a column */ + public void setNullString(int col, String newNull) { + myHeader.positionAfterIndex("TBCOL", col + 1); + try { + myHeader.addValue("TNULL" + (col + 1), newNull, "ntf::asciitablehdu:tnullN:1"); + } catch (HeaderCardException e) { + System.err.println("Impossible exception in setNullString" + e); + } + data.setNullString(col, newNull); + } + + /** Add a column */ + public int addColumn(Object newCol) throws FitsException { + + data.addColumn(newCol); + + // Move the iterator to point after all the data describing + // the previous column. + + Cursor iter = + myHeader.positionAfterIndex("TBCOL", data.getNCols()); + + int rowlen = data.addColInfo(getNCols(), iter); + int oldRowlen = myHeader.getIntValue("NAXIS1"); + myHeader.setNaxis(1, rowlen + oldRowlen); + + int oldTfields = myHeader.getIntValue("TFIELDS"); + try { + myHeader.addValue("TFIELDS", oldTfields + 1, "ntf::asciitablehdu:tfields:1"); + } catch (Exception e) { + System.err.println("Impossible exception at addColumn:" + e); + } + return getNCols(); + } + + /** + * Print a little information about the data set. + */ + public void info() { + System.out.println("ASCII Table:"); + System.out.println(" Header:"); + System.out.println(" Number of fields:" + myHeader.getIntValue("TFIELDS")); + System.out.println(" Number of rows: " + myHeader.getIntValue("NAXIS2")); + System.out.println(" Length of row: " + myHeader.getIntValue("NAXIS1")); + System.out.println(" Data:"); + Object[] data = (Object[]) getKernel(); + for (int i = 0; i < getNCols(); i += 1) { + System.out.println(" " + i + ":" + ArrayFuncs.arrayDescription(data[i])); + } + } + + /** Return the FITS data structure associated with this HDU. + */ + public Data getData() { + return data; + } + + /** Return the keyword column stems for an ASCII table. + */ + public String[] columnKeyStems() { + return keyStems; + } +} + diff --git a/src/nom/tam/fits/BadHeaderException.java b/src/nom/tam/fits/BadHeaderException.java new file mode 100644 index 0000000..168e85c --- /dev/null +++ b/src/nom/tam/fits/BadHeaderException.java @@ -0,0 +1,26 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +/** This exception indicates that an error + * was detected while parsing a FITS header record. + */ +public class BadHeaderException + extends FitsException { + + public BadHeaderException() { + super(); + } + + public BadHeaderException(String msg) { + super(msg); + } +} diff --git a/src/nom/tam/fits/BasicHDU.java b/src/nom/tam/fits/BasicHDU.java new file mode 100644 index 0000000..419b2cb --- /dev/null +++ b/src/nom/tam/fits/BasicHDU.java @@ -0,0 +1,498 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +import java.io.IOException; + +import nom.tam.util.ArrayDataInput; +import nom.tam.util.ArrayDataOutput; +import java.util.Iterator; + +import java.util.Date; + +/** This abstract class is the parent of all HDU types. + * It provides basic functionality for an HDU. + */ +public abstract class BasicHDU implements FitsElement { + + public static final int BITPIX_BYTE = 8; + public static final int BITPIX_SHORT = 16; + public static final int BITPIX_INT = 32; + public static final int BITPIX_LONG = 64; + public static final int BITPIX_FLOAT = -32; + public static final int BITPIX_DOUBLE = -64; + /** The associated header. */ + protected Header myHeader = null; + /** The associated data unit. */ + protected Data myData = null; + /** Is this the first HDU in a FITS file? */ + protected boolean isPrimary = false; + + /** Create a Data object to correspond to the header description. + * @return An unfilled Data object which can be used to read + * in the data for this HDU. + * @exception FitsException if the Data object could not be created + * from this HDU's Header + */ + abstract Data manufactureData() throws FitsException; + + /** Skip the Data object immediately after the given Header object on + * the given stream object. + * @param stream the stream which contains the data. + * @param Header template indicating length of Data section + * @exception IOException if the Data object could not be skipped. + */ + public static void skipData(ArrayDataInput stream, Header hdr) + throws IOException { + stream.skipBytes(hdr.getDataSize()); + } + + /** Skip the Data object for this HDU. + * @param stream the stream which contains the data. + * @exception IOException if the Data object could not be skipped. + */ + public void skipData(ArrayDataInput stream) + throws IOException { + skipData(stream, myHeader); + } + + /** Read in the Data object for this HDU. + * @param stream the stream from which the data is read. + * @exception FitsException if the Data object could not be created + * from this HDU's Header + */ + public void readData(ArrayDataInput stream) + throws FitsException { + myData = null; + try { + myData = manufactureData(); + } finally { + // if we cannot build a Data object, skip this section + if (myData == null) { + try { + skipData(stream, myHeader); + } catch (Exception e) { + } + } + } + + myData.read(stream); + } + + /** Get the associated header */ + public Header getHeader() { + return myHeader; + } + + /** Get the starting offset of the HDU */ + public long getFileOffset() { + return myHeader.getFileOffset(); + } + + /** Get the associated Data object*/ + public Data getData() { + return myData; + } + + /** Get the non-FITS data object */ + public Object getKernel() { + try { + return myData.getKernel(); + } catch (FitsException e) { + return null; + } + } + + /** Get the total size in bytes of the HDU. + * @return The size in bytes. + */ + public long getSize() { + int size = 0; + + if (myHeader != null) { + size += myHeader.getSize(); + } + if (myData != null) { + size += myData.getSize(); + } + return size; + } + + /** Check that this is a valid header for the HDU. + * @param header to validate. + * @return true if this is a valid header. + */ + public static boolean isHeader(Header header) { + return false; + } + + /** Print out some information about this HDU. + */ + public abstract void info(); + + /** Check if a field is present and if so print it out. + * @param The header keyword. + * @param Was it found in the header? + */ + boolean checkField(String name) { + String value = myHeader.getStringValue(name); + if (value == null) { + return false; + } + + return true; + } + + /* Read out the HDU from the data stream. This + * will overwrite any existing header and data components. + */ + public void read(ArrayDataInput stream) + throws FitsException, IOException { + myHeader = Header.readHeader(stream); + myData = myHeader.makeData(); + myData.read(stream); + } + + /* Write out the HDU + * @param stream The data stream to be written to. + */ + public void write(ArrayDataOutput stream) + throws FitsException { + if (myHeader != null) { + myHeader.write(stream); + } + if (myData != null) { + myData.write(stream); + } + try { + stream.flush(); + } catch (java.io.IOException e) { + throw new FitsException("Error flushing at end of HDU: " + + e.getMessage()); + } + } + + /** Is the HDU rewriteable */ + public boolean rewriteable() { + return myHeader.rewriteable() && myData.rewriteable(); + } + + /** Rewrite the HDU */ + public void rewrite() + throws FitsException, IOException { + + if (rewriteable()) { + myHeader.rewrite(); + myData.rewrite(); + } else { + throw new FitsException("Invalid attempt to rewrite HDU"); + } + } + + /** + * Get the String value associated with keyword. + * @param hdr the header piece of an HDU + * @param keyword the FITS keyword + * @return either null or a String with leading/trailing + * blanks stripped. + */ + public String getTrimmedString(String keyword) { + String s = myHeader.getStringValue(keyword); + if (s != null) { + s = s.trim(); + } + return s; + } + + public int getBitPix() + throws FitsException { + int bitpix = myHeader.getIntValue("BITPIX", -1); + switch (bitpix) { + case BITPIX_BYTE: + case BITPIX_SHORT: + case BITPIX_INT: + case BITPIX_FLOAT: + case BITPIX_DOUBLE: + break; + default: + throw new FitsException("Unknown BITPIX type " + bitpix); + } + + return bitpix; + } + + public int[] getAxes() + throws FitsException { + int nAxis = myHeader.getIntValue("NAXIS", 0); + if (nAxis < 0) { + throw new FitsException("Negative NAXIS value " + nAxis); + } + if (nAxis > 999) { + throw new FitsException("NAXIS value " + nAxis + " too large"); + } + + if (nAxis == 0) { + return null; + } + + int[] axes = new int[nAxis]; + for (int i = 1; i <= nAxis; i++) { + axes[nAxis - i] = myHeader.getIntValue("NAXIS" + i, 0); + } + + return axes; + } + + public int getParameterCount() { + return myHeader.getIntValue("PCOUNT", 0); + } + + public int getGroupCount() { + return myHeader.getIntValue("GCOUNT", 1); + } + + public double getBScale() { + return myHeader.getDoubleValue("BSCALE", 1.0); + } + + public double getBZero() { + return myHeader.getDoubleValue("BZERO", 0.0); + } + + public String getBUnit() { + return getTrimmedString("BUNIT"); + } + + public int getBlankValue() + throws FitsException { + if (!myHeader.containsKey("BLANK")) { + throw new FitsException("BLANK undefined"); + } + return myHeader.getIntValue("BLANK"); + } + + /** + * Get the FITS file creation date as a Date object. + * @return either null or a Date object + */ + public Date getCreationDate() { + try { + return new FitsDate(myHeader.getStringValue("DATE")).toDate(); + } catch (FitsException e) { + return null; + } + } + + /** + * Get the FITS file observation date as a Date object. + * @return either null or a Date object + */ + public Date getObservationDate() { + try { + return new FitsDate(myHeader.getStringValue("DATE-OBS")).toDate(); + } catch (FitsException e) { + return null; + } + } + + /** + * Get the name of the organization which created this FITS file. + * @return either null or a String object + */ + public String getOrigin() { + return getTrimmedString("ORIGIN"); + } + + /** + * Get the name of the telescope which was used to acquire the data in + * this FITS file. + * @return either null or a String object + */ + public String getTelescope() { + return getTrimmedString("TELESCOP"); + } + + /** + * Get the name of the instrument which was used to acquire the data in + * this FITS file. + * @return either null or a String object + */ + public String getInstrument() { + return getTrimmedString("INSTRUME"); + } + + /** + * Get the name of the person who acquired the data in this FITS file. + * @return either null or a String object + */ + public String getObserver() { + return getTrimmedString("OBSERVER"); + } + + /** + * Get the name of the observed object in this FITS file. + * @return either null or a String object + */ + public String getObject() { + return getTrimmedString("OBJECT"); + } + + /** + * Get the equinox in years for the celestial coordinate system in which + * positions given in either the header or data are expressed. + * @return either null or a String object + */ + public double getEquinox() { + return myHeader.getDoubleValue("EQUINOX", -1.0); + } + + /** + * Get the equinox in years for the celestial coordinate system in which + * positions given in either the header or data are expressed. + * @return either null or a String object + * @deprecated Replaced by getEquinox + * @see #getEquinox() + */ + public double getEpoch() { + return myHeader.getDoubleValue("EPOCH", -1.0); + } + + /** + * Return the name of the person who compiled the information in + * the data associated with this header. + * @return either null or a String object + */ + public String getAuthor() { + return getTrimmedString("AUTHOR"); + } + + /** + * Return the citation of a reference where the data associated with + * this header are published. + * @return either null or a String object + */ + public String getReference() { + return getTrimmedString("REFERENC"); + } + + /** + * Return the minimum valid value in the array. + * @return minimum value. + */ + public double getMaximumValue() { + return myHeader.getDoubleValue("DATAMAX"); + } + + /** + * Return the minimum valid value in the array. + * @return minimum value. + */ + public double getMinimumValue() { + return myHeader.getDoubleValue("DATAMIN"); + } + + /** Indicate whether HDU can be primary HDU. + * This method must be overriden in HDU types which can + * appear at the beginning of a FITS file. + */ + boolean canBePrimary() { + return false; + } + + /** Reset the input stream to the beginning of the HDU, i.e., the beginning of the header */ + public boolean reset() { + return myHeader.reset(); + } + + /** Indicate that an HDU is the first element of a FITS file. */ + void setPrimaryHDU(boolean newPrimary) throws FitsException { + + if (newPrimary && !canBePrimary()) { + throw new FitsException("Invalid attempt to make HDU of type:" + + this.getClass().getName() + " primary."); + } else { + this.isPrimary = newPrimary; + } + + // Some FITS readers don't like the PCOUNT and GCOUNT keywords + // in a primary array or they EXTEND keyword in extensions. + + if (isPrimary && !myHeader.getBooleanValue("GROUPS", false)) { + myHeader.deleteKey("PCOUNT"); + myHeader.deleteKey("GCOUNT"); + } + + if (isPrimary) { + HeaderCard card = myHeader.findCard("EXTEND"); + if (card == null) { + getAxes(); // Leaves the iterator pointing to the last NAXISn card. + myHeader.nextCard(); + myHeader.addValue("EXTEND", true, "ntf::basichdu:extend:1"); + } + } + + if (!isPrimary) { + + Iterator iter = myHeader.iterator(); + + int pcount = myHeader.getIntValue("PCOUNT", 0); + int gcount = myHeader.getIntValue("GCOUNT", 1); + int naxis = myHeader.getIntValue("NAXIS", 0); + myHeader.deleteKey("EXTEND"); + HeaderCard card; + HeaderCard pcard = myHeader.findCard("PCOUNT"); + HeaderCard gcard = myHeader.findCard("GCOUNT"); + + myHeader.getCard(2 + naxis); + if (pcard == null) { + myHeader.addValue("PCOUNT", pcount, "ntf::basichdu:pcount:1"); + } + if (gcard == null) { + myHeader.addValue("GCOUNT", gcount, "ntf::basichdu:gcount:1"); + } + iter = myHeader.iterator(); + } + + } + + /** Add information to the header */ + public void addValue(String key, boolean val, String comment) + throws HeaderCardException { + myHeader.addValue(key, val, comment); + } + + public void addValue(String key, int val, String comment) + throws HeaderCardException { + myHeader.addValue(key, val, comment); + } + + public void addValue(String key, double val, String comment) + throws HeaderCardException { + myHeader.addValue(key, val, comment); + } + + public void addValue(String key, String val, String comment) + throws HeaderCardException { + myHeader.addValue(key, val, comment); + } + + /** Get an HDU without content */ + public static BasicHDU getDummyHDU() { + try { + // Update suggested by Laurent Bourges + ImageData img = new ImageData((Object) null); + return FitsFactory.HDUFactory(ImageHDU.manufactureHeader(img), img); + } catch (FitsException e) { + System.err.println("Impossible exception in getDummyHDU"); + return null; + } + } +} diff --git a/src/nom/tam/fits/BinaryTable.java b/src/nom/tam/fits/BinaryTable.java new file mode 100644 index 0000000..103b4bf --- /dev/null +++ b/src/nom/tam/fits/BinaryTable.java @@ -0,0 +1,1777 @@ +package nom.tam.fits; + +/* Copyright: Thomas McGlynn 1997-2000. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +import java.io.*; +import nom.tam.util.*; +import java.lang.reflect.Array; +import java.util.Vector; + +/** This class defines the methods for accessing FITS binary table data. + */ +public class BinaryTable extends Data implements TableData { + + /** This is the area in which variable length column data lives. + */ + FitsHeap heap; + /** The number of bytes between the end of the data and the heap */ + int heapOffset; + // Added by A. Kovacs (4/1/08) + // as a way for checking whether the heap was initialized from stream... + /** Has the heap been read */ + boolean heapReadFromStream = false; + /** The sizes of each column (in number of entries per row) + */ + int[] sizes; + /** The dimensions of each column. + * If a column is a scalar then entry for that + * index is an array of length 0. + */ + int[][] dimens; + /** Info about column */ + int[] flags; + /** Flag indicating that we've given Variable length conversion warning. + * We only want to do that once per HDU. + */ + private boolean warnedOnVariableConversion = false; + final static int COL_CONSTANT = 0; + final static int COL_VARYING = 1; + final static int COL_COMPLEX = 2; + final static int COL_STRING = 4; + final static int COL_BOOLEAN = 8; + final static int COL_BIT = 16; + final static int COL_LONGVARY = 32; + /** The number of rows in the table. + */ + int nRow; + /** The number of columns in the table. + */ + int nCol; + /** The length in bytes of each row. + */ + int rowLen; + /** The base classes for the arrays in the table. + */ + Class[] bases; + /** An example of the structure of a row + */ + Object[] modelRow; + /** A pointer to the data in the columns. This + * variable is only used to assist in the + * construction of BinaryTable's that are defined + * to point to an array of columns. It is + * not generally filled. The ColumnTable is used + * to store the actual data of the BinaryTable. + */ + Object[] columns; + /** Where the data is actually stored. + */ + ColumnTable table; + /** The stream used to input the image + */ + ArrayDataInput currInput; + + /** Create a null binary table data segment. + */ + public BinaryTable() throws FitsException { + + try { + table = new ColumnTable(new Object[0], new int[0]); + } catch (TableException e) { + System.err.println("Impossible exception in BinaryTable() constructor" + e); + } + + heap = new FitsHeap(0); + extendArrays(0); + nRow = 0; + nCol = 0; + rowLen = 0; + } + + /** Create a binary table from given header information. + * + * @param header A header describing what the binary + * table should look like. + */ + public BinaryTable(Header myHeader) throws FitsException { + + long heapSizeL = myHeader.getLongValue("PCOUNT"); + long heapOffsetL = myHeader.getLongValue("THEAP"); + if (heapOffsetL > Integer.MAX_VALUE) { + throw new FitsException("Heap Offset > 2GB"); + } + heapOffset = (int) heapOffsetL; + if (heapSizeL > Integer.MAX_VALUE) { + throw new FitsException("Heap size > 2 GB"); + } + int heapSize = (int) heapSizeL; + + int rwsz = myHeader.getIntValue("NAXIS1"); + nRow = myHeader.getIntValue("NAXIS2"); + + // Subtract out the size of the regular table from + // the heap offset. + + if (heapOffset > 0) { + heapOffset -= nRow * rwsz; + } + + if (heapOffset < 0 || heapOffset > heapSize) { + throw new FitsException("Inconsistent THEAP and PCOUNT"); + } + + if (heapSize - heapOffset > Integer.MAX_VALUE) { + throw new FitsException("Unable to allocate heap > 2GB"); + } + + heap = new FitsHeap((heapSize - heapOffset)); + nCol = myHeader.getIntValue("TFIELDS"); + rowLen = 0; + + extendArrays(nCol); + for (int col = 0; col < nCol; col += 1) { + rowLen += processCol(myHeader, col); + } + + HeaderCard card = myHeader.findCard("NAXIS1"); + card.setValue(String.valueOf(rowLen)); + myHeader.updateLine("NAXIS1", card); + + } + + /** Create a binary table from existing data in row order. + * + * @param data The data used to initialize the binary table. + */ + public BinaryTable(Object[][] data) throws FitsException { + this(convertToColumns(data)); + } + + /** Create a binary table from existing data in column order. + */ + public BinaryTable(Object[] o) throws FitsException { + + heap = new FitsHeap(0); + modelRow = new Object[o.length]; + extendArrays(o.length); + + + for (int i = 0; i < o.length; i += 1) { + addColumn(o[i]); + } + } + + /** Create a binary table from an existing ColumnTable */ + public BinaryTable(ColumnTable tab) { + + nCol = tab.getNCols(); + + extendArrays(nCol); + + bases = tab.getBases(); + sizes = tab.getSizes(); + + modelRow = new Object[nCol]; + + dimens = new int[nCol][]; + + // Set all flags to 0. + flags = new int[nCol]; + + // Set the column dimension. Note that + // we cannot distinguish an array of length 1 from a + // scalar here: we assume a scalar. + for (int col = 0; col < nCol; col += 1) { + if (sizes[col] != 1) { + dimens[col] = new int[]{sizes[col]}; + } else { + dimens[col] = new int[0]; + } + } + + for (int col = 0; col < nCol; col += 1) { + modelRow[col] = ArrayFuncs.newInstance(bases[col], sizes[col]); + } + + columns = null; + table = tab; + + heap = new FitsHeap(0); + rowLen = 0; + for (int col = 0; col < nCol; col += 1) { + rowLen += sizes[col] * ArrayFuncs.getBaseLength(tab.getColumn(col)); + } + heapOffset = 0; + nRow = tab.getNRows(); + } + + /** Return a row that may be used for direct i/o to the table. + */ + public Object[] getModelRow() { + return modelRow; + } + + /** Process one column from a FITS Header */ + private int processCol(Header header, int col) throws FitsException { + + String tform = header.getStringValue("TFORM" + (col + 1)); + if (tform == null) { + throw new FitsException("Attempt to process column " + (col + 1) + " but no TFORMn found."); + } + tform = tform.trim(); + + String tdims = header.getStringValue("TDIM" + (col + 1)); + + if (tdims != null) { + tdims = tdims.trim(); + } + + char type = getTFORMType(tform); + if (type == 'P' || type == 'Q') { + flags[col] |= COL_VARYING; + if (type == 'Q') { + flags[col] |= COL_LONGVARY; + } + type = getTFORMVarType(tform); + } + + + int size = getTFORMLength(tform); + + // Handle the special size cases. + // + // Bit arrays (8 bits fit in a byte) + if (type == 'X') { + size = (size + 7) / 8; + flags[col] |= COL_BIT; + + // Variable length arrays always have a two-element pointer (offset and size) + } else if (isVarCol(col)) { + size = 2; + } + + // bSize is the number of bytes in the field. + int bSize = size; + + int[] dims = null; + + // Cannot really handle arbitrary arrays of bits. + if (tdims != null && type != 'X' && !isVarCol(col)) { + dims = getTDims(tdims); + } + + if (dims == null) { + if (size == 1) { + dims = new int[0]; // Marks this as a scalar column + } else { + dims = new int[]{size}; + } + } + + if (type == 'C' || type == 'M') { + flags[col] |= COL_COMPLEX; + } + + Class colBase = null; + + switch (type) { + case 'A': + colBase = byte.class; + flags[col] |= COL_STRING; + bases[col] = String.class; + break; + + case 'L': + colBase = byte.class; + bases[col] = boolean.class; + flags[col] |= COL_BOOLEAN; + break; + case 'X': + case 'B': + colBase = byte.class; + bases[col] = byte.class; + break; + + case 'I': + colBase = short.class; + bases[col] = short.class; + bSize *= 2; + break; + + case 'J': + colBase = int.class; + bases[col] = int.class; + bSize *= 4; + break; + + case 'K': + colBase = long.class; + bases[col] = long.class; + bSize *= 8; + break; + + case 'E': + case 'C': + colBase = float.class; + bases[col] = float.class; + bSize *= 4; + break; + + case 'D': + case 'M': + colBase = double.class; + bases[col] = double.class; + bSize *= 8; + break; + + default: + throw new FitsException("Invalid type in column:" + col); + } + + if (isVarCol(col)) { + + dims = new int[]{nRow, 2}; + colBase = int.class; + bSize = 8; + + if (isLongVary(col)) { + colBase = long.class; + bSize = 16; + } + } + + if (!isVarCol(col) && isComplex(col)) { + + int[] xdims = new int[dims.length + 1]; + System.arraycopy(dims, 0, xdims, 0, dims.length); + xdims[dims.length] = 2; + dims = xdims; + bSize *= 2; + size *= 2; + } + + modelRow[col] = ArrayFuncs.newInstance(colBase, dims); + dimens[col] = dims; + sizes[col] = size; + + return bSize; + } + + /** Get the type in the TFORM field */ + char getTFORMType(String tform) { + + for (int i = 0; i < tform.length(); i += 1) { + if (!Character.isDigit(tform.charAt(i))) { + return tform.charAt(i); + } + } + return 0; + } + + /** Get the type in a varying length column TFORM */ + char getTFORMVarType(String tform) { + + int ind = tform.indexOf("P"); + if (ind < 0) { + ind = tform.indexOf("Q"); + } + + if (tform.length() > ind + 1) { + return tform.charAt(ind + 1); + } else { + return 0; + } + } + + /** Get the explicit or implied length of the TFORM field */ + int getTFORMLength(String tform) { + + tform = tform.trim(); + + if (Character.isDigit(tform.charAt(0))) { + return initialNumber(tform); + + } else { + return 1; + } + } + + /** Get an unsigned number at the beginning of a string */ + private int initialNumber(String tform) { + + int i; + for (i = 0; i < tform.length(); i += 1) { + + if (!Character.isDigit(tform.charAt(i))) { + break; + } + + } + + return Integer.parseInt(tform.substring(0, i)); + } + + /** Parse the TDIMS value. + * + * If the TDIMS value cannot be deciphered a one-d + * array with the size given in arrsiz is returned. + * + * @param tdims The value of the TDIMSn card. + * @param arraySize The size field found on the TFORMn card. + * @return An int array of the desired dimensions. + * Note that the order of the tdims is the inverse + * of the order in the TDIMS key. + */ + public static int[] getTDims(String tdims) { + + // The TDIMs value should be of the form: "(iiii,jjjj,kkk,...)" + + int[] dims = null; + + int first = tdims.indexOf('('); + int last = tdims.lastIndexOf(')'); + if (first >= 0 && last > first) { + + tdims = tdims.substring(first + 1, last - first); + + java.util.StringTokenizer st = new java.util.StringTokenizer(tdims, ","); + int dim = st.countTokens(); + if (dim > 0) { + + dims = new int[dim]; + + for (int i = dim - 1; i >= 0; i -= 1) { + dims[i] = Integer.parseInt(st.nextToken().trim()); + } + } + } + return dims; + } + + /** Convert a column from float/double to float complex/double complex. + * This is only possible for certain columns. The return status + * indicates if the conversion is possible. + * @param index The 0-based index of the column to be reset. + * @return Whether the conversion is possible. + */ + boolean setComplexColumn(int index) throws FitsException { + + // Currently there is almost no change required to the BinaryTable + // object itself when we convert an eligible column to complex, since the internal + // representation of the data is unchanged. We just need + // to set the flag that the column is complex. + + // Check that the index is valid, + // the data type is float or double + // the most rapidly changing index in the array has dimension 2. + if (index >= 0 && index < bases.length + && (bases[index] == float.class || bases[index] == double.class) + && dimens[index][dimens[index].length - 1] == 2) { + // By coincidence a variable length column will also have + // a last index of 2, so we'll get here. Otherwise + // we'd need to test that in parallel rather than in series. + + // If this is a variable length column, then + // we need to check the length of each row. + if ((flags[index] & COL_VARYING) != 0) { + + // We need to make sure that for every row, there are + // an even number of elements so that we can + // convert to an integral number of complex numbers. + Object col = getFlattenedColumn(index); + if (col instanceof int[]) { + int[] ptrs = (int[]) col; + for (int i = 1; i < ptrs.length; i += 2) { + if (ptrs[i] % 2 != 0) { + return false; + } + } + } else { + long[] ptrs = (long[]) col; + for (int i = 1; i < ptrs.length; i += 1) { + if (ptrs[i] % 2 != 0) { + return false; + } + } + } + } + // Set the column to complex + flags[index] |= COL_COMPLEX; + return true; + } + return false; + } + + /** Update a FITS header to reflect the current state of the data. + */ + public void fillHeader(Header h) throws FitsException { + + try { + h.setXtension("BINTABLE"); + h.setBitpix(8); + h.setNaxes(2); + h.setNaxis(1, rowLen); + h.setNaxis(2, nRow); + h.addValue("PCOUNT", heap.size(), "ntf::binarytable:pcount:1"); + h.addValue("GCOUNT", 1, "ntf::binarytable:gcount:1"); + Cursor iter = h.iterator(); + iter.setKey("GCOUNT"); + iter.next(); + iter.add("TFIELDS", new HeaderCard("TFIELDS", modelRow.length, "ntf::binarytable:tfields:1")); + + for (int i = 0; i < modelRow.length; i += 1) { + if (i > 0) { + h.positionAfterIndex("TFORM", i); + } + fillForColumn(h, i, iter); + } + } catch (HeaderCardException e) { + System.err.println("Error updating BinaryTableHeader:" + e); + } + } + + /** Updata the header to reflect information about a given column. + * This routine tries to ensure that the Header is organized by column. + */ + void pointToColumn(int col, Header hdr) throws FitsException { + + Cursor iter = hdr.iterator(); + if (col > 0) { + hdr.positionAfterIndex("TFORM", col); + } + fillForColumn(hdr, col, iter); + } + + /** Update the header to reflect the details of a given column */ + void fillForColumn(Header h, int col, Cursor iter) throws FitsException { + + String tform; + + if (isVarCol(col)) { + if (isLongVary(col)) { + tform = "1Q"; + } else { + tform = "1P"; + } + + } else { + tform = "" + sizes[col]; + } + + if (bases[col] == int.class) { + tform += "J"; + } else if (bases[col] == short.class || bases[col] == char.class) { + tform += "I"; + } else if (bases[col] == byte.class) { + tform += "B"; + } else if (bases[col] == float.class) { + if (isComplex(col)) { + tform += "C"; + } else { + tform += "E"; + } + } else if (bases[col] == double.class) { + if (isComplex(col)) { + tform += "M"; + } else { + tform += "D"; + } + } else if (bases[col] == long.class) { + tform += "K"; + } else if (bases[col] == boolean.class) { + tform += "L"; + } else if (bases[col] == String.class) { + tform += "A"; + } else { + throw new FitsException("Invalid column data class:" + bases[col]); + } + + + String key = "TFORM" + (col + 1); + iter.add(key, new HeaderCard(key, tform, "ntf::binarytable:tformN:1")); + + if (dimens[col].length > 0 && !isVarCol(col)) { + + StringBuffer tdim = new StringBuffer(); + char comma = '('; + for (int i = dimens[col].length - 1; i >= 0; i -= 1) { + tdim.append(comma); + tdim.append(dimens[col][i]); + comma = ','; + } + tdim.append(')'); + key = "TDIM" + (col + 1); + iter.add(key, new HeaderCard(key, new String(tdim), "ntf::headercard:tdimN:1")); + } + } + + /** Create a column table given the number of + * rows and a model row. This is used when + * we defer instantiation of the ColumnTable until + * the user requests data from the table. + */ + private ColumnTable createTable() throws FitsException { + + int nfields = modelRow.length; + + Object[] arrCol = new Object[nfields]; + + for (int i = 0; i < nfields; i += 1) { + arrCol[i] = ArrayFuncs.newInstance( + ArrayFuncs.getBaseClass(modelRow[i]), + sizes[i] * nRow); + } + + ColumnTable table; + + try { + table = new ColumnTable(arrCol, sizes); + } catch (TableException e) { + throw new FitsException("Unable to create table:" + e); + } + + return table; + } + + /** Convert a two-d table to a table of columns. Handle + * String specially. Every other element of data should be + * a primitive array of some dimensionality. + */ + private static Object[] convertToColumns(Object[][] data) { + + Object[] row = data[0]; + int nrow = data.length; + + Object[] results = new Object[row.length]; + + for (int col = 0; col < row.length; col += 1) { + + if (row[col] instanceof String) { + + String[] sa = new String[nrow]; + + for (int irow = 0; irow < nrow; irow += 1) { + sa[irow] = (String) data[irow][col]; + } + + results[col] = sa; + + } else { + + Class base = ArrayFuncs.getBaseClass(row[col]); + int[] dims = ArrayFuncs.getDimensions(row[col]); + + if (dims.length > 1 || dims[0] > 1) { + int[] xdims = new int[dims.length + 1]; + xdims[0] = nrow; + + Object[] arr = (Object[]) ArrayFuncs.newInstance(base, xdims); + for (int irow = 0; irow < nrow; irow += 1) { + arr[irow] = data[irow][col]; + } + results[col] = arr; + } else { + Object arr = ArrayFuncs.newInstance(base, nrow); + for (int irow = 0; irow < nrow; irow += 1) { + System.arraycopy(data[irow][col], 0, arr, irow, 1); + } + results[col] = arr; + } + + } + } + return results; + } + + /** Get a given row + * @param row The index of the row to be returned. + * @return A row of data. + */ + public Object[] getRow(int row) throws FitsException { + + if (!validRow(row)) { + throw new FitsException("Invalid row"); + } + + Object[] res; + if (table != null) { + res = getMemoryRow(row); + } else { + res = getFileRow(row); + } + return res; + } + + /** Get a row from memory. + */ + private Object[] getMemoryRow(int row) throws FitsException { + + Object[] data = new Object[modelRow.length]; + for (int col = 0; col < modelRow.length; col += 1) { + Object o = table.getElement(row, col); + o = columnToArray(col, o, 1); + data[col] = encurl(o, col, 1); + if (data[col] instanceof Object[]) { + data[col] = ((Object[]) data[col])[0]; + } + } + + return data; + + } + + /** Get a row from the file. + */ + private Object[] getFileRow(int row) throws FitsException { + + /** Read the row from memory */ + Object[] data = new Object[nCol]; + for (int col = 0; col < data.length; col += 1) { + data[col] = ArrayFuncs.newInstance( + ArrayFuncs.getBaseClass(modelRow[col]), + sizes[col]); + } + + try { + FitsUtil.reposition(currInput, fileOffset + row * rowLen); + currInput.readLArray(data); + } catch (IOException e) { + throw new FitsException("Error in deferred row read"); + } + + for (int col = 0; col < data.length; col += 1) { + data[col] = columnToArray(col, data[col], 1); + data[col] = encurl(data[col], col, 1); + if (data[col] instanceof Object[]) { + data[col] = ((Object[]) data[col])[0]; + } + } + return data; + } + + /** Replace a row in the table. + * @param row The index of the row to be replaced. + * @param data The new values for the row. + * @exception FitsException Thrown if the new row cannot + * match the existing data. + */ + public void setRow(int row, Object data[]) throws FitsException { + + if (table == null) { + getData(); + } + + if (data.length != getNCols()) { + throw new FitsException("Updated row size does not agree with table"); + } + + Object[] ydata = new Object[data.length]; + + for (int col = 0; col < data.length; col += 1) { + Object o = ArrayFuncs.flatten(data[col]); + ydata[col] = arrayToColumn(col, o); + } + + try { + table.setRow(row, ydata); + } catch (TableException e) { + throw new FitsException("Error modifying table: " + e); + } + } + + /** Replace a column in the table. + * @param col The index of the column to be replaced. + * @param xcol The new data for the column + * @exception FitsException Thrown if the data does not match + * the current column description. + */ + public void setColumn(int col, Object xcol) throws FitsException { + + xcol = arrayToColumn(col, xcol); + xcol = ArrayFuncs.flatten(xcol); + setFlattenedColumn(col, xcol); + } + + /** Set a column with the data aleady flattened. + * + * @param col The index of the column to be replaced. + * @param data The new data array. This should be a one-d + * primitive array. + * @exception FitsException Thrown if the type of length of + * the replacement data differs from the + * original. + */ + public void setFlattenedColumn(int col, Object data) throws FitsException { + + if (table == null) { + getData(); + } + + Object oldCol = table.getColumn(col); + if (data.getClass() != oldCol.getClass() + || Array.getLength(data) != Array.getLength(oldCol)) { + throw new FitsException("Replacement column mismatch at column:" + col); + } + try { + table.setColumn(col, data); + } catch (TableException e) { + throw new FitsException("Unable to set column:" + col + " error:" + e); + } + } + + /** Get a given column + * @param col The index of the column. + */ + public Object getColumn(int col) throws FitsException { + + if (table == null) { + getData(); + } + + Object res = getFlattenedColumn(col); + res = encurl(res, col, nRow); + return res; + } + + private Object encurl(Object res, int col, int rows) { + + if (bases[col] != String.class) { + + if (!isVarCol(col) && (dimens[col].length > 0)) { + + int[] dims = new int[dimens[col].length + 1]; + System.arraycopy(dimens[col], 0, dims, 1, dimens[col].length); + dims[0] = rows; + res = ArrayFuncs.curl(res, dims); + } + + } else { + + // Handle Strings. Remember the last element + // in dimens is the length of the Strings and + // we already used that when we converted from + // byte arrays to strings. So we need to ignore + // the last element of dimens, and add the row count + // at the beginning to curl. + + if (dimens[col].length > 2) { + int[] dims = new int[dimens[col].length]; + + System.arraycopy(dimens[col], 0, dims, 1, dimens[col].length - 1); + dims[0] = rows; + + res = ArrayFuncs.curl(res, dims); + } + } + + return res; + + } + + /** Get a column in flattened format. + * For large tables getting a column in standard format can be + * inefficient because a separate object is needed for + * each row. Leaving the data in flattened format means + * that only a single object is created. + * @param col + */ + public Object getFlattenedColumn(int col) throws FitsException { + + if (table == null) { + getData(); + } + + if (!validColumn(col)) { + throw new FitsException("Invalid column"); + } + + Object res = table.getColumn(col); + return columnToArray(col, res, nRow); + } + + /** Get a particular element from the table. + * @param i The row of the element. + * @param j The column of the element. + */ + public Object getElement(int i, int j) throws FitsException { + + if (!validRow(i) || !validColumn(j)) { + throw new FitsException("No such element"); + } + + Object ele; + if (isVarCol(j) && table == null) { + // Have to read in entire data set. + getData(); + } + + if (table == null) { + // This is really inefficient. + // Need to either save the row, or just read the one element. + Object[] row = getRow(i); + ele = row[j]; + + } else { + + ele = table.getElement(i, j); + ele = columnToArray(j, ele, 1); + + ele = encurl(ele, j, 1); + if (ele instanceof Object[]) { + ele = ((Object[]) ele)[0]; + } + } + + return ele; + } + + /** Get a particular element from the table but + * do no processing of this element (e.g., + * dimension conversion or extraction of + * variable length array elements/) + * @param i The row of the element. + * @param j The column of the element. + */ + public Object getRawElement(int i, int j) throws FitsException { + + if (table == null) { + getData(); + } + return table.getElement(i, j); + } + + /** Add a row at the end of the table. Given the way the + * table is structured this will normally not be very efficient. + * @param o An array of elements to be added. Each element of o + * should be an array of primitives or a String. + */ + public int addRow(Object[] o) throws FitsException { + + if (table == null) { + getData(); + } + + if (nCol == 0 && nRow == 0) { + for (int i = 0; i < o.length; i += 1) { + addColumn(o); + } + } else { + + Object[] flatRow = new Object[getNCols()]; + for (int i = 0; i < getNCols(); i += 1) { + Object x = ArrayFuncs.flatten(o[i]); + flatRow[i] = arrayToColumn(i, x); + } + try { + table.addRow(flatRow); + } catch (TableException e) { + throw new FitsException("Error add row to table"); + } + + nRow += 1; + } + + return nRow; + } + + /** Delete rows from a table. + * @param row The 0-indexed start of the rows to be deleted. + * @param len The number of rows to be deleted. + */ + public void deleteRows(int row, int len) throws FitsException { + try { + getData(); + table.deleteRows(row, len); + nRow -= len; + } catch (TableException e) { + throw new FitsException("Error deleting row block " + row + " to " + (row + len - 1) + " from table"); + } + } + + /** Add a column to the end of a table. + * @param o An array of identically structured objects with the + * same number of elements as other columns in the table. + */ + public int addColumn(Object o) throws FitsException { + + int primeDim = Array.getLength(o); + + extendArrays(nCol + 1); + Class base = ArrayFuncs.getBaseClass(o); + + // A varying length column is a two-d primitive + // array where the second index is not constant. + // We do not support Q types here, since Java + // can't handle the long indices anyway... + // This will probably change in some version of Java. + + if (isVarying(o)) { + flags[nCol] |= COL_VARYING; + dimens[nCol] = new int[]{2}; + } + + if (isVaryingComp(o)) { + flags[nCol] |= COL_VARYING | COL_COMPLEX; + dimens[nCol] = new int[]{2}; + } + + // Flatten out everything but 1-D arrays and the + // two-D arrays associated with variable length columns. + + if (!isVarCol(nCol)) { + + int[] allDim = ArrayFuncs.getDimensions(o); + + // Add a dimension for the length of Strings. + if (base == String.class) { + int[] xdim = new int[allDim.length + 1]; + System.arraycopy(allDim, 0, xdim, 0, allDim.length); + xdim[allDim.length] = -1; + allDim = xdim; + } + + if (allDim.length == 1) { + dimens[nCol] = new int[0]; + + } else { + + dimens[nCol] = new int[allDim.length - 1]; + System.arraycopy(allDim, 1, dimens[nCol], 0, allDim.length - 1); + o = ArrayFuncs.flatten(o); + } + } + + addFlattenedColumn(o, dimens[nCol]); + if (nRow == 0 && nCol == 0) { + nRow = primeDim; + } + nCol += 1; + return getNCols(); + + } + + private boolean isVaryingComp(Object o) { + String classname = o.getClass().getName(); + if (classname.equals("[[[F")) { + return checkCompVary((float[][][]) o); + } else if (classname.equals("[[[D")) { + return checkDCompVary((double[][][]) o); + } + return false; + } + + /** Is this a variable length column? + * It is if it's a two-d primitive array and + * the second dimension is not constant. + * It may also be a 3-d array of type float or double + * where the last index is always 2 (when the second index + * is non-zero). In this case it can be + * a complex varying column. + */ + private boolean isVarying(Object o) { + + if (o == null) { + return false; + } + String classname = o.getClass().getName(); + + if (classname.length() != 3 + || classname.charAt(0) != '[' + || classname.charAt(1) != '[') { + return false; + } + + Object[] ox = (Object[]) o; + if (ox.length < 2) { + return false; + } + + int flen = Array.getLength(ox[0]); + for (int i = 1; i < ox.length; i += 1) { + if (Array.getLength(ox[i]) != flen) { + return true; + } + } + return false; + } + + // Check if this is consistent with a varying + // complex row. That requires + // The second index varies. + // The third index is 2 whenever the second + // index is non-zero. + // This function will fail if nulls are encountered. + private boolean checkCompVary(float[][][] o) { + + boolean varying = false; + int len0 = o[0].length; + for (int i = 0; i < o.length; i += 1) { + if (o[i].length != len0) { + varying = true; + } + if (o[i].length > 0) { + for (int j = 0; j < o[i].length; j += 1) { + if (o[i][j].length != 2) { + return false; + } + } + } + } + return varying; + } + + private boolean checkDCompVary(double[][][] o) { + boolean varying = false; + int len0 = o[0].length; + for (int i = 0; i < o.length; i += 1) { + if (o[i].length != len0) { + varying = true; + } + if (o[i].length > 0) { + for (int j = 0; j < o[i].length; j += 1) { + if (o[i][j].length != 2) { + return false; + } + } + } + } + return varying; + } + + /** Add a column where the data is already flattened. + * @param o The new column data. This should be a one-dimensional + * primitive array. + * @param dims The dimensions of one row of the column. + */ + public int addFlattenedColumn(Object o, int[] dims) throws FitsException { + + extendArrays(nCol + 1); + + bases[nCol] = ArrayFuncs.getBaseClass(o); + + if (bases[nCol] == boolean.class) { + flags[nCol] |= COL_BOOLEAN; + } else if (bases[nCol] == String.class) { + flags[nCol] |= COL_STRING; + } + + // Convert to column first in case + // this is a String or variable length array. + + o = arrayToColumn(nCol, o); + + int size = 1; + + for (int dim = 0; dim < dims.length; dim += 1) { + size *= dims[dim]; + } + sizes[nCol] = size; + + if (size != 0) { + int xRow = Array.getLength(o) / size; + if (xRow > 0 && nCol != 0 && xRow != nRow) { + throw new FitsException("Added column does not have correct row count"); + } + } + + if (!isVarCol(nCol)) { + modelRow[nCol] = ArrayFuncs.newInstance(ArrayFuncs.getBaseClass(o), dims); + rowLen += size * ArrayFuncs.getBaseLength(o); + } else { + if (isLongVary(nCol)) { + modelRow[nCol] = new long[2]; + rowLen += 16; + } else { + modelRow[nCol] = new int[2]; + rowLen += 8; + } + } + + // Only add to table if table already exists or if we + // are filling up the last element in columns. + // This way if we allocate a bunch of columns at the beginning + // we only create the column table after we have all the columns + // ready. + + columns[nCol] = o; + + try { + if (table != null) { + table.addColumn(o, sizes[nCol]); + } else if (nCol == columns.length - 1) { + table = new ColumnTable(columns, sizes); + } + } catch (TableException e) { + throw new FitsException("Error in ColumnTable:" + e); + } + return nCol; + } + + /** Get the number of rows in the table + */ + public int getNRows() { + return nRow; + } + + /** Get the number of columns in the table. + */ + public int getNCols() { + return nCol; + } + + /** Check to see if this is a valid row. + * @param i The Java index (first=0) of the row to check. + */ + protected boolean validRow(int i) { + + if (getNRows() > 0 && i >= 0 && i < getNRows()) { + return true; + } else { + return false; + } + } + + /** Check if the column number is valid. + * + * @param j The Java index (first=0) of the column to check. + */ + protected boolean validColumn(int j) { + return (j >= 0 && j < getNCols()); + } + + /** Replace a single element within the table. + * + * @param i The row of the data. + * @param j The column of the data. + * @param o The replacement data. + */ + public void setElement(int i, int j, Object o) throws FitsException { + + getData(); + + try { + if (isVarCol(j)) { + + int size = Array.getLength(o); + // The offset for the row is the offset to the heap plus the offset within the heap. + int offset = (int) heap.getSize(); + heap.putData(o); + if (isLongVary(j)) { + table.setElement(i, j, new long[]{size, offset}); + } else { + table.setElement(i, j, new int[]{size, offset}); + } + + } else { + table.setElement(i, j, ArrayFuncs.flatten(o)); + } + } catch (TableException e) { + throw new FitsException("Error modifying table:" + e); + } + } + + /** Read the data -- or defer reading on random access + */ + public void read(ArrayDataInput i) throws FitsException { + + setFileOffset(i); + currInput = i; + + if (i instanceof RandomAccess) { + + try { + i.skipBytes(getTrueSize()); + } catch (IOException e) { + throw new FitsException("Unable to skip binary table HDU:" + e); + } + try { + i.skipBytes(FitsUtil.padding(getTrueSize())); + } catch (EOFException e) { + throw new PaddingException("Missing padding after binary table:" + e, this); + } catch (IOException e) { + throw new FitsException("Error skipping padding after binary table:" + e); + } + + } else { + + /** Read the data associated with the HDU including the hash area if present. + * @param i The input stream + */ + if (table == null) { + table = createTable(); + } + + readTrueData(i); + } + } + + /** Read table, heap and padding */ + protected void readTrueData(ArrayDataInput i) throws FitsException { + try { + table.read(i); + i.skipBytes(heapOffset); + heap.read(i); + heapReadFromStream = true; + + } catch (IOException e) { + throw new FitsException("Error reading binary table data:" + e); + } + try { + i.skipBytes(FitsUtil.padding(getTrueSize())); + } catch (EOFException e) { + throw new PaddingException("Error skipping padding after binary table", this); + } catch (IOException e) { + throw new FitsException("Error reading binary table data padding:" + e); + } + } + + /** Read the heap which contains the data for variable length + * arrays. + * A. Kovacs (4/1/08) Separated heap reading, s.t. the heap can + * be properly initialized even if in deferred read mode. + * columnToArray() checks and initializes the heap as necessary. + */ + protected void readHeap(ArrayDataInput input) throws FitsException { + FitsUtil.reposition(input, fileOffset + nRow * rowLen + heapOffset); + heap.read(input); + heapReadFromStream = true; + } + + /** Get the size of the data in the HDU sans padding. + */ + public long getTrueSize() { + long len = ((long) nRow) * rowLen; + if (heap.size() > 0) { + len += heap.size() + heapOffset; + } + return len; + } + + /** Write the table, heap and padding */ + public void write(ArrayDataOutput os) throws FitsException { + + getData(); + int len; + + try { + + // First write the table. + len = table.write(os); + if (heapOffset > 0) { + int off = heapOffset; + // Minimize memory usage. This also accommodates + // the possibility that heapOffset > 2GB. + // Previous code might have allocated up to 2GB + // array. [In practice this is always going + // to be really small though...] + int arrSiz = 4000000; + while (off > 0) { + if (arrSiz > off) { + arrSiz = (int) off; + } + os.write(new byte[arrSiz]); + off -= arrSiz; + } + } + + // Now check if we need to write the heap + if (heap.size() > 0) { + heap.write(os); + } + + FitsUtil.pad(os, getTrueSize()); + + } catch (IOException e) { + throw new FitsException("Unable to write table:" + e); + } + } + + public Object getData() throws FitsException { + + + if (table == null) { + + if (currInput == null) { + throw new FitsException("Cannot find input for deferred read"); + } + + table = createTable(); + + long currentOffset = FitsUtil.findOffset(currInput); + FitsUtil.reposition(currInput, fileOffset); + readTrueData(input); + FitsUtil.reposition(currInput, currentOffset); + } + + return table; + } + + public int[][] getDimens() { + return dimens; + } + + public Class[] getBases() { + return table.getBases(); + } + + public char[] getTypes() { + if (table == null) { + try { + getData(); + } catch (FitsException e) { + } + } + return table.getTypes(); + } + + public Object[] getFlatColumns() { + if (table == null) { + try { + getData(); + } catch (FitsException e) { + } + } + return table.getColumns(); + } + + public int[] getSizes() { + return sizes; + } + + /** Convert the external representation to the + * BinaryTable representation. Transformation include + * boolean -> T/F, Strings -> byte arrays, + * variable length arrays -> pointers (after writing data + * to heap). + */ + private Object arrayToColumn(int col, Object o) throws FitsException { + + if (flags[col] == 0) { + return o; + } + + if (!isVarCol(col)) { + + if (isString(col)) { + + // Convert strings to array of bytes. + int[] dims = dimens[col]; + + // Set the length of the string if we are just adding the column. + if (dims[dims.length - 1] < 0) { + dims[dims.length - 1] = FitsUtil.maxLength((String[]) o); + } + if (o instanceof String) { + o = new String[]{(String) o}; + } + o = FitsUtil.stringsToByteArray((String[]) o, dims[dims.length - 1]); + + + } else if (isBoolean(col)) { + + // Convert true/false to 'T'/'F' + o = FitsUtil.booleanToByte((boolean[]) o); + } + + } else { + + if (isBoolean(col)) { + + // Handle addRow/addElement + if (o instanceof boolean[]) { + o = new boolean[][]{(boolean[]) o}; + } + + // Convert boolean to byte arrays + boolean[][] to = (boolean[][]) o; + byte[][] xo = new byte[to.length][]; + for (int i = 0; i < to.length; i += 1) { + xo[i] = FitsUtil.booleanToByte(to[i]); + } + o = xo; + } + + // Write all rows of data onto the heap. + int offset = heap.putData(o); + + int blen = ArrayFuncs.getBaseLength(o); + + // Handle an addRow of a variable length element. + // In this case we only get a one-d array, but we just + // make is 1 x n to get the second dimension. + if (!(o instanceof Object[])) { + o = new Object[]{o}; + } + + // Create the array descriptors + int nrow = Array.getLength(o); + int factor = 1; + if (isComplex(col)) { + factor = 2; + } + if (isLongVary(col)) { + long[] descrip = new long[2 * nrow]; + + Object[] x = (Object[]) o; + // Fill the descriptor for each row. + for (int i = 0; i < nrow; i += 1) { + int len = Array.getLength(x[i]); + descrip[2 * i] = len; + descrip[2 * i + 1] = offset; + offset += len * blen * factor; + } + o = descrip; + } else { + int[] descrip = new int[2 * nrow]; + + Object[] x = (Object[]) o; + + // Fill the descriptor for each row. + for (int i = 0; i < nrow; i += 1) { + int len = Array.getLength(x[i]); + descrip[2 * i] = len; + descrip[2 * i + 1] = offset; + offset += len * blen * factor; + } + o = descrip; + } + } + + return o; + } + + /** Convert data from binary table representation to external + * Java representation. + */ + private Object columnToArray(int col, Object o, int rows) throws FitsException { + + // Most of the time we need do nothing! + if (flags[col] == 0) { + return o; + } + + // If a varying length column use the descriptors to + // extract appropriate information from the headers. + if (isVarCol(col)) { + + // A. Kovacs (4/1/08) + // Ensure that the heap has been initialized + if (!heapReadFromStream) { + readHeap(currInput); + } + + int[] descrip; + if (isLongVary(col)) { + // Convert longs to int's. This is dangerous. + if (!warnedOnVariableConversion) { + System.err.println("Warning: converting long variable array pointers to int's"); + warnedOnVariableConversion = true; + } + descrip = (int[]) ArrayFuncs.convertArray(o, int.class); + } else { + descrip = (int[]) o; + } + + int nrow = descrip.length / 2; + + Object[] res; // Res will be the result of extracting from the heap. + int[] dims; // Used to create result arrays. + + + if (isComplex(col)) { + // Complex columns have an extra dimension for each row + dims = new int[]{nrow, 0, 0}; + res = (Object[]) ArrayFuncs.newInstance(bases[col], dims); + // Set up dims for individual rows. + dims = new int[2]; + dims[1] = 2; + + // ---> Added clause by Attila Kovacs (13 July 2007) + // String columns have to read data into a byte array at first + // then do the string conversion later. + + } else if (isString(col)) { + dims = new int[]{nrow, 0}; + res = (Object[]) ArrayFuncs.newInstance(byte.class, dims); + + } else { + // Non-complex data has a simple primitive array for each row + dims = new int[]{nrow, 0}; + res = (Object[]) ArrayFuncs.newInstance(bases[col], dims); + } + + // Now read in each requested row. + for (int i = 0; i < nrow; i += 1) { + Object row; + int offset = descrip[2 * i + 1]; + int dim = descrip[2 * i]; + + if (isComplex(col)) { + dims[0] = dim; + row = ArrayFuncs.newInstance(bases[col], dims); + + // ---> Added clause by Attila Kovacs (13 July 2007) + // Again, String entries read data into a byte array at first + // then do the string conversion later. + } else if (isString(col)) { + // For string data, we need to read bytes and convert + // to strings + row = ArrayFuncs.newInstance(byte.class, dim); + + } else if (isBoolean(col)) { + // For boolean data, we need to read bytes and convert + // to booleans. + row = ArrayFuncs.newInstance(byte.class, dim); + + } else { + row = ArrayFuncs.newInstance(bases[col], dim); + } + + heap.getData(offset, row); + + // Now do the boolean conversion. + if (isBoolean(col)) { + row = FitsUtil.byteToBoolean((byte[]) row); + } + + res[i] = row; + } + o = res; + + } else { // Fixed length columns + + // Need to convert String byte arrays to appropriate Strings. + if (isString(col)) { + int[] dims = dimens[col]; + byte[] bytes = (byte[]) o; + if (bytes.length > 0) { + if (dims.length > 0) { + o = FitsUtil.byteArrayToStrings(bytes, dims[dims.length - 1]); + } else { + o = FitsUtil.byteArrayToStrings(bytes, 1); + } + } else { + // This probably fails for multidimensional arrays of strings where + // all elements are null. + String[] str = new String[rows]; + for (int i = 0; i < str.length; i += 1) { + str[i] = ""; + } + o = str; + } + + } else if (isBoolean(col)) { + o = FitsUtil.byteToBoolean((byte[]) o); + } + } + + return o; + } + + /** Make sure the arrays which describe the columns are + * long enough, and if not extend them. + */ + private void extendArrays(int need) { + + boolean wasNull = false; + if (sizes == null) { + wasNull = true; + + } else if (sizes.length > need) { + return; + } + + // Allocate the arrays. + int[] newSizes = new int[need]; + int[][] newDimens = new int[need][]; + int[] newFlags = new int[need]; + Object[] newModel = new Object[need]; + Object[] newColumns = new Object[need]; + Class[] newBases = new Class[need]; + + if (!wasNull) { + int len = sizes.length; + System.arraycopy(sizes, 0, newSizes, 0, len); + System.arraycopy(dimens, 0, newDimens, 0, len); + System.arraycopy(flags, 0, newFlags, 0, len); + System.arraycopy(modelRow, 0, newModel, 0, len); + System.arraycopy(columns, 0, newColumns, 0, len); + System.arraycopy(bases, 0, newBases, 0, len); + } + + sizes = newSizes; + dimens = newDimens; + flags = newFlags; + modelRow = newModel; + columns = newColumns; + bases = newBases; + } + + /** What is the size of the heap -- including the offset from the end of the + * table data. + */ + public int getHeapSize() { + return heapOffset + heap.size(); + } + + /** What is the offset to the heap */ + public int getHeapOffset() { + return heapOffset; + } + + /** Does this column have variable length arrays? */ + boolean isVarCol(int col) { + return (flags[col] & COL_VARYING) != 0; + } + + /** Does this column have variable length arrays? */ + boolean isLongVary(int col) { + return (flags[col] & COL_LONGVARY) != 0; + } + + /** Is this column a string column */ + private boolean isString(int col) { + return (flags[col] & COL_STRING) != 0; + } + + /** Is this column complex? */ + private boolean isComplex(int col) { + return (flags[col] & COL_COMPLEX) != 0; + } + + /** Is this column a boolean column */ + private boolean isBoolean(int col) { + return (flags[col] & COL_BOOLEAN) != 0; + } + + /** Is this column a bit column */ + private boolean isBit(int col) { + return (flags[col] & COL_BOOLEAN) != 0; + } + + /** Delete a set of columns. Note that this + * does not fix the header, so users should normally + * call the routine in TableHDU. + */ + public void deleteColumns(int start, int len) throws FitsException { + getData(); + try { + rowLen = table.deleteColumns(start, len); + nCol -= len; + } catch (Exception e) { + throw new FitsException("Error deleting columns from BinaryTable:" + e); + } + } + + /** Update the header after a deletion. */ + public void updateAfterDelete(int oldNcol, Header hdr) throws FitsException { + hdr.addValue("NAXIS1", rowLen, "ntf::binarytable:naxis1:1"); + } +} diff --git a/src/nom/tam/fits/BinaryTableHDU.java b/src/nom/tam/fits/BinaryTableHDU.java new file mode 100644 index 0000000..2b5c720 --- /dev/null +++ b/src/nom/tam/fits/BinaryTableHDU.java @@ -0,0 +1,274 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +import nom.tam.util.ArrayFuncs; +import nom.tam.util.*; +import java.io.IOException; +import java.lang.reflect.Array; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; + +/** FITS binary table header/data unit */ +public class BinaryTableHDU + extends TableHDU { + + private BinaryTable table; + /** The standard column keywords for a binary table. */ + private String[] keyStems = {"TTYPE", "TFORM", "TUNIT", "TNULL", "TSCAL", "TZERO", "TDISP", "TDIM"}; + + public BinaryTableHDU(Header hdr, Data datum) { + + super((TableData) datum); + myHeader = hdr; + myData = datum; + table = (BinaryTable) datum; + + } + + /** Create data from a binary table header. + * @param header the template specifying the binary table. + * @exception FitsException if there was a problem with the header. + */ + public static Data manufactureData(Header header) throws FitsException { + return new BinaryTable(header); + } + + public Data manufactureData() throws FitsException { + return manufactureData(myHeader); + } + + /** Build a binary table HDU from the supplied data. + * @param table the array used to build the binary table. + * @exception FitsException if there was a problem with the data. + */ + public static Header manufactureHeader(Data data) throws FitsException { + Header hdr = new Header(); + data.fillHeader(hdr); + return hdr; + } + + /** Encapsulate data in a BinaryTable data type */ + public static Data encapsulate(Object o) throws FitsException { + + if (o instanceof nom.tam.util.ColumnTable) { + return new BinaryTable((nom.tam.util.ColumnTable) o); + } else if (o instanceof Object[][]) { + return new BinaryTable((Object[][]) o); + } else if (o instanceof Object[]) { + return new BinaryTable((Object[]) o); + } else { + throw new FitsException("Unable to encapsulate object of type:" + + o.getClass().getName() + " as BinaryTable"); + } + } + + /** Check that this is a valid binary table header. + * @param header to validate. + * @return true if this is a binary table header. + */ + public static boolean isHeader(Header header) { + String xten = header.getStringValue("XTENSION"); + if (xten == null) { + return false; + } + xten = xten.trim(); + if (xten.equals("BINTABLE") || xten.equals("A3DTABLE")) { + return true; + } else { + return false; + } + } + + /** Check that this HDU has a valid header. + * @return true if this HDU has a valid header. + */ + public boolean isHeader() { + return isHeader(myHeader); + } + + /* Check if this data object is consistent with a binary table. There + * are three options: a column table object, an Object[][], or an Object[]. + * This routine doesn't check that the dimensions of arrays are properly + * consistent. + */ + public static boolean isData(Object o) { + + if (o instanceof nom.tam.util.ColumnTable || o instanceof Object[][] + || o instanceof Object[]) { + return true; + } else { + return false; + } + } + + /** Add a column without any associated header information. + * + * @param data The column data to be added. Data should be an Object[] where + * type of all of the constituents is identical. The length + * of data should match the other columns. Note: It is + * valid for data to be a 2 or higher dimensionality primitive + * array. In this case the column index is the first (in Java speak) + * index of the array. E.g., if called with int[30][20][10], the + * number of rows in the table should be 30 and this column + * will have elements which are 2-d integer arrays with TDIM = (10,20). + * @exception FitsException the column could not be added. + */ + public int addColumn(Object data) throws FitsException { + + int col = table.addColumn(data); + table.pointToColumn(getNCols() - 1, myHeader); + return col; + } + + // Need to tell header about the Heap before writing. + public void write(ArrayDataOutput ado) throws FitsException { + + int oldSize = myHeader.getIntValue("PCOUNT"); + if (oldSize != table.getHeapSize()) { + myHeader.addValue("PCOUNT", table.getHeapSize(), "ntf::binarytablehdu:pcount:1"); + } + + if (myHeader.getIntValue("PCOUNT") == 0) { + myHeader.deleteKey("THEAP"); + } else { + myHeader.getIntValue("TFIELDS"); + int offset = myHeader.getIntValue("NAXIS1") + * myHeader.getIntValue("NAXIS2") + + table.getHeapOffset(); + myHeader.addValue("THEAP", offset, "ntf::binarytablehdu:theap:1"); + } + + super.write(ado); + } + + /** + * Convert a column in the table to complex. Only tables with appropriate + * types and dimensionalities can be converted. It is legal to call this on + * a column that is already complex. + * + * @param index The 0-based index of the column to be converted. + * @return Whether the column can be converted + * @throws FitsException + */ + public boolean setComplexColumn(int index) throws FitsException { + boolean status = false; + if (table.setComplexColumn(index)) { + + // No problem with the data. Make sure the header + // is right. + + int[] dimens = table.getDimens()[index]; + Class base = table.getBases()[index]; + + int dim = 1; + String tdim = ""; + String sep = ""; + // Don't loop over all values. + // The last is the [2] for the complex data. + for (int i = 0; i < dimens.length - 1; i += 1) { + dim *= dimens[i]; + tdim = dimens[i] + sep + tdim; + sep = ","; + } + String suffix = "C"; // For complex + // Update the TFORMn keyword. + if (base == double.class) { + suffix = "M"; + } + + // Worry about variable length columns. + String prefix = ""; + if (table.isVarCol(index)) { + prefix = "P"; + dim = 1; + if (table.isLongVary(index)) { + prefix = "Q"; + } + } + + // Now update the header. + myHeader.findCard("TFORM" + (index + 1)); + HeaderCard hc = myHeader.nextCard(); + String oldComment = hc.getComment(); + if (oldComment == null) { + oldComment = "Column converted to complex"; + } + myHeader.addValue("TFORM" + (index + 1), dim + prefix + suffix, oldComment); + if (tdim.length() > 0) { + myHeader.addValue("TDIM" + (index + 1), "(" + tdim + ")", "ntf::binarytablehdu:tdimN:1"); + } else { + // Just in case there used to be a TDIM card that's no longer needed. + myHeader.removeCard("TDIM" + (index + 1)); + } + status = true; + } + return status; + } + + private void prtField(String type, String field) { + String val = myHeader.getStringValue(field); + if (val != null) { + System.out.print(type + '=' + val + "; "); + } + } + + /** Print out some information about this HDU. + */ + public void info() { + + BinaryTable myData = (BinaryTable) this.myData; + + System.out.println(" Binary Table"); + System.out.println(" Header Information:"); + + int nhcol = myHeader.getIntValue("TFIELDS", -1); + int nrow = myHeader.getIntValue("NAXIS2", -1); + int rowsize = myHeader.getIntValue("NAXIS1", -1); + + System.out.print(" " + nhcol + " fields"); + System.out.println(", " + nrow + " rows of length " + rowsize); + + for (int i = 1; i <= nhcol; i += 1) { + System.out.print(" " + i + ":"); + prtField("Name", "TTYPE" + i); + prtField("Format", "TFORM" + i); + prtField("Dimens", "TDIM" + i); + System.out.println(""); + } + + System.out.println(" Data Information:"); + if (myData == null + || table.getNRows() == 0 || table.getNCols() == 0) { + System.out.println(" No data present"); + if (table.getHeapSize() > 0) { + System.out.println(" Heap size is: " + table.getHeapSize() + " bytes"); + } + } else { + + System.out.println(" Number of rows=" + table.getNRows()); + System.out.println(" Number of columns=" + table.getNCols()); + if (table.getHeapSize() > 0) { + System.out.println(" Heap size is: " + table.getHeapSize() + " bytes"); + } + Object[] cols = table.getFlatColumns(); + for (int i = 0; i < cols.length; i += 1) { + System.out.println(" " + i + ":" + ArrayFuncs.arrayDescription(cols[i])); + } + } + } + + /** What are the standard column stems for a binary table? + */ + public String[] columnKeyStems() { + return keyStems; + } +} diff --git a/src/nom/tam/fits/Data.java b/src/nom/tam/fits/Data.java new file mode 100644 index 0000000..17788c8 --- /dev/null +++ b/src/nom/tam/fits/Data.java @@ -0,0 +1,117 @@ +package nom.tam.fits; + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +import java.io.*; +import nom.tam.util.*; + +/** This class provides methods to access the data segment of an + * HDU. + */ +public abstract class Data implements FitsElement { + + /** This is the object which contains the actual data for the HDU. + * + */ + /** The starting location of the data when last read */ + protected long fileOffset = -1; + /** The size of the data when last read */ + protected long dataSize; + /** The inputstream used. */ + protected RandomAccess input; + + /** Get the file offset */ + public long getFileOffset() { + return fileOffset; + } + + /** Set the fields needed for a re-read */ + protected void setFileOffset(Object o) { + if (o instanceof RandomAccess) { + fileOffset = FitsUtil.findOffset(o); + dataSize = getTrueSize(); + input = (RandomAccess) o; + } + } + + /** Write the data -- including any buffering needed + * @param o The output stream on which to write the data. + */ + public abstract void write(ArrayDataOutput o) throws FitsException; + + /** Read a data array into the current object and if needed position + * to the beginning of the next FITS block. + * @param i The input data stream + */ + public abstract void read(ArrayDataInput i) throws FitsException; + + public void rewrite() throws FitsException { + + if (!rewriteable()) { + throw new FitsException("Illegal attempt to rewrite data"); + } + + FitsUtil.reposition(input, fileOffset); + write((ArrayDataOutput) input); + try { + ((ArrayDataOutput) input).flush(); + } catch (IOException e) { + throw new FitsException("Error in rewrite flush: " + e); + } + } + + public boolean reset() { + try { + FitsUtil.reposition(input, fileOffset); + return true; + } catch (Exception e) { + return false; + } + } + + public boolean rewriteable() { + if (input == null + || fileOffset < 0 + || (getTrueSize() + 2879) / 2880 != (dataSize + 2879) / 2880) { + return false; + } else { + return true; + } + } + + abstract long getTrueSize(); + + /** Get the size of the data element in bytes */ + public long getSize() { + return FitsUtil.addPadding(getTrueSize()); + } + + /** Return the data array object. + */ + public abstract Object getData() throws FitsException; + + /** Return the non-FITS data object */ + public Object getKernel() throws FitsException { + return getData(); + } + + /** Modify a header to point to this data + */ + abstract void fillHeader(Header head) throws FitsException; +} diff --git a/src/nom/tam/fits/Fits.java b/src/nom/tam/fits/Fits.java new file mode 100644 index 0000000..cfb9c0e --- /dev/null +++ b/src/nom/tam/fits/Fits.java @@ -0,0 +1,1047 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +import java.io.*; +import java.net.*; +import java.util.*; + +import nom.tam.util.*; + +/** This class provides access to routines to allow users + * to read and write FITS files. + *

+ * + *

+ * Description of the Package + *

+ * This FITS package attempts to make using FITS files easy, + * but does not do exhaustive error checking. Users should + * not assume that just because a FITS file can be read + * and written that it is necessarily legal FITS. + * + * + *

+ * + * + * @version 1.06.0 May 21, 2011 + */ +public class Fits { + + /** The input stream associated with this Fits object. + */ + private ArrayDataInput dataStr; + + /** A vector of HDUs that have been added to this + * Fits object. + */ + private Vector hduList = new Vector(); + + /** Has the input stream reached the EOF? + */ + private boolean atEOF; + + /** The last offset we reached. + * A -1 is used to indicate that we + * cannot use the offset. + */ + private long lastFileOffset = -1; + + /** Indicate the version of these classes */ + public static String version() { + + // Version 0.1: Original test FITS classes -- 9/96 + // Version 0.2: Pre-alpha release 10/97 + // Complete rewrite using BufferedData*** and + // ArrayFuncs utilities. + // Version 0.3: Pre-alpha release 1/98 + // Incorporation of HDU hierarchy developed + // by Dave Glowacki and various bug fixes. + // Version 0.4: Alpha-release 2/98 + // BinaryTable classes revised to use + // ColumnTable classes. + // Version 0.5: Random Groups Data 3/98 + // Version 0.6: Handling of bad/skipped FITS, FitsDate (D. Glowacki) 3/98 + // Version 0.9: ASCII tables, Tiled images, Faux, Bad and SkippedHDU class + // deleted. 12/99 + // Version 0.91: Changed visibility of some methods. + // Minor fixes. + // Version 0.92: Fix bug in BinaryTable when reading from stream. + // Version 0.93: Supports HIERARCH header cards. Added FitsElement interface. + // Several bug fixes especially for null HDUs. + // Version 0.96: Address issues with mandatory keywords. + // Fix problem where some keywords were not properly keyed. + // Version 0.96a: Update version in FITS + // Version 0.99: Added support for Checksums (thanks to RJ Mathar). + // Fixed bug with COMMENT and HISTORY keywords (Rose Early) + // Changed checking for compression and fixed bug with TFORM + // handling in binary tables (Laurent Michel) + // Distinguished arrays of length 1 from scalars in + // binary tables (Jorgo Bakker) + // Fixed bug in handling of length 0 values in headers (Fred Romerfanger, Jorgo Bakker) + // Truncated BufferedFiles when finishing write (but only + // for FITS file as a whole.) + // Fixed bug writing binary tables with deferred reads. + // Made addLine methods in Header public. + // Changed ArrayFuncs.newInstance to handle inputs with dimensionality of 0. + // Version 0.99.1 + // Added deleteRows and deleteColumns functionality to all tables. + // This includes changes + // to TableData, TableHDU, AsciiTable, BinaryTable and util/ColumnTable. + // Row deletions were suggested by code of R. Mathar but this works + // on all types of tables and implements the deletions at a lower level. + // Completely revised util.HashedList to use more standard features from + // Collections. The HashedList now melds a HashMap and ArrayList + // Added sort to HashedList function to enable sorting of the list. + // The logic now uses a simple index for the iterators rather than + // traversing a linked list. + // Added sort before write in Header to ensure keywords are in correct order. + // This uses a new HeaderOrder class which implements java.util.Comparator to + // indicate the required order for FITS keywords. Users should now + // be able to write required keywords anywhere without getting errors + // later when they try to write out the FITS file. + // Fixed bug in setColumn in util.Column table where the new column + // was not being pointed to. Any new column resets the table. + // Several fixes to BinaryTable to address errors in variable length + // array handling. + // Several fixes to the handling of variable length array in binary tables. + // (noted by Guillame Belanger). + // Several fixes and changes suggested by Richard Mathar mostly + // in BinaryTable. + // Version 0.99.2 + // Revised test routines to use Junit. Note that Junit tests + // use annotations and require Java 1.5. + // Added ArrayFuncs.arrayEquals() methods to compare + // arbitrary arrays. + // Fixed bugs in handling of 0 length columns and table update. + // Version 0.99.3 + // Additional fixes for 0 length strings. + // Version 0.99.4 + // Changed handling of constructor for File objects + // 0.99.5 + // Add ability to handle FILE, HTTPS and FTP URLs and to + // handle redirects amongst different protocols. + // 0.99.5 + // Fixes to String handling (A. Kovacs) + // Truncating long doubles to fit in + // standard header. + // Made some methods public in FitsFactory + // Added Set + // 0.99.6 + // Fix to BinaryTable (L. Michel) + // Version 1.00.0 + // Support for .Z compressed data. + // Better detection of compressed data streams + // Bug fix for binary tables (A. Kovacs) + // Version 1.00.1 (2/09) + // Fix for exponential format in header keywords + // Version 1.00.2 (3/09) + // Fixed offsets when users read rows or elements + // within multiHDU files. + // Version 1.01.0 + // Fixes bugs and adds some more graceful + // error handling for situations where arrays + // could exceed 2G. More work is needed here though. + // Data.getTrueSize() now returns a long. + // + // Fixed bug with initial blanks in HIERARCH + // values. + // Version 1.02.0 (7/09) + // Fixes bugs in ASCII tables where integer and real + // fields that are blank should be read as 0 per the FITS + // standard. (L. Michel) + // + // Adds PaddingException to allow users to read + // improperly padded last HDU in FITS file. (suggested by L. Michel) + // This required changes to the Fits.java and all of the Data subclasses + // as well as the new exception classes. + // Version 1.03.0 (7/09) + // Many changes to support long (>2GB) arrays in + // reads and size computations: + // ArrayDataInput.readArray deprecated in + // favor of ArrayDataInput.readLArray + // ArrayUtil.computeSize -> ArrayUtil.computeLSize + // ArrayUtil.nElements -> ArrayUtil.nLElements + // The skipBytes method in ArrayDataInput is overloaded + // to take a long argument and return a long value (in + // addition to the method inherited from DataInput + // the takes and returns an int). + // + // Corresponding changes in FITS classes. + // [Note that there are still many restrictions + // due to the array size limits in Java.] + // + // A number of obsolete comments regarding BITPIX=64 being non-standard + // were removed. + // If errors are found in reading the Header of an HDU + // an IOException is now returned in some situations + // where an Error was being returned. + // + // A bug in the new PaddingException was fixed that + // lets truncated ImageHDUs have Tilers. + // + // Version 1.03.1 (7/09) + // Changed FitsUtil.byteArrayToStrings to make + // sure that deleted white space is eligible for + // garbage collection. (J.C. Segovia) + // + // Version 1.04.0 (12/09) + // + // Added support for the long string convention + // (see JavaDocs for Header). + // Fixed errors in handling of strings with embedded + // quotes. + // Other minor bugs. + + // Version 1.05.0 (12/10) + // Several fixes suggested by Laurent Bourges + // - Better handling of strings in binary tables + // including handling of truncated strings (with + // embedded nuls) and detection of illegal + // non-printing characters. + // - New table metadata functions in TableHDU + // - Handling of complex data including variable + // length arrays. + // Added a number of convenience methods + // - FitsUtil.pad() is used to write padding + // rather than separate code in many classes + // - FitsUtil.HDUFactory will now create + // an HDU from a Header or input data. + // - reset() method added to FitsElement to simplify + // reading of Fits data using low level access. + // This is implemented in many classes. + // - dumpArray method in ArrayFuncs for convenience + // in debugging. + // Version 1.05.1 (2/11) + // Fixed error in Long string support where the + // COMMENT keyword was being used instead of the + // correct CONTINUE. (V. Forchi) + // An error in the positioning of the Header cursor + // for primary images was noted by V. Forchi. Updates + // to the header could easily result in writing + // records before the EXTEND keyword which is a violation + // of the FITS standard. + // Version 1.06.0 (5/11) + // Substantial reworking of compression to accommodate + // BZIP2 compression. The Apache Bzip library is used or + // since this is very slow, the user can specify a local + // command to do the decompression using the BZIP_DECOMPRESSOR + // environment variable. This is assumed to require a + // '-' argument which is added if not supplied by the user. + // The decompressor should act as a filter between standard input + // and output. + // + // User compression flags are now completely + // ignored and the compression and the compression + // is determined entirely by the content of the stream. + // The Apache library will be needed in the + // classpath to accommodate BZIP2 inputs if the user + // does not supply the BZIP_DECOMPRESSOR. + // + // Adding additional compression methods should be much easier and + // may only involve adding a couple of lines in the + // FitsUtil.decompress function if a decompressor class + // is available. + // + // One subtle consequence of how compression is now handled + // is that there is no advantage for users to + // create their own BufferedDataInputStream's. + // Users should just provide a standard input stream + // and allow the FITS library to wrap it in a + // BufferedDataInputStream. + // + // A bug in the UndefinedData class was detected + // Vincenzo Forzi and has been corrected. + // + // The nom.tam.util.AsciiFuncs class now handles + // ASCII encoding to more cleanly separate this + // functionality from the FITS library and to enable + // Java 1.5 compatibitity. (Suggested by changes of L.Bourges) + // Some other V1.5 incompatiblities removed. + // + // The HeaderCommentsMap class is now provided to enable + // users to control the comments that are generated in system + // generated header cards. The map is initialized to values + // that should be the same as the current defaults. This + // should allow users to emulate the comments of other packages. + // + // All Java code has been passed through NetBeans formatter + // so that it should have a more uniform appearance. + + return "1.060"; + } + + /** Create an empty Fits object which is not + * associated with an input stream. + */ + public Fits() { + } + + /** Create a Fits object associated with + * the given data stream. + * Compression is determined from the first few bytes of the stream. + * @param str The data stream. + */ + public Fits(InputStream str) throws FitsException { + streamInit(str, false); + } + + /** Create a Fits object associated with a data stream. + * @param str The data stream. + * @param compressed Is the stream compressed? This is currently ignored. + * Compression is determined from the first two bytes in the stream. + */ + public Fits(InputStream str, boolean compressed) + throws FitsException { + streamInit(str); + } + + /** Initialize the stream. + * @param str The user specified input stream + * @param seekable ignored + */ + protected void streamInit(InputStream str, boolean seekable) + throws FitsException { + streamInit(str); + } + + /** Do the stream initialization. + * + * @param str The input stream. + * @param compressed Is this data compressed? This flag + * is ignored. The compression is determined from the stream content. + * @param seekable Can one seek on the stream. This parameter is ignored. + */ + protected void streamInit(InputStream str, boolean compressed, + boolean seekable) + throws FitsException { + streamInit(str); + } + + /** Initialize the input stream. Mostly this + * checks to see if the stream is compressed and + * wraps the stream if necessary. Even if the + * stream is not compressed, it will likely be wrapped + * in a PushbackInputStream. So users should probably + * not supply a BufferedDataInputStream themselves, but + * should allow the Fits class to do the wrapping. + * @param str + * @throws FitsException + */ + protected void streamInit(InputStream str) throws FitsException { + str = FitsUtil.decompress(str); + if (str instanceof ArrayDataInput) { + dataStr = (ArrayDataInput) str; + } else { + // Use efficient blocking for input. + dataStr = new BufferedDataInputStream(str); + } + } + + /** Initialize using buffered random access. + * This implies that the data is uncompressed. + * @param f + * @throws FitsException + */ + protected void randomInit(File f) throws FitsException { + + String permissions = "r"; + if (!f.exists() || !f.canRead()) { + throw new FitsException("Non-existent or unreadable file"); + } + if (f.canWrite()) { + permissions += "w"; + } + try { + dataStr = new BufferedFile(f, permissions); + + ((BufferedFile) dataStr).seek(0); + } catch (IOException e) { + throw new FitsException("Unable to open file " + f.getPath()); + } + } + + /** Associate FITS object with a File. If the file is + * compressed a stream will be used, otherwise random access + * will be supported. + * @param myFile The File object. + */ + public Fits(File myFile) throws FitsException { + this(myFile, FitsUtil.isCompressed(myFile)); + } + + /** Associate the Fits object with a File + * @param myFile The File object. + * @param compressed Is the data compressed? + */ + public Fits(File myFile, boolean compressed) throws FitsException { + fileInit(myFile, compressed); + } + + /** Get a stream from the file and then use the stream initialization. + * @param myFile The File to be associated. + * @param compressed Is the data compressed? + */ + protected void fileInit(File myFile, boolean compressed) throws FitsException { + + try { + if (compressed) { + FileInputStream str = new FileInputStream(myFile); + streamInit(str); + } else { + randomInit(myFile); + } + } catch (IOException e) { + throw new FitsException("Unable to create Input Stream from File: " + myFile); + } + } + + /** Associate the FITS object with a file or URL. + * + * The string is assumed to be a URL if it begins one of the + * protocol strings. + * If the string ends in .gz it is assumed that + * the data is in a compressed format. + * All string comparisons are case insensitive. + * + * @param filename The name of the file or URL to be processed. + * @exception FitsException Thrown if unable to find or open + * a file or URL from the string given. + **/ + public Fits(String filename) throws FitsException { + this(filename, FitsUtil.isCompressed(filename)); + } + + /** Associate the FITS object with a file or URL. + * + * The string is assumed to be a URL if it begins one of the + * protocol strings. + * If the string ends in .gz it is assumed that + * the data is in a compressed format. + * All string comparisons are case insensitive. + * + * @param filename The name of the file or URL to be processed. + * @exception FitsException Thrown if unable to find or open + * a file or URL from the string given. + **/ + public Fits(String filename, boolean compressed) throws FitsException { + + InputStream inp; + + if (filename == null) { + throw new FitsException("Null FITS Identifier String"); + } + + int len = filename.length(); + String lc = filename.toLowerCase(); + try { + URL test = new URL(filename); + InputStream is = FitsUtil.getURLStream(new URL(filename), 0); + streamInit(is); + return; + } catch (Exception e) { + // Just try it as a file + } + + File fil = new File(filename); + if (fil.exists()) { + fileInit(fil, compressed); + return; + } + + + try { + InputStream str = ClassLoader.getSystemClassLoader().getResourceAsStream(filename); + streamInit(str); + } catch (Exception e) { + // + } + + } + + /** Associate the FITS object with a given uncompressed URL + * @param myURL The URL to be associated with the FITS file. + * @param compressed Compression flag, ignored. + * @exception FitsException Thrown if unable to use the specified URL. + */ + public Fits(URL myURL, boolean compressed) throws FitsException { + this(myURL); + } + + /** Associate the FITS object with a given URL + * @param myURL + * @exception FitsException Thrown if unable to find or open + * a file or URL from the string given. + */ + public Fits(URL myURL) throws FitsException { + try { + streamInit(FitsUtil.getURLStream(myURL, 0)); + } catch (IOException e) { + throw new FitsException("Unable to open input from URL:" + myURL); + } + } + + /** Return all HDUs for the Fits object. If the + * FITS file is associated with an external stream make + * sure that we have exhausted the stream. + * @return an array of all HDUs in the Fits object. Returns + * null if there are no HDUs associated with this object. + */ + public BasicHDU[] read() throws FitsException { + + readToEnd(); + + int size = getNumberOfHDUs(); + + if (size == 0) { + return null; + } + + BasicHDU[] hdus = new BasicHDU[size]; + hduList.copyInto(hdus); + return hdus; + } + + /** Read the next HDU on the default input stream. + * @return The HDU read, or null if an EOF was detected. + * Note that null is only returned when the EOF is detected immediately + * at the beginning of reading the HDU. + */ + public BasicHDU readHDU() throws FitsException, IOException { + + if (dataStr == null || atEOF) { + return null; + } + + if (dataStr instanceof nom.tam.util.RandomAccess && lastFileOffset > 0) { + FitsUtil.reposition(dataStr, lastFileOffset); + } + + Header hdr = Header.readHeader(dataStr); + if (hdr == null) { + atEOF = true; + return null; + } + + Data datum = hdr.makeData(); + try { + datum.read(dataStr); + } catch (PaddingException e) { + e.updateHeader(hdr); + throw e; + } + + lastFileOffset = FitsUtil.findOffset(dataStr); + BasicHDU nextHDU = FitsFactory.HDUFactory(hdr, datum); + + hduList.addElement(nextHDU); + return nextHDU; + } + + /** Skip HDUs on the associate input stream. + * @param n The number of HDUs to be skipped. + */ + public void skipHDU(int n) throws FitsException, IOException { + for (int i = 0; i < n; i += 1) { + skipHDU(); + } + } + + /** Skip the next HDU on the default input stream. + */ + public void skipHDU() throws FitsException, IOException { + + if (atEOF) { + return; + } else { + Header hdr = new Header(dataStr); + if (hdr == null) { + atEOF = true; + return; + } + int dataSize = (int) hdr.getDataSize(); + dataStr.skip(dataSize); + } + } + + /** Return the n'th HDU. + * If the HDU is already read simply return a pointer to the + * cached data. Otherwise read the associated stream + * until the n'th HDU is read. + * @param n The index of the HDU to be read. The primary HDU is index 0. + * @return The n'th HDU or null if it could not be found. + */ + public BasicHDU getHDU(int n) throws FitsException, IOException { + + int size = getNumberOfHDUs(); + + for (int i = size; i <= n; i += 1) { + BasicHDU hdu; + hdu = readHDU(); + if (hdu == null) { + return null; + } + } + + try { + return (BasicHDU) hduList.elementAt(n); + } catch (NoSuchElementException e) { + throw new FitsException("Internal Error: hduList build failed"); + } + } + + /** Read to the end of the associated input stream */ + private void readToEnd() throws FitsException { + + while (dataStr != null && !atEOF) { + try { + if (readHDU() == null) { + break; + } + } catch (IOException e) { + throw new FitsException("IO error: " + e); + } + } + } + + /** Return the number of HDUs in the Fits object. If the + * FITS file is associated with an external stream make + * sure that we have exhausted the stream. + * @return number of HDUs. + * @deprecated The meaning of size of ambiguous. Use + */ + public int size() throws FitsException { + readToEnd(); + return getNumberOfHDUs(); + } + + /** Add an HDU to the Fits object. Users may intermix + * calls to functions which read HDUs from an associated + * input stream with the addHDU and insertHDU calls, + * but should be careful to understand the consequences. + * + * @param myHDU The HDU to be added to the end of the FITS object. + */ + public void addHDU(BasicHDU myHDU) + throws FitsException { + insertHDU(myHDU, getNumberOfHDUs()); + } + + /** Insert a FITS object into the list of HDUs. + * + * @param myHDU The HDU to be inserted into the list of HDUs. + * @param n The location at which the HDU is to be inserted. + */ + public void insertHDU(BasicHDU myHDU, int n) + throws FitsException { + + if (myHDU == null) { + return; + } + + if (n < 0 || n > getNumberOfHDUs()) { + throw new FitsException("Attempt to insert HDU at invalid location: " + n); + } + + try { + + if (n == 0) { + + // Note that the previous initial HDU is no longer the first. + // If we were to insert tables backwards from last to first, + // we could get a lot of extraneous DummyHDUs but we currently + // do not worry about that. + + if (getNumberOfHDUs() > 0) { + ((BasicHDU) hduList.elementAt(0)).setPrimaryHDU(false); + } + + if (myHDU.canBePrimary()) { + myHDU.setPrimaryHDU(true); + hduList.insertElementAt(myHDU, 0); + } else { + insertHDU(BasicHDU.getDummyHDU(), 0); + myHDU.setPrimaryHDU(false); + hduList.insertElementAt(myHDU, 1); + } + } else { + myHDU.setPrimaryHDU(false); + hduList.insertElementAt(myHDU, n); + } + } catch (NoSuchElementException e) { + throw new FitsException("hduList inconsistency in insertHDU"); + } + + } + + /** Delete an HDU from the HDU list. + * + * @param n The index of the HDU to be deleted. + * If n is 0 and there is more than one HDU present, then + * the next HDU will be converted from an image to + * primary HDU if possible. If not a dummy header HDU + * will then be inserted. + */ + public void deleteHDU(int n) throws FitsException { + int size = getNumberOfHDUs(); + if (n < 0 || n >= size) { + throw new FitsException("Attempt to delete non-existent HDU:" + n); + } + try { + hduList.removeElementAt(n); + if (n == 0 && size > 1) { + BasicHDU newFirst = (BasicHDU) hduList.elementAt(0); + if (newFirst.canBePrimary()) { + newFirst.setPrimaryHDU(true); + } else { + insertHDU(BasicHDU.getDummyHDU(), 0); + } + } + } catch (NoSuchElementException e) { + throw new FitsException("Internal Error: hduList Vector Inconsitency"); + } + } + + /** Write a Fits Object to an external Stream. + * + * @param dos A DataOutput stream. + */ + public void write(DataOutput os) throws FitsException { + + ArrayDataOutput obs; + boolean newOS = false; + + if (os instanceof ArrayDataOutput) { + obs = (ArrayDataOutput) os; + } else if (os instanceof DataOutputStream) { + newOS = true; + obs = new BufferedDataOutputStream((DataOutputStream) os); + } else { + throw new FitsException("Cannot create ArrayDataOutput from class " + + os.getClass().getName()); + } + + BasicHDU hh; + for (int i = 0; i < getNumberOfHDUs(); i += 1) { + try { + hh = (BasicHDU) hduList.elementAt(i); + hh.write(obs); + } catch (ArrayIndexOutOfBoundsException e) { + e.printStackTrace(); + throw new FitsException("Internal Error: Vector Inconsistency" + e); + } + } + if (newOS) { + try { + obs.flush(); + obs.close(); + } catch (IOException e) { + System.err.println("Warning: error closing FITS output stream"); + } + } + try { + if (obs instanceof BufferedFile) { + ((BufferedFile) obs).setLength(((BufferedFile) obs).getFilePointer()); + } + } catch (IOException e) { + // Ignore problems... + } + + } + + /** Read a FITS file from an InputStream object. + * + * @param is The InputStream stream whence the FITS information + * is found. + */ + public void read(InputStream is) throws FitsException, IOException { + + boolean newIS = false; + + if (is instanceof ArrayDataInput) { + dataStr = (ArrayDataInput) is; + } else { + dataStr = new BufferedDataInputStream(is); + } + + read(); + + if (newIS) { + dataStr.close(); + dataStr = null; + } + + } + + /** Get the current number of HDUs in the Fits object. + * @return The number of HDU's in the object. + * @deprecated See getNumberOfHDUs() + */ + public int currentSize() { + return hduList.size(); + } + + /** Get the current number of HDUs in the Fits object. + * @return The number of HDU's in the object. + */ + public int getNumberOfHDUs() { + return hduList.size(); + } + + /** Get the data stream used for the Fits Data. + * @return The associated data stream. Users may wish to + * call this function after opening a Fits object when + * they wish detailed control for writing some part of the FITS file. + */ + public ArrayDataInput getStream() { + return dataStr; + } + + /** Set the data stream to be used for future input. + * + * @param stream The data stream to be used. + */ + public void setStream(ArrayDataInput stream) { + dataStr = stream; + atEOF = false; + lastFileOffset = -1; + } + + /** Create an HDU from the given header. + * @param h The header which describes the FITS extension + */ + public static BasicHDU makeHDU(Header h) throws FitsException { + Data d = FitsFactory.dataFactory(h); + return FitsFactory.HDUFactory(h, d); + } + + /** Create an HDU from the given data kernel. + * @param o The data to be described in this HDU. + */ + public static BasicHDU makeHDU(Object o) throws FitsException { + return FitsFactory.HDUFactory(o); + } + + /** Create an HDU from the given Data. + * @param datum The data to be described in this HDU. + */ + public static BasicHDU makeHDU(Data datum) throws FitsException { + Header hdr = new Header(); + datum.fillHeader(hdr); + return FitsFactory.HDUFactory(hdr, datum); + } + + /** + * Add or update the CHECKSUM keyword. + * @param hdr the primary or other header to get the current DATE + * @throws nom.tam.fits.HeaderCardException + * @author R J Mathar + * @since 2005-10-05 + */ + public static void setChecksum(BasicHDU hdu) + throws nom.tam.fits.HeaderCardException, nom.tam.fits.FitsException, java.io.IOException { + /* the next line with the delete is needed to avoid some unexpected + * problems with non.tam.fits.Header.checkCard() which otherwise says + * it expected PCOUNT and found DATE. + */ + Header hdr = hdu.getHeader(); + hdr.deleteKey("CHECKSUM"); + /* This would need org.nevec.utils.DateUtils compiled before org.nevec.prima.fits .... + * final String doneAt = DateUtils.dateToISOstring(0) ; + * We need to save the value of the comment string because this is becoming part + * of the checksum calculated and needs to be re-inserted again - with the same string - + * when the second/final call to addValue() is made below. + */ + final String doneAt = HeaderCommentsMap.getComment("fits:checksum:1"); + hdr.addValue("CHECKSUM", "0000000000000000", doneAt); + + /* Convert the entire sequence of 2880 byte header cards into a byte array. + * The main benefit compared to the C implementations is that we do not need to worry + * about the particular byte order on machines (Linux/VAX/MIPS vs Hp-UX, Sparc...) supposed that + * the correct implementation is in the write() interface. + */ + ByteArrayOutputStream hduByteImage = new ByteArrayOutputStream(); + System.err.flush(); + hdu.write(new BufferedDataOutputStream(hduByteImage)); + final byte[] data = hduByteImage.toByteArray(); + final long csu = checksum(data); + + /* This time we do not use a deleteKey() to ensure that the keyword is replaced "in place". + * Note that the value of the checksum is actually independent to a permutation of the + * 80-byte records within the header. + */ + hdr.addValue("CHECKSUM", checksumEnc(csu, true), doneAt); + } + + /** + * Add or Modify the CHECKSUM keyword in all headers. + * @throws nom.tam.fits.HeaderCardException + * @throws nom.tam.fits.FitsException + * @author R J Mathar + * @since 2005-10-05 + */ + public void setChecksum() + throws nom.tam.fits.HeaderCardException, nom.tam.fits.FitsException, java.io.IOException { + for (int i = 0; i < getNumberOfHDUs(); i += 1) { + setChecksum(getHDU(i)); + } + } + + /** + * Calculate the Seaman-Pence 32-bit 1's complement checksum over the byte stream. The option + * to start from an intermediate checksum accumulated over another previous + * byte stream is not implemented. + * The implementation accumulates in two 64-bit integer values the two low-order and the two + * high-order bytes of adjacent 4-byte groups. A carry-over of bits is never done within the main + * loop (only once at the end at reduction to a 32-bit positive integer) since an overflow + * of a 64-bit value (signed, with maximum at 2^63-1) by summation of 16-bit values could only + * occur after adding approximately 140G short values (=2^47) (280GBytes) or more. We assume + * for now that this routine here is never called to swallow FITS files of that size or larger. + * @param data the byte sequence + * @return the 32bit checksum in the range from 0 to 2^32-1 + * @see http://heasarc.gsfc.nasa.gov/docs/heasarc/fits/checksum.html + * @author R J Mathar + * @since 2005-10-05 + */ + private static long checksum(final byte[] data) { + long hi = 0; + long lo = 0; + final int len = 2 * (data.length / 4); + // System.out.println(data.length + " bytes") ; + final int remain = data.length % 4; + /* a write(2) on Sparc/PA-RISC would write the MSB first, on Linux the LSB; by some kind + * of coincidence, we can stay with the byte order known from the original C version of + * the algorithm. + */ + for (int i = 0; i < len; i += 2) { + /* The four bytes in this block handled by a single 'i' are each signed (-128 to 127) + * in Java and need to be masked indivdually to avoid sign extension /propagation. + */ + hi += (data[2 * i] << 8) & 0xff00L | data[2 * i + 1] & 0xffL; + lo += (data[2 * i + 2] << 8) & 0xff00L | data[2 * i + 3] & 0xffL; + } + + /* The following three cases actually cannot happen since FITS records are multiples of 2880 bytes. + */ + if (remain >= 1) { + hi += (data[2 * len] << 8) & 0xff00L; + } + if (remain >= 2) { + hi += data[2 * len + 1] & 0xffL; + } + if (remain >= 3) { + lo += (data[2 * len + 2] << 8) & 0xff00L; + } + + long hicarry = hi >>> 16; + long locarry = lo >>> 16; + while (hicarry != 0 || locarry != 0) { + hi = (hi & 0xffffL) + locarry; + lo = (lo & 0xffffL) + hicarry; + hicarry = hi >>> 16; + locarry = lo >>> 16; + } + return (hi << 16) + lo; + } + + /** + * Encode a 32bit integer according to the Seaman-Pence proposal. + * @param c the checksum previously calculated + * @return the encoded string of 16 bytes. + * @see http://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/checksum/node14.html#SECTION00035000000000000000 + * @author R J Mathar + * @since 2005-10-05 + */ + private static String checksumEnc(final long c, final boolean compl) { + byte[] asc = new byte[16]; + final int[] exclude = {0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60}; + final long[] mask = {0xff000000L, 0xff0000L, 0xff00L, 0xffL}; + final int offset = 0x30; /* ASCII 0 (zero */ + final long value = compl ? ~c : c; + for (int i = 0; i < 4; i++) { + final int byt = (int) ((value & mask[i]) >>> (24 - 8 * i)); // each byte becomes four + final int quotient = byt / 4 + offset; + final int remainder = byt % 4; + int[] ch = new int[4]; + for (int j = 0; j < 4; j++) { + ch[j] = quotient; + } + + ch[0] += remainder; + boolean check = true; + for (; check;) // avoid ASCII punctuation + { + check = false; + for (int k = 0; k < exclude.length; k++) { + for (int j = 0; j < 4; j += 2) { + if (ch[j] == exclude[k] || ch[j + 1] == exclude[k]) { + ch[j]++; + ch[j + 1]--; + check = true; + } + } + } + } + + for (int j = 0; j < 4; j++) // assign the bytes + { + asc[4 * j + i] = (byte) (ch[j]); + } + } + + // shift the bytes 1 to the right circularly. + try { + String resul = AsciiFuncs.asciiString(asc, 15, 1); + return resul.concat(AsciiFuncs.asciiString(asc, 0, 15)); + } catch (Exception e) { + // Impossible I hope + System.err.println("CheckSum Error finding ASCII encoding"); + return null; + } + } +} diff --git a/src/nom/tam/fits/FitsDate.java b/src/nom/tam/fits/FitsDate.java new file mode 100644 index 0000000..82641b0 --- /dev/null +++ b/src/nom/tam/fits/FitsDate.java @@ -0,0 +1,365 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * This class was contributed by D. Glowacki. + */ +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.text.DecimalFormat; + +public class FitsDate { + + private int year = -1; + private int month = -1; + private int mday = -1; + private int hour = -1; + private int minute = -1; + private int second = -1; + private int millisecond = -1; + private Date date = null; + + /** + * Convert a FITS date string to a Java Date object. + * @param dStr the FITS date + * @return either null or a Date object + * @exception FitsException if dStr does not + * contain a valid FITS date. + */ + public FitsDate(String dStr) + throws FitsException { + // if the date string is null, we are done + if (dStr == null) { + return; + } + + // if the date string is empty, we are done + dStr = dStr.trim(); + if (dStr.length() == 0) { + return; + } + + // if string contains at least 8 characters... + int len = dStr.length(); + if (len >= 8) { + int first; + + // ... and there is a "/" in the string... + first = dStr.indexOf('-'); + if (first == 4 && first < len) { + + // ... this must be an new-style date + buildNewDate(dStr, first, len); + + // no "/" found; maybe it is an old-style date... + } else { + + first = dStr.indexOf('/'); + if (first > 1 && first < len) { + + // ... this must be an old-style date + buildOldDate(dStr, first, len); + } + } + } + + if (year == -1) { + throw new FitsException("Bad FITS date string \"" + dStr + '"'); + } + } + + private void buildOldDate(String dStr, int first, int len) { + int middle = dStr.indexOf('/', first + 1); + if (middle > first + 2 && middle < len) { + + try { + + year = Integer.parseInt(dStr.substring(middle + 1)) + 1900; + month = Integer.parseInt(dStr.substring(first + 1, middle)); + mday = Integer.parseInt(dStr.substring(0, first)); + + } catch (NumberFormatException e) { + + year = month = mday = -1; + } + } + } + + private void parseTime(String tStr) + throws FitsException { + int first = tStr.indexOf(':'); + if (first < 0) { + throw new FitsException("Bad time"); + } + + int len = tStr.length(); + + int middle = tStr.indexOf(':', first + 1); + if (middle > first + 2 && middle < len) { + + if (middle + 3 < len && tStr.charAt(middle + 3) == '.') { + double d = Double.valueOf(tStr.substring(middle + 3)).doubleValue(); + millisecond = (int) (d * 1000); + + len = middle + 3; + } + + try { + hour = Integer.parseInt(tStr.substring(0, first)); + minute = Integer.parseInt(tStr.substring(first + 1, middle)); + second = Integer.parseInt(tStr.substring(middle + 1, len)); + } catch (NumberFormatException e) { + hour = minute = second = millisecond = -1; + } + } + } + + private void buildNewDate(String dStr, int first, int len) + throws FitsException { + // find the middle separator + int middle = dStr.indexOf('-', first + 1); + if (middle > first + 2 && middle < len) { + + try { + + // if this date string includes a time... + if (middle + 3 < len && dStr.charAt(middle + 3) == 'T') { + + // ... try to parse the time + try { + parseTime(dStr.substring(middle + 4)); + } catch (FitsException e) { + throw new FitsException("Bad time in FITS date string \"" + + dStr + "\""); + } + + // we got the time; mark the end of the date string + len = middle + 3; + } + + // parse date string + year = Integer.parseInt(dStr.substring(0, first)); + month = Integer.parseInt(dStr.substring(first + 1, middle)); + mday = Integer.parseInt(dStr.substring(middle + 1, len)); + + } catch (NumberFormatException e) { + + // yikes, something failed; reset everything + year = month = mday = hour = minute = second = millisecond = -1; + } + } + } + + /** Get a Java Date object corresponding to this + * FITS date. + * @return The Java Date object. + */ + public Date toDate() { + if (date == null && year != -1) { + TimeZone tz = TimeZone.getTimeZone("GMT"); + GregorianCalendar cal = new GregorianCalendar(tz); + + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month - 1); + cal.set(Calendar.DAY_OF_MONTH, mday); + + if (hour == -1) { + + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + + } else { + + cal.set(Calendar.HOUR_OF_DAY, hour); + cal.set(Calendar.MINUTE, minute); + cal.set(Calendar.SECOND, second); + if (millisecond == -1) { + cal.set(Calendar.MILLISECOND, 0); + } else { + cal.set(Calendar.MILLISECOND, millisecond); + } + } + + date = cal.getTime(); + } + + return date; + } + + /** Return the current date in FITS date format */ + public static String getFitsDateString() { + return getFitsDateString(new Date(), true); + } + + /** Create FITS format date string Java Date object. + * @param epoch The epoch to be converted to FITS format. + */ + public static String getFitsDateString(Date epoch) { + return getFitsDateString(epoch, true); + } + + /** Create FITS format date string. + * Note that the date is not rounded. + * @param epoch The epoch to be converted to FITS format. + * @param timeOfDay Should time of day information be included? + */ + public static String getFitsDateString(Date epoch, boolean timeOfDay) { + + try { + GregorianCalendar cal = new GregorianCalendar( + TimeZone.getTimeZone("GMT")); + + + cal.setTime(epoch); + + StringBuffer fitsDate = new StringBuffer(); + DecimalFormat df = new DecimalFormat("0000"); + fitsDate.append(df.format(cal.get(Calendar.YEAR))); + fitsDate.append("-"); + df = new DecimalFormat("00"); + + fitsDate.append(df.format(cal.get(Calendar.MONTH) + 1)); + fitsDate.append("-"); + fitsDate.append(df.format(cal.get(Calendar.DAY_OF_MONTH))); + + if (timeOfDay) { + fitsDate.append("T"); + fitsDate.append(df.format(cal.get(Calendar.HOUR_OF_DAY))); + fitsDate.append(":"); + fitsDate.append(df.format(cal.get(Calendar.MINUTE))); + fitsDate.append(":"); + fitsDate.append(df.format(cal.get(Calendar.SECOND))); + fitsDate.append("."); + df = new DecimalFormat("000"); + fitsDate.append(df.format(cal.get(Calendar.MILLISECOND))); + } + + return new String(fitsDate); + + } catch (Exception e) { + + return new String(""); + } + } + + public String toString() { + if (year == -1) { + return ""; + } + + StringBuffer buf = new StringBuffer(23); + buf.append(year); + buf.append('-'); + if (month < 10) { + buf.append('0'); + } + buf.append(month); + buf.append('-'); + if (mday < 10) { + buf.append('0'); + } + buf.append(mday); + + if (hour != -1) { + + buf.append('T'); + if (hour < 10) { + buf.append('0'); + } + + buf.append(hour); + buf.append(':'); + + if (minute < 10) { + buf.append('0'); + } + + buf.append(minute); + buf.append(':'); + + if (second < 10) { + buf.append('0'); + } + buf.append(second); + + if (millisecond != -1) { + buf.append('.'); + + if (millisecond < 100) { + if (millisecond < 10) { + buf.append("00"); + } else { + buf.append('0'); + } + } + buf.append(millisecond); + } + } + + return buf.toString(); + } + + public static void testArgs(String args[]) { + for (int i = 0; i < args.length; i++) { + + try { + FitsDate fd = new FitsDate(args[i]); + System.out.println("\"" + args[i] + "\" => " + fd + " => " + + fd.toDate()); + } catch (Exception e) { + System.err.println("Date \"" + args[i] + "\" threw " + + e.getClass().getName() + "(" + e.getMessage() + + ")"); + } + } + } + + public static void autotest() { + String[] good = new String[6]; + good[0] = "20/09/79"; + good[1] = "1997-07-25"; + good[2] = "1987-06-05T04:03:02.01"; + good[3] = "1998-03-10T16:58:34"; + good[4] = null; + good[5] = " "; + testArgs(good); + + String[] badOld = new String[4]; + badOld[0] = "20/09/"; + badOld[1] = "/09/79"; + badOld[2] = "09//79"; + badOld[3] = "20/09/79/"; + testArgs(badOld); + + String[] badNew = new String[4]; + badNew[0] = "1997-07"; + badNew[1] = "-07-25"; + badNew[2] = "1997--07-25"; + badNew[3] = "1997-07-25-"; + testArgs(badNew); + + String[] badMisc = new String[4]; + badMisc[0] = "5-Aug-1992"; + badMisc[1] = "28/02/91 16:32:00"; + badMisc[2] = "18-Feb-1993"; + badMisc[3] = "nn/nn/nn"; + testArgs(badMisc); + } + + public static void main(String args[]) { + if (args.length == 0) { + autotest(); + } else { + testArgs(args); + } + } +} diff --git a/src/nom/tam/fits/FitsElement.java b/src/nom/tam/fits/FitsElement.java new file mode 100644 index 0000000..7a66b03 --- /dev/null +++ b/src/nom/tam/fits/FitsElement.java @@ -0,0 +1,44 @@ +/** This inteface describes allows uses to easily perform + * basic I/O operations + * on a FITS element. + */ +package nom.tam.fits; + +import nom.tam.util.*; +import java.io.IOException; + +public interface FitsElement { + + /** Read the contents of the element from an input source. + * @param in The input source. + */ + public void read(ArrayDataInput in) throws FitsException, IOException; + + /** Write the contents of the element to a data sink. + * @param out The data sink. + */ + public void write(ArrayDataOutput out) throws FitsException, IOException; + + /** Rewrite the contents of the element in place. + * The data must have been orignally read from a random + * access device, and the size of the element may not have changed. + */ + public void rewrite() throws FitsException, IOException; + + /** Get the byte at which this element begins. + * This is only available if the data is originally read from + * a random access medium. + */ + public long getFileOffset(); + + /** Can this element be rewritten? */ + public boolean rewriteable(); + + /** The size of this element in bytes */ + public long getSize(); + + /** Reset the input stream to point to the beginning of this element + * @return True if the reset succeeded. + */ + public boolean reset(); +} diff --git a/src/nom/tam/fits/FitsException.java b/src/nom/tam/fits/FitsException.java new file mode 100644 index 0000000..22aaeb2 --- /dev/null +++ b/src/nom/tam/fits/FitsException.java @@ -0,0 +1,25 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +public class FitsException extends Exception { + + public FitsException() { + super(); + } + + public FitsException(String msg) { + super(msg); + } + + public FitsException(String msg, Exception reason) { + super(msg, reason); + } +} diff --git a/src/nom/tam/fits/FitsFactory.java b/src/nom/tam/fits/FitsFactory.java new file mode 100644 index 0000000..c9d458d --- /dev/null +++ b/src/nom/tam/fits/FitsFactory.java @@ -0,0 +1,136 @@ +package nom.tam.fits; + +/** This class contains the code which + * associates particular FITS types with header + * and data configurations. It comprises + * a set of Factory methods which call + * appropriate methods in the HDU classes. + * If -- God forbid -- a new FITS HDU type were + * created, then the XXHDU, XXData classes would + * need to be added and this file modified but + * no other changes should be needed in the FITS libraries. + * + */ +public class FitsFactory { + + private static boolean useAsciiTables = true; + private static boolean useHierarch = false; + private static boolean checkAsciiStrings = false; + + /** Indicate whether ASCII tables should be used + * where feasible. + */ + public static void setUseAsciiTables(boolean flag) { + useAsciiTables = flag; + } + + /** Get the current status of ASCII table writing */ + static boolean getUseAsciiTables() { + return useAsciiTables; + } + + /** Enable/Disable hierarchical keyword processing. */ + public static void setUseHierarch(boolean flag) { + useHierarch = flag; + } + + /** Enable/Disable checking of strings values used in tables + * to ensure that they are within the range specified by the + * FITS standard. The standard only allows the values 0x20 - 0x7E + * with null bytes allowed in one limited context. + * Disabled by default. + */ + public static void setCheckAsciiStrings(boolean flag) { + checkAsciiStrings = flag; + } + + /** Get the current status for string checking. */ + static boolean getCheckAsciiStrings() { + return checkAsciiStrings; + } + + /** Are we processing HIERARCH style keywords */ + public static boolean getUseHierarch() { + return useHierarch; + } + + /** Given a Header return an appropriate datum. + */ + public static Data dataFactory(Header hdr) throws FitsException { + + if (ImageHDU.isHeader(hdr)) { + Data d = ImageHDU.manufactureData(hdr); + hdr.afterExtend(); // Fix for positioning error noted by V. Forchi + return d; + } else if (RandomGroupsHDU.isHeader(hdr)) { + return RandomGroupsHDU.manufactureData(hdr); + } else if (useAsciiTables && AsciiTableHDU.isHeader(hdr)) { + return AsciiTableHDU.manufactureData(hdr); + } else if (BinaryTableHDU.isHeader(hdr)) { + return BinaryTableHDU.manufactureData(hdr); + } else if (UndefinedHDU.isHeader(hdr)) { + return UndefinedHDU.manufactureData(hdr); + } else { + throw new FitsException("Unrecognizable header in dataFactory"); + } + + } + + /** Given an object, create the appropriate + * FITS header to describe it. + * @param o The object to be described. + */ + public static BasicHDU HDUFactory(Object o) throws FitsException { + Data d; + Header h; + + if (o instanceof Header) { + h = (Header) o; + d = dataFactory(h); + + } else if (ImageHDU.isData(o)) { + d = ImageHDU.encapsulate(o); + h = ImageHDU.manufactureHeader(d); + } else if (RandomGroupsHDU.isData(o)) { + d = RandomGroupsHDU.encapsulate(o); + h = RandomGroupsHDU.manufactureHeader(d); + } else if (useAsciiTables && AsciiTableHDU.isData(o)) { + d = AsciiTableHDU.encapsulate(o); + h = AsciiTableHDU.manufactureHeader(d); + } else if (BinaryTableHDU.isData(o)) { + d = BinaryTableHDU.encapsulate(o); + h = BinaryTableHDU.manufactureHeader(d); + } else if (UndefinedHDU.isData(o)) { + d = UndefinedHDU.encapsulate(o); + h = UndefinedHDU.manufactureHeader(d); + } else { + throw new FitsException("Invalid data presented to HDUFactory"); + } + + return HDUFactory(h, d); + + } + + /** Given Header and data objects return + * the appropriate type of HDU. + */ + public static BasicHDU HDUFactory(Header hdr, Data d) throws + FitsException { + + if (d instanceof ImageData) { + return new ImageHDU(hdr, d); + } else if (d instanceof RandomGroupsData) { + return new RandomGroupsHDU(hdr, d); + } else if (d instanceof AsciiTable) { + return new AsciiTableHDU(hdr, d); + } else if (d instanceof BinaryTable) { + return new BinaryTableHDU(hdr, d); + } else if (d instanceof UndefinedData) { + return new UndefinedHDU(hdr, d); + } + + return null; + } +} + + diff --git a/src/nom/tam/fits/FitsHeap.java b/src/nom/tam/fits/FitsHeap.java new file mode 100644 index 0000000..d5a1236 --- /dev/null +++ b/src/nom/tam/fits/FitsHeap.java @@ -0,0 +1,179 @@ +package nom.tam.fits; + +import nom.tam.util.*; +import java.io.*; + +/** This class supports the FITS heap. This + * is currently used for variable length columns + * in binary tables. + */ +public class FitsHeap implements FitsElement { + + /** The storage buffer */ + private byte[] heap; + /** The current used size of the buffer <= heap.length */ + private int heapSize; + /** The offset within a file where the heap begins */ + private long fileOffset = -1; + /** Has the heap ever been expanded? */ + private boolean expanded = false; + /** The stream the last read used */ + private ArrayDataInput input; + /** Our current offset into the heap. When we read from + * the heap we use a byte array input stream. So long + * as we continue to read further into the heap, we can + * continue to use the same stream, but we need to + * recreate the stream whenever we skip backwards. + */ + private int heapOffset = 0; + /** A stream used to read the heap data */ + private BufferedDataInputStream bstr; + + /** Create a heap of a given size. */ + FitsHeap(int size) { + heap = new byte[size]; + heapSize = size; + } + + /** Read the heap */ + public void read(ArrayDataInput str) throws FitsException { + + if (str instanceof RandomAccess) { + fileOffset = FitsUtil.findOffset(str); + input = str; + } + + if (heap != null) { + try { + str.read(heap, 0, heapSize); + } catch (IOException e) { + throw new FitsException("Error reading heap:" + e); + } + } + + bstr = null; + } + + /** Write the heap */ + public void write(ArrayDataOutput str) throws FitsException { + try { + str.write(heap, 0, heapSize); + } catch (IOException e) { + throw new FitsException("Error writing heap:" + e); + } + } + + public boolean rewriteable() { + return fileOffset >= 0 && input instanceof ArrayDataOutput && !expanded; + } + + /** Attempt to rewrite the heap with the current contents. + * Note that no checking is done to make sure that the + * heap does not extend past its prior boundaries. + */ + public void rewrite() throws IOException, FitsException { + if (rewriteable()) { + ArrayDataOutput str = (ArrayDataOutput) input; + FitsUtil.reposition(str, fileOffset); + write(str); + } else { + throw new FitsException("Invalid attempt to rewrite FitsHeap"); + } + + } + + public boolean reset() { + try { + FitsUtil.reposition(input, fileOffset); + return true; + } catch (Exception e) { + return false; + } + } + + /** Get data from the heap. + * @param offset The offset at which the data begins. + * @param array The array to be extracted. + */ + public void getData(int offset, Object array) throws FitsException { + + try { + // Can we reuse the existing byte stream? + if (bstr == null || heapOffset > offset) { + heapOffset = 0; + bstr = new BufferedDataInputStream( + new ByteArrayInputStream(heap)); + } + + bstr.skipBytes(offset - heapOffset); + heapOffset = offset; + heapOffset += bstr.readLArray(array); + + } catch (IOException e) { + throw new FitsException("Error decoding heap area at offset=" + offset + + ". Exception: Exception " + e); + } + } + + /** Check if the Heap can accommodate a given requirement. + * If not expand the heap. + */ + void expandHeap(int need) { + + // Invalidate any existing input stream to the heap. + bstr = null; + + if (heapSize + need > heap.length) { + expanded = true; + int newlen = (heapSize + need) * 2; + if (newlen < 16384) { + newlen = 16384; + } + byte[] newHeap = new byte[newlen]; + System.arraycopy(heap, 0, newHeap, 0, heapSize); + heap = newHeap; + } + } + + /** Add some data to the heap. */ + int putData(Object data) throws FitsException { + + long lsize = ArrayFuncs.computeLSize(data); + if (lsize > Integer.MAX_VALUE) { + throw new FitsException("FITS Heap > 2 G"); + } + int size = (int) lsize; + expandHeap(size); + ByteArrayOutputStream bo = new ByteArrayOutputStream(size); + + try { + BufferedDataOutputStream o = new BufferedDataOutputStream(bo); + o.writeArray(data); + o.flush(); + o.close(); + } catch (IOException e) { + throw new FitsException("Unable to write variable column length data"); + } + + System.arraycopy(bo.toByteArray(), 0, heap, heapSize, size); + int oldOffset = heapSize; + heapSize += size; + + return oldOffset; + } + + /** Return the size of the Heap */ + public int size() { + return heapSize; + } + + /** Return the size of the heap using the more bean compatbile format */ + public long getSize() { + return size(); + } + + /** Get the file offset of the heap */ + public long getFileOffset() { + return fileOffset; + } +} diff --git a/src/nom/tam/fits/FitsUtil.java b/src/nom/tam/fits/FitsUtil.java new file mode 100644 index 0000000..014eb38 --- /dev/null +++ b/src/nom/tam/fits/FitsUtil.java @@ -0,0 +1,488 @@ +package nom.tam.fits; + +/* Copyright: Thomas McGlynn 1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +import nom.tam.util.RandomAccess; + +import java.io.IOException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PushbackInputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; + +import java.net.URL; +import java.net.URLConnection; + +import java.util.Map; +import java.util.List; +import java.util.zip.GZIPInputStream; +import nom.tam.util.ArrayDataOutput; +import nom.tam.util.AsciiFuncs; + +/** This class comprises static + * utility functions used throughout + * the FITS classes. + */ +public class FitsUtil { + + private static boolean wroteCheckingError = false; + + /** Reposition a random access stream to a requested offset */ + public static void reposition(Object o, long offset) + throws FitsException { + + if (o == null) { + throw new FitsException("Attempt to reposition null stream"); + } + if (!(o instanceof RandomAccess) + || offset < 0) { + throw new FitsException("Invalid attempt to reposition stream " + o + + " of type " + o.getClass().getName() + + " to " + offset); + } + + try { + ((RandomAccess) o).seek(offset); + } catch (IOException e) { + throw new FitsException("Unable to repostion stream " + o + + " of type " + o.getClass().getName() + + " to " + offset + " Exception:" + e); + } + } + + /** Find out where we are in a random access file */ + public static long findOffset(Object o) { + + if (o instanceof RandomAccess) { + return ((RandomAccess) o).getFilePointer(); + } else { + return -1; + } + } + + /** How many bytes are needed to fill the last 2880 block? */ + public static int padding(int size) { + return padding((long) size); + } + + public static int padding(long size) { + + int mod = (int) (size % 2880); + if (mod > 0) { + mod = 2880 - mod; + } + return mod; + } + + /** Total size of blocked FITS element */ + public static int addPadding(int size) { + return size + padding(size); + } + + public static long addPadding(long size) { + return size + padding(size); + } + + /** This method decompresses a compressed + * input stream. The decompression method is + * selected automatically based upon the first two bytes read. + * @param compressed The compressed input stram + * @return A stream which wraps the input stream and decompresses + * it. If the input stream is not compressed, a + * pushback input stream wrapping the original stream is returned. + */ + static InputStream decompress(InputStream compressed) throws FitsException { + + PushbackInputStream pb = new PushbackInputStream(compressed, 2); + + int mag1 = -1; + int mag2 = -1; + + try { + mag1 = pb.read(); + mag2 = pb.read(); + + if (mag1 == 0x1f && mag2 == 0x8b) { + // Push the data back into the stream + pb.unread(mag2); + pb.unread(mag1); + return new GZIPInputStream(pb); + } else if (mag1 == 0x1f && mag2 == 0x9d) { + // Push the data back into the stream + pb.unread(mag2); + pb.unread(mag1); + return compressInputStream(pb); + } else if (mag1 == 'B' && mag2 == 'Z') { + if (System.getenv("BZIP_DECOMPRESSOR") != null) { + pb.unread(mag2); + pb.unread(mag1); + return bunzipper(pb); + } + // Don't pushback + String cname = "org.apache.tools.bzip2.CBZip2InputStream"; + // Note that we forego generics here since we don't + // want any explicit mention of this class so that users + // can compile and run without worrying about having the class in hand. + try { + Constructor con = Class.forName(cname).getConstructor(InputStream.class); + return (InputStream) con.newInstance(pb); + } catch (Exception e) { + System.err.println("Unable to find constructor for BZIP2 decompression. Is the Apache BZIP jar in the classpath?"); + throw new FitsException("No CBZip2InputStream class found for bzip2 compressed file"); + } + } else { + // Push the data back into the stream + pb.unread(mag2); + pb.unread(mag1); + return pb; + } + + } catch (IOException e) { + // This is probably a prelude to failure... + throw new FitsException("Unable to analyze input stream"); + } + } + + static InputStream compressInputStream(final InputStream compressed) throws FitsException { + try { + Process proc = new ProcessBuilder("uncompress", "-c").start(); + + // This is the input to the process -- but + // an output from here. + final OutputStream input = proc.getOutputStream(); + + // Now copy everything in a separate thread. + Thread copier = new Thread( + new Runnable() { + + public void run() { + try { + byte[] buffer = new byte[8192]; + int len; + while ((len = compressed.read(buffer, 0, buffer.length)) > 0) { + input.write(buffer, 0, len); + } + compressed.close(); + input.close(); + } catch (IOException e) { + return; + } + } + }); + copier.start(); + return proc.getInputStream(); + } catch (Exception e) { + throw new FitsException("Unable to read .Z compressed stream.\nIs `uncompress' in the path?\n:" + e); + } + } + + /** Is a file compressed? */ + public static boolean isCompressed(File test) { + InputStream fis = null; + try { + if (test.exists()) { + fis = new FileInputStream(test); + int mag1 = fis.read(); + int mag2 = fis.read(); + fis.close(); + if (mag1 == 0x1f && (mag2 == 0x8b || mag2 == 0x9d)) { + return true; + } else if (mag1 == 'B' && mag2 == 'Z') { + return true; + } else { + return false; + } + } + + } catch (IOException e) { + // This is probably a prelude to failure... + return false; + + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + } + } + } + return false; + } + + /** Check if a file seems to be compressed. + */ + public static boolean isCompressed(String filename) { + if (filename == null) { + return false; + } + FileInputStream fis = null; + File test = new File(filename); + if (test.exists()) { + return isCompressed(test); + } + + int len = filename.length(); + return len > 2 && (filename.substring(len - 3).equalsIgnoreCase(".gz") || filename.substring(len - 2).equals(".Z")); + } + + /** Get the maximum length of a String in a String array. + */ + public static int maxLength(String[] o) throws FitsException { + + int max = 0; + for (int i = 0; i < o.length; i += 1) { + if (o[i] != null && o[i].length() > max) { + max = o[i].length(); + } + } + return max; + } + + /** Copy an array of Strings to bytes.*/ + public static byte[] stringsToByteArray(String[] o, int maxLen) { + byte[] res = new byte[o.length * maxLen]; + for (int i = 0; i < o.length; i += 1) { + byte[] bstr = null; + if (o[i] == null) { + bstr = new byte[0]; + } else { + bstr = AsciiFuncs.getBytes(o[i]); + } + int cnt = bstr.length; + if (cnt > maxLen) { + cnt = maxLen; + } + System.arraycopy(bstr, 0, res, i * maxLen, cnt); + for (int j = cnt; j < maxLen; j += 1) { + res[i * maxLen + j] = (byte) ' '; + } + } + return res; + } + + /** Convert bytes to Strings */ + public static String[] byteArrayToStrings(byte[] o, int maxLen) { + boolean checking = FitsFactory.getCheckAsciiStrings(); + + // Note that if a String in a binary table contains an internal 0, + // the FITS standard says that it is to be considered as terminating + // the string at that point, so that software reading the + // data back may not include subsequent characters. + // No warning of this truncation is given. + + String[] res = new String[o.length / maxLen]; + for (int i = 0; i < res.length; i += 1) { + + int start = i * maxLen; + int end = start + maxLen; + // Pre-trim the string to avoid keeping memory + // hanging around. (Suggested by J.C. Segovia, ESA). + + // Note that the FITS standard does not mandate + // that we should be trimming the string at all, but + // this seems to best meet the desires of the community. + for (; start < end; start += 1) { + if (o[start] != 32) { + break; // Skip only spaces. + } + } + + for (; end > start; end -= 1) { + if (o[end - 1] != 32) { + break; + } + } + + // For FITS binary tables, 0 values are supposed + // to terminate strings, a la C. [They shouldn't appear in + // any other context.] + // Other non-printing ASCII characters + // should always be an error which we can check for + // if the user requests. + + // The lack of handling of null bytes was noted by Laurent Bourges. + boolean errFound = false; + for (int j = start; j < end; j += 1) { + + if (o[j] == 0) { + end = j; + break; + } + if (checking) { + if (o[j] < 32 || o[j] > 126) { + errFound = true; + o[j] = 32; + } + } + } + res[i] = AsciiFuncs.asciiString(o, start, end - start); + if (errFound && !wroteCheckingError) { + System.err.println("Warning: Invalid ASCII character[s] detected in string:" + res[i]); + System.err.println(" Converted to space[s]. Any subsequent invalid characters will be converted silently"); + wroteCheckingError = true; + } + } + return res; + + } + + /** Convert an array of booleans to bytes */ + static byte[] booleanToByte(boolean[] bool) { + + byte[] byt = new byte[bool.length]; + for (int i = 0; i < bool.length; i += 1) { + byt[i] = bool[i] ? (byte) 'T' : (byte) 'F'; + } + return byt; + } + + /** Convert an array of bytes to booleans */ + static boolean[] byteToBoolean(byte[] byt) { + boolean[] bool = new boolean[byt.length]; + + + + + for (int i = 0; i < byt.length; i += 1) { + bool[i] = (byt[i] == 'T'); + } + return bool; + } + + /** Get a stream to a URL accommodating possible redirections. + * Note that if a redirection request points to a different + * protocol than the original request, then the redirection + * is not handled automatically. + */ + public static InputStream getURLStream(URL url, int level) throws IOException { + + // Hard coded....sigh + if (level > 5) { + throw new IOException("Two many levels of redirection in URL"); + } + URLConnection conn = url.openConnection(); +// Map> hdrs = conn.getHeaderFields(); + Map hdrs = conn.getHeaderFields(); + + // Read through the headers and see if there is a redirection header. + // We loop (rather than just do a get on hdrs) + // since we want to match without regard to case. + String[] keys = (String[]) hdrs.keySet().toArray(new String[0]); +// for (String key: hdrs.keySet()) { + for (int i = 0; i < keys.length; i += 1) { + String key = keys[i]; + + if (key != null && key.toLowerCase().equals("location")) { +// String val = hdrs.get(key).get(0); + String val = (String) ((List) hdrs.get(key)).get(0); + if (val != null) { + val = val.trim(); + if (val.length() > 0) { + // Redirect + return getURLStream(new URL(val), level + 1); + } + } + } + } + // No redirection + return conn.getInputStream(); + } + + /** Add padding to an output stream. */ + public static void pad(ArrayDataOutput stream, long size) throws FitsException { + pad(stream, size, (byte) 0); + } + + /** Add padding to an output stream. */ + public static void pad(ArrayDataOutput stream, long size, byte fill) + throws FitsException { + int len = padding(size); + if (len > 0) { + byte[] buf = new byte[len]; + for (int i = 0; i < len; i += 1) { + buf[i] = fill; + } + try { + stream.write(buf); + stream.flush(); + } catch (Exception e) { + throw new FitsException("Unable to write padding", e); + } + } + } + + static InputStream bunzipper(final InputStream pb) throws FitsException { + String cmd = System.getenv("BZIP_DECOMPRESSOR"); + // Allow the user to have already specified the - option. + if (cmd.indexOf(" -") < 0) { + cmd += " -"; + } + final OutputStream out; + String[] flds = cmd.split(" +"); + Thread t; + Process p; + try { + p = new ProcessBuilder(flds).start(); + out = p.getOutputStream(); + + t = new Thread(new Runnable() { + + public void run() { + try { + byte[] buf = new byte[16384]; + int len; + long total = 0; + while ((len = pb.read(buf)) > 0) { + try { + out.write(buf, 0, len); + } catch (Exception e) { + // Skip this. It can happen when we + // stop reading the compressed file in mid stream. + break; + } + total += len; + } + pb.close(); + out.close(); + + } catch (IOException e) { + throw new Error("Error reading BZIP compression using: " + System.getenv("BZIP_DECOMPRESSOR"), e); + } + } + }); + + } catch (Exception e) { + throw new FitsException("Error initiating BZIP decompression: " + e); + } + t.start(); + return new CloseIS(p.getInputStream(), pb, out); + + } +} + +class CloseIS extends FilterInputStream { + + InputStream i; + OutputStream o; + + CloseIS(InputStream inp, InputStream i, OutputStream o) { + super(inp); + this.i = i; + this.o = o; + } + + public void close() throws IOException { + super.close(); + o.close(); + i.close(); + } +} + diff --git a/src/nom/tam/fits/Header.java b/src/nom/tam/fits/Header.java new file mode 100644 index 0000000..fc0cd35 --- /dev/null +++ b/src/nom/tam/fits/Header.java @@ -0,0 +1,1300 @@ +package nom.tam.fits; + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +import java.io.*; +import java.util.*; +import nom.tam.util.RandomAccess; +import nom.tam.util.*; + +/** This class describes methods to access and manipulate the header + * for a FITS HDU. This class does not include code specific + * to particular types of HDU. + * + * As of version 1.1 this class supports the long keyword convention + * which allows long string keyword values to be split among multiple + * keywords + *
+ *    KEY        = 'ABC&'   /A comment
+ *    CONTINUE      'DEF&'  / Another comment
+ *    CONTINUE      'GHIJKL '
+ * 
+ * The methods getStringValue(key), addValue(key,value,comment) + * and deleteCard(key) will get, create/update and delete long string + * values if the longStringsEnabled flag is set. This flag is set + * automatically when a FITS header with a LONGSTRN card is found. + * The value is not checked. It may also be set/unset using the + * static method setLongStringsEnabled(boolean). [So if a user wishes to ensure + * that it is not set, it should be unset after any header is read] + * When long strings are found in the FITS header users should be careful not to interpose + * new header cards within a long value sequence. + * + * When writing long strings, the comment is included in the last + * card. If a user is writing long strings, a the keyword + * LONGSTRN = 'OGIP 1.0' + * should be added to the FITS header, but this is not done automatically + * for the user. + */ +public class Header implements FitsElement { + + /** The actual header data stored as a HashedList of + * HeaderCard's. + */ + private HashedList cards = new HashedList(); + /** This iterator allows one to run through the list. + */ + private Cursor iter = cards.iterator(0); + /** Offset of this Header in the FITS file */ + private long fileOffset = -1; + /** Number of cards in header last time it was read */ + private int oldSize; + /** Input descriptor last time header was read */ + private ArrayDataInput input; + + /** Create an empty header */ + public Header() { + } + /** Do we support long strings when reading/writing keywords */ + private static boolean longStringsEnabled = false; + + public static void setLongStringsEnabled(boolean flag) { + longStringsEnabled = flag; + } + + public static boolean getLongStringsEnabled() { + return longStringsEnabled; + } + + /** Create a header and populate it from the input stream + * @param is The input stream where header information is expected. + */ + public Header(ArrayDataInput is) + throws TruncatedFileException, IOException { + read(is); + } + + /** Create a header and initialize it with a vector of strings. + * @param newCards Card images to be placed in the header. + */ + public Header(String[] newCards) { + + for (int i = 0; i < newCards.length; i += 1) { + HeaderCard card = new HeaderCard(newCards[i]); + if (card.getValue() == null) { + cards.add(card); + } else { + cards.add(card.getKey(), card); + } + + } + } + + /** Create a header which points to the + * given data object. + * @param o The data object to be described. + * @exception FitsException if the data was not valid for this header. + */ + public Header(Data o) throws FitsException { + o.fillHeader(this); + } + + /** Create the data element corresponding to the current header */ + public Data makeData() throws FitsException { + return FitsFactory.dataFactory(this); + } + + /** + * Update a line in the header + * @param key The key of the card to be replaced. + * @param card A new card + */ + public void updateLine(String key, HeaderCard card) throws HeaderCardException { + removeCard(key); + iter.add(key, card); + } + + /** + * Overwrite the lines in the header. + * Add the new PHDU header to the current one. If keywords appear + * twice, the new value and comment overwrite the current contents. + * + * @param newHdr the list of new header data lines to replace the current + * ones. + * @throws nom.tam.fits.HeaderCardException + * @author Richard J Mathar + * @since 2005-10-24 + */ + public void updateLines(final Header newHdr) throws nom.tam.fits.HeaderCardException { + Cursor j = newHdr.iterator(); + + while (j.hasNext()) { + HeaderCard nextHCard = (HeaderCard) j.next(); + // updateLine() doesn't work with COMMENTs because + // this would allow only one COMMENT in total in each header + if (nextHCard.getKey().startsWith("COMMENT")) { + insertComment(nextHCard.getComment()); + } else { + updateLine(nextHCard.getKey(), nextHCard); + } + } + } + + /** Find the number of cards in the header */ + public int getNumberOfCards() { + return cards.size(); + } + + /** Get an iterator over the header cards */ + public Cursor iterator() { + return cards.iterator(0); + } + + /** Get the offset of this header */ + public long getFileOffset() { + return fileOffset; + } + + /** Calculate the unpadded size of the data segment from + * the header information. + * + * @return the unpadded data segment size. + */ + int trueDataSize() { + + if (!isValidHeader()) { + return 0; + } + + + int naxis = getIntValue("NAXIS", 0); + int bitpix = getIntValue("BITPIX"); + + int[] axes = new int[naxis]; + + for (int axis = 1; axis <= naxis; axis += 1) { + axes[axis - 1] = getIntValue("NAXIS" + axis, 0); + } + + boolean isGroup = getBooleanValue("GROUPS", false); + + int pcount = getIntValue("PCOUNT", 0); + int gcount = getIntValue("GCOUNT", 1); + + int startAxis = 0; + + if (isGroup && naxis > 1 && axes[0] == 0) { + startAxis = 1; + } + + int size = 1; + for (int i = startAxis; i < naxis; i += 1) { + size *= axes[i]; + } + + size += pcount; + size *= gcount; + + // Now multiply by the number of bits per pixel and + // convert to bytes. + size *= Math.abs(getIntValue("BITPIX", 0)) / 8; + + return size; + } + + /** Return the size of the data including any needed padding. + * @return the data segment size including any needed padding. + */ + public long getDataSize() { + return FitsUtil.addPadding(trueDataSize()); + } + + /** Get the size of the header in bytes */ + public long getSize() { + return headerSize(); + } + + /** Return the size of the header data including padding. + * @return the header size including any needed padding. + */ + int headerSize() { + + if (!isValidHeader()) { + return 0; + } + + return FitsUtil.addPadding(cards.size() * 80); + } + + /** Is this a valid header. + * @return true for a valid header, + * false otherwise. + */ + boolean isValidHeader() { + + if (getNumberOfCards() < 4) { + return false; + } + iter = iterator(); + + String key = ((HeaderCard) iter.next()).getKey(); + if (!key.equals("SIMPLE") && !key.equals("XTENSION")) { + return false; + } + key = ((HeaderCard) iter.next()).getKey(); + if (!key.equals("BITPIX")) { + return false; + } + key = ((HeaderCard) iter.next()).getKey(); + if (!key.equals("NAXIS")) { + return false; + } + while (iter.hasNext()) { + key = ((HeaderCard) iter.next()).getKey(); + } + if (!key.equals("END")) { + return false; + } + return true; + + } + + /** Find the card associated with a given key. + * If found this sets the mark to the card, otherwise it + * unsets the mark. + * @param key The header key. + * @return null if the keyword could not be found; + * return the HeaderCard object otherwise. + */ + public HeaderCard findCard(String key) { + + HeaderCard card = (HeaderCard) cards.get(key); + if (card != null) { + iter.setKey(key); + } + return card; + } + + /** Get the value associated with the key as an int. + * @param key The header key. + * @param dft The value to be returned if the key is not found. + */ + public int getIntValue(String key, int dft) { + return (int) getLongValue(key, (long) dft); + } + + /** Get the int value associated with the given key. + * @param key The header key. + * @return The associated value or 0 if not found. + */ + public int getIntValue(String key) { + return (int) getLongValue(key); + } + + /** Get the long value associated with the given key. + * @param key The header key. + * @return The associated value or 0 if not found. + */ + public long getLongValue(String key) { + return getLongValue(key, 0L); + } + + /** Get the long value associated with the given key. + * @param key The header key. + * @param dft The default value to be returned if the key cannot be found. + * @return the associated value. + */ + public long getLongValue(String key, long dft) { + + HeaderCard fcard = findCard(key); + if (fcard == null) { + return dft; + } + + try { + String v = fcard.getValue(); + if (v != null) { + return Long.parseLong(v); + } + } catch (NumberFormatException e) { + } + + return dft; + } + + /** Get the float value associated with the given key. + * @param key The header key. + * @param dft The value to be returned if the key is not found. + */ + public float getFloatValue(String key, float dft) { + return (float) getDoubleValue(key, dft); + } + + /** Get the float value associated with the given key. + * @param key The header key. + * @return The associated value or 0.0 if not found. + */ + public float getFloatValue(String key) { + return (float) getDoubleValue(key); + } + + /** Get the double value associated with the given key. + * @param key The header key. + * @return The associated value or 0.0 if not found. + */ + public double getDoubleValue(String key) { + return getDoubleValue(key, 0.); + } + + /** Get the double value associated with the given key. + * @param key The header key. + * @param dft The default value to return if the key cannot be found. + * @return the associated value. + */ + public double getDoubleValue(String key, double dft) { + + HeaderCard fcard = findCard(key); + if (fcard == null) { + return dft; + } + + try { + String v = fcard.getValue(); + if (v != null) { + return new Double(v).doubleValue(); + } + } catch (NumberFormatException e) { + } + + return dft; + } + + /** Get the boolean value associated with the given key. + * @param The header key. + * @return The value found, or false if not found or if the + * keyword is not a logical keyword. + */ + public boolean getBooleanValue(String key) { + return getBooleanValue(key, false); + } + + /** Get the boolean value associated with the given key. + * @param key The header key. + * @param dft The value to be returned if the key cannot be found + * or if the parameter does not seem to be a boolean. + * @return the associated value. + */ + public boolean getBooleanValue(String key, boolean dft) { + + HeaderCard fcard = findCard(key); + if (fcard == null) { + return dft; + } + + String val = fcard.getValue(); + if (val == null) { + return dft; + } + + if (val.equals("T")) { + return true; + } else if (val.equals("F")) { + return false; + } else { + return dft; + } + } + + /** Get the String value associated with the given key. + * + * @param key The header key. + * @return The associated value or null if not found or if the value is not a string. + */ + public String getStringValue(String key) { + + HeaderCard fcard = findCard(key); + if (fcard == null || !fcard.isStringValue()) { + return null; + } + + String val = fcard.getValue(); + boolean append = longStringsEnabled + && val != null && val.endsWith("&"); + iter.next(); // skip the primary card. + while (append) { + HeaderCard nxt = (HeaderCard) iter.next(); + if (nxt == null) { + append = false; + } else { + key = nxt.getKey(); + String comm = nxt.getComment(); + if (key == null || comm == null || !key.equals("CONTINUE")) { + append = false; + } else { + comm = continueString(comm); + if (comm != null) { + comm = comm.substring(1, comm.length() - 1); + val = val.substring(0, val.length() - 1) + comm; + append = comm.endsWith("&"); + } + } + } + } + + return val; + } + + /** Add a card image to the header. + * @param fcard The card to be added. + */ + public void addLine(HeaderCard fcard) { + + if (fcard != null) { + if (fcard.isKeyValuePair()) { + iter.add(fcard.getKey(), fcard); + } else { + iter.add(fcard); + } + } + } + + /** Add a card image to the header. + * @param card The card to be added. + * @exception HeaderCardException If the card is not valid. + */ + public void addLine(String card) + throws HeaderCardException { + addLine(new HeaderCard(card)); + } + + /** Create a header by reading the information from the input stream. + * @param dis The input stream to read the data from. + * @return null if there was a problem with the header; + * otherwise return the header read from the input stream. + */ + public static Header readHeader(ArrayDataInput dis) + throws TruncatedFileException, IOException { + Header myHeader = new Header(); + try { + myHeader.read(dis); + } catch (EOFException e) { + // An EOF exception is thrown only if the EOF was detected + // when reading the first card. In this case we want + // to return a null. + return null; + } + return myHeader; + } + + /** Read a stream for header data. + * @param dis The input stream to read the data from. + * @return null if there was a problem with the header; + * otherwise return the header read from the input stream. + */ + public void read(ArrayDataInput dis) + throws TruncatedFileException, IOException { + if (dis instanceof RandomAccess) { + fileOffset = FitsUtil.findOffset(dis); + } else { + fileOffset = -1; + } + + byte[] buffer = new byte[80]; + + boolean firstCard = true; + int count = 0; + + try { + while (true) { + + int len; + + int need = 80; + + try { + + while (need > 0) { + len = dis.read(buffer, 80 - need, need); + count += 1; + if (len == 0) { + throw new TruncatedFileException(); + } + need -= len; + } + } catch (EOFException e) { + + // Rethrow the EOF if we are at the beginning of the header, + // otherwise we have a FITS error. + // + if (firstCard && need == 80) { + throw e; + } + throw new TruncatedFileException(e.getMessage()); + } + + String cbuf = AsciiFuncs.asciiString(buffer); + HeaderCard fcard = new HeaderCard(cbuf); + + if (firstCard) { + + String key = fcard.getKey(); + + if (key == null || (!key.equals("SIMPLE") && !key.equals("XTENSION"))) { + throw new IOException("Not FITS format at " + fileOffset + ":" + cbuf); + } + firstCard = false; + } + + String key = fcard.getKey(); + if (key != null && cards.containsKey(key)) { + System.err.println("Warning: multiple occurrences of key:" + key); + } + + // We don't check the value here. If the user + // wants to be sure that long strings are disabled, + // they can call setLongStringsEnabled(false) after + // reading the header. + if (key.equals("LONGSTRN")) { + longStringsEnabled = true; + } + // save card + addLine(fcard); + if (cbuf.substring(0, 8).equals("END ")) { + break; // Out of reading the header. + } + } + + } catch (EOFException e) { + throw e; + + } catch (Exception e) { + if (!(e instanceof EOFException)) { + // For compatibility with Java V5 we just add in the error message + // rather than using using the cause mechanism. + // Probably should update this when we can ignore Java 5. + throw new IOException("Invalid FITS Header:"+ e); + } + } + if (fileOffset >= 0) { + oldSize = cards.size(); + input = dis; + } + + // Read to the end of the current FITS block. + // + try { + dis.skipBytes(FitsUtil.padding(count * 80)); + } catch (IOException e) { + throw new TruncatedFileException(e.getMessage()); + } + } + + /** Find the card associated with a given key. + * @param key The header key. + * @return null if the keyword could not be found; + * return the card image otherwise. + */ + public String findKey(String key) { + HeaderCard card = findCard(key); + if (card == null) { + return null; + } else { + return card.toString(); + } + } + + /** Replace the key with a new key. Typically this is used + * when deleting or inserting columns so that TFORMx -> TFORMx-1 + * @param oldKey The old header keyword. + * @param newKey the new header keyword. + * @return true if the card was replaced. + * @exception HeaderCardException If newKey is not a + * valid FITS keyword. + */ + boolean replaceKey(String oldKey, String newKey) + throws HeaderCardException { + + HeaderCard oldCard = findCard(oldKey); + if (oldCard == null) { + return false; + } + if (!cards.replaceKey(oldKey, newKey)) { + throw new HeaderCardException("Duplicate key in replace"); + } + + oldCard.setKey(newKey); + + return true; + } + + /** Write the current header (including any needed padding) to the + * output stream. + * @param dos The output stream to which the data is to be written. + * @exception FitsException if the header could not be written. + */ + public void write(ArrayDataOutput dos) throws FitsException { + + fileOffset = FitsUtil.findOffset(dos); + + // Ensure that all cards are in the proper order. + cards.sort(new HeaderOrder()); + checkBeginning(); + checkEnd(); + if (cards.size() <= 0) { + return; + } + + + Cursor iter = cards.iterator(0); + + try { + while (iter.hasNext()) { + HeaderCard card = (HeaderCard) iter.next(); + + byte[] b = AsciiFuncs.getBytes(card.toString()); + dos.write(b); + } + + FitsUtil.pad(dos, getNumberOfCards() * 80, (byte) ' '); + } catch (IOException e) { + throw new FitsException("IO Error writing header: " + e); + } + try { + dos.flush(); + } catch (IOException e) { + } + + } + + /** Rewrite the header. */ + public void rewrite() throws FitsException, IOException { + + ArrayDataOutput dos = (ArrayDataOutput) input; + + if (rewriteable()) { + FitsUtil.reposition(dos, fileOffset); + write(dos); + dos.flush(); + } else { + throw new FitsException("Invalid attempt to rewrite Header."); + } + } + + /** Reset the file pointer to the beginning of the header */ + public boolean reset() { + try { + FitsUtil.reposition(input, fileOffset); + return true; + } catch (Exception e) { + return false; + } + } + + /** Can the header be rewritten without rewriting the entire file? */ + public boolean rewriteable() { + + if (fileOffset >= 0 + && input instanceof ArrayDataOutput + && (cards.size() + 35) / 36 == (oldSize + 35) / 36) { + return true; + } else { + return false; + } + } + + /** Add or replace a key with the given boolean value and comment. + * @param key The header key. + * @param val The boolean value. + * @param comment A comment to append to the card. + * @exception HeaderCardException If the parameters cannot build a + * valid FITS card. + */ + public void addValue(String key, boolean val, String comment) + throws HeaderCardException { + removeCard(key); + iter.add(key, new HeaderCard(key, val, comment)); + } + + /** Add or replace a key with the given double value and comment. + * Note that float values will be promoted to doubles. + * @param key The header key. + * @param val The double value. + * @param comment A comment to append to the card. + * @exception HeaderCardException If the parameters cannot build a + * valid FITS card. + */ + public void addValue(String key, double val, String comment) + throws HeaderCardException { + removeCard(key); + iter.add(key, new HeaderCard(key, val, comment)); + } + + ; + + /** Add or replace a key with the given string value and comment. + * @param key The header key. + * @param val The string value. + * @param comment A comment to append to the card. + * @exception HeaderCardException If the parameters cannot build a + * valid FITS card. + */ + public void addValue(String key, String val, String comment) + throws HeaderCardException { + removeCard(key); + // Remember that quotes get doubled in the value... + if (longStringsEnabled && val.replace("'", "''").length() > 68) { + addLongString(key, val, comment); + } else { + iter.add(key, new HeaderCard(key, val, comment)); + } + } + + /** Add or replace a key with the given long value and comment. + * Note that int's will be promoted to long's. + * @param key The header key. + * @param val The long value. + * @param comment A comment to append to the card. + * @exception HeaderCardException If the parameters cannot build a + * valid FITS card. + */ + public void addValue(String key, long val, String comment) + throws HeaderCardException { + removeCard(key); + iter.add(key, new HeaderCard(key, val, comment)); + } + + private int getAdjustedLength(String in, int max) { + // Find the longest string that we can use when + // we accommodate needing to double quotes. + int size = 0; + int i; + for (i = 0; i < in.length() && size < max; i += 1) { + if (in.charAt(i) == '\'') { + size += 2; + if (size > max) { + break; // Jumped over the edge + } + } else { + size += 1; + } + } + return i; + } + + protected void addLongString(String key, String val, String comment) + throws HeaderCardException { + // We assume that we've made the test so that + // we need to write a long string. We need to + // double the quotes in the string value. addValue + // takes care of that for us, but we need to do it + // ourselves when we are extending into the comments. + // We also need to be careful that single quotes don't + // make the string too long and that we don't split + // in the middle of a quote. + int off = getAdjustedLength(val, 67); + String curr = val.substring(0, off) + '&'; + // No comment here since we're using as much of the card as we can + addValue(key, curr, null); + val = val.substring(off); + + while (val != null && val.length() > 0) { + off = getAdjustedLength(val, 67); + if (off < val.length()) { + curr = "'" + val.substring(0, off).replace("'", "''") + "&'"; + val = val.substring(off); + } else { + curr = "'" + val.replace("'", "''") + "' / " + comment; + val = null; + } + + iter.add(new HeaderCard("CONTINUE", null, curr)); + } + } + + /** Delete a key. + * @param key The header key. + */ + public void removeCard(String key) + throws HeaderCardException { + + if (cards.containsKey(key)) { + iter.setKey(key); + if (iter.hasNext()) { + HeaderCard hc = (HeaderCard) iter.next(); + String val = hc.getValue(); + boolean delExtensions = + longStringsEnabled && val != null && val.endsWith("&"); + iter.remove(); + while (delExtensions) { + hc = (HeaderCard) iter.next(); + if (hc == null) { + delExtensions = false; + } else { + if (hc.getKey().equals("CONTINUE")) { + String more = hc.getComment(); + more = continueString(more); + if (more != null) { + iter.remove(); + delExtensions = more.endsWith("&'"); + } else { + delExtensions = false; + } + } else { + delExtensions = false; + } + } + } + } + } + } + + /** Look for the continuation part of a COMMENT. + * The comment may also include a 'real' comment, e.g., + *
+     *  X = 'AB&'
+     *  CONTINUE 'CDEF' / ABC
+     *  
+ * Here we are looking for just the 'CDEF' part of the CONTINUE card. + */ + private String continueString(String input) { + if (input == null) { + return null; + } + + input = input.trim(); + if (input.length() < 2 || input.charAt(0) != '\'') { + return null; + } + + for (int i = 1; i < input.length(); i += 1) { + char c = input.charAt(i); + if (c == '\'') { + if (i < input.length() - 1 && input.charAt(i + 1) == c) { + // consecutive quotes -> escaped single quote + // Get rid of the extra quote. + input = input.substring(0, i) + input.substring(i + 1); + continue; // Check the next character. + } else { + // Found closing apostrophe + return input.substring(0, i + 1); + } + } + } + // Never found a closing apostrophe. + return null; + } + + /** Add a line to the header using the COMMENT style, i.e., no '=' + * in column 9. + * @param header The comment style header. + * @param value A string to follow the header. + * @exception HeaderCardException If the parameters cannot build a + * valid FITS card. + */ + public void insertCommentStyle(String header, String value) { + // Should just truncate strings, so we should never get + // an exception... + + try { + iter.add(new HeaderCard(header, null, value)); + } catch (HeaderCardException e) { + System.err.println("Impossible Exception for comment style:" + header + ":" + value); + } + } + + /** Add a COMMENT line. + * @param value The comment. + * @exception HeaderCardException If the parameter is not a + * valid FITS comment. + */ + public void insertComment(String value) + throws HeaderCardException { + insertCommentStyle("COMMENT", value); + } + + /** Add a HISTORY line. + * @param value The history record. + * @exception HeaderCardException If the parameter is not a + * valid FITS comment. + */ + public void insertHistory(String value) + throws HeaderCardException { + insertCommentStyle("HISTORY", value); + } + + /** Delete the card associated with the given key. + * Nothing occurs if the key is not found. + * + * @param key The header key. + */ + public void deleteKey(String key) { + + iter.setKey(key); + if (iter.hasNext()) { + iter.next(); + iter.remove(); + } + } + + /** Tests if the specified keyword is present in this table. + * @param key the keyword to be found. + * @return true if the specified keyword is present in this + * table; false otherwise. + */ + public final boolean containsKey(String key) { + return cards.containsKey(key); + } + + /** Create a header for a null image. + */ + void nullImage() { + + iter = iterator(); + try { + addValue("SIMPLE", true, "ntf::header:simple:2"); + addValue("BITPIX", 8, "ntf::header:bitpix:2"); + addValue("NAXIS", 0, "ntf::header:naxis:2"); + addValue("EXTEND", true, "ntf::header:extend:2"); + } catch (HeaderCardException e) { + } + } + + /** Set the SIMPLE keyword to the given value. + * @param val The boolean value -- Should be true for FITS data. + */ + public void setSimple(boolean val) { + deleteKey("SIMPLE"); + deleteKey("XTENSION"); + + // If we're flipping back to and from the primary header + // we need to add in the EXTEND keyword whenever we become + // a primary, because it's not permitted in the extensions + // (at least not where it needs to be in the primary array). + if (findCard("NAXIS") != null) { + int nax = getIntValue("NAXIS"); + + iter = iterator(); + + + if (findCard("NAXIS" + nax) != null) { + HeaderCard hc = (HeaderCard) iter.next(); + try { + removeCard("EXTEND"); + iter.add("EXTEND", new HeaderCard("EXTEND", true, "ntf::header:extend:1")); + } catch (Exception e) { // Ignore the exception + } + ; + } + } + + iter = iterator(); + try { + iter.add("SIMPLE", + new HeaderCard("SIMPLE", val, "ntf::header:simple:1")); + } catch (HeaderCardException e) { + System.err.println("Impossible exception at setSimple " + e); + } + } + + /** Set the XTENSION keyword to the given value. + * @param val The name of the extension. "IMAGE" and "BINTABLE" are supported. + */ + public void setXtension(String val) { + deleteKey("SIMPLE"); + deleteKey("XTENSION"); + deleteKey("EXTEND"); + iter = iterator(); + try { + iter.add("XTENSION", + new HeaderCard("XTENSION", val, "ntf::header:xtension:1")); + } catch (HeaderCardException e) { + System.err.println("Impossible exception at setXtension " + e); + } + } + + /** Set the BITPIX value for the header. + * @param val. The following values are permitted by FITS conventions: + *
    + *
  • 8 -- signed bytes data. Also used for tables. + *
  • 16 -- signed short data. + *
  • 32 -- signed int data. + *
  • 64 -- signed long data. + *
  • -32 -- IEEE 32 bit floating point numbers. + *
  • -64 -- IEEE 64 bit floating point numbers. + *
+ */ + public void setBitpix(int val) { + iter = iterator(); + iter.next(); + try { + iter.add("BITPIX", new HeaderCard("BITPIX", val, "ntf::header:bitpix:1")); + } catch (HeaderCardException e) { + System.err.println("Impossible exception at setBitpix " + e); + } + } + + /** Set the value of the NAXIS keyword + * @param val The dimensionality of the data. + */ + public void setNaxes(int val) { + iter.setKey("BITPIX"); + if (iter.hasNext()) { + iter.next(); + } + + try { + iter.add("NAXIS", new HeaderCard("NAXIS", val, "ntf::header:naxis:1")); + } catch (HeaderCardException e) { + System.err.println("Impossible exception at setNaxes " + e); + } + } + + /** Set the dimension for a given axis. + * @param axis The axis being set. + * @param dim The dimension + */ + public void setNaxis(int axis, int dim) { + + if (axis <= 0) { + return; + } + if (axis == 1) { + iter.setKey("NAXIS"); + } else if (axis > 1) { + iter.setKey("NAXIS" + (axis - 1)); + } + if (iter.hasNext()) { + iter.next(); + } + try { + iter.add("NAXIS" + axis, + new HeaderCard("NAXIS" + axis, dim, "ntf::header:naxisN:1")); + + } catch (HeaderCardException e) { + System.err.println("Impossible exception at setNaxis " + e); + } + } + + /** Ensure that the header begins with + * a valid set of keywords. Note that we + * do not check the values of these keywords. + */ + void checkBeginning() throws FitsException { + + iter = iterator(); + + if (!iter.hasNext()) { + throw new FitsException("Empty Header"); + } + HeaderCard card = (HeaderCard) iter.next(); + String key = card.getKey(); + if (!key.equals("SIMPLE") && !key.equals("XTENSION")) { + throw new FitsException("No SIMPLE or XTENSION at beginning of Header"); + } + boolean isTable = false; + boolean isExtension = false; + if (key.equals("XTENSION")) { + String value = card.getValue(); + if (value == null) { + throw new FitsException("Empty XTENSION keyword"); + } + + isExtension = true; + + if (value.equals("BINTABLE") || value.equals("A3DTABLE") + || value.equals("TABLE")) { + isTable = true; + } + } + + cardCheck("BITPIX"); + cardCheck("NAXIS"); + + int nax = getIntValue("NAXIS"); + iter.next(); + + for (int i = 1; i <= nax; i += 1) { + cardCheck("NAXIS" + i); + } + + if (isExtension) { + cardCheck("PCOUNT"); + cardCheck("GCOUNT"); + if (isTable) { + cardCheck("TFIELDS"); + } + } + // This does not check for the EXTEND keyword which + // if present in the primary array must immediately follow + // the NAXISn. + } + + /** Check if the given key is the next one available in + * the header. + */ + private void cardCheck(String key) throws FitsException { + + if (!iter.hasNext()) { + throw new FitsException("Header terminates before " + key); + } + HeaderCard card = (HeaderCard) iter.next(); + if (!card.getKey().equals(key)) { + throw new FitsException("Key " + key + " not found where expected." + + "Found " + card.getKey()); + } + } + + /** Ensure that the header has exactly one END keyword in + * the appropriate location. + */ + void checkEnd() { + + // Ensure we have an END card only at the end of the + // header. + // + iter = iterator(); + HeaderCard card; + + while (iter.hasNext()) { + card = (HeaderCard) iter.next(); + if (!card.isKeyValuePair() && card.getKey().equals("END")) { + iter.remove(); + } + } + try { + // End cannot have a comment + iter.add(new HeaderCard("END", null, null)); + } catch (HeaderCardException e) { + } + } + + /** Print the header to a given stream. + * @param ps the stream to which the card images are dumped. + */ + public void dumpHeader(PrintStream ps) { + iter = iterator(); + while (iter.hasNext()) { + ps.println(iter.next()); + } + } + + /***** Deprecated methods *******/ + /** Find the number of cards in the header + * @deprecated see numberOfCards(). The units + * of the size of the header may be unclear. + */ + public int size() { + return cards.size(); + } + + /** Get the n'th card image in the header + * @return the card image; return null if the n'th card + * does not exist. + * @deprecated An iterator should be used for sequential + * access to the header. + */ + public String getCard(int n) { + if (n >= 0 && n < cards.size()) { + iter = cards.iterator(n); + HeaderCard c = (HeaderCard) iter.next(); + return c.toString(); + } + return null; + } + + /** Get the n'th key in the header. + * @return the card image; return null if the n'th key + * does not exist. + * @deprecated An iterator should be used for sequential + * access to the header. + */ + public String getKey(int n) { + + String card = getCard(n); + if (card == null) { + return null; + } + + String key = card.substring(0, 8); + if (key.charAt(0) == ' ') { + return ""; + } + + + if (key.indexOf(' ') >= 1) { + key = key.substring(0, key.indexOf(' ')); + } + return key; + } + + /** Create a header which points to the + * given data object. + * @param o The data object to be described. + * @exception FitsException if the data was not valid for this header. + * @deprecated Use the appropriate Header constructor. + */ + public void pointToData(Data o) throws FitsException { + o.fillHeader(this); + } + + /** Find the end of a set of keywords describing a column or axis + * (or anything else terminated by an index. This routine leaves + * the header ready to add keywords after any existing keywords + * with the index specified. The user should specify a + * prefix to a keyword that is guaranteed to be present. + */ + Cursor positionAfterIndex(String prefix, int col) { + String colnum = "" + col; + + iter.setKey(prefix + colnum); + + if (iter.hasNext()) { + + // Bug fix (references to forward) here by Laurent Borges + boolean forward = false; + + String key; + while (iter.hasNext()) { + + key = ((HeaderCard) iter.next()).getKey().trim(); + if (key == null + || key.length() <= colnum.length() + || !key.substring(key.length() - colnum.length()).equals(colnum)) { + forward = true; + break; + } + } + if (forward) { + iter.prev(); // Gone one too far, so skip back an element. + } + } + return iter; + } + + /** Get the next card in the Header using the current iterator */ + public HeaderCard nextCard() { + if (iter == null) { + return null; + } + if (iter.hasNext()) { + return (HeaderCard) iter.next(); + } else { + return null; + } + } + + /** Move after the EXTEND keyword in images. + * Used in bug fix noted by V. Forchi + */ + void afterExtend() { + if (findCard("EXTEND") != null) { + nextCard(); + } + } +} diff --git a/src/nom/tam/fits/HeaderCard.java b/src/nom/tam/fits/HeaderCard.java new file mode 100644 index 0000000..0775c53 --- /dev/null +++ b/src/nom/tam/fits/HeaderCard.java @@ -0,0 +1,633 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes -- including + * this class. + */ +/** This class describes methods to access and manipulate the individual + * cards for a FITS Header. + */ +public class HeaderCard { + + /** The keyword part of the card (set to null if there's no keyword) */ + private String key; + /** The value part of the card (set to null if there's no value) */ + private String value; + /** The comment part of the card (set to null if there's no comment) */ + private String comment; + /** Does this card represent a nullable field. ? */ + private boolean nullable; + /** A flag indicating whether or not this is a string value */ + private boolean isString; + /** Maximum length of a FITS keyword field */ + public static final int MAX_KEYWORD_LENGTH = 8; + /** Maximum length of a FITS value field */ + public static final int MAX_VALUE_LENGTH = 70; + /** padding for building card images */ + private static String space80 = " "; + + /** Create a HeaderCard from its component parts + * @param key keyword (null for a comment) + * @param value value (null for a comment or keyword without an '=') + * @param comment comment + * @exception HeaderCardException for any invalid keyword + */ + public HeaderCard(String key, double value, String comment) + throws HeaderCardException { + this(key, dblString(value), comment); + isString = false; + } + + /** Create a HeaderCard from its component parts + * @param key keyword (null for a comment) + * @param value value (null for a comment or keyword without an '=') + * @param comment comment + * @exception HeaderCardException for any invalid keyword + */ + public HeaderCard(String key, boolean value, String comment) + throws HeaderCardException { + this(key, value ? "T" : "F", comment); + isString = false; + } + + /** Create a HeaderCard from its component parts + * @param key keyword (null for a comment) + * @param value value (null for a comment or keyword without an '=') + * @param comment comment + * @exception HeaderCardException for any invalid keyword + */ + public HeaderCard(String key, int value, String comment) + throws HeaderCardException { + this(key, String.valueOf(value), comment); + isString = false; + } + + /** Create a HeaderCard from its component parts + * @param key keyword (null for a comment) + * @param value value (null for a comment or keyword without an '=') + * @param comment comment + * @exception HeaderCardException for any invalid keyword + */ + public HeaderCard(String key, long value, String comment) + throws HeaderCardException { + this(key, String.valueOf(value), comment); + isString = false; + } + + /** Create a HeaderCard from its component parts + * @param key keyword (null for a comment) + * @param value value (null for a comment or keyword without an '=') + * @param comment comment + * @exception HeaderCardException for any invalid keyword or value + */ + public HeaderCard(String key, String value, String comment) + throws HeaderCardException { + this(key, value, comment, false); + } + + /** Create a comment style card. + * This constructor builds a card which has no value. + * This may be either a comment style card in which case the + * nullable field should be false, or a value field which + * has a null value, in which case the nullable field should be + * true. + * @param key The key for the comment or nullable field. + * @param comment The comment + * @param nullable Is this a nullable field or a comment-style card? + */ + public HeaderCard(String key, String comment, boolean nullable) + throws HeaderCardException { + this(key, null, comment, nullable); + } + + /** Create a string from a double making sure that it's + * not more than 20 characters long. + * Probably would be better if we had a way to override this + * since we can loose precision for some doubles. + */ + private static String dblString(double input) { + String value = String.valueOf(input); + if (value.length() > 20) { + value = new java.util.Formatter().format("%20.13G", input).out().toString(); + } + return value; + } + + /** Create a HeaderCard from its component parts + * @param key Keyword (null for a COMMENT) + * @param value Value + * @param comment Comment + * @param nullable Is this a nullable value card? + * @exception HeaderCardException for any invalid keyword or value + */ + public HeaderCard(String key, String value, String comment, boolean nullable) + throws HeaderCardException { + if (comment != null && comment.startsWith("ntf::")) { + String ckey = comment.substring(5); // Get rid of ntf:: prefix + comment = HeaderCommentsMap.getComment(ckey); + } + if (key == null && value != null) { + throw new HeaderCardException("Null keyword with non-null value"); + } + + if (key != null && key.length() > MAX_KEYWORD_LENGTH) { + if (!FitsFactory.getUseHierarch() + || !key.substring(0, 9).equals("HIERARCH.")) { + throw new HeaderCardException("Keyword too long"); + } + } + + if (value != null) { + value = value.replaceAll(" *$", ""); + + if (value.length() > MAX_VALUE_LENGTH) { + throw new HeaderCardException("Value too long"); + } + + if (value.startsWith("'")) { + if (value.charAt(value.length() - 1) != '\'') { + throw new HeaderCardException("Missing end quote in string value"); + } + + value = value.substring(1, value.length() - 1).trim(); + + } + } + + this.key = key; + this.value = value; + this.comment = comment; + this.nullable = nullable; + isString = true; + } + + /** Create a HeaderCard from a FITS card image + * @param card the 80 character card image + */ + public HeaderCard(String card) { + key = null; + value = null; + comment = null; + isString = false; + + if (card.length() > 80) { + card = card.substring(0, 80); + } + + if (FitsFactory.getUseHierarch() + && card.length() > 9 + && card.substring(0, 9).equals("HIERARCH ")) { + hierarchCard(card); + return; + } + + // We are going to assume that the value has no blanks in + // it unless it is enclosed in quotes. Also, we assume that + // a / terminates the string (except inside quotes) + + // treat short lines as special keywords + if (card.length() < 9) { + key = card; + return; + } + + // extract the key + key = card.substring(0, 8).trim(); + + // if it is an empty key, assume the remainder of the card is a comment + if (key.length() == 0) { + key = ""; + comment = card.substring(8); + return; + } + + // Non-key/value pair lines are treated as keyed comments + if (key.equals("COMMENT") || key.equals("HISTORY") + || !card.substring(8, 10).equals("= ")) { + comment = card.substring(8).trim(); + return; + } + + // extract the value/comment part of the string + String valueAndComment = card.substring(10).trim(); + + // If there is no value/comment part, we are done. + if (valueAndComment.length() == 0) { + value = ""; + return; + } + + int vend = -1; + boolean quote = false; + + // If we have a ' then find the matching '. + if (valueAndComment.charAt(0) == '\'') { + + int offset = 1; + while (offset < valueAndComment.length()) { + + // look for next single-quote character + vend = valueAndComment.indexOf("'", offset); + + // if the quote character is the last character on the line... + if (vend == valueAndComment.length() - 1) { + break; + } + + // if we did not find a matching single-quote... + if (vend == -1) { + // pretend this is a comment card + key = null; + comment = card; + return; + } + + // if this is not an escaped single-quote, we are done + if (valueAndComment.charAt(vend + 1) != '\'') { + break; + } + + // skip past escaped single-quote + offset = vend + 2; + } + + // break apart character string + value = valueAndComment.substring(1, vend).trim(); + value = value.replace("''", "'"); + + + if (vend + 1 >= valueAndComment.length()) { + comment = null; + } else { + + comment = valueAndComment.substring(vend + 1).trim(); + if (comment.charAt(0) == '/') { + if (comment.length() > 1) { + comment = comment.substring(1); + } else { + comment = ""; + } + } + + if (comment.length() == 0) { + comment = null; + } + + } + isString = true; + + + } else { + + // look for a / to terminate the field. + int slashLoc = valueAndComment.indexOf('/'); + if (slashLoc != -1) { + comment = valueAndComment.substring(slashLoc + 1).trim(); + value = valueAndComment.substring(0, slashLoc).trim(); + } else { + value = valueAndComment; + } + } + } + + /** Process HIERARCH style cards... + * HIERARCH LEV1 LEV2 ... = value / comment + * The keyword for the card will be "HIERARCH.LEV1.LEV2..." + * A '/' is assumed to start a comment. + */ + private void hierarchCard(String card) { + + String name = ""; + String token = null; + String separator = ""; + int[] tokLimits; + int posit = 0; + int commStart = -1; + + // First get the hierarchy levels + while ((tokLimits = getToken(card, posit)) != null) { + token = card.substring(tokLimits[0], tokLimits[1]); + if (!token.equals("=")) { + name += separator + token; + separator = "."; + } else { + tokLimits = getToken(card, tokLimits[1]); + if (tokLimits != null) { + token = card.substring(tokLimits[0], tokLimits[1]); + } else { + key = name; + value = null; + comment = null; + return; + } + break; + } + posit = tokLimits[1]; + } + key = name; + + + // At the end? + if (tokLimits == null) { + value = null; + comment = null; + isString = false; + return; + } + + // Really should consolidate the two instances + // of this test in this class! + if (token.charAt(0) == '\'') { + // Find the next undoubled quote... + isString = true; + if (token.length() > 1 && token.charAt(1) == '\'' + && (token.length() == 2 || token.charAt(2) != '\'')) { + value = ""; + commStart = tokLimits[0] + 2; + } else if (card.length() < tokLimits[0] + 2) { + value = null; + comment = null; + isString = false; + return; + } else { + int i; + for (i = tokLimits[0] + 1; i < card.length(); i += 1) { + if (card.charAt(i) == '\'') { + if (i == card.length() - 1) { + value = card.substring(tokLimits[0] + 1, i); + commStart = i + 1; + break; + } else if (card.charAt(i + 1) == '\'') { + // Doubled quotes. + i += 1; + continue; + } else { + value = card.substring(tokLimits[0] + 1, i); + commStart = i + 1; + break; + } + } + } + } + if (commStart < 0) { + value = null; + comment = null; + isString = false; + return; + } + for (int i = commStart; i < card.length(); i += 1) { + if (card.charAt(i) == '/') { + comment = card.substring(i + 1).trim(); + break; + } else if (card.charAt(i) != ' ') { + comment = null; + break; + } + } + } else { + isString = false; + int sl = token.indexOf('/'); + if (sl == 0) { + value = null; + comment = card.substring(tokLimits[0] + 1); + } else if (sl > 0) { + value = token.substring(0, sl); + comment = card.substring(tokLimits[0] + sl + 1); + } else { + value = token; + + for (int i = tokLimits[1]; i < card.length(); i += 1) { + if (card.charAt(i) == '/') { + comment = card.substring(i + 1).trim(); + break; + } else if (card.charAt(i) != ' ') { + comment = null; + break; + } + } + } + } + } + + /** Get the next token. Can't use StringTokenizer + * since we sometimes need to know the position within + * the string. + */ + private int[] getToken(String card, int posit) { + + int i; + for (i = posit; i < card.length(); i += 1) { + if (card.charAt(i) != ' ') { + break; + } + } + + if (i >= card.length()) { + return null; + } + + if (card.charAt(i) == '=') { + return new int[]{i, i + 1}; + } + + int j; + for (j = i + 1; j < card.length(); j += 1) { + if (card.charAt(j) == ' ' || card.charAt(j) == '=') { + break; + } + } + return new int[]{i, j}; + } + + /** Does this card contain a string value? + */ + public boolean isStringValue() { + return isString; + } + + /** Is this a key/value card? + */ + public boolean isKeyValuePair() { + return (key != null && value != null); + } + + /** Set the key. + */ + void setKey(String newKey) { + key = newKey; + } + + /** Return the keyword from this card + */ + public String getKey() { + return key; + } + + /** Return the value from this card + */ + public String getValue() { + return value; + } + + /** Set the value for this card. + */ + public void setValue(String update) { + value = update; + } + + /** Return the comment from this card + */ + public String getComment() { + return comment; + } + + /** Return the 80 character card image + */ + public String toString() { + StringBuffer buf = new StringBuffer(80); + + // start with the keyword, if there is one + if (key != null) { + if (key.length() > 9 && key.substring(0, 9).equals("HIERARCH.")) { + return hierarchToString(); + } + buf.append(key); + if (key.length() < 8) { + buf.append(space80.substring(0, 8 - buf.length())); + } + } + + if (value != null || nullable) { + buf.append("= "); + + if (value != null) { + + if (isString) { + // left justify the string inside the quotes + buf.append('\''); + buf.append(value.replace("'", "''")); + if (buf.length() < 19) { + + buf.append(space80.substring(0, 19 - buf.length())); + } + buf.append('\''); + // Now add space to the comment area starting at column 40 + if (buf.length() < 30) { + buf.append(space80.substring(0, 30 - buf.length())); + } + + } else { + + int offset = buf.length(); + if (value.length() < 20) { + buf.append(space80.substring(0, 20 - value.length())); + } + + buf.append(value); + + } + } else { + // Pad out a null value. + buf.append(space80.substring(0, 20)); + } + + // if there is a comment, add a comment delimiter + if (comment != null) { + buf.append(" / "); + } + + } else if (comment != null && comment.startsWith("= ")) { + buf.append(" "); + } + + // finally, add any comment + if (comment != null) { + buf.append(comment); + } + + // make sure the final string is exactly 80 characters long + if (buf.length() > 80) { + buf.setLength(80); + + } else { + + if (buf.length() < 80) { + buf.append(space80.substring(0, 80 - buf.length())); + } + } + + return buf.toString(); + } + + private String hierarchToString() { + + + StringBuffer b = new StringBuffer(80); + int p = 0; + String space = ""; + while (p < key.length()) { + int q = key.indexOf('.', p); + if (q < 0) { + b.append(space + key.substring(p)); + break; + } else { + b.append(space + key.substring(p, q)); + } + space = " "; + p = q + 1; + } + + if (value != null || nullable) { + b.append("= "); + + if (value != null) { + // Try to align values + int avail = 80 - (b.length() + value.length()); + + if (isString) { + avail -= 2; + } + if (comment != null) { + avail -= 3 + comment.length(); + } + + if (avail > 0 && b.length() < 29) { + b.append(space80.substring(0, Math.min(avail, 29 - b.length()))); + } + + if (isString) { + b.append('\''); + } else if (avail > 0 && value.length() < 10) { + b.append(space80.substring(0, Math.min(avail, 10 - value.length()))); + } + b.append(value); + if (isString) { + b.append('\''); + } + } else if (b.length() < 30) { + + // Pad out a null value + b.append(space80.substring(0, 30 - b.length())); + } + } + + + if (comment != null) { + b.append(" / " + comment); + } + if (b.length() < 80) { + b.append(space80.substring(0, 80 - b.length())); + } + String card = new String(b); + if (card.length() > 80) { + card = card.substring(0, 80); + } + return card; + } +} diff --git a/src/nom/tam/fits/HeaderCardException.java b/src/nom/tam/fits/HeaderCardException.java new file mode 100644 index 0000000..77c018b --- /dev/null +++ b/src/nom/tam/fits/HeaderCardException.java @@ -0,0 +1,25 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ + +/* This class was contributed by David Glowacki */ +public class HeaderCardException + extends FitsException { + + public HeaderCardException() { + super(); + } + + public HeaderCardException(String s) { + super(s); + } +} + diff --git a/src/nom/tam/fits/HeaderCommentsMap.java b/src/nom/tam/fits/HeaderCommentsMap.java new file mode 100644 index 0000000..d8cb599 --- /dev/null +++ b/src/nom/tam/fits/HeaderCommentsMap.java @@ -0,0 +1,87 @@ +/* + * This class provides a modifiable map in which the comment fields for FITS + * header keywords + * produced by this library are set. The map is a simple String -> String + * map where the key Strings are normally class:keyword:id where class is + * the class name where the keyword is set, keyword is the keyword set and id + * is an integer used to distinguish multiple instances. + * + * Most users need not worry about this class, but users who wish to customize + * the appearance of FITS files may update the map. The code itself is likely + * to be needed to understand which values in the map must be modified. + * + * Note that the Header writing utilities look for the prefix ntf:: in comments + * and if this is found, the comment is replaced by looking in this map for + * a key given by the remainder of the original comment. + */ + +package nom.tam.fits; + +import java.util.HashMap; +import java.util.Map; + +public class HeaderCommentsMap { + + private static Map commentMap = new HashMap(); + static { + commentMap.put("header:extend:1", "Extensions are permitted"); + commentMap.put("header:simple:1", "Java FITS: "+ new java.util.Date()); + commentMap.put("header:xtension:1", "Java FITS: "+ new java.util.Date()); + commentMap.put("header:naxis:1", "Dimensionality"); + commentMap.put("header:extend:2", "Extensions are permitted"); + commentMap.put("asciitable:pcount:1", "No group data"); + commentMap.put("asciitable:gcount:1", "One group"); + commentMap.put("asciitable:tfields:1", "Number of fields in table"); + commentMap.put("asciitable:tbcolN:1", "Column offset"); + commentMap.put("asciitable:naxis1:1", "Size of row in bytes"); + commentMap.put("undefineddata:naxis1:1","Number of Bytes"); + commentMap.put("undefineddata:extend:1","Extensions are permitted"); + commentMap.put("binarytablehdu:pcount:1", "Includes heap"); + commentMap.put("binarytable:naxis1:1", "Bytes per row"); + commentMap.put("fits:checksum:1", "as of " + FitsDate.getFitsDateString()); + commentMap.put("basichdu:extend:1", "Allow extensions"); + commentMap.put("basichdu:gcount:1", "Required value"); + commentMap.put("basichdu:pcount:1", "Required value"); + commentMap.put("imagedata:extend:1", "Extension permitted"); + commentMap.put("imagedata:pcount:1", "No extra parameters"); + commentMap.put("imagedata:gcount:1", "One group"); + commentMap.put("tablehdu:tfields:1", "Number of table fields"); + /* Null entries: + * header:bitpix:1 + * header:simple:2 + * header:bitpix:2 + * header:naxisN:1 + * header:naxis:2 + * undefineddata:pcount:1 + * undefineddata:gcount:1 + * randomgroupsdata:naxis1:1 + * randomgroupsdata:naxisN:1 + * randomgroupsdata:groups:1 + * randomgroupsdata:gcount:1 + * randomgroupsdata:pcount:1 + * binarytablehdu:theap:1 + * binarytablehdu:tdimN:1 + * asciitable:tformN:1 + * asciitablehdu:tnullN:1 + * asciitablehdu:tfields:1 + * binarytable:pcount:1 + * binarytable:gcount:1 + * binarytable:tfields:1 + * binarytable:tformN:1 + * binarytable:tdimN:1 + * tablehdu:naxis2:1 + */ + } + + public static String getComment(String key) { + return commentMap.get(key); + } + + public static void updateComment(String key, String comment) { + commentMap.put(key, comment); + } + + public static void deleteComment(String key) { + commentMap.remove(key); + } +} diff --git a/src/nom/tam/fits/HeaderOrder.java b/src/nom/tam/fits/HeaderOrder.java new file mode 100644 index 0000000..4a46cd5 --- /dev/null +++ b/src/nom/tam/fits/HeaderOrder.java @@ -0,0 +1,150 @@ +package nom.tam.fits; + +/** This class implements a comparator which ensures + * that FITS keywords are written out in a proper order. + */ +public class HeaderOrder implements java.util.Comparator { + + /** Can two cards be exchanged when being written out? */ + public boolean equals(Object a, Object b) { + return compare(a, b) == 0; + } + + /** Which order should the cards indexed by these keys + * be written out? This method assumes that the + * arguments are either the FITS Header keywords as + * strings, and some other type (or null) for comment + * style keywords. + * + * @return -1 if the first argument should be written first
+ * 1 if the second argument should be written first
+ * 0 if either is legal. + */ + public int compare(Object a, Object b) { + + String c1, c2; + + if (a != null && a instanceof String) { + c1 = (String) a; + } else { + c1 = " "; + } + + if (b != null && b instanceof String) { + c2 = (String) b; + } else { + c2 = " "; + } + + + // Equals are equal + if (c1.equals(c2)) { + return 0; + } + + // Now search in the order in which cards must appear + // in the header. + + if (c1.equals("SIMPLE") || c1.equals("XTENSION")) { + return -1; + } + if (c2.equals("SIMPLE") || c2.equals("XTENSION")) { + return 1; + } + + if (c1.equals("BITPIX")) { + return -1; + } + if (c2.equals("BITPIX")) { + return 1; + } + + if (c1.equals("NAXIS")) { + return -1; + } + if (c2.equals("NAXIS")) { + return 1; + } + + // Check the NAXISn cards. These must + // be in axis order. + + if (naxisN(c1) > 0) { + if (naxisN(c2) > 0) { + if (naxisN(c1) < naxisN(c2)) { + return -1; + } else { + return 1; + } + } + return -1; + } + + if (naxisN(c2) > 0) { + return 1; + } + + if (c1.equals("PCOUNT")) { + return -1; + } + if (c2.equals("PCOUNT")) { + return 1; + } + + if (c1.equals("GCOUNT")) { + return -1; + } + if (c2.equals("GCOUNT")) { + return 1; + } + + if (c1.equals("TFIELDS")) { + return -1; + } + if (c2.equals("TFIELDS")) { + return 1; + } + + // In principal this only needs to be in the first 36 cards, + // but we put it here since it's convenient. BLOCKED is + // deprecated currently. + if (c1.equals("BLOCKED")) { + return -1; + } + if (c2.equals("BLOCKED")) { + return 1; + } + + // Note that this must be at the end, so the + // values returned are inverted. + if (c1.equals("END")) { + return 1; + } + if (c2.equals("END")) { + return -1; + } + + // All other cards can be in any order. + return 0; + } + + /** Find the index for NAXISn keywords */ + private int naxisN(String key) { + + if (key.length() > 5 && key.substring(0, 5).equals("NAXIS")) { + for (int i = 5; i < key.length(); i += 1) { + + boolean number = true; + char c = key.charAt(i); + if ('0' > c || c > '9') { + number = false; + break; + } + if (number) { + return Integer.parseInt(key.substring(5)); + } + } + } + return -1; + } +} diff --git a/src/nom/tam/fits/ImageData.java b/src/nom/tam/fits/ImageData.java new file mode 100644 index 0000000..611ac51 --- /dev/null +++ b/src/nom/tam/fits/ImageData.java @@ -0,0 +1,341 @@ +package nom.tam.fits; + +import java.lang.reflect.Array; +import nom.tam.image.ImageTiler; +import nom.tam.util.*; +import java.io.*; + + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +/** This class instantiates FITS primary HDU and IMAGE extension data. + * Essentially these data are a primitive multi-dimensional array. + *

+ * Starting in version 0.9 of the FITS library, this routine + * allows users to defer the reading of images if the FITS + * data is being read from a file. An ImageTiler object is + * supplied which can return an arbitrary subset of the image + * as a one dimensional array -- suitable for manipulation by + * standard Java libraries. A call to the getData() method + * will still return a multi-dimensional array, but the + * image data will not be read until the user explicitly requests. + * it. + */ +public class ImageData extends Data { + + /** The size of the data */ + long byteSize; + /** The actual array of data. This + * is normally a multi-dimensional primitive array. + * It may be null until the getData() routine is + * invoked, or it may be filled by during the read + * call when a non-random access device is used. + */ + Object dataArray; + + /** This class describes an array */ + protected class ArrayDesc { + + int[] dims; + Class type; + + ArrayDesc(int[] dims, Class type) { + this.dims = dims; + this.type = type; + } + } + /** A description of what the data should look like */ + ArrayDesc dataDescription; + + /** This inner class allows the ImageTiler + * to see if the user has read in the data. + */ + protected class ImageDataTiler extends nom.tam.image.ImageTiler { + + ImageDataTiler(RandomAccess o, long offset, ArrayDesc d) { + super(o, offset, d.dims, d.type); + } + + public Object getMemoryImage() { + return dataArray; + } + } + /** The image tiler associated with this image. */ + private ImageTiler tiler; + + /** Create an array from a header description. + * This is typically how data will be created when reading + * FITS data from a file where the header is read first. + * This creates an empty array. + * @param h header to be used as a template. + * @exception FitsException if there was a problem with the header description. + */ + public ImageData(Header h) throws FitsException { + + dataDescription = parseHeader(h); + } + + protected ArrayDesc parseHeader(Header h) throws FitsException { + + int bitpix; + int type; + int ndim; + int[] dims; + + int i; + + Object dataArray; + + Class baseClass; + + + int gCount = h.getIntValue("GCOUNT", 1); + int pCount = h.getIntValue("PCOUNT", 0); + if (gCount > 1 || pCount != 0) { + throw new FitsException("Group data treated as images"); + } + + bitpix = h.getIntValue("BITPIX", 0); + + if (bitpix == 8) { + baseClass = Byte.TYPE; + } else if (bitpix == 16) { + baseClass = Short.TYPE; + } else if (bitpix == 32) { + baseClass = Integer.TYPE; + } else if (bitpix == 64) { + baseClass = Long.TYPE; + } else if (bitpix == -32) { + baseClass = Float.TYPE; + } else if (bitpix == -64) { + baseClass = Double.TYPE; + } else { + throw new FitsException("Invalid BITPIX:" + bitpix); + } + + ndim = h.getIntValue("NAXIS", 0); + dims = new int[ndim]; + + + // Note that we have to invert the order of the axes + // for the FITS file to get the order in the array we + // are generating. + + byteSize = 1; + for (i = 0; i < ndim; i += 1) { + int cdim = h.getIntValue("NAXIS" + (i + 1), 0); + if (cdim < 0) { + throw new FitsException("Invalid array dimension:" + cdim); + } + byteSize *= cdim; + dims[ndim - i - 1] = cdim; + } + byteSize *= Math.abs(bitpix) / 8; + if (ndim == 0) { + byteSize = 0; + } + return new ArrayDesc(dims, baseClass); + } + + /** Create the equivalent of a null data element. + */ + public ImageData() { + dataArray = new byte[0]; + byteSize = 0; + } + + /** Create an ImageData object using the specified object to + * initialize the data array. + * @param x The initial data array. This should be a primitive + * array but this is not checked currently. + */ + public ImageData(Object x) { + dataArray = x; + byteSize = ArrayFuncs.computeLSize(x); + } + + /** Fill header with keywords that describe + * image data. + * @param head The FITS header + * @exception FitsException if the object does not contain + * valid image data. + */ + protected void fillHeader(Header head) throws FitsException { + + + if (dataArray == null) { + head.nullImage(); + return; + } + + String classname = dataArray.getClass().getName(); + + int[] dimens = ArrayFuncs.getDimensions(dataArray); + + if (dimens == null || dimens.length == 0) { + throw new FitsException("Image data object not array"); + } + + + int bitpix; + switch (classname.charAt(dimens.length)) { + case 'B': + bitpix = 8; + break; + case 'S': + bitpix = 16; + break; + case 'I': + bitpix = 32; + break; + case 'J': + bitpix = 64; + break; + case 'F': + bitpix = -32; + break; + case 'D': + bitpix = -64; + break; + default: + throw new FitsException("Invalid Object Type for FITS data:" + + classname.charAt(dimens.length)); + } + + // if this is neither a primary header nor an image extension, + // make it a primary header + head.setSimple(true); + head.setBitpix(bitpix); + head.setNaxes(dimens.length); + + for (int i = 1; i <= dimens.length; i += 1) { + if (dimens[i - 1] == -1) { + throw new FitsException("Unfilled array for dimension: " + i); + } + head.setNaxis(i, dimens[dimens.length - i]); + } + head.addValue("EXTEND", true,"ntf::imagedata:extend:1"); // Just in case! + head.addValue("PCOUNT", 0, "ntf::imagedata:pcount:1"); + head.addValue("GCOUNT", 1, "ntf::imagedata:gcount:1"); + + } + + public void read(ArrayDataInput i) throws FitsException { + + // Don't need to read null data (noted by Jens Knudstrup) + if (byteSize == 0) { + return; + } + setFileOffset(i); + + + if (i instanceof RandomAccess) { + tiler = new ImageDataTiler((RandomAccess) i, + ((RandomAccess) i).getFilePointer(), + dataDescription); + try { + // Handle long skips. + i.skipBytes(byteSize); + } catch (IOException e) { + throw new FitsException("Unable to skip over image:" + e); + } + + } else { + dataArray = ArrayFuncs.newInstance(dataDescription.type, + dataDescription.dims); + try { + i.readLArray(dataArray); + } catch (IOException e) { + throw new FitsException("Unable to read image data:" + e); + } + + tiler = new ImageDataTiler(null, 0, dataDescription); + } + + int pad = FitsUtil.padding(getTrueSize()); + try { + i.skipBytes(pad); + } catch (EOFException e) { + throw new PaddingException("Error skipping padding after image", this); + } catch (IOException e) { + throw new FitsException("Error skipping padding after image"); + } + } + + public void write(ArrayDataOutput o) throws FitsException { + + // Don't need to write null data (noted by Jens Knudstrup) + if (byteSize == 0) { + return; + } + + if (dataArray == null) { + if (tiler != null) { + + // Need to read in the whole image first. + try { + dataArray = tiler.getCompleteImage(); + } catch (IOException e) { + throw new FitsException("Error attempting to fill image"); + } + + } else if (dataArray == null && dataDescription != null) { + // Need to create an array to match a specified header. + dataArray = ArrayFuncs.newInstance(dataDescription.type, + dataDescription.dims); + + } else { + // This image isn't ready to be written! + throw new FitsException("Null image data"); + } + } + + try { + o.writeArray(dataArray); + } catch (IOException e) { + throw new FitsException("IO Error on image write" + e); + } + + FitsUtil.pad(o, getTrueSize()); + } + + /** Get the size in bytes of the data */ + protected long getTrueSize() { + return byteSize; + } + + /** Return the actual data. + * Note that this may return a null when + * the data is not readable. It might be better + * to throw a FitsException, but this is + * a very commonly called method and we prefered + * not to change how users must invoke it. + */ + public Object getData() { + + if (dataArray == null && tiler != null) { + try { + dataArray = tiler.getCompleteImage(); + } catch (Exception e) { + return null; + } + } + + return dataArray; + } + + void setTiler(ImageTiler tiler) { + this.tiler = tiler; + } + + public ImageTiler getTiler() { + return tiler; + } +} diff --git a/src/nom/tam/fits/ImageHDU.java b/src/nom/tam/fits/ImageHDU.java new file mode 100644 index 0000000..eb21e70 --- /dev/null +++ b/src/nom/tam/fits/ImageHDU.java @@ -0,0 +1,166 @@ +package nom.tam.fits; +/* Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ + +import nom.tam.util.ArrayFuncs; +import nom.tam.util.BufferedDataInputStream; +import nom.tam.image.ImageTiler; + +/** FITS image header/data unit */ +public class ImageHDU + extends BasicHDU { + + /** Build an image HDU using the supplied data. + * @param obj the data used to build the image. + * @exception FitsException if there was a problem with the data. + */ + public ImageHDU(Header h, Data d) + throws FitsException { + myData = d; + myHeader = h; + + } + + /** Indicate that Images can appear at the beginning of a FITS dataset */ + protected boolean canBePrimary() { + return true; + } + + /** Change the Image from/to primary */ + protected void setPrimaryHDU(boolean status) { + + try { + super.setPrimaryHDU(status); + } catch (FitsException e) { + System.err.println("Impossible exception in ImageData"); + } + + if (status) { + myHeader.setSimple(true); + } else { + myHeader.setXtension("IMAGE"); + } + } + + /** Check that this HDU has a valid header for this type. + * @return true if this HDU has a valid header. + */ + public static boolean isHeader(Header hdr) { + boolean found = false; + found = hdr.getBooleanValue("SIMPLE"); + if (!found) { + String s = hdr.getStringValue("XTENSION"); + if (s != null) { + if (s.trim().equals("IMAGE") || s.trim().equals("IUEIMAGE")) { + found = true; + } + } + } + if (!found) { + return false; + } + return !hdr.getBooleanValue("GROUPS"); + } + + /** Check if this object can be described as a FITS image. + * @param o The Object being tested. + */ + public static boolean isData(Object o) { + String s = o.getClass().getName(); + + int i; + for (i = 0; i < s.length(); i += 1) { + if (s.charAt(i) != '[') { + break; + } + } + + // Allow all non-boolean/Object arrays. + // This does not check the rectangularity of the array though. + if (i <= 0 || s.charAt(i) == 'L' || s.charAt(i) == 'Z') { + return false; + } else { + return true; + } + } + + /** Create a Data object to correspond to the header description. + * @return An unfilled Data object which can be used to read + * in the data for this HDU. + * @exception FitsException if the image extension could not be created. + */ + public Data manufactureData() + throws FitsException { + return manufactureData(myHeader); + } + + public static Data manufactureData(Header hdr) + throws FitsException { + return new ImageData(hdr); + } + + /** Create a header that describes the given + * image data. + * @param o The image to be described. + * @exception FitsException if the object does not contain + * valid image data. + */ + public static Header manufactureHeader(Data d) + throws FitsException { + + if (d == null) { + return null; + } + + Header h = new Header(); + d.fillHeader(h); + + return h; + } + + /** Encapsulate an object as an ImageHDU. */ + public static Data encapsulate(Object o) throws FitsException { + return new ImageData(o); + } + + public ImageTiler getTiler() { + return ((ImageData) myData).getTiler(); + } + + /** Print out some information about this HDU. + */ + public void info() { + if (isHeader(myHeader)) { + System.out.println(" Image"); + } else { + System.out.println(" Image (bad header)"); + } + + System.out.println(" Header Information:"); + System.out.println(" BITPIX=" + myHeader.getIntValue("BITPIX", -1)); + int naxis = myHeader.getIntValue("NAXIS", -1); + System.out.println(" NAXIS=" + naxis); + for (int i = 1; i <= naxis; i += 1) { + System.out.println(" NAXIS" + i + "=" + + myHeader.getIntValue("NAXIS" + i, -1)); + } + + System.out.println(" Data information:"); + try { + if (myData.getData() == null) { + System.out.println(" No Data"); + } else { + System.out.println(" " + + ArrayFuncs.arrayDescription(myData.getData())); + } + } catch (Exception e) { + System.out.println(" Unable to get data"); + } + } +} diff --git a/src/nom/tam/fits/Laurent_changes.txt b/src/nom/tam/fits/Laurent_changes.txt new file mode 100644 index 0000000..bb22f5b --- /dev/null +++ b/src/nom/tam/fits/Laurent_changes.txt @@ -0,0 +1,39 @@ +In BinaryTable: + + -- For ComplexData has bSize *= 2, size*=2. + Not sure this is right. Seems to be handled by adding dimension... + + -- getTformType (and getTformLength) methods + Changed name to ...TFORM... and made accessible to + other classes by giving it package level access. + + +In FitsFactory + -- Added setCheckAsciiStrings flag (which defaults to false). + If set then when a warning will be noted if invalid ASCII characters + are found, but the program will not fail. + + -- Added ASCII charset variable. + +In HeaderCard, FitsUtil, nom.tam.util.ByteParser + -- Conversions of bytes to strings now use the FitsFactory.ASCII charset. + + +In Header + -- Fixed bug with positioning of header card in positionAfterIndex + +Changes to changes + +In BinaryTable: didn't change getTFORMType to protected but to package +level access, since that is actual what we are using. + +Did not make classes final. I can easily conceive of users wishing to extend +classes like BinaryTable to customize to their own needs. + +Did not incorporate the small formatting changes. Many of them (including +the spacing in expressions would probably be a good idea), but I've become +very wary of using the ++ and -- operators (see my contribution in the thread at +http://groups.google.com/group/comp.lang.java.programmer/browse_frm/thread/357b4587c4b36352/dcb152343ace64f5?lnk=gst&q=mcglynn#dcb152343ace64f5 +for some of the motivation). + +Did not incorporate changes to packages (not that I expected to). diff --git a/src/nom/tam/fits/PaddingException.java b/src/nom/tam/fits/PaddingException.java new file mode 100644 index 0000000..c174b77 --- /dev/null +++ b/src/nom/tam/fits/PaddingException.java @@ -0,0 +1,54 @@ +package nom.tam.fits; + +/** + * This exception is thrown if an error is found + * reading the padding following a valid FITS HDU. + * This padding is required by the FITS standard, but + * some FITS writes forego writing it. To access such data + * users can use something like: + * + * + * Fits f = new Fits("somefile"); + * try { + * f.read(); + * } catch (PaddingException e) { + * f.addHDU(e.getHDU()); + * } + * + * to ensure that a truncated HDU is included in the FITS object. + * Generally the FITS file have already added any HDUs prior + * to the truncatd one. + */ +public class PaddingException extends FitsException { + + /** The HDU where the error happened. + */ + private BasicHDU truncatedHDU; + + /** + * When the error is thrown, the data object being + * read must be supplied. We initially create a dummy + * header for this. If someone is reading the entire + * HDU, then they can trap the exception and set the header + * to the appropriate value. + */ + public PaddingException(Data datum) throws FitsException { + truncatedHDU = FitsFactory.HDUFactory(datum.getKernel()); + // We want to use the original Data object... so + truncatedHDU = FitsFactory.HDUFactory(truncatedHDU.getHeader(), datum); + } + + public PaddingException(String msg, Data datum) throws FitsException { + super(msg); + truncatedHDU = FitsFactory.HDUFactory(datum.getKernel()); + truncatedHDU = FitsFactory.HDUFactory(truncatedHDU.getHeader(), datum); + } + + void updateHeader(Header hdr) throws FitsException { + truncatedHDU = FitsFactory.HDUFactory(hdr, truncatedHDU.getData()); + } + + public BasicHDU getTruncatedHDU() { + return truncatedHDU; + } +} diff --git a/src/nom/tam/fits/RandomGroupsData.java b/src/nom/tam/fits/RandomGroupsData.java new file mode 100644 index 0000000..ae6e9e1 --- /dev/null +++ b/src/nom/tam/fits/RandomGroupsData.java @@ -0,0 +1,143 @@ +package nom.tam.fits; +/* Copyright: Thomas McGlynn 1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ + +import nom.tam.util.*; +import java.io.IOException; +import java.io.EOFException; + +/** This class instantiates FITS Random Groups data. + * Random groups are instantiated as a two-dimensional + * array of objects. The first dimension of the array + * is the number of groups. The second dimension is 2. + * The first object in every row is a one dimensional + * parameter array. The second element is the n-dimensional + * data array. + */ +public class RandomGroupsData extends Data { + + private Object[][] dataArray; + + /** Create the equivalent of a null data element. + */ + public RandomGroupsData() { + dataArray = new Object[0][]; + } + + /** Create a RandomGroupsData object using the specified object to + * initialize the data array. + * @param x The initial data array. This should a two-d + * array of objects as described above. + */ + public RandomGroupsData(Object[][] x) { + dataArray = x; + } + + /** Get the size of the actual data element. */ + protected long getTrueSize() { + + if (dataArray != null && dataArray.length > 0) { + return (ArrayFuncs.computeLSize(dataArray[0][0]) + + ArrayFuncs.computeLSize(dataArray[0][1])) * dataArray.length; + } else { + return 0; + } + } + + /** Read the RandomGroupsData */ + public void read(ArrayDataInput str) throws FitsException { + + setFileOffset(str); + + try { + str.readLArray(dataArray); + } catch (IOException e) { + throw new FitsException("IO error reading Random Groups data "+e); + } + int pad = FitsUtil.padding(getTrueSize()); + try { + str.skipBytes(pad); + } catch (EOFException e) { + throw new PaddingException("EOF reading padding after random groups", this); + } catch (IOException e) { + throw new FitsException("IO error reading padding after random groups"); + } + } + + /** Write the RandomGroupsData */ + public void write(ArrayDataOutput str) throws FitsException { + try { + str.writeArray(dataArray); + FitsUtil.pad(str, getTrueSize()); + } catch (IOException e) { + throw new FitsException("IO error writing random groups data "+e); + } + } + + protected void fillHeader(Header h) throws FitsException { + + if (dataArray.length <= 0 || dataArray[0].length != 2) { + throw new FitsException("Data not conformable to Random Groups"); + } + + int gcount = dataArray.length; + Object paraSamp = dataArray[0][0]; + Object dataSamp = dataArray[0][1]; + + Class pbase = nom.tam.util.ArrayFuncs.getBaseClass(paraSamp); + Class dbase = nom.tam.util.ArrayFuncs.getBaseClass(dataSamp); + + if (pbase != dbase) { + throw new FitsException("Data and parameters do not agree in type for random group"); + } + + int[] pdims = nom.tam.util.ArrayFuncs.getDimensions(paraSamp); + int[] ddims = nom.tam.util.ArrayFuncs.getDimensions(dataSamp); + + if (pdims.length != 1) { + throw new FitsException("Parameters are not 1 d array for random groups"); + } + + // Got the information we need to build the header. + + h.setSimple(true); + if (dbase == byte.class) { + h.setBitpix(8); + } else if (dbase == short.class) { + h.setBitpix(16); + } else if (dbase == int.class) { + h.setBitpix(32); + } else if (dbase == long.class) { // Non-standard + h.setBitpix(64); + } else if (dbase == float.class) { + h.setBitpix(-32); + } else if (dbase == double.class) { + h.setBitpix(-64); + } else { + throw new FitsException("Data type:"+dbase+" not supported for random groups"); + } + + + h.setNaxes(ddims.length+1); + h.addValue("NAXIS1", 0, "ntf::randomgroupsdata:naxis1:1"); + for (int i=2; i<=ddims.length+1; i += 1) { + h.addValue("NAXIS"+i, ddims[i-2], "ntf::randomgroupsdata:naxisN:1"); + } + + h.addValue("GROUPS", true, "ntf::randomgroupsdata:groups:1"); + h.addValue("GCOUNT", dataArray.length, "ntf::randomgroupsdata:gcount:1"); + h.addValue("PCOUNT", pdims[0], "ntf::randomgroupsdata:pcount:1"); + } + + public Object getData() { + return dataArray; + } + + +} diff --git a/src/nom/tam/fits/RandomGroupsHDU.java b/src/nom/tam/fits/RandomGroupsHDU.java new file mode 100644 index 0000000..4ad51ff --- /dev/null +++ b/src/nom/tam/fits/RandomGroupsHDU.java @@ -0,0 +1,263 @@ +package nom.tam.fits; + +import nom.tam.util.ArrayFuncs; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +/** Random groups HDUs. Note that the internal storage of random + * groups is a Object[ngroup][2] array. The first element of + * each group is the parameter data from that group. The second element + * is the data. The parameters should be a one dimensional array + * of the primitive types byte, short, int, long, float or double. + * The second element is a n-dimensional array of the same type. + * When analyzing group data structure only the first group is examined, + * but for a valid FITS file all groups must have the same structure. + */ +public class RandomGroupsHDU extends BasicHDU { + + Object dataArray; + + /** Create an HDU from the given header and data */ + public RandomGroupsHDU(Header h, Data d) { + myHeader = h; + myData = d; + } + + /** Indicate that a RandomGroupsHDU can come at + * the beginning of a FITS file. + */ + protected boolean canBePrimary() { + return true; + } + + /** Move a RandomGroupsHDU to or from the beginning + * of a FITS file. Note that the FITS standard only + * supports Random Groups data at the beginning + * of the file, but we allow it within Image extensions. + */ + protected void setPrimaryHDU(boolean status) { + try { + super.setPrimaryHDU(status); + } catch (FitsException e) { + System.err.println("Unreachable catch in RandomGroupsHDU"); + } + if (status) { + myHeader.setSimple(true); + } else { + myHeader.setXtension("IMAGE"); + } + } + + /** Make a header point to the given object. + * @param odata The random groups data the header should describe. + */ + static Header manufactureHeader(Data d) throws FitsException { + + if (d == null) { + throw new FitsException("Attempt to create null Random Groups data"); + } + Header h = new Header(); + d.fillHeader(h); + return h; + + } + + /** Is this a random groups header? + * @param myHeader The header to be tested. + */ + public static boolean isHeader(Header hdr) { + + if (hdr.getBooleanValue("SIMPLE")) { + return hdr.getBooleanValue("GROUPS"); + } + + String s = hdr.getStringValue("XTENSION"); + if (s.trim().equals("IMAGE")) { + return hdr.getBooleanValue("GROUPS"); + } + + return false; + } + + /** Check that this HDU has a valid header. + * @return true if this HDU has a valid header. + */ + public boolean isHeader() { + return isHeader(myHeader); + } + + /** Check if this data is compatible with Random Groups structure. + * Must be an Object[ngr][2] structure with both elements of each + * group having the same base type and the first element being + * a simple primitive array. We do not check anything but + * the first row. + */ + public static boolean isData(Object oo) { + if (oo instanceof Object[][]) { + + Object[][] o = (Object[][]) oo; + + if (o.length > 0) { + if (o[0].length == 2) { + if (ArrayFuncs.getBaseClass(o[0][0]) + == ArrayFuncs.getBaseClass(o[0][1])) { + String cn = o[0][0].getClass().getName(); + if (cn.length() == 2 && cn.charAt(1) != 'Z' + || cn.charAt(1) != 'C') { + return true; + } + } + } + } + } + return false; + } + + /** Create a FITS Data object corresponding to + * this HDU header. + */ + public Data manufactureData() throws FitsException { + return manufactureData(myHeader); + } + + /** Create FITS data object corresponding to a given header. + */ + public static Data manufactureData(Header hdr) throws FitsException { + + int gcount = hdr.getIntValue("GCOUNT", -1); + int pcount = hdr.getIntValue("PCOUNT", -1); + + if (!hdr.getBooleanValue("GROUPS") + || hdr.getIntValue("NAXIS1", -1) != 0 + || gcount < 0 || pcount < 0 + || hdr.getIntValue("NAXIS") < 2) { + throw new FitsException("Invalid Random Groups Parameters"); + } + + // Allocate the object. + Object[][] dataArray; + + if (gcount > 0) { + dataArray = new Object[gcount][2]; + } else { + dataArray = new Object[0][]; + } + + Object[] sampleRow = generateSampleRow(hdr); + for (int i = 0; i < gcount; i += 1) { + ((Object[][]) dataArray)[i][0] = + ((Object[]) nom.tam.util.ArrayFuncs.deepClone(sampleRow))[0]; + ((Object[][]) dataArray)[i][1] = + ((Object[]) nom.tam.util.ArrayFuncs.deepClone(sampleRow))[1]; + } + return new RandomGroupsData(dataArray); + + } + + static Object[] generateSampleRow(Header h) + throws FitsException { + + int ndim = h.getIntValue("NAXIS", 0) - 1; + int[] dims = new int[ndim]; + + int bitpix = h.getIntValue("BITPIX", 0); + + + Class baseClass; + + switch (bitpix) { + case 8: + baseClass = Byte.TYPE; + break; + case 16: + baseClass = Short.TYPE; + break; + case 32: + baseClass = Integer.TYPE; + break; + case 64: + baseClass = Long.TYPE; + break; + case -32: + baseClass = Float.TYPE; + break; + case -64: + baseClass = Double.TYPE; + break; + default: + throw new FitsException("Invalid BITPIX:" + bitpix); + } + + // Note that we have to invert the order of the axes + // for the FITS file to get the order in the array we + // are generating. Also recall that NAXIS1=0, so that + // we have an 'extra' dimension. + + for (int i = 0; i < ndim; i += 1) { + long cdim = h.getIntValue("NAXIS" + (i + 2), 0); + if (cdim < 0) { + throw new FitsException("Invalid array dimension:" + cdim); + } + dims[ndim - i - 1] = (int) cdim; + } + + Object[] sample = new Object[2]; + sample[0] = ArrayFuncs.newInstance(baseClass, h.getIntValue("PCOUNT")); + sample[1] = ArrayFuncs.newInstance(baseClass, dims); + + return sample; + } + + public static Data encapsulate(Object o) throws FitsException { + if (o instanceof Object[][]) { + return new RandomGroupsData((Object[][]) o); + } else { + throw new FitsException("Attempt to encapsulate invalid data in Random Group"); + } + } + + /** Display structural information about the current HDU. + */ + public void info() { + + System.out.println("Random Groups HDU"); + if (myHeader != null) { + System.out.println(" HeaderInformation:"); + System.out.println(" Ngroups:" + myHeader.getIntValue("GCOUNT")); + System.out.println(" Npar: " + myHeader.getIntValue("PCOUNT")); + System.out.println(" BITPIX: " + myHeader.getIntValue("BITPIX")); + System.out.println(" NAXIS: " + myHeader.getIntValue("NAXIS")); + for (int i = 0; i < myHeader.getIntValue("NAXIS"); i += 1) { + System.out.println(" NAXIS" + (i + 1) + "= " + + myHeader.getIntValue("NAXIS" + (i + 1))); + } + } else { + System.out.println(" No Header Information"); + } + + + Object[][] data = null; + if (myData != null) { + try { + data = (Object[][]) myData.getData(); + } catch (FitsException e) { + data = null; + } + } + + if (data == null || data.length < 1 || data[0].length != 2) { + System.out.println(" Invalid/unreadable data"); + } else { + System.out.println(" Number of groups:" + data.length); + System.out.println(" Parameters: " + nom.tam.util.ArrayFuncs.arrayDescription(data[0][0])); + System.out.println(" Data:" + nom.tam.util.ArrayFuncs.arrayDescription(data[0][1])); + } + } +} diff --git a/src/nom/tam/fits/TableData.java b/src/nom/tam/fits/TableData.java new file mode 100644 index 0000000..02c9b95 --- /dev/null +++ b/src/nom/tam/fits/TableData.java @@ -0,0 +1,29 @@ +package nom.tam.fits; + + +/** This class allows FITS binary and ASCII tables to + * be accessed via a common interface. + */ + +public interface TableData { + + public abstract Object[] getRow (int row) throws FitsException; + public abstract Object getColumn (int col) throws FitsException; + public abstract Object getElement(int row, int col) throws FitsException; + + public abstract void setRow (int row, Object[] newRow) throws FitsException; + public abstract void setColumn (int col, Object newCol) throws FitsException; + public abstract void setElement (int row, int col, Object element) throws FitsException; + + public abstract int addRow (Object[] newRow) throws FitsException; + public abstract int addColumn(Object newCol) throws FitsException; + + public abstract void deleteRows(int row, int len) throws FitsException; + public abstract void deleteColumns(int row, int len) throws FitsException; + + public abstract void updateAfterDelete(int oldNcol, Header hdr) throws FitsException; + + public abstract int getNCols(); + public abstract int getNRows(); + +} diff --git a/src/nom/tam/fits/TableHDU.java b/src/nom/tam/fits/TableHDU.java new file mode 100644 index 0000000..544ba60 --- /dev/null +++ b/src/nom/tam/fits/TableHDU.java @@ -0,0 +1,351 @@ +package nom.tam.fits; + +import java.util.Iterator; +import nom.tam.util.Cursor; + +/** This class allows FITS binary and ASCII tables to + * be accessed via a common interface. + * + * Bug Fix: 3/28/01 to findColumn. + */ +public abstract class TableHDU extends BasicHDU { + + private TableData table; + private int currentColumn; + + /** Create the TableHDU. Note that this + * will normally only be invoked by subclasses + * in the FITS package. + * @param td The data for the table. + */ + TableHDU(TableData td) { + table = td; + } + + /** Get a specific row of the table */ + public Object[] getRow(int row) throws FitsException { + return table.getRow(row); + } + + /** Get a specific column of the table where + * the column name is specified using the TTYPEn keywords + * in the header. + * @param colName The name of the column to be extracted. + * @throws FitsException + */ + public Object getColumn(String colName) throws FitsException { + return getColumn(findColumn(colName)); + } + + /** Get a specific column from the table using 0-based column + * indexing. + */ + public Object getColumn(int col) throws FitsException { + return table.getColumn(col); + } + + /** Get all of the columns of the table. + */ + public Object[] getColumns() throws FitsException { + Object[] result = new Object[getNCols()]; + for (int i = 0; i < result.length; i += 1) { + result[i] = getColumn(i); + } + return result; + } + + /** Get a specific element of the table using 0-based indices. + * + */ + public Object getElement(int row, int col) throws FitsException { + return table.getElement(row, col); + } + + /** Update a row within a table. + * + */ + public void setRow(int row, Object[] newRow) throws FitsException { + table.setRow(row, newRow); + } + + /** Update a column within a table. The new column should have the + * same format as the column being replaced. + */ + public void setColumn(String colName, Object newCol) throws FitsException { + setColumn(findColumn(colName), newCol); + } + + /** Update a column within a table. The new column should have the same + * format ast the column being replaced. + */ + public void setColumn(int col, Object newCol) throws FitsException { + table.setColumn(col, newCol); + } + + /** Update a single element within the table. + */ + public void setElement(int row, int col, Object element) throws FitsException { + table.setElement(row, col, element); + } + + /** Add a row to the end of the table. If this is the first row, + * then this will add appropriate columns for each of the entries. + */ + public int addRow(Object[] newRow) throws FitsException { + + int row = table.addRow(newRow); + myHeader.addValue("NAXIS2", row, "ntf::tablehdu:naxis2:1"); + return row; + } + + /** Find the 0-based column index corresponding to a particular + * column name. + */ + public int findColumn(String colName) { + + for (int i = 0; i < getNCols(); i += 1) { + + String val = myHeader.getStringValue("TTYPE" + (i + 1)); + if (val != null && val.trim().equals(colName)) { + return i; + } + } + return -1; + } + + /** Add a column to the table. */ + public abstract int addColumn(Object data) throws FitsException; + + /** Get the number of columns for this table + * @return The number of columns in the table. + */ + public int getNCols() { + return table.getNCols(); + } + + /** Get the number of rows for this table + * @return The number of rows in the table. + */ + public int getNRows() { + return table.getNRows(); + } + + /** Get the name of a column in the table. + * @param index The 0-based column index. + * @return The column name. + * @exception FitsException if an invalid index was requested. + */ + public String getColumnName(int index) { + + String ttype = myHeader.getStringValue("TTYPE" + (index + 1)); + if (ttype != null) { + ttype = ttype.trim(); + } + return ttype; + } + + public void setColumnName(int index, String name, String comment) + throws FitsException { + setColumnMeta(index, "TTYPE", name, comment, true); + } + + /** Specify column metadata for a given column in a way that + * allows all of the column metadata for a given column + * to be organized together. + * + * @param index The 0-based index of the column + * @param key The column key. I.e., the keyword will be key+(index+1) + * @param value The value to be placed in the header. + * @param comment The comment for the header + * @param after Should the header card be after the current column metadata block + * (true), or immediately before the TFORM card (false). + * @throws FitsException + */ + public void setColumnMeta(int index, String key, String value, String comment, boolean after) + throws FitsException { + setCurrentColumn(index, after); + myHeader.addValue(key + (index + 1), value, comment); + } + + /** Convenience method for getting column data. Note that this works + * only for metadata that returns a string value. This is equivalent + * to getStringValue(type+index); + */ + public String getColumnMeta(int index, String type) { + return myHeader.getStringValue(type+(index+1)); + } + + public void setColumnMeta(int index, String key, String value, String comment) + throws FitsException { + setColumnMeta(index, key, value, comment, true); + } + + public void setColumnMeta(int index, String key, long value, String comment, boolean after) + throws FitsException { + setCurrentColumn(index, after); + myHeader.addValue(key + (index + 1), value, comment); + } + + public void setColumnMeta(int index, String key, double value, String comment, boolean after) + throws FitsException { + setCurrentColumn(index, after); + myHeader.addValue(key + (index + 1), value, comment); + } + + public void setColumnMeta(int index, String key, boolean value, String comment, boolean after) + throws FitsException { + setCurrentColumn(index, after); + myHeader.addValue(key + (index + 1), value, comment); + } + + /** Get the FITS type of a column in the table. + * @param index The 0-based index of the column. + * @return The FITS type. + * @exception FitsException if an invalid index was requested. + */ + public String getColumnFormat(int index) + throws FitsException { + int flds = myHeader.getIntValue("TFIELDS", 0); + if (index < 0 || index >= flds) { + throw new FitsException("Bad column index " + index + " (only " + flds + + " columns)"); + } + + return myHeader.getStringValue("TFORM" + (index + 1)).trim(); + } + + /** Set the cursor in the header to point after the + * metadata for the specified column + * @param col The 0-based index of the column + */ + public void setCurrentColumn(int col) { + setCurrentColumn(col, true); + } + + /** Set the cursor in the header to point either before the + * TFORM value or after the column metadat + * @param col The 0-based index of the column + * @param after True if the cursor should be placed after the existing column + * metadata or false if the cursor is to be placed before the TFORM value. + * If no corresponding TFORM is found, the cursoe will be placed at the end of + * current header. + */ + public void setCurrentColumn(int col, boolean after) { + if (after) { + myHeader.positionAfterIndex("TFORM", col + 1); + } else { + String tform = "TFORM" + (col + 1); + myHeader.findCard(tform); + } + } + + /** + * Remove all rows from the table starting at some specific index from the table. + * Inspired by a routine by R. Mathar but re-implemented using the DataTable and + * changes to AsciiTable so that it can be done easily for both Binary and ASCII tables. + * @param row the (0-based) index of the first row to be deleted. + * @throws FitsExcpetion if an error occurs. + */ + public void deleteRows(final int row) throws FitsException { + deleteRows(row, getNRows() - row); + } + + /** + * Remove a number of adjacent rows from the table. This routine + * was inspired by code by R.Mathar but re-implemented using changes + * in the ColumnTable class abd AsciiTable so that we can do + * it for all FITS tables. + * @param firstRow the (0-based) index of the first row to be deleted. + * This is zero-based indexing: 0<=firstrow< number of rows. + * @param nRow the total number of rows to be deleted. + * @throws FitsException If an error occurs in the deletion. + */ + public void deleteRows(final int firstRow, int nRow) throws FitsException { + + // Just ignore invalid requests. + if (nRow <= 0 || firstRow >= getNRows() || nRow <= 0) { + return; + } + + /* correct if more rows are requested than available */ + if (nRow > getNRows() - firstRow) { + nRow = getNRows() - firstRow; + } + + table.deleteRows(firstRow, nRow); + myHeader.setNaxis(2, getNRows()); + } + + /** Delete a set of columns from a table. + */ + public void deleteColumnsIndexOne(int column, int len) throws FitsException { + deleteColumnsIndexZero(column - 1, len); + } + + /** Delete a set of columns from a table. + */ + public void deleteColumnsIndexZero(int column, int len) throws FitsException { + deleteColumnsIndexZero(column, len, columnKeyStems()); + } + + /** Delete a set of columns from a table. + * @param column The one-indexed start column. + * @param len The number of columns to delete. + * @param fields Stems for the header fields to be removed + * for the table. + */ + public void deleteColumnsIndexOne(int column, int len, String[] fields) throws FitsException { + deleteColumnsIndexZero(column - 1, len, fields); + } + + /** Delete a set of columns from a table. + * @param column The zero-indexed start column. + * @param len The number of columns to delete. + * @param fields Stems for the header fields to be removed + * for the table. + */ + public void deleteColumnsIndexZero(int column, int len, String[] fields) throws FitsException { + + if (column < 0 || len < 0 || column + len > getNCols()) { + throw new FitsException("Illegal columns deletion request- Start:" + column + " Len:" + len + " from table with " + getNCols() + " columns"); + } + + if (len == 0) { + return; + } + + int ncol = getNCols(); + table.deleteColumns(column, len); + + + // Get rid of the keywords for the deleted columns + for (int col = column; col < column + len; col += 1) { + for (int fld = 0; fld < fields.length; fld += 1) { + String key = fields[fld] + (col + 1); + myHeader.deleteKey(key); + } + } + + // Shift the keywords for the columns after the deleted columns + for (int col = column + len; col < ncol; col += 1) { + for (int fld = 0; fld < fields.length; fld += 1) { + String oldKey = fields[fld] + (col + 1); + String newKey = fields[fld] + (col + 1 - len); + if (myHeader.containsKey(oldKey)) { + myHeader.replaceKey(oldKey, newKey); + } + } + } + // Update the number of fields. + myHeader.addValue("TFIELDS", getNCols(), "ntf::tablehdu:tfields:1"); + + // Give the data sections a chance to update the header too. + table.updateAfterDelete(ncol, myHeader); + } + + /** Get the stems of the keywords that are associated + * with table columns. Users can supplement this + * with their own and call the appropriate deleteColumns fields. + */ + public abstract String[] columnKeyStems(); +} diff --git a/src/nom/tam/fits/TruncatedFileException.java b/src/nom/tam/fits/TruncatedFileException.java new file mode 100644 index 0000000..0a2fbf2 --- /dev/null +++ b/src/nom/tam/fits/TruncatedFileException.java @@ -0,0 +1,25 @@ +package nom.tam.fits; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +/** This exception is thrown when an EOF is detected in the middle + * of an HDU. + */ +public class TruncatedFileException + extends FitsException { + + public TruncatedFileException() { + super(); + } + + public TruncatedFileException(String msg) { + super(msg); + } +} diff --git a/src/nom/tam/fits/UndefinedData.java b/src/nom/tam/fits/UndefinedData.java new file mode 100644 index 0000000..69a0981 --- /dev/null +++ b/src/nom/tam/fits/UndefinedData.java @@ -0,0 +1,148 @@ +package nom.tam.fits; + +import nom.tam.util.*; +import java.io.*; + + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ +/** This class provides a simple holder for data which is + * not handled by other classes. + */ +public class UndefinedData extends Data { + + /** The size of the data */ + long byteSize; + byte[] data; + + public UndefinedData(Header h) throws FitsException { + + /** Just get a byte buffer to hold the data. + */ + // Bug fix by Vincenzo Forzi. + int naxis = h.getIntValue("NAXIS"); + + int size = naxis > 0 ? 1 : 0; + for (int i = 0; i < naxis; i += 1) { + size *= h.getIntValue("NAXIS" + (i + 1)); + } + size += h.getIntValue("PCOUNT"); + if (h.getIntValue("GCOUNT") > 1) { + size *= h.getIntValue("GCOUNT"); + } + size *= Math.abs(h.getIntValue("BITPIX") / 8); + + data = new byte[size]; + byteSize = size; + } + + /** Create an UndefinedData object using the specified object. + */ + public UndefinedData(Object x) { + + byteSize = ArrayFuncs.computeLSize(x); + data = new byte[(int) byteSize]; + } + + /** Fill header with keywords that describe data. + * @param head The FITS header + */ + protected void fillHeader(Header head) { + + try { + head.setXtension("UNKNOWN"); + head.setBitpix(8); + head.setNaxes(1); + head.addValue("NAXIS1", byteSize,"ntf::undefineddata:naxis1:1"); + head.addValue("PCOUNT", 0, "ntf::undefineddata:pcount:1"); + head.addValue("GCOUNT", 1, "ntf::undefineddata:gcount:1"); + head.addValue("EXTEND", true, "ntf::undefineddata:extend:1"); // Just in case! + } catch (HeaderCardException e) { + System.err.println("Unable to create unknown header:" + e); + } + + } + + public void read(ArrayDataInput i) throws FitsException { + setFileOffset(i); + + if (i instanceof RandomAccess) { + try { + i.skipBytes(byteSize); + } catch (IOException e) { + throw new FitsException("Unable to skip over data:" + e); + } + + } else { + try { + i.readFully(data); + } catch (IOException e) { + throw new FitsException("Unable to read unknown data:" + e); + } + + } + + int pad = FitsUtil.padding(getTrueSize()); + try { + i.skipBytes(pad); + } catch (EOFException e) { + throw new PaddingException("EOF skipping padding in undefined data", this); + } catch (IOException e) { + throw new FitsException("Error skipping padding in undefined data"); + } + } + + public void write(ArrayDataOutput o) throws FitsException { + + if (data == null) { + getData(); + } + + if (data == null) { + throw new FitsException("Null unknown data"); + } + + try { + o.write(data); + } catch (IOException e) { + throw new FitsException("IO Error on unknown data write" + e); + } + + FitsUtil.pad(o, getTrueSize()); + + } + + /** Get the size in bytes of the data */ + protected long getTrueSize() { + return byteSize; + } + + /** Return the actual data. + * Note that this may return a null when + * the data is not readable. It might be better + * to throw a FitsException, but this is + * a very commonly called method and we prefered + * not to change how users must invoke it. + */ + public Object getData() { + + if (data == null) { + + try { + FitsUtil.reposition(input, fileOffset); + input.read(data); + } catch (Exception e) { + return null; + } + } + + return data; + } +} diff --git a/src/nom/tam/fits/UndefinedHDU.java b/src/nom/tam/fits/UndefinedHDU.java new file mode 100644 index 0000000..e854e55 --- /dev/null +++ b/src/nom/tam/fits/UndefinedHDU.java @@ -0,0 +1,93 @@ +package nom.tam.fits; +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + * Many thanks to David Glowacki (U. Wisconsin) for substantial + * improvements, enhancements and bug fixes. + */ + +import nom.tam.util.ArrayFuncs; + +/** Holder for unknown data types. */ +public class UndefinedHDU + extends BasicHDU { + + /** Build an image HDU using the supplied data. + * @param obj the data used to build the image. + * @exception FitsException if there was a problem with the data. + */ + public UndefinedHDU(Header h, Data d) + throws FitsException { + myData = d; + myHeader = h; + + } + + /* Check if we can find the length of the data for this + * header. + * @return true if this HDU has a valid header. + */ + public static boolean isHeader(Header hdr) { + if (hdr.getStringValue("XTENSION") != null + && hdr.getIntValue("NAXIS", -1) >= 0) { + return true; + } + return false; + } + + /** Check if we can use the following object as + * in an Undefined FITS block. We allow this + * so long as computeLSize can get a size. Note + * that computeLSize may be wrong! + * @param o The Object being tested. + */ + public static boolean isData(Object o) { + return ArrayFuncs.computeLSize(o) > 0; + } + + /** Create a Data object to correspond to the header description. + * @return An unfilled Data object which can be used to read + * in the data for this HDU. + * @exception FitsException if the image extension could not be created. + */ + public Data manufactureData() + throws FitsException { + return manufactureData(myHeader); + } + + public static Data manufactureData(Header hdr) + throws FitsException { + return new UndefinedData(hdr); + } + + /** Create a header that describes the given + * image data. + * @param o The image to be described. + * @exception FitsException if the object does not contain + * valid image data. + */ + public static Header manufactureHeader(Data d) + throws FitsException { + + Header h = new Header(); + d.fillHeader(h); + + return h; + } + + /** Encapsulate an object as an ImageHDU. */ + public static Data encapsulate(Object o) throws FitsException { + return new UndefinedData(o); + } + + /** Print out some information about this HDU. + */ + public void info() { + + System.out.println(" Unhandled/Undefined/Unknown Type"); + System.out.println(" XTENSION=" + myHeader.getStringValue("XTENSION").trim()); + System.out.println(" Apparent size:" + myData.getTrueSize()); + } +} diff --git a/src/nom/tam/fits/comments.txt b/src/nom/tam/fits/comments.txt new file mode 100644 index 0000000..ebcd4ae --- /dev/null +++ b/src/nom/tam/fits/comments.txt @@ -0,0 +1,67 @@ +# This file contains comment values for standard keywords. +# The first token on each non-comment line is a keyword that +# identifies the comment of the form class_keyword_n +# where class is the class in which the data is written +# keyword is the key value for the comment being written +# and n is an integer (usually 1) used to distinguish multiple +# entries. + +header_simple_1 Null Image Header +header_bitpix_1 BITPIX for null image +header_naxis_1 NAXIS for null image + +header_extend_1 Extensions are permitted +header_simple_2 Java FITS: $DATE +header_xtension_1 Java FITS: $DATE +header_bitpix_2 +header_naxis_2 Dimensionality +header_naxisN_1 +header_end_1 + +randomgroupsdata_naxis1_1 +randomgroupsdata_naxisN_1 +randomgroupsdata_groups_1 +randomgroupsdata_gcount_1 +randomgroupsdata_pcount_1 + +binarytablehdu_pcount_1 +binarytablehdu_theap_1 +binarytablehdu_tformN_1 +binarytablehdu_tdimN_1 + +asciitable_pcount_1 No group data +asciitable_gcount_1 One group +asciitable_tfields_1 Number of fields in table + +asciitable_tbcolN_1 Column Offset +asciitable_naxis1_1 Size of row in bytes +asciitablehdu_tnullN_1 +asciitablehdu_tfields_1 + +binarytable_pcount_1 +binarytable_gcount_1 +binarytable_naxis1_1 Bytes per row +binarytable_tfields + +basichdu_extend_1 Allow extensions +basichdu_pcount_1 Required value +basichdu_gcount_1 Required value + +imagedata_extend_1 Extension permitted +imagedata_pcount_1 No extra parameters +imagedata_gcount_1 One group + +tablehdu_naxis2_1 +tablehdu_tfields_1 Number of table fields + + + + + + + + +undefineddata_naxis1_1 Number of bytes in unknown structure +undefineddata_pcount_1 +undefineddata_gcount_1 +undefineddata_extend_1 Extensions are permitted \ No newline at end of file diff --git a/src/nom/tam/fits/test/AsciiTableTest.java b/src/nom/tam/fits/test/AsciiTableTest.java new file mode 100644 index 0000000..9200c22 --- /dev/null +++ b/src/nom/tam/fits/test/AsciiTableTest.java @@ -0,0 +1,313 @@ +package nom.tam.fits.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.util.*; +import nom.tam.fits.*; + +/** This class tests the AsciiTableHDU and AsciiTable FITS + * classes and implicitly the ByteFormatter and ByteParser + * classes in the nam.tam.util library. + * Tests include: + * Create columns of every type + * Read columns of every type + * Create a table column by column + * Create a table row by row + * Use deferred input on rows + * Use deferred input on elements + * Read rows, columns and elements from in-memory kernel. + * Specify width of columns. + * Rewrite data/header in place. + * Set and read null elements. + */ +public class AsciiTableTest { + + Object[] getSampleCols() { + + float[] realCol = new float[50]; + + for (int i = 0; i < realCol.length; i += 1) { + realCol[i] = 10000.F * (i) * (i) * (i) + 1; + } + + int[] intCol = (int[]) ArrayFuncs.convertArray(realCol, int.class); + long[] longCol = (long[]) ArrayFuncs.convertArray(realCol, long.class); + double[] doubleCol = (double[]) ArrayFuncs.convertArray(realCol, double.class); + + String[] strCol = new String[realCol.length]; + + for (int i = 0; i < realCol.length; i += 1) { + strCol[i] = "ABC" + String.valueOf(realCol[i]) + "CDE"; + } + return new Object[]{realCol, intCol, longCol, doubleCol, strCol}; + } + + Fits makeAsciiTable() throws Exception { + Object[] cols = getSampleCols(); + // Create the new ASCII table. + Fits f = new Fits(); + f.addHDU(Fits.makeHDU(cols)); + return f; + } + + public void writeFile(Fits f, String name) throws Exception { + BufferedFile bf = new BufferedFile(name, "rw"); + f.write(bf); + bf.flush(); + bf.close(); + } + + @Test + public void test() throws Exception { + createByColumn(); + createByRow(); + readByRow(); + readByColumn(); + readByElement(); + modifyTable(); + delete(); + } + + public void createByColumn() throws Exception { + Fits f = makeAsciiTable(); + writeFile(f, "at1.fits"); + + // Read back the data from the file. + f = new Fits("at1.fits"); + AsciiTableHDU hdu = (AsciiTableHDU) f.getHDU(1); + + Object[] inputs = getSampleCols(); + Object[] outputs = (Object[]) hdu.getKernel(); + + for (int i = 0; i < 50; i += 1) { + ((String[]) outputs[4])[i] = ((String[]) outputs[4])[i].trim(); + } + + for (int j = 0; j < 5; j += 1) { + assertEquals("ByCol:" + j, true, ArrayFuncs.arrayEquals(inputs[j], outputs[j], 1.e-6, 1.e-14)); + } + + } + + Object[] getRow(int i) { + return new Object[]{ + new int[]{i}, + new float[]{i}, + new String[]{"Str" + i} + }; + } + + Object[] getRowBlock(int max) { + Object[] o = new Object[]{new int[max], new float[max], new String[max]}; + for (int i = 0; i < max; i += 1) { + ((int[]) o[0])[i] = i; + ((float[]) o[1])[i] = i; + ((String[]) o[2])[i] = "Str" + i; + } + return o; + } + + public void createByRow() throws Exception { + + // Create a table row by row . + Fits f = new Fits(); + AsciiTable data = new AsciiTable(); + Object[] row = new Object[4]; + + for (int i = 0; i < 50; i += 1) { + data.addRow(getRow(i)); + } + + f.addHDU(Fits.makeHDU(data)); + + writeFile(f, "at2.fits"); + + // Read it back. + f = new Fits("at2.fits"); + + Object[] output = (Object[]) f.getHDU(1).getKernel(); + Object[] input = getRowBlock(50); + + for (int i = 0; i < 50; i += 1) { + String[] str = (String[]) output[2]; + String[] istr = (String[]) input[2]; + int len1 = str[1].length(); + str[i] = str[i].trim(); + // The first row would have set the length for all the + // remaining rows... + if (istr[i].length() > len1) { + istr[i] = istr[i].substring(0, len1); + } + } + + for (int j = 0; j < 3; j += 1) { + assertEquals("ByRow:" + j, true, ArrayFuncs.arrayEquals(input[j], output[j], 1.e-6, 1.e-14)); + } + } + + public void readByRow() throws Exception { + + Fits f = new Fits("at1.fits"); + Object[] cols = getSampleCols(); + + AsciiTableHDU hdu = (AsciiTableHDU) f.getHDU(1); + AsciiTable data = (AsciiTable) hdu.getData(); + + for (int i = 0; i < data.getNRows(); i += 1) { + assertEquals("Rows:" + i, 50, data.getNRows()); + Object[] row = data.getRow(i); + assertEquals("Ascii Rows: float" + i, 1.F, ((float[]) cols[0])[i] / ((float[]) row[0])[0], 1.e-6); + assertEquals("Ascii Rows: int" + i, ((int[]) cols[1])[i], ((int[]) row[1])[0]); + assertEquals("Ascii Rows: long" + i, ((long[]) cols[2])[i], ((long[]) row[2])[0]); + assertEquals("Ascii Rows: double" + i, 1., ((double[]) cols[3])[i] / ((double[]) row[3])[0], 1.e-14); + String[] st = (String[]) row[4]; + st[0] = st[0].trim(); + assertEquals("Ascii Rows: Str" + i, ((String[]) cols[4])[i], ((String[]) row[4])[0]); + } + } + + public void readByColumn() throws Exception { + Fits f = new Fits("at1.fits"); + AsciiTableHDU hdu = (AsciiTableHDU) f.getHDU(1); + AsciiTable data = (AsciiTable) hdu.getData(); + Object[] cols = getSampleCols(); + + assertEquals("Number of rows", data.getNRows(), 50); + assertEquals("Number of columns", data.getNCols(), 5); + + for (int j = 0; j < data.getNCols(); j += 1) { + Object col = data.getColumn(j); + if (j == 4) { + String[] st = (String[]) col; + for (int i = 0; i < st.length; i += 1) { + st[i] = st[i].trim(); + } + } + assertEquals("Ascii Columns:" + j, true, ArrayFuncs.arrayEquals(cols[j], col, 1.e-6, 1.e-14)); + } + } + + public void readByElement() throws Exception { + + Fits f = new Fits("at2.fits"); + AsciiTableHDU hdu = (AsciiTableHDU) f.getHDU(1); + AsciiTable data = (AsciiTable) hdu.getData(); + + + for (int i = 0; i < data.getNRows(); i += 1) { + Object[] row = (Object[]) data.getRow(i); + for (int j = 0; j < data.getNCols(); j += 1) { + Object val = data.getElement(i, j); + assertEquals("Ascii readElement", true, ArrayFuncs.arrayEquals(val, row[j])); + } + } + } + + public void modifyTable() throws Exception { + + Fits f = new Fits("at1.fits"); + Object[] samp = getSampleCols(); + + AsciiTableHDU hdu = (AsciiTableHDU) f.getHDU(1); + AsciiTable data = (AsciiTable) hdu.getData(); + float[] f1 = (float[]) data.getColumn(0); + float[] f2 = (float[]) f1.clone(); + for (int i = 0; i < f2.length; i += 1) { + f2[i] = 2 * f2[i]; + } + + data.setColumn(0, f2); + f1 = new float[]{3.14159f}; + data.setElement(3, 0, f1); + + hdu.setNullString(0, "**INVALID**"); + data.setNull(5, 0, true); + data.setNull(6, 0, true); + + Object[] row = new Object[5]; + row[0] = new float[]{6.28f}; + row[1] = new int[]{22}; + row[2] = new long[]{0}; + row[3] = new double[]{-3}; + row[4] = new String[]{"A string"}; + + data.setRow(5, row); + + data.setElement(4, 2, new long[]{54321}); + + BufferedFile bf = new BufferedFile("at1x.fits", "rw"); + f.write(bf); + + + f = new Fits("at1x.fits"); + AsciiTable tab = (AsciiTable) f.getHDU(1).getData(); + Object[] kern = (Object[]) tab.getKernel(); + + float[] fx = (float[]) kern[0]; + int[] ix = (int[]) kern[1]; + long[] lx = (long[]) kern[2]; + double[] dx = (double[]) kern[3]; + String[] sx = (String[]) kern[4]; + + float[] fy = (float[]) samp[0]; + int[] iy = (int[]) samp[1]; + long[] ly = (long[]) samp[2]; + double[] dy = (double[]) samp[3]; + String[] sy = (String[]) samp[4]; + + assertEquals("Null", true, tab.isNull(6, 0)); + assertEquals("Null2", false, tab.isNull(5, 0)); + + for (int i = 0; i < data.getNRows(); i += 1) { + if (i != 5) { + if (i != 6) { // Null + assertEquals("f" + i, 1., f2[i] / fx[i], 1.e-6); + } + assertEquals("i" + i, iy[i], ix[i]); + if (i == 4) { + assertEquals("l4", 54321L, lx[i]); + } else { + assertEquals("l" + i, ly[i], lx[i]); + } + assertEquals("d" + i, 1., dy[i] / dx[i], 1.e-14); + assertEquals("s" + i, sy[i], sx[i].trim()); + } + } + Object[] r5 = (Object[]) data.getRow(5); + String[] st = (String[]) r5[4]; + st[0] = st[0].trim(); + assertEquals("row5", true, ArrayFuncs.arrayEquals(row, r5, 1.e-6, 1.e-14)); + } + + public void delete() throws Exception { + + Fits f = new Fits("at1.fits"); + + TableHDU th = (TableHDU) f.getHDU(1); + assertEquals("delrBef", 50, th.getNRows()); + th.deleteRows(2, 2); + assertEquals("delrAft", 48, th.getNRows()); + BufferedFile bf = new BufferedFile("at1y.fits", "rw"); + f.write(bf); + bf.close(); + + f = new Fits("at1y.fits"); + th = (TableHDU) f.getHDU(1); + assertEquals("delrAft2", 48, th.getNRows()); + + assertEquals("delcBef", 5, th.getNCols()); + th.deleteColumnsIndexZero(3, 2); + assertEquals("delcAft1", 3, th.getNCols()); + th.deleteColumnsIndexZero(0, 2); + assertEquals("delcAft2", 1, th.getNCols()); + bf = new BufferedFile("at1z.fits", "rw"); + f.write(bf); + bf.close(); + + f = new Fits("at1z.fits"); + th = (TableHDU) f.getHDU(1); + assertEquals("delcAft3", 1, th.getNCols()); + } +} diff --git a/src/nom/tam/fits/test/BinaryTableTest.java b/src/nom/tam/fits/test/BinaryTableTest.java new file mode 100644 index 0000000..57b1a91 --- /dev/null +++ b/src/nom/tam/fits/test/BinaryTableTest.java @@ -0,0 +1,757 @@ +package nom.tam.fits.test; + +import nom.tam.util.*; +import nom.tam.fits.*; +import java.io.*; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; +import java.lang.reflect.*; + +/** This class tests the binary table classes for + * the Java FITS library, notably BinaryTableHDU, + * BinaryTable, FitsHeap and the utility class ColumnTable. + * Tests include: + *

+ *     Reading and writing data of all valid types.
+ *     Reading and writing variable length da
+ *     Creating binary tables from:
+ *        Object[][] array
+ *        Object[] array
+ *        ColumnTable
+ *        Column x Column
+ *        Row x Row
+ *     Read binary table
+ *        Row x row
+ *        Element x element
+ *     Modify
+ *        Row, column, element
+ *     Rewrite binary table in place
+ * 
+ */ +public class BinaryTableTest { + + byte[] bytes = new byte[50]; + byte[][] bits = new byte[50][2]; + boolean[] bools = new boolean[50]; + short[][] shorts = new short[50][3]; + int[] ints = new int[50]; + float[][][] floats = new float[50][4][4]; + double[] doubles = new double[50]; + long[] longs = new long[50]; + String[] strings = new String[50]; + float[][] vf = new float[50][]; + short[][] vs = new short[50][]; + double[][] vd = new double[50][]; + boolean[][] vbool = new boolean[50][]; + float[][][] vc = new float[50][][]; + double[][][] vdc = new double[50][][]; + float[][] complex = new float[50][2]; + float[][][] complex_arr = new float[50][4][2]; + double[][] dcomplex = new double[50][2]; + double[][][] dcomplex_arr = new double[50][4][2]; + + @Before + public void initialize() { + + for (int i = 0; i < bytes.length; i += 1) { + bytes[i] = (byte) (2 * i); + bits[i][0] = bytes[i]; + bits[i][1] = (byte) (~bytes[i]); + bools[i] = (bytes[i] % 8) == 0 ? true : false; + + shorts[i][0] = (short) (2 * i); + shorts[i][1] = (short) (3 * i); + shorts[i][2] = (short) (4 * i); + + ints[i] = i * i; + for (int j = 0; j < 4; j += 1) { + for (int k = 0; k < 4; k += 1) { + floats[i][j][k] = (float) (i + j * Math.exp(k)); + } + } + doubles[i] = 3 * Math.sin(i); + longs[i] = i * i * i * i; + strings[i] = "abcdefghijklmnopqrstuvwxzy".substring(0, i % 20); + + vf[i] = new float[i + 1]; + vf[i][i / 2] = i * 3; + vs[i] = new short[i / 10 + 1]; + vs[i][i / 10] = (short) -i; + vd[i] = new double[i % 2 == 0 ? 1 : 2]; + vd[i][0] = 99.99; + vbool[i] = new boolean[i / 10]; + if (i >= 10) { + vbool[i][0] = i % 2 == 1; + } + + int m5 = i % 5; + vc[i] = new float[m5][]; + for (int j = 0; j < m5; j += 1) { + vc[i][j] = new float[2]; + vc[i][j][0] = i; + vc[i][j][1] = -j; + } + vdc[i] = new double[m5][]; + for (int j = 0; j < m5; j += 1) { + vdc[i][j] = new double[2]; + vdc[i][j][0] = -j; + vdc[i][j][1] = i; + } + double rad = 2 * i * Math.PI / bytes.length; + complex[i][0] = (float) (Math.cos(rad)); + complex[i][1] = (float) (Math.sin(rad)); + dcomplex[i][0] = complex[i][0]; + dcomplex[i][1] = complex[i][1]; + for (int j = 0; j < 4; j += 1) { + complex_arr[i][j][0] = (j + 1) * complex[i][0]; + complex_arr[i][j][1] = (j + 1) * complex[i][1]; + dcomplex_arr[i][j][0] = (j + 1) * complex[i][0]; + dcomplex_arr[i][j][1] = (j + 1) * complex[i][1]; + } + } + } + + @Test + public void testSimpleIO() throws Exception { + + FitsFactory.setUseAsciiTables(false); + + Fits f = new Fits(); + Object[] data = new Object[]{bytes, bits, bools, shorts, ints, + floats, doubles, longs, strings, complex, dcomplex, complex_arr, dcomplex_arr}; + f.addHDU(Fits.makeHDU(data)); + + BinaryTableHDU bhdu = (BinaryTableHDU) f.getHDU(1); + bhdu.setColumnName(0, "bytes", null); + bhdu.setColumnName(1, "bits", "bits later on"); + bhdu.setColumnName(6, "doubles", null); + bhdu.setColumnName(5, "floats", "4 x 4 array"); + + BufferedFile bf = new BufferedFile("bt1.fits", "rw"); + f.write(bf); + bf.flush(); + bf.close(); + + + f = new Fits("bt1.fits"); + f.read(); + + assertEquals("NHDU", 2, f.getNumberOfHDUs()); + + + BinaryTableHDU thdu = (BinaryTableHDU) f.getHDU(1); + Header hdr = thdu.getHeader(); + + assertEquals("HDR1", data.length, hdr.getIntValue("TFIELDS")); + assertEquals("HDR2", 2, hdr.getIntValue("NAXIS")); + assertEquals("HDR3", 8, hdr.getIntValue("BITPIX")); + assertEquals("HDR4", "BINTABLE", hdr.getStringValue("XTENSION")); + assertEquals("HDR5", "bytes", hdr.getStringValue("TTYPE1")); + assertEquals("HDR6", "doubles", hdr.getStringValue("TTYPE7")); + + for (int i = 0; i < data.length; i += 1) { + Object col = thdu.getColumn(i); + if (i == 8) { + String[] st = (String[]) col; + + for (int j = 0; j < st.length; j += 1) { + st[j] = st[j].trim(); + } + } + assertEquals("Data" + i, true, ArrayFuncs.arrayEquals(data[i], col)); + } + } + + @Test + public void testSimpleComplex() throws Exception { + try { + FitsFactory.setUseAsciiTables(false); + + Fits f = new Fits(); + Object[] data = new Object[]{bytes, bits, bools, shorts, ints, + floats, doubles, longs, strings, complex, dcomplex, complex_arr, dcomplex_arr}; + BinaryTableHDU bhdu = (BinaryTableHDU) Fits.makeHDU(data); + + bhdu.setComplexColumn(9); + bhdu.setComplexColumn(10); + bhdu.setComplexColumn(11); + bhdu.setComplexColumn(12); + + f.addHDU(bhdu); + bhdu.setColumnName(9, "Complex1", null); + + BufferedFile bf = new BufferedFile("bt1c.fits", "rw"); + f.write(bf); + bf.flush(); + bf.close(); + + f = new Fits("bt1c.fits"); + f.read(); + + assertEquals("NHDUc", 2, f.getNumberOfHDUs()); + + + BinaryTableHDU thdu = null; + thdu = (BinaryTableHDU) f.getHDU(1); + Header hdr = thdu.getHeader(); + + for (int i = 0; i < data.length; i += 1) { + + Object col = thdu.getColumn(i); + if (i == 8) { + String[] st = (String[]) col; + + for (int j = 0; j < st.length; j += 1) { + st[j] = st[j].trim(); + } + } + int n = Array.getLength(data[i]); + + assertEquals("DataC" + i, true, ArrayFuncs.arrayEquals(data[i], col)); + } + + } catch (Exception e) { + e.printStackTrace(System.err); + throw e; + } + + } + + @Test + public void testRowDelete() throws Exception { + Fits f = new Fits("bt1.fits"); + f.read(); + + BinaryTableHDU thdu = (BinaryTableHDU) f.getHDU(1); + + assertEquals("Del1", 50, thdu.getNRows()); + thdu.deleteRows(10, 20); + assertEquals("Del2", 30, thdu.getNRows()); + + double[] dbl = (double[]) thdu.getColumn(6); + assertEquals("del3", dbl[9], doubles[9], 0); + assertEquals("del4", dbl[10], doubles[30], 0); + + BufferedFile bf = new BufferedFile("bt1x.fits", "rw"); + f.write(bf); + bf.close(); + + f = new Fits("bt1x.fits"); + f.read(); + thdu = (BinaryTableHDU) f.getHDU(1); + dbl = (double[]) thdu.getColumn(6); + assertEquals("del5", 30, thdu.getNRows()); + assertEquals("del6", 13, thdu.getNCols()); + assertEquals("del7", dbl[9], doubles[9], 0); + assertEquals("del8", dbl[10], doubles[30], 0); + + thdu.deleteRows(20); + assertEquals("del9", 20, thdu.getNRows()); + dbl = (double[]) thdu.getColumn(6); + assertEquals("del10", 20, dbl.length); + assertEquals("del11", dbl[0], doubles[0], 0); + assertEquals("del12", dbl[19], doubles[39], 0); + } + + @Test + public void testVar() throws Exception { + try { + Object[] data = new Object[]{floats, vf, vs, vd, shorts, vbool, vc, vdc}; + BasicHDU hdu = Fits.makeHDU(data); + Fits f = new Fits(); + f.addHDU(hdu); + BufferedDataOutputStream bdos = new BufferedDataOutputStream(new FileOutputStream("bt2.fits")); + f.write(bdos); + bdos.close(); + + f = new Fits("bt2.fits"); + f.read(); + BinaryTableHDU bhdu = (BinaryTableHDU) f.getHDU(1); + Header hdr = bhdu.getHeader(); + + assertEquals("var1", true, hdr.getIntValue("PCOUNT") > 0); + assertEquals("var2", data.length, hdr.getIntValue("TFIELDS")); + + for (int i = 0; i < data.length; i += 1) { + assertEquals("vardata" + i, true, ArrayFuncs.arrayEquals(data[i], bhdu.getColumn(i))); + } + } catch (Exception e) { + e.printStackTrace(System.err); + throw e; + } + } + + @Test + public void testSet() throws Exception { + + Fits f = new Fits("bt2.fits"); + f.read(); + BinaryTableHDU bhdu = (BinaryTableHDU) f.getHDU(1); + Header hdr = bhdu.getHeader(); + + // Check the various set methods on variable length data. + float[] dta = (float[]) bhdu.getElement(4, 1); + dta = new float[]{22, 21, 20}; + bhdu.setElement(4, 1, dta); + + BufferedDataOutputStream bdos = new BufferedDataOutputStream(new FileOutputStream("bt2a.fits")); + f.write(bdos); + bdos.close(); + + f = new Fits("bt2a.fits"); + bhdu = (BinaryTableHDU) f.getHDU(1); + float[] xdta = (float[]) bhdu.getElement(4, 1); + + assertEquals("ts1", true, ArrayFuncs.arrayEquals(dta, xdta)); + assertEquals("ts2", true, ArrayFuncs.arrayEquals(bhdu.getElement(3, 1), vf[3])); + assertEquals("ts4", true, ArrayFuncs.arrayEquals(bhdu.getElement(5, 1), vf[5])); + + assertEquals("ts5", true, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), dta)); + + float tvf[] = new float[]{101, 102, 103, 104}; + vf[4] = tvf; + + bhdu.setColumn(1, vf); + assertEquals("ts6", true, ArrayFuncs.arrayEquals(bhdu.getElement(3, 1), vf[3])); + assertEquals("ts7", true, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), vf[4])); + assertEquals("ts8", true, ArrayFuncs.arrayEquals(bhdu.getElement(5, 1), vf[5])); + + bdos = new BufferedDataOutputStream(new FileOutputStream("bt2b.fits")); + f.write(bdos); + bdos.close(); + + f = new Fits("bt2b.fits"); + bhdu = (BinaryTableHDU) f.getHDU(1); + assertEquals("ts9", true, ArrayFuncs.arrayEquals(bhdu.getElement(3, 1), vf[3])); + assertEquals("ts10", true, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), vf[4])); + assertEquals("ts11", true, ArrayFuncs.arrayEquals(bhdu.getElement(5, 1), vf[5])); + + Object[] rw = bhdu.getRow(4); + + float[] trw = new float[]{-1, -2, -3, -4, -5, -6}; + rw[1] = trw; + + bhdu.setRow(4, rw); + assertEquals("ts12", true, ArrayFuncs.arrayEquals(bhdu.getElement(3, 1), vf[3])); + assertEquals("ts13", false, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), vf[4])); + assertEquals("ts14", true, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), trw)); + assertEquals("ts15", true, ArrayFuncs.arrayEquals(bhdu.getElement(5, 1), vf[5])); + + bdos = new BufferedDataOutputStream(new FileOutputStream("bt2c.fits")); + f.write(bdos); + bdos.close(); + + f = new Fits("bt2c.fits"); + bhdu = (BinaryTableHDU) f.getHDU(1); + assertEquals("ts16", true, ArrayFuncs.arrayEquals(bhdu.getElement(3, 1), vf[3])); + assertEquals("ts17", false, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), vf[4])); + assertEquals("ts18", true, ArrayFuncs.arrayEquals(bhdu.getElement(4, 1), trw)); + assertEquals("ts19", true, ArrayFuncs.arrayEquals(bhdu.getElement(5, 1), vf[5])); + } + + @Test + public void buildByColumn() throws Exception { + + BinaryTable btab = new BinaryTable(); + + btab.addColumn(floats); + btab.addColumn(vf); + btab.addColumn(strings); + btab.addColumn(vbool); + btab.addColumn(ints); + btab.addColumn(vc); + btab.addColumn(complex); + + Fits f = new Fits(); + f.addHDU(Fits.makeHDU(btab)); + + BufferedDataOutputStream bdos = new BufferedDataOutputStream(new FileOutputStream("bt3.fits")); + f.write(bdos); + + f = new Fits("bt3.fits"); + BinaryTableHDU bhdu = (BinaryTableHDU) f.getHDU(1); + btab = (BinaryTable) bhdu.getData(); + + assertEquals("col1", true, ArrayFuncs.arrayEquals(floats, bhdu.getColumn(0))); + assertEquals("col2", true, ArrayFuncs.arrayEquals(vf, bhdu.getColumn(1))); + assertEquals("col6", true, ArrayFuncs.arrayEquals(vc, bhdu.getColumn(5))); + assertEquals("col7", true, ArrayFuncs.arrayEquals(complex, bhdu.getColumn(6))); + + String[] col = (String[]) bhdu.getColumn(2); + for (int i = 0; i < col.length; i += 1) { + col[i] = col[i].trim(); + } + assertEquals("coi3", true, ArrayFuncs.arrayEquals(strings, col)); + + assertEquals("col4", true, ArrayFuncs.arrayEquals(vbool, bhdu.getColumn(3))); + assertEquals("col5", true, ArrayFuncs.arrayEquals(ints, bhdu.getColumn(4))); + } + + @Test + public void buildByRow() throws Exception { + + Fits f = new Fits("bt2.fits"); + f.read(); + BinaryTableHDU bhdu = (BinaryTableHDU) f.getHDU(1); + Header hdr = bhdu.getHeader(); + BinaryTable btab = (BinaryTable) bhdu.getData(); + for (int i = 0; i < 50; i += 1) { + + Object[] row = btab.getRow(i); + float[] qx = (float[]) row[1]; + float[][] p = (float[][]) row[0]; + p[0][0] = (float) (i * Math.sin(i)); + btab.addRow(row); + } + + f = new Fits(); + f.addHDU(Fits.makeHDU(btab)); + BufferedFile bf = new BufferedFile("bt4.fits", "rw"); + f.write(bf); + bf.flush(); + bf.close(); + + f = new Fits("bt4.fits"); + + btab = (BinaryTable) f.getHDU(1).getData(); + assertEquals("row1", 100, btab.getNRows()); + + + // Try getting data before we read in the table. + + float[][][] xf = (float[][][]) btab.getColumn(0); + assertEquals("row2", (float) 0., xf[50][0][0], 0); + assertEquals("row3", (float) (49 * Math.sin(49)), xf[99][0][0], 0); + + for (int i = 0; i < xf.length; i += 3) { + + boolean[] ba = (boolean[]) btab.getElement(i, 5); + float[] fx = (float[]) btab.getElement(i, 1); + + int trow = i % 50; + + assertEquals("row4", true, ArrayFuncs.arrayEquals(ba, vbool[trow])); + assertEquals("row6", true, ArrayFuncs.arrayEquals(fx, vf[trow])); + + } + float[][][] cmplx = (float[][][]) btab.getColumn(6); + for (int i = 0; i < vc.length; i += 1) { + for (int j = 0; j < vc[i].length; j += 1) { + assertEquals("rowvc" + i + "_" + j, true, ArrayFuncs.arrayEquals(vc[i][j], cmplx[i + vc.length][j])); + } + } + // Fill the table. + f.getHDU(1).getData(); + + xf = (float[][][]) btab.getColumn(0); + assertEquals("row7", 0.F, xf[50][0][0], 0); + assertEquals("row8", (float) (49 * Math.sin(49)), xf[99][0][0], 0); + + for (int i = 0; i < xf.length; i += 3) { + + boolean[] ba = (boolean[]) btab.getElement(i, 5); + float[] fx = (float[]) btab.getElement(i, 1); + + int trow = i % 50; + + assertEquals("row9", true, ArrayFuncs.arrayEquals(ba, vbool[trow])); + assertEquals("row11", true, ArrayFuncs.arrayEquals(fx, vf[trow])); + + } + } + + @Test + public void testObj() throws Exception { + + /*** Create a binary table from an Object[][] array */ + Object[][] x = new Object[5][3]; + for (int i = 0; i < 5; i += 1) { + x[i][0] = new float[]{i}; + x[i][1] = new String("AString" + i); + x[i][2] = new int[][]{{i, 2 * i}, {3 * i, 4 * i}}; + } + + Fits f = new Fits(); + BasicHDU hdu = Fits.makeHDU(x); + f.addHDU(hdu); + BufferedFile bf = new BufferedFile("bt5.fits", "rw"); + f.write(bf); + bf.close(); + + /** Now get rid of some columns */ + BinaryTableHDU xhdu = (BinaryTableHDU) hdu; + + + // First column + assertEquals("delcol1", 3, xhdu.getNCols()); + xhdu.deleteColumnsIndexOne(1, 1); + assertEquals("delcol2", 2, xhdu.getNCols()); + + xhdu.deleteColumnsIndexZero(1, 1); + assertEquals("delcol3", 1, xhdu.getNCols()); + + bf = new BufferedFile("bt6.fits", "rw"); + f.write(bf); + + f = new Fits("bt6.fits"); + + xhdu = (BinaryTableHDU) f.getHDU(1); + assertEquals("delcol4", 1, xhdu.getNCols()); + } + + @Test + public void testDegenerate() throws Exception { + + String[] sa = new String[10]; + int[][] ia = new int[10][0]; + Fits f = new Fits(); + + for (int i = 0; i < sa.length; i += 1) { + sa[i] = ""; + } + + Object[] data = new Object[]{sa, ia}; + BinaryTableHDU bhdu = (BinaryTableHDU) Fits.makeHDU(data); + Header hdr = bhdu.getHeader(); + f.addHDU(bhdu); + BufferedFile bf = new BufferedFile("bt7.fits", "rw"); + f.write(bf); + bf.close(); + + assertEquals("degen1", 2, hdr.getIntValue("TFIELDS")); + assertEquals("degen2", 10, hdr.getIntValue("NAXIS2")); + assertEquals("degen3", 0, hdr.getIntValue("NAXIS1")); + + f = new Fits("bt7.fits"); + bhdu = (BinaryTableHDU) f.getHDU(1); + + hdr = bhdu.getHeader(); + assertEquals("degen4", 2, hdr.getIntValue("TFIELDS")); + assertEquals("degen5", 10, hdr.getIntValue("NAXIS2")); + assertEquals("degen6", 0, hdr.getIntValue("NAXIS1")); + } + + @Test + public void testDegen2() throws Exception { + FitsFactory.setUseAsciiTables(false); + + Object[] data = new Object[]{ + new String[]{"a", "b", "c", "d", "e", "f"}, + new int[]{1, 2, 3, 4, 5, 6}, + new float[]{1.f, 2.f, 3.f, 4.f, 5.f, 6.f}, + new String[]{"", "", "", "", "", ""}, + new String[]{"a", "", "c", "", "e", "f"}, + new String[]{"", "b", "c", "d", "e", "f"}, + new String[]{"a", "b", "c", "d", "e", ""}, + new String[]{null, null, null, null, null, null}, + new String[]{"a", null, "c", null, "e", "f"}, + new String[]{null, "b", "c", "d", "e", "f"}, + new String[]{"a", "b", "c", "d", "e", null} + }; + + Fits f = new Fits(); + f.addHDU(Fits.makeHDU(data)); + BufferedFile ff = new BufferedFile("bt8.fits", "rw"); + f.write(ff); + + f = new Fits("bt8.fits"); + BinaryTableHDU bhdu = (BinaryTableHDU) f.getHDU(1); + + assertEquals("deg21", "e", bhdu.getElement(4, data.length - 1)); + assertEquals("deg22", "", bhdu.getElement(5, data.length - 1)); + + String[] col = (String[]) bhdu.getColumn(0); + assertEquals("deg23", "a", col[0]); + assertEquals("deg24", "f", col[5]); + + col = (String[]) bhdu.getColumn(3); + assertEquals("deg25", "", col[0]); + assertEquals("deg26", "", col[5]); + + col = (String[]) bhdu.getColumn(7); // All nulls + assertEquals("deg27", "", col[0]); + assertEquals("deg28", "", col[5]); + + col = (String[]) bhdu.getColumn(8); + + assertEquals("deg29", "a", col[0]); + assertEquals("deg210", "", col[1]); + } + + @Test + public void testMultHDU() throws Exception { + BufferedFile ff = new BufferedFile("bt9.fits", "rw"); + Object[] data = new Object[]{bytes, bits, bools, shorts, ints, + floats, doubles, longs, strings}; + + Fits f = new Fits(); + + // Add two identical HDUs + f.addHDU(Fits.makeHDU(data)); + f.addHDU(Fits.makeHDU(data)); + f.write(ff); + ff.close(); + + f = new Fits("bt9.fits"); + + f.readHDU(); + BinaryTableHDU hdu; + // This would fail before... + int count = 0; + while ((hdu = (BinaryTableHDU) f.readHDU()) != null) { + int nrow = hdu.getHeader().getIntValue("NAXIS2"); + count += 1; + assertEquals(nrow, 50); + for (int i = 0; i < nrow; i += 1) { + Object o = hdu.getRow(i); + } + } + assertEquals(count, 2); + } + + @Test + public void testByteArray() { + String[] sarr = {"abc", " de", "f"}; + byte[] barr = {'a', 'b', 'c', ' ', 'b', 'c', 'a', 'b', ' '}; + + byte[] obytes = nom.tam.fits.FitsUtil.stringsToByteArray(sarr, 3); + assertEquals("blen", obytes.length, 9); + assertEquals("b1", obytes[0], (byte) 'a'); + assertEquals("b1", obytes[1], (byte) 'b'); + assertEquals("b1", obytes[2], (byte) 'c'); + assertEquals("b1", obytes[3], (byte) ' '); + assertEquals("b1", obytes[4], (byte) 'd'); + assertEquals("b1", obytes[5], (byte) 'e'); + assertEquals("b1", obytes[6], (byte) 'f'); + assertEquals("b1", obytes[7], (byte) ' '); + assertEquals("b1", obytes[8], (byte) ' '); + + String[] ostrings = nom.tam.fits.FitsUtil.byteArrayToStrings(barr, 3); + assertEquals("slen", ostrings.length, 3); + assertEquals("s1", ostrings[0], "abc"); + assertEquals("s2", ostrings[1], "bc"); + assertEquals("s3", ostrings[2], "ab"); + } + + @Test + public void columnMetaTest() throws Exception { + Object[] data = new Object[]{shorts, ints, + floats, doubles}; + + Fits f = new Fits(); + + // Add two identical HDUs + BinaryTableHDU bhdu = (BinaryTableHDU) Fits.makeHDU(data); + f.addHDU(bhdu); + + // makeHDU creates the TFORM keywords and sometimes + // the TDIM keywords. Let's add some additional + // column metadata. For each column we'll want a TTYPE, TCOMM, + // TUNIT and TX and TY + // value and we want the final header to be in this order + // TTYPE, TCOMM, TFORM, [TDIM,] TUNIT, TX, TY + int oldNCols = bhdu.getNCols(); + + for (int i = 0; i < bhdu.getNCols(); i += 1) { + bhdu.setColumnMeta(i, "TTYPE", "NAM" + (i + 1), null, false); + bhdu.setColumnMeta(i, "TCOMM", true, "Comment in comment", false); + bhdu.setColumnMeta(i, "TUNIT", "UNIT" + (i + 1), null, true); + bhdu.setColumnMeta(i, "TX", (i + 1), null, true); + bhdu.setColumnMeta(i, "TY", 2. * (i + 1), null, true); + } + + BufferedFile ff = new BufferedFile("bt10.fits", "rw"); + f.write(ff); + ff.close(); + f = new Fits("bt10.fits"); + + bhdu = (BinaryTableHDU) f.getHDU(1); + Header hdr = bhdu.getHeader(); + assertEquals("metaCount", oldNCols, bhdu.getNCols()); + for (int i = 0; i < bhdu.getNCols(); i += 1) { + // If this worked, the first header should be the TTYPE + hdr.findCard("TTYPE" + (i + 1)); + HeaderCard hc = hdr.nextCard(); + assertEquals("M" + i + "0", "TTYPE" + (i + 1), hc.getKey()); + hc = hdr.nextCard(); + assertEquals("M" + i + "A", "TCOMM" + (i + 1), hc.getKey()); + hc = hdr.nextCard(); + assertEquals("M" + i + "B", "TFORM" + (i + 1), hc.getKey()); + hc = hdr.nextCard(); + // There may have been a TDIM keyword inserted automatically. Let's + // skip it if it was. It should only appear immediately after the + // TFORM keyword. + if (hc.getKey().startsWith("TDIM")) { + hc = hdr.nextCard(); + } + assertEquals("M" + i + "C", "TUNIT" + (i + 1), hc.getKey()); + hc = hdr.nextCard(); + assertEquals("M" + i + "D", "TX" + (i + 1), hc.getKey()); + hc = hdr.nextCard(); + assertEquals("M" + i + "E", "TY" + (i + 1), hc.getKey()); + } + } + + @Test + public void specialStringsTest() throws Exception { + String[] strings = new String[]{ + "abc", + "abc\000", + "abc\012abc", + "abc\000abc", + "abc\177", + "abc\001def\002ghi\003" + }; + + String[] results1 = new String[]{ + strings[0], strings[0], strings[2], strings[0], strings[4], strings[5] + }; + String[] results2 = new String[]{ + strings[0], strings[0], "abc abc", strings[0], "abc ", "abc def ghi " + }; + + FitsFactory.setUseAsciiTables(false); + FitsFactory.setCheckAsciiStrings(false); + + Fits f = new Fits(); + + Object[] objs = new Object[]{strings}; + BinaryTableHDU bhdu = (BinaryTableHDU) Fits.makeHDU(objs); + f.addHDU(bhdu); + + BufferedFile bf = new BufferedFile("bt11a.fits", "rw"); + f.write(bf); + + bf.close(); + + f = new Fits("bt11a.fits"); + bhdu = (BinaryTableHDU) f.getHDU(1); + String[] vals = (String[]) bhdu.getColumn(0); + for (int i = 0; i < strings.length; i += 1) { + assertEquals("ssa" + i, results1[i], vals[i]); + } + + FitsFactory.setCheckAsciiStrings(true); + System.err.println(" A warning about invalid ASCII strings should follow."); + f = new Fits(); + + bhdu = (BinaryTableHDU) Fits.makeHDU(objs); + f.addHDU(bhdu); + bf = new BufferedFile("bt11b.fits", "rw"); + f.write(bf); + + bf.close(); + + f = new Fits("bt11b.fits"); + bhdu = (BinaryTableHDU) f.getHDU(1); + vals = (String[]) bhdu.getColumn(0); + for (int i = 0; i < strings.length; i += 1) { + assertEquals("ssb" + i, results2[i], vals[i]); + } + + FitsFactory.setCheckAsciiStrings(false); + } +} diff --git a/src/nom/tam/fits/test/CompressTest.java b/src/nom/tam/fits/test/CompressTest.java new file mode 100644 index 0000000..dd3aad0 --- /dev/null +++ b/src/nom/tam/fits/test/CompressTest.java @@ -0,0 +1,228 @@ +package nom.tam.fits.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.image.*; +import nom.tam.util.*; +import nom.tam.fits.*; + +import java.net.URL; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +/** Test reading .Z and .gz compressed files. + */ +public class CompressTest { + + @Test + public void testgz() throws Exception { + + File fil = new File("."); + System.out.println("File is:" + fil.getCanonicalPath()); + Fits f = new Fits("http://heasarc.gsfc.nasa.gov/FTP/asca/data/rev2/43021000/images/ad43021000gis25670_lo.totsky.gz"); + + BasicHDU h = f.readHDU(); + int[][] data = (int[][]) h.getKernel(); + double sum = 0; + for (int i = 0; i < data.length; i += 1) { + for (int j = 0; j < data[i].length; j += 1) { + sum += data[i][j]; + } + } + assertEquals("ZCompress", sum, 296915., 0); + } + + @Test + public void testZ() throws Exception { + + Fits f = new Fits("http://heasarc.gsfc.nasa.gov/FTP/rosat/data/pspc/processed_data/600000/rp600245n00/rp600245n00_im1.fits.Z"); + + BasicHDU h = f.readHDU(); + short[][] data = (short[][]) h.getKernel(); + double sum = 0; + for (int i = 0; i < data.length; i += 1) { + for (int j = 0; j < data[i].length; j += 1) { + sum += data[i][j]; + } + } + assertEquals("ZCompress", sum, 91806., 0); + } + + @Test + public void testStream() throws Exception { + InputStream is; + + is = new FileInputStream("test.fits"); + assertEquals("Stream1", 300, streamRead(is, false, false)); + + is = new FileInputStream("test.fits.Z"); + assertEquals("Stream2", 300, streamRead(is, false, false)); + + is = new FileInputStream("test.fits.gz"); + assertEquals("Stream3", 300, streamRead(is, false, false)); + + is = new FileInputStream("test.fits"); + assertEquals("Stream4", 300, streamRead(is, false, true)); + + is = new FileInputStream("test.fits.Z"); + assertEquals("Stream5", 300, streamRead(is, false, true)); + + is = new FileInputStream("test.fits.gz"); + assertEquals("Stream6", 300, streamRead(is, false, true)); + + + is = new FileInputStream("test.fits.Z"); + assertEquals("Stream7", 300, streamRead(is, true, true)); + + is = new FileInputStream("test.fits.gz"); + assertEquals("Stream8", 300, streamRead(is, true, true)); + + is = new FileInputStream("test.fits.bz2"); + assertEquals("Stream9", 300, streamRead(is, true, true)); + } + + @Test + public void testFile() throws Exception { + File is = new File("test.fits"); + assertEquals("File1", 300, fileRead(is, false, false)); + + is = new File("test.fits.Z"); + assertEquals("File2", 300, fileRead(is, false, false)); + + is = new File("test.fits.gz"); + assertEquals("File3", 300, fileRead(is, false, false)); + + is = new File("test.fits"); + assertEquals("File4", 300, fileRead(is, false, true)); + + is = new File("test.fits.Z"); + assertEquals("File7", 300, fileRead(is, true, true)); + + is = new File("test.fits.gz"); + assertEquals("File8", 300, fileRead(is, true, true)); + + is = new File("test.fits.bz2"); + assertEquals("File9", 300, fileRead(is, true, true)); + } + + @Test + public void testString() throws Exception { + String is = "test.fits"; + assertEquals("String1", 300, stringRead(is, false, false)); + + is = "test.fits.Z"; + assertEquals("String2", 300, stringRead(is, false, false)); + + is = "test.fits.gz"; + assertEquals("String3", 300, stringRead(is, false, false)); + + is = "test.fits"; + assertEquals("String4", 300, stringRead(is, false, true)); + + is = "test.fits.Z"; + assertEquals("String7", 300, stringRead(is, true, true)); + + is = "test.fits.gz"; + assertEquals("String8", 300, stringRead(is, true, true)); + + is = "test.fits.bz2"; + assertEquals("String8", 300, stringRead(is, true, true)); + + } + + @Test + public void testURL() throws Exception { + String is = "test.fits"; + assertEquals("String1", 300, urlRead(is, false, false)); + + is = "test.fits.Z"; + assertEquals("String2", 300, urlRead(is, false, false)); + + is = "test.fits.gz"; + assertEquals("String3", 300, urlRead(is, false, false)); + + is = "test.fits"; + assertEquals("String4", 300, urlRead(is, false, true)); + + is = "test.fits.Z"; + assertEquals("String7", 300, urlRead(is, true, true)); + + is = "test.fits.gz"; + assertEquals("String8", 300, urlRead(is, true, true)); + + is = "test.fits.bz2"; + assertEquals("String8", 300, urlRead(is, true, true)); + } + + int urlRead(String is, boolean comp, boolean useComp) + throws Exception { + File fil = new File(is); + + String path = fil.getCanonicalPath(); + URL u = new URL("file://" + path); + + Fits f; + if (useComp) { + f = new Fits(u, comp); + } else { + f = new Fits(u); + } + short[][] data = (short[][]) f.readHDU().getKernel(); + + return total(data); + } + + int streamRead(InputStream is, boolean comp, boolean useComp) + throws Exception { + Fits f; + if (useComp) { + f = new Fits(is, comp); + } else { + f = new Fits(is); + } + short[][] data = (short[][]) f.readHDU().getKernel(); + is.close(); + + return total(data); + } + + int fileRead(File is, boolean comp, boolean useComp) + throws Exception { + Fits f; + if (useComp) { + f = new Fits(is, comp); + } else { + f = new Fits(is); + } + short[][] data = (short[][]) f.readHDU().getKernel(); + + return total(data); + } + + int stringRead(String is, boolean comp, boolean useComp) + throws Exception { + Fits f; + if (useComp) { + f = new Fits(is, comp); + } else { + f = new Fits(is); + } + short[][] data = (short[][]) f.readHDU().getKernel(); + + return total(data); + } + + int total(short[][] data) { + int total = 0; + for (int i = 0; i < data.length; i += 1) { + for (int j = 0; j < data[i].length; j += 1) { + total += data[i][j]; + } + } + return total; + } +} diff --git a/src/nom/tam/fits/test/DateTester.java b/src/nom/tam/fits/test/DateTester.java new file mode 100644 index 0000000..aef3198 --- /dev/null +++ b/src/nom/tam/fits/test/DateTester.java @@ -0,0 +1,49 @@ +package nom.tam.fits.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.fits.FitsDate; + +/** Test the FITS date class. + * This class is derived from the internal testing utilities + * in FitsDate written by David Glowacki. + */ +public class DateTester { + + @Test + public void test() { + + assertEquals("t1", true, testArg("20/09/79")); + assertEquals("t1", true, testArg("1997-07-25")); + assertEquals("t1", true, testArg("1987-06-05T04:03:02.01")); + assertEquals("t1", true, testArg("1998-03-10T16:58:34")); + assertEquals("t1", true, testArg(null)); + assertEquals("t1", true, testArg(" ")); + + assertEquals("t1", false, testArg("20/09/")); + assertEquals("t1", false, testArg("/09/79")); + assertEquals("t1", false, testArg("09//79")); + assertEquals("t1", false, testArg("20/09/79/")); + + assertEquals("t1", false, testArg("1997-07")); + assertEquals("t1", false, testArg("-07-25")); + assertEquals("t1", false, testArg("1997--07-25")); + assertEquals("t1", false, testArg("1997-07-25-")); + + assertEquals("t1", false, testArg("5-Aug-1992")); + assertEquals("t1", false, testArg("28/02/91 16:32:00")); + assertEquals("t1", false, testArg("18-Feb-1993")); + assertEquals("t1", false, testArg("nn/nn/nn")); + } + + boolean testArg(String arg) { + try { + FitsDate fd = new FitsDate(arg); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/nom/tam/fits/test/HeaderCardTest.java b/src/nom/tam/fits/test/HeaderCardTest.java new file mode 100644 index 0000000..e6dced1 --- /dev/null +++ b/src/nom/tam/fits/test/HeaderCardTest.java @@ -0,0 +1,159 @@ +package nom.tam.fits.test; + +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import junit.framework.JUnit4TestAdapter; +import nom.tam.fits.HeaderCard; +import nom.tam.fits.FitsFactory; + +public class HeaderCardTest { + + @Test + public void test1() throws Exception { + + HeaderCard p; + p = new HeaderCard("SIMPLE = T"); + + assertEquals("t1", "SIMPLE", p.getKey()); + assertEquals("t2", "T", p.getValue()); + assertNull("t3", p.getComment()); + + p = new HeaderCard("VALUE = 123"); + assertEquals("t4", "VALUE", p.getKey()); + assertEquals("t5", "123", p.getValue()); + assertNull("t3", p.getComment()); + + p = new HeaderCard("VALUE = 1.23698789798798E23 / Comment "); + assertEquals("t6", "VALUE", p.getKey()); + assertEquals("t7", "1.23698789798798E23", p.getValue()); + assertEquals("t8", "Comment", p.getComment()); + + String lng = "111111111111111111111111111111111111111111111111111111111111111111111111"; + p = new HeaderCard("COMMENT " + lng); + assertEquals("t9", "COMMENT", p.getKey()); + assertNull("t10", p.getValue()); + assertEquals("t11", lng, p.getComment()); + + boolean thrown = false; + try { + // + p = new HeaderCard("VALUE = ' "); + } catch (Exception e) { + thrown = true; + } + assertEquals("t12", true, thrown); + + + p = new HeaderCard("COMMENT " + lng + lng); + assertEquals("t13", lng, p.getComment()); + + HeaderCard z = new HeaderCard("TTTT", 1.234567891234567891234567e101, "a comment"); + assertTrue("t14", z.toString().indexOf("E") > 0); + } + + @Test + public void test3() throws Exception { + + HeaderCard p = new HeaderCard("KEY", "VALUE", "COMMENT"); + assertEquals("x1", + "KEY = 'VALUE ' / COMMENT ", + p.toString()); + + p = new HeaderCard("KEY", 123, "COMMENT"); + assertEquals("x2", + "KEY = 123 / COMMENT ", + p.toString()); + p = new HeaderCard("KEY", 1.23, "COMMENT"); + assertEquals("x3", + "KEY = 1.23 / COMMENT ", + p.toString()); + p = new HeaderCard("KEY", true, "COMMENT"); + assertEquals("x4", + "KEY = T / COMMENT ", + p.toString()); + + + boolean thrown = false; + try { + p = new HeaderCard("LONGKEYWORD", 123, "COMMENT"); + } catch (Exception e) { + thrown = true; + } + assertEquals("x5", true, thrown); + + thrown = false; + String lng = "00000000001111111111222222222233333333334444444444555555555566666666667777777777"; + try { + p = new HeaderCard("KEY", lng, "COMMENT"); + } catch (Exception e) { + thrown = true; + } + assertEquals("x6", true, thrown); + + + // Only trailing spaces are stripped. + p = new HeaderCard("STRING", "VALUE", null); + assertEquals("x6", "VALUE", p.getValue()); + + p = new HeaderCard("STRING", "VALUE ", null); + assertEquals("x7", "VALUE", p.getValue()); + + p = new HeaderCard("STRING", " VALUE", null); + assertEquals("x8", " VALUE", p.getValue()); + + p = new HeaderCard("STRING", " VALUE ", null); + assertEquals("x9", " VALUE", p.getValue()); + + p = new HeaderCard("QUOTES", "ABC'DEF", null); + assertEquals("x10", "ABC'DEF", p.getValue()); + assertEquals("x10b", p.toString().indexOf("''") > 0, true); + + p = new HeaderCard("QUOTES", "ABC''DEF", null); + assertEquals("x11", "ABC''DEF", p.getValue()); + assertEquals("x10b", p.toString().indexOf("''''") > 0, true); + } + + @Test + public void testHierarch() throws Exception { + + HeaderCard hc; + String key = "HIERARCH.TEST1.TEST2.INT"; + boolean thrown = false; + try { + hc = new HeaderCard(key, 123, "Comment"); + } catch (Exception e) { + thrown = true; + } + assertEquals("h1", true, thrown); + + String card = "HIERARCH TEST1 TEST2 INT= 123 / Comment "; + hc = new HeaderCard(card); + assertEquals("h2", "HIERARCH", hc.getKey()); + assertNull("h3", hc.getValue()); + assertEquals("h4", "TEST1 TEST2 INT= 123 / Comment", hc.getComment()); + + FitsFactory.setUseHierarch(true); + + hc = new HeaderCard(key, 123, "Comment"); + + assertEquals("h5", key, hc.getKey()); + assertEquals("h6", "123", hc.getValue()); + assertEquals("h7", "Comment", hc.getComment()); + + hc = new HeaderCard(card); + assertEquals("h8", key, hc.getKey()); + assertEquals("h9", "123", hc.getValue()); + assertEquals("h10", "Comment", hc.getComment()); + } + + @Test + public void testLongDoubles() throws Exception { + // Check to see if we make long double values + // fit in the recommended space. + HeaderCard hc = new HeaderCard("TEST", -1.234567890123456789e-123, "dummy"); + String val = hc.getValue(); + assertEquals("tld1", val.length(), 20); + } +} diff --git a/src/nom/tam/fits/test/HeaderTest.java b/src/nom/tam/fits/test/HeaderTest.java new file mode 100644 index 0000000..ea915ff --- /dev/null +++ b/src/nom/tam/fits/test/HeaderTest.java @@ -0,0 +1,296 @@ +package nom.tam.fits.test; + +import nom.tam.fits.*; +import nom.tam.util.*; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +public class HeaderTest { + + /** Check out header manipulation. + */ + @Test + public void simpleImages() throws Exception { + float[][] img = new float[300][300]; + + Fits f = new Fits(); + + ImageHDU hdu = (ImageHDU) Fits.makeHDU(img); + BufferedFile bf = new BufferedFile("ht1.fits", "rw"); + f.addHDU(hdu); + f.write(bf); + bf.close(); + + f = new Fits("ht1.fits"); + hdu = (ImageHDU) f.getHDU(0); + Header hdr = hdu.getHeader(); + + assertEquals("NAXIS", 2, hdr.getIntValue("NAXIS")); + assertEquals("NAXIS1", 300, hdr.getIntValue("NAXIS1")); + assertEquals("NAXIS2", 300, hdr.getIntValue("NAXIS2")); + assertEquals("NAXIS2a", 300, hdr.getIntValue("NAXIS2", -1)); + assertEquals("NAXIS3", -1, hdr.getIntValue("NAXIS3", -1)); + + assertEquals("BITPIX", -32, hdr.getIntValue("BITPIX")); + + + Cursor c = hdr.iterator(); + HeaderCard hc = (HeaderCard) c.next(); + assertEquals("SIMPLE_1", "SIMPLE", hc.getKey()); + + hc = (HeaderCard) c.next(); + assertEquals("BITPIX_2", "BITPIX", hc.getKey()); + + hc = (HeaderCard) c.next(); + assertEquals("NAXIS_3", "NAXIS", hc.getKey()); + + hc = (HeaderCard) c.next(); + assertEquals("NAXIS1_4", "NAXIS1", hc.getKey()); + + hc = (HeaderCard) c.next(); + assertEquals("NAXIS2_5", "NAXIS2", hc.getKey()); + } + + /** Confirm initial location versus EXTEND keyword (V. Forchi). */ + @Test + public void extendTest() throws Exception { + Fits f = new Fits("ht1.fits"); + Header h = f.getHDU(0).getHeader(); + h.addValue("TESTKEY", "TESTVAL", "TESTCOMM"); + h.rewrite(); + f.getStream().close(); + f = new Fits("ht1.fits"); + h = f.getHDU(0).getHeader(); + + // We should be pointed after the EXTEND and before TESTKEY + h.addValue("TESTKEY2", "TESTVAL2", null); // Should precede TESTKEY + + Cursor c = h.iterator(); + assertEquals("E1", ((HeaderCard) c.next()).getKey(), "SIMPLE"); + assertEquals("E2", ((HeaderCard) c.next()).getKey(), "BITPIX"); + assertEquals("E3", ((HeaderCard) c.next()).getKey(), "NAXIS"); + assertEquals("E4", ((HeaderCard) c.next()).getKey(), "NAXIS1"); + assertEquals("E5", ((HeaderCard) c.next()).getKey(), "NAXIS2"); + assertEquals("E6", ((HeaderCard) c.next()).getKey(), "EXTEND"); + assertEquals("E7", ((HeaderCard) c.next()).getKey(), "TESTKEY2"); + assertEquals("E8", ((HeaderCard) c.next()).getKey(), "TESTKEY"); + + } + + @Test + public void cursorTest() throws Exception { + + Fits f = new Fits("ht1.fits"); + ImageHDU hdu = (ImageHDU) f.getHDU(0); + Header hdr = hdu.getHeader(); + Cursor c = hdr.iterator(); + + + c.setKey("XXX"); + c.add("CTYPE1", new HeaderCard("CTYPE1", "GLON-CAR", "Galactic Longitude")); + c.add("CTYPE2", new HeaderCard("CTYPE2", "GLAT-CAR", "Galactic Latitude")); + c.setKey("CTYPE1"); // Move before CTYPE1 + c.add("CRVAL1", new HeaderCard("CRVAL1", 0., "Longitude at reference")); + c.setKey("CTYPE2"); // Move before CTYPE2 + c.add("CRVAL2", new HeaderCard("CRVAL2", -90., "Latitude at reference")); + c.setKey("CTYPE1"); // Just practicing moving around!! + c.add("CRPIX1", new HeaderCard("CRPIX1", 150.0, "Reference Pixel X")); + c.setKey("CTYPE2"); + c.add("CRPIX2", new HeaderCard("CRPIX2", 0., "Reference pixel Y")); + c.add("INV2", new HeaderCard("INV2", true, "Invertible axis")); + c.add("SYM2", new HeaderCard("SYM2", "YZ SYMMETRIC", "Symmetries...")); + + assertEquals("CTYPE1", "GLON-CAR", hdr.getStringValue("CTYPE1")); + assertEquals("CRPIX2", 0., hdr.getDoubleValue("CRPIX2", -2.), 0); + + c.setKey("CRVAL1"); + HeaderCard hc = (HeaderCard) c.next(); + assertEquals("CRVAL1_c", "CRVAL1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("CRPIX1_c", "CRPIX1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("CTYPE1_c", "CTYPE1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("CRVAL2_c", "CRVAL2", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("CRPIX2_c", "CRPIX2", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("INV2_c", "INV2", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("SYM2_c", "SYM2", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("CTYPE2_c", "CTYPE2", hc.getKey()); + + hdr.findCard("CRPIX1"); + hdr.addValue("INTVAL1", 1, "An integer value"); + hdr.addValue("LOG1", true, "A true value"); + hdr.addValue("LOGB1", false, "A false value"); + hdr.addValue("FLT1", 1.34, "A float value"); + hdr.addValue("FLT2", -1.234567890e-134, "A very long float"); + hdr.insertComment("Comment after flt2"); + + c.setKey("INTVAL1"); + hc = (HeaderCard) c.next(); + assertEquals("INTVAL1", "INTVAL1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("LOG1", "LOG1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("LOGB1", "LOGB1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("FLT1", "FLT1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("FLT2", "FLT2", hc.getKey()); + c.next(); // Skip comment + hc = (HeaderCard) c.next(); + assertEquals("CRPIX1x", "CRPIX1", hc.getKey()); + + assertEquals("FLT1", 1.34, hdr.getDoubleValue("FLT1", 0), 0); + c.setKey("FLT1"); + c.next(); + c.remove(); + assertEquals("FLT1", 0., hdr.getDoubleValue("FLT1", 0), 0); + c.setKey("LOGB1"); + hc = (HeaderCard) c.next(); + assertEquals("AftDel1", "LOGB1", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("AftDel2", "FLT2", hc.getKey()); + hc = (HeaderCard) c.next(); + assertEquals("AftDel3", "Comment after flt2", hc.getComment()); + } + + @Test + public void testBadHeader() throws Exception { + + Fits f = new Fits("ht1.fits"); + ImageHDU hdu = (ImageHDU) f.getHDU(0); + Header hdr = hdu.getHeader(); + Cursor c = hdr.iterator(); + + c = hdr.iterator(); + c.next(); + c.next(); + c.remove(); + boolean thrown = false; + try { + hdr.rewrite(); + } catch (Exception e) { + thrown = true; + } + assertEquals("BITPIX delete", true, thrown); + } + + @Test + public void testUpdateHeaderComments() throws Exception { + byte[][] z = new byte[4][4]; + Fits f = new Fits(); + f.addHDU(FitsFactory.HDUFactory(z)); + BufferedFile bf = new BufferedFile("hx1.fits", "rw"); + f.write(bf); + bf.close(); + f = new Fits("hx1.fits"); + HeaderCard c1 = f.getHDU(0).getHeader().findCard("SIMPLE"); + assertEquals("tuhc1", c1.getComment(), HeaderCommentsMap.getComment("header:simple:1")); + c1 = f.getHDU(0).getHeader().findCard("BITPIX"); + assertEquals("tuhc2", c1.getComment(), HeaderCommentsMap.getComment("header:bitpix:1")); + HeaderCommentsMap.updateComment("header:bitpix:1", "A byte array"); + HeaderCommentsMap.deleteComment("header:simple:1"); + f = new Fits(); + f.addHDU(FitsFactory.HDUFactory(z)); + bf = new BufferedFile("hx2.fits", "rw"); + f.write(bf); + bf.close(); + f = new Fits("hx2.fits"); + c1 = f.getHDU(0).getHeader().findCard("SIMPLE"); + assertEquals("tuhc1", c1.getComment(), null); + c1 = f.getHDU(0).getHeader().findCard("BITPIX"); + assertEquals("tuhc2", c1.getComment(), "A byte array"); + } + + @Test + public void testRewrite() throws Exception { + + // Should be rewriteable until we add enough cards to + // start a new block. + + Fits f = new Fits("ht1.fits"); + ImageHDU hdu = (ImageHDU) f.getHDU(0); + Header hdr = hdu.getHeader(); + Cursor c = hdr.iterator(); + + int nc = hdr.getNumberOfCards(); + int nb = (nc - 1) / 36; + + while (hdr.rewriteable()) { + int nbx = (hdr.getNumberOfCards() - 1) / 36; + assertEquals("Rewrite:" + nbx, nb == nbx, hdr.rewriteable()); + c.add(new HeaderCard("DUMMY" + nbx, null, null)); + } + } + + @Test + public void longStringTest() throws Exception { + + Header hdr = new Fits("ht1.fits").getHDU(0).getHeader(); + + String seq = "0123456789"; + String lng = ""; + for (int i = 0; i < 20; i += 1) { + lng += seq; + } + assertEquals("Initial state:", false, Header.getLongStringsEnabled()); + Header.setLongStringsEnabled(true); + assertEquals("Set state:", true, Header.getLongStringsEnabled()); + hdr.addValue("LONG1", lng, "Here is a comment"); + hdr.addValue("LONG2", "xx'yy'zz" + lng, "Another comment"); + hdr.addValue("SHORT", "A STRING ENDING IN A &", null); + hdr.addValue("LONGISH", lng + "&", null); + hdr.addValue("LONGSTRN", "OGIP 1.0", "Uses long strings"); + + String sixty = seq + seq + seq + seq + seq + seq; + hdr.addValue("APOS1", sixty + "''''''''''", "Should be 70 chars long"); + hdr.addValue("APOS2", sixty + " ''''''''''", "Should be 71 chars long"); + + // Now try to read the values back. + BufferedFile bf = new BufferedFile("ht4.hdr", "rw"); + hdr.write(bf); + bf.close(); + String val = hdr.getStringValue("LONG1"); + assertEquals("LongT1", val, lng); + val = hdr.getStringValue("LONG2"); + assertEquals("LongT2", val, "xx'yy'zz" + lng); + assertEquals("APOS1", hdr.getStringValue("APOS1").length(), 70); + assertEquals("APOS2", hdr.getStringValue("APOS2").length(), 71); + Header.setLongStringsEnabled(false); + val = hdr.getStringValue("LONG1"); + assertEquals("LongT3", true, !val.equals(lng)); + assertEquals("Longt4", true, val.length() <= 70); + assertEquals("longamp1", hdr.getStringValue("SHORT"), "A STRING ENDING IN A &"); + bf = new BufferedFile("ht4.hdr", "r"); + hdr = new Header(bf); + assertEquals("Set state2:", true, Header.getLongStringsEnabled()); + val = hdr.getStringValue("LONG1"); + assertEquals("LongT5", val, lng); + val = hdr.getStringValue("LONG2"); + assertEquals("LongT6", val, "xx'yy'zz" + lng); + assertEquals("longamp2", hdr.getStringValue("LONGISH"), lng + "&"); + assertEquals("APOS1b", hdr.getStringValue("APOS1").length(), 70); + assertEquals("APOS2b", hdr.getStringValue("APOS2").length(), 71); + assertEquals("APOS2c", hdr.getStringValue("APOS2"), sixty + " ''''''''''"); + assertEquals("longamp1b", hdr.getStringValue("SHORT"), "A STRING ENDING IN A &"); + assertEquals("longamp2b", hdr.getStringValue("LONGISH"), lng + "&"); + + int cnt = hdr.getNumberOfCards(); + // This should remove all three cards associated with + // LONG1 + hdr.removeCard("LONG1"); + assertEquals("deltest", cnt - 3, hdr.getNumberOfCards()); + Header.setLongStringsEnabled(false); + // With long strings disabled this should only remove one more card. + hdr.removeCard("LONG2"); + assertEquals("deltest2", cnt - 4, hdr.getNumberOfCards()); + + } +} + diff --git a/src/nom/tam/fits/test/ImageTest.java b/src/nom/tam/fits/test/ImageTest.java new file mode 100644 index 0000000..47e339d --- /dev/null +++ b/src/nom/tam/fits/test/ImageTest.java @@ -0,0 +1,130 @@ +package nom.tam.fits.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.image.*; +import nom.tam.util.*; +import nom.tam.fits.*; + +import java.io.File; + +/** Test the ImageHDU, ImageData and ImageTiler classes. + * - multiple HDU's in a single file + * - deferred input of HDUs + * - creating and reading arrays of all permitted types. + * - Tiles of 1, 2 and 3 dimensions + * - from a file + * - from internal data + * - Multiple tiles extracted from an image. + */ +public class ImageTest { + + @Test + public void test() throws Exception { + + Fits f = new Fits(); + + byte[][] bimg = new byte[40][40]; + for (int i = 10; i < 30; i += 1) { + for (int j = 10; j < 30; j += 1) { + bimg[i][j] = (byte) (i + j); + } + } + + short[][] simg = (short[][]) ArrayFuncs.convertArray(bimg, short.class); + int[][] iimg = (int[][]) ArrayFuncs.convertArray(bimg, int.class); + long[][] limg = (long[][]) ArrayFuncs.convertArray(bimg, long.class); + float[][] fimg = (float[][]) ArrayFuncs.convertArray(bimg, float.class); + double[][] dimg = (double[][]) ArrayFuncs.convertArray(bimg, double.class); + int[][][] img3 = new int[10][20][30]; + for (int i = 0; i < 10; i += 1) { + for (int j = 0; j < 20; j += 1) { + for (int k = 0; k < 30; k += 1) { + img3[i][j][k] = i + j + k; + } + } + } + + double[] img1 = (double[]) ArrayFuncs.flatten(dimg); + + // Make HDUs of various types. + f.addHDU(Fits.makeHDU(bimg)); + f.addHDU(Fits.makeHDU(simg)); + f.addHDU(Fits.makeHDU(iimg)); + f.addHDU(Fits.makeHDU(limg)); + f.addHDU(Fits.makeHDU(fimg)); + f.addHDU(Fits.makeHDU(dimg)); + f.addHDU(Fits.makeHDU(img3)); + f.addHDU(Fits.makeHDU(img1)); + + assertEquals("HDU count before", f.getNumberOfHDUs(), 8); + + + // Write a FITS file. + + BufferedFile bf = new BufferedFile("image1.fits", "rw"); + f.write(bf); + bf.flush(); + bf.close(); + bf = null; + + + f = null; + + bf = new BufferedFile("image1.fits"); + + // Read a FITS file + f = new Fits("image1.fits"); + BasicHDU[] hdus = f.read(); + + assertEquals("HDU count after", f.getNumberOfHDUs(), 8); + assertEquals("byte image", true, ArrayFuncs.arrayEquals(bimg, hdus[0].getData().getKernel())); + assertEquals("short image", true, ArrayFuncs.arrayEquals(simg, hdus[1].getData().getKernel())); + assertEquals("int image", true, ArrayFuncs.arrayEquals(iimg, hdus[2].getData().getKernel())); + assertEquals("long image", true, ArrayFuncs.arrayEquals(limg, hdus[3].getData().getKernel())); + assertEquals("float image", true, ArrayFuncs.arrayEquals(fimg, hdus[4].getData().getKernel())); + assertEquals("double image", true, ArrayFuncs.arrayEquals(dimg, hdus[5].getData().getKernel())); + assertEquals("int3 image", true, ArrayFuncs.arrayEquals(img3, hdus[6].getData().getKernel())); + assertEquals("double1 image", true, ArrayFuncs.arrayEquals(img1, hdus[7].getData().getKernel())); + } + + @Test + public void fileTest() throws Exception { + + byte[][] bimg = new byte[40][40]; + for (int i = 10; i < 30; i += 1) { + for (int j = 10; j < 30; j += 1) { + bimg[i][j] = (byte) (i + j); + } + } + + short[][] simg = (short[][]) ArrayFuncs.convertArray(bimg, short.class); + int[][] iimg = (int[][]) ArrayFuncs.convertArray(bimg, int.class); + long[][] limg = (long[][]) ArrayFuncs.convertArray(bimg, long.class); + float[][] fimg = (float[][]) ArrayFuncs.convertArray(bimg, float.class); + double[][] dimg = (double[][]) ArrayFuncs.convertArray(bimg, double.class); + int[][][] img3 = new int[10][20][30]; + for (int i = 0; i < 10; i += 1) { + for (int j = 0; j < 20; j += 1) { + for (int k = 0; k < 30; k += 1) { + img3[i][j][k] = i + j + k; + } + } + } + double[] img1 = (double[]) ArrayFuncs.flatten(dimg); + + Fits f = new Fits(new File("image1.fits")); + BasicHDU[] hdus = f.read(); + + assertEquals("fbyte image", true, ArrayFuncs.arrayEquals(bimg, hdus[0].getData().getKernel())); + assertEquals("fshort image", true, ArrayFuncs.arrayEquals(simg, hdus[1].getData().getKernel())); + assertEquals("fint image", true, ArrayFuncs.arrayEquals(iimg, hdus[2].getData().getKernel())); + assertEquals("flong image", true, ArrayFuncs.arrayEquals(limg, hdus[3].getData().getKernel())); + assertEquals("ffloat image", true, ArrayFuncs.arrayEquals(fimg, hdus[4].getData().getKernel())); + assertEquals("fdouble image", true, ArrayFuncs.arrayEquals(dimg, hdus[5].getData().getKernel())); + assertEquals("fint3 image", true, ArrayFuncs.arrayEquals(img3, hdus[6].getData().getKernel())); + assertEquals("fdouble1 image", true, ArrayFuncs.arrayEquals(img1, hdus[7].getData().getKernel())); + } +} diff --git a/src/nom/tam/fits/test/PaddingTest.java b/src/nom/tam/fits/test/PaddingTest.java new file mode 100644 index 0000000..2a07a5e --- /dev/null +++ b/src/nom/tam/fits/test/PaddingTest.java @@ -0,0 +1,155 @@ +package nom.tam.fits.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.image.*; +import nom.tam.util.*; +import nom.tam.fits.*; + +import java.io.File; + +/** Test that we can read files that fail due to lack of padding in the final HDU. + */ +public class PaddingTest { + + @Test + public void test1() throws Exception { + + Fits f = new Fits(); + + byte[][] bimg = new byte[20][20]; + for (int i = 0; i < 20; i += 1) { + for (int j = 0; j < 20; j += 1) { + bimg[i][j] = (byte) (i + j); + } + } + + BasicHDU hdu = Fits.makeHDU(bimg); + Header hdr = hdu.getHeader(); + hdr.addValue("NEWKEY", "TESTVALUE", "Test keyword"); + BufferedFile bf = new BufferedFile("padding1.fits", "rw"); + hdr.write(bf); + bf.writeArray(bimg); // The data but no following padding. + bf.flush(); + bf.close(); + + // Now try reading this back. + f = new Fits("padding1.fits"); + + try { + f.read(); + } catch (PaddingException e) { + assertEquals("HDUCount", 0, f.getNumberOfHDUs()); + f.addHDU(e.getTruncatedHDU()); + assertEquals("HDUCount2", 1, f.getNumberOfHDUs()); + } + + + ImageHDU hdu0 = (ImageHDU) (f.getHDU(0)); + byte[][] aa = (byte[][]) hdu0.getKernel(); + int miss = 0; + int match = 0; + for (int i = 0; i < 20; i += 1) { + for (int j = 0; j < 20; j += 1) { + if (aa[i][j] != (byte) (i + j)) { + miss += 1; + } else { + match += 1; + } + } + } + assertEquals("PadMiss1:", miss, 0); + assertEquals("PadMatch1:", match, 400); + // Make sure we got the real header and not the one generated strictly from the data. + assertEquals("Update header:", hdu0.getHeader().getStringValue("NEWKEY"), "TESTVALUE"); + + + nom.tam.image.ImageTiler it = hdu0.getTiler(); + + // Remember that the tile is always a flattened + // 1-D representation of the data. + byte[] data = (byte[]) it.getTile(new int[]{2, 2}, new int[]{2, 2}); + + assertEquals("tilet1:", data.length, 4); + assertEquals("tilet2:", data[0] + 0, 4); + assertEquals("tilet3:", data[1] + 0, 5); + assertEquals("tilet4:", data[2] + 0, 5); + assertEquals("tilet5:", data[3] + 0, 6); + + } + + @Test + public void test2() throws Exception { + + Fits f = new Fits(); + + byte[][] bimg = new byte[20][20]; + for (int i = 0; i < 20; i += 1) { + for (int j = 0; j < 20; j += 1) { + bimg[i][j] = (byte) (i + j); + } + } + + BasicHDU hdu = Fits.makeHDU(bimg); + f.addHDU(hdu); + + // First create a FITS file with a truncated second HDU. + BufferedFile bf = new BufferedFile("padding2.fits", "rw"); + f.write(bf); + + hdu.getHeader().setXtension("IMAGE"); + Cursor curs = hdu.getHeader().iterator(); + int cnt = 0; + // Write the header + while (curs.hasNext()) { + bf.write(((HeaderCard) curs.next()).toString().getBytes()); + cnt += 1; + } + + // The padding between header and data + byte[] b = new byte[(36 - cnt) * 80]; // Assuming fewer than 36 cards. + for (int i = 0; i < b.length; i += 1) { + b[i] = 32; // i.e., a blank + } + bf.write(b); + for (int i = 0; i < 20; i += 1) { + for (int j = 0; j < 20; j += 1) { + bimg[i][j] = (byte) (2 * (i + j)); + } + } + bf.writeArray(bimg); // The data but no following padding. + bf.flush(); + bf.close(); + + // Now try reading this back. + f = new Fits("padding2.fits"); + + try { + f.read(); + } catch (PaddingException e) { + assertEquals("HDUCount", 1, f.getNumberOfHDUs()); + f.addHDU(e.getTruncatedHDU()); + assertEquals("HDUCount2", 2, f.getNumberOfHDUs()); + } + + ImageHDU hdu0 = (ImageHDU) (f.getHDU(0)); + ImageHDU hdu1 = (ImageHDU) (f.getHDU(1)); + byte[][] aa = (byte[][]) hdu0.getKernel(); + byte[][] bb = (byte[][]) hdu1.getKernel(); + int miss = 0; + int match = 0; + for (int i = 0; i < 20; i += 1) { + for (int j = 0; j < 20; j += 1) { + if (bb[i][j] != (byte) (2 * aa[i][j])) { + miss += 1; + } else { + match += 1; + } + } + } + assertEquals("PadMiss2:", miss, 0); + assertEquals("PadMatch2:", match, 400); + } +} diff --git a/src/nom/tam/fits/test/RandomGroupsTest.java b/src/nom/tam/fits/test/RandomGroupsTest.java new file mode 100644 index 0000000..5b76470 --- /dev/null +++ b/src/nom/tam/fits/test/RandomGroupsTest.java @@ -0,0 +1,96 @@ +package nom.tam.fits.test; + +import nom.tam.util.*; +import nom.tam.fits.*; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +/** Test random groups formats in FITS data. + * Write and read random groups data + */ +public class RandomGroupsTest { + + @Test + public void test() throws Exception { + + float[][] fa = new float[20][20]; + float[] pa = new float[3]; + + BufferedFile bf = new BufferedFile("rg1.fits", "rw"); + + Object[][] data = new Object[1][2]; + data[0][0] = pa; + data[0][1] = fa; + + // First lets write out the file painfully group by group. + BasicHDU hdu = Fits.makeHDU(data); + Header hdr = hdu.getHeader(); + // Change the number of groups + hdr.addValue("GCOUNT", 20, "Number of groups"); + hdr.write(bf); + + for (int i = 0; i < 20; i += 1) { + + for (int j = 0; j < pa.length; j += 1) { + pa[j] = i + j; + } + for (int j = 0; j < fa.length; j += 1) { + fa[j][j] = i * j; + } + // Write a group + bf.writeArray(data); + } + + byte[] padding = new byte[FitsUtil.padding(20 * ArrayFuncs.computeLSize(data))]; + bf.write(padding); + + bf.flush(); + bf.close(); + + // Read back the data. + Fits f = new Fits("rg1.fits"); + BasicHDU[] hdus = f.read(); + + data = (Object[][]) hdus[0].getKernel(); + + for (int i = 0; i < data.length; i += 1) { + + pa = (float[]) data[i][0]; + fa = (float[][]) data[i][1]; + for (int j = 0; j < pa.length; j += 1) { + assertEquals("paramTest:" + i + " " + j, (float) (i + j), pa[j], 0); + } + for (int j = 0; j < fa.length; j += 1) { + assertEquals("dataTest:" + i + " " + j, (float) (i * j), fa[j][j], 0); + } + } + + // Now do it in one fell swoop -- but we have to have + // all the data in place first. + f = new Fits(); + + // Generate a FITS HDU from the kernel. + f.addHDU(Fits.makeHDU(data)); + bf = new BufferedFile("rg2.fits", "rw"); + f.write(bf); + + bf.flush(); + bf.close(); + + f = new Fits("rg2.fits"); + data = (Object[][]) f.read()[0].getKernel(); + for (int i = 0; i < data.length; i += 1) { + + pa = (float[]) data[i][0]; + fa = (float[][]) data[i][1]; + for (int j = 0; j < pa.length; j += 1) { + assertEquals("paramTest:" + i + " " + j, (float) (i + j), pa[j], 0); + } + for (int j = 0; j < fa.length; j += 1) { + assertEquals("dataTest:" + i + " " + j, (float) (i * j), fa[j][j], 0); + } + } + } +} diff --git a/src/nom/tam/fits/test/TilerTest.java b/src/nom/tam/fits/test/TilerTest.java new file mode 100644 index 0000000..1566320 --- /dev/null +++ b/src/nom/tam/fits/test/TilerTest.java @@ -0,0 +1,76 @@ +package nom.tam.fits.test; + +import nom.tam.fits.*; +import nom.tam.util.*; +import nom.tam.image.ImageTiler; +import java.io.*; + + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +/** This class tests the ImageTiler. It + * first creates a FITS file and then reads + * it back and allows the user to select + * tiles. The values of the corner and center + * pixels for the selected tile are displayed. + * Both file and memory tiles are checked. + */ +public class TilerTest { + + void doTile(String test, + float[][] data, + ImageTiler t, + int x, int y, int nx, int ny) + throws Exception { + + float[] tile = new float[nx * ny]; + t.getTile(tile, new int[]{y, x}, new int[]{ny, nx}); + + + float sum0 = 0; + float sum1 = 0; + + for (int i = 0; i < nx; i += 1) { + for (int j = 0; j < ny; j += 1) { + sum0 += tile[i + j * nx]; + sum1 += data[j + y][i + x]; + } + } + + assertEquals("Tiler" + test, sum0, sum1, 0); + } + + @Test + public void test() throws Exception { + + float[][] data = new float[300][300]; + + for (int i = 0; i < 300; i += 1) { + for (int j = 0; j < 300; j += 1) { + data[i][j] = 1000 * i + j; + } + } + + Fits f = new Fits(); + + BufferedFile bf = new BufferedFile("tiler1.fits", "rw"); + f.addHDU(Fits.makeHDU(data)); + + f.write(bf); + bf.close(); + + f = new Fits("tiler1.fits"); + + ImageHDU h = (ImageHDU) f.readHDU(); + + ImageTiler t = h.getTiler(); + doTile("t1", data, t, 200, 200, 50, 50); + doTile("t2", data, t, 133, 133, 72, 26); + + Object o = h.getData().getKernel(); + doTile("t3", data, t, 200, 200, 50, 50); + doTile("t4", data, t, 133, 133, 72, 26); + } +} diff --git a/src/nom/tam/fits/test/test.fits b/src/nom/tam/fits/test/test.fits new file mode 100644 index 0000000..40067b3 Binary files /dev/null and b/src/nom/tam/fits/test/test.fits differ diff --git a/src/nom/tam/fits/test/test.fits.Z b/src/nom/tam/fits/test/test.fits.Z new file mode 100644 index 0000000..3b86474 Binary files /dev/null and b/src/nom/tam/fits/test/test.fits.Z differ diff --git a/src/nom/tam/fits/test/test.fits.bz2 b/src/nom/tam/fits/test/test.fits.bz2 new file mode 100644 index 0000000..4da6093 Binary files /dev/null and b/src/nom/tam/fits/test/test.fits.bz2 differ diff --git a/src/nom/tam/fits/test/test.fits.gz b/src/nom/tam/fits/test/test.fits.gz new file mode 100644 index 0000000..f9f56b2 Binary files /dev/null and b/src/nom/tam/fits/test/test.fits.gz differ diff --git a/src/nom/tam/fits/utilities/FitsCopy.java b/src/nom/tam/fits/utilities/FitsCopy.java new file mode 100644 index 0000000..57ec7c9 --- /dev/null +++ b/src/nom/tam/fits/utilities/FitsCopy.java @@ -0,0 +1,33 @@ +package nom.tam.fits.utilities; + +import nom.tam.fits.*; +import nom.tam.util.*; + +public class FitsCopy { + + public static void main(String[] args) throws Exception { + + String file = args[0]; + + Fits f = new Fits(file); + int i = 0; + BasicHDU h; + + do { + h = f.readHDU(); + if (h != null) { + if (i == 0) { + System.out.println("\n\nPrimary header:\n"); + } else { + System.out.println("\n\nExtension " + i + ":\n"); + } + i += 1; + h.info(); + } + } while (h != null); + BufferedFile bf = new BufferedFile(args[1], "rw"); + f.write(bf); + bf.close(); + + } +} diff --git a/src/nom/tam/fits/utilities/FitsReader.java b/src/nom/tam/fits/utilities/FitsReader.java new file mode 100644 index 0000000..742e713 --- /dev/null +++ b/src/nom/tam/fits/utilities/FitsReader.java @@ -0,0 +1,29 @@ +package nom.tam.fits.utilities; + +import nom.tam.fits.*; + +public class FitsReader { + + public static void main(String[] args) throws Exception { + + String file = args[0]; + + Fits f = new Fits(file); + int i = 0; + BasicHDU h; + + do { + h = f.readHDU(); + if (h != null) { + if (i == 0) { + System.out.println("\n\nPrimary header:\n"); + } else { + System.out.println("\n\nExtension " + i + ":\n"); + } + i += 1; + h.info(); + } + } while (h != null); + + } +} diff --git a/src/nom/tam/image/ImageTiler.java b/src/nom/tam/image/ImageTiler.java new file mode 100644 index 0000000..a7c3500 --- /dev/null +++ b/src/nom/tam/image/ImageTiler.java @@ -0,0 +1,310 @@ +package nom.tam.image; + +import nom.tam.util.*; +import java.lang.reflect.Array; +import java.io.IOException; + +/** This class provides a subset of an N-dimensional image. + * Modified May 2, 2000 by T. McGlynn to permit + * tiles that go off the edge of the image. + */ +public class ImageTiler { + + RandomAccess f; + long fileOffset; + int[] dims; + Class base; + + /** Create a tiler. + * @param f The random access device from which image data may be read. + * This may be null if the tile information is available from + * memory. + * @param fileOffset The file offset within the RandomAccess device at which + * the data begins. + * @param dims The actual dimensions of the image. + * @param base The base class (should be a primitive type) of the image. + */ + public ImageTiler(RandomAccess f, long fileOffset, int[] dims, + Class base) { + this.f = f; + this.fileOffset = fileOffset; + this.dims = dims; + this.base = base; + } + + /** See if we can get the image data from memory. + * This may be overriden by other classes, notably + * in nom.tam.fits.ImageData. + */ + public Object getMemoryImage() { + return null; + } + + /** Get a subset of the image. An image tile is returned + * as a one-dimensional array although the image will + * normally be multi-dimensional. + * @param The starting corner (using 0 as the start) for the image. + * @param The length requested in each dimension. + */ + public Object getTile(int[] corners, int[] lengths) throws IOException { + + if (corners.length != dims.length || lengths.length != dims.length) { + throw new IOException("Inconsistent sub-image request"); + } + + int arraySize = 1; + for (int i = 0; i < dims.length; i += 1) { + + if (corners[i] < 0 || lengths[i] < 0 || corners[i] + lengths[i] > dims[i]) { + throw new IOException("Sub-image not within image"); + } + + arraySize *= lengths[i]; + } + + Object outArray = ArrayFuncs.newInstance(base, arraySize); + + getTile(outArray, corners, lengths); + return outArray; + } + + /** Get a tile, filling in a prespecified array. + * This version does not check that the user hase + * entered a valid set of corner and length arrays. + * ensure that out matches the + * length implied by the lengths array. + * + * @param outArray The output tile array. A one-dimensional + * array. + * Data not within the valid limits of the image will + * be left unchanged. The length of this + * array should be the product of lengths. + * @param corners The corners of the tile. + * @param lengths The dimensions of the tile. + * + */ + public void getTile(Object outArray, int[] corners, int[] lengths) + throws IOException { + + Object data = getMemoryImage(); + + if (data == null && f == null) { + throw new IOException("No data source for tile subset"); + } + fillTile(data, outArray, dims, corners, lengths); + } + + /** Fill the subset. + * @param data The memory-resident data image. + * This may be null if the image is to + * be read from a file. This should + * be a multi-dimensional primitive array. + * @param o The tile to be filled. This is a + * simple primitive array. + * @param dims The dimensions of the full image. + * @param corners The indices of the corner of the image. + * @param lengths The dimensions of the subset. + */ + protected void fillTile(Object data, Object o, int[] dims, int[] corners, int[] lengths) + throws IOException { + + + int n = dims.length; + int[] posits = new int[n]; + int baseLength = ArrayFuncs.getBaseLength(o); + int segment = lengths[n - 1]; + + System.arraycopy(corners, 0, posits, 0, n); + long currentOffset = 0; + if (data == null) { + currentOffset = f.getFilePointer(); + } + + int outputOffset = 0; + + + do { + + // This implies there is some overlap + // in the last index (in conjunction + // with other tests) + + int mx = dims.length - 1; + boolean validSegment = + posits[mx] + lengths[mx] >= 0 + && posits[mx] < dims[mx]; + + + // Don't do anything for the current + // segment if anything but the + // last index is out of range. + + if (validSegment) { + for (int i = 0; i < mx; i += 1) { + if (posits[i] < 0 || posits[i] >= dims[i]) { + validSegment = false; + break; + } + } + } + + if (validSegment) { + if (data != null) { + fillMemData(data, posits, segment, o, outputOffset, 0); + } else { + int offset = getOffset(dims, posits) * baseLength; + + // Point to offset at real beginning + // of segment + int actualLen = segment; + int actualOffset = offset; + int actualOutput = outputOffset; + if (posits[mx] < 0) { + actualOffset -= posits[mx] * baseLength; + actualOutput -= posits[mx]; + actualLen += posits[mx]; + } + if (posits[mx] + segment > dims[mx]) { + actualLen -= posits[mx] + segment - dims[mx]; + } + fillFileData(o, actualOffset, actualOutput, actualLen); + } + } + outputOffset += segment; + + } while (incrementPosition(corners, posits, lengths)); + if (data == null) { + f.seek(currentOffset); + } + } + + /** Fill a single segment from memory. + * This routine is called recursively to handle multi-dimensional + * arrays. E.g., if data is three-dimensional, this will + * recurse two levels until we get a call with a single dimensional + * datum. At that point the appropriate data will be copied + * into the output. + * + * @param data The in-memory image data. + * @param posits The current position for which data is requested. + * @param length The size of the segments. + * @param output The output tile. + * @param outputOffset The current offset into the output tile. + * @param dim The current dimension being + */ + protected void fillMemData(Object data, int[] posits, int length, + Object output, int outputOffset, int dim) { + + + if (data instanceof Object[]) { + + Object[] xo = (Object[]) data; + fillMemData(xo[posits[dim]], posits, length, output, outputOffset, dim + 1); + + } else { + + // Adjust the spacing for the actual copy. + int startFrom = posits[dim]; + int startTo = outputOffset; + int copyLength = length; + + if (posits[dim] < 0) { + startFrom -= posits[dim]; + startTo -= posits[dim]; + copyLength += posits[dim]; + } + if (posits[dim] + length > dims[dim]) { + copyLength -= (posits[dim] + length - dims[dim]); + } + + System.arraycopy(data, startFrom, output, startTo, copyLength); + } + } + + /** File a tile segment from a file. + * @param output The output tile. + * @param delta The offset from the beginning of the image in bytes. + * @param outputOffset The index into the output array. + * @param segment The number of elements to be read for this segment. + */ + protected void fillFileData(Object output, int delta, int outputOffset, + int segment) throws IOException { + + + f.seek(fileOffset + delta); + + if (base == float.class) { + f.read((float[]) output, outputOffset, segment); + } else if (base == int.class) { + f.read((int[]) output, outputOffset, segment); + } else if (base == short.class) { + f.read((short[]) output, outputOffset, segment); + } else if (base == double.class) { + f.read((double[]) output, outputOffset, segment); + } else if (base == byte.class) { + f.read((byte[]) output, outputOffset, segment); + } else if (base == char.class) { + f.read((char[]) output, outputOffset, segment); + } else if (base == long.class) { + f.read((long[]) output, outputOffset, segment); + } else { + throw new IOException("Invalid type for tile array"); + } + } + + /** Increment the offset within the position array. + * Note that we never look at the last index since + * we copy data a block at a time and not byte by byte. + * @param start The starting corner values. + * @param current The current offsets. + * @param lengths The desired dimensions of the subset. + */ + protected static boolean incrementPosition(int[] start, + int[] current, + int[] lengths) { + + for (int i = start.length - 2; i >= 0; i -= 1) { + if (current[i] - start[i] < lengths[i] - 1) { + current[i] += 1; + for (int j = i + 1; j < start.length - 1; j += 1) { + current[j] = start[j]; + } + return true; + } + } + return false; + } + + /** Get the offset of a given position. + * @param dims The dimensions of the array. + * @param pos The index requested. + */ + public static final int getOffset(int[] dims, int[] pos) { + + int offset = 0; + for (int i = 0; i < dims.length; i += 1) { + if (i > 0) { + offset *= dims[i]; + } + offset += pos[i]; + } + return offset; + } + + /** Read the entire image into a multidimensional + * array. + */ + public Object getCompleteImage() throws IOException { + + if (f == null) { + throw new IOException("Attempt to read from null file"); + } + long currentOffset = f.getFilePointer(); + Object o = ArrayFuncs.newInstance(base, dims); + f.seek(fileOffset); + f.readLArray(o); + f.seek(currentOffset); + return o; + } +} + diff --git a/src/nom/tam/util/ArrayDataInput.java b/src/nom/tam/util/ArrayDataInput.java new file mode 100644 index 0000000..394103c --- /dev/null +++ b/src/nom/tam/util/ArrayDataInput.java @@ -0,0 +1,65 @@ +package nom.tam.util; + +import java.io.IOException; + +public interface ArrayDataInput extends java.io.DataInput { + + /** Read a generic (possibly multidimenionsional) primitive array. + * An Object[] array is also a legal argument if each element + * of the array is a legal. + *

+ * The ArrayDataInput classes do not support String input since + * it is unclear how one would read in an Array of strings. + * @param o A [multidimensional] primitive (or Object) array. + * @deprecated See readLArray(Object o). + */ + public int readArray(Object o) throws IOException; + + /** Read an array. This version works even if the + * underlying data is more than 2 Gigabytes. + */ + public long readLArray(Object o) throws IOException; + + /* Read a complete primitive array */ + public int read(byte[] buf) throws IOException; + + public int read(boolean[] buf) throws IOException; + + public int read(short[] buf) throws IOException; + + public int read(char[] buf) throws IOException; + + public int read(int[] buf) throws IOException; + + public int read(long[] buf) throws IOException; + + public int read(float[] buf) throws IOException; + + public int read(double[] buf) throws IOException; + + /* Read a segment of a primitive array. */ + public int read(byte[] buf, int offset, int size) throws IOException; + + public int read(boolean[] buf, int offset, int size) throws IOException; + + public int read(char[] buf, int offset, int size) throws IOException; + + public int read(short[] buf, int offset, int size) throws IOException; + + public int read(int[] buf, int offset, int size) throws IOException; + + public int read(long[] buf, int offset, int size) throws IOException; + + public int read(float[] buf, int offset, int size) throws IOException; + + public int read(double[] buf, int offset, int size) throws IOException; + + /* Skip (forward) in a file */ + public long skip(long distance) throws IOException; + + /* Skip and require that the data be there. */ + public long skipBytes(long toSkip) throws IOException; + + /* Close the file. */ + public void close() throws IOException; +} diff --git a/src/nom/tam/util/ArrayDataOutput.java b/src/nom/tam/util/ArrayDataOutput.java new file mode 100644 index 0000000..5bc91f3 --- /dev/null +++ b/src/nom/tam/util/ArrayDataOutput.java @@ -0,0 +1,67 @@ +package nom.tam.util; + +import java.io.IOException; + +public interface ArrayDataOutput extends java.io.DataOutput { + + /** Write a generic (possibly multi-dimenionsional) primitive or String + * array. An array of Objects is also allowed if all + * of the elements are valid arrays. + *

+ * This routine is not called 'write' to avoid possible compilation + * errors in routines which define only some of the other methods + * of the interface (and defer to the superclass on others). + * In that case there is an ambiguity as to whether to + * call the routine in the current class but convert to + * Object, or call the method from the super class with + * the same type argument. + * @param o The primitive or String array to be written. + * @throws IOException if the argument is not of the proper type + */ + public void writeArray(Object o) throws IOException; + + /* Write a complete array */ + public void write(byte[] buf) throws IOException; + + public void write(boolean[] buf) throws IOException; + + public void write(short[] buf) throws IOException; + + public void write(char[] buf) throws IOException; + + public void write(int[] buf) throws IOException; + + public void write(long[] buf) throws IOException; + + public void write(float[] buf) throws IOException; + + public void write(double[] buf) throws IOException; + + /* Write an array of Strings */ + public void write(String[] buf) throws IOException; + + /* Write a segment of a primitive array. */ + public void write(byte[] buf, int offset, int size) throws IOException; + + public void write(boolean[] buf, int offset, int size) throws IOException; + + public void write(char[] buf, int offset, int size) throws IOException; + + public void write(short[] buf, int offset, int size) throws IOException; + + public void write(int[] buf, int offset, int size) throws IOException; + + public void write(long[] buf, int offset, int size) throws IOException; + + public void write(float[] buf, int offset, int size) throws IOException; + + public void write(double[] buf, int offset, int size) throws IOException; + + /* Write some of an array of Strings */ + public void write(String[] buf, int offset, int size) throws IOException; + + /* Flush the output buffer */ + public void flush() throws IOException; + + public void close() throws IOException; +} diff --git a/src/nom/tam/util/ArrayFuncs.java b/src/nom/tam/util/ArrayFuncs.java new file mode 100644 index 0000000..7e629d7 --- /dev/null +++ b/src/nom/tam/util/ArrayFuncs.java @@ -0,0 +1,1259 @@ +// Member of the utility package. +// Modified July 20, 2009 to handle very large arrays +// in some contexts. +package nom.tam.util; + +/* Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +import java.lang.reflect.*; +import java.util.Arrays; + +/** This is a package of static functions which perform + * computations on arrays. Generally these routines attempt + * to complete without throwing errors by ignoring data + * they cannot understand. + */ +public class ArrayFuncs implements PrimitiveInfo { + + /** Compute the size of an object. Note that this only handles + * arrays or scalars of the primitive objects and Strings. It + * returns 0 for any object array element it does not understand. + * + * @param o The object whose size is desired. + * @deprecated May silently underestimate the size + * if the size > 2 GB. + */ + public static int computeSize(Object o) { + return (int) computeLSize(o); + } + + public static long computeLSize(Object o) { + + if (o == null) { + return 0; + } + + long size = 0; + String classname = o.getClass().getName(); + if (classname.substring(0, 2).equals("[[")) { + + for (int i = 0; i < ((Object[]) o).length; i += 1) { + size += computeLSize(((Object[]) o)[i]); + } + return size; + } + + if (classname.charAt(0) == '[' && classname.charAt(1) != 'L') { + char c = classname.charAt(1); + + for (int i = 0; i < PrimitiveInfo.suffixes.length; i += 1) { + if (c == PrimitiveInfo.suffixes[i]) { + return (long) (Array.getLength(o)) * PrimitiveInfo.sizes[i]; + } + } + return 0; + } + + // Do we have a non-primitive array? + if (classname.charAt(0) == '[') { + int len = 0; + for (int i = 0; i < Array.getLength(o); i += 1) { + len += computeLSize(Array.get(o, i)); + } + return len; + } + + // Now a few special scalar objects. + if (classname.substring(0, 10).equals("java.lang.")) { + classname = classname.substring(10, classname.length()); + if (classname.equals("Integer") || classname.equals("Float")) { + return 4; + } else if (classname.equals("Double") || classname.equals("Long")) { + return 8; + } else if (classname.equals("Short") || classname.equals("Char")) { + return 2; + } else if (classname.equals("Byte") || classname.equals("Boolean")) { + return 1; + } else if (classname.equals("String")) { + return ((String) o).length(); + } else { + return 0; + } + } + return 0; + } + + /** Count the number of elements in an array. + * @deprecated May silently underestimate + * size if number is > 2 G. + */ + public static int nElements(Object o) { + return (int) nLElements(o); + } + + /** Count the number of elements in an array. + * @deprecated May silently underestimate + * size if number is > 2 G. + */ + public static long nLElements(Object o) { + + if (o == null) { + return 0; + } + + String classname = o.getClass().getName(); + if (classname.charAt(1) == '[') { + int count = 0; + for (int i = 0; i < ((Object[]) o).length; i += 1) { + count += nLElements(((Object[]) o)[i]); + } + return count; + + } else if (classname.charAt(0) == '[') { + return Array.getLength(o); + + } else { + return 1; + } + } + + /** Try to create a deep clone of an Array or a standard clone of a scalar. + * The object may comprise arrays of + * any primitive type or any Object type which implements Cloneable. + * However, if the Object is some kind of collection, e.g., a Vector + * then only a shallow copy of that object is made. I.e., deep refers + * only to arrays. + * + * @param o The object to be copied. + */ + public static Object deepClone(Object o) { + + if (o == null) { + return null; + } + + String classname = o.getClass().getName(); + + // Is this an array? + if (classname.charAt(0) != '[') { + return genericClone(o); + } + + // Check if this is a 1D primitive array. + if (classname.charAt(1) != '[' && classname.charAt(1) != 'L') { + try { + // Some compilers (SuperCede, e.g.) still + // think you have to catch this... + if (false) { + throw new CloneNotSupportedException(); + } + switch (classname.charAt(1)) { + case 'B': + return ((byte[]) o).clone(); + case 'Z': + return ((boolean[]) o).clone(); + case 'C': + return ((char[]) o).clone(); + case 'S': + return ((short[]) o).clone(); + case 'I': + return ((int[]) o).clone(); + case 'J': + return ((long[]) o).clone(); + case 'F': + return ((float[]) o).clone(); + case 'D': + return ((double[]) o).clone(); + default: + System.err.println("Unknown primtive array class:" + classname); + return null; + + } + } catch (CloneNotSupportedException e) { + } + } + + // Get the base type. + int ndim = 1; + while (classname.charAt(ndim) == '[') { + ndim += 1; + } + Class baseClass; + if (classname.charAt(ndim) != 'L') { + baseClass = getBaseClass(o); + } else { + try { + baseClass = Class.forName(classname.substring(ndim + 1, classname.length() - 1)); + } catch (ClassNotFoundException e) { + System.err.println("Internal error: class definition inconsistency: " + classname); + return null; + } + } + + // Allocate the array but make all but the first dimension 0. + int[] dims = new int[ndim]; + dims[0] = Array.getLength(o); + for (int i = 1; i < ndim; i += 1) { + dims[i] = 0; + } + + Object copy = ArrayFuncs.newInstance(baseClass, dims); + + // Now fill in the next level down by recursion. + for (int i = 0; i < dims[0]; i += 1) { + Array.set(copy, i, deepClone(Array.get(o, i))); + } + + return copy; + } + + /** Clone an Object if possible. + * + * This method returns an Object which is a clone of the + * input object. It checks if the method implements the + * Cloneable interface and then uses reflection to invoke + * the clone method. This can't be done directly since + * as far as the compiler is concerned the clone method for + * Object is protected and someone could implement Cloneable but + * leave the clone method protected. The cloning can fail in a + * variety of ways which are trapped so that it returns null instead. + * This method will generally create a shallow clone. If you + * wish a deep copy of an array the method deepClone should be used. + * + * @param o The object to be cloned. + */ + public static Object genericClone(Object o) { + + if (!(o instanceof Cloneable)) { + return null; + } + + Class[] argTypes = new Class[0]; + Object[] args = new Object[0]; + Class type = o.getClass(); + + try { + return type.getMethod("clone", argTypes).invoke(o, args); + } catch (Exception e) { + if (type.isArray()) { + return deepClone(o); + } + // Implements cloneable, but does not + // apparently make clone public. + return null; + } + } + + /** Copy one array into another. + * This function copies the contents of one array + * into a previously allocated array. + * The arrays must agree in type and size. + * @param original The array to be copied. + * @param copy The array to be copied into. This + * array must already be fully allocated. + */ + public static void copyArray(Object original, Object copy) { + String oname = original.getClass().getName(); + String cname = copy.getClass().getName(); + + if (!oname.equals(cname)) { + return; + } + + if (oname.charAt(0) != '[') { + return; + } + + if (oname.charAt(1) == '[') { + Object[] x = (Object[]) original; + Object[] y = (Object[]) copy; + if (x.length != y.length) { + return; + } + for (int i = 0; i < x.length; i += 1) { + copyArray(x, y); + } + } + int len = Array.getLength(original); + + System.arraycopy(original, 0, copy, 0, len); + } + + /** Find the dimensions of an object. + * + * This method returns an integer array with the dimensions + * of the object o which should usually be an array. + * + * It returns an array of dimension 0 for scalar objects + * and it returns -1 for dimension which have not been allocated, + * e.g., int[][][] x = new int[100][][]; should return [100,-1,-1]. + * + * @param o The object to get the dimensions of. + */ + public static int[] getDimensions(Object o) { + + if (o == null) { + return null; + } + + String classname = o.getClass().getName(); + + int ndim = 0; + + while (classname.charAt(ndim) == '[') { + ndim += 1; + } + + int[] dimens = new int[ndim]; + + for (int i = 0; i < ndim; i += 1) { + dimens[i] = -1; // So that we can distinguish a null from a 0 length. + } + + for (int i = 0; i < ndim; i += 1) { + dimens[i] = java.lang.reflect.Array.getLength(o); + if (dimens[i] == 0) { + return dimens; + } + if (i != ndim - 1) { + o = ((Object[]) o)[0]; + if (o == null) { + return dimens; + } + } + } + return dimens; + } + + /** This routine returns the base array of a multi-dimensional + * array. I.e., a one-d array of whatever the array is composed + * of. Note that arrays are not guaranteed to be rectangular, + * so this returns o[0][0].... + */ + public static Object getBaseArray(Object o) { + String cname = o.getClass().getName(); + if (cname.charAt(1) == '[') { + return getBaseArray(((Object[]) o)[0]); + } else { + return o; + } + } + + /** This routine returns the base class of an object. This is just + * the class of the object for non-arrays. + */ + public static Class getBaseClass(Object o) { + + if (o == null) { + return Void.TYPE; + } + + String className = o.getClass().getName(); + + int dims = 0; + while (className.charAt(dims) == '[') { + dims += 1; + } + + if (dims == 0) { + return o.getClass(); + } + + char c = className.charAt(dims); + for (int i = 0; i < PrimitiveInfo.suffixes.length; i += 1) { + if (c == PrimitiveInfo.suffixes[i]) { + return PrimitiveInfo.classes[i]; + } + } + + if (c == 'L') { + try { + return Class.forName(className.substring(dims + 1, className.length() - 1)); + } catch (ClassNotFoundException e) { + return null; + } + } + return null; + } + + /** This routine returns the size of the base element of an array. + * @param o The array object whose base length is desired. + * @return the size of the object in bytes, 0 if null, or + * -1 if not a primitive array. + */ + public static int getBaseLength(Object o) { + + if (o == null) { + return 0; + } + + String className = o.getClass().getName(); + + int dims = 0; + + while (className.charAt(dims) == '[') { + dims += 1; + } + + if (dims == 0) { + return -1; + } + + char c = className.charAt(dims); + for (int i = 0; i < PrimitiveInfo.suffixes.length; i += 1) { + if (c == PrimitiveInfo.suffixes[i]) { + return PrimitiveInfo.sizes[i]; + } + } + return -1; + } + + /** Create an array and populate it with a test pattern. + * + * @param baseType The base type of the array. This is expected to + * be a numeric type, but this is not checked. + * @param dims The desired dimensions. + * @return An array object populated with a simple test pattern. + */ + public static Object generateArray(Class baseType, int[] dims) { + + // Generate an array and populate it with a test pattern of + // data. + + Object x = ArrayFuncs.newInstance(baseType, dims); + testPattern(x, (byte) 0); + return x; + } + + /** Just create a simple pattern cycling through valid byte values. + * We use bytes because they can be cast to any other numeric type. + * @param o The array in which the test pattern is to be set. + * @param start The value for the first element. + */ + public static byte testPattern(Object o, byte start) { + + int[] dims = getDimensions(o); + if (dims.length > 1) { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + start = testPattern(((Object[]) o)[i], start); + } + + } else if (dims.length == 1) { + for (int i = 0; i < dims[0]; i += 1) { + java.lang.reflect.Array.setByte(o, i, start); + start += 1; + } + } + return start; + } + + /** Generate a description of an array (presumed rectangular). + * @param o The array to be described. + */ + public static String arrayDescription(Object o) { + + Class base = getBaseClass(o); + if (base == Void.TYPE) { + return "NULL"; + } + + int[] dims = getDimensions(o); + + StringBuffer desc = new StringBuffer(); + + // Note that all instances Class describing a given class are + // the same so we can use == here. + boolean found = false; + + for (int i = 0; i < PrimitiveInfo.classes.length; i += 1) { + if (base == PrimitiveInfo.classes[i]) { + found = true; + desc.append(PrimitiveInfo.types[i]); + break; + } + } + + if (!found) { + desc.append(base.getName()); + } + + if (dims != null) { + desc.append("["); + for (int i = 0; i < dims.length; i += 1) { + desc.append("" + dims[i]); + if (i < dims.length - 1) { + desc.append("]["); + } + } + desc.append("]"); + } + return new String(desc); + } + + /** Examine the structure of an array in detail. + * @param o The array to be examined. + */ + public static void examinePrimitiveArray(Object o) { + String className = o.getClass().getName(); + + // If we have a two-d array, or if the array is a one-d array + // of Objects, then recurse over the next dimension. We handle + // Object specially because each element could itself be an array. + if (className.substring(0, 2).equals("[[") + || className.equals("[Ljava.lang.Object;")) { + System.out.println("["); + for (int i = 0; i < ((Object[]) o).length; i += 1) { + examinePrimitiveArray(((Object[]) o)[i]); + } + System.out.print("]"); + } else if (className.charAt(0) != '[') { + System.out.println(className); + } else { + System.out.println("[" + java.lang.reflect.Array.getLength(o) + "]" + + className.substring(1)); + } + } + + /** Given an array of arbitrary dimensionality return + * the array flattened into a single dimension. + * @param input The input array. + */ + public static Object flatten(Object input) { + + int[] dimens = getDimensions(input); + if (dimens.length <= 1) { + return input; + } + int size = 1; + for (int i = 0; i < dimens.length; i += 1) { + size *= dimens[i]; + } + + Object flat = ArrayFuncs.newInstance(getBaseClass(input), size); + + if (size == 0) { + return flat; + } + + int offset = 0; + doFlatten(input, flat, offset); + return flat; + } + + /** This routine does the actually flattening of multi-dimensional + * arrays. + * @param input The input array to be flattened. + * @param output The flattened array. + * @param offset The current offset within the output array. + * @return The number of elements within the array. + */ + protected static int doFlatten(Object input, Object output, int offset) { + + String classname = input.getClass().getName(); + if (classname.charAt(0) != '[') { + throw new RuntimeException("Attempt to flatten non-array"); + } + int size = Array.getLength(input); + + if (classname.charAt(1) != '[') { + System.arraycopy(input, 0, output, offset, size); + return size; + } + int total = 0; + Object[] xx = (Object[]) input; + for (int i = 0; i < size; i += 1) { + int len = doFlatten(xx[i], output, offset + total); + total += len; + } + return total; + } + + /** Curl an input array up into a multi-dimensional array. + * + * @param input The one dimensional array to be curled. + * @param dimens The desired dimensions + * @return The curled array. + */ + public static Object curl(Object input, int[] dimens) { + + if (input == null) { + return null; + } + String classname = input.getClass().getName(); + if (classname.charAt(0) != '[' || classname.charAt(1) == '[') { + throw new RuntimeException("Attempt to curl non-1D array"); + } + + int size = Array.getLength(input); + + int test = 1; + for (int i = 0; i < dimens.length; i += 1) { + test *= dimens[i]; + } + + if (test != size) { + throw new RuntimeException("Curled array does not fit desired dimensions"); + } + + Class base = getBaseClass(input); + + Object newArray = ArrayFuncs.newInstance(base, dimens); + + int offset = 0; + + doCurl(input, newArray, dimens, offset); + return newArray; + + } + + /** Do the curling of the 1-d to multi-d array. + * @param input The 1-d array to be curled. + * @param output The multi-dimensional array to be filled. + * @param dimens The desired output dimensions. + * @param offset The current offset in the input array. + * @return The number of elements curled. + */ + protected static int doCurl(Object input, Object output, + int[] dimens, int offset) { + + if (dimens.length == 1) { + System.arraycopy(input, offset, output, 0, dimens[0]); + return dimens[0]; + } + + int total = 0; + int[] xdimens = new int[dimens.length - 1]; + for (int i = 1; i < dimens.length; i += 1) { + xdimens[i - 1] = dimens[i]; + } + + for (int i = 0; i < dimens[0]; i += 1) { + total += doCurl(input, ((Object[]) output)[i], xdimens, offset + total); + } + return total; + } + + /** Create an array of a type given by new type with + * the dimensionality given in array. + * @param array A possibly multidimensional array to be converted. + * @param newType The desired output type. This should be one of the + * class descriptors for primitive numeric data, e.g., double.type. + */ + public static Object mimicArray(Object array, Class newType) { + + String classname = array.getClass().getName(); + if (classname.charAt(0) != '[') { + return null; + } + + int dims = 1; + + while (classname.charAt(dims) == '[') { + dims += 1; + } + + Object mimic; + + if (dims > 1) { + + Object[] xarray = (Object[]) array; + int[] dimens = new int[dims]; + dimens[0] = xarray.length; // Leave other dimensions at 0. + + + mimic = ArrayFuncs.newInstance(newType, dimens); + + for (int i = 0; i < xarray.length; i += 1) { + Object temp = mimicArray(xarray[i], newType); + ((Object[]) mimic)[i] = temp; + } + + } else { + mimic = ArrayFuncs.newInstance(newType, Array.getLength(array)); + } + + return mimic; + } + + /** Convert an array to a specified type. This method supports conversions + * only among the primitive numeric types. + * @param array A possibly multidimensional array to be converted. + * @param newType The desired output type. This should be one of the + * class descriptors for primitive numeric data, e.g., double.type. + * @param preserve If set, and the requested type is the same as the + * original, then the original is returned. + */ + public static Object convertArray(Object array, Class newType, boolean reuse) { + + if (getBaseClass(array) == newType && reuse) { + return array; + } else { + return convertArray(array, newType); + } + } + + /** Convert an array to a specified type. This method supports conversions + * only among the primitive numeric types. + * @param array A possibly multidimensional array to be converted. + * @param newType The desired output type. This should be one of the + * class descriptors for primitive numeric data, e.g., double.type. + */ + public static Object convertArray(Object array, Class newType) { + + /* We break this up into two steps so that users + * can reuse an array many times and only allocate a + * new array when needed. + */ + + /* First create the full new array. */ + Object mimic = mimicArray(array, newType); + if (mimic == null) { + return mimic; + } + + /* Now copy the info into the new array */ + copyInto(array, mimic); + + return mimic; + } + + /** Copy an array into an array of a different type. + * The dimensions and dimensionalities of the two + * arrays should be the same. + * @param array The original array. + * @param mimic The array mimicking the original. + */ + public static void copyInto(Object array, Object mimic) { + + String classname = array.getClass().getName(); + if (classname.charAt(0) != '[') { + return; + } + + /* Do multidimensional arrays recursively */ + if (classname.charAt(1) == '[') { + + for (int i = 0; i < ((Object[]) array).length; i += 1) { + copyInto(((Object[]) array)[i], ((Object[]) mimic)[i]); + } + + } else { + + byte[] xbarr; + short[] xsarr; + char[] xcarr; + int[] xiarr; + long[] xlarr; + float[] xfarr; + double[] xdarr; + + Class base = getBaseClass(array); + Class newType = getBaseClass(mimic); + + if (base == byte.class) { + byte[] barr = (byte[]) array; + + if (newType == byte.class) { + System.arraycopy(array, 0, mimic, 0, barr.length); + + } else if (newType == short.class) { + xsarr = (short[]) mimic; + for (int i = 0; i < barr.length; i += 1) { + xsarr[i] = barr[i]; + } + + } else if (newType == char.class) { + xcarr = (char[]) mimic; + for (int i = 0; i < barr.length; i += 1) { + xcarr[i] = (char) barr[i]; + } + + } else if (newType == int.class) { + xiarr = (int[]) mimic; + for (int i = 0; i < barr.length; i += 1) { + xiarr[i] = barr[i]; + } + + } else if (newType == long.class) { + xlarr = (long[]) mimic; + for (int i = 0; i < barr.length; i += 1) { + xlarr[i] = barr[i]; + } + + } else if (newType == float.class) { + xfarr = (float[]) mimic; + for (int i = 0; i < barr.length; i += 1) { + xfarr[i] = barr[i]; + } + + } else if (newType == double.class) { + xdarr = (double[]) mimic; + for (int i = 0; i < barr.length; i += 1) { + xdarr[i] = barr[i]; + } + } + + } else if (base == short.class) { + short[] sarr = (short[]) array; + + if (newType == byte.class) { + xbarr = (byte[]) mimic; + for (int i = 0; i < sarr.length; i += 1) { + xbarr[i] = (byte) sarr[i]; + } + + } else if (newType == short.class) { + System.arraycopy(array, 0, mimic, 0, sarr.length); + + } else if (newType == char.class) { + xcarr = (char[]) mimic; + for (int i = 0; i < sarr.length; i += 1) { + xcarr[i] = (char) sarr[i]; + } + + } else if (newType == int.class) { + xiarr = (int[]) mimic; + for (int i = 0; i < sarr.length; i += 1) { + xiarr[i] = sarr[i]; + } + + } else if (newType == long.class) { + xlarr = (long[]) mimic; + for (int i = 0; i < sarr.length; i += 1) { + xlarr[i] = sarr[i]; + } + + } else if (newType == float.class) { + xfarr = (float[]) mimic; + for (int i = 0; i < sarr.length; i += 1) { + xfarr[i] = sarr[i]; + } + + } else if (newType == double.class) { + xdarr = (double[]) mimic; + for (int i = 0; i < sarr.length; i += 1) { + xdarr[i] = sarr[i]; + } + } + + } else if (base == char.class) { + char[] carr = (char[]) array; + + if (newType == byte.class) { + xbarr = (byte[]) mimic; + for (int i = 0; i < carr.length; i += 1) { + xbarr[i] = (byte) carr[i]; + } + + } else if (newType == short.class) { + xsarr = (short[]) mimic; + for (int i = 0; i < carr.length; i += 1) { + xsarr[i] = (short) carr[i]; + } + + } else if (newType == char.class) { + System.arraycopy(array, 0, mimic, 0, carr.length); + + } else if (newType == int.class) { + xiarr = (int[]) mimic; + for (int i = 0; i < carr.length; i += 1) { + xiarr[i] = carr[i]; + } + + } else if (newType == long.class) { + xlarr = (long[]) mimic; + for (int i = 0; i < carr.length; i += 1) { + xlarr[i] = carr[i]; + } + + } else if (newType == float.class) { + xfarr = (float[]) mimic; + for (int i = 0; i < carr.length; i += 1) { + xfarr[i] = carr[i]; + } + + } else if (newType == double.class) { + xdarr = (double[]) mimic; + for (int i = 0; i < carr.length; i += 1) { + xdarr[i] = carr[i]; + } + } + + } else if (base == int.class) { + int[] iarr = (int[]) array; + + if (newType == byte.class) { + xbarr = (byte[]) mimic; + for (int i = 0; i < iarr.length; i += 1) { + xbarr[i] = (byte) iarr[i]; + } + + } else if (newType == short.class) { + xsarr = (short[]) mimic; + for (int i = 0; i < iarr.length; i += 1) { + xsarr[i] = (short) iarr[i]; + } + + } else if (newType == char.class) { + xcarr = (char[]) mimic; + for (int i = 0; i < iarr.length; i += 1) { + xcarr[i] = (char) iarr[i]; + } + + } else if (newType == int.class) { + System.arraycopy(array, 0, mimic, 0, iarr.length); + + } else if (newType == long.class) { + xlarr = (long[]) mimic; + for (int i = 0; i < iarr.length; i += 1) { + xlarr[i] = iarr[i]; + } + + } else if (newType == float.class) { + xfarr = (float[]) mimic; + for (int i = 0; i < iarr.length; i += 1) { + xfarr[i] = iarr[i]; + } + + } else if (newType == double.class) { + xdarr = (double[]) mimic; + for (int i = 0; i < iarr.length; i += 1) { + xdarr[i] = iarr[i]; + } + } + + + } else if (base == long.class) { + long[] larr = (long[]) array; + + if (newType == byte.class) { + xbarr = (byte[]) mimic; + for (int i = 0; i < larr.length; i += 1) { + xbarr[i] = (byte) larr[i]; + } + + } else if (newType == short.class) { + xsarr = (short[]) mimic; + for (int i = 0; i < larr.length; i += 1) { + xsarr[i] = (short) larr[i]; + } + + } else if (newType == char.class) { + xcarr = (char[]) mimic; + for (int i = 0; i < larr.length; i += 1) { + xcarr[i] = (char) larr[i]; + } + + } else if (newType == int.class) { + xiarr = (int[]) mimic; + for (int i = 0; i < larr.length; i += 1) { + xiarr[i] = (int) larr[i]; + } + + } else if (newType == long.class) { + System.arraycopy(array, 0, mimic, 0, larr.length); + + } else if (newType == float.class) { + xfarr = (float[]) mimic; + for (int i = 0; i < larr.length; i += 1) { + xfarr[i] = (float) larr[i]; + } + + } else if (newType == double.class) { + xdarr = (double[]) mimic; + for (int i = 0; i < larr.length; i += 1) { + xdarr[i] = (double) larr[i]; + } + } + + } else if (base == float.class) { + float[] farr = (float[]) array; + + if (newType == byte.class) { + xbarr = (byte[]) mimic; + for (int i = 0; i < farr.length; i += 1) { + xbarr[i] = (byte) farr[i]; + } + + } else if (newType == short.class) { + xsarr = (short[]) mimic; + for (int i = 0; i < farr.length; i += 1) { + xsarr[i] = (short) farr[i]; + } + + } else if (newType == char.class) { + xcarr = (char[]) mimic; + for (int i = 0; i < farr.length; i += 1) { + xcarr[i] = (char) farr[i]; + } + + } else if (newType == int.class) { + xiarr = (int[]) mimic; + for (int i = 0; i < farr.length; i += 1) { + xiarr[i] = (int) farr[i]; + } + + } else if (newType == long.class) { + xlarr = (long[]) mimic; + for (int i = 0; i < farr.length; i += 1) { + xlarr[i] = (long) farr[i]; + } + + } else if (newType == float.class) { + System.arraycopy(array, 0, mimic, 0, farr.length); + + } else if (newType == double.class) { + xdarr = (double[]) mimic; + for (int i = 0; i < farr.length; i += 1) { + xdarr[i] = farr[i]; + } + } + + + } else if (base == double.class) { + double[] darr = (double[]) array; + + if (newType == byte.class) { + xbarr = (byte[]) mimic; + for (int i = 0; i < darr.length; i += 1) { + xbarr[i] = (byte) darr[i]; + } + + } else if (newType == short.class) { + xsarr = (short[]) mimic; + for (int i = 0; i < darr.length; i += 1) { + xsarr[i] = (short) darr[i]; + } + + } else if (newType == char.class) { + xcarr = (char[]) mimic; + for (int i = 0; i < darr.length; i += 1) { + xcarr[i] = (char) darr[i]; + } + + } else if (newType == int.class) { + xiarr = (int[]) mimic; + for (int i = 0; i < darr.length; i += 1) { + xiarr[i] = (int) darr[i]; + } + + } else if (newType == long.class) { + xlarr = (long[]) mimic; + for (int i = 0; i < darr.length; i += 1) { + xlarr[i] = (long) darr[i]; + } + + } else if (newType == float.class) { + xfarr = (float[]) mimic; + for (int i = 0; i < darr.length; i += 1) { + xfarr[i] = (float) darr[i]; + } + + } else if (newType == double.class) { + System.arraycopy(array, 0, mimic, 0, darr.length); + } + } + } + + return; + + } + + /** Allocate an array dynamically. The Array.newInstance method + * does not throw an error when there is insufficient memory + * and silently returns a null. + * @param cl The class of the array. + * @param dim The dimension of the array. + * @return The allocated array. + * @throws An OutOfMemoryError if insufficient space is available. + */ + public static Object newInstance(Class cl, int dim) { + + Object o = Array.newInstance(cl, dim); + if (o == null) { + String desc = cl + "[" + dim + "]"; + throw new OutOfMemoryError("Unable to allocate array: " + desc); + } + return o; + } + + /** Allocate an array dynamically. The Array.newInstance method + * does not throw an error and silently returns a null. + * + * @param cl The class of the array. + * @param dims The dimensions of the array. + * @return The allocated array. + * @throws An OutOfMemoryError if insufficient space is available. + */ + public static Object newInstance(Class cl, int[] dims) { + + if (dims.length == 0) { + // Treat a scalar as a 1-d array of length 1 + dims = new int[]{1}; + } + + Object o = Array.newInstance(cl, dims); + if (o == null) { + String desc = cl + "["; + String comma = ""; + for (int i = 0; i < dims.length; i += 1) { + desc += comma + dims[i]; + comma = ","; + } + desc += "]"; + throw new OutOfMemoryError("Unable to allocate array: " + desc); + } + return o; + } + + /** Are two objects equal? Arrays have the standard object equals + * method which only returns true if the two object are the same. + * This method returns true if every element of the arrays match. + * The inputs may be of any dimensionality. The dimensionality + * and dimensions of the arrays must match as well as any elements. + * If the elements are non-primitive. non-array objects, then the + * equals method is called for each element. + * If both elements are multi-dimensional arrays, then + * the method recurses. + */ + public static boolean arrayEquals(Object x, Object y) { + return arrayEquals(x, y, 0, 0); + } + + /** Are two objects equal? Arrays have the standard object equals + * method which only returns true if the two object are the same. + * This method returns true if every element of the arrays match. + * The inputs may be of any dimensionality. The dimensionality + * and dimensions of the arrays must match as well as any elements. + * If the elements are non-primitive. non-array objects, then the + * equals method is called for each element. + * If both elements are multi-dimensional arrays, then + * the method recurses. + */ + public static boolean arrayEquals(Object x, Object y, double tolf, double told) { + + // Handle the special cases first. + // We treat null == null so that two object arrays + // can match if they have matching null elements. + if (x == null && y == null) { + return true; + } + + if (x == null || y == null) { + return false; + } + + Class xClass = x.getClass(); + Class yClass = y.getClass(); + + if (xClass != yClass) { + return false; + } + + if (!xClass.isArray()) { + return x.equals(y); + + } else { + if (xClass.equals(int[].class)) { + return Arrays.equals((int[]) x, (int[]) y); + + } else if (xClass.equals(double[].class)) { + if (told == 0) { + return Arrays.equals((double[]) x, (double[]) y); + } else { + return doubleArrayEquals((double[]) x, (double[]) y, told); + } + + } else if (xClass.equals(long[].class)) { + return Arrays.equals((long[]) x, (long[]) y); + + } else if (xClass.equals(float[].class)) { + if (tolf == 0) { + return Arrays.equals((float[]) x, (float[]) y); + } else { + return floatArrayEquals((float[]) x, (float[]) y, (float) tolf); + } + + } else if (xClass.equals(byte[].class)) { + return Arrays.equals((byte[]) x, (byte[]) y); + + } else if (xClass.equals(short[].class)) { + return Arrays.equals((short[]) x, (short[]) y); + + } else if (xClass.equals(char[].class)) { + return Arrays.equals((char[]) x, (char[]) y); + + } else if (xClass.equals(boolean[].class)) { + return Arrays.equals((boolean[]) x, (boolean[]) y); + + } else { + // Non-primitive and multidimensional arrays can be + // cast to Object[] + Object[] xo = (Object[]) x; + Object[] yo = (Object[]) y; + if (xo.length != yo.length) { + return false; + } + for (int i = 0; i < xo.length; i += 1) { + if (!arrayEquals(xo[i], yo[i], tolf, told)) { + return false; + } + } + + return true; + + } + } + } + + /** Compare two double arrays using a given tolerance */ + public static boolean doubleArrayEquals(double[] x, double[] y, double tol) { + + for (int i = 0; i < x.length; i += 1) { + if (x[i] == 0) { + return y[i] == 0; + } + if (Math.abs((y[i] - x[i]) / x[i]) > tol) { + return false; + } + } + return true; + } + + /** Compare two float arrays using a given tolerance */ + public static boolean floatArrayEquals(float[] x, float[] y, float tol) { + + for (int i = 0; i < x.length; i += 1) { + if (x[i] == 0) { + return y[i] == 0; + } + if (Math.abs((y[i] - x[i]) / x[i]) > tol) { + return false; + } + } + return true; + } + + /** Dump an array on the given print steam */ + public static void dumpArray(java.io.PrintStream p, Object arr) { + // Get the dimensionality and then dump. + if (arr == null) { + p.print("null "); + } else { + Class nm = arr.getClass(); + if (nm.isArray()) { + p.print("["); + for (int i = 0; i < java.lang.reflect.Array.getLength(arr); i += 1) { + dumpArray(p, java.lang.reflect.Array.get(arr, i)); + } + p.print("]\n"); + } else { + p.print(" " + arr.toString() + " "); + } + } + } +} diff --git a/src/nom/tam/util/AsciiFuncs.java b/src/nom/tam/util/AsciiFuncs.java new file mode 100644 index 0000000..a74ea16 --- /dev/null +++ b/src/nom/tam/util/AsciiFuncs.java @@ -0,0 +1,44 @@ +/* + * This class provides conversions to ASCII strings without breaking + * compatibility with Java 1.5. + */ +package nom.tam.util; + +import java.io.UnsupportedEncodingException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author tmcglynn + */ +public class AsciiFuncs { + + public final static String ASCII = "US-ASCII"; + + /** Convert to ASCII or return null if not compatible */ + public static String asciiString(byte[] buf) { + return asciiString(buf, 0, buf.length); + } + + /** Convert to ASCII or return null if not compatible */ + public static String asciiString(byte[] buf, int start, int len) { + try { + return new String(buf, start, len, ASCII); + } catch (java.io.UnsupportedEncodingException e) { + // Shouldn't happen + System.err.println("AsciiFuncs.asciiString error finding ASCII encoding"); + return null; + } + } + + /** Convert an ASCII string to bytes */ + public static byte[] getBytes(String in) { + try { + return in.getBytes(ASCII); + } catch (UnsupportedEncodingException ex) { + System.err.println("Unable to find ASCII encoding"); + return null; + } + } +} diff --git a/src/nom/tam/util/BufferedDataInputStream.java b/src/nom/tam/util/BufferedDataInputStream.java new file mode 100644 index 0000000..e8347cf --- /dev/null +++ b/src/nom/tam/util/BufferedDataInputStream.java @@ -0,0 +1,697 @@ +package nom.tam.util; + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +// What do we use in here? +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.EOFException; + +/** This class is intended for high performance I/O in scientific applications. + * It combines the functionality of the BufferedInputStream and the + * DataInputStream as well as more efficient handling of arrays. + * This minimizes the number of method calls that are required to + * read data. Informal tests of this method show that it can + * be as much as 10 times faster than using a DataInputStream layered + * on a BufferedInputStream for writing large arrays. The performance + * gain on scalars or small arrays will be less but there should probably + * never be substantial degradation of performance. + *

+ * Many new read calls are added to allow efficient reading + * off array data. The read(Object o) call provides + * for reading a primitive array of arbitrary type or + * dimensionality. There are also reads for each type + * of one dimensional array. + *

+ * Note that there is substantial duplication of code to minimize method + * invocations. E.g., the floating point read routines read the data + * as integer values and then convert to float. However the integer + * code is duplicated rather than invoked. There has been + * considerable effort expended to ensure that these routines are + * efficient, but they could easily be superceded if + * an efficient underlying I/O package were ever delivered + * as part of the basic Java libraries. + * [This has subsequently happened with the NIO package + * and in an ideal universe these classes would be + * rewritten to take advantage of NIO.] + *

+ * Testing and timing routines are provided in the + * nom.tam.util.test.BufferedFileTester class. + * + * Version 1.1: October 12, 2000: Fixed handling of EOF to return + * partially read arrays when EOF is detected. + * Version 1.2: July 20, 2009: Added handling of very large Object + * arrays. Additional work is required to handle very large arrays + * generally. + */ +public class BufferedDataInputStream + extends BufferedInputStream + implements ArrayDataInput { + + private long primitiveArrayCount; + private byte[] bb = new byte[8]; + + /** Use the BufferedInputStream constructor + */ + public BufferedDataInputStream(InputStream o) { + super(o, 32768); + } + + /** Use the BufferedInputStream constructor + */ + public BufferedDataInputStream(InputStream o, int bufLength) { + super(o, bufLength); + } + + /** Read a byte array. This is the only method + * for reading arrays in the fundamental I/O classes. + * @param obuf The byte array. + * @param offset The starting offset into the array. + * @param len The number of bytes to read. + * @return The actual number of bytes read. + */ + public int read(byte[] obuf, int offset, int len) throws IOException { + + int total = 0; + + while (len > 0) { + + // Use just the buffered I/O to get needed info. + + int xlen = super.read(obuf, offset, len); + if (xlen <= 0) { + if (total == 0) { + throw new EOFException(); + } else { + return total; + } + } else { + len -= xlen; + total += xlen; + offset += xlen; + } + } + return total; + + } + + /** Read a boolean value. + * @return b The value read. + */ + public boolean readBoolean() throws IOException { + + int b = read(); + if (b == 1) { + return true; + } else { + return false; + } + } + + /** Read a byte value in the range -128 to 127. + * @return The byte value as a byte (see read() to return the value + * as an integer. + */ + public byte readByte() throws IOException { + return (byte) read(); + } + + /** Read a byte value in the range 0-255. + * @return The byte value as an integer. + */ + public int readUnsignedByte() throws IOException { + return read() | 0x00ff; + } + + /** Read an integer. + * @return The integer value. + */ + public int readInt() throws IOException { + + if (read(bb, 0, 4) < 4) { + throw new EOFException(); + } + int i = bb[0] << 24 | (bb[1] & 0xFF) << 16 | (bb[2] & 0xFF) << 8 | (bb[3] & 0xFF); + return i; + } + + /** Read a 2-byte value as a short (-32788 to 32767) + * @return The short value. + */ + public short readShort() throws IOException { + + if (read(bb, 0, 2) < 2) { + throw new EOFException(); + } + + short s = (short) (bb[0] << 8 | (bb[1] & 0xFF)); + return s; + } + + /** Read a 2-byte value in the range 0-65536. + * @return the value as an integer. + */ + public int readUnsignedShort() throws IOException { + + if (read(bb, 0, 2) < 2) { + throw new EOFException(); + } + + return (bb[0] & 0xFF) << 8 | (bb[1] & 0xFF); + } + + /** Read a 2-byte value as a character. + * @return The character read. + */ + public char readChar() throws IOException { + byte[] b = new byte[2]; + + if (read(b, 0, 2) < 2) { + throw new EOFException(); + } + + char c = (char) (b[0] << 8 | (b[1] & 0xFF)); + return c; + } + + /** Read a long. + * @return The value read. + */ + public long readLong() throws IOException { + + // use two ints as intermediarys to + // avoid casts of bytes to longs... + if (read(bb, 0, 8) < 8) { + throw new EOFException(); + } + int i1 = bb[0] << 24 | (bb[1] & 0xFF) << 16 | (bb[2] & 0xFF) << 8 | (bb[3] & 0xFF); + int i2 = bb[4] << 24 | (bb[5] & 0xFF) << 16 | (bb[6] & 0xFF) << 8 | (bb[7] & 0xFF); + return (((long) i1) << 32) | (((long) i2) & 0x00000000ffffffffL); + } + + /** Read a 4 byte real number. + * @return The value as a float. + */ + public float readFloat() throws IOException { + + if (read(bb, 0, 4) < 4) { + throw new EOFException(); + } + + int i = bb[0] << 24 | (bb[1] & 0xFF) << 16 | (bb[2] & 0xFF) << 8 | (bb[3] & 0xFF); + return Float.intBitsToFloat(i); + + } + + /** Read an 8 byte real number. + * @return The value as a double. + */ + public double readDouble() throws IOException { + + if (read(bb, 0, 8) < 8) { + throw new EOFException(); + } + + int i1 = bb[0] << 24 | (bb[1] & 0xFF) << 16 | (bb[2] & 0xFF) << 8 | (bb[3] & 0xFF); + int i2 = bb[4] << 24 | (bb[5] & 0xFF) << 16 | (bb[6] & 0xFF) << 8 | (bb[7] & 0xFF); + + return Double.longBitsToDouble(((long) i1) << 32 | ((long) i2 & 0x00000000ffffffffL)); + } + + /** Read a buffer and signal an EOF if the buffer + * cannot be fully read. + * @param b The buffer to be read. + */ + public void readFully(byte[] b) throws IOException { + readFully(b, 0, b.length); + } + + /** Read a buffer and signal an EOF if the requested elements + * cannot be read. + * + * This differs from read(b,off,len) since that call + * will not signal and end of file unless no bytes can + * be read. However both of these routines will attempt + * to fill their buffers completely. + * @param b The input buffer. + * @param off The requested offset into the buffer. + * @param len The number of bytes requested. + */ + public void readFully(byte[] b, int off, int len) throws IOException { + + if (off < 0 || len < 0 || off + len > b.length) { + throw new IOException("Attempt to read outside byte array"); + } + + if (read(b, off, len) < len) { + throw new EOFException(); + } + } + /** Skip the requested number of bytes. + * This differs from the skip call in that + * it takes an long argument and will throw + * an end of file if the full number of bytes cannot be skipped. + * @param toSkip The number of bytes to skip. + */ + private byte[] skipBuf = null; + + public int skipBytes(int toSkip) throws IOException { + return (int) skipBytes((long) toSkip); + } + + public long skipBytes(long toSkip) throws IOException { + + long need = toSkip; + + while (need > 0) { + + try { + long got = skip(need); + if (got > 0) { + need -= got; + } else { + break; + } + } catch (IOException e) { + // Some input streams (process outputs) don't allow + // skipping. The kludgy solution here is to + // try to do a read when we get an error in the skip.... + // Real IO errors will presumably casue an error + // in these reads too. + if (skipBuf == null) { + skipBuf = new byte[8192]; + } + while (need > 8192) { + int got = read(skipBuf, 0, 8192); + if (got <= 0) { + break; + } + need -= got; + } + while (need > 0) { + int got = read(skipBuf, 0, (int) need); + if (got <= 0) { + break; + } + need -= got; + } + } + + } + + if (need > 0) { + throw new EOFException(); + } else { + return toSkip; + } + } + + /** Read a String in the UTF format. + * The implementation of this is very inefficient and + * use of this class is not recommended for applications + * which will use this routine heavily. + * @return The String that was read. + */ + public String readUTF() throws IOException { + + // Punt on this one and use DataInputStream routines. + DataInputStream d = new DataInputStream(this); + return d.readUTF(); + + } + + /** + * Emulate the deprecated DataInputStream.readLine() method. + * Originally we used the method itself, but Alan Brighton + * suggested using a BufferedReader to eliminate the deprecation warning. + * This method is slow regardless. + * + * @return The String read. + * @deprecated Use BufferedReader methods. + */ + public String readLine() throws IOException { + // Punt on this and use BufferedReader routines. + BufferedReader d = new BufferedReader(new InputStreamReader(this)); + return d.readLine(); + } + + /** This routine provides efficient reading of arrays of any primitive type. + * It is an error to invoke this method with an object that is not an array + * of some primitive type. Note that there is no corresponding capability + * to writePrimitiveArray in BufferedDataOutputStream to read in an + * array of Strings. + * + * @param o The object to be read. It must be an array of a primitive type, + * or an array of Object's. + * @deprecated See readLArray(Object o). + */ + public int readPrimitiveArray(Object o) throws IOException { + + // Note that we assume that only a single thread is + // doing a primitive Array read at any given time. Otherwise + // primitiveArrayCount can be wrong and also the + // input data can be mixed up. + + primitiveArrayCount = 0; + return (int) readLArray(o); + } + + /** Read an object. An EOF will be signaled if the + * object cannot be fully read. The getPrimitiveArrayCount() + * method may then be used to get a minimum number of bytes read. + * @param o The object to be read. This object should + * be a primitive (possibly multi-dimensional) array. + * + * @returns The number of bytes read. + * @deprecated See readLArray(Object) which handles large arrays properly. + */ + public int readArray(Object o) throws IOException { + return (int) readLArray(o); + } + + /** Read an object. An EOF will be signaled if the + * object cannot be fully read. The getPrimitiveArrayCount() + * method may then be used to get a minimum number of bytes read. + * @param o The object to be read. This object should + * be a primitive (possibly multi-dimensional) array. + * + * @returns The number of bytes read. + */ + public long readLArray(Object o) throws IOException { + primitiveArrayCount = 0; + return primitiveArrayRecurse(o); + } + + /** Read recursively over a multi-dimensional array. + * @return The number of bytes read. + */ + protected long primitiveArrayRecurse(Object o) throws IOException { + + if (o == null) { + return primitiveArrayCount; + } + + String className = o.getClass().getName(); + + if (className.charAt(0) != '[') { + throw new IOException("Invalid object passed to BufferedDataInputStream.readArray:" + className); + } + + // Is this a multidimensional array? If so process recursively. + if (className.charAt(1) == '[') { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + primitiveArrayRecurse(((Object[]) o)[i]); + } + } else { + + // This is a one-d array. Process it using our special functions. + switch (className.charAt(1)) { + case 'Z': + primitiveArrayCount += read((boolean[]) o, 0, ((boolean[]) o).length); + break; + case 'B': + int len = read((byte[]) o, 0, ((byte[]) o).length); + primitiveArrayCount += len; + + if (len < ((byte[]) o).length) { + throw new EOFException(); + } + break; + case 'C': + primitiveArrayCount += read((char[]) o, 0, ((char[]) o).length); + break; + case 'S': + primitiveArrayCount += read((short[]) o, 0, ((short[]) o).length); + break; + case 'I': + primitiveArrayCount += read((int[]) o, 0, ((int[]) o).length); + break; + case 'J': + primitiveArrayCount += read((long[]) o, 0, ((long[]) o).length); + break; + case 'F': + primitiveArrayCount += read((float[]) o, 0, ((float[]) o).length); + break; + case 'D': + primitiveArrayCount += read((double[]) o, 0, ((double[]) o).length); + break; + case 'L': + + // Handle an array of Objects by recursion. Anything + // else is an error. + if (className.equals("[Ljava.lang.Object;")) { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + primitiveArrayRecurse(((Object[]) o)[i]); + } + } else { + throw new IOException("Invalid object passed to BufferedDataInputStream.readArray: " + className); + } + break; + default: + throw new IOException("Invalid object passed to BufferedDataInputStream.readArray: " + className); + } + } + return primitiveArrayCount; + } + + /** Ensure that the requested number of bytes + * are available in the buffer or throw an EOF + * if they cannot be obtained. Note that this + * routine will try to fill the buffer completely. + * + * @param The required number of bytes. + */ + private void fillBuf(int need) throws IOException { + + if (count > pos) { + System.arraycopy(buf, pos, buf, 0, count - pos); + count -= pos; + need -= count; + pos = 0; + } else { + count = 0; + pos = 0; + } + + while (need > 0) { + + + int len = in.read(buf, count, buf.length - count); + if (len <= 0) { + throw new EOFException(); + } + count += len; + need -= len; + } + } + + /** Read a boolean array */ + public int read(boolean[] b) throws IOException { + return read(b, 0, b.length); + } + + /** Read a boolean array. + */ + public int read(boolean[] b, int start, int len) throws IOException { + + int i = start; + try { + for (; i < start + len; i += 1) { + + if (pos >= count) { + fillBuf(1); + } + + if (buf[pos] == 1) { + b[i] = true; + } else { + b[i] = false; + } + pos += 1; + } + } catch (EOFException e) { + return eofCheck(e, i, start, 1); + } + return len; + } + + /** Read a short array */ + public int read(short[] s) throws IOException { + return read(s, 0, s.length); + } + + /** Read a short array */ + public int read(short[] s, int start, int len) throws IOException { + + int i = start; + try { + for (; i < start + len; i += 1) { + if (count - pos < 2) { + fillBuf(2); + } + s[i] = (short) (buf[pos] << 8 | (buf[pos + 1] & 0xFF)); + pos += 2; + } + } catch (EOFException e) { + return eofCheck(e, i, start, 2); + } + return 2 * len; + } + + /** Read a character array */ + public int read(char[] c) throws IOException { + return read(c, 0, c.length); + } + + /** Read a character array */ + public int read(char[] c, int start, int len) throws IOException { + + int i = start; + try { + for (; i < start + len; i += 1) { + if (count - pos < 2) { + fillBuf(2); + } + c[i] = (char) (buf[pos] << 8 | (buf[pos + 1] & 0xFF)); + pos += 2; + } + } catch (EOFException e) { + return eofCheck(e, i, start, 2); + } + return 2 * len; + } + + /** Read an integer array */ + public int read(int[] i) throws IOException { + return read(i, 0, i.length); + } + + /** Read an integer array */ + public int read(int[] i, int start, int len) throws IOException { + + int ii = start; + try { + for (; ii < start + len; ii += 1) { + + if (count - pos < 4) { + fillBuf(4); + } + + i[ii] = buf[pos] << 24 + | (buf[pos + 1] & 0xFF) << 16 + | (buf[pos + 2] & 0xFF) << 8 + | (buf[pos + 3] & 0xFF); + pos += 4; + } + } catch (EOFException e) { + return eofCheck(e, ii, start, 4); + } + return i.length * 4; + } + + /** Read a long array */ + public int read(long[] l) throws IOException { + return read(l, 0, l.length); + } + + /** Read a long array */ + public int read(long[] l, int start, int len) throws IOException { + + int i = start; + try { + for (; i < start + len; i += 1) { + if (count - pos < 8) { + fillBuf(8); + } + int i1 = buf[pos] << 24 | (buf[pos + 1] & 0xFF) << 16 | (buf[pos + 2] & 0xFF) << 8 | (buf[pos + 3] & 0xFF); + int i2 = buf[pos + 4] << 24 | (buf[pos + 5] & 0xFF) << 16 | (buf[pos + 6] & 0xFF) << 8 | (buf[pos + 7] & 0xFF); + l[i] = ((long) i1) << 32 | ((long) i2 & 0x00000000FFFFFFFFL); + pos += 8; + } + + } catch (EOFException e) { + return eofCheck(e, i, start, 8); + } + return 8 * len; + } + + /** Read a float array */ + public int read(float[] f) throws IOException { + return read(f, 0, f.length); + } + + /** Read a float array */ + public int read(float[] f, int start, int len) throws IOException { + + int i = start; + try { + for (; i < start + len; i += 1) { + if (count - pos < 4) { + fillBuf(4); + } + int t = buf[pos] << 24 + | (buf[pos + 1] & 0xFF) << 16 + | (buf[pos + 2] & 0xFF) << 8 + | (buf[pos + 3] & 0xFF); + f[i] = Float.intBitsToFloat(t); + pos += 4; + } + } catch (EOFException e) { + return eofCheck(e, i, start, 4); + } + return 4 * len; + } + + /** Read a double array */ + public int read(double[] d) throws IOException { + return read(d, 0, d.length); + } + + /** Read a double array */ + public int read(double[] d, int start, int len) throws IOException { + + int i = start; + try { + for (; i < start + len; i += 1) { + + if (count - pos < 8) { + fillBuf(8); + } + int i1 = buf[pos] << 24 | (buf[pos + 1] & 0xFF) << 16 | (buf[pos + 2] & 0xFF) << 8 | (buf[pos + 3] & 0xFF); + int i2 = buf[pos + 4] << 24 | (buf[pos + 5] & 0xFF) << 16 | (buf[pos + 6] & 0xFF) << 8 | (buf[pos + 7] & 0xFF); + d[i] = Double.longBitsToDouble( + ((long) i1) << 32 | ((long) i2 & 0x00000000FFFFFFFFL)); + pos += 8; + } + } catch (EOFException e) { + return eofCheck(e, i, start, 8); + } + return 8 * len; + } + + /** For array reads return an EOF if unable to + * read any data. + */ + private int eofCheck(EOFException e, int i, int start, int length) + throws EOFException { + + if (i == start) { + throw e; + } else { + return (i - start) * length; + } + } + + /** Represent the stream as a string */ + public String toString() { + return super.toString() + "[count=" + count + ",pos=" + pos + "]"; + } +} diff --git a/src/nom/tam/util/BufferedDataOutputStream.java b/src/nom/tam/util/BufferedDataOutputStream.java new file mode 100644 index 0000000..2a5890a --- /dev/null +++ b/src/nom/tam/util/BufferedDataOutputStream.java @@ -0,0 +1,475 @@ +package nom.tam.util; + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +// What do we use in here? +import java.io.OutputStream; +import java.io.BufferedOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; + +/** This class is intended for high performance I/O in scientific applications. + * It combines the functionality of the BufferedOutputStream and the + * DataOutputStream as well as more efficient handling of arrays. + * This minimizes the number of method calls that are required to + * write data. Informal tests of this method show that it can + * be as much as 10 times faster than using a DataOutputStream layered + * on a BufferedOutputStream for writing large arrays. The performance + * gain on scalars or small arrays will be less but there should probably + * never be substantial degradation of performance. + *

+ * Note that there is substantial duplication of code to minimize method + * invocations. However simple output methods were used where empirical + * tests seemed to indicate that the simpler method did not cost any time. + * It seems likely that most of these variations will be + * washed out across different compilers and users who wish to tune + * the method for their particular system may wish to compare the + * the implementation of write(int[], int, int) with write(float[], int, int). + *

+ * Testing and timing for this class is + * peformed in the nom.tam.util.test.BufferedFileTester class. + */ +public class BufferedDataOutputStream + extends BufferedOutputStream + implements ArrayDataOutput { + + /** Use the BufferedOutputStream constructor + * @param o An open output stream. + */ + public BufferedDataOutputStream(OutputStream o) { + super(o, 32768); + } + + /** Use the BufferedOutputStream constructor + * @param o An open output stream. + * @param bufLength The buffer size. + */ + public BufferedDataOutputStream(OutputStream o, int bufLength) { + super(o, bufLength); + } + + /** Write a boolean value + * @param b The value to be written. Externally true is represented as + * a byte of 1 and false as a byte value of 0. + */ + public void writeBoolean(boolean b) throws IOException { + + checkBuf(1); + if (b) { + buf[count++] = 1; + } else { + buf[count++] = 0; + } + } + + /** Write a byte value. + */ + public void writeByte(int b) throws IOException { + checkBuf(1); + buf[count++] = (byte) b; + } + + /** Write an integer value. + */ + public void writeInt(int i) throws IOException { + + checkBuf(4); + buf[count++] = (byte) (i >>> 24); + buf[count++] = (byte) (i >>> 16); + buf[count++] = (byte) (i >>> 8); + buf[count++] = (byte) i; + } + + /** Write a short value. + */ + public void writeShort(int s) throws IOException { + + checkBuf(2); + buf[count++] = (byte) (s >>> 8); + buf[count++] = (byte) s; + + } + + /** Write a char value. + */ + public void writeChar(int c) throws IOException { + + checkBuf(2); + buf[count++] = (byte) (c >>> 8); + buf[count++] = (byte) c; + } + + /** Write a long value. + */ + public void writeLong(long l) throws IOException { + + checkBuf(8); + + buf[count++] = (byte) (l >>> 56); + buf[count++] = (byte) (l >>> 48); + buf[count++] = (byte) (l >>> 40); + buf[count++] = (byte) (l >>> 32); + buf[count++] = (byte) (l >>> 24); + buf[count++] = (byte) (l >>> 16); + buf[count++] = (byte) (l >>> 8); + buf[count++] = (byte) l; + } + + /** Write a float value. + */ + public void writeFloat(float f) throws IOException { + + checkBuf(4); + + int i = Float.floatToIntBits(f); + + buf[count++] = (byte) (i >>> 24); + buf[count++] = (byte) (i >>> 16); + buf[count++] = (byte) (i >>> 8); + buf[count++] = (byte) i; + + + } + + /** Write a double value. + */ + public void writeDouble(double d) throws IOException { + + checkBuf(8); + long l = Double.doubleToLongBits(d); + + buf[count++] = (byte) (l >>> 56); + buf[count++] = (byte) (l >>> 48); + buf[count++] = (byte) (l >>> 40); + buf[count++] = (byte) (l >>> 32); + buf[count++] = (byte) (l >>> 24); + buf[count++] = (byte) (l >>> 16); + buf[count++] = (byte) (l >>> 8); + buf[count++] = (byte) l; + + } + + /** Write a string using the local protocol to convert char's to bytes. + * + * @param s The string to be written. + */ + public void writeBytes(String s) throws IOException { + + write(s.getBytes(), 0, s.length()); + } + + /** Write a string as an array of chars. + */ + public void writeChars(String s) throws IOException { + + for (int i = 0; i < s.length(); i += 1) { + writeChar(s.charAt(i)); + } + } + + /** Write a string as a UTF. Note that this class does not + * handle this situation efficiently since it creates + * new DataOutputStream to handle each call. + */ + public void writeUTF(String s) throws IOException { + + // Punt on this one and use standard routines. + DataOutputStream d = new DataOutputStream(this); + d.writeUTF(s); + d.flush(); + d.close(); + } + + /** This routine provides efficient writing of arrays of any primitive type. + * The String class is also handled but it is an error to invoke this + * method with an object that is not an array of these types. If the + * array is multidimensional, then it calls itself recursively to write + * the entire array. Strings are written using the standard + * 1 byte format (i.e., as in writeBytes). + * + * If the array is an array of objects, then writePrimitiveArray will + * be called for each element of the array. + * + * @param o The object to be written. It must be an array of a primitive + * type, Object, or String. + */ + public void writePrimitiveArray(Object o) throws IOException { + writeArray(o); + } + + /** This routine provides efficient writing of arrays of any primitive type. + * The String class is also handled but it is an error to invoke this + * method with an object that is not an array of these types. If the + * array is multidimensional, then it calls itself recursively to write + * the entire array. Strings are written using the standard + * 1 byte format (i.e., as in writeBytes). + * + * If the array is an array of objects, then writePrimitiveArray will + * be called for each element of the array. + * + * @param o The object to be written. It must be an array of a primitive + * type, Object, or String. + */ + public void writeArray(Object o) throws IOException { + String className = o.getClass().getName(); + + if (className.charAt(0) != '[') { + throw new IOException("Invalid object passed to BufferedDataOutputStream.write" + className); + } + + // Is this a multidimensional array? If so process recursively. + if (className.charAt(1) == '[') { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + writeArray(((Object[]) o)[i]); + } + } else { + + // This is a one-d array. Process it using our special functions. + switch (className.charAt(1)) { + case 'Z': + write((boolean[]) o, 0, ((boolean[]) o).length); + break; + case 'B': + write((byte[]) o, 0, ((byte[]) o).length); + break; + case 'C': + write((char[]) o, 0, ((char[]) o).length); + break; + case 'S': + write((short[]) o, 0, ((short[]) o).length); + break; + case 'I': + write((int[]) o, 0, ((int[]) o).length); + break; + case 'J': + write((long[]) o, 0, ((long[]) o).length); + break; + case 'F': + write((float[]) o, 0, ((float[]) o).length); + break; + case 'D': + write((double[]) o, 0, ((double[]) o).length); + break; + case 'L': + + // Handle two exceptions: an array of strings, or an + // array of objects. . + if (className.equals("[Ljava.lang.String;")) { + write((String[]) o, 0, ((String[]) o).length); + } else if (className.equals("[Ljava.lang.Object;")) { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + writeArray(((Object[]) o)[i]); + } + } else { + throw new IOException("Invalid object passed to BufferedDataOutputStream.writeArray: " + className); + } + break; + default: + throw new IOException("Invalid object passed to BufferedDataOutputStream.writeArray: " + className); + } + } + + } + + /** Write an array of booleans. + */ + public void write(boolean[] b) throws IOException { + write(b, 0, b.length); + } + + /** Write a segment of an array of booleans. + */ + public void write(boolean[] b, int start, int len) throws IOException { + + for (int i = start; i < start + len; i += 1) { + + if (count + 1 > buf.length) { + checkBuf(1); + } + if (b[i]) { + buf[count++] = 1; + } else { + buf[count++] = 0; + } + } + } + + /** Write an array of shorts. + */ + public void write(short[] s) throws IOException { + write(s, 0, s.length); + } + + /** Write a segment of an array of shorts. + */ + public void write(short[] s, int start, int len) throws IOException { + + for (int i = start; i < start + len; i += 1) { + if (count + 2 > buf.length) { + checkBuf(2); + } + buf[count++] = (byte) (s[i] >> 8); + buf[count++] = (byte) (s[i]); + } + } + + /** Write an array of char's. + */ + public void write(char[] c) throws IOException { + write(c, 0, c.length); + } + + /** Write a segment of an array of char's. + */ + public void write(char[] c, int start, int len) throws IOException { + + for (int i = start; i < start + len; i += 1) { + if (count + 2 > buf.length) { + checkBuf(2); + } + buf[count++] = (byte) (c[i] >> 8); + buf[count++] = (byte) (c[i]); + } + } + + /** Write an array of int's. + */ + public void write(int[] i) throws IOException { + write(i, 0, i.length); + } + + /** Write a segment of an array of int's. + */ + public void write(int[] i, int start, int len) throws IOException { + + for (int ii = start; ii < start + len; ii += 1) { + if (count + 4 > buf.length) { + checkBuf(4); + } + + buf[count++] = (byte) (i[ii] >>> 24); + buf[count++] = (byte) (i[ii] >>> 16); + buf[count++] = (byte) (i[ii] >>> 8); + buf[count++] = (byte) (i[ii]); + + } + + } + + /** Write an array of longs. + */ + public void write(long[] l) throws IOException { + write(l, 0, l.length); + } + + /** Write a segement of an array of longs. + */ + public void write(long[] l, int start, int len) throws IOException { + + for (int i = start; i < start + len; i += 1) { + if (count + 8 > buf.length) { + checkBuf(8); + } + int t = (int) (l[i] >>> 32); + + buf[count++] = (byte) (t >>> 24); + buf[count++] = (byte) (t >>> 16); + buf[count++] = (byte) (t >>> 8); + buf[count++] = (byte) (t); + + t = (int) (l[i]); + + buf[count++] = (byte) (t >>> 24); + buf[count++] = (byte) (t >>> 16); + buf[count++] = (byte) (t >>> 8); + buf[count++] = (byte) (t); + } + } + + /** Write an array of floats. + */ + public void write(float[] f) throws IOException { + write(f, 0, f.length); + } + + public void write(float[] f, int start, int len) throws IOException { + + for (int i = start; i < start + len; i += 1) { + + if (count + 4 > buf.length) { + checkBuf(4); + } + int t = Float.floatToIntBits(f[i]); + buf[count++] = (byte) (t >>> 24); + buf[count++] = (byte) (t >>> 16); + buf[count++] = (byte) (t >>> 8); + buf[count++] = (byte) t; + } + } + + /** Write an array of doubles. + */ + public void write(double[] d) throws IOException { + write(d, 0, d.length); + } + + public void write(double[] d, int start, int len) throws IOException { + + for (int i = start; i < start + len; i += 1) { + if (count + 8 > buf.length) { + checkBuf(8); + } + long t = Double.doubleToLongBits(d[i]); + + int ix = (int) (t >>> 32); + + buf[count++] = (byte) (ix >>> 24); + buf[count++] = (byte) (ix >>> 16); + buf[count++] = (byte) (ix >>> 8); + buf[count++] = (byte) (ix); + + ix = (int) t; + + buf[count++] = (byte) (ix >>> 24); + buf[count++] = (byte) (ix >>> 16); + buf[count++] = (byte) (ix >>> 8); + buf[count++] = (byte) ix; + } + + } + + /** Write an array of Strings -- equivalent to calling writeBytes for each string. + */ + public void write(String[] s) throws IOException { + write(s, 0, s.length); + } + + /** Write a segment of an array of Strings. + * Equivalent to calling writeBytes for the selected elements. + */ + public void write(String[] s, int start, int len) throws IOException { + + // Do not worry about buffering this specially since the + // strings may be of differing lengths. + + for (int i = 0; i < s.length; i += 1) { + writeBytes(s[i]); + } + } + + /* See if there is enough space to add + * something to the buffer. + */ + protected void checkBuf(int need) throws IOException { + + if (count + need > buf.length) { + out.write(buf, 0, count); + count = 0; + } + } +} diff --git a/src/nom/tam/util/BufferedFile.java b/src/nom/tam/util/BufferedFile.java new file mode 100644 index 0000000..289ec15 --- /dev/null +++ b/src/nom/tam/util/BufferedFile.java @@ -0,0 +1,1150 @@ +package nom.tam.util; + +/* Copyright: Thomas McGlynn 1997-1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +/** This class is intended for high performance I/O in scientific applications. + * It adds buffering to the RandomAccessFile and also + * provides efficient handling of arrays. Primitive arrays + * may be written using a single method call. Large buffers + * are used to minimize synchronization overheads since methods + * of this class are not synchronized. + *

+ * Note that although this class supports most of the + * contract of RandomAccessFile it does not (and can not) + * extend that class since many of the methods of + * RandomAccessFile are final. In practice this + * method works much like the StreamFilter classes. + * All methods are implemented in this class but + * some are simply delegated to an underlying RandomAccessFile member. + *

+ * Testing and timing routines are available in + * the nom.tam.util.test.BufferedFileTester class. + * + * Version 1.1 October 12, 2000: Fixed handling of EOF in array reads + * so that a partial array will be returned when an EOF is detected. + * Excess bytes that cannot be used to construct array elements will + * be discarded (e.g., if there are 2 bytes left and the user is + * reading an int array). + * Version 1.2 December 8, 2002: Added getChannel method. + * Version 1.3 March 2, 2007: Added File based constructors. + * Version 1.4 July 20, 2009: Added support for >2G Object reads. + * This is still a bit problematic in that we do not support + * primitive arrays larger than 2 GB/atomsize. However except + * in the case of bytes this is not currently a major issue. + * + */ +import java.io.RandomAccessFile; +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.EOFException; + +public class BufferedFile + implements ArrayDataInput, ArrayDataOutput, RandomAccess { + + /** The current offset into the buffer */ + private int bufferOffset; + /** The number of valid characters in the buffer */ + private int bufferLength; + /** The declared length of the buffer array */ + private int bufferSize; + /** Counter used in reading arrays */ + private long primitiveArrayCount; + /** The data buffer. */ + private byte[] buffer; + /** The underlying access to the file system */ + private RandomAccessFile raf; + /** The offset of the beginning of the current buffer */ + private long fileOffset; + /** Is the buffer being used for input or output */ + private boolean doingInput; + + /** Create a read-only buffered file */ + public BufferedFile(String filename) throws IOException { + this(filename, "r", 32768); + } + + /** Create a buffered file with the given mode. + * @param filename The file to be accessed. + * @param mode A string composed of "r" and "w" for + * read and write access. + */ + public BufferedFile(String filename, String mode) throws IOException { + this(filename, mode, 32768); + } + + /** Create a buffered file from a File descriptor */ + public BufferedFile(File file) throws IOException { + this(file, "r", 32768); + } + + /** Create a buffered file from a File descriptor */ + public BufferedFile(File file, String mode) throws IOException { + this(file, mode, 32768); + } + + /** Create a buffered file with the given mode and a specified + * buffer size. + * @param filename The file to be accessed. + * @param mode A string composed of "r" and "w" indicating + * read or write access. + * @param buffer The buffer size to be used. This should be + * substantially larger than 100 bytes and + * defaults to 32768 bytes in the other + * constructors. + */ + public BufferedFile(String filename, String mode, int bufferSize) throws IOException { + + File file = new File(filename); + initialize(file, mode, bufferSize); + } + + /** Create a buffered file from a file descriptor */ + public BufferedFile(File file, String mode, int bufferSize) throws IOException { + initialize(file, mode, bufferSize); + } + + protected void initialize(File file, String mode, int bufferSize) throws IOException { + + raf = new RandomAccessFile(file, mode); + buffer = new byte[bufferSize]; + bufferOffset = 0; + bufferLength = 0; + fileOffset = 0; + this.bufferSize = bufferSize; + + } + + /** Create a buffered file using a mapped + + + /** Read an entire byte array. + * Note BufferedFile will return a partially filled array + * only at an end-of-file. + * @param buf The array to be filled. + */ + public int read(byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + /** Read into a segment of a byte array. + * @param buf The array to be filled. + * @param offset The starting location for input. + * @param length The number of bytes to be read. Fewer bytes + * will be read if an EOF is reached. + */ + public int read(byte[] buf, int offset, int len) throws IOException { + + checkBuffer(-1); + int total = 0; + + // Ensure that the entire buffer is read. + while (len > 0) { + + if (bufferOffset < bufferLength) { + + int get = len; + if (bufferOffset + get > bufferLength) { + get = bufferLength - bufferOffset; + } + System.arraycopy(buffer, bufferOffset, buf, offset, get); + len -= get; + bufferOffset += get; + offset += get; + total += get; + continue; + + } else { + + // This might be pretty long, but we know that the + // old buffer is exhausted. + try { + if (len > bufferSize) { + checkBuffer(bufferSize); + } else { + checkBuffer(len); + } + } catch (EOFException e) { + if (bufferLength > 0) { + System.arraycopy(buffer, 0, buf, offset, bufferLength); + total += bufferLength; + bufferLength = 0; + } + if (total == 0) { + throw e; + } else { + return total; + } + } + } + } + + return total; + } + + /** This should only be used when a small number of + * bytes is required (substantially smaller than + * bufferSize. + */ + private void checkBuffer(int needBytes) throws IOException { + + // Check if the buffer has some pending output. + if (!doingInput && bufferOffset > 0) { + flush(); + } + doingInput = true; + + if (bufferOffset + needBytes < bufferLength) { + return; + } + /* Move the last few bytes to the beginning of the buffer + * and read in enough data to fill the current demand. + */ + int len = bufferLength - bufferOffset; + + /* Note that new location that the beginning of the buffer + * corresponds to. + */ + fileOffset += bufferOffset; + if (len > 0) { + System.arraycopy(buffer, bufferOffset, buffer, 0, len); + } + needBytes -= len; + bufferLength = len; + bufferOffset = 0; + + while (needBytes > 0) { + len = raf.read(buffer, bufferLength, bufferSize - bufferLength); + if (len < 0) { + throw new EOFException(); + } + needBytes -= len; + bufferLength += len; + } + } + + /** Read a byte */ + public int read() throws IOException { + checkBuffer(1); + bufferOffset += 1; + return buffer[bufferOffset - 1]; + } + + /** Skip from the current position. + * @param offset The number of bytes from the + * current position. This may + * be negative. + */ + public long skip(long offset) throws IOException { + + if (offset > 0 && fileOffset + bufferOffset + offset > raf.length()) { + offset = raf.length() - fileOffset - bufferOffset; + seek(raf.length()); + } else if (fileOffset + bufferOffset + offset < 0) { + offset = -(fileOffset + bufferOffset); + seek(0); + } else { + seek(fileOffset + bufferOffset + offset); + } + return offset; + } + + /** Move to the current offset from the beginning of the file. + * A user may move past the end of file but this + * does not extend the file unless data is written there. + */ + public void seek(long offsetFromStart) throws IOException { + + if (!doingInput) { + // Have to flush before a seek... + flush(); + } + + // Are we within the current buffer? + if (fileOffset <= offsetFromStart && offsetFromStart < fileOffset + bufferLength) { + bufferOffset = (int) (offsetFromStart - fileOffset); + } else { + + // Seek to the desired location. + if (offsetFromStart < 0) { + offsetFromStart = 0; + } + + fileOffset = offsetFromStart; + raf.seek(fileOffset); + + // Invalidate the current buffer. + bufferLength = 0; + bufferOffset = 0; + } + } + + /** Read a boolean + * @return a boolean generated from the next + * byte in the input. + */ + public boolean readBoolean() throws IOException { + return convertToBoolean(); + } + + /** Get a boolean from the buffer */ + private boolean convertToBoolean() throws IOException { + checkBuffer(1); + bufferOffset += 1; + return buffer[bufferOffset - 1] == 1; + } + + /** Read a byte + * @return the next byte in the input. + */ + public byte readByte() throws IOException { + checkBuffer(1); + bufferOffset += 1; + return buffer[bufferOffset - 1]; + } + + /** Read an unsigned byte. + * @return the unsigned value of the next byte as + * an integer. + */ + public int readUnsignedByte() throws IOException { + checkBuffer(1); + bufferOffset += 1; + return buffer[bufferOffset - 1] | 0x00ff; + } + + /** Read an int + * @return an integer read from the input. + */ + public int readInt() throws IOException { + return convertToInt(); + } + + /** Get an integer value from the buffer */ + private int convertToInt() throws IOException { + checkBuffer(4); + int x = bufferOffset; + int i = buffer[x] << 24 | (buffer[x + 1] & 0xFF) << 16 | (buffer[x + 2] & 0xFF) << 8 | (buffer[x + 3] & 0xFF); + bufferOffset += 4; + return i; + } + + /** Read a short + * @return a short read from the input. + */ + public short readShort() throws IOException { + return convertToShort(); + } + + /** Get a short from the buffer */ + private short convertToShort() throws IOException { + checkBuffer(2); + short s = (short) (buffer[bufferOffset] << 8 | (buffer[bufferOffset + 1] & 0xFF)); + bufferOffset += 2; + return s; + } + + /** Read an unsigned short. + * @return an unsigned short value as an integer. + */ + public int readUnsignedShort() throws IOException { + return readShort() & 0xFFFF; + } + + /** Read a char + * @return a char read from the input. + */ + public char readChar() throws IOException { + return convertToChar(); + } + + /** Get a char from the buffer */ + private char convertToChar() throws IOException { + checkBuffer(2); + char c = (char) (buffer[bufferOffset] << 8 | (buffer[bufferOffset + 1] & 0xFF)); + bufferOffset += 2; + return c; + } + + /** Read a long. + * @return a long value read from the input. + */ + public long readLong() throws IOException { + return convertToLong(); + } + + /** Get a long value from the buffer */ + private long convertToLong() throws IOException { + checkBuffer(8); + int x = bufferOffset; + + int i1 = buffer[x] << 24 | (buffer[x + 1] & 0xFF) << 16 | (buffer[x + 2] & 0xFF) << 8 | (buffer[x + 3] & 0xFF); + int i2 = buffer[x + 4] << 24 | (buffer[x + 5] & 0xFF) << 16 | (buffer[x + 6] & 0xFF) << 8 | (buffer[x + 7] & 0xFF); + bufferOffset += 8; + + return (((long) i1) << 32) | (((long) i2) & 0x00000000ffffffffL); + } + + /** Read a float. + * @return a float value read from the input. + */ + public float readFloat() throws IOException { + return Float.intBitsToFloat(convertToInt()); + } + + /** Read a double. + * @return a double value read from the input. + */ + public double readDouble() throws IOException { + return Double.longBitsToDouble(convertToLong()); + } + + /** Read a byte array fully. + * Since the read method of this class reads an entire + * buffer, the only difference with readFully is that + * readFully will signal an EOF if the buffer cannot be filled. + */ + public void readFully(byte[] b) throws IOException { + readFully(b, 0, b.length); + } + + /** Read a byte array fully. + * Since the read method of this class reads an entire + * buffer, the only difference with readFully is that + * readFully will signal an EOF if the buffer cannot be filled. + */ + public void readFully(byte[] b, int off, int len) throws IOException { + + if (off < 0 || len < 0 || off + len > b.length) { + throw new IOException("Attempt to read outside byte array"); + } + + if (read(b, off, len) < len) { + throw new EOFException(); + } + } + + /** Skip the number of bytes. + * This differs from the skip method in that + * it will throw an EOF if a forward skip cannot + * be fully accomplished... (However that isn't + * supposed to happen with a random access file, + * so there is probably no operational difference). + */ + public int skipBytes(int toSkip) throws IOException { + return (int) skipBytes((long) toSkip); + } + + public long skipBytes(long toSkip) throws IOException { + + // Note that we allow negative skips... + if (skip(toSkip) < toSkip) { + throw new EOFException(); + } else { + return toSkip; + } + } + + /** Read a string encoded as a UTF. + * @return the string. + */ + public String readUTF() throws IOException { + checkBuffer(-1); + raf.seek(fileOffset + bufferOffset); + String utf = raf.readUTF(); + fileOffset = raf.getFilePointer(); + + // Invalidate the buffer. + bufferLength = 0; + bufferOffset = 0; + + return utf; + } + + /** Read a line of input. + * @return the next line. + */ + public String readLine() throws IOException { + + checkBuffer(-1); + raf.seek(fileOffset + bufferOffset); + String line = raf.readLine(); + fileOffset = raf.getFilePointer(); + + // Invalidate the buffer. + bufferLength = 0; + bufferOffset = 0; + + return line; + } + + /** This routine provides efficient reading of arrays of any primitive type. + * @deprecated The readLArray(Object) routine should be used to + * ensure that large arrays which read more than + * two-gigabytes return the proper value. + * + * + * @param o The object to be read. It must be an array of a primitive type, + * or an array of Object's. + */ + public int readArray(Object o) throws IOException { + return (int) readLArray(o); + } + + /** This routine provides efficient reading of arrays of any primitive + * type. + * @param o The object to be read. It must be an arraof of a primtive type + * (or any dimension), or an array of Objects which contains + * pointers to primitive arrays or other object arrays. + */ + public long readLArray(Object o) throws IOException { + + + // Note that we assume that only a single thread is + // doing a primitive Array read at any given time. Otherwise + // primitiveArrayCount can be wrong and also the + // input data can be mixed up. If this assumption is not + // true we need to synchronize this call. + + primitiveArrayCount = 0; + return primitiveArrayRecurse(o); + } + + protected long primitiveArrayRecurse(Object o) throws IOException { + + if (o == null) { + return primitiveArrayCount; + } + + String className = o.getClass().getName(); + + if (className.charAt(0) != '[') { + throw new IOException("Invalid object passed to BufferedDataInputStream.readArray:" + className); + } + + // Is this a multidimensional array? If so process recursively. + if (className.charAt(1) == '[') { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + primitiveArrayRecurse(((Object[]) o)[i]); + } + } else { + + // This is a one-d array. Process it using our special functions. + switch (className.charAt(1)) { + case 'Z': + primitiveArrayCount += read((boolean[]) o, 0, ((boolean[]) o).length); + break; + case 'B': + int len = read((byte[]) o, 0, ((byte[]) o).length); + break; + case 'C': + primitiveArrayCount += read((char[]) o, 0, ((char[]) o).length); + break; + case 'S': + primitiveArrayCount += read((short[]) o, 0, ((short[]) o).length); + break; + case 'I': + primitiveArrayCount += read((int[]) o, 0, ((int[]) o).length); + break; + case 'J': + primitiveArrayCount += read((long[]) o, 0, ((long[]) o).length); + break; + case 'F': + primitiveArrayCount += read((float[]) o, 0, ((float[]) o).length); + break; + case 'D': + primitiveArrayCount += read((double[]) o, 0, ((double[]) o).length); + break; + case 'L': + + // Handle an array of Objects by recursion. Anything + // else is an error. + if (className.equals("[Ljava.lang.Object;")) { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + primitiveArrayRecurse(((Object[]) o)[i]); + } + } else { + throw new IOException("Invalid object passed to BufferedFile.readPrimitiveArray: " + className); + } + break; + default: + throw new IOException("Invalid object passed to BufferedDataInputStream.readArray: " + className); + } + } + return primitiveArrayCount; + } + + public int read(boolean[] b) throws IOException { + return read(b, 0, b.length); + } + + public int read(boolean[] b, int start, int length) throws IOException { + + int i = start; + try { + for (; i < start + length; i += 1) { + b[i] = convertToBoolean(); + } + return length; + } catch (EOFException e) { + return eofCheck(e, start, i, 1); + } + } + + public int read(short[] s) throws IOException { + return read(s, 0, s.length); + } + + public int read(short[] s, int start, int length) throws IOException { + + int i = start; + try { + for (; i < start + length; i += 1) { + s[i] = convertToShort(); + } + return length * 2; + } catch (EOFException e) { + return eofCheck(e, start, i, 2); + } + } + + public int read(char[] c) throws IOException { + return read(c, 0, c.length); + } + + public int read(char[] c, int start, int length) throws IOException { + + int i = start; + try { + for (; i < start + length; i += 1) { + c[i] = convertToChar(); + } + return length * 2; + } catch (EOFException e) { + return eofCheck(e, start, i, 2); + } + } + + public int read(int[] i) throws IOException { + return read(i, 0, i.length); + } + + public int read(int[] i, int start, int length) throws IOException { + + int ii = start; + try { + for (; ii < start + length; ii += 1) { + i[ii] = convertToInt(); + } + return length * 4; + } catch (EOFException e) { + return eofCheck(e, start, ii, 4); + } + } + + public int read(long[] l) throws IOException { + return read(l, 0, l.length); + } + + public int read(long[] l, int start, int length) throws IOException { + + int i = start; + try { + for (; i < start + length; i += 1) { + l[i] = convertToLong(); + } + return length * 8; + } catch (EOFException e) { + return eofCheck(e, start, i, 8); + } + + } + + public int read(float[] f) throws IOException { + return read(f, 0, f.length); + } + + public int read(float[] f, int start, int length) throws IOException { + + int i = start; + try { + for (; i < start + length; i += 1) { + f[i] = Float.intBitsToFloat(convertToInt()); + } + return length * 4; + } catch (EOFException e) { + return eofCheck(e, start, i, 4); + } + } + + public int read(double[] d) throws IOException { + return read(d, 0, d.length); + } + + public int read(double[] d, int start, int length) throws IOException { + + int i = start; + try { + for (; i < start + length; i += 1) { + d[i] = Double.longBitsToDouble(convertToLong()); + } + return length * 8; + } catch (EOFException e) { + return eofCheck(e, start, i, 8); + } + } + + /** See if an exception should be thrown during an array read. */ + private int eofCheck(EOFException e, int start, int index, int length) + throws EOFException { + if (start == index) { + throw e; + } else { + return (index - start) * length; + } + } + + /**** Output Routines ****/ + private void needBuffer(int need) throws IOException { + + if (doingInput) { + + fileOffset += bufferOffset; + raf.seek(fileOffset); + + doingInput = false; + + bufferOffset = 0; + bufferLength = 0; + } + + if (bufferOffset + need >= bufferSize) { + raf.write(buffer, 0, bufferOffset); + fileOffset += bufferOffset; + bufferOffset = 0; + } + } + + public void write(int buf) throws IOException { + convertFromByte(buf); + } + + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + + public void write(byte[] buf, int offset, int length) throws IOException { + + if (length < bufferSize) { + /* If we can use the buffer do so... */ + needBuffer(length); + System.arraycopy(buf, offset, buffer, bufferOffset, length); + bufferOffset += length; + } else { + /* Otherwise flush the buffer and write the data directly. + * Make sure that we indicate that the buffer is clean when + * we're done. + */ + flush(); + + raf.write(buf, offset, length); + + fileOffset += length; + + doingInput = false; + bufferOffset = 0; + bufferLength = 0; + } + } + + /** Flush output buffer if necessary. + * This method is not present in RandomAccessFile + * but users may need to call flush to ensure + * that data has been written. + */ + public void flush() throws IOException { + + if (!doingInput && bufferOffset > 0) { + raf.write(buffer, 0, bufferOffset); + fileOffset += bufferOffset; + bufferOffset = 0; + bufferLength = 0; + } + } + + /** Clear up any pending output at cleanup. + */ + protected void finalize() { + try { + if (getFD().valid()) { + flush(); + close(); + } + } catch (Exception e) { + } + } + + /** Write a boolean value + * @param b The value to be written. Externally true is represented as + * a byte of 1 and false as a byte value of 0. + */ + public void writeBoolean(boolean b) throws IOException { + convertFromBoolean(b); + } + + private void convertFromBoolean(boolean b) throws IOException { + needBuffer(1); + if (b) { + buffer[bufferOffset] = (byte) 1; + } else { + buffer[bufferOffset] = (byte) 0; + } + bufferOffset += 1; + } + + /** Write a byte value. + */ + public void writeByte(int b) throws IOException { + convertFromByte(b); + } + + private void convertFromByte(int b) throws IOException { + needBuffer(1); + buffer[bufferOffset++] = (byte) b; + } + + /** Write an integer value. + */ + public void writeInt(int i) throws IOException { + convertFromInt(i); + } + + private void convertFromInt(int i) throws IOException { + + needBuffer(4); + buffer[bufferOffset++] = (byte) (i >>> 24); + buffer[bufferOffset++] = (byte) (i >>> 16); + buffer[bufferOffset++] = (byte) (i >>> 8); + buffer[bufferOffset++] = (byte) i; + } + + /** Write a short value. + */ + public void writeShort(int s) throws IOException { + + convertFromShort(s); + } + + private void convertFromShort(int s) throws IOException { + needBuffer(2); + + buffer[bufferOffset++] = (byte) (s >>> 8); + buffer[bufferOffset++] = (byte) s; + } + + /** Write a char value. + */ + public void writeChar(int c) throws IOException { + convertFromChar(c); + } + + private void convertFromChar(int c) throws IOException { + needBuffer(2); + buffer[bufferOffset++] = (byte) (c >>> 8); + buffer[bufferOffset++] = (byte) c; + } + + /** Write a long value. + */ + public void writeLong(long l) throws IOException { + convertFromLong(l); + } + + private void convertFromLong(long l) throws IOException { + needBuffer(8); + + buffer[bufferOffset++] = (byte) (l >>> 56); + buffer[bufferOffset++] = (byte) (l >>> 48); + buffer[bufferOffset++] = (byte) (l >>> 40); + buffer[bufferOffset++] = (byte) (l >>> 32); + buffer[bufferOffset++] = (byte) (l >>> 24); + buffer[bufferOffset++] = (byte) (l >>> 16); + buffer[bufferOffset++] = (byte) (l >>> 8); + buffer[bufferOffset++] = (byte) l; + } + + /** Write a float value. + */ + public void writeFloat(float f) throws IOException { + convertFromInt(Float.floatToIntBits(f)); + } + + /** Write a double value. + */ + public void writeDouble(double d) throws IOException { + convertFromLong(Double.doubleToLongBits(d)); + } + + /** Write a string using the local protocol to convert char's to bytes. + * + * @param s The string to be written. + */ + public void writeBytes(String s) throws IOException { + write(s.getBytes(), 0, s.length()); + } + + /** Write a string as an array of chars. + */ + public void writeChars(String s) throws IOException { + + int len = s.length(); + for (int i = 0; i < len; i += 1) { + convertFromChar(s.charAt(i)); + } + } + + /** Write a string as a UTF. + */ + public void writeUTF(String s) throws IOException { + flush(); + raf.writeUTF(s); + fileOffset = raf.getFilePointer(); + } + + /** This routine provides efficient writing of arrays of any primitive type. + * The String class is also handled but it is an error to invoke this + * method with an object that is not an array of these types. If the + * array is multidimensional, then it calls itself recursively to write + * the entire array. Strings are written using the standard + * 1 byte format (i.e., as in writeBytes). + * + * If the array is an array of objects, then write will + * be called for each element of the array. + * + * @param o The object to be written. It must be an array of a primitive + * type, Object, or String. + */ + public void writeArray(Object o) throws IOException { + String className = o.getClass().getName(); + + if (className.charAt(0) != '[') { + throw new IOException("Invalid object passed to BufferedFile.writeArray:" + className); + } + + // Is this a multidimensional array? If so process recursively. + if (className.charAt(1) == '[') { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + writeArray(((Object[]) o)[i]); + } + } else { + + // This is a one-d array. Process it using our special functions. + switch (className.charAt(1)) { + case 'Z': + write((boolean[]) o, 0, ((boolean[]) o).length); + break; + case 'B': + write((byte[]) o, 0, ((byte[]) o).length); + break; + case 'C': + write((char[]) o, 0, ((char[]) o).length); + break; + case 'S': + write((short[]) o, 0, ((short[]) o).length); + break; + case 'I': + write((int[]) o, 0, ((int[]) o).length); + break; + case 'J': + write((long[]) o, 0, ((long[]) o).length); + break; + case 'F': + write((float[]) o, 0, ((float[]) o).length); + break; + case 'D': + write((double[]) o, 0, ((double[]) o).length); + break; + case 'L': + + // Handle two exceptions: an array of strings, or an + // array of objects. . + if (className.equals("[Ljava.lang.String;")) { + write((String[]) o, 0, ((String[]) o).length); + } else if (className.equals("[Ljava.lang.Object;")) { + for (int i = 0; i < ((Object[]) o).length; i += 1) { + writeArray(((Object[]) o)[i]); + } + } else { + throw new IOException("Invalid object passed to BufferedFile.write: " + className); + } + break; + default: + throw new IOException("Invalid object passed to BufferedFile.write: " + className); + } + } + + } + + /** Write an array of booleans. + */ + public void write(boolean[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(boolean[] b, int start, int length) throws IOException { + for (int i = start; i < start + length; i += 1) { + convertFromBoolean(b[i]); + } + } + + /** Write an array of shorts. + */ + public void write(short[] s) throws IOException { + write(s, 0, s.length); + } + + public void write(short[] s, int start, int length) throws IOException { + + for (int i = start; i < start + length; i += 1) { + convertFromShort(s[i]); + } + } + + /** Write an array of char's. + */ + public void write(char[] c) throws IOException { + write(c, 0, c.length); + } + + public void write(char[] c, int start, int length) throws IOException { + + for (int i = start; i < start + length; i += 1) { + convertFromChar(c[i]); + } + } + + /** Write an array of int's. + */ + public void write(int[] i) throws IOException { + write(i, 0, i.length); + } + + public void write(int[] i, int start, int length) throws IOException { + for (int ii = start; ii < start + length; ii += 1) { + convertFromInt(i[ii]); + } + } + + /** Write an array of longs. + */ + public void write(long[] l) throws IOException { + write(l, 0, l.length); + } + + public void write(long[] l, int start, int length) throws IOException { + + for (int i = start; i < start + length; i += 1) { + convertFromLong(l[i]); + } + } + + /** Write an array of floats. + */ + public void write(float[] f) throws IOException { + write(f, 0, f.length); + } + + public void write(float[] f, int start, int length) throws IOException { + for (int i = start; i < start + length; i += 1) { + convertFromInt(Float.floatToIntBits(f[i])); + } + } + + /** Write an array of doubles. + */ + public void write(double[] d) throws IOException { + write(d, 0, d.length); + } + + public void write(double[] d, int start, int length) throws IOException { + + for (int i = start; i < start + length; i += 1) { + convertFromLong(Double.doubleToLongBits(d[i])); + } + } + + /** Write an array of Strings -- equivalent to calling writeBytes for each string. + */ + public void write(String[] s) throws IOException { + write(s, 0, s.length); + } + + public void write(String[] s, int start, int length) throws IOException { + for (int i = start; i < start + length; i += 1) { + writeBytes(s[i]); + } + } + + /** Close the file */ + public void close() throws IOException { + flush(); + raf.close(); + } + + /** Get the file descriptor associated with + * this stream. Note that this returns the file + * descriptor of the associated RandomAccessFile. + */ + public FileDescriptor getFD() throws IOException { + return raf.getFD(); + } + + /** Get the channel associated with + * this file. Note that this returns the channel + * of the associated RandomAccessFile. + * Note that since the BufferedFile buffers the I/O's to the + * underlying file, the offset of the channel may be + * different than the offset of the BufferedFile. This + * is different than for a RandomAccessFile where the + * offsets are guaranteed to be the same. + */ + public java.nio.channels.FileChannel getChannel() { + return raf.getChannel(); + } + + /** Get the current length of the file. + */ + public long length() throws IOException { + flush(); + return raf.length(); + } + + /** Get the current offset into the file. + */ + public long getFilePointer() { + return fileOffset + bufferOffset; + } + + /** Set the length of the file. This method calls + * the method of the same name in RandomAccessFile which + * is only available in JDK1.2 and greater. This method + * may be deleted for compilation with earlier versions. + * + * @param newLength The number of bytes at which the file + * is set. + * + */ + public void setLength(long newLength) throws IOException { + + flush(); + raf.setLength(newLength); + if (newLength < fileOffset) { + fileOffset = newLength; + } + + } +} diff --git a/src/nom/tam/util/ByteFormatter.java b/src/nom/tam/util/ByteFormatter.java new file mode 100644 index 0000000..f704f32 --- /dev/null +++ b/src/nom/tam/util/ByteFormatter.java @@ -0,0 +1,869 @@ +package nom.tam.util; + +/** This class provides mechanisms for + * efficiently formatting numbers and Strings. + * Data is appended to existing byte arrays. Note + * that the formatting of real or double values + * may differ slightly (in the last bit) from + * the standard Java packages since this routines + * are optimized for speed rather than accuracy. + *

+ * The methods in this class create no objects. + *

+ * If a number cannot fit into the requested space + * the truncateOnOverlow flag controls whether the + * formatter will attempt to append it using the + * available length in the output (a la C or Perl style + * formats). If this flag is set, or if the number + * cannot fit into space left in the buffer it is 'truncated' + * and the requested space is filled with a truncation fill + * character. A TruncationException may be thrown if the truncationThrow + * flag is set. + *

+ * This class does not explicitly support separate methods + * for formatting reals in exponential notation. Real numbers + * near one are by default formatted in decimal notation while + * numbers with large (or very negative) exponents are formatted + * in exponential notation. By setting the limits at which these + * transitions take place the user can force either exponential or + * decimal notation. + * + */ +public final class ByteFormatter { + + /** Internal buffers used in formatting fields */ + private byte[] tbuf1 = new byte[32]; + private byte[] tbuf2 = new byte[32]; + private static final double ilog10 = 1. / Math.log(10); + /** Should we truncate overflows or just run over limit */ + private boolean truncateOnOverflow = true; + /** What do we use to fill when we cannot print the number? */ + private byte truncationFill = (byte) '*'; // Default is often used in Fortran + /** Throw exception on truncations */ + private boolean truncationThrow = true; + /** Should we right align? */ + private boolean align = false; + /** Minimum magnitude to print in non-scientific notation. */ + double simpleMin = 1.e-3; + /** Maximum magnitude to print in non-scientific notation. */ + double simpleMax = 1.e6; + /** Powers of 10. We overextend on both sides. + * These should perhaps be tabulated rather than + * computed though it may be faster to calculate + * them than to read in the extra bytes in the class file. + */ + private static final double tenpow[]; + /** What index of tenpow is 10^0 */ + private static final int zeropow; + + static { // Static initializer + + int min = (int) Math.floor((int) (Math.log(Double.MIN_VALUE) * ilog10)); + int max = (int) Math.floor((int) (Math.log(Double.MAX_VALUE) * ilog10)); + max += 1; + + tenpow = new double[(max - min) + 1]; + + + for (int i = 0; i < tenpow.length; i += 1) { + tenpow[i] = Math.pow(10, i + min); + } + zeropow = -min; + } + /** Digits. We could handle other bases + * by extending or truncating this list and changing + * the division by 10 (and it's factors) at various + * locations. + */ + private static final byte[] digits = { + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', + (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9'}; + + /** Set the truncation behavior. + * @param val If set to true (the default) then do not + * exceed the requested length. If a number cannot + * be sensibly formatted, the truncation fill character + * may be inserted. + */ + public void setTruncateOnOverflow(boolean val) { + truncateOnOverflow = val; + } + + /** Should truncations cause a truncation overflow? */ + public void setTruncationThrow(boolean throwException) { + truncationThrow = throwException; + } + + /** Set the truncation fill character. + * @param val The character to be used in subsequent truncations. + */ + public void setTruncationFill(char val) { + truncationFill = (byte) val; + } + + /** Set the alignment flag. + * @param val Should numbers be right aligned? + */ + public void setAlign(boolean val) { + align = val; + } + + /** Set the range of real numbers that will be formatted in + * non-scientific notation, i.e., .00001 rather than 1.0e-5. + * The sign of the number is ignored. + * @param min The minimum value for non-scientific notation. + * @param max The maximum value for non-scientific notation. + */ + public void setSimpleRange(double min, double max) { + simpleMin = min; + simpleMax = max; + } + + /** Format an int into an array. + * @param val The int to be formatted. + * @param array The array in which to place the result. + * @return The number of characters used. + */ + public int format(int val, byte[] array) throws TruncationException { + return format(val, array, 0, array.length); + } + + /** Format an int into an existing array. + * @param val Integer to be formatted + * @param buf Buffer in which result is to be stored + * @param off Offset within buffer + * @param len Maximum length of integer + * @return offset of next unused character in input buffer. + */ + public int format(int val, byte[] buf, + int off, int len) throws TruncationException { + + // Special case + if (val == Integer.MIN_VALUE) { + if (len > 10 || (!truncateOnOverflow && buf.length - off > 10)) { + return format("-2147483648", buf, off, len); + } else { + truncationFiller(buf, off, len); + return off + len; + } + } + + int pos = Math.abs(val); + + // First count the number of characters in the result. + // Otherwise we need to use an intermediary buffer. + + int ndig = 1; + int dmax = 10; + + while (ndig < 10 && pos >= dmax) { + ndig += 1; + dmax *= 10; + } + + if (val < 0) { + ndig += 1; + } + + // Truncate if necessary. + if ((truncateOnOverflow && ndig > len) || ndig > buf.length - off) { + truncationFiller(buf, off, len); + return off + len; + } + + // Right justify if requested. + if (align) { + off = alignFill(buf, off, len - ndig); + } + + // Now insert the actual characters we want -- backwards + // We use a do{} while() to handle the case of 0. + + off += ndig; + + int xoff = off - 1; + do { + buf[xoff] = digits[pos % 10]; + xoff -= 1; + pos /= 10; + } while (pos > 0); + + if (val < 0) { + buf[xoff] = (byte) '-'; + } + + return off; + } + + /** Format a long into an array. + * @param val The long to be formatted. + * @param array The array in which to place the result. + * @return The number of characters used. + */ + public int format(long val, byte[] array) throws TruncationException { + return format(val, array, 0, array.length); + } + + /** Format a long into an existing array. + * @param val Long to be formatted + * @param buf Buffer in which result is to be stored + * @param off Offset within buffer + * @param len Maximum length of integer + * @return offset of next unused character in input buffer. + */ + public int format(long val, byte[] buf, + int off, int len) throws TruncationException { + + // Special case + if (val == Long.MIN_VALUE) { + if (len > 19 || (!truncateOnOverflow && buf.length - off > 19)) { + return format("-9223372036854775808", buf, off, len); + } else { + truncationFiller(buf, off, len); + return off + len; + } + } + + long pos = Math.abs(val); + + // First count the number of characters in the result. + // Otherwise we need to use an intermediary buffer. + + int ndig = 1; + long dmax = 10; + + // Might be faster to try to do this partially in ints + while (ndig < 19 && pos >= dmax) { + + ndig += 1; + dmax *= 10; + } + + if (val < 0) { + ndig += 1; + } + + // Truncate if necessary. + + if ((truncateOnOverflow && ndig > len) || ndig > buf.length - off) { + truncationFiller(buf, off, len); + return off + len; + } + + // Right justify if requested. + if (align) { + off = alignFill(buf, off, len - ndig); + } + + // Now insert the actual characters we want -- backwards. + + + off += ndig; + int xoff = off - 1; + + buf[xoff] = (byte) '0'; + boolean last = (pos == 0); + + while (!last) { + + // Work on ints rather than longs. + + int giga = (int) (pos % 1000000000L); + pos /= 1000000000L; + + last = (pos == 0); + + for (int i = 0; i < 9; i += 1) { + + buf[xoff] = digits[giga % 10]; + xoff -= 1; + giga /= 10; + if (last && giga == 0) { + break; + } + } + } + + + if (val < 0) { + buf[xoff] = (byte) '-'; + } + + return off; + } + + /** Format a boolean into an existing array. + */ + public int format(boolean val, byte[] array) { + return format(val, array, 0, array.length); + } + + /** Format a boolean into an existing array + * @param val The boolean to be formatted + * @param array The buffer in which to format the data. + * @param off The starting offset within the buffer. + * @param len The maximum number of characters to use + * use in formatting the number. + * @return Offset of next available character in buffer. + */ + public int format(boolean val, byte[] array, int off, + int len) { + if (align && len > 1) { + off = alignFill(array, off, len - 1); + } + + if (len > 0) { + if (val) { + array[off] = (byte) 'T'; + } else { + array[off] = (byte) 'F'; + } + off += 1; + } + return off; + } + + /** Insert a string at the beginning of an array */ + public int format(String val, byte[] array) { + return format(val, array, 0, array.length); + } + + /** Insert a String into an existing character array. + * If the String is longer than len, then only the + * the initial len characters will be inserted. + * @param val The string to be inserted. A null string + * will insert len spaces. + * @param array The buffer in which to insert the string. + * @param off The starting offset to insert the string. + * @param len The maximum number of characters to insert. + * @return Offset of next available character in buffer. + */ + public int format(String val, byte[] array, int off, int len) { + + if (val == null) { + for (int i = 0; i < len; i += 1) { + array[off + i] = (byte) ' '; + } + return off + len; + } + + int slen = val.length(); + + if ((truncateOnOverflow && slen > len) || (slen > array.length - off)) { + val = val.substring(0, len); + slen = len; + } + + if (align && (len > slen)) { + off = alignFill(array, off, len - slen); + } + + /** We should probably require ASCII here, but for the nonce we do not [TAM 5/11] */ + System.arraycopy(val.getBytes(), 0, array, off, slen); + return off + slen; + } + + /** Format a float into an array. + * @param val The float to be formatted. + * @param array The array in which to place the result. + * @return The number of characters used. + */ + public int format(float val, byte[] array) throws TruncationException { + return format(val, array, 0, array.length); + } + + /** Format a float into an existing byteacter array. + *

+ * This is hard to do exactly right... The JDK code does + * stuff with rational arithmetic and so forth. + * We use a much simpler algorithm which may give + * an answer off in the lowest order bit. + * Since this is pure Java, it should still be consistent + * from machine to machine. + *

+ * Recall that the binary representation of + * the float is of the form d = 0.bbbbbbbb x 2n + * where there are up to 24 binary digits in the binary + * fraction (including the assumed leading 1 bit + * for normalized numbers). + * We find a value m such that 10m d is between + * 224 and >232. + * This product will be exactly convertible to an int + * with no loss of precision. Getting the + * decimal representation for that is trivial (see formatInteger). + * This is a decimal mantissa and we have an exponent (-m). + * All we have to do is manipulate the decimal point + * to where we want to see it. Errors can + * arise due to roundoff in the scaling multiplication, but + * should be very small. + * + * @param val Float to be formatted + * @param buf Buffer in which result is to be stored + * @param off Offset within buffer + * @param len Maximum length of field + * @return Offset of next character in buffer. + */ + public int format(float val, byte[] buf, + int off, int len) throws TruncationException { + + float pos = (float) Math.abs(val); + + int minlen, actlen; + + // Special cases + if (pos == 0.) { + return format("0.0", buf, off, len); + } else if (Float.isNaN(val)) { + return format("NaN", buf, off, len); + } else if (Float.isInfinite(val)) { + if (val > 0) { + return format("Infinity", buf, off, len); + } else { + return format("-Infinity", buf, off, len); + } + } + + int power = (int) Math.floor((Math.log(pos) * ilog10)); + int shift = 8 - power; + float scale; + float scale2 = 1; + + // Scale the number so that we get a number ~ n x 10^8. + if (shift < 30) { + scale = (float) tenpow[shift + zeropow]; + } else { + // Can get overflow if the original number is + // very small, so we break out the shift + // into two multipliers. + scale2 = (float) tenpow[30 + zeropow]; + scale = (float) tenpow[shift - 30 + zeropow]; + } + + + pos = (pos * scale) * scale2; + + // Parse the float bits. + + int bits = Float.floatToIntBits(pos); + + // The exponent should be a little more than 23 + int exp = ((bits & 0x7F800000) >> 23) - 127; + + int numb = (bits & 0x007FFFFF); + + if (exp > -127) { + // Normalized.... + numb |= (0x00800000); + } else { + // Denormalized + exp += 1; + } + + + // Multiple this number by the excess of the exponent + // over 24. This completes the conversion of float to int + // (<<= did not work on Alpha TruUnix) + + numb = numb << (exp - 23L); + + // Get a decimal mantissa. + boolean oldAlign = align; + align = false; + int ndig = format(numb, tbuf1, 0, 32); + align = oldAlign; + + + // Now format the float. + + return combineReal(val, buf, off, len, tbuf1, ndig, shift); + } + + /** Format a double into an array. + * @param val The double to be formatted. + * @param array The array in which to place the result. + * @return The number of characters used. + */ + public int format(double val, byte[] array) throws TruncationException { + return format(val, array, 0, array.length); + } + + /** Format a double into an existing character array. + *

+ * This is hard to do exactly right... The JDK code does + * stuff with rational arithmetic and so forth. + * We use a much simpler algorithm which may give + * an answer off in the lowest order bit. + * Since this is pure Java, it should still be consistent + * from machine to machine. + *

+ * Recall that the binary representation of + * the double is of the form d = 0.bbbbbbbb x 2n + * where there are up to 53 binary digits in the binary + * fraction (including the assumed leading 1 bit + * for normalized numbers). + * We find a value m such that 10m d is between + * 253 and >263. + * This product will be exactly convertible to a long + * with no loss of precision. Getting the + * decimal representation for that is trivial (see formatLong). + * This is a decimal mantissa and we have an exponent (-m). + * All we have to do is manipulate the decimal point + * to where we want to see it. Errors can + * arise due to roundoff in the scaling multiplication, but + * should be no more than a single bit. + * + * @param val Double to be formatted + * @param buf Buffer in which result is to be stored + * @param off Offset within buffer + * @param len Maximum length of integer + * @return offset of next unused character in input buffer. + */ + public int format(double val, byte[] buf, + int off, int len) throws TruncationException { + + double pos = Math.abs(val); + + int minlen, actlen; + + // Special cases -- It is OK if these get truncated. + if (pos == 0.) { + return format("0.0", buf, off, len); + } else if (Double.isNaN(val)) { + return format("NaN", buf, off, len); + } else if (Double.isInfinite(val)) { + if (val > 0) { + return format("Infinity", buf, off, len); + } else { + return format("-Infinity", buf, off, len); + } + } + + int power = (int) (Math.log(pos) * ilog10); + int shift = 17 - power; + double scale; + double scale2 = 1; + + // Scale the number so that we get a number ~ n x 10^17. + if (shift < 200) { + scale = tenpow[shift + zeropow]; + } else { + // Can get overflow if the original number is + // very small, so we break out the shift + // into two multipliers. + scale2 = tenpow[200 + zeropow]; + scale = tenpow[shift - 200 + zeropow]; + } + + + pos = (pos * scale) * scale2; + + // Parse the double bits. + + long bits = Double.doubleToLongBits(pos); + + // The exponent should be a little more than 52. + int exp = (int) (((bits & 0x7FF0000000000000L) >> 52) - 1023); + + long numb = (bits & 0x000FFFFFFFFFFFFFL); + + if (exp > -1023) { + // Normalized.... + numb |= (0x0010000000000000L); + } else { + // Denormalized + exp += 1; + } + + + // Multiple this number by the excess of the exponent + // over 52. This completes the conversion of double to long. + numb = numb << (exp - 52); + + // Get a decimal mantissa. + boolean oldAlign = align; + align = false; + int ndig = format(numb, tbuf1, 0, 32); + align = oldAlign; + + // Now format the double. + + return combineReal(val, buf, off, len, tbuf1, ndig, shift); + } + + /** This method formats a double given + * a decimal mantissa and exponent information. + * @param val The original number + * @param buf Output buffer + * @param off Offset into buffer + * @param len Maximum number of characters to use in buffer. + * @param mant A decimal mantissa for the number. + * @param lmant The number of characters in the mantissa + * @param shift The exponent of the power of 10 that + * we shifted val to get the given mantissa. + * @return Offset of next available character in buffer. + */ + int combineReal(double val, byte[] buf, int off, int len, + byte[] mant, int lmant, int shift) throws TruncationException { + + // First get the minimum size for the number + + double pos = Math.abs(val); + boolean simple = false; + int minSize; + int maxSize; + + if (pos >= simpleMin && pos <= simpleMax) { + simple = true; + } + + int exp = lmant - shift - 1; + int lexp = 0; + + if (!simple) { + + boolean oldAlign = align; + align = false; + lexp = format(exp, tbuf2, 0, 32); + align = oldAlign; + + minSize = lexp + 2; // e.g., 2e-12 + maxSize = lexp + lmant + 2; // add in "." and e + } else { + if (exp >= 0) { + minSize = exp + 1; // e.g. 32 + + // Special case. E.g., 99.9 has + // minumum size of 3. + int i; + for (i = 0; i < lmant && i <= exp; i += 1) { + if (mant[i] != (byte) '9') { + break; + } + } + if (i > exp && i < lmant && mant[i] >= (byte) '5') { + minSize += 1; + } + + maxSize = lmant + 1; // Add in "." + if (maxSize <= minSize) { // Very large numbers. + maxSize = minSize + 1; + } + } else { + minSize = 2; + maxSize = 1 + Math.abs(exp) + lmant; + } + } + if (val < 0) { + minSize += 1; + maxSize += 1; + } + + // Can the number fit? + if ((truncateOnOverflow && minSize > len) + || (minSize > buf.length - off)) { + truncationFiller(buf, off, len); + return off + len; + } + + // Do we need to align it? + if (maxSize < len && align) { + int nal = len - maxSize; + off = alignFill(buf, off, nal); + len -= nal; + } + + + int off0 = off; + + // Now begin filling in the buffer. + if (val < 0) { + buf[off] = (byte) '-'; + off += 1; + len -= 1; + } + + + if (simple) { + return Math.abs(mantissa(mant, lmant, exp, simple, buf, off, len)); + } else { + off = mantissa(mant, lmant, 0, simple, buf, off, len - lexp - 1); + if (off < 0) { + off = -off; + len -= off; + // Handle the expanded exponent by filling + if (exp == 9 || exp == 99) { + // Cannot fit... + if (off + len == minSize) { + truncationFiller(buf, off, len); + return off + len; + } else { + // Steal a character from the mantissa. + off -= 1; + } + } + exp += 1; + lexp = format(exp, tbuf2, 0, 32); + } + buf[off] = (byte) 'E'; + off += 1; + System.arraycopy(tbuf2, 0, buf, off, lexp); + return off + lexp; + } + } + + /** Write the mantissa of the number. This method addresses + * the subtleties involved in rounding numbers. + */ + int mantissa(byte[] mant, int lmant, int exp, boolean simple, + byte[] buf, int off, int len) { + + // Save in case we need to extend the number. + int off0 = off; + int pos = 0; + + if (exp < 0) { + buf[off] = (byte) '0'; + len -= 1; + off += 1; + if (len > 0) { + buf[off] = (byte) '.'; + off += 1; + len -= 1; + } + // Leading 0s in small numbers. + int cexp = exp; + while (cexp < -1 && len > 0) { + buf[off] = (byte) '0'; + cexp += 1; + off += 1; + len -= 1; + } + + } else { + + // Print out all digits to the left of the decimal. + while (exp >= 0 && pos < lmant) { + buf[off] = mant[pos]; + off += 1; + pos += 1; + len -= 1; + exp -= 1; + } + // Trust we have enough space for this. + for (int i = 0; i <= exp; i += 1) { + buf[off] = (byte) '0'; + off += 1; + len -= 1; + } + + // Add in a decimal if we have space. + if (len > 0) { + buf[off] = (byte) '.'; + len -= 1; + off += 1; + } + } + + // Now handle the digits to the right of the decimal. + while (len > 0 && pos < lmant) { + buf[off] = mant[pos]; + off += 1; + exp -= 1; + len -= 1; + pos += 1; + } + + // Now handle rounding. + + if (pos < lmant && mant[pos] >= (byte) '5') { + int i; + + // Increment to the left until we find a non-9 + for (i = off - 1; i >= off0; i -= 1) { + + + if (buf[i] == (byte) '.' || buf[i] == (byte) '-') { + continue; + } + if (buf[i] == (byte) '9') { + buf[i] = (byte) '0'; + } else { + buf[i] += 1; + break; + } + } + + // Now we handle 99.99 case. This can cause problems + // in two cases. If we are not using scientific notation + // then we may want to convert 99.9 to 100., i.e., + // we need to move the decimal point. If there is no + // decimal point, then we must not be truncating on overflow + // but we should be allowed to write it to the + // next character (i.e., we are not at the end of buf). + // + // If we are printing in scientific notation, then we want + // to convert 9.99 to 1.00, i.e. we do not move the decimal. + // However we need to signal that the exponent should be + // incremented by one. + // + // We cannot have aligned the number, since that requires + // the full precision number to fit within the requested + // length, and we would have printed out the entire + // mantissa (i.e., pos >= lmant) + + if (i < off0) { + + buf[off0] = (byte) '1'; + boolean foundDecimal = false; + for (i = off0 + 1; i < off; i += 1) { + if (buf[i] == (byte) '.') { + foundDecimal = true; + if (simple) { + buf[i] = (byte) '0'; + i += 1; + if (i < off) { + buf[i] = (byte) '.'; + } + } + break; + } + } + if (simple && !foundDecimal) { + buf[off + 1] = (byte) '0'; // 99 went to 100 + off += 1; + } + + off = -off; // Signal to change exponent if necessary. + } + + } + + return off; + } + + /** Fill the buffer with truncation characters. After filling + * the buffer, a TruncationException will be thrown if the + * appropriate flag is set. + */ + void truncationFiller(byte[] buffer, int offset, int length) + throws TruncationException { + + for (int i = offset; i < offset + length; i += 1) { + buffer[i] = truncationFill; + } + if (truncationThrow) { + throw new TruncationException(); + } + return; + } + + /** Fill the buffer with blanks to align + * a field. + */ + public int alignFill(byte[] buffer, int offset, int len) { + for (int i = offset; i < offset + len; i += 1) { + buffer[i] = (byte) ' '; + } + return offset + len; + } +} diff --git a/src/nom/tam/util/ByteParser.java b/src/nom/tam/util/ByteParser.java new file mode 100644 index 0000000..59de84e --- /dev/null +++ b/src/nom/tam/util/ByteParser.java @@ -0,0 +1,481 @@ +package nom.tam.util; + +import java.io.UnsupportedEncodingException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** This class provides routines + * for efficient parsing of data stored in a byte array. + * This routine is optimized (in theory at least!) for efficiency + * rather than accuracy. The values read in for doubles or floats + * may differ in the last bit or so from the standard input + * utilities, especially in the case where a float is specified + * as a very long string of digits (substantially longer than + * the precision of the type). + *

+ * The get methods generally are available with or without a length + * parameter specified. When a length parameter is specified only + * the bytes with the specified range from the current offset will + * be search for the number. If no length is specified, the entire + * buffer from the current offset will be searched. + *

+ * The getString method returns a string with leading and trailing + * white space left intact. For all other get calls, leading + * white space is ignored. If fillFields is set, then the get + * methods check that only white space follows valid data and a + * FormatException is thrown if that is not the case. If + * fillFields is not set and valid data is found, then the + * methods return having read as much as possible. E.g., for + * the sequence "T123.258E13", a getBoolean, getInteger and + * getFloat call would return true, 123, and 2.58e12 when + * called in succession. + * + */ +public class ByteParser { + + /** Array being parsed */ + private byte[] input; + /** Current offset into input. */ + private int offset; + /** Length of last parsed value */ + private int numberLength; + /** Did we find a sign last time we checked? */ + private boolean foundSign; + /** Do we fill up fields? */ + private boolean fillFields = false; + + /** Construct a parser. + * @param input The byte array to be parsed. + * Note that the array can be re-used by + * refilling its contents and resetting the offset. + */ + public ByteParser(byte[] input) { + this.input = input; + this.offset = 0; + } + + /** Set the buffer for the parser */ + public void setBuffer(byte[] buf) { + this.input = buf; + this.offset = 0; + } + + /** Get the buffer being used by the parser */ + public byte[] getBuffer() { + return input; + } + + /** Set the offset into the array. + * @param offset The desired offset from the beginning + * of the array. + */ + public void setOffset(int offset) { + this.offset = offset; + } + + /** Do we require a field to completely fill up the specified + * length (with optional leading and trailing white space. + @param flag Is filling required? + */ + public void setFillFields(boolean flag) { + fillFields = flag; + } + + /** Get the current offset + @return The current offset within the buffer. + */ + public int getOffset() { + return offset; + } + + /** Get the number of characters used to parse the previous + * number (or the length of the previous String returned). + */ + public int getNumberLength() { + return numberLength; + } + + /** Read in the buffer until a double is read. This will read + * the entire buffer if fillFields is set. + * @return The value found. + */ + public double getDouble() throws FormatException { + return getDouble(input.length - offset); + } + + /** Look for a double in the buffer. + * Leading spaces are ignored. + * @param length The maximum number of characters + * used to parse this number. If fillFields + * is specified then exactly only whitespace may follow + * a valid double value. + */ + public double getDouble(int length) throws FormatException { + + int startOffset = offset; + + boolean error = true; + + double number = 0; + int i = 0; + + // Skip initial blanks. + length -= skipWhite(length); + + if (length == 0) { + numberLength = offset - startOffset; + return 0; + } + + double mantissaSign = checkSign(); + if (foundSign) { + length -= 1; + } + + // Look for the special strings NaN, Inf, + if (length >= 3 + && (input[offset] == 'n' || input[offset] == 'N') + && (input[offset + 1] == 'a' || input[offset + 1] == 'A') + && (input[offset + 2] == 'n' || input[offset + 2] == 'N')) { + + number = Double.NaN; + length -= 3; + offset += 3; + + // Look for the longer string first then try the shorter. + } else if (length >= 8 + && (input[offset] == 'i' || input[offset] == 'I') + && (input[offset + 1] == 'n' || input[offset + 1] == 'N') + && (input[offset + 2] == 'f' || input[offset + 2] == 'F') + && (input[offset + 3] == 'i' || input[offset + 3] == 'I') + && (input[offset + 4] == 'n' || input[offset + 4] == 'N') + && (input[offset + 5] == 'i' || input[offset + 5] == 'I') + && (input[offset + 6] == 't' || input[offset + 6] == 'T') + && (input[offset + 7] == 'y' || input[offset + 7] == 'Y')) { + number = Double.POSITIVE_INFINITY; + length -= 8; + offset += 8; + + } else if (length >= 3 + && (input[offset] == 'i' || input[offset] == 'I') + && (input[offset + 1] == 'n' || input[offset + 1] == 'N') + && (input[offset + 2] == 'f' || input[offset + 2] == 'F')) { + number = Double.POSITIVE_INFINITY; + length -= 3; + offset += 3; + + } else { + + number = getBareInteger(length); // This will update offset + length -= numberLength; // Set by getBareInteger + + if (numberLength > 0) { + error = false; + } + + // Check for fractional values after decimal + if (length > 0 && input[offset] == '.') { + + offset += 1; + length -= 1; + + double numerator = getBareInteger(length); + if (numerator > 0) { + number += numerator / Math.pow(10., numberLength); + } + length -= numberLength; + if (numberLength > 0) { + error = false; + } + } + + if (error) { + offset = startOffset; + numberLength = 0; + throw new FormatException("Invalid real field"); + } + + // Look for an exponent + if (length > 0) { + + // Our Fortran heritage means that we allow 'D' for the exponent indicator. + if (input[offset] == 'e' || input[offset] == 'E' + || input[offset] == 'd' || input[offset] == 'D') { + + offset += 1; + length -= 1; + if (length > 0) { + int sign = checkSign(); + if (foundSign) { + length -= 1; + } + + int exponent = (int) getBareInteger(length); + + // For very small numbers we try to miminize + // effects of denormalization. + if (exponent * sign > -300) { + number *= Math.pow(10., exponent * sign); + } else { + number = 1.e-300 * (number * Math.pow(10., exponent * sign + 300)); + } + length -= numberLength; + } + } + } + } + + if (fillFields && length > 0) { + + if (isWhite(length)) { + offset += length; + } else { + numberLength = 0; + offset = startOffset; + throw new FormatException("Non-blanks following real."); + } + } + + numberLength = offset - startOffset; + return mantissaSign * number; + } + + /** Get a floating point value from the buffer. (see getDouble(int()) + */ + public float getFloat() throws FormatException { + return (float) getDouble(input.length - offset); + } + + /** Get a floating point value in a region of the buffer */ + public float getFloat(int length) throws FormatException { + return (float) getDouble(length); + } + + /** Convert a region of the buffer to an integer */ + public int getInt(int length) throws FormatException { + int startOffset = offset; + + length -= skipWhite(length); + if (length == 0) { + numberLength = offset - startOffset; + return 0; + } + + int number = 0; + boolean error = true; + + int sign = checkSign(); + if (foundSign) { + length -= 1; + } + + while (length > 0 && input[offset] >= '0' && input[offset] <= '9') { + number = number * 10 + input[offset] - '0'; + offset += 1; + length -= 1; + error = false; + } + + if (error) { + numberLength = 0; + offset = startOffset; + throw new FormatException("Invalid Integer"); + } + + if (length > 0 && fillFields) { + if (isWhite(length)) { + offset += length; + } else { + numberLength = 0; + offset = startOffset; + throw new FormatException("Non-white following integer"); + } + } + + numberLength = offset - startOffset; + return sign * number; + } + + /** Look for an integer at the beginning of the buffer */ + public int getInt() throws FormatException { + return getInt(input.length - offset); + } + + /** Look for a long in a specified region of the buffer */ + public long getLong(int length) throws FormatException { + + int startOffset = offset; + + // Skip white space. + length -= skipWhite(length); + if (length == 0) { + numberLength = offset - startOffset; + return 0; + } + + long number = 0; + boolean error = true; + + long sign = checkSign(); + if (foundSign) { + length -= 1; + } + + while (length > 0 && input[offset] >= '0' && input[offset] <= '9') { + number = number * 10 + input[offset] - '0'; + error = false; + offset += 1; + length -= 1; + } + + if (error) { + numberLength = 0; + offset = startOffset; + throw new FormatException("Invalid long number"); + } + + if (length > 0 && fillFields) { + if (isWhite(length)) { + offset += length; + } else { + offset = startOffset; + numberLength = 0; + throw new FormatException("Non-white following long"); + } + } + numberLength = offset - startOffset; + return sign * number; + } + + /** Get a string + * @param length The length of the string. + */ + public String getString(int length) { + + String s = AsciiFuncs.asciiString(input, offset, length); + offset += length; + numberLength = length; + return s; + } + + /** Get a boolean value from the beginning of the buffer */ + public boolean getBoolean() throws FormatException { + return getBoolean(input.length - offset); + } + + /** Get a boolean value from a specified region of the buffer */ + public boolean getBoolean(int length) throws FormatException { + + int startOffset = offset; + length -= skipWhite(length); + if (length == 0) { + throw new FormatException("Blank boolean field"); + } + + boolean value = false; + if (input[offset] == 'T' || input[offset] == 't') { + value = true; + } else if (input[offset] != 'F' && input[offset] != 'f') { + numberLength = 0; + offset = startOffset; + throw new FormatException("Invalid boolean value"); + } + offset += 1; + length -= 1; + + if (fillFields && length > 0) { + if (isWhite(length)) { + offset += length; + } else { + numberLength = 0; + offset = startOffset; + throw new FormatException("Non-white following boolean"); + } + } + numberLength = offset - startOffset; + return value; + } + + /** Skip bytes in the buffer */ + public void skip(int nBytes) { + offset += nBytes; + } + + /** Get the integer value starting at the current position. + * This routine returns a double rather than an int/long + * to enable it to read very long integers (with reduced + * precision) such as 111111111111111111111111111111111111111111. + * Note that this routine does set numberLength. + * + * @param length The maximum number of characters to use. + */ + private double getBareInteger(int length) { + + int startOffset = offset; + double number = 0; + + while (length > 0 && input[offset] >= '0' && input[offset] <= '9') { + + number *= 10; + number += input[offset] - '0'; + offset += 1; + length -= 1; + } + numberLength = offset - startOffset; + return number; + } + + /** Skip white space. This routine skips with space in + * the input and returns the number of character skipped. + * White space is defined as ' ', '\t', '\n' or '\r' + * + * @param length The maximum number of characters to skip. + */ + public int skipWhite(int length) { + + int i; + for (i = 0; i < length; i += 1) { + if (input[offset + i] != ' ' && input[offset + i] != '\t' + && input[offset + i] != '\n' && input[offset + i] != '\r') { + break; + } + } + + offset += i; + return i; + + } + + /** Find the sign for a number . + * This routine looks for a sign (+/-) at the current location + * and return +1/-1 if one is found, or +1 if not. + * The foundSign boolean is set if a sign is found and offset is + * incremented. + */ + private int checkSign() { + + foundSign = false; + + if (input[offset] == '+') { + foundSign = true; + offset += 1; + return 1; + } else if (input[offset] == '-') { + foundSign = true; + offset += 1; + return -1; + } + + return 1; + } + + /** Is a region blank? + * @param length The length of the region to be tested + */ + private boolean isWhite(int length) { + int oldOffset = offset; + boolean value = skipWhite(length) == length; + offset = oldOffset; + return value; + } +} diff --git a/src/nom/tam/util/ColumnTable.java b/src/nom/tam/util/ColumnTable.java new file mode 100644 index 0000000..20679d2 --- /dev/null +++ b/src/nom/tam/util/ColumnTable.java @@ -0,0 +1,833 @@ +package nom.tam.util; + +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +import java.io.*; +import java.lang.reflect.Array; + +/** A data table is conventionally considered to consist of rows and + * columns, where the structure within each column is constant, but + * different columns may have different structures. I.e., structurally + * columns may differ but rows are identical. + * Typically tabular data is usually stored in row order which can + * make it extremely difficult to access efficiently using Java. + * This class provides efficient + * access to data which is stored in row order and allows users to + * get and set the elements of the table. + * The table can consist only of arrays of primitive types. + * Data stored in column order can + * be efficiently read and written using the + * BufferedDataXputStream classes. + * + * The table is represented entirely as a set of one-dimensional primitive + * arrays. For a given column, a row consists of some number of + * contiguous elements of the array. Each column is required to have + * the same number of rows. + */ +public class ColumnTable implements DataTable { + + /** The columns to be read/written */ + private Object[] arrays; + /** The number of elements in a row for each column */ + private int[] sizes; + /** The number of rows */ + private int nrow; + /** The number or rows to read/write in one I/O. */ + private int chunk; + /** The size of a row in bytes */ + private int rowSize; + /** The base type of each row (using the second character + * of the [x class names of the arrays. + */ + private char[] types; + private Class[] bases; + // The following arrays are used to avoid having to check + // casts during the I/O loops. + // They point to elements of arrays. + private byte[][] bytePointers; + private short[][] shortPointers; + private int[][] intPointers; + private long[][] longPointers; + private float[][] floatPointers; + private double[][] doublePointers; + private char[][] charPointers; + private boolean[][] booleanPointers; + + /** Create the object after checking consistency. + * @param arrays An array of one-d primitive arrays. + * @param sizes The number of elements in each row + * for the corresponding column + */ + public ColumnTable(Object[] arrays, int[] sizes) throws TableException { + setup(arrays, sizes); + } + + /** Actually perform the initialization. + */ + protected void setup(Object[] arrays, int[] sizes) throws TableException { + + checkArrayConsistency(arrays, sizes); + getNumberOfRows(); + initializePointers(); + + } + + /** Get the number of rows in the table. + */ + public int getNRows() { + return nrow; + } + + /** Get the number of columns in the table. + */ + public int getNCols() { + return arrays.length; + } + + /** Get a particular column. + * @param col The column desired. + * @return an object containing the column data desired. + * This will be an instance of a 1-d primitive array. + */ + public Object getColumn(int col) { + return arrays[col]; + } + + /** + * Set the values in a particular column. The new values must match the old in + * length but not necessarily in type. + * + * @param col + * The column to modify. + * @param newColumn + * The new column data. This should be a primitive array. + * @exception TableException + * Thrown when the new data is not commenserable with information + * in the table. + */ + public void setColumn(int col, Object newColumn) throws TableException { + + boolean reset = newColumn.getClass() != arrays[col].getClass() + || Array.getLength(newColumn) != Array.getLength(arrays[col]); + arrays[col] = newColumn; + if (reset) { + setup(arrays, sizes); + } else { + // This is required, because otherwise the typed pointer may point to the old + // array, which has been replaced by newColumn. Added by Jeroen de Jong, 1 Aug 2006 + initializePointers(); + } + } + + /** Add a column */ + public void addColumn(Object newColumn, int size) throws TableException { + + String classname = newColumn.getClass().getName(); + nrow = checkColumnConsistency(newColumn, classname, nrow, size); + + rowSize += nrow * ArrayFuncs.getBaseLength(newColumn); + + getNumberOfRows(); + + int ncol = arrays.length; + + Object[] newArrays = new Object[ncol + 1]; + int[] newSizes = new int[ncol + 1]; + Class[] newBases = new Class[ncol + 1]; + char[] newTypes = new char[ncol + 1]; + + System.arraycopy(arrays, 0, newArrays, 0, ncol); + System.arraycopy(sizes, 0, newSizes, 0, ncol); + System.arraycopy(bases, 0, newBases, 0, ncol); + System.arraycopy(types, 0, newTypes, 0, ncol); + + arrays = newArrays; + sizes = newSizes; + bases = newBases; + types = newTypes; + + arrays[ncol] = newColumn; + sizes[ncol] = size; + bases[ncol] = ArrayFuncs.getBaseClass(newColumn); + types[ncol] = classname.charAt(1); + addPointer(newColumn); + } + + /** Add a row to the table. This method is very inefficient + * for adding multiple rows and should be avoided if possible. + */ + public void addRow(Object[] row) throws TableException { + + if (arrays.length == 0) { + + for (int i = 0; i < row.length; i += 1) { + addColumn(row[i], Array.getLength(row[i])); + } + + } else { + + if (row.length != arrays.length) { + throw new TableException("Row length mismatch"); + } + + for (int i = 0; i < row.length; i += 1) { + if (row[i].getClass() != arrays[i].getClass() + || Array.getLength(row[i]) != sizes[i]) { + throw new TableException("Row column mismatch at column:" + i); + } + Object xarray = ArrayFuncs.newInstance(bases[i], (nrow + 1) * sizes[i]); + System.arraycopy(arrays[i], 0, xarray, 0, nrow * sizes[i]); + System.arraycopy(row[i], 0, xarray, nrow * sizes[i], sizes[i]); + arrays[i] = xarray; + } + initializePointers(); + nrow += 1; + } + } + + /** Get a element of the table. + * @param row The row desired. + * @param col The column desired. + * @return A primitive array containing the information. Note + * that an array will be returned even if the element + * is a scalar. + */ + public Object getElement(int row, int col) { + + Object x = ArrayFuncs.newInstance(bases[col], sizes[col]); + System.arraycopy(arrays[col], sizes[col] * row, x, 0, sizes[col]); + return x; + } + + /** Modify an element of the table. + * @param row The row containing the element. + * @param col The column containing the element. + * @param x The new datum. This should be 1-d primitive + * array. + * @exception TableException Thrown when the new data + * is not of the same type as + * the data it replaces. + */ + public void setElement(int row, int col, Object x) + throws TableException { + + String classname = x.getClass().getName(); + + if (!classname.equals("[" + types[col])) { + throw new TableException("setElement: Incompatible element type"); + } + + if (Array.getLength(x) != sizes[col]) { + throw new TableException("setElement: Incompatible element size"); + } + + System.arraycopy(x, 0, arrays[col], sizes[col] * row, sizes[col]); + } + + /** Get a row of data. + * @param The row desired. + * @return An array of objects each containing a primitive array. + */ + public Object getRow(int row) { + + Object[] x = new Object[arrays.length]; + for (int col = 0; col < arrays.length; col += 1) { + x[col] = getElement(row, col); + } + return x; + } + + /** Modify a row of data. + * @param row The row to be modified. + * @param x The data to be modified. This should be an + * array of objects. It is described as an Object + * here since other table implementations may + * use other methods to store the data (e.g., + * @see ColumnTable.getColumn. + */ + public void setRow(int row, Object x) throws TableException { + + if (!(x instanceof Object[])) { + throw new TableException("setRow: Incompatible row"); + } + + for (int col = 0; col < arrays.length; col += 1) { + setElement(row, col, ((Object[]) x)[col]); + } + } + + /** Check that the columns and sizes are consistent. + * Inconsistencies include: + *

    + *
  • arrays and sizes have different lengths. + *
  • an element of arrays is not a primitive array. + *
  • the size of an array is not divisible by the sizes entry. + *
  • the number of rows differs for the columns. + *
+ * @param arrays The arrays defining the columns. + * @param sizes The number of elements in each row for the column. + */ + protected void checkArrayConsistency(Object[] arrays, int[] sizes) + throws TableException { + + // This routine throws an error if it detects an inconsistency + // between the arrays being read in. + + // First check that the lengths of the two arrays are the same. + if (arrays.length != sizes.length) { + throw new TableException("readArraysAsColumns: Incompatible arrays and sizes."); + } + + // Now check that that we fill up all of the arrays exactly. + int ratio = 0; + int rowSize = 0; + + this.types = new char[arrays.length]; + this.bases = new Class[arrays.length]; + + // Check for a null table. + boolean nullTable = true; + + for (int i = 0; i < arrays.length; i += 1) { + + String classname = arrays[i].getClass().getName(); + + ratio = checkColumnConsistency(arrays[i], classname, ratio, sizes[i]); + + rowSize += sizes[i] * ArrayFuncs.getBaseLength(arrays[i]); + types[i] = classname.charAt(1); + bases[i] = ArrayFuncs.getBaseClass(arrays[i]); + } + + this.nrow = ratio; + this.rowSize = rowSize; + this.arrays = arrays; + this.sizes = sizes; + } + + private int checkColumnConsistency(Object data, String classname, int ratio, int size) + throws TableException { + + + if (classname.charAt(0) != '[' || classname.length() != 2) { + throw new TableException("Non-primitive array for column"); + } + + int thisSize = Array.getLength(data); + if ((thisSize == 0 && size != 0 && ratio != 0) + || (thisSize != 0 && size == 0)) { + throw new TableException("Size mismatch in column: " + thisSize + " != " + size); + } + + // The row size must evenly divide the size of the array. + if (size != 0 && thisSize % size != 0) { + throw new TableException("Row size does not divide array for column"); + } + + // Finally the ratio of sizes must be the same for all columns -- this + // is the number of rows in the table. + int thisRatio = 0; + if (size > 0) { + thisRatio = thisSize / size; + + if (ratio != 0 && (thisRatio != ratio)) { + throw new TableException("Different number of rows in different columns"); + } + } + if (thisRatio > 0) { + return thisRatio; + } else { + return ratio; + } + + } + + /** Calculate the number of rows to read/write at a time. + * @param rowSize The size of a row in bytes. + * @param nrows The number of rows in the table. + */ + protected void getNumberOfRows() { + + int bufSize = 65536; + + // If a row is larger than bufSize, then read one row at a time. + if (rowSize == 0) { + this.chunk = 0; + + } else if (rowSize > bufSize) { + this.chunk = 1; + + // If the entire set is not too big, just read it all. + } else if (bufSize / rowSize >= nrow) { + this.chunk = nrow; + } else { + this.chunk = bufSize / rowSize + 1; + } + + } + + /** Set the pointer arrays for the eight primitive types + * to point to the appropriate elements of arrays. + */ + protected void initializePointers() { + + int nbyte, nshort, nint, nlong, nfloat, ndouble, nchar, nboolean; + + // Count how many of each type we have. + nbyte = 0; + nshort = 0; + nint = 0; + nlong = 0; + nfloat = 0; + ndouble = 0; + nchar = 0; + nboolean = 0; + + for (int col = 0; col < arrays.length; col += 1) { + switch (types[col]) { + + case 'B': + nbyte += 1; + break; + case 'S': + nshort += 1; + break; + case 'I': + nint += 1; + break; + case 'J': + nlong += 1; + break; + case 'F': + nfloat += 1; + break; + case 'D': + ndouble += 1; + break; + case 'C': + nchar += 1; + break; + case 'Z': + nboolean += 1; + break; + } + } + + // Allocate the pointer arrays. Note that many will be + // zero-length. + + bytePointers = new byte[nbyte][]; + shortPointers = new short[nshort][]; + intPointers = new int[nint][]; + longPointers = new long[nlong][]; + floatPointers = new float[nfloat][]; + doublePointers = new double[ndouble][]; + charPointers = new char[nchar][]; + booleanPointers = new boolean[nboolean][]; + + // Now set the pointers. + nbyte = 0; + nshort = 0; + nint = 0; + nlong = 0; + nfloat = 0; + ndouble = 0; + nchar = 0; + nboolean = 0; + + for (int col = 0; col < arrays.length; col += 1) { + switch (types[col]) { + + case 'B': + bytePointers[nbyte] = (byte[]) arrays[col]; + nbyte += 1; + break; + case 'S': + shortPointers[nshort] = (short[]) arrays[col]; + nshort += 1; + break; + case 'I': + intPointers[nint] = (int[]) arrays[col]; + nint += 1; + break; + case 'J': + longPointers[nlong] = (long[]) arrays[col]; + nlong += 1; + break; + case 'F': + floatPointers[nfloat] = (float[]) arrays[col]; + nfloat += 1; + break; + case 'D': + doublePointers[ndouble] = (double[]) arrays[col]; + ndouble += 1; + break; + case 'C': + charPointers[nchar] = (char[]) arrays[col]; + nchar += 1; + break; + case 'Z': + booleanPointers[nboolean] = (boolean[]) arrays[col]; + nboolean += 1; + break; + } + } + } + + // Add a pointer in the pointer lists. + protected void addPointer(Object data) throws TableException { + String classname = data.getClass().getName(); + char type = classname.charAt(1); + + switch (type) { + case 'B': { + byte[][] xb = new byte[bytePointers.length + 1][]; + System.arraycopy(bytePointers, 0, xb, 0, bytePointers.length); + xb[bytePointers.length] = (byte[]) data; + bytePointers = xb; + break; + } + case 'Z': { + boolean[][] xb = new boolean[booleanPointers.length + 1][]; + System.arraycopy(booleanPointers, 0, xb, 0, booleanPointers.length); + xb[booleanPointers.length] = (boolean[]) data; + booleanPointers = xb; + break; + } + case 'S': { + short[][] xb = new short[shortPointers.length + 1][]; + System.arraycopy(shortPointers, 0, xb, 0, shortPointers.length); + xb[shortPointers.length] = (short[]) data; + shortPointers = xb; + break; + } + case 'C': { + char[][] xb = new char[charPointers.length + 1][]; + System.arraycopy(charPointers, 0, xb, 0, charPointers.length); + xb[charPointers.length] = (char[]) data; + charPointers = xb; + break; + } + case 'I': { + int[][] xb = new int[intPointers.length + 1][]; + System.arraycopy(intPointers, 0, xb, 0, intPointers.length); + xb[intPointers.length] = (int[]) data; + intPointers = xb; + break; + } + case 'J': { + long[][] xb = new long[longPointers.length + 1][]; + System.arraycopy(longPointers, 0, xb, 0, longPointers.length); + xb[longPointers.length] = (long[]) data; + longPointers = xb; + break; + } + case 'F': { + float[][] xb = new float[floatPointers.length + 1][]; + System.arraycopy(floatPointers, 0, xb, 0, floatPointers.length); + xb[floatPointers.length] = (float[]) data; + floatPointers = xb; + break; + } + case 'D': { + double[][] xb = new double[doublePointers.length + 1][]; + System.arraycopy(doublePointers, 0, xb, 0, doublePointers.length); + xb[doublePointers.length] = (double[]) data; + doublePointers = xb; + break; + } + default: + throw new TableException("Invalid type for added column:" + classname); + } + } + + /** Read a table. + * @param is The input stream to read from. + */ + public int read(ArrayDataInput is) throws IOException { + + int currRow = 0; + + // While we have not finished reading the table.. + for (int row = 0; row < nrow; row += 1) { + + int ibyte = 0; + int ishort = 0; + int iint = 0; + int ilong = 0; + int ichar = 0; + int ifloat = 0; + int idouble = 0; + int iboolean = 0; + + // Loop over the columns within the row. + for (int col = 0; col < arrays.length; col += 1) { + + int arrOffset = sizes[col] * row; + int size = sizes[col]; + + switch (types[col]) { + // In anticpated order of use. + case 'I': + int[] ia = intPointers[iint]; + iint += 1; + is.read(ia, arrOffset, size); + break; + + case 'S': + short[] s = shortPointers[ishort]; + ishort += 1; + is.read(s, arrOffset, size); + break; + + case 'B': + byte[] b = bytePointers[ibyte]; + ibyte += 1; + is.read(b, arrOffset, size); + break; + + case 'F': + float[] f = floatPointers[ifloat]; + ifloat += 1; + is.read(f, arrOffset, size); + break; + + case 'D': + double[] d = doublePointers[idouble]; + idouble += 1; + is.read(d, arrOffset, size); + break; + + case 'C': + char[] c = charPointers[ichar]; + ichar += 1; + is.read(c, arrOffset, size); + break; + + case 'J': + long[] l = longPointers[ilong]; + ilong += 1; + is.read(l, arrOffset, size); + break; + + case 'Z': + + boolean[] bool = booleanPointers[iboolean]; + iboolean += 1; + is.read(bool, arrOffset, size); + break; + } + } + } + + // All done if we get here... + return rowSize * nrow; + } + + /** Write a table. + * @param os the output stream to write to. + */ + public int write(ArrayDataOutput os) throws IOException { + + if (rowSize == 0) { + return 0; + } + + for (int row = 0; row < nrow; row += 1) { + + int ibyte = 0; + int ishort = 0; + int iint = 0; + int ilong = 0; + int ichar = 0; + int ifloat = 0; + int idouble = 0; + int iboolean = 0; + + // Loop over the columns within the row. + for (int col = 0; col < arrays.length; col += 1) { + + int arrOffset = sizes[col] * row; + int size = sizes[col]; + + switch (types[col]) { + // In anticpated order of use. + case 'I': + int[] ia = intPointers[iint]; + iint += 1; + os.write(ia, arrOffset, size); + break; + + case 'S': + short[] s = shortPointers[ishort]; + ishort += 1; + os.write(s, arrOffset, size); + break; + + case 'B': + byte[] b = bytePointers[ibyte]; + ibyte += 1; + os.write(b, arrOffset, size); + break; + + case 'F': + float[] f = floatPointers[ifloat]; + ifloat += 1; + os.write(f, arrOffset, size); + break; + + case 'D': + double[] d = doublePointers[idouble]; + idouble += 1; + os.write(d, arrOffset, size); + break; + + case 'C': + char[] c = charPointers[ichar]; + ichar += 1; + os.write(c, arrOffset, size); + break; + + case 'J': + long[] l = longPointers[ilong]; + ilong += 1; + os.write(l, arrOffset, size); + break; + + case 'Z': + boolean[] bool = booleanPointers[iboolean]; + iboolean += 1; + os.write(bool, arrOffset, size); + break; + } + + } + + } + + // All done if we get here... + return rowSize * nrow; + } + + /** Get the base classes of the columns. + * @return An array of Class objects, one for each column. + */ + public Class[] getBases() { + return bases; + } + + /** Get the characters describing the base classes of the columns. + * @return An array of char's, one for each column. + */ + public char[] getTypes() { + return types; + } + + /** Get the actual data arrays */ + public Object[] getColumns() { + return arrays; + } + + public int[] getSizes() { + return sizes; + } + + /** Delete a row from the table. + * @param row The row (0-indexed) to be deleted. + */ + public void deleteRow(int row) throws TableException { + deleteRows(row, 1); + } + + /** Delete a contiguous set of rows from the table. + * @param row The row (0-indexed) to be deleted. + * @param length The number of rows to be deleted. + * @throws TableException if the request goes outside + * the boundaries of the table or if the length is negative. + */ + public void deleteRows(int row, int length) throws TableException { + + if (row < 0 || length < 0 || row + length > nrow) { + throw new TableException("Invalid request to delete rows start: " + row + " length:" + length + + " for table with " + nrow + " rows."); + } + + if (length == 0) { + return; + } + + for (int col = 0; col < arrays.length; col += 1) { + + int sz = sizes[col]; + int newSize = sz * (nrow - length); + Object newArr = ArrayFuncs.newInstance(bases[col], newSize); + + // Copy whatever comes before the deletion + System.arraycopy(arrays[col], 0, newArr, 0, row * sz); + + // Copy whatever comes after the deletion + System.arraycopy(arrays[col], (row + length) * sz, newArr, row * sz, (nrow - row - length) * sz); + arrays[col] = newArr; + } + nrow -= length; + initializePointers(); + } + + /** Delete a contiguous set of columns from the table. + * @param col The column (0-indexed) to be deleted. + * @param length The number of rows to be deleted. + * @throws TableException if the request goes outside + * the boundaries of the table or if the length is negative. + */ + public int deleteColumns(int start, int len) throws TableException { + + int ncol = arrays.length; + + if (start < 0 || len < 0 || start + len > ncol) { + throw new TableException("Invalid request to delete columns start: " + start + " length:" + len + + " for table with " + ncol + " columns."); + } + + if (len == 0) { + return rowSize; + } + + for (int i = start; i < start + len; i += 1) { + rowSize -= sizes[i] * ArrayFuncs.getBaseLength(arrays[i]); + } + + int ocol = ncol; + ncol -= len; + + Object[] newArrays = new Object[ncol]; + int[] newSizes = new int[ncol]; + Class[] newBases = new Class[ncol]; + char[] newTypes = new char[ncol]; + + System.arraycopy(arrays, 0, newArrays, 0, start); + System.arraycopy(sizes, 0, newSizes, 0, start); + System.arraycopy(bases, 0, newBases, 0, start); + System.arraycopy(types, 0, newTypes, 0, start); + + int rem = ocol - (start + len); + + System.arraycopy(arrays, start + len, newArrays, start, rem); + System.arraycopy(sizes, start + len, newSizes, start, rem); + System.arraycopy(bases, start + len, newBases, start, rem); + System.arraycopy(types, start + len, newTypes, start, rem); + + + arrays = newArrays; + sizes = newSizes; + bases = newBases; + types = newTypes; + + initializePointers(); + return rowSize; + } +} diff --git a/src/nom/tam/util/Cursor.java b/src/nom/tam/util/Cursor.java new file mode 100644 index 0000000..5b97f26 --- /dev/null +++ b/src/nom/tam/util/Cursor.java @@ -0,0 +1,31 @@ +package nom.tam.util; + +/** This interface extends the Iterator interface + * to allow insertion of data and move to previous entries + * in a collection. + */ +public interface Cursor extends java.util.Iterator { + + /** Is there a previous element in the collection? */ + public abstract boolean hasPrev(); + + /** Get the previous element */ + public abstract Object prev() throws java.util.NoSuchElementException; + + /** Point the list at a particular element. + * Point to the end of the list if the key is not found. + */ + public abstract void setKey(Object key); + + /** Add an unkeyed element to the collection. + * The new element is placed such that it will be called + * by a prev() call, but not a next() call. + */ + public abstract void add(Object reference); + + /** Add a keyed element to the collection. + * The new element is placed such that it will be called + * by a prev() call, but not a next() call. + */ + public abstract void add(Object key, Object reference); +} diff --git a/src/nom/tam/util/DataIO.java b/src/nom/tam/util/DataIO.java new file mode 100644 index 0000000..16312ec --- /dev/null +++ b/src/nom/tam/util/DataIO.java @@ -0,0 +1,12 @@ +package nom.tam.util; + +import java.io.DataInput; +import java.io.DataOutput; + +/** This interface combines the DataInput, DataOutput and + * RandomAccess interfaces to provide a reference type + * which can be used to build BufferedFile in a fashion + * that accommodates both the RandomAccessFile and ByteBuffers + */ +public interface DataIO extends DataInput, DataOutput, RandomAccess { +} diff --git a/src/nom/tam/util/DataTable.java b/src/nom/tam/util/DataTable.java new file mode 100644 index 0000000..4f98e38 --- /dev/null +++ b/src/nom/tam/util/DataTable.java @@ -0,0 +1,32 @@ +package nom.tam.util; + +/* Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +/** This interface defines the properties that + * a generic table should have. + */ +public interface DataTable { + + public abstract void setRow(int row, Object newRow) + throws TableException; + + public abstract Object getRow(int row); + + public abstract void setColumn(int column, Object newColumn) + throws TableException; + + public abstract Object getColumn(int column); + + public abstract void setElement(int row, int col, Object newElement) + throws TableException; + + public abstract Object getElement(int row, int col); + + public abstract int getNRows(); + + public abstract int getNCols(); +} diff --git a/src/nom/tam/util/FormatException.java b/src/nom/tam/util/FormatException.java new file mode 100644 index 0000000..c96090c --- /dev/null +++ b/src/nom/tam/util/FormatException.java @@ -0,0 +1,12 @@ +package nom.tam.util; + +public class FormatException extends java.lang.Exception { + + FormatException() { + super(); + } + + FormatException(String msg) { + super(msg); + } +} diff --git a/src/nom/tam/util/HashedList.java b/src/nom/tam/util/HashedList.java new file mode 100644 index 0000000..5d64712 --- /dev/null +++ b/src/nom/tam/util/HashedList.java @@ -0,0 +1,376 @@ +package nom.tam.util; + +/* Copyright: Thomas McGlynn 1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +/** This class implements a structure which can + * be accessed either through a hash or + * as linear list. Only some elements may have + * a hash key. + * + * This class is motivated by the FITS header + * structure where a user may wish to go through + * the header element by element, or jump directly + * to a given keyword. It assumes that all + * keys are unique. However, all elements in the + * structure need not have a key. + * + * This class does only the search structure + * and knows nothing of the semantics of the + * referenced objects. + * + */ +import java.util.*; +import java.lang.reflect.Array; + +import nom.tam.util.ArrayFuncs; + +import java.util.HashMap; +import java.util.ArrayList; + +public class HashedList implements Collection { + + /** An ordered list of the keys */ + private ArrayList ordered = new ArrayList(); + /** The key value pairs */ + private HashMap keyed = new HashMap(); + /** This is used to generate unique keys for + * elements entered without an key. + */ + private int unkeyedIndex = 0; + + private class HashedListIterator implements Cursor { + + /** This index points to the value that would be returned in + * the next 'next' call. + */ + private int current; + + HashedListIterator(int start) { + current = start; + } + + /** Is there another element? */ + public boolean hasNext() { + return current >= 0 && current < ordered.size(); + } + + /** Is there a previous element? */ + public boolean hasPrev() { + return current > 0; + } + + /** Get the next entry. */ + public Object next() throws NoSuchElementException { + + if (current < 0 || current >= ordered.size()) { + throw new NoSuchElementException("Outside list"); + + } else { + Object key = ordered.get(current); + current += 1; + return keyed.get(key); + } + } + + /** Get the previous entry. */ + public Object prev() throws NoSuchElementException { + if (current <= 0) { + throw new NoSuchElementException("Before beginning of list"); + } + current -= 1; + Object key = ordered.get(current); + return keyed.get(key); + } + + /** Remove an entry from the tree. Note that this can + * now be called anytime after the iterator is created. + */ + public void remove() { + if (current > 0 && current <= ordered.size()) { + + HashedList.this.remove(current - 1); + + // If we just removed the last entry, then we need + // to go back one. + if (current > 0) { + current -= 1; + } + } + } + + /** Add an entry at the current location. The new entry goes before + * the entry that would be returned in the next 'next' call, and + * that call will not be affected by the insertion. + * Note: this method is not in the Iterator interface. + */ + public void add(Object ref) { + Integer nKey = new Integer(unkeyedIndex); + unkeyedIndex += 1; + HashedList.this.add(current, nKey, ref); + current += 1; + } + + /** Add a keyed entry at the current location. The new entry is inserted + * before the entry that would be returned in the next invocation of + * 'next'. The return value for that call is unaffected. + * Note: this method is not in the Iterator interface. + */ + public void add(Object key, Object ref) { + HashedList.this.add(current, key, ref); + current += 1; + } + + /** Point the iterator to a particular keyed entry. This + * method is not in the Iterator interface. + * @param key + */ + public void setKey(Object key) { + if (keyed.containsKey(key)) { + current = ordered.indexOf(key); + } else { + current = ordered.size(); + } + + } + } + + /** Add an element to the end of the list. */ + public boolean add(Object reference) { + Integer nKey = new Integer(unkeyedIndex); + unkeyedIndex += 1; + HashedList.this.add(ordered.size(), nKey, reference); + return true; + + } + + /** Add a keyed element to the end of the list. */ + public boolean add(Object key, Object reference) { + add(ordered.size(), key, reference); + return true; + } + + /** Add an element to the list. + * @param pos The element before which the current element + * be placed. If pos is null put the element at + * the end of the list. + * @param key The hash key for the new object. This may be null + * for an unkeyed entry. + * @param reference The actual object being stored. + */ + public boolean add(int pos, Object key, Object reference) { + + if (keyed.containsKey(key)) { + int oldPos = ordered.indexOf(key); + removeKey(key); + if (oldPos < pos) { + pos -= 1; + } + } + + keyed.put(key, reference); + if (pos >= ordered.size()) { + ordered.add(key); + + } else { + ordered.add(pos, key); + } + + return true; + } + + /** Remove a keyed object from the list. Unkeyed + * objects can be removed from the list using a + * HashedListIterator or using the remove(Object) + * method. + */ + public boolean removeKey(Object key) { + if (keyed.containsKey(key)) { + int index = ordered.indexOf(key); + keyed.remove(key); + ordered.remove(index); + return true; + } + return false; + } + + /** Remove an object from the list giving just + * the object value. + */ + public boolean remove(Object o) { + + if (keyed.containsValue(o)) { + for (int i = 0; i < ordered.size(); i += 1) { + if (keyed.get(ordered.get(i)).equals(o)) { + return removeKey(ordered.get(i)); + } + } + } + return false; + } + + /** Remove an object from the list giving the object index..*/ + public boolean remove(int index) { + if (index >= 0 && index < ordered.size()) { + Object key = ordered.get(index); + return removeKey(key); + } + return false; + } + + /** Return an iterator over the entire list. + * The iterator may be used to delete + * entries as well as to retrieve existing + * entries. A knowledgeable user can + * cast this to a HashedListIterator and + * use it to add as well as delete entries. + */ + public Iterator iterator() { + return new HashedListIterator(0); + } + + /** Return an iterator over the list starting + * with the entry with a given key. + */ + public HashedListIterator iterator(Object key) throws NoSuchElementException { + if (keyed.containsKey(key)) { + return new HashedListIterator(ordered.indexOf(key)); + } else { + throw new NoSuchElementException("Unknown key for iterator:" + key); + } + } + + /** Return an iterator starting with the n'th + * entry. + */ + public HashedListIterator iterator(int n) throws NoSuchElementException { + if (n >= 0 && n <= ordered.size()) { + return new HashedListIterator(n); + } else { + throw new NoSuchElementException("Invalid index for iterator:" + n); + } + } + + /** Return the value of a keyed entry. Non-keyed + * entries may be returned by requesting an iterator. + */ + public Object get(Object key) { + return keyed.get(key); + } + + /** Return the n'th entry from the beginning. */ + public Object get(int n) throws NoSuchElementException { + return keyed.get(ordered.get(n)); + } + + /** Replace the key of a given element. + * @param oldKey The previous key. This key must + * be present in the hash. + * @param newKey The new key. This key + * must not be present in the hash. + * @return if the replacement was successful. + */ + public boolean replaceKey(Object oldKey, Object newKey) { + + if (!keyed.containsKey(oldKey) || keyed.containsKey(newKey)) { + return false; + } + + Object oldVal = keyed.get(oldKey); + int index = ordered.indexOf(oldKey); + remove(index); + return add(index, newKey, oldVal); + + } + + /** Check if the key is included in the list */ + public boolean containsKey(Object key) { + return keyed.containsKey(key); + } + + /** Return the number of elements in the list. */ + public int size() { + return ordered.size(); + } + + /** Add another collection to this one list. + * All entries are added as unkeyed entries to the end of the list. + */ + public boolean addAll(Collection c) { + Object[] array = c.toArray(); + for (int i = 0; i < array.length; i += 1) { + add(array[i]); + } + return true; + } + + /** Clear the collection */ + public void clear() { + keyed.clear(); + ordered.clear(); + } + + /** Does the HashedList contain this element? */ + public boolean contains(Object o) { + return keyed.containsValue(o); + } + + /** Does the HashedList contain all the elements + * of this other collection. + */ + public boolean containsAll(Collection c) { + return keyed.values().containsAll(c); + } + + /** Is the HashedList empty? */ + public boolean isEmpty() { + return keyed.isEmpty(); + } + + /** Remove all the elements that are found in another collection. */ + public boolean removeAll(Collection c) { + Object[] o = c.toArray(); + boolean result = false; + for (int i = 0; i < o.length; i += 1) { + result = result | remove(o[i]); + } + return result; + } + + /** Retain only elements contained in another collection */ + public boolean retainAll(Collection c) { + + Iterator iter = iterator(); + boolean result = false; + while (iter.hasNext()) { + Object o = iter.next(); + if (!c.contains(o)) { + iter.remove(); + result = true; + } + } + return result; + } + + /** Convert to an array of objects */ + public Object[] toArray() { + Object[] o = new Object[ordered.size()]; + return toArray(o); + } + + /** Convert to an array of objects of + * a specified type. + */ + public Object[] toArray(Object[] o) { + return keyed.values().toArray(o); + } + + /** Sort the keys into some desired order. + */ + public void sort(Comparator comp) { + java.util.Collections.sort(ordered, comp); + } +} diff --git a/src/nom/tam/util/PrimitiveInfo.java b/src/nom/tam/util/PrimitiveInfo.java new file mode 100644 index 0000000..7a0e5a9 --- /dev/null +++ b/src/nom/tam/util/PrimitiveInfo.java @@ -0,0 +1,38 @@ +package nom.tam.util; + +/** This interface collects some information about Java primitives. + */ +public interface PrimitiveInfo { + + /** Suffixes used for the classnames for primitive arrays. */ + char[] suffixes = new char[]{'B', 'S', 'C', 'I', 'J', 'F', 'D', 'Z'}; + /** Classes of the primitives. These should be in windening order + * (char is as always a problem). + */ + Class[] classes = new Class[]{ + byte.class, short.class, char.class, int.class, + long.class, float.class, double.class, boolean.class}; + /** Is this a numeric class */ + boolean[] isNumeric = new boolean[]{true, true, true, true, true, true, true, false}; + /** Full names */ + String[] types = new String[]{ + "byte", "short", "char", "int", + "long", "float", "double", "boolean" + }; + /** Sizes */ + int[] sizes = new int[]{1, 2, 2, 4, 8, 4, 8, 1}; + /** Index of first element of above arrays referring to a numeric type */ + int FIRST_NUMERIC = 0; + /** Index of last element of above arrays referring to a numeric type */ + int LAST_NUMERIC = 6; + int BYTE_INDEX = 0; + int SHORT_INDEX = 1; + int CHAR_INDEX = 2; + int INT_INDEX = 3; + int LONG_INDEX = 4; + int FLOAT_INDEX = 5; + int DOUBLE_INDEX = 6; + int BOOLEAN_INDEX = 7; +} + + diff --git a/src/nom/tam/util/RandomAccess.java b/src/nom/tam/util/RandomAccess.java new file mode 100644 index 0000000..5ddbc62 --- /dev/null +++ b/src/nom/tam/util/RandomAccess.java @@ -0,0 +1,15 @@ +package nom.tam.util; + +/** These packages define the methods which indicate that + * an i/o stream may be accessed in arbitrary order. + * The method signatures are taken from RandomAccessFile + * though that class does not implement this interface. + */ +public interface RandomAccess extends ArrayDataInput { + + /** Move to a specified location in the stream. */ + public void seek(long offsetFromStart) throws java.io.IOException; + + /** Get the current position in the stream */ + public long getFilePointer(); +} diff --git a/src/nom/tam/util/TableException.java b/src/nom/tam/util/TableException.java new file mode 100644 index 0000000..554b6f4 --- /dev/null +++ b/src/nom/tam/util/TableException.java @@ -0,0 +1,19 @@ +package nom.tam.util; +/* + * Copyright: Thomas McGlynn 1997-1998. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ + +public class TableException extends Exception { + + public TableException() { + super(); + } + + public TableException(String msg) { + super(msg); + } +} diff --git a/src/nom/tam/util/TruncationException.java b/src/nom/tam/util/TruncationException.java new file mode 100644 index 0000000..434f1bc --- /dev/null +++ b/src/nom/tam/util/TruncationException.java @@ -0,0 +1,12 @@ +package nom.tam.util; + +public class TruncationException extends Exception { + + public TruncationException() { + super(); + } + + public TruncationException(String msg) { + super(msg); + } +} diff --git a/src/nom/tam/util/test/ArrayFuncs2Test.java b/src/nom/tam/util/test/ArrayFuncs2Test.java new file mode 100644 index 0000000..2b21e3e --- /dev/null +++ b/src/nom/tam/util/test/ArrayFuncs2Test.java @@ -0,0 +1,161 @@ +package nom.tam.util.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; +import nom.tam.util.ArrayFuncs; + +public class ArrayFuncs2Test { + + /** Test and demonstrate the ArrayFuncs methods. + */ + @Test + public void test() { + + int[][][] test1 = new int[10][9][8]; + boolean[][] test2 = new boolean[4][]; + test2[0] = new boolean[5]; + test2[1] = new boolean[4]; + test2[2] = new boolean[3]; + test2[3] = new boolean[2]; + + double[][] test3 = new double[10][20]; + StringBuffer[][] test4 = new StringBuffer[3][2]; + + assertEquals("getBaseClass()", int.class, ArrayFuncs.getBaseClass(test1)); + assertEquals("getBaseLength()", 4, ArrayFuncs.getBaseLength(test1)); + assertEquals("computeSize()", 4 * 8 * 9 * 10, ArrayFuncs.computeSize(test1)); + assertEquals("computeLSize()", 4 * 8 * 9 * 10L, ArrayFuncs.computeLSize(test1)); + + assertEquals("getBaseClass(boolean)", boolean.class, ArrayFuncs.getBaseClass(test2)); + assertEquals("getBaseLength(boolean)", 1, ArrayFuncs.getBaseLength(test2)); + assertEquals("computeSize() not rect", 1 * (2 + 3 + 4 + 5), ArrayFuncs.computeSize(test2)); + assertEquals("computeLSize() not rect", 1L * (2 + 3 + 4 + 5), ArrayFuncs.computeLSize(test2)); + + assertEquals("getBaseClass(double)", double.class, ArrayFuncs.getBaseClass(test3)); + assertEquals("getBaseLength(double)", 8, ArrayFuncs.getBaseLength(test3)); + assertEquals("computeSize(double)", 8 * 10 * 20, ArrayFuncs.computeSize(test3)); + assertEquals("computeLSize(double)", 8 * 10 * 20L, ArrayFuncs.computeLSize(test3)); + + assertEquals("getBaseClass(StrBuf)", StringBuffer.class, ArrayFuncs.getBaseClass(test4)); + assertEquals("getBaseLength(StrBuf)", -1, ArrayFuncs.getBaseLength(test4)); + assertEquals("computeSize(StrBuf)", 0, ArrayFuncs.computeSize(test4)); + assertEquals("computeLSize(StrBuf)", 0L, ArrayFuncs.computeLSize(test4)); + + Object[] agg = new Object[4]; + agg[0] = test1; + agg[1] = test2; + agg[2] = test3; + agg[3] = test4; + + assertEquals("getBaseClass(Object[])", Object.class, ArrayFuncs.getBaseClass(agg)); + assertEquals("getBaseLength(Object[])", -1, ArrayFuncs.getBaseLength(agg)); + + // Add up all the primitive arrays and ignore the objects. + assertEquals("computeSize(Object[])", 2880 + 14 + 1600 + 0, ArrayFuncs.computeSize(agg)); + assertEquals("computeLSize(Object[])", 2880L + 14 + 1600 + 0, ArrayFuncs.computeLSize(agg)); + + // Try allocating a very large array. This is likely to fail + // in the allocation step, so don't consider that to be a failure. + try { + float[][] data = new float[10000][30000]; + long expect = 10000L * 30000 * 4; + assertEquals("computLSize(big)", ArrayFuncs.computeLSize(data), expect); + + } catch (Error e) { + System.out.println("Unable to allocate large array. Test skipped"); + } + + + for (int i = 0; i < test1.length; i += 1) { + for (int j = 0; j < test1[i].length; j += 1) { + for (int k = 0; k < test1[i][j].length; k += 1) { + test1[i][j][k] = i + j + k; + } + } + } + int[][][] test5 = (int[][][]) ArrayFuncs.deepClone(test1); + + assertEquals("deepClone()", true, ArrayFuncs.arrayEquals(test1, test5)); + test5[1][1][1] = -3; + assertEquals("arrayEquals()", false, ArrayFuncs.arrayEquals(test1, test5)); + + int[] dimsOrig = ArrayFuncs.getDimensions(test1); + int[] test6 = (int[]) ArrayFuncs.flatten(test1); + + int[] dims = ArrayFuncs.getDimensions(test6); + + assertEquals("getDimensions()", 3, dimsOrig.length); + assertEquals("getDimensions()", 10, dimsOrig[0]); + assertEquals("getDimensions()", 9, dimsOrig[1]); + assertEquals("getDimensions()", 8, dimsOrig[2]); + assertEquals("flatten()", 1, dims.length); + + int[] newdims = {8, 9, 10}; + + int[][][] test7 = (int[][][]) ArrayFuncs.curl(test6, newdims); + + int[] dimsAfter = ArrayFuncs.getDimensions(test7); + + assertEquals("curl()", 3, dimsAfter.length); + assertEquals("getDimensions()", 8, dimsAfter[0]); + assertEquals("getDimensions()", 9, dimsAfter[1]); + assertEquals("getDimensions()", 10, dimsAfter[2]); + + byte[][][] xtest1 = (byte[][][]) ArrayFuncs.convertArray(test1, byte.class); + + assertEquals("convertArray(toByte)", byte.class, ArrayFuncs.getBaseClass(xtest1)); + assertEquals("convertArray(tobyte)", test1[3][3][3], (int) xtest1[3][3][3]); + + double[][][] xtest2 = (double[][][]) ArrayFuncs.convertArray(test1, double.class); + assertEquals("convertArray(toByte)", double.class, ArrayFuncs.getBaseClass(xtest2)); + assertEquals("convertArray(tobyte)", test1[3][3][3], (int) xtest2[3][3][3]); + + int[] xtest3 = (int[]) ArrayFuncs.newInstance(int.class, 20); + int[] xtd = ArrayFuncs.getDimensions(xtest3); + assertEquals("newInstance(vector)", 1, xtd.length); + assertEquals("newInstance(vector)", 20, xtd[0]); + double[][][][] xtest4 = (double[][][][]) ArrayFuncs.newInstance(double.class, new int[]{5, 4, 3, 2}); + xtd = ArrayFuncs.getDimensions(xtest4); + assertEquals("newInstance(tensor)", 4, xtd.length); + assertEquals("newInstance(tensor)", 5, xtd[0]); + assertEquals("newInstance(tensor)", 4, xtd[1]); + assertEquals("newInstance(tensor)", 3, xtd[2]); + assertEquals("newInstance(tensor)", 2, xtd[3]); + assertEquals("nElements()", 120, ArrayFuncs.nElements(xtest4)); + assertEquals("nLElements()", 120L, ArrayFuncs.nLElements(xtest4)); + + ArrayFuncs.testPattern(xtest4, (byte) -1); + + assertEquals("testPattern()", (double) -1, xtest4[0][0][0][0], 0); + assertEquals("testPattern()", (double) 118, xtest4[4][3][2][1], 0); + double[] xtest4x = (double[]) ArrayFuncs.getBaseArray(xtest4); + + assertEquals("getBaseArray()", 2, xtest4x.length); + + double[] x = {1, 2, 3, 4, 5}; + double[] y = new double[x.length]; + for (int i = 0; i < y.length; i += 1) { + y[i] = x[i] + 1.E-10; + } + + assertEquals("eqTest", false, ArrayFuncs.arrayEquals(x, y)); + assertEquals("eqTest2", true, ArrayFuncs.arrayEquals(x, y, 0., 1.e-9)); + assertEquals("eqTest3", true, ArrayFuncs.arrayEquals(x, y, 1.E-5, 1.e-9)); + assertEquals("eqTest4", false, ArrayFuncs.arrayEquals(x, y, 0., 1.e-11)); + assertEquals("eqTest5", false, ArrayFuncs.arrayEquals(x, y, 1.E-5, 0.)); + + float[] fx = {1, 2, 3, 4, 5}; + float[] fy = new float[fx.length]; + for (int i = 0; i < fy.length; i += 1) { + fy[i] = fx[i] + 1.E-5F; + } + + assertEquals("eqTest6", false, ArrayFuncs.arrayEquals(fx, fy)); + assertEquals("eqTest7", true, ArrayFuncs.arrayEquals(fx, fy, 1.E-4, 0.)); + assertEquals("eqTest8", false, ArrayFuncs.arrayEquals(fx, fy, 1.E-6, 0.)); + assertEquals("eqTest9", false, ArrayFuncs.arrayEquals(fx, fy, 0., 0.)); + assertEquals("eqTest10", false, ArrayFuncs.arrayEquals(fx, fy, 0., 1.E-4)); + + } +} diff --git a/src/nom/tam/util/test/ArrayFuncsTest.java b/src/nom/tam/util/test/ArrayFuncsTest.java new file mode 100644 index 0000000..df5efb0 --- /dev/null +++ b/src/nom/tam/util/test/ArrayFuncsTest.java @@ -0,0 +1,364 @@ +/* + * ArrayFuncsTest.java + * JUnit based test + * + * Created on December 2, 2007, 7:19 PM + */ +package nom.tam.util.test; + +import junit.framework.*; +import java.lang.reflect.*; +import java.util.Arrays; +import nom.tam.util.ArrayFuncs; + +/** + * + * @author Thomas McGlynn + */ +public class ArrayFuncsTest extends TestCase { + + public ArrayFuncsTest(String testName) { + super(testName); + } + + protected void setUp() throws Exception { + } + + protected void tearDown() throws Exception { + } + + /** + * Test of computeSize method, of class nom.tam.util.ArrayFuncs. + */ + public void testComputeSize() { + System.out.println("computeSize"); + + Object o = null; + + int expResult = 0; + int result = ArrayFuncs.computeSize(o); + assertEquals(expResult, result); + int[][] x = new int[2][3]; + assertEquals(ArrayFuncs.computeSize(x), 24); + assertEquals(ArrayFuncs.computeSize(new double[3]), 24); + assertEquals(ArrayFuncs.computeSize("1234"), 4); + assertEquals(ArrayFuncs.computeSize(new Object()), 0); + assertEquals(ArrayFuncs.computeSize(new Double[5]), 0); + assertEquals(ArrayFuncs.computeSize(new Double[]{ + new Double(0), new Double(1), new Double(2)}), 24); + assertEquals(ArrayFuncs.computeLSize(x), 24); + assertEquals(ArrayFuncs.computeLSize(new double[3]), 24); + assertEquals(ArrayFuncs.computeLSize("1234"), 4); + assertEquals(ArrayFuncs.computeLSize(new Object()), 0); + assertEquals(ArrayFuncs.computeLSize(new Double[5]), 0); + assertEquals(ArrayFuncs.computeLSize(new Double[]{ + new Double(0), new Double(1), new Double(2)}), 24); + } + + /** + * Test of nElements method, of class nom.tam.util.ArrayFuncs. + */ + public void testNElements() { + System.out.println("nElements"); + + Object o = null; + + assertEquals(ArrayFuncs.nElements(null), 0); + assertEquals(ArrayFuncs.nElements(new int[2][2][3]), 12); + assertEquals(ArrayFuncs.nLElements(null), 0); + assertEquals(ArrayFuncs.nLElements(new int[2][2][3]), 12); + } + + /** + * Test of deepClone method, of class nom.tam.util.ArrayFuncs. + */ + public void testDeepClone() { + int[][] test = {{0, 1}, {2, 3}, {4, 5}}; + int[][] result = (int[][]) nom.tam.util.ArrayFuncs.deepClone(test); + + for (int i = 0; i < test.length; i += 1) { + for (int j = 0; j < test[i].length; j += 1) { + assertEquals(test[i][j], result[i][j]); + } + } + } + + public class CloneTest implements Cloneable { + + public int value = 2; + + public Object clone() { + try { + return super.clone(); + } catch (Exception e) { + } + return null; + } + + public boolean equals(Object x) { + return (x instanceof CloneTest) + && (((CloneTest) x).value == this.value); + } + } + + /** + * Test of genericClone method, of class nom.tam.util.ArrayFuncs. + */ + public void testGenericClone() { + System.out.println("genericClone"); + + Object o = new int[]{1, 2, 3}; + + Object result = nom.tam.util.ArrayFuncs.genericClone(o); + + int[] x = (int[]) o; + int[] y = (int[]) result; + for (int i = 0; i < x.length; i += 1) { + assertEquals(x[i], y[i]); + } + CloneTest xa = new CloneTest(); + xa.value = 4; + Object ya = ArrayFuncs.genericClone(xa); + assertTrue(xa != ya); + assertTrue(xa.equals(ya)); + } + + /** + * Test of copyArray method, of class nom.tam.util.ArrayFuncs. + */ + public void testCopyArray() { + System.out.println("copyArray"); + + double[] start = new double[]{1, 2, 3, 4, 5, 6}; + double[] finish = new double[6]; + ArrayFuncs.copyArray(start, finish); + assertTrue(ArrayFuncs.arrayEquals(start, finish)); + } + + /** + * Test of getDimensions method, of class nom.tam.util.ArrayFuncs. + */ + public void testGetDimensions() { + System.out.println("getDimensions"); + + Object o = null; + int[] expResult = null; + int[] result = nom.tam.util.ArrayFuncs.getDimensions(o); + assertEquals(expResult, result); + + assertEquals(ArrayFuncs.getDimensions(new Integer(0)).length, 0); + int[][] test = new int[2][3]; + int[] dims = ArrayFuncs.getDimensions(test); + assertEquals(dims.length, 2); + assertEquals(dims[0], 2); + assertEquals(dims[1], 3); + } + + /** + * Test of getBaseArray method, of class nom.tam.util.ArrayFuncs. + */ + public void testGetBaseArray() { + + int[][][] test = new int[2][3][4]; + byte b = 0; + ArrayFuncs.testPattern(test, b); + + assertEquals(ArrayFuncs.getBaseArray(test), test[0][0]); + } + + /** + * Test of getBaseClass method, of class nom.tam.util.ArrayFuncs. + */ + public void testGetBaseClass() { + System.out.println("getBaseClass"); + + assertEquals(ArrayFuncs.getBaseClass(new int[2][3]), int.class); + assertEquals(ArrayFuncs.getBaseClass(new String[3]), String.class); + } + + /** + * Test of getBaseLength method, of class nom.tam.util.ArrayFuncs. + */ + public void testGetBaseLength() { + + assertEquals(ArrayFuncs.getBaseLength(new int[2][3]), 4); + assertEquals(ArrayFuncs.getBaseLength(new double[2][3]), 8); + assertEquals(ArrayFuncs.getBaseLength(new byte[2][3]), 1); + assertEquals(ArrayFuncs.getBaseLength(new short[2][3]), 2); + assertEquals(ArrayFuncs.getBaseLength(new int[2][3]), 4); + assertEquals(ArrayFuncs.getBaseLength(new char[2][3]), 2); + assertEquals(ArrayFuncs.getBaseLength(new float[2][3]), 4); + assertEquals(ArrayFuncs.getBaseLength(new boolean[2][3]), 1); + assertEquals(ArrayFuncs.getBaseLength(new Object[2][3]), -1); + } + + /** + * Test of generateArray method, of class nom.tam.util.ArrayFuncs. + */ + public void testGenerateArray() { + System.out.println("generateArray"); + + Class baseType = int.class; + int[] dims = {2, 3, 4}; + + Object result = nom.tam.util.ArrayFuncs.generateArray(baseType, dims); + assertEquals(result.getClass(), int[][][].class); + int[][][] x = (int[][][]) result; + assertEquals(x.length, 2); + assertEquals(x[0].length, 3); + assertEquals(x[0][0].length, 4); + + } + + /** + * Test of testPattern method, of class nom.tam.util.ArrayFuncs. + */ + public void testTestPattern() { + System.out.println("testPattern"); + + byte start = 2; + int[] arr = new int[8]; + + byte expResult = 0; + byte result = nom.tam.util.ArrayFuncs.testPattern(arr, start); + assertEquals(result, (byte) (start + arr.length)); + assertEquals(start, arr[0]); + assertEquals(start + arr.length - 1, arr[arr.length - 1]); + } + + /** + * Test of flatten method, of class nom.tam.util.ArrayFuncs. + */ + public void testFlatten() { + System.out.println("flatten"); + + int[][][] test = new int[2][3][4]; + + int[] result = (int[]) ArrayFuncs.flatten(test); + assertEquals(result.length, 24); + } + + /** + * Test of curl method, of class nom.tam.util.ArrayFuncs. + */ + public void testCurl() { + System.out.println("curl"); + + int[] dimens = new int[]{2, 3, 4}; + int[] test = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; + + int[][][] res = (int[][][]) nom.tam.util.ArrayFuncs.curl(test, dimens); + assertEquals(res.length, 2); + assertEquals(res[0].length, 3); + assertEquals(res[0][0].length, 4); + assertEquals(res[0][0][0], 0); + assertEquals(res[0][0][3], 3); + assertEquals(res[1][2][3], 23); + } + + /** + * Test of mimicArray method, of class nom.tam.util.ArrayFuncs. + */ + public void testMimicArray() { + System.out.println("mimicArray"); + + int[][] array = new int[2][3]; + Class newType = double.class; + + double[][] result = (double[][]) nom.tam.util.ArrayFuncs.mimicArray(array, newType); + assertEquals(result.length, array.length); + assertEquals(result[0].length, array[0].length); + } + + /** + * Test of convertArray method, of class nom.tam.util.ArrayFuncs. + */ + public void testConvertArray() { + System.out.println("convertArray"); + + int[][] array = {{1, 2, 3}, {4, 5, 6}}; + Class newType = double.class; + + boolean reuse = true; + double[][] dres = (double[][]) ArrayFuncs.convertArray(array, newType, reuse); + assertEquals(dres.length, array.length); + assertEquals(dres[0].length, array[0].length); + + newType = int.class; + int[][] ires = (int[][]) ArrayFuncs.convertArray(array, newType, true); + assertEquals(array, ires); + + ires = (int[][]) ArrayFuncs.convertArray(array, newType, false); + assertNotSame(array, ires); + assertTrue(ArrayFuncs.arrayEquals(array, ires)); + } + + /** + * Test of copyInto method, of class nom.tam.util.ArrayFuncs. + */ + public void testCopyInto() { + System.out.println("copyInto"); + + int[][] x = {{2, 3, 4}, {5, 6, 7}}; + double[][] y = new double[2][3]; + + ArrayFuncs.copyInto(x, y); + + assertEquals((double) x[0][0], y[0][0]); + assertEquals((double) x[1][2], y[1][2]); + } + + /** + * Test of arrayEquals method, of class nom.tam.util.ArrayFuncs. + */ + public void testArrayEquals() { + System.out.println("arrayEquals"); + + int[][] x = {{1, 2, 3}, {4, 5, 6}}; + int[][] y = {{1, 2, 3}, {4, 5, 6}}; + int[][] z = {{1, 2, 3}, {4, 5, 7}}; + int[][] t = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + + + assertTrue(ArrayFuncs.arrayEquals(null, null)); + assertFalse(ArrayFuncs.arrayEquals(null, new int[2])); + assertTrue(ArrayFuncs.arrayEquals(x, y)); + assertFalse(ArrayFuncs.arrayEquals(x, z)); + assertFalse(ArrayFuncs.arrayEquals(x, t)); + assertTrue(ArrayFuncs.arrayEquals(x[0], z[0])); + } + + /** + * Test of doubleArrayEquals method, of class nom.tam.util.ArrayFuncs. + */ + public void testDoubleArrayEquals() { + + double x[] = {1, 2, 3}; + double y[] = {1, 2, 3}; + System.out.println("doubleArrayEquals"); + + double tol = 0.0; + + assertTrue(ArrayFuncs.doubleArrayEquals(x, y, tol)); + x[0] += 1.e-14; + assertFalse(ArrayFuncs.doubleArrayEquals(x, y, tol)); + tol = 1.e-13; + assertTrue(ArrayFuncs.doubleArrayEquals(x, y, tol)); + } + + /** + * Test of floatArrayEquals method, of class nom.tam.util.ArrayFuncs. + */ + public void testFloatArrayEquals() { + float x[] = {1f, 2f, 3f}; + float y[] = {1f, 2f, 3f}; + System.out.println("floatArrayEquals"); + + float tol = 0.0F; + assertTrue(ArrayFuncs.floatArrayEquals(x, y, tol)); + x[0] += 1.e-6f; + assertFalse(ArrayFuncs.floatArrayEquals(x, y, tol)); + tol = 1.e-5f; + assertTrue(ArrayFuncs.floatArrayEquals(x, y, tol)); + } +} diff --git a/src/nom/tam/util/test/BigFileTest.java b/src/nom/tam/util/test/BigFileTest.java new file mode 100644 index 0000000..81f40a3 --- /dev/null +++ b/src/nom/tam/util/test/BigFileTest.java @@ -0,0 +1,60 @@ +package nom.tam.util.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.util.BufferedFile; +import nom.tam.util.BufferedDataInputStream; +import java.io.FileInputStream; + +public class BigFileTest { + + @Test + public void test() throws Exception { + try { + // First create a 3 GB file. + String fname = System.getenv("BIGFILETEST"); + if (fname == null) { + System.out.println("BIGFILETEST environment not set. Returning without test"); + return; + } + System.out.println("Big file test. Takes quite a while."); + byte[] buf = new byte[100000000]; // 100 MB + BufferedFile bf = new BufferedFile(fname, "rw"); + byte sample = 13; + + for (int i = 0; i < 30; i += 1) { + bf.write(buf); // 30 x 100 MB = 3 GB. + if (i == 24) { + bf.write(new byte[]{sample}); + } // Add a marker. + } + bf.close(); + + // Now try to skip within the file. + bf = new BufferedFile(fname, "r"); + long skip = 2500000000L; // 2.5 G + + long val1 = bf.skipBytes(skip); + long val2 = bf.getFilePointer(); + int val = bf.read(); + bf.close(); + + assertEquals("SkipResult", skip, val1); + assertEquals("SkipPos", skip, val2); + assertEquals("SkipVal", (int) sample, val); + + BufferedDataInputStream bdis = new BufferedDataInputStream( + new FileInputStream(fname)); + val1 = bdis.skipBytes(skip); + val = bdis.read(); + bdis.close(); + assertEquals("SSkipResult", skip, val1); + assertEquals("SSkipVal", (int) sample, val); + } catch (Exception e) { + e.printStackTrace(System.err); + throw e; + } + } +} diff --git a/src/nom/tam/util/test/BufferedFileTester.java b/src/nom/tam/util/test/BufferedFileTester.java new file mode 100644 index 0000000..55d1d77 --- /dev/null +++ b/src/nom/tam/util/test/BufferedFileTester.java @@ -0,0 +1,867 @@ +/* Copyright: Thomas McGlynn 1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +package nom.tam.util.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +import nom.tam.util.*; +import nom.tam.util.ArrayFuncs; +import java.io.*; + +/** This class provides runs tests of the + * BufferedI/O classes: BufferedFile, BufferedDataInputStream + * and BufferedDataOutputStream. A limited comparison + * to the standard I/O classes can also be made. + *

+ * Input and output of all primitive scalar and array types is + * tested, however input and output of String data is not. + * Users may choose to test the BufferedFile class, the + * BufferedDataXPUT classes array methods, the BufferedDataXPUT + * classes using the methods of DataXput, the traditional + * I/O classes, or any combination thereof. + */ +public class BufferedFileTester { + + /** Usage: java nom.tam.util.test.BufferedFileTester file [dim [iter [flags]]] + * where + * file is the file to be read and written. + * dim is the dimension of the arrays to be written. + * iter is the number of times each array is written. + * flags a string indicating what I/O to test + * O -- test old I/O (RandomAccessFile and standard streams) + * R -- BufferedFile (i.e., random access) + * S -- BufferedDataXPutStream + * X -- BufferedDataXPutStream using standard methods + */ + public static void main(String[] args) throws Exception { + + String filename = args[0]; + int dim = 1000; + if (args.length > 1) { + dim = Integer.parseInt(args[1]); + } + int iter = 1; + if (args.length > 2) { + iter = Integer.parseInt(args[2]); + } + + System.out.println("Allocating arrays."); + double[] db = new double[dim]; + float[] fl = new float[dim]; + int[] in = new int[dim]; + long[] ln = new long[dim]; + short[] sh = new short[dim]; + byte[] by = new byte[dim]; + char[] ch = new char[dim]; + boolean[] bl = new boolean[dim]; + + System.out.println("Initializing arrays -- may take a while"); + int sign = 1; + for (int i = 0; i < dim; i += 1) { + + + double x = sign * Math.pow(10., 20 * Math.random() - 10); + db[i] = x; + fl[i] = (float) x; + + if (Math.abs(x) < 1) { + x = 1 / x; + } + + in[i] = (int) x; + ln[i] = (long) x; + sh[i] = (short) x; + by[i] = (byte) x; + ch[i] = (char) x; + bl[i] = x > 0; + + sign = -sign; + } + + // Ensure special values are tested. + + by[0] = Byte.MIN_VALUE; + by[1] = Byte.MAX_VALUE; + by[2] = 0; + ch[0] = Character.MIN_VALUE; + ch[1] = Character.MAX_VALUE; + ch[2] = 0; + sh[0] = Short.MAX_VALUE; + sh[1] = Short.MIN_VALUE; + sh[0] = 0; + in[0] = Integer.MAX_VALUE; + in[1] = Integer.MIN_VALUE; + in[2] = 0; + ln[0] = Long.MIN_VALUE; + ln[1] = Long.MAX_VALUE; + ln[2] = 0; + fl[0] = Float.MIN_VALUE; + fl[1] = Float.MAX_VALUE; + fl[2] = Float.POSITIVE_INFINITY; + fl[3] = Float.NEGATIVE_INFINITY; + fl[4] = Float.NaN; + fl[5] = 0; + db[0] = Double.MIN_VALUE; + db[1] = Double.MAX_VALUE; + db[2] = Double.POSITIVE_INFINITY; + db[3] = Double.NEGATIVE_INFINITY; + db[4] = Double.NaN; + db[5] = 0; + + double[] db2 = new double[dim]; + float[] fl2 = new float[dim]; + int[] in2 = new int[dim]; + long[] ln2 = new long[dim]; + short[] sh2 = new short[dim]; + byte[] by2 = new byte[dim]; + char[] ch2 = new char[dim]; + boolean[] bl2 = new boolean[dim]; + + int[][][][] multi = new int[10][10][10][10]; + int[][][][] multi2 = new int[10][10][10][10]; + for (int i = 0; i < 10; i += 1) { + multi[i][i][i][i] = i; + } + + if (args.length < 4 || args[3].indexOf('O') >= 0) { + standardFileTest(filename, iter, in, in2); + standardStreamTest(filename, iter, in, in2); + } + + if (args.length < 4 || args[3].indexOf('X') >= 0) { + buffStreamSimpleTest(filename, iter, in, in2); + } + + if (args.length < 4 || args[3].indexOf('R') >= 0) { + bufferedFileTest(filename, iter, db, db2, fl, fl2, ln, ln2, in, in2, sh, sh2, + ch, ch2, by, by2, bl, bl2, multi, multi2); + } + + + if (args.length < 4 || args[3].indexOf('S') >= 0) { + bufferedStreamTest(filename, iter, db, db2, fl, fl2, ln, ln2, in, in2, sh, sh2, + ch, ch2, by, by2, bl, bl2, multi, multi2); + } + } + + public static void standardFileTest(String filename, int iter, int[] in, int[] in2) + throws Exception { + System.out.println("Standard I/O library: java.io.RandomAccessFile"); + + RandomAccessFile f = new RandomAccessFile(filename, "rw"); + int dim = in.length; + resetTime(); + f.seek(0); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + f.writeInt(in[i]); + } + } + System.out.println(" RAF Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + f.seek(0); + resetTime(); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + in2[i] = f.readInt(); + } + } + System.out.println(" RAF Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + + + synchronized (f) { + f.seek(0); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + f.writeInt(in[i]); + } + } + System.out.println(" SyncRAF Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + f.seek(0); + resetTime(); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + in2[i] = f.readInt(); + } + } + } + System.out.println(" SyncRAF Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + } + + public static void standardStreamTest(String filename, int iter, int[] in, int[] in2) + throws Exception { + System.out.println("Standard I/O library: java.io.DataXXputStream"); + System.out.println(" layered atop a BufferedXXputStream"); + + DataOutputStream f = new DataOutputStream(new BufferedOutputStream( + new FileOutputStream(filename), 32768)); + resetTime(); + int dim = in.length; + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + f.writeInt(in[i]); + } + } + f.flush(); + f.close(); + System.out.println(" DIS Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + + DataInputStream is = new DataInputStream(new BufferedInputStream( + new FileInputStream(filename), 32768)); + resetTime(); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + in2[i] = is.readInt(); + } + } + System.out.println(" DIS Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + + + f = new DataOutputStream(new BufferedOutputStream( + new FileOutputStream(filename), 32768)); + resetTime(); + dim = in.length; + synchronized (f) { + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + f.writeInt(in[i]); + } + } + f.flush(); + f.close(); + System.out.println(" DIS Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + + is = new DataInputStream(new BufferedInputStream( + new FileInputStream(filename), 32768)); + resetTime(); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + in2[i] = is.readInt(); + } + } + } + System.out.println(" DIS Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + } + + public static void buffStreamSimpleTest(String filename, int iter, int[] in, int[] in2) + throws Exception { + + System.out.println("New libraries: nom.tam.BufferedDataXXputStream"); + System.out.println(" Using non-array I/O"); + BufferedDataOutputStream f = new BufferedDataOutputStream( + new FileOutputStream(filename), 32768); + resetTime(); + int dim = in.length; + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + f.writeInt(in[i]); + } + } + f.flush(); + f.close(); + System.out.println(" BDS Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + + BufferedDataInputStream is = new BufferedDataInputStream(new BufferedInputStream( + new FileInputStream(filename), 32768)); + resetTime(); + for (int j = 0; j < iter; j += 1) { + for (int i = 0; i < dim; i += 1) { + in2[i] = is.readInt(); + } + } + System.out.println(" BDS Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + } + + public static void bufferedStreamTest(String filename, int iter, double[] db, double[] db2, + float[] fl, float[] fl2, long[] ln, long[] ln2, + int[] in, int[] in2, short[] sh, short[] sh2, + char[] ch, char[] ch2, byte[] by, byte[] by2, + boolean[] bl, boolean[] bl2, + int[][][][] multi, int[][][][] multi2) throws Exception { + + int dim = db.length; + + double ds = Math.random() - 0.5; + double ds2; + float fs = (float) (Math.random() - 0.5); + float fs2; + int is = (int) (1000000 * (Math.random() - 500000)); + int is2; + long ls = (long) (100000000000L * (Math.random() - 50000000000L)); + long ls2; + short ss = (short) (60000 * (Math.random() - 30000)); + short ss2; + char cs = (char) (60000 * Math.random()); + char cs2; + byte bs = (byte) (256 * Math.random() - 128); + byte bs2; + boolean bls = (Math.random() > 0.5); + boolean bls2; + System.out.println("New libraries: nom.tam.util.BufferedDataXXputStream"); + System.out.println(" Using array I/O methods"); + + { + BufferedDataOutputStream f = new BufferedDataOutputStream(new FileOutputStream(filename)); + + resetTime(); + for (int i = 0; i < iter; i += 1) { + f.writeArray(db); + } + System.out.println(" BDS Dbl write: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(fl); + } + System.out.println(" BDS Flt write: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(in); + } + System.out.println(" BDS Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(ln); + } + System.out.println(" BDS Lng write: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(sh); + } + System.out.println(" BDS Sht write: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(ch); + } + System.out.println(" BDS Chr write: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray((byte[]) by); + } + System.out.println(" BDS Byt write: " + (1 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(bl); + } + System.out.println(" BDS Boo write: " + (1 * dim * iter) / (1000 * deltaTime())); + + f.writeByte(bs); + f.writeChar(cs); + f.writeShort(ss); + f.writeInt(is); + f.writeLong(ls); + f.writeFloat(fs); + f.writeDouble(ds); + f.writeBoolean(bls); + + f.writeArray(multi); + f.flush(); + f.close(); + } + + { + BufferedDataInputStream f = new BufferedDataInputStream(new FileInputStream(filename)); + + + resetTime(); + for (int i = 0; i < iter; i += 1) { + f.readLArray(db2); + } + System.out.println(" BDS Dbl read: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(fl2); + } + System.out.println(" BDS Flt read: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(in2); + } + System.out.println(" BDS Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(ln2); + } + System.out.println(" BDS Lng read: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(sh2); + } + System.out.println(" BDS Sht read: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(ch2); + } + System.out.println(" BDS Chr read: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray((byte[]) by2); + } + System.out.println(" BDS Byt read: " + (1 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(bl2); + } + System.out.println(" BDS Boo read: " + (1 * dim * iter) / (1000 * deltaTime())); + + bs2 = f.readByte(); + cs2 = f.readChar(); + ss2 = f.readShort(); + is2 = f.readInt(); + ls2 = f.readLong(); + fs2 = f.readFloat(); + ds2 = f.readDouble(); + bls2 = f.readBoolean(); + + for (int i = 0; i < 10; i += 1) { + multi2[i][i][i][i] = 0; + } + + // Now read only pieces of the multidimensional array. + for (int i = 0; i < 5; i += 1) { + System.out.println("Multiread:" + i); + // Skip the odd initial indices and + // read the evens. + f.skipBytes(4000); + f.readLArray(multi2[2 * i + 1]); + } + f.close(); + } + + System.out.println("Stream Verification:"); + System.out.println(" An error should be reported for double and float NaN's"); + System.out.println(" Arrays:"); + + for (int i = 0; i < dim; i += 1) { + + if (db[i] != db2[i]) { + System.out.println(" Double error at " + i + " " + db[i] + " " + db2[i]); + } + if (fl[i] != fl2[i]) { + System.out.println(" Float error at " + i + " " + fl[i] + " " + fl2[i]); + } + if (in[i] != in2[i]) { + System.out.println(" Int error at " + i + " " + in[i] + " " + in2[i]); + } + if (ln[i] != ln2[i]) { + System.out.println(" Long error at " + i + " " + ln[i] + " " + ln2[i]); + } + if (sh[i] != sh2[i]) { + System.out.println(" Short error at " + i + " " + sh[i] + " " + sh2[i]); + } + if (ch[i] != ch2[i]) { + System.out.println(" Char error at " + i + " " + (int) ch[i] + " " + (int) ch2[i]); + } + if (by[i] != by2[i]) { + System.out.println(" Byte error at " + i + " " + by[i] + " " + by2[i]); + } + if (bl[i] != bl2[i]) { + System.out.println(" Bool error at " + i + " " + bl[i] + " " + bl2[i]); + } + } + + System.out.println(" Scalars:"); + // Check the scalars. + if (bls != bls2) { + System.out.println(" Bool Scalar mismatch:" + bls + " " + bls2); + } + if (bs != bs2) { + System.out.println(" Byte Scalar mismatch:" + bs + " " + bs2); + } + if (cs != cs2) { + System.out.println(" Char Scalar mismatch:" + (int) cs + " " + (int) cs2); + } + if (ss != ss2) { + System.out.println(" Short Scalar mismatch:" + ss + " " + ss2); + } + if (is != is2) { + System.out.println(" Int Scalar mismatch:" + is + " " + is2); + } + if (ls != ls2) { + System.out.println(" Long Scalar mismatch:" + ls + " " + ls2); + } + if (fs != fs2) { + System.out.println(" Float Scalar mismatch:" + fs + " " + fs2); + } + if (ds != ds2) { + System.out.println(" Double Scalar mismatch:" + ds + " " + ds2); + } + + System.out.println(" Multi: odd rows should match"); + for (int i = 0; i < 10; i += 1) { + System.out.println(" " + i + " " + multi[i][i][i][i] + " " + multi2[i][i][i][i]); + } + System.out.println("Done BufferedStream Tests"); + } + + public static void bufferedFileTest(String filename, int iter, double[] db, double[] db2, + float[] fl, float[] fl2, long[] ln, long[] ln2, + int[] in, int[] in2, short[] sh, short[] sh2, + char[] ch, char[] ch2, byte[] by, byte[] by2, + boolean[] bl, boolean[] bl2, + int[][][][] multi, int[][][][] multi2) throws Exception { + + + int dim = db.length; + + double ds = Math.random() - 0.5; + double ds2; + float fs = (float) (Math.random() - 0.5); + float fs2; + int is = (int) (1000000 * (Math.random() - 500000)); + int is2; + long ls = (long) (100000000000L * (Math.random() - 50000000000L)); + long ls2; + short ss = (short) (60000 * (Math.random() - 30000)); + short ss2; + char cs = (char) (60000 * Math.random()); + char cs2; + byte bs = (byte) (256 * Math.random() - 128); + byte bs2; + boolean bls = (Math.random() > 0.5); + boolean bls2; + + System.out.println("New libraries: nom.tam.util.BufferedFile"); + System.out.println(" Using array I/O methods."); + + BufferedFile f = new BufferedFile(filename, "rw"); + + resetTime(); + for (int i = 0; i < iter; i += 1) { + f.writeArray(db); + } + System.out.println(" BF Dbl write: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(fl); + } + System.out.println(" BF Flt write: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(in); + } + System.out.println(" BF Int write: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(ln); + } + System.out.println(" BF Lng write: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(sh); + } + System.out.println(" BF Sht write: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(ch); + } + System.out.println(" BF Chr write: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(by); + } + System.out.println(" BF Byt write: " + (1 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.writeArray(bl); + } + System.out.println(" BF Boo write: " + (1 * dim * iter) / (1000 * deltaTime())); + + f.writeByte(bs); + f.writeChar(cs); + f.writeShort(ss); + f.writeInt(is); + f.writeLong(ls); + f.writeFloat(fs); + f.writeDouble(ds); + f.writeBoolean(bls); + + f.writeArray(multi); + f.seek(0); + + + resetTime(); + for (int i = 0; i < iter; i += 1) { + f.readLArray(db2); + } + System.out.println(" BF Dbl read: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(fl2); + } + System.out.println(" BF Flt read: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(in2); + } + System.out.println(" BF Int read: " + (4 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(ln2); + } + System.out.println(" BF Lng read: " + (8 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(sh2); + } + System.out.println(" BF Sht read: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(ch2); + } + System.out.println(" BF Chr read: " + (2 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(by2); + } + System.out.println(" BF Byt read: " + (1 * dim * iter) / (1000 * deltaTime())); + for (int i = 0; i < iter; i += 1) { + f.readLArray(bl2); + } + System.out.println(" BF Boo read: " + (1 * dim * iter) / (1000 * deltaTime())); + + bs2 = f.readByte(); + cs2 = f.readChar(); + ss2 = f.readShort(); + is2 = f.readInt(); + ls2 = f.readLong(); + fs2 = f.readFloat(); + ds2 = f.readDouble(); + bls2 = f.readBoolean(); + + // Now read only pieces of the multidimensional array. + for (int i = 0; i < 5; i += 1) { + // Skip the odd initial indices and + // read the evens. + f.skipBytes(4000); + f.readLArray(multi2[2 * i + 1]); + } + + System.out.println("BufferedFile Verification:"); + System.out.println(" An error should be reported for double and float NaN's"); + System.out.println(" Arrays:"); + + for (int i = 0; i < dim; i += 1) { + + if (db[i] != db2[i]) { + System.out.println(" Double error at " + i + " " + db[i] + " " + db2[i]); + } + if (fl[i] != fl2[i]) { + System.out.println(" Float error at " + i + " " + fl[i] + " " + fl2[i]); + } + if (in[i] != in2[i]) { + System.out.println(" Int error at " + i + " " + in[i] + " " + in2[i]); + } + if (ln[i] != ln2[i]) { + System.out.println(" Long error at " + i + " " + ln[i] + " " + ln2[i]); + } + if (sh[i] != sh2[i]) { + System.out.println(" Short error at " + i + " " + sh[i] + " " + sh2[i]); + } + if (ch[i] != ch2[i]) { + System.out.println(" Char error at " + i + " " + (int) ch[i] + " " + (int) ch2[i]); + } + if (by[i] != by2[i]) { + System.out.println(" Byte error at " + i + " " + by[i] + " " + by2[i]); + } + if (bl[i] != bl2[i]) { + System.out.println(" Bool error at " + i + " " + bl[i] + " " + bl2[i]); + } + } + + System.out.println(" Scalars:"); + // Check the scalars. + if (bls != bls2) { + System.out.println(" Bool Scalar mismatch:" + bls + " " + bls2); + } + if (bs != bs2) { + System.out.println(" Byte Scalar mismatch:" + bs + " " + bs2); + } + if (cs != cs2) { + System.out.println(" Char Scalar mismatch:" + (int) cs + " " + (int) cs2); + } + if (ss != ss2) { + System.out.println(" Short Scalar mismatch:" + ss + " " + ss2); + } + if (is != is2) { + System.out.println(" Int Scalar mismatch:" + is + " " + is2); + } + if (ls != ls2) { + System.out.println(" Long Scalar mismatch:" + ls + " " + ls2); + } + if (fs != fs2) { + System.out.println(" Float Scalar mismatch:" + fs + " " + fs2); + } + if (ds != ds2) { + System.out.println(" Double Scalar mismatch:" + ds + " " + ds2); + } + + System.out.println(" Multi: odd rows should match"); + for (int i = 0; i < 10; i += 1) { + System.out.println(" " + i + " " + multi[i][i][i][i] + " " + multi2[i][i][i][i]); + } + System.out.println("Done BufferedFile Tests"); + } + static long lastTime; + + static void resetTime() { + lastTime = new java.util.Date().getTime(); + } + + static double deltaTime() { + long time = lastTime; + lastTime = new java.util.Date().getTime(); + return (lastTime - time) / 1000.; + } + + @Test + public void testBufferedFile() throws Exception { + + double[][] td = new double[100][600]; + for (int i = 0; i < 100; i += 1) { + for (int j = 0; j < 600; j += 1) { + td[i][j] = i + 2 * j; + } + } + int[][][] ti = new int[5][4][3]; + for (int i = 0; i < 5; i += 1) { + for (int j = 0; j < 4; j += 1) { + for (int k = 0; k < 3; k += 1) { + ti[i][j][k] = i * j * k; + } + } + } + + float[][] tf = new float[10][]; + for (int i = 0; i < 10; i += 1) { + tf[i] = new float[i]; + for (int j = 0; j < i; j += 1) { + tf[i][j] = (float) Math.sin(i * j); + } + } + + boolean[] tb = new boolean[100]; + for (int i = 2; i < 100; i += 1) { + tb[i] = !tb[i - 1]; + } + + short[][] ts = new short[5][5]; + ts[2][2] = 222; + + byte[] tbyte = new byte[1024]; + for (int i = 0; i < tbyte.length; i += 1) { + tbyte[i] = (byte) i; + } + + char[] tc = new char[10]; + tc[3] = 'c'; + + long[][][] tl0 = new long[1][1][1]; + long[][][] tl1 = new long[1][1][0]; + + BufferedFile bf = new BufferedFile("jtest.fil", "rw"); + + bf.writeArray(td); + bf.writeArray(tf); + bf.writeArray(ti); + bf.writeArray(ts); + bf.writeArray(tb); + bf.writeArray(tbyte); + bf.writeArray(tc); + bf.writeArray(tl0); + bf.writeArray(tl1); + bf.writeArray(ts); + + bf.close(); + + bf = new BufferedFile("jtest.fil", "r"); + + boolean thrown = false; + + try { + bf.writeArray(td); + } catch (Exception e) { + thrown = true; + } + assertEquals("BufferedFile protections", true, thrown); + try { + bf.close(); + } catch (Exception e) { + } + + bf = new BufferedFile("jtest.fil", "r"); + + testArray(bf, "double", td); + testArray(bf, "float", tf); + testArray(bf, "int", ti); + testArray(bf, "short", ts); + testArray(bf, "bool", tb); + testArray(bf, "byte", tbyte); + testArray(bf, "char", tc); + testArray(bf, "long1", tl0); + testArray(bf, "longnull", tl1); + testArray(bf, "short2", ts); + } + + @Test + public void testBufferedStreams() throws Exception { + + double[][] td = new double[100][600]; + for (int i = 0; i < 100; i += 1) { + for (int j = 0; j < 600; j += 1) { + td[i][j] = i + 2 * j; + } + } + int[][][] ti = new int[5][4][3]; + for (int i = 0; i < 5; i += 1) { + for (int j = 0; j < 4; j += 1) { + for (int k = 0; k < 3; k += 1) { + ti[i][j][k] = i * j * k; + } + } + } + + float[][] tf = new float[10][]; + for (int i = 0; i < 10; i += 1) { + tf[i] = new float[i]; + for (int j = 0; j < i; j += 1) { + tf[i][j] = (float) Math.sin(i * j); + } + } + + boolean[] tb = new boolean[100]; + for (int i = 2; i < 100; i += 1) { + tb[i] = !tb[i - 1]; + } + + short[][] ts = new short[5][5]; + ts[2][2] = 222; + + byte[] tbyte = new byte[1024]; + for (int i = 0; i < tbyte.length; i += 1) { + tbyte[i] = (byte) i; + } + + char[] tc = new char[10]; + tc[3] = 'c'; + + long[][][] tl0 = new long[1][1][1]; + long[][][] tl1 = new long[1][1][0]; + + BufferedDataOutputStream bf = new BufferedDataOutputStream( + new FileOutputStream("jtest.fil")); + + bf.writeArray(td); + bf.writeArray(tf); + bf.writeArray(ti); + bf.writeArray(ts); + bf.writeArray(tb); + bf.writeArray(tbyte); + bf.writeArray(tc); + bf.writeArray(tl0); + bf.writeArray(tl1); + bf.writeArray(ts); + + bf.close(); + + BufferedDataInputStream bi = new BufferedDataInputStream( + new FileInputStream("jtest.fil")); + + testArray(bi, "sdouble", td); + testArray(bi, "sfloat", tf); + testArray(bi, "sint", ti); + testArray(bi, "sshort", ts); + testArray(bi, "sbool", tb); + testArray(bi, "sbyte", tbyte); + testArray(bi, "schar", tc); + testArray(bi, "slong1", tl0); + testArray(bi, "slongnull", tl1); + testArray(bi, "sshort2", ts); + } + + void testArray(ArrayDataInput bf, String label, Object array) throws Exception { + Object newArray = ArrayFuncs.mimicArray(array, ArrayFuncs.getBaseClass(array)); + bf.readLArray(newArray); + boolean state = ArrayFuncs.arrayEquals(array, newArray); + assertEquals(label, true, state); + } +} + + diff --git a/src/nom/tam/util/test/ByteFormatParseTest.java b/src/nom/tam/util/test/ByteFormatParseTest.java new file mode 100644 index 0000000..3c811ac --- /dev/null +++ b/src/nom/tam/util/test/ByteFormatParseTest.java @@ -0,0 +1,369 @@ +package nom.tam.util.test; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +/** This class tests the ByteFormatter and ByteParser classes. + */ +import nom.tam.util.*; +import java.util.Arrays; + +public class ByteFormatParseTest { + + byte[] buffer = new byte[100000]; + ByteFormatter bf = new ByteFormatter(); + ByteParser bp = new ByteParser(buffer); + int offset = 0; + int cnt = 0; + + @Test + public void testInt() throws Exception { + + for (int i = 0; i < 10; i += 1) { + buffer[i] = (byte) ' '; + } + bp.setOffset(0); + assertEquals("IntBlank", 0, bp.getInt(10)); + + bf.setAlign(true); + bf.setTruncationThrow(false); + + int[] tint = new int[100]; + + tint[0] = Integer.MIN_VALUE; + tint[1] = Integer.MAX_VALUE; + tint[2] = 0; + + for (int i = 0; i < tint.length; i += 1) { + tint[i] = (int) (Integer.MAX_VALUE * (2 * (Math.random() - .5))); + } + + + // Write 100 numbers + int colSize = 12; + while (cnt < tint.length) { + offset = bf.format(tint[cnt], buffer, offset, colSize); + cnt += 1; + if (cnt % 8 == 0) { + offset = bf.format("\n", buffer, offset, 1); + } + } + + // Now see if we can get them back + bp.setOffset(0); + for (int i = 0; i < tint.length; i += 1) { + + int chk = bp.getInt(colSize); + + assertEquals("IntegersRA", chk, tint[i]); + if ((i + 1) % 8 == 0) { + bp.skip(1); + } + } + + // Now do it with left-aligned numbers. + bf.setAlign(false); + bp.setFillFields(true); + offset = 0; + colSize = 12; + cnt = 0; + offset = 0; + while (cnt < tint.length) { + int oldOffset = offset; + offset = bf.format(tint[cnt], buffer, offset, colSize); + int nb = colSize - (offset - oldOffset); + if (nb > 0) { + offset = bf.alignFill(buffer, offset, nb); + } + cnt += 1; + if (cnt % 8 == 0) { + offset = bf.format("\n", buffer, offset, 1); + } + } + + // Now see if we can get them back + bp.setOffset(0); + for (int i = 0; i < tint.length; i += 1) { + + int chk = bp.getInt(colSize); + + assertEquals("IntegersLA", chk, tint[i]); + if ((i + 1) % 8 == 0) { + bp.skip(1); + } + } + + offset = 0; + colSize = 12; + cnt = 0; + offset = 0; + while (cnt < tint.length) { + offset = bf.format(tint[cnt], buffer, offset, colSize); + cnt += 1; + if (cnt % 8 == 0) { + offset = bf.format("\n", buffer, offset, 1); + } + } + + String myStr = new String(buffer, 0, offset); + assertEquals("No spaces", -1, myStr.indexOf(" ")); + + bf.setAlign(false); + + offset = 0; + colSize = 12; + cnt = 0; + offset = 0; + while (cnt < tint.length) { + offset = bf.format(tint[cnt], buffer, offset, colSize); + offset = bf.format(" ", buffer, offset, 1); + cnt += 1; + } + myStr = new String(buffer, 0, offset); + String[] array = myStr.split(" "); + + assertEquals("Split size", 100, array.length); + + for (int i = 0; i < array.length; i += 1) { + assertEquals("Parse token", tint[i], Integer.parseInt(array[i])); + } + + + bf.setTruncationThrow(false); + + int val = 1; + Arrays.fill(buffer, (byte) ' '); + + for (int i = 0; i < 10; i += 1) { + offset = bf.format(val, buffer, 0, 6); + String test = (val + " ").substring(0, 6); + if (i < 6) { + assertEquals("TestTrunc" + i, test, new String(buffer, 0, 6)); + } else { + assertEquals("TestTrunc" + i, "******", new String(buffer, 0, 6)); + } + val *= 10; + } + + bf.setTruncationThrow(true); + val = 1; + for (int i = 0; i < 10; i += 1) { + boolean thrown = false; + try { + offset = bf.format(val, buffer, 0, 6); + } catch (TruncationException e) { + thrown = true; + } + if (i < 6) { + assertEquals("TestTruncThrow" + i, false, thrown); + } else { + assertEquals("TestTruncThrow" + i, true, thrown); + } + val *= 10; + } + } + + @Test + public void testLong() throws Exception { + + for (int i = 0; i < 10; i += 1) { + buffer[i] = (byte) ' '; + } + bp.setOffset(0); + assertEquals("LongBlank", 0L, bp.getLong(10)); + + long[] lng = new long[100]; + for (int i = 0; i < lng.length; i += 1) { + lng[i] = (long) (Long.MAX_VALUE * (2 * (Math.random() - 0.5))); + } + + lng[0] = Long.MAX_VALUE; + lng[1] = Long.MIN_VALUE; + lng[2] = 0; + + bf.setTruncationThrow(false); + bp.setFillFields(true); + bf.setAlign(true); + offset = 0; + for (int i = 0; i < lng.length; i += 1) { + offset = bf.format(lng[i], buffer, offset, 20); + if ((i + 1) % 4 == 0) { + offset = bf.format("\n", buffer, offset, 1); + } + } + + bp.setOffset(0); + + for (int i = 0; i < lng.length; i += 1) { + assertEquals("Long check", lng[i], bp.getLong(20)); + if ((i + 1) % 4 == 0) { + bp.skip(1); + } + } + } + + @Test + public void testFloat() throws Exception { + + for (int i = 0; i < 10; i += 1) { + buffer[i] = (byte) ' '; + } + bp.setOffset(0); + assertEquals("FloatBlank", 0.f, bp.getFloat(10), 0.); + + float[] flt = new float[100]; + for (int i = 6; i < flt.length; i += 1) { + flt[i] = (float) (2 * (Math.random() - 0.5) * Math.pow(10, 60 * (Math.random() - 0.5))); + } + + flt[0] = Float.MAX_VALUE; + flt[1] = Float.MIN_VALUE; + flt[2] = 0; + flt[3] = Float.NaN; + flt[4] = Float.POSITIVE_INFINITY; + flt[5] = Float.NEGATIVE_INFINITY; + + + bf.setTruncationThrow(false); + bf.setAlign(true); + + offset = 0; + cnt = 0; + + while (cnt < flt.length) { + offset = bf.format(flt[cnt], buffer, offset, 24); + cnt += 1; + if (cnt % 4 == 0) { + offset = bf.format("\n", buffer, offset, 1); + } + } + + + bp.setOffset(0); + + for (int i = 0; i < flt.length; i += 1) { + + float chk = bp.getFloat(24); + + float dx = Math.abs(chk - flt[i]); + if (flt[i] != 0) { + dx = dx / Math.abs(flt[i]); + } + if (Float.isNaN(flt[i])) { + assertEquals("Float check:" + i, true, Float.isNaN(chk)); + } else if (Float.isInfinite(flt[i])) { + assertEquals("Float check:" + i, flt[i], chk, 0); + } else { + assertEquals("Float check:" + i, 0., dx, 1.e-6); + } + if ((i + 1) % 4 == 0) { + bp.skip(1); + } + } + } + + @Test + public void testDouble() throws Exception { + + for (int i = 0; i < 10; i += 1) { + buffer[i] = (byte) ' '; + } + bp.setOffset(0); + assertEquals("DoubBlank", 0., bp.getDouble(10), 0.); + + double[] dbl = new double[100]; + for (int i = 6; i < dbl.length; i += 1) { + dbl[i] = 2 * (Math.random() - 0.5) * Math.pow(10, 60 * (Math.random() - 0.5)); + } + + dbl[0] = Double.MAX_VALUE; + dbl[1] = Double.MIN_VALUE; + dbl[2] = 0; + dbl[3] = Double.NaN; + dbl[4] = Double.POSITIVE_INFINITY; + dbl[5] = Double.NEGATIVE_INFINITY; + + + bf.setTruncationThrow(false); + bf.setAlign(true); + offset = 0; + cnt = 0; + while (cnt < dbl.length) { + offset = bf.format(dbl[cnt], buffer, offset, 25); + cnt += 1; + if (cnt % 4 == 0) { + offset = bf.format("\n", buffer, offset, 1); + } + } + + + bp.setOffset(0); + for (int i = 0; i < dbl.length; i += 1) { + + double chk = bp.getDouble(25); + + double dx = Math.abs(chk - dbl[i]); + if (dbl[i] != 0) { + dx = dx / Math.abs(dbl[i]); + } + if (Double.isNaN(dbl[i])) { + assertEquals("Double check:" + i, true, Double.isNaN(chk)); + } else if (Double.isInfinite(dbl[i])) { + assertEquals("Double check:" + i, dbl[i], chk, 0); + } else { + assertEquals("Double check:" + i, 0., dx, 1.e-14); + } + + if ((i + 1) % 4 == 0) { + bp.skip(1); + } + } + } + + @Test + public void testBoolean() throws Exception { + + boolean[] btst = new boolean[100]; + for (int i = 0; i < btst.length; i += 1) { + btst[i] = Math.random() > 0.5; + } + offset = 0; + for (int i = 0; i < btst.length; i += 1) { + offset = bf.format(btst[i], buffer, offset, 1); + offset = bf.format(" ", buffer, offset, 1); + } + + bp.setOffset(0); + for (int i = 0; i < btst.length; i += 1) { + assertEquals("Boolean:" + i, btst[i], bp.getBoolean()); + } + } + + @Test + public void testString() throws Exception { + + offset = 0; + String bigStr = "abcdefghijklmnopqrstuvwxyz"; + + for (int i = 0; i < 100; i += 1) { + offset = bf.format(bigStr.substring(i % 27), buffer, offset, 13); + offset = bf.format(" ", buffer, offset, 1); + } + + bp.setOffset(0); + for (int i = 0; i < 100; i += 1) { + int ind = i % 27; + if (ind > 13) { + ind = 13; + } + String want = bigStr.substring(i % 27); + if (want.length() > 13) { + want = want.substring(0, 13); + } + String s = bp.getString(want.length()); + assertEquals("String:" + i, want, s); + bp.skip(1); + } + } +} diff --git a/src/nom/tam/util/test/HashedListTest.java b/src/nom/tam/util/test/HashedListTest.java new file mode 100644 index 0000000..fcc7c08 --- /dev/null +++ b/src/nom/tam/util/test/HashedListTest.java @@ -0,0 +1,233 @@ +package nom.tam.util.test; + +/* Copyright: Thomas McGlynn 1999. + * This code may be used for any purpose, non-commercial + * or commercial so long as this copyright notice is retained + * in the source code or included in or referred to in any + * derived software. + */ +import nom.tam.util.HashedList; +import nom.tam.util.Cursor; +import java.util.*; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; + +/** This class tests and illustrates the use + * of the HashedList class. Tests are in three + * parts. + *

+ * The first section (in testCollection) tests the methods + * that are present in the Collection interface. + * All of the optional methods of that interface + * are supported. This involves tests of the + * HashedClass interface directly. + *

+ * The second set of tests uses the Iterator (in testIterator) + * returned by the iterator() method and tests + * the standard Iterator methods to display + * and remove rows from the HashedList. + *

+ * The third set of tests (in testCursor) tests the extended + * capabilities of the HashedListIterator + * to add rows to the table, and to work + * as a cursor to move in a non-linear fashion + * through the list. + * + */ +public class HashedListTest { + + @Test + public void testCollection() { + + HashedList h1 = new HashedList(); + HashedList h2 = new HashedList(); + + Cursor i = h1.iterator(0); + Iterator j; + + // Add a few unkeyed rows. + + + h1.add("Row 1"); + h1.add("Row 2"); + h1.add("Row 3"); + + assertEquals("Adding unkeyed rows", 3, h1.size()); + + assertEquals("Has row 1", true, h1.contains("Row 1")); + assertEquals("Has row 2", true, h1.contains("Row 2")); + h1.remove("Row 2"); + assertEquals("Has row 1", true, h1.contains("Row 1")); + assertEquals("Has row 2", false, h1.contains("Row 2")); + + assertEquals("Delete unkeyed rows", 2, h1.size()); + + h1.clear(); + assertEquals("Cleared unkeyed rows", 0, h1.size()); + + h1.add("key 1", "Row 1"); + h1.add("key 2", "Row 2"); + h1.add("key 3", "Row 3"); + + assertEquals("Adding keyed rows", 3, h1.size()); + + assertEquals("Has Row 1", true, h1.contains("Row 1")); + assertEquals("Has key 1", true, h1.containsKey("key 1")); + assertEquals("Has Row 2", true, h1.contains("Row 2")); + assertEquals("Has key 2", true, h1.containsKey("key 2")); + assertEquals("Has Row 3", true, h1.contains("Row 3")); + assertEquals("Has key 3", true, h1.containsKey("key 3")); + + h1.removeKey("key 2"); + assertEquals("Delete keyed row", 2, h1.size()); + assertEquals("Has Row 1", true, h1.contains("Row 1")); + assertEquals("Has key 1", true, h1.containsKey("key 1")); + assertEquals("Has Row 2", false, h1.contains("Row 2")); + assertEquals("Has key 2", false, h1.containsKey("key 2")); + assertEquals("Has Row 3", true, h1.contains("Row 3")); + assertEquals("Has key 3", true, h1.containsKey("key 3")); + + h1.clear(); + assertEquals("Clear keyed rows", 0, h1.size()); + + h1.add("key 1", "Row 1"); + h1.add("key 2", "Row 2"); + h1.add("key 3", "Row 3"); + assertEquals("Re-Adding keyed rows", 3, h1.size()); + assertEquals("Has Row 2", true, h1.contains("Row 2")); + assertEquals("Has key 2", true, h1.containsKey("key 2")); + + h2.add("key 4", "Row 4"); + h2.add("key 5", "Row 5"); + + assertEquals("containsAll(beforeAdd)", false, h1.containsAll(h2)); + + h1.addAll(h2); + + assertEquals("addAll()", 5, h1.size()); + assertEquals("containsAll(afterAdd)", true, h1.containsAll(h2)); + assertEquals("has row 4", true, h1.contains("Row 4")); + h1.remove("Row 4"); + assertEquals("dropped row 4", false, h1.contains("Row 4")); + assertEquals("containsAll(afterDrop)", false, h1.containsAll(h2)); + + assertEquals("isEmpty(false)", false, h1.isEmpty()); + h1.remove("Row 1"); + h1.remove("Row 2"); + h1.remove("Row 3"); + h1.remove("Row 5"); + assertEquals("isEmpty(true)", true, h1.isEmpty()); + h1.add("Row 1"); + h1.add("Row 2"); + h1.add("Row 3"); + h1.addAll(h2); + assertEquals("Adding back", 5, h1.size()); + h1.removeAll(h2); + + assertEquals("removeAll()", 3, h1.size()); + h1.addAll(h2); + + assertEquals("Adding back again", 5, h1.size()); + h1.retainAll(h2); + assertEquals("retainAll()", 2, h1.size()); + + } + + @Test + public void testIterator() { + + HashedList h1 = new HashedList(); + + h1.add("key 4", "Row 4"); + h1.add("key 5", "Row 5"); + + + Iterator j = h1.iterator(); + assertEquals("next1", true, j.hasNext()); + assertEquals("TestIter1", "Row 4", (String) j.next()); + assertEquals("next2", true, j.hasNext()); + assertEquals("TestIter2", "Row 5", (String) j.next()); + assertEquals("next3", false, j.hasNext()); + + h1.clear(); + + h1.add("key 1", "Row 1"); + h1.add("key 2", "Row 2"); + h1.add("Row 3"); + h1.add("key 4", "Row 4"); + h1.add("Row 5"); + + assertEquals("Before remove", true, h1.contains("Row 2")); + j = h1.iterator(); + j.next(); + j.next(); + j.remove(); // Should get rid of second row + assertEquals("After remove", false, h1.contains("Row 2")); + assertEquals("n3", true, j.hasNext()); + assertEquals("n3v", "Row 3", (String) j.next()); + assertEquals("n4", true, j.hasNext()); + assertEquals("n4v", "Row 4", (String) j.next()); + assertEquals("n5", true, j.hasNext()); + assertEquals("n5v", "Row 5", (String) j.next()); + assertEquals("n6", false, j.hasNext()); + } + + @Test + public void TestCursor() { + + HashedList h1 = new HashedList(); + + h1.add("key 1", "Row 1"); + h1.add("Row 3"); + h1.add("key 4", "Row 4"); + h1.add("Row 5"); + + Cursor j = (Cursor) h1.iterator(0); + assertEquals("n1x", true, j.hasNext()); + assertEquals("n1xv", "Row 1", (String) j.next()); + assertEquals("n1xv", "Row 3", (String) j.next()); + + assertEquals("No Row 2", false, h1.containsKey("key 2")); + assertEquals("No Row 2", false, h1.contains("Row 2")); + j.setKey("key 1"); + assertEquals("setKey()", "Row 1", (String) j.next()); + j.add("key 2", "Row 2"); + assertEquals("has Row 2", true, h1.contains("Row 2")); + assertEquals("after add", "Row 3", (String) j.next()); + + j.setKey("key 4"); + assertEquals("setKey(1)", "Row 4", (String) j.next()); + assertEquals("setKey(2)", "Row 5", (String) j.next()); + assertEquals("setKey(3)", false, j.hasNext()); + + + j.setKey("key 2"); + assertEquals("setKey(4)", "Row 2", (String) j.next()); + assertEquals("setKey(5)", "Row 3", (String) j.next()); + j.add("Row 3.5"); + j.add("Row 3.6"); + assertEquals("After add", 7, h1.size()); + + j = h1.iterator("key 2"); + j.add("Row 1.5"); + j.add("key 1.7", "Row 1.7"); + j.add("Row 1.9"); + assertEquals("next() after adds", "Row 2", (String) j.next()); + j.setKey("key 1.7"); + assertEquals("next() after adds", "Row 1.7", (String) j.next()); + assertEquals("prev(1)", "Row 1.7", (String) j.prev()); + assertEquals("prev(2)", "Row 1.5", (String) j.prev()); + assertEquals("prev(3)", true, j.hasPrev()); + assertEquals("prev(4)", "Row 1", (String) j.prev()); + assertEquals("prev(5)", false, j.hasPrev()); + } + + void show(HashedList h, String msg) { + Iterator t = h.iterator(); + System.out.println("\n Looking at list:" + msg); + while (t.hasNext()) { + System.out.println("Has element:" + t.next()); + } + } +}