Begin versioning. v1.06.0
authorW. Trevor King <wking@drexel.edu>
Fri, 7 Oct 2011 04:02:15 +0000 (00:02 -0400)
committerW. Trevor King <wking@drexel.edu>
Fri, 7 Oct 2011 04:02:22 +0000 (00:02 -0400)
74 files changed:
build.xml [new file with mode: 0644]
src/nom/tam/fits/AsciiTable.java [new file with mode: 0644]
src/nom/tam/fits/AsciiTableHDU.java [new file with mode: 0644]
src/nom/tam/fits/BadHeaderException.java [new file with mode: 0644]
src/nom/tam/fits/BasicHDU.java [new file with mode: 0644]
src/nom/tam/fits/BinaryTable.java [new file with mode: 0644]
src/nom/tam/fits/BinaryTableHDU.java [new file with mode: 0644]
src/nom/tam/fits/Data.java [new file with mode: 0644]
src/nom/tam/fits/Fits.java [new file with mode: 0644]
src/nom/tam/fits/FitsDate.java [new file with mode: 0644]
src/nom/tam/fits/FitsElement.java [new file with mode: 0644]
src/nom/tam/fits/FitsException.java [new file with mode: 0644]
src/nom/tam/fits/FitsFactory.java [new file with mode: 0644]
src/nom/tam/fits/FitsHeap.java [new file with mode: 0644]
src/nom/tam/fits/FitsUtil.java [new file with mode: 0644]
src/nom/tam/fits/Header.java [new file with mode: 0644]
src/nom/tam/fits/HeaderCard.java [new file with mode: 0644]
src/nom/tam/fits/HeaderCardException.java [new file with mode: 0644]
src/nom/tam/fits/HeaderCommentsMap.java [new file with mode: 0644]
src/nom/tam/fits/HeaderOrder.java [new file with mode: 0644]
src/nom/tam/fits/ImageData.java [new file with mode: 0644]
src/nom/tam/fits/ImageHDU.java [new file with mode: 0644]
src/nom/tam/fits/Laurent_changes.txt [new file with mode: 0644]
src/nom/tam/fits/PaddingException.java [new file with mode: 0644]
src/nom/tam/fits/RandomGroupsData.java [new file with mode: 0644]
src/nom/tam/fits/RandomGroupsHDU.java [new file with mode: 0644]
src/nom/tam/fits/TableData.java [new file with mode: 0644]
src/nom/tam/fits/TableHDU.java [new file with mode: 0644]
src/nom/tam/fits/TruncatedFileException.java [new file with mode: 0644]
src/nom/tam/fits/UndefinedData.java [new file with mode: 0644]
src/nom/tam/fits/UndefinedHDU.java [new file with mode: 0644]
src/nom/tam/fits/comments.txt [new file with mode: 0644]
src/nom/tam/fits/test/AsciiTableTest.java [new file with mode: 0644]
src/nom/tam/fits/test/BinaryTableTest.java [new file with mode: 0644]
src/nom/tam/fits/test/CompressTest.java [new file with mode: 0644]
src/nom/tam/fits/test/DateTester.java [new file with mode: 0644]
src/nom/tam/fits/test/HeaderCardTest.java [new file with mode: 0644]
src/nom/tam/fits/test/HeaderTest.java [new file with mode: 0644]
src/nom/tam/fits/test/ImageTest.java [new file with mode: 0644]
src/nom/tam/fits/test/PaddingTest.java [new file with mode: 0644]
src/nom/tam/fits/test/RandomGroupsTest.java [new file with mode: 0644]
src/nom/tam/fits/test/TilerTest.java [new file with mode: 0644]
src/nom/tam/fits/test/test.fits [new file with mode: 0644]
src/nom/tam/fits/test/test.fits.Z [new file with mode: 0644]
src/nom/tam/fits/test/test.fits.bz2 [new file with mode: 0644]
src/nom/tam/fits/test/test.fits.gz [new file with mode: 0644]
src/nom/tam/fits/utilities/FitsCopy.java [new file with mode: 0644]
src/nom/tam/fits/utilities/FitsReader.java [new file with mode: 0644]
src/nom/tam/image/ImageTiler.java [new file with mode: 0644]
src/nom/tam/util/ArrayDataInput.java [new file with mode: 0644]
src/nom/tam/util/ArrayDataOutput.java [new file with mode: 0644]
src/nom/tam/util/ArrayFuncs.java [new file with mode: 0644]
src/nom/tam/util/AsciiFuncs.java [new file with mode: 0644]
src/nom/tam/util/BufferedDataInputStream.java [new file with mode: 0644]
src/nom/tam/util/BufferedDataOutputStream.java [new file with mode: 0644]
src/nom/tam/util/BufferedFile.java [new file with mode: 0644]
src/nom/tam/util/ByteFormatter.java [new file with mode: 0644]
src/nom/tam/util/ByteParser.java [new file with mode: 0644]
src/nom/tam/util/ColumnTable.java [new file with mode: 0644]
src/nom/tam/util/Cursor.java [new file with mode: 0644]
src/nom/tam/util/DataIO.java [new file with mode: 0644]
src/nom/tam/util/DataTable.java [new file with mode: 0644]
src/nom/tam/util/FormatException.java [new file with mode: 0644]
src/nom/tam/util/HashedList.java [new file with mode: 0644]
src/nom/tam/util/PrimitiveInfo.java [new file with mode: 0644]
src/nom/tam/util/RandomAccess.java [new file with mode: 0644]
src/nom/tam/util/TableException.java [new file with mode: 0644]
src/nom/tam/util/TruncationException.java [new file with mode: 0644]
src/nom/tam/util/test/ArrayFuncs2Test.java [new file with mode: 0644]
src/nom/tam/util/test/ArrayFuncsTest.java [new file with mode: 0644]
src/nom/tam/util/test/BigFileTest.java [new file with mode: 0644]
src/nom/tam/util/test/BufferedFileTester.java [new file with mode: 0644]
src/nom/tam/util/test/ByteFormatParseTest.java [new file with mode: 0644]
src/nom/tam/util/test/HashedListTest.java [new file with mode: 0644]

diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..869a4e4
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<project name="fits" default="all" basedir=".">
+  <target name="init" description="Sets build properties">
+    <property name="src" value="${basedir}/src"/>
+    <property name="build" value="${basedir}/build"/>
+    <property name="doc" value="${basedir}/doc"/>
+    <path id="project.class.path">
+    </path>
+    <path id="build.class.path">
+      <!--pathelement location="JUNIT.JAR"/-->
+      <pathelement location="/usr/share/junit-4/lib/junit.jar"/>
+    </path>
+    <path id="test.class.path">
+      <pathelement location="${build}"/>
+    </path>
+  </target>
+  <target name="all" depends="jar,javadoc"
+          description="Pseudo-target that builds JAR and Javadoc">
+  </target>
+  <target name="build" depends="init"
+          description="Compiles the classes">
+    <mkdir dir="${build}"/>
+    <javac destdir="${build}" srcdir="${src}" debug="true"
+           deprecation="true" includeantruntime="false">
+      <classpath refid="project.class.path"/>
+      <classpath refid="build.class.path"/>
+    </javac>
+  </target>
+  <target name="test" depends="build">
+    <junit>
+      <classpath refid="project.class.path" />
+      <classpath refid="test.class.path"/>
+      <formatter type="brief" usefile="false" />
+      <batchtest>
+        <fileset dir="${build}" includes="**/test/*.class" />
+      </batchtest>
+    </junit>
+  </target>
+  <target name="javadoc" depends="init"
+          description="Generates Javadoc API documentation">
+    <mkdir dir="${doc}/api"/>
+    <javadoc packagenames="*"
+             sourcepath="${src}" destdir="${doc}/api"
+             author="true"       version="true"
+             use="true"          private="true"/>
+  </target>
+  <target name="jar" depends="build"
+          description="Builds a project JAR file">
+    <jar basedir="${build}" jarfile="${basedir}/fits.jar">
+      <manifest>
+        <attribute name="Version" value="1.06.0"/>
+        <attribute name="Main-Class"
+                   value="fits"/>
+      </manifest>
+    </jar>
+  </target>
+  <target name="clean" depends="init"
+          description="Erase all generated files and dirs">
+    <delete dir="${build}" verbose="true"/>
+    <delete dir="${doc}/api" verbose="true"/>
+    <delete file="fits.jar" verbose="true"/>
+  </target>
+</project>
diff --git a/src/nom/tam/fits/AsciiTable.java b/src/nom/tam/fits/AsciiTable.java
new file mode 100644 (file)
index 0000000..3d47c0e
--- /dev/null
@@ -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 (file)
index 0000000..344c84a
--- /dev/null
@@ -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 <CODE>true</CODE> 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 <CODE>true</CODE> 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 (file)
index 0000000..168e85c
--- /dev/null
@@ -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 (file)
index 0000000..419b2cb
--- /dev/null
@@ -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 <CODE>true</CODE> 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 <CODE>keyword</CODE>.
+     * @param hdr      the header piece of an HDU
+     * @param keyword  the FITS keyword
+     * @return either <CODE>null</CODE> 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 <CODE>Date</CODE> object.
+     * @return either <CODE>null</CODE> 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 <CODE>Date</CODE> object.
+     * @return either <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> or a String object
+     */
+    public String getObserver() {
+        return getTrimmedString("OBSERVER");
+    }
+
+    /**
+     * Get the name of the observed object in this FITS file.
+     * @return either <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 (file)
index 0000000..103b4bf
--- /dev/null
@@ -0,0 +1,1777 @@
+package nom.tam.fits;\r
+\r
+/* Copyright: Thomas McGlynn 1997-2000.\r
+ * This code may be used for any purpose, non-commercial\r
+ * or commercial so long as this copyright notice is retained\r
+ * in the source code or included in or referred to in any\r
+ * derived software.\r
+ *\r
+ * Many thanks to David Glowacki (U. Wisconsin) for substantial\r
+ * improvements, enhancements and bug fixes.\r
+ */\r
+import java.io.*;\r
+import nom.tam.util.*;\r
+import java.lang.reflect.Array;\r
+import java.util.Vector;\r
+\r
+/** This class defines the methods for accessing FITS binary table data.\r
+ */\r
+public class BinaryTable extends Data implements TableData {\r
+\r
+    /** This is the area in which variable length column data lives.\r
+     */\r
+    FitsHeap heap;\r
+    /** The number of bytes between the end of the data and the heap */\r
+    int heapOffset;\r
+    // Added by A. Kovacs (4/1/08)\r
+    // as a way for checking whether the heap was initialized from stream...\r
+    /** Has the heap been read */\r
+    boolean heapReadFromStream = false;\r
+    /** The sizes of each column (in number of entries per row)\r
+     */\r
+    int[] sizes;\r
+    /** The dimensions of each column.\r
+     *  If a column is a scalar then entry for that\r
+     *  index is an array of length 0.\r
+     */\r
+    int[][] dimens;\r
+    /** Info about column */\r
+    int[] flags;\r
+    /** Flag indicating that we've given Variable length conversion warning.\r
+     * We only want to do that once per HDU.\r
+     */\r
+    private boolean warnedOnVariableConversion = false;\r
+    final static int COL_CONSTANT = 0;\r
+    final static int COL_VARYING = 1;\r
+    final static int COL_COMPLEX = 2;\r
+    final static int COL_STRING = 4;\r
+    final static int COL_BOOLEAN = 8;\r
+    final static int COL_BIT = 16;\r
+    final static int COL_LONGVARY = 32;\r
+    /** The number of rows in the table.\r
+     */\r
+    int nRow;\r
+    /** The number of columns in the table.\r
+     */\r
+    int nCol;\r
+    /** The length in bytes of each row.\r
+     */\r
+    int rowLen;\r
+    /** The base classes for the arrays in the table.\r
+     */\r
+    Class[] bases;\r
+    /** An example of the structure of a row\r
+     */\r
+    Object[] modelRow;\r
+    /** A pointer to the data in the columns.  This\r
+     *  variable is only used to assist in the\r
+     *  construction of BinaryTable's that are defined\r
+     *  to point to an array of columns.  It is\r
+     *  not generally filled.  The ColumnTable is used\r
+     *  to store the actual data of the BinaryTable.\r
+     */\r
+    Object[] columns;\r
+    /** Where the data is actually stored.\r
+     */\r
+    ColumnTable table;\r
+    /** The stream used to input the image\r
+     */\r
+    ArrayDataInput currInput;\r
+\r
+    /** Create a null binary table data segment.\r
+     */\r
+    public BinaryTable() throws FitsException {\r
+\r
+        try {\r
+            table = new ColumnTable(new Object[0], new int[0]);\r
+        } catch (TableException e) {\r
+            System.err.println("Impossible exception in BinaryTable() constructor" + e);\r
+        }\r
+\r
+        heap = new FitsHeap(0);\r
+        extendArrays(0);\r
+        nRow = 0;\r
+        nCol = 0;\r
+        rowLen = 0;\r
+    }\r
+\r
+    /** Create a binary table from given header information.\r
+     *\r
+     * @param header    A header describing what the binary\r
+     *                 table should look like.\r
+     */\r
+    public BinaryTable(Header myHeader) throws FitsException {\r
+\r
+        long heapSizeL = myHeader.getLongValue("PCOUNT");\r
+        long heapOffsetL = myHeader.getLongValue("THEAP");\r
+        if (heapOffsetL > Integer.MAX_VALUE) {\r
+            throw new FitsException("Heap Offset > 2GB");\r
+        }\r
+        heapOffset = (int) heapOffsetL;\r
+        if (heapSizeL > Integer.MAX_VALUE) {\r
+            throw new FitsException("Heap size > 2 GB");\r
+        }\r
+        int heapSize = (int) heapSizeL;\r
+\r
+        int rwsz = myHeader.getIntValue("NAXIS1");\r
+        nRow = myHeader.getIntValue("NAXIS2");\r
+\r
+        // Subtract out the size of the regular table from\r
+        // the heap offset.\r
+\r
+        if (heapOffset > 0) {\r
+            heapOffset -= nRow * rwsz;\r
+        }\r
+\r
+        if (heapOffset < 0 || heapOffset > heapSize) {\r
+            throw new FitsException("Inconsistent THEAP and PCOUNT");\r
+        }\r
+\r
+        if (heapSize - heapOffset > Integer.MAX_VALUE) {\r
+            throw new FitsException("Unable to allocate heap > 2GB");\r
+        }\r
+\r
+        heap = new FitsHeap((heapSize - heapOffset));\r
+        nCol = myHeader.getIntValue("TFIELDS");\r
+        rowLen = 0;\r
+\r
+        extendArrays(nCol);\r
+        for (int col = 0; col < nCol; col += 1) {\r
+            rowLen += processCol(myHeader, col);\r
+        }\r
+\r
+        HeaderCard card = myHeader.findCard("NAXIS1");\r
+        card.setValue(String.valueOf(rowLen));\r
+        myHeader.updateLine("NAXIS1", card);\r
+\r
+    }\r
+\r
+    /** Create a binary table from existing data in row order.\r
+     *\r
+     * @param data The data used to initialize the binary table.\r
+     */\r
+    public BinaryTable(Object[][] data) throws FitsException {\r
+        this(convertToColumns(data));\r
+    }\r
+\r
+    /** Create a binary table from existing data in column order.\r
+     */\r
+    public BinaryTable(Object[] o) throws FitsException {\r
+\r
+        heap = new FitsHeap(0);\r
+        modelRow = new Object[o.length];\r
+        extendArrays(o.length);\r
+\r
+\r
+        for (int i = 0; i < o.length; i += 1) {\r
+            addColumn(o[i]);\r
+        }\r
+    }\r
+\r
+    /** Create a binary table from an existing ColumnTable */\r
+    public BinaryTable(ColumnTable tab) {\r
+\r
+        nCol = tab.getNCols();\r
+\r
+        extendArrays(nCol);\r
+\r
+        bases = tab.getBases();\r
+        sizes = tab.getSizes();\r
+\r
+        modelRow = new Object[nCol];\r
+\r
+        dimens = new int[nCol][];\r
+\r
+        // Set all flags to 0.\r
+        flags = new int[nCol];\r
+\r
+        // Set the column dimension.  Note that\r
+        // we cannot distinguish an array of length 1 from a\r
+        // scalar here: we assume a scalar.\r
+        for (int col = 0; col < nCol; col += 1) {\r
+            if (sizes[col] != 1) {\r
+                dimens[col] = new int[]{sizes[col]};\r
+            } else {\r
+                dimens[col] = new int[0];\r
+            }\r
+        }\r
+\r
+        for (int col = 0; col < nCol; col += 1) {\r
+            modelRow[col] = ArrayFuncs.newInstance(bases[col], sizes[col]);\r
+        }\r
+\r
+        columns = null;\r
+        table = tab;\r
+\r
+        heap = new FitsHeap(0);\r
+        rowLen = 0;\r
+        for (int col = 0; col < nCol; col += 1) {\r
+            rowLen += sizes[col] * ArrayFuncs.getBaseLength(tab.getColumn(col));\r
+        }\r
+        heapOffset = 0;\r
+        nRow = tab.getNRows();\r
+    }\r
+\r
+    /** Return a row that may be used for direct i/o to the table.\r
+     */\r
+    public Object[] getModelRow() {\r
+        return modelRow;\r
+    }\r
+\r
+    /** Process one column from a FITS Header */\r
+    private int processCol(Header header, int col) throws FitsException {\r
+\r
+        String tform = header.getStringValue("TFORM" + (col + 1));\r
+        if (tform == null) {\r
+            throw new FitsException("Attempt to process column " + (col + 1) + " but no TFORMn found.");\r
+        }\r
+        tform = tform.trim();\r
+\r
+        String tdims = header.getStringValue("TDIM" + (col + 1));\r
+\r
+        if (tdims != null) {\r
+            tdims = tdims.trim();\r
+        }\r
+\r
+        char type = getTFORMType(tform);\r
+        if (type == 'P' || type == 'Q') {\r
+            flags[col] |= COL_VARYING;\r
+            if (type == 'Q') {\r
+                flags[col] |= COL_LONGVARY;\r
+            }\r
+            type = getTFORMVarType(tform);\r
+        }\r
+\r
+\r
+        int size = getTFORMLength(tform);\r
+\r
+        // Handle the special size cases.\r
+        //\r
+        // Bit arrays (8 bits fit in a byte)\r
+        if (type == 'X') {\r
+            size = (size + 7) / 8;\r
+            flags[col] |= COL_BIT;\r
+\r
+            // Variable length arrays always have a two-element pointer (offset and size)\r
+        } else if (isVarCol(col)) {\r
+            size = 2;\r
+        }\r
+\r
+        // bSize is the number of bytes in the field.\r
+        int bSize = size;\r
+\r
+        int[] dims = null;\r
+\r
+        // Cannot really handle arbitrary arrays of bits.\r
+        if (tdims != null && type != 'X' && !isVarCol(col)) {\r
+            dims = getTDims(tdims);\r
+        }\r
+\r
+        if (dims == null) {\r
+            if (size == 1) {\r
+                dims = new int[0];  // Marks this as a scalar column\r
+            } else {\r
+                dims = new int[]{size};\r
+            }\r
+        }\r
+\r
+        if (type == 'C' || type == 'M') {\r
+            flags[col] |= COL_COMPLEX;\r
+        }\r
+\r
+        Class colBase = null;\r
+\r
+        switch (type) {\r
+            case 'A':\r
+                colBase = byte.class;\r
+                flags[col] |= COL_STRING;\r
+                bases[col] = String.class;\r
+                break;\r
+\r
+            case 'L':\r
+                colBase = byte.class;\r
+                bases[col] = boolean.class;\r
+                flags[col] |= COL_BOOLEAN;\r
+                break;\r
+            case 'X':\r
+            case 'B':\r
+                colBase = byte.class;\r
+                bases[col] = byte.class;\r
+                break;\r
+\r
+            case 'I':\r
+                colBase = short.class;\r
+                bases[col] = short.class;\r
+                bSize *= 2;\r
+                break;\r
+\r
+            case 'J':\r
+                colBase = int.class;\r
+                bases[col] = int.class;\r
+                bSize *= 4;\r
+                break;\r
+\r
+            case 'K':\r
+                colBase = long.class;\r
+                bases[col] = long.class;\r
+                bSize *= 8;\r
+                break;\r
+\r
+            case 'E':\r
+            case 'C':\r
+                colBase = float.class;\r
+                bases[col] = float.class;\r
+                bSize *= 4;\r
+                break;\r
+\r
+            case 'D':\r
+            case 'M':\r
+                colBase = double.class;\r
+                bases[col] = double.class;\r
+                bSize *= 8;\r
+                break;\r
+\r
+            default:\r
+                throw new FitsException("Invalid type in column:" + col);\r
+        }\r
+\r
+        if (isVarCol(col)) {\r
+\r
+            dims = new int[]{nRow, 2};\r
+            colBase = int.class;\r
+            bSize = 8;\r
+\r
+            if (isLongVary(col)) {\r
+                colBase = long.class;\r
+                bSize = 16;\r
+            }\r
+        }\r
+\r
+        if (!isVarCol(col) && isComplex(col)) {\r
+\r
+            int[] xdims = new int[dims.length + 1];\r
+            System.arraycopy(dims, 0, xdims, 0, dims.length);\r
+            xdims[dims.length] = 2;\r
+            dims = xdims;\r
+            bSize *= 2;\r
+            size *= 2;\r
+        }\r
+\r
+        modelRow[col] = ArrayFuncs.newInstance(colBase, dims);\r
+        dimens[col] = dims;\r
+        sizes[col] = size;\r
+\r
+        return bSize;\r
+    }\r
+\r
+    /** Get the type in the TFORM field */\r
+    char getTFORMType(String tform) {\r
+\r
+        for (int i = 0; i < tform.length(); i += 1) {\r
+            if (!Character.isDigit(tform.charAt(i))) {\r
+                return tform.charAt(i);\r
+            }\r
+        }\r
+        return 0;\r
+    }\r
+\r
+    /** Get the type in a varying length column TFORM */\r
+    char getTFORMVarType(String tform) {\r
+\r
+        int ind = tform.indexOf("P");\r
+        if (ind < 0) {\r
+            ind = tform.indexOf("Q");\r
+        }\r
+\r
+        if (tform.length() > ind + 1) {\r
+            return tform.charAt(ind + 1);\r
+        } else {\r
+            return 0;\r
+        }\r
+    }\r
+\r
+    /** Get the explicit or implied length of the TFORM field */\r
+    int getTFORMLength(String tform) {\r
+\r
+        tform = tform.trim();\r
+\r
+        if (Character.isDigit(tform.charAt(0))) {\r
+            return initialNumber(tform);\r
+\r
+        } else {\r
+            return 1;\r
+        }\r
+    }\r
+\r
+    /** Get an unsigned number at the beginning of a string */\r
+    private int initialNumber(String tform) {\r
+\r
+        int i;\r
+        for (i = 0; i < tform.length(); i += 1) {\r
+\r
+            if (!Character.isDigit(tform.charAt(i))) {\r
+                break;\r
+            }\r
+\r
+        }\r
+\r
+        return Integer.parseInt(tform.substring(0, i));\r
+    }\r
+\r
+    /** Parse the TDIMS value.\r
+     *\r
+     * If the TDIMS value cannot be deciphered a one-d\r
+     * array with the size given in arrsiz is returned.\r
+     *\r
+     * @param tdims   The value of the TDIMSn card.\r
+     * @param arraySize  The size field found on the TFORMn card.\r
+     * @return        An int array of the desired dimensions.\r
+     *                Note that the order of the tdims is the inverse\r
+     *                of the order in the TDIMS key.\r
+     */\r
+    public static int[] getTDims(String tdims) {\r
+\r
+        // The TDIMs value should be of the form: "(iiii,jjjj,kkk,...)"\r
+\r
+        int[] dims = null;\r
+\r
+        int first = tdims.indexOf('(');\r
+        int last = tdims.lastIndexOf(')');\r
+        if (first >= 0 && last > first) {\r
+\r
+            tdims = tdims.substring(first + 1, last - first);\r
+\r
+            java.util.StringTokenizer st = new java.util.StringTokenizer(tdims, ",");\r
+            int dim = st.countTokens();\r
+            if (dim > 0) {\r
+\r
+                dims = new int[dim];\r
+\r
+                for (int i = dim - 1; i >= 0; i -= 1) {\r
+                    dims[i] = Integer.parseInt(st.nextToken().trim());\r
+                }\r
+            }\r
+        }\r
+        return dims;\r
+    }\r
+\r
+    /** Convert a column from float/double to float complex/double complex.\r
+     *  This is only possible for certain columns.  The return status\r
+     *  indicates if the conversion is possible.\r
+     *  @param index  The 0-based index of the column to be reset.\r
+     *  @return Whether the conversion is possible.\r
+     */\r
+    boolean setComplexColumn(int index) throws FitsException {\r
+\r
+        // Currently there is almost no change required to the BinaryTable\r
+        // object itself when we convert an eligible column to complex, since the internal\r
+        // representation of the data is unchanged.  We just need\r
+        // to set the flag that the column is complex.\r
+\r
+        // Check that the index is valid,\r
+        //             the data type is float or double\r
+        //             the most rapidly changing index in the array has dimension 2.\r
+        if (index >= 0 && index < bases.length\r
+                && (bases[index] == float.class || bases[index] == double.class)\r
+                && dimens[index][dimens[index].length - 1] == 2) {\r
+            // By coincidence a variable length column will also have\r
+            // a last index of 2, so we'll get here.  Otherwise\r
+            // we'd need to test that in parallel rather than in series.\r
+\r
+            // If this is a variable length column, then\r
+            // we need to check the length of each row.\r
+            if ((flags[index] & COL_VARYING) != 0) {\r
+\r
+                // We need to make sure that for every row, there are\r
+                // an even number of elements so that we can\r
+                // convert to an integral number of complex numbers.\r
+                Object col = getFlattenedColumn(index);\r
+                if (col instanceof int[]) {\r
+                    int[] ptrs = (int[]) col;\r
+                    for (int i = 1; i < ptrs.length; i += 2) {\r
+                        if (ptrs[i] % 2 != 0) {\r
+                            return false;\r
+                        }\r
+                    }\r
+                } else {\r
+                    long[] ptrs = (long[]) col;\r
+                    for (int i = 1; i < ptrs.length; i += 1) {\r
+                        if (ptrs[i] % 2 != 0) {\r
+                            return false;\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+            // Set the column to complex\r
+            flags[index] |= COL_COMPLEX;\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /** Update a FITS header to reflect the current state of the data.\r
+     */\r
+    public void fillHeader(Header h) throws FitsException {\r
+\r
+        try {\r
+            h.setXtension("BINTABLE");\r
+            h.setBitpix(8);\r
+            h.setNaxes(2);\r
+            h.setNaxis(1, rowLen);\r
+            h.setNaxis(2, nRow);\r
+            h.addValue("PCOUNT", heap.size(), "ntf::binarytable:pcount:1");\r
+            h.addValue("GCOUNT", 1, "ntf::binarytable:gcount:1");\r
+            Cursor iter = h.iterator();\r
+            iter.setKey("GCOUNT");\r
+            iter.next();\r
+            iter.add("TFIELDS", new HeaderCard("TFIELDS", modelRow.length, "ntf::binarytable:tfields:1"));\r
+\r
+            for (int i = 0; i < modelRow.length; i += 1) {\r
+                if (i > 0) {\r
+                    h.positionAfterIndex("TFORM", i);\r
+                }\r
+                fillForColumn(h, i, iter);\r
+            }\r
+        } catch (HeaderCardException e) {\r
+            System.err.println("Error updating BinaryTableHeader:" + e);\r
+        }\r
+    }\r
+\r
+    /** Updata the header to reflect information about a given column.\r
+     *  This routine tries to ensure that the Header is organized by column.\r
+     */\r
+    void pointToColumn(int col, Header hdr) throws FitsException {\r
+\r
+        Cursor iter = hdr.iterator();\r
+        if (col > 0) {\r
+            hdr.positionAfterIndex("TFORM", col);\r
+        }\r
+        fillForColumn(hdr, col, iter);\r
+    }\r
+\r
+    /** Update the header to reflect the details of a given column */\r
+    void fillForColumn(Header h, int col, Cursor iter) throws FitsException {\r
+\r
+        String tform;\r
+\r
+        if (isVarCol(col)) {\r
+            if (isLongVary(col)) {\r
+                tform = "1Q";\r
+            } else {\r
+                tform = "1P";\r
+            }\r
+\r
+        } else {\r
+            tform = "" + sizes[col];\r
+        }\r
+\r
+        if (bases[col] == int.class) {\r
+            tform += "J";\r
+        } else if (bases[col] == short.class || bases[col] == char.class) {\r
+            tform += "I";\r
+        } else if (bases[col] == byte.class) {\r
+            tform += "B";\r
+        } else if (bases[col] == float.class) {\r
+            if (isComplex(col)) {\r
+                tform += "C";\r
+            } else {\r
+                tform += "E";\r
+            }\r
+        } else if (bases[col] == double.class) {\r
+            if (isComplex(col)) {\r
+                tform += "M";\r
+            } else {\r
+                tform += "D";\r
+            }\r
+        } else if (bases[col] == long.class) {\r
+            tform += "K";\r
+        } else if (bases[col] == boolean.class) {\r
+            tform += "L";\r
+        } else if (bases[col] == String.class) {\r
+            tform += "A";\r
+        } else {\r
+            throw new FitsException("Invalid column data class:" + bases[col]);\r
+        }\r
+\r
+\r
+        String key = "TFORM" + (col + 1);\r
+        iter.add(key, new HeaderCard(key, tform, "ntf::binarytable:tformN:1"));\r
+\r
+        if (dimens[col].length > 0 && !isVarCol(col)) {\r
+\r
+            StringBuffer tdim = new StringBuffer();\r
+            char comma = '(';\r
+            for (int i = dimens[col].length - 1; i >= 0; i -= 1) {\r
+                tdim.append(comma);\r
+                tdim.append(dimens[col][i]);\r
+                comma = ',';\r
+            }\r
+            tdim.append(')');\r
+            key = "TDIM" + (col + 1);\r
+            iter.add(key, new HeaderCard(key, new String(tdim), "ntf::headercard:tdimN:1"));\r
+        }\r
+    }\r
+\r
+    /** Create a column table given the number of\r
+     *  rows and a model row.  This is used when\r
+     *  we defer instantiation of the ColumnTable until\r
+     *  the user requests data from the table.\r
+     */\r
+    private ColumnTable createTable() throws FitsException {\r
+\r
+        int nfields = modelRow.length;\r
+\r
+        Object[] arrCol = new Object[nfields];\r
+\r
+        for (int i = 0; i < nfields; i += 1) {\r
+            arrCol[i] = ArrayFuncs.newInstance(\r
+                    ArrayFuncs.getBaseClass(modelRow[i]),\r
+                    sizes[i] * nRow);\r
+        }\r
+\r
+        ColumnTable table;\r
+\r
+        try {\r
+            table = new ColumnTable(arrCol, sizes);\r
+        } catch (TableException e) {\r
+            throw new FitsException("Unable to create table:" + e);\r
+        }\r
+\r
+        return table;\r
+    }\r
+\r
+    /** Convert a two-d table to a table of columns.  Handle\r
+     *  String specially.  Every other element of data should be\r
+     *  a primitive array of some dimensionality.\r
+     */\r
+    private static Object[] convertToColumns(Object[][] data) {\r
+\r
+        Object[] row = data[0];\r
+        int nrow = data.length;\r
+\r
+        Object[] results = new Object[row.length];\r
+\r
+        for (int col = 0; col < row.length; col += 1) {\r
+\r
+            if (row[col] instanceof String) {\r
+\r
+                String[] sa = new String[nrow];\r
+\r
+                for (int irow = 0; irow < nrow; irow += 1) {\r
+                    sa[irow] = (String) data[irow][col];\r
+                }\r
+\r
+                results[col] = sa;\r
+\r
+            } else {\r
+\r
+                Class base = ArrayFuncs.getBaseClass(row[col]);\r
+                int[] dims = ArrayFuncs.getDimensions(row[col]);\r
+\r
+                if (dims.length > 1 || dims[0] > 1) {\r
+                    int[] xdims = new int[dims.length + 1];\r
+                    xdims[0] = nrow;\r
+\r
+                    Object[] arr = (Object[]) ArrayFuncs.newInstance(base, xdims);\r
+                    for (int irow = 0; irow < nrow; irow += 1) {\r
+                        arr[irow] = data[irow][col];\r
+                    }\r
+                    results[col] = arr;\r
+                } else {\r
+                    Object arr = ArrayFuncs.newInstance(base, nrow);\r
+                    for (int irow = 0; irow < nrow; irow += 1) {\r
+                        System.arraycopy(data[irow][col], 0, arr, irow, 1);\r
+                    }\r
+                    results[col] = arr;\r
+                }\r
+\r
+            }\r
+        }\r
+        return results;\r
+    }\r
+\r
+    /** Get a given row\r
+     * @param row The index of the row to be returned.\r
+     * @return A row of data.\r
+     */\r
+    public Object[] getRow(int row) throws FitsException {\r
+\r
+        if (!validRow(row)) {\r
+            throw new FitsException("Invalid row");\r
+        }\r
+\r
+        Object[] res;\r
+        if (table != null) {\r
+            res = getMemoryRow(row);\r
+        } else {\r
+            res = getFileRow(row);\r
+        }\r
+        return res;\r
+    }\r
+\r
+    /** Get a row from memory.\r
+     */\r
+    private Object[] getMemoryRow(int row) throws FitsException {\r
+\r
+        Object[] data = new Object[modelRow.length];\r
+        for (int col = 0; col < modelRow.length; col += 1) {\r
+            Object o = table.getElement(row, col);\r
+            o = columnToArray(col, o, 1);\r
+            data[col] = encurl(o, col, 1);\r
+            if (data[col] instanceof Object[]) {\r
+                data[col] = ((Object[]) data[col])[0];\r
+            }\r
+        }\r
+\r
+        return data;\r
+\r
+    }\r
+\r
+    /** Get a row from the file.\r
+     */\r
+    private Object[] getFileRow(int row) throws FitsException {\r
+\r
+        /** Read the row from memory */\r
+        Object[] data = new Object[nCol];\r
+        for (int col = 0; col < data.length; col += 1) {\r
+            data[col] = ArrayFuncs.newInstance(\r
+                    ArrayFuncs.getBaseClass(modelRow[col]),\r
+                    sizes[col]);\r
+        }\r
+\r
+        try {\r
+            FitsUtil.reposition(currInput, fileOffset + row * rowLen);\r
+            currInput.readLArray(data);\r
+        } catch (IOException e) {\r
+            throw new FitsException("Error in deferred row read");\r
+        }\r
+\r
+        for (int col = 0; col < data.length; col += 1) {\r
+            data[col] = columnToArray(col, data[col], 1);\r
+            data[col] = encurl(data[col], col, 1);\r
+            if (data[col] instanceof Object[]) {\r
+                data[col] = ((Object[]) data[col])[0];\r
+            }\r
+        }\r
+        return data;\r
+    }\r
+\r
+    /** Replace a row in the table.\r
+     * @param row  The index of the row to be replaced.\r
+     * @param data The new values for the row.\r
+     * @exception FitsException Thrown if the new row cannot\r
+     *                          match the existing data.\r
+     */\r
+    public void setRow(int row, Object data[]) throws FitsException {\r
+\r
+        if (table == null) {\r
+            getData();\r
+        }\r
+\r
+        if (data.length != getNCols()) {\r
+            throw new FitsException("Updated row size does not agree with table");\r
+        }\r
+\r
+        Object[] ydata = new Object[data.length];\r
+\r
+        for (int col = 0; col < data.length; col += 1) {\r
+            Object o = ArrayFuncs.flatten(data[col]);\r
+            ydata[col] = arrayToColumn(col, o);\r
+        }\r
+\r
+        try {\r
+            table.setRow(row, ydata);\r
+        } catch (TableException e) {\r
+            throw new FitsException("Error modifying table: " + e);\r
+        }\r
+    }\r
+\r
+    /** Replace a column in the table.\r
+     * @param col The index of the column to be replaced.\r
+     * @param xcol The new data for the column\r
+     * @exception FitsException Thrown if the data does not match\r
+     *                          the current column description.\r
+     */\r
+    public void setColumn(int col, Object xcol) throws FitsException {\r
+\r
+        xcol = arrayToColumn(col, xcol);\r
+        xcol = ArrayFuncs.flatten(xcol);\r
+        setFlattenedColumn(col, xcol);\r
+    }\r
+\r
+    /** Set a column with the data aleady flattened.\r
+     *\r
+     * @param col  The index of the column to be replaced.\r
+     * @param data The new data array.  This should be a one-d\r
+     *             primitive array.\r
+     * @exception FitsException Thrown if the type of length of\r
+     *                         the replacement data differs from the\r
+     *                         original.\r
+     */\r
+    public void setFlattenedColumn(int col, Object data) throws FitsException {\r
+\r
+        if (table == null) {\r
+            getData();\r
+        }\r
+\r
+        Object oldCol = table.getColumn(col);\r
+        if (data.getClass() != oldCol.getClass()\r
+                || Array.getLength(data) != Array.getLength(oldCol)) {\r
+            throw new FitsException("Replacement column mismatch at column:" + col);\r
+        }\r
+        try {\r
+            table.setColumn(col, data);\r
+        } catch (TableException e) {\r
+            throw new FitsException("Unable to set column:" + col + " error:" + e);\r
+        }\r
+    }\r
+\r
+    /** Get a given column\r
+     * @param col The index of the column.\r
+     */\r
+    public Object getColumn(int col) throws FitsException {\r
+\r
+        if (table == null) {\r
+            getData();\r
+        }\r
+\r
+        Object res = getFlattenedColumn(col);\r
+        res = encurl(res, col, nRow);\r
+        return res;\r
+    }\r
+\r
+    private Object encurl(Object res, int col, int rows) {\r
+\r
+        if (bases[col] != String.class) {\r
+\r
+            if (!isVarCol(col) && (dimens[col].length > 0)) {\r
+\r
+                int[] dims = new int[dimens[col].length + 1];\r
+                System.arraycopy(dimens[col], 0, dims, 1, dimens[col].length);\r
+                dims[0] = rows;\r
+                res = ArrayFuncs.curl(res, dims);\r
+            }\r
+\r
+        } else {\r
+\r
+            // Handle Strings.  Remember the last element\r
+            // in dimens is the length of the Strings and\r
+            // we already used that when we converted from\r
+            // byte arrays to strings.  So we need to ignore\r
+            // the last element of dimens, and add the row count\r
+            // at the beginning to curl.\r
+\r
+            if (dimens[col].length > 2) {\r
+                int[] dims = new int[dimens[col].length];\r
+\r
+                System.arraycopy(dimens[col], 0, dims, 1, dimens[col].length - 1);\r
+                dims[0] = rows;\r
+\r
+                res = ArrayFuncs.curl(res, dims);\r
+            }\r
+        }\r
+\r
+        return res;\r
+\r
+    }\r
+\r
+    /** Get a column in flattened format.\r
+     * For large tables getting a column in standard format can be\r
+     * inefficient because a separate object is needed for\r
+     * each row.  Leaving the data in flattened format means\r
+     * that only a single object is created.\r
+     * @param col\r
+     */\r
+    public Object getFlattenedColumn(int col) throws FitsException {\r
+\r
+        if (table == null) {\r
+            getData();\r
+        }\r
+\r
+        if (!validColumn(col)) {\r
+            throw new FitsException("Invalid column");\r
+        }\r
+\r
+        Object res = table.getColumn(col);\r
+        return columnToArray(col, res, nRow);\r
+    }\r
+\r
+    /** Get a particular element from the table.\r
+     * @param i The row of the element.\r
+     * @param j The column of the element.\r
+     */\r
+    public Object getElement(int i, int j) throws FitsException {\r
+\r
+        if (!validRow(i) || !validColumn(j)) {\r
+            throw new FitsException("No such element");\r
+        }\r
+\r
+        Object ele;\r
+        if (isVarCol(j) && table == null) {\r
+            // Have to read in entire data set.\r
+            getData();\r
+        }\r
+\r
+        if (table == null) {\r
+            // This is really inefficient.\r
+            // Need to either save the row, or just read the one element.\r
+            Object[] row = getRow(i);\r
+            ele = row[j];\r
+\r
+        } else {\r
+\r
+            ele = table.getElement(i, j);\r
+            ele = columnToArray(j, ele, 1);\r
+\r
+            ele = encurl(ele, j, 1);\r
+            if (ele instanceof Object[]) {\r
+                ele = ((Object[]) ele)[0];\r
+            }\r
+        }\r
+\r
+        return ele;\r
+    }\r
+\r
+    /** Get a particular element from the table but\r
+     *  do no processing of this element (e.g.,\r
+     *  dimension conversion or extraction of\r
+     *  variable length array elements/)\r
+     * @param i The row of the element.\r
+     * @param j The column of the element.\r
+     */\r
+    public Object getRawElement(int i, int j) throws FitsException {\r
+\r
+        if (table == null) {\r
+            getData();\r
+        }\r
+        return table.getElement(i, j);\r
+    }\r
+\r
+    /** Add a row at the end of the table.  Given the way the\r
+     *  table is structured this will normally not be very efficient.\r
+     *  @param o An array of elements to be added.  Each element of o\r
+     *  should be an array of primitives or a String.\r
+     */\r
+    public int addRow(Object[] o) throws FitsException {\r
+\r
+        if (table == null) {\r
+            getData();\r
+        }\r
+\r
+        if (nCol == 0 && nRow == 0) {\r
+            for (int i = 0; i < o.length; i += 1) {\r
+                addColumn(o);\r
+            }\r
+        } else {\r
+\r
+            Object[] flatRow = new Object[getNCols()];\r
+            for (int i = 0; i < getNCols(); i += 1) {\r
+                Object x = ArrayFuncs.flatten(o[i]);\r
+                flatRow[i] = arrayToColumn(i, x);\r
+            }\r
+            try {\r
+                table.addRow(flatRow);\r
+            } catch (TableException e) {\r
+                throw new FitsException("Error add row to table");\r
+            }\r
+\r
+            nRow += 1;\r
+        }\r
+\r
+        return nRow;\r
+    }\r
+\r
+    /** Delete rows from a table.\r
+     *  @param row The 0-indexed start of the rows to be deleted.\r
+     *  @param len The number of rows to be deleted.\r
+     */\r
+    public void deleteRows(int row, int len) throws FitsException {\r
+        try {\r
+            getData();\r
+            table.deleteRows(row, len);\r
+            nRow -= len;\r
+        } catch (TableException e) {\r
+            throw new FitsException("Error deleting row block " + row + " to " + (row + len - 1) + " from table");\r
+        }\r
+    }\r
+\r
+    /** Add a column to the end of a table.\r
+     * @param o An array of identically structured objects with the\r
+     *          same number of elements as other columns in the table.\r
+     */\r
+    public int addColumn(Object o) throws FitsException {\r
+\r
+        int primeDim = Array.getLength(o);\r
+\r
+        extendArrays(nCol + 1);\r
+        Class base = ArrayFuncs.getBaseClass(o);\r
+\r
+        // A varying length column is a two-d primitive\r
+        // array where the second index is not constant.\r
+        // We do not support Q types here, since Java\r
+        // can't handle the long indices anyway...\r
+        // This will probably change in some version of Java.\r
+\r
+        if (isVarying(o)) {\r
+            flags[nCol] |= COL_VARYING;\r
+            dimens[nCol] = new int[]{2};\r
+        }\r
+\r
+        if (isVaryingComp(o)) {\r
+            flags[nCol] |= COL_VARYING | COL_COMPLEX;\r
+            dimens[nCol] = new int[]{2};\r
+        }\r
+\r
+        // Flatten out everything but 1-D arrays and the\r
+        // two-D arrays associated with variable length columns.\r
+\r
+        if (!isVarCol(nCol)) {\r
+\r
+            int[] allDim = ArrayFuncs.getDimensions(o);\r
+\r
+            // Add a dimension for the length of Strings.\r
+            if (base == String.class) {\r
+                int[] xdim = new int[allDim.length + 1];\r
+                System.arraycopy(allDim, 0, xdim, 0, allDim.length);\r
+                xdim[allDim.length] = -1;\r
+                allDim = xdim;\r
+            }\r
+\r
+            if (allDim.length == 1) {\r
+                dimens[nCol] = new int[0];\r
+\r
+            } else {\r
+\r
+                dimens[nCol] = new int[allDim.length - 1];\r
+                System.arraycopy(allDim, 1, dimens[nCol], 0, allDim.length - 1);\r
+                o = ArrayFuncs.flatten(o);\r
+            }\r
+        }\r
+\r
+        addFlattenedColumn(o, dimens[nCol]);\r
+        if (nRow == 0 && nCol == 0) {\r
+            nRow = primeDim;\r
+        }\r
+        nCol += 1;\r
+        return getNCols();\r
+\r
+    }\r
+\r
+    private boolean isVaryingComp(Object o) {\r
+        String classname = o.getClass().getName();\r
+        if (classname.equals("[[[F")) {\r
+            return checkCompVary((float[][][]) o);\r
+        } else if (classname.equals("[[[D")) {\r
+            return checkDCompVary((double[][][]) o);\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /** Is this a variable length column?\r
+     *  It is if it's a two-d primitive array and\r
+     *  the second dimension is not constant.\r
+     *  It may also be a 3-d array of type float or double\r
+     *  where the last index is always 2 (when the second index\r
+     *  is non-zero).  In this case it can be\r
+     *  a complex varying column.\r
+     */\r
+    private boolean isVarying(Object o) {\r
+\r
+        if (o == null) {\r
+            return false;\r
+        }\r
+        String classname = o.getClass().getName();\r
+\r
+        if (classname.length() != 3\r
+                || classname.charAt(0) != '['\r
+                || classname.charAt(1) != '[') {\r
+            return false;\r
+        }\r
+\r
+        Object[] ox = (Object[]) o;\r
+        if (ox.length < 2) {\r
+            return false;\r
+        }\r
+\r
+        int flen = Array.getLength(ox[0]);\r
+        for (int i = 1; i < ox.length; i += 1) {\r
+            if (Array.getLength(ox[i]) != flen) {\r
+                return true;\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    // Check if this is consistent with a varying\r
+    // complex row.  That requires\r
+    //     The second index varies.\r
+    //     The third index is 2 whenever the second\r
+    //     index is non-zero.\r
+    // This function will fail if nulls are encountered.\r
+    private boolean checkCompVary(float[][][] o) {\r
+\r
+        boolean varying = false;\r
+        int len0 = o[0].length;\r
+        for (int i = 0; i < o.length; i += 1) {\r
+            if (o[i].length != len0) {\r
+                varying = true;\r
+            }\r
+            if (o[i].length > 0) {\r
+                for (int j = 0; j < o[i].length; j += 1) {\r
+                    if (o[i][j].length != 2) {\r
+                        return false;\r
+                    }\r
+                }\r
+            }\r
+        }\r
+        return varying;\r
+    }\r
+\r
+    private boolean checkDCompVary(double[][][] o) {\r
+        boolean varying = false;\r
+        int len0 = o[0].length;\r
+        for (int i = 0; i < o.length; i += 1) {\r
+            if (o[i].length != len0) {\r
+                varying = true;\r
+            }\r
+            if (o[i].length > 0) {\r
+                for (int j = 0; j < o[i].length; j += 1) {\r
+                    if (o[i][j].length != 2) {\r
+                        return false;\r
+                    }\r
+                }\r
+            }\r
+        }\r
+        return varying;\r
+    }\r
+\r
+    /** Add a column where the data is already flattened.\r
+     * @param o      The new column data.  This should be a one-dimensional\r
+     *               primitive array.\r
+     * @param dims The dimensions of one row of the column.\r
+     */\r
+    public int addFlattenedColumn(Object o, int[] dims) throws FitsException {\r
+\r
+        extendArrays(nCol + 1);\r
+\r
+        bases[nCol] = ArrayFuncs.getBaseClass(o);\r
+\r
+        if (bases[nCol] == boolean.class) {\r
+            flags[nCol] |= COL_BOOLEAN;\r
+        } else if (bases[nCol] == String.class) {\r
+            flags[nCol] |= COL_STRING;\r
+        }\r
+\r
+        // Convert to column first in case\r
+        // this is a String or variable length array.\r
+\r
+        o = arrayToColumn(nCol, o);\r
+\r
+        int size = 1;\r
+\r
+        for (int dim = 0; dim < dims.length; dim += 1) {\r
+            size *= dims[dim];\r
+        }\r
+        sizes[nCol] = size;\r
+\r
+        if (size != 0) {\r
+            int xRow = Array.getLength(o) / size;\r
+            if (xRow > 0 && nCol != 0 && xRow != nRow) {\r
+                throw new FitsException("Added column does not have correct row count");\r
+            }\r
+        }\r
+\r
+        if (!isVarCol(nCol)) {\r
+            modelRow[nCol] = ArrayFuncs.newInstance(ArrayFuncs.getBaseClass(o), dims);\r
+            rowLen += size * ArrayFuncs.getBaseLength(o);\r
+        } else {\r
+            if (isLongVary(nCol)) {\r
+                modelRow[nCol] = new long[2];\r
+                rowLen += 16;\r
+            } else {\r
+                modelRow[nCol] = new int[2];\r
+                rowLen += 8;\r
+            }\r
+        }\r
+\r
+        // Only add to table if table already exists or if we\r
+        // are filling up the last element in columns.\r
+        // This way if we allocate a bunch of columns at the beginning\r
+        // we only create the column table after we have all the columns\r
+        // ready.\r
+\r
+        columns[nCol] = o;\r
+\r
+        try {\r
+            if (table != null) {\r
+                table.addColumn(o, sizes[nCol]);\r
+            } else if (nCol == columns.length - 1) {\r
+                table = new ColumnTable(columns, sizes);\r
+            }\r
+        } catch (TableException e) {\r
+            throw new FitsException("Error in ColumnTable:" + e);\r
+        }\r
+        return nCol;\r
+    }\r
+\r
+    /** Get the number of rows in the table\r
+     */\r
+    public int getNRows() {\r
+        return nRow;\r
+    }\r
+\r
+    /** Get the number of columns in the table.\r
+     */\r
+    public int getNCols() {\r
+        return nCol;\r
+    }\r
+\r
+    /** Check to see if this is a valid row.\r
+     * @param i The Java index (first=0) of the row to check.\r
+     */\r
+    protected boolean validRow(int i) {\r
+\r
+        if (getNRows() > 0 && i >= 0 && i < getNRows()) {\r
+            return true;\r
+        } else {\r
+            return false;\r
+        }\r
+    }\r
+\r
+    /** Check if the column number is valid.\r
+     *\r
+     * @param j The Java index (first=0) of the column to check.\r
+     */\r
+    protected boolean validColumn(int j) {\r
+        return (j >= 0 && j < getNCols());\r
+    }\r
+\r
+    /** Replace a single element within the table.\r
+     *\r
+     * @param i The row of the data.\r
+     * @param j The column of the data.\r
+     * @param o The replacement data.\r
+     */\r
+    public void setElement(int i, int j, Object o) throws FitsException {\r
+\r
+        getData();\r
+\r
+        try {\r
+            if (isVarCol(j)) {\r
+\r
+                int size = Array.getLength(o);\r
+                // The offset for the row is the offset to the heap plus the offset within the heap.\r
+                int offset = (int) heap.getSize();\r
+                heap.putData(o);\r
+                if (isLongVary(j)) {\r
+                    table.setElement(i, j, new long[]{size, offset});\r
+                } else {\r
+                    table.setElement(i, j, new int[]{size, offset});\r
+                }\r
+\r
+            } else {\r
+                table.setElement(i, j, ArrayFuncs.flatten(o));\r
+            }\r
+        } catch (TableException e) {\r
+            throw new FitsException("Error modifying table:" + e);\r
+        }\r
+    }\r
+\r
+    /** Read the data -- or defer reading on random access\r
+     */\r
+    public void read(ArrayDataInput i) throws FitsException {\r
+\r
+        setFileOffset(i);\r
+        currInput = i;\r
+\r
+        if (i instanceof RandomAccess) {\r
+\r
+            try {\r
+                i.skipBytes(getTrueSize());\r
+            } catch (IOException e) {\r
+                throw new FitsException("Unable to skip binary table HDU:" + e);\r
+            }\r
+            try {\r
+                i.skipBytes(FitsUtil.padding(getTrueSize()));\r
+            } catch (EOFException e) {\r
+                throw new PaddingException("Missing padding after binary table:" + e, this);\r
+            } catch (IOException e) {\r
+                throw new FitsException("Error skipping padding after binary table:" + e);\r
+            }\r
+\r
+        } else {\r
+\r
+            /** Read the data associated with the HDU including the hash area if present.\r
+             * @param i The input stream\r
+             */\r
+            if (table == null) {\r
+                table = createTable();\r
+            }\r
+\r
+            readTrueData(i);\r
+        }\r
+    }\r
+\r
+    /** Read table, heap and padding */\r
+    protected void readTrueData(ArrayDataInput i) throws FitsException {\r
+        try {\r
+            table.read(i);\r
+            i.skipBytes(heapOffset);\r
+            heap.read(i);\r
+            heapReadFromStream = true;\r
+\r
+        } catch (IOException e) {\r
+            throw new FitsException("Error reading binary table data:" + e);\r
+        }\r
+        try {\r
+            i.skipBytes(FitsUtil.padding(getTrueSize()));\r
+        } catch (EOFException e) {\r
+            throw new PaddingException("Error skipping padding after binary table", this);\r
+        } catch (IOException e) {\r
+            throw new FitsException("Error reading binary table data padding:" + e);\r
+        }\r
+    }\r
+\r
+    /** Read the heap which contains the data for variable length\r
+     *  arrays.\r
+     * A. Kovacs (4/1/08) Separated heap reading, s.t. the heap can\r
+     * be properly initialized even if in deferred read mode.\r
+     * columnToArray() checks and initializes the heap as necessary.\r
+     */\r
+    protected void readHeap(ArrayDataInput input) throws FitsException {\r
+        FitsUtil.reposition(input, fileOffset + nRow * rowLen + heapOffset);\r
+        heap.read(input);\r
+        heapReadFromStream = true;\r
+    }\r
+\r
+    /** Get the size of the data in the HDU sans padding.\r
+     */\r
+    public long getTrueSize() {\r
+        long len = ((long) nRow) * rowLen;\r
+        if (heap.size() > 0) {\r
+            len += heap.size() + heapOffset;\r
+        }\r
+        return len;\r
+    }\r
+\r
+    /** Write the table, heap and padding */\r
+    public void write(ArrayDataOutput os) throws FitsException {\r
+\r
+        getData();\r
+        int len;\r
+\r
+        try {\r
+\r
+            // First write the table.\r
+            len = table.write(os);\r
+            if (heapOffset > 0) {\r
+                int off = heapOffset;\r
+                // Minimize memory usage.  This also accommodates\r
+                // the possibility that heapOffset > 2GB.\r
+                // Previous code might have allocated up to 2GB\r
+                // array.  [In practice this is always going\r
+                // to be really small though...]\r
+                int arrSiz = 4000000;\r
+                while (off > 0) {\r
+                    if (arrSiz > off) {\r
+                        arrSiz = (int) off;\r
+                    }\r
+                    os.write(new byte[arrSiz]);\r
+                    off -= arrSiz;\r
+                }\r
+            }\r
+\r
+            // Now check if we need to write the heap\r
+            if (heap.size() > 0) {\r
+                heap.write(os);\r
+            }\r
+\r
+            FitsUtil.pad(os, getTrueSize());\r
+\r
+        } catch (IOException e) {\r
+            throw new FitsException("Unable to write table:" + e);\r
+        }\r
+    }\r
+\r
+    public Object getData() throws FitsException {\r
+\r
+\r
+        if (table == null) {\r
+\r
+            if (currInput == null) {\r
+                throw new FitsException("Cannot find input for deferred read");\r
+            }\r
+\r
+            table = createTable();\r
+\r
+            long currentOffset = FitsUtil.findOffset(currInput);\r
+            FitsUtil.reposition(currInput, fileOffset);\r
+            readTrueData(input);\r
+            FitsUtil.reposition(currInput, currentOffset);\r
+        }\r
+\r
+        return table;\r
+    }\r
+\r
+    public int[][] getDimens() {\r
+        return dimens;\r
+    }\r
+\r
+    public Class[] getBases() {\r
+        return table.getBases();\r
+    }\r
+\r
+    public char[] getTypes() {\r
+        if (table == null) {\r
+            try {\r
+                getData();\r
+            } catch (FitsException e) {\r
+            }\r
+        }\r
+        return table.getTypes();\r
+    }\r
+\r
+    public Object[] getFlatColumns() {\r
+        if (table == null) {\r
+            try {\r
+                getData();\r
+            } catch (FitsException e) {\r
+            }\r
+        }\r
+        return table.getColumns();\r
+    }\r
+\r
+    public int[] getSizes() {\r
+        return sizes;\r
+    }\r
+\r
+    /** Convert the external representation to the\r
+     *  BinaryTable representation.  Transformation include\r
+     *  boolean -> T/F, Strings -> byte arrays,\r
+     *  variable length arrays -> pointers (after writing data\r
+     *  to heap).\r
+     */\r
+    private Object arrayToColumn(int col, Object o) throws FitsException {\r
+\r
+        if (flags[col] == 0) {\r
+            return o;\r
+        }\r
+\r
+        if (!isVarCol(col)) {\r
+\r
+            if (isString(col)) {\r
+\r
+                // Convert strings to array of bytes.\r
+                int[] dims = dimens[col];\r
+\r
+                // Set the length of the string if we are just adding the column.\r
+                if (dims[dims.length - 1] < 0) {\r
+                    dims[dims.length - 1] = FitsUtil.maxLength((String[]) o);\r
+                }\r
+                if (o instanceof String) {\r
+                    o = new String[]{(String) o};\r
+                }\r
+                o = FitsUtil.stringsToByteArray((String[]) o, dims[dims.length - 1]);\r
+\r
+\r
+            } else if (isBoolean(col)) {\r
+\r
+                // Convert true/false to 'T'/'F'\r
+                o = FitsUtil.booleanToByte((boolean[]) o);\r
+            }\r
+\r
+        } else {\r
+\r
+            if (isBoolean(col)) {\r
+\r
+                // Handle addRow/addElement\r
+                if (o instanceof boolean[]) {\r
+                    o = new boolean[][]{(boolean[]) o};\r
+                }\r
+\r
+                // Convert boolean to byte arrays\r
+                boolean[][] to = (boolean[][]) o;\r
+                byte[][] xo = new byte[to.length][];\r
+                for (int i = 0; i < to.length; i += 1) {\r
+                    xo[i] = FitsUtil.booleanToByte(to[i]);\r
+                }\r
+                o = xo;\r
+            }\r
+\r
+            // Write all rows of data onto the heap.\r
+            int offset = heap.putData(o);\r
+\r
+            int blen = ArrayFuncs.getBaseLength(o);\r
+\r
+            // Handle an addRow of a variable length element.\r
+            // In this case we only get a one-d array, but we just\r
+            // make is 1 x n to get the second dimension.\r
+            if (!(o instanceof Object[])) {\r
+                o = new Object[]{o};\r
+            }\r
+\r
+            // Create the array descriptors\r
+            int nrow = Array.getLength(o);\r
+            int factor = 1;\r
+            if (isComplex(col)) {\r
+                factor = 2;\r
+            }\r
+            if (isLongVary(col)) {\r
+                long[] descrip = new long[2 * nrow];\r
+\r
+                Object[] x = (Object[]) o;\r
+                // Fill the descriptor for each row.\r
+                for (int i = 0; i < nrow; i += 1) {\r
+                    int len = Array.getLength(x[i]);\r
+                    descrip[2 * i] = len;\r
+                    descrip[2 * i + 1] = offset;\r
+                    offset += len * blen * factor;\r
+                }\r
+                o = descrip;\r
+            } else {\r
+                int[] descrip = new int[2 * nrow];\r
+\r
+                Object[] x = (Object[]) o;\r
+\r
+                // Fill the descriptor for each row.\r
+                for (int i = 0; i < nrow; i += 1) {\r
+                    int len = Array.getLength(x[i]);\r
+                    descrip[2 * i] = len;\r
+                    descrip[2 * i + 1] = offset;\r
+                    offset += len * blen * factor;\r
+                }\r
+                o = descrip;\r
+            }\r
+        }\r
+\r
+        return o;\r
+    }\r
+\r
+    /** Convert data from binary table representation to external\r
+     *  Java representation.\r
+     */\r
+    private Object columnToArray(int col, Object o, int rows) throws FitsException {\r
+\r
+        // Most of the time we need do nothing!\r
+        if (flags[col] == 0) {\r
+            return o;\r
+        }\r
+\r
+        // If a varying length column use the descriptors to\r
+        // extract appropriate information from the headers.\r
+        if (isVarCol(col)) {\r
+\r
+            // A. Kovacs (4/1/08)\r
+            // Ensure that the heap has been initialized\r
+            if (!heapReadFromStream) {\r
+                readHeap(currInput);\r
+            }\r
+\r
+            int[] descrip;\r
+            if (isLongVary(col)) {\r
+                // Convert longs to int's.  This is dangerous.\r
+                if (!warnedOnVariableConversion) {\r
+                    System.err.println("Warning: converting long variable array pointers to int's");\r
+                    warnedOnVariableConversion = true;\r
+                }\r
+                descrip = (int[]) ArrayFuncs.convertArray(o, int.class);\r
+            } else {\r
+                descrip = (int[]) o;\r
+            }\r
+\r
+            int nrow = descrip.length / 2;\r
+\r
+            Object[] res;  // Res will be the result of extracting from the heap.\r
+            int[] dims;    // Used to create result arrays.\r
+\r
+\r
+            if (isComplex(col)) {\r
+                // Complex columns have an extra dimension for each row\r
+                dims = new int[]{nrow, 0, 0};\r
+                res = (Object[]) ArrayFuncs.newInstance(bases[col], dims);\r
+                // Set up dims for individual rows.\r
+                dims = new int[2];\r
+                dims[1] = 2;\r
+\r
+                // ---> Added clause by Attila Kovacs (13 July 2007)\r
+                // String columns have to read data into a byte array at first\r
+                // then do the string conversion later.\r
+\r
+            } else if (isString(col)) {\r
+                dims = new int[]{nrow, 0};\r
+                res = (Object[]) ArrayFuncs.newInstance(byte.class, dims);\r
+\r
+            } else {\r
+                // Non-complex data has a simple primitive array for each row\r
+                dims = new int[]{nrow, 0};\r
+                res = (Object[]) ArrayFuncs.newInstance(bases[col], dims);\r
+            }\r
+\r
+            // Now read in each requested row.\r
+            for (int i = 0; i < nrow; i += 1) {\r
+                Object row;\r
+                int offset = descrip[2 * i + 1];\r
+                int dim = descrip[2 * i];\r
+\r
+                if (isComplex(col)) {\r
+                    dims[0] = dim;\r
+                    row = ArrayFuncs.newInstance(bases[col], dims);\r
+\r
+                    // ---> Added clause by Attila Kovacs (13 July 2007)\r
+                    // Again, String entries read data into a byte array at first\r
+                    // then do the string conversion later.\r
+                } else if (isString(col)) {\r
+                    // For string data, we need to read bytes and convert\r
+                    // to strings\r
+                    row = ArrayFuncs.newInstance(byte.class, dim);\r
+\r
+                } else if (isBoolean(col)) {\r
+                    // For boolean data, we need to read bytes and convert\r
+                    // to booleans.\r
+                    row = ArrayFuncs.newInstance(byte.class, dim);\r
+\r
+                } else {\r
+                    row = ArrayFuncs.newInstance(bases[col], dim);\r
+                }\r
+\r
+                heap.getData(offset, row);\r
+\r
+                // Now do the boolean conversion.\r
+                if (isBoolean(col)) {\r
+                    row = FitsUtil.byteToBoolean((byte[]) row);\r
+                }\r
+\r
+                res[i] = row;\r
+            }\r
+            o = res;\r
+\r
+        } else {  // Fixed length columns\r
+\r
+            // Need to convert String byte arrays to appropriate Strings.\r
+            if (isString(col)) {\r
+                int[] dims = dimens[col];\r
+                byte[] bytes = (byte[]) o;\r
+                if (bytes.length > 0) {\r
+                    if (dims.length > 0) {\r
+                        o = FitsUtil.byteArrayToStrings(bytes, dims[dims.length - 1]);\r
+                    } else {\r
+                        o = FitsUtil.byteArrayToStrings(bytes, 1);\r
+                    }\r
+                } else {\r
+                    // This probably fails for multidimensional arrays of strings where\r
+                    // all elements are null.\r
+                    String[] str = new String[rows];\r
+                    for (int i = 0; i < str.length; i += 1) {\r
+                        str[i] = "";\r
+                    }\r
+                    o = str;\r
+                }\r
+\r
+            } else if (isBoolean(col)) {\r
+                o = FitsUtil.byteToBoolean((byte[]) o);\r
+            }\r
+        }\r
+\r
+        return o;\r
+    }\r
+\r
+    /** Make sure the arrays which describe the columns are\r
+     *  long enough, and if not extend them.\r
+     */\r
+    private void extendArrays(int need) {\r
+\r
+        boolean wasNull = false;\r
+        if (sizes == null) {\r
+            wasNull = true;\r
+\r
+        } else if (sizes.length > need) {\r
+            return;\r
+        }\r
+\r
+        // Allocate the arrays.\r
+        int[] newSizes = new int[need];\r
+        int[][] newDimens = new int[need][];\r
+        int[] newFlags = new int[need];\r
+        Object[] newModel = new Object[need];\r
+        Object[] newColumns = new Object[need];\r
+        Class[] newBases = new Class[need];\r
+\r
+        if (!wasNull) {\r
+            int len = sizes.length;\r
+            System.arraycopy(sizes, 0, newSizes, 0, len);\r
+            System.arraycopy(dimens, 0, newDimens, 0, len);\r
+            System.arraycopy(flags, 0, newFlags, 0, len);\r
+            System.arraycopy(modelRow, 0, newModel, 0, len);\r
+            System.arraycopy(columns, 0, newColumns, 0, len);\r
+            System.arraycopy(bases, 0, newBases, 0, len);\r
+        }\r
+\r
+        sizes = newSizes;\r
+        dimens = newDimens;\r
+        flags = newFlags;\r
+        modelRow = newModel;\r
+        columns = newColumns;\r
+        bases = newBases;\r
+    }\r
+\r
+    /** What is the size of the heap -- including the offset from the end of the\r
+     *  table data.\r
+     */\r
+    public int getHeapSize() {\r
+        return heapOffset + heap.size();\r
+    }\r
+\r
+    /** What is the offset to the heap */\r
+    public int getHeapOffset() {\r
+        return heapOffset;\r
+    }\r
+\r
+    /** Does this column have variable length arrays? */\r
+    boolean isVarCol(int col) {\r
+        return (flags[col] & COL_VARYING) != 0;\r
+    }\r
+\r
+    /** Does this column have variable length arrays? */\r
+    boolean isLongVary(int col) {\r
+        return (flags[col] & COL_LONGVARY) != 0;\r
+    }\r
+\r
+    /** Is this column a string column */\r
+    private boolean isString(int col) {\r
+        return (flags[col] & COL_STRING) != 0;\r
+    }\r
+\r
+    /** Is this column complex? */\r
+    private boolean isComplex(int col) {\r
+        return (flags[col] & COL_COMPLEX) != 0;\r
+    }\r
+\r
+    /** Is this column a boolean column */\r
+    private boolean isBoolean(int col) {\r
+        return (flags[col] & COL_BOOLEAN) != 0;\r
+    }\r
+\r
+    /** Is this column a bit column */\r
+    private boolean isBit(int col) {\r
+        return (flags[col] & COL_BOOLEAN) != 0;\r
+    }\r
+\r
+    /** Delete a set of columns.  Note that this\r
+     *  does not fix the header, so users should normally\r
+     *  call the routine in TableHDU.\r
+     */\r
+    public void deleteColumns(int start, int len) throws FitsException {\r
+        getData();\r
+        try {\r
+            rowLen = table.deleteColumns(start, len);\r
+            nCol -= len;\r
+        } catch (Exception e) {\r
+            throw new FitsException("Error deleting columns from BinaryTable:" + e);\r
+        }\r
+    }\r
+\r
+    /** Update the header after a deletion. */\r
+    public void updateAfterDelete(int oldNcol, Header hdr) throws FitsException {\r
+        hdr.addValue("NAXIS1", rowLen, "ntf::binarytable:naxis1:1");\r
+    }\r
+}\r
diff --git a/src/nom/tam/fits/BinaryTableHDU.java b/src/nom/tam/fits/BinaryTableHDU.java
new file mode 100644 (file)
index 0000000..2b5c720
--- /dev/null
@@ -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 <CODE>true</CODE> 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 <CODE>true</CODE> 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.  <b> Note:</b> 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 (file)
index 0000000..17788c8
--- /dev/null
@@ -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.
+     * <ul>
+     *  <li> For images and primary data this is a simple (but possibly
+     *       multi-dimensional) primitive array.  When group data is
+     *       supported it will be a possibly multidimensional array
+     *       of group objects.
+     *  <li> For ASCII data it is a two dimensional Object array where
+     *       each of the constituent objects is a primitive array of length 1.
+     *  <li> For Binary data it is a two dimensional Object array where
+     *       each of the constituent objects is a primitive array of arbitrary
+     *       (more or less) dimensionality.
+     *  </ul>
+     */
+    /** 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 (file)
index 0000000..cfb9c0e
--- /dev/null
@@ -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.
+ * <p>
+ *
+ * <p>
+ * <b> Description of the Package </b>
+ * <p>
+ * 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.
+ *
+ *
+ * <ul>
+ * <li> The Fits class provides capabilities to
+ *      read and write data at the HDU level, and to
+ *      add and delete HDU's from the current Fits object.
+ *      A large number of constructors are provided which
+ *      allow users to associate the Fits object with
+ *      some form of external data.  This external
+ *      data may be in a compressed format.
+ * <li> The HDU class is a factory class which is used to
+ *      create HDUs.  HDU's can be of a number of types
+ *      derived from the abstract class BasicHDU.
+ *      The hierarchy of HDUs is:
+ *      <ul>
+ *      <li>BasicHDU
+ *           <ul>
+ *           <li> ImageHDU
+ *           <li> RandomGroupsHDU
+ *           <li> TableHDU
+ *                <ul>
+ *                <li> BinaryTableHDU
+ *                <li> AsciiTableHDU
+ *                </ul>
+ *           </ul>
+ *       </ul>
+ *
+ * <li> The Header class provides many functions to
+ *      add, delete and read header keywords in a variety
+ *      of formats.
+ * <li> The HeaderCard class provides access to the structure
+ *      of a FITS header card.
+ * <li> The Data class is an abstract class which provides
+ *      the basic methods for reading and writing FITS data.
+ *      Users will likely only be interested in the getData
+ *      method which returns that actual FITS data.
+ * <li> The TableHDU class provides a large number of
+ *      methods to access and modify information in
+ *      tables.
+ * <li> The Column class
+ *      combines the Header information and Data corresponding to
+ *      a given column.
+ * </ul>
+ *
+ *
+ * @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 (file)
index 0000000..82641b0
--- /dev/null
@@ -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 <CODE>Date</CODE> object.
+     * @param dStr     the FITS date
+     * @return either <CODE>null</CODE> or a Date object
+     * @exception FitsException        if <CODE>dStr</CODE> 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 (file)
index 0000000..7a66b03
--- /dev/null
@@ -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 (file)
index 0000000..22aaeb2
--- /dev/null
@@ -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 (file)
index 0000000..c9d458d
--- /dev/null
@@ -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 (file)
index 0000000..d5a1236
--- /dev/null
@@ -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 (file)
index 0000000..014eb38
--- /dev/null
@@ -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<String,List<String>> 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 (file)
index 0000000..fc0cd35
--- /dev/null
@@ -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
+ * <pre>
+ *    KEY        = 'ABC&'   /A comment
+ *    CONTINUE      'DEF&'  / Another comment
+ *    CONTINUE      'GHIJKL '
+ * </pre>
+ * 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 <CODE>true</CODE> for a valid header,
+     *         <CODE>false</CODE> 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 <CODE>null</CODE> 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 <CODE>int</CODE> 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 <CODE>long</CODE> 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 <CODE>long</CODE> 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 <CODE>float</CODE> 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 <CODE>float</CODE> 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 <CODE>double</CODE> 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 <CODE>double</CODE> 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 <CODE>boolean</CODE> 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 <CODE>boolean</CODE> 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 <CODE>String</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 <CODE>true</CODE> if the card was replaced.
+     * @exception HeaderCardException If <CODE>newKey</CODE> 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.,
+     *  <pre>
+     *  X = 'AB&'
+     *  CONTINUE 'CDEF' / ABC
+     *  </pre>
+     *  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 <CODE>true<CODE> if the specified keyword is present in this
+     *         table; <CODE>false<CODE> 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:
+     * <ul>
+     * <li> 8  -- signed bytes data.  Also used for tables.
+     * <li> 16 -- signed short data.
+     * <li> 32 -- signed int data.
+     * <li> 64 -- signed long data.
+     * <li> -32 -- IEEE 32 bit floating point numbers.
+     * <li> -64 -- IEEE 64 bit floating point numbers.
+     * </ul>
+     */
+    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 <CODE>null</CODE> 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 <CODE>null</CODE> 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 (file)
index 0000000..0775c53
--- /dev/null
@@ -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 (file)
index 0000000..77c018b
--- /dev/null
@@ -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 (file)
index 0000000..d8cb599
--- /dev/null
@@ -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<String,String> commentMap = new HashMap<String,String>();
+    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 (file)
index 0000000..4a46cd5
--- /dev/null
@@ -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 <br>
+     *          1 if the second argument should be written first <br>
+     *          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 (file)
index 0000000..611ac51
--- /dev/null
@@ -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.
+ * <p>
+ * 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 (file)
index 0000000..eb21e70
--- /dev/null
@@ -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 <CODE>true</CODE> 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 (file)
index 0000000..bb22f5b
--- /dev/null
@@ -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 (file)
index 0000000..c174b77
--- /dev/null
@@ -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:
+ * 
+ * <code>
+ *     Fits f = new Fits("somefile");
+ *     try {
+ *          f.read();
+ *     } catch (PaddingException e) {
+ *          f.addHDU(e.getHDU());
+ *     }
+ * </code>
+ * 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 (file)
index 0000000..ae6e9e1
--- /dev/null
@@ -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 (file)
index 0000000..4ad51ff
--- /dev/null
@@ -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 <CODE>true</CODE> 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 (file)
index 0000000..02c9b95
--- /dev/null
@@ -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 (file)
index 0000000..544ba60
--- /dev/null
@@ -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 (file)
index 0000000..0a2fbf2
--- /dev/null
@@ -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 (file)
index 0000000..69a0981
--- /dev/null
@@ -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 (file)
index 0000000..e854e55
--- /dev/null
@@ -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 <CODE>true</CODE> 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 (file)
index 0000000..ebcd4ae
--- /dev/null
@@ -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 (file)
index 0000000..9200c22
--- /dev/null
@@ -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 (file)
index 0000000..57b1a91
--- /dev/null
@@ -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:
+ *  <pre>
+ *     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
+ * </pre>
+ */
+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 (file)
index 0000000..dd3aad0
--- /dev/null
@@ -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 (file)
index 0000000..aef3198
--- /dev/null
@@ -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 (file)
index 0000000..e6dced1
--- /dev/null
@@ -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 (file)
index 0000000..ea915ff
--- /dev/null
@@ -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 (file)
index 0000000..47e339d
--- /dev/null
@@ -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 (file)
index 0000000..2a07a5e
--- /dev/null
@@ -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 (file)
index 0000000..5b76470
--- /dev/null
@@ -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 (file)
index 0000000..1566320
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..57ec7c9
--- /dev/null
@@ -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 (file)
index 0000000..742e713
--- /dev/null
@@ -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 (file)
index 0000000..a7c3500
--- /dev/null
@@ -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 (file)
index 0000000..394103c
--- /dev/null
@@ -0,0 +1,65 @@
+package nom.tam.util;\r
+\r
+import java.io.IOException;\r
+\r
+public interface ArrayDataInput extends java.io.DataInput {\r
+\r
+    /** Read a generic (possibly multidimenionsional) primitive array.\r
+     * An  Object[] array is also a legal argument if each element\r
+     * of the array is a legal.\r
+     * <p>\r
+     * The ArrayDataInput classes do not support String input since\r
+     * it is unclear how one would read in an Array of strings.\r
+     * @param o   A [multidimensional] primitive (or Object) array.\r
+     * @deprecated See readLArray(Object o).\r
+     */\r
+    public int readArray(Object o) throws IOException;\r
+\r
+    /** Read an array. This version works even if the\r
+     * underlying data is more than 2 Gigabytes.\r
+     */\r
+    public long readLArray(Object o) throws IOException;\r
+\r
+    /* Read a complete primitive array */\r
+    public int read(byte[] buf) throws IOException;\r
+\r
+    public int read(boolean[] buf) throws IOException;\r
+\r
+    public int read(short[] buf) throws IOException;\r
+\r
+    public int read(char[] buf) throws IOException;\r
+\r
+    public int read(int[] buf) throws IOException;\r
+\r
+    public int read(long[] buf) throws IOException;\r
+\r
+    public int read(float[] buf) throws IOException;\r
+\r
+    public int read(double[] buf) throws IOException;\r
+\r
+    /* Read a segment of a primitive array. */\r
+    public int read(byte[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(boolean[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(char[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(short[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(int[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(long[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(float[] buf, int offset, int size) throws IOException;\r
+\r
+    public int read(double[] buf, int offset, int size) throws IOException;\r
+\r
+    /* Skip (forward) in a file */\r
+    public long skip(long distance) throws IOException;\r
+\r
+    /* Skip and require that the data be there. */\r
+    public long skipBytes(long toSkip) throws IOException;\r
+\r
+    /* Close the file. */\r
+    public void close() throws IOException;\r
+}\r
diff --git a/src/nom/tam/util/ArrayDataOutput.java b/src/nom/tam/util/ArrayDataOutput.java
new file mode 100644 (file)
index 0000000..5bc91f3
--- /dev/null
@@ -0,0 +1,67 @@
+package nom.tam.util;\r
+\r
+import java.io.IOException;\r
+\r
+public interface ArrayDataOutput extends java.io.DataOutput {\r
+\r
+    /** Write a generic (possibly multi-dimenionsional) primitive or String\r
+     *  array.  An array of Objects is also allowed if all\r
+     *  of the elements are valid arrays.\r
+     *  <p>\r
+     *  This routine is not called 'write' to avoid possible compilation\r
+     *  errors in routines which define only some of the other methods\r
+     *  of the interface (and defer to the superclass on others).\r
+     *  In that case there is an ambiguity as to whether to\r
+     *  call the routine in the current class but convert to\r
+     *  Object, or call the method from the super class with\r
+     *  the same type argument.\r
+     *  @param o       The primitive or String array to be written.\r
+     *  @throws IOException if the argument is not of the proper type\r
+     */\r
+    public void writeArray(Object o) throws IOException;\r
+\r
+    /* Write a complete array */\r
+    public void write(byte[] buf) throws IOException;\r
+\r
+    public void write(boolean[] buf) throws IOException;\r
+\r
+    public void write(short[] buf) throws IOException;\r
+\r
+    public void write(char[] buf) throws IOException;\r
+\r
+    public void write(int[] buf) throws IOException;\r
+\r
+    public void write(long[] buf) throws IOException;\r
+\r
+    public void write(float[] buf) throws IOException;\r
+\r
+    public void write(double[] buf) throws IOException;\r
+\r
+    /* Write an array of Strings */\r
+    public void write(String[] buf) throws IOException;\r
+\r
+    /* Write a segment of a primitive array. */\r
+    public void write(byte[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(boolean[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(char[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(short[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(int[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(long[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(float[] buf, int offset, int size) throws IOException;\r
+\r
+    public void write(double[] buf, int offset, int size) throws IOException;\r
+\r
+    /* Write some of an array of Strings */\r
+    public void write(String[] buf, int offset, int size) throws IOException;\r
+\r
+    /* Flush the output buffer */\r
+    public void flush() throws IOException;\r
+\r
+    public void close() throws IOException;\r
+}\r
diff --git a/src/nom/tam/util/ArrayFuncs.java b/src/nom/tam/util/ArrayFuncs.java
new file mode 100644 (file)
index 0000000..7e629d7
--- /dev/null
@@ -0,0 +1,1259 @@
+// Member of the utility package.\r
+// Modified July 20, 2009 to handle very large arrays\r
+// in some contexts.\r
+package nom.tam.util;\r
+\r
+/* Copyright: Thomas McGlynn 1997-1998.\r
+ * This code may be used for any purpose, non-commercial\r
+ * or commercial so long as this copyright notice is retained\r
+ * in the source code or included in or referred to in any\r
+ * derived software.\r
+ */\r
+import java.lang.reflect.*;\r
+import java.util.Arrays;\r
+\r
+/** This is a package of static functions which perform\r
+ * computations on arrays.  Generally these routines attempt\r
+ * to complete without throwing errors by ignoring data\r
+ * they cannot understand.\r
+ */\r
+public class ArrayFuncs implements PrimitiveInfo {\r
+\r
+    /** Compute the size of an object.  Note that this only handles\r
+     * arrays or scalars of the primitive objects and Strings.  It\r
+     * returns 0 for any object array element it does not understand.\r
+     *\r
+     * @param o The object whose size is desired.\r
+     * @deprecated May silently underestimate the size\r
+     *   if the size > 2 GB.\r
+     */\r
+    public static int computeSize(Object o) {\r
+        return (int) computeLSize(o);\r
+    }\r
+\r
+    public static long computeLSize(Object o) {\r
+\r
+        if (o == null) {\r
+            return 0;\r
+        }\r
+\r
+        long size = 0;\r
+        String classname = o.getClass().getName();\r
+        if (classname.substring(0, 2).equals("[[")) {\r
+\r
+            for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                size += computeLSize(((Object[]) o)[i]);\r
+            }\r
+            return size;\r
+        }\r
+\r
+        if (classname.charAt(0) == '[' && classname.charAt(1) != 'L') {\r
+            char c = classname.charAt(1);\r
+\r
+            for (int i = 0; i < PrimitiveInfo.suffixes.length; i += 1) {\r
+                if (c == PrimitiveInfo.suffixes[i]) {\r
+                    return (long) (Array.getLength(o)) * PrimitiveInfo.sizes[i];\r
+                }\r
+            }\r
+            return 0;\r
+        }\r
+\r
+        // Do we have a non-primitive array?\r
+        if (classname.charAt(0) == '[') {\r
+            int len = 0;\r
+            for (int i = 0; i < Array.getLength(o); i += 1) {\r
+                len += computeLSize(Array.get(o, i));\r
+            }\r
+            return len;\r
+        }\r
+\r
+        // Now a few special scalar objects.\r
+        if (classname.substring(0, 10).equals("java.lang.")) {\r
+            classname = classname.substring(10, classname.length());\r
+            if (classname.equals("Integer") || classname.equals("Float")) {\r
+                return 4;\r
+            } else if (classname.equals("Double") || classname.equals("Long")) {\r
+                return 8;\r
+            } else if (classname.equals("Short") || classname.equals("Char")) {\r
+                return 2;\r
+            } else if (classname.equals("Byte") || classname.equals("Boolean")) {\r
+                return 1;\r
+            } else if (classname.equals("String")) {\r
+                return ((String) o).length();\r
+            } else {\r
+                return 0;\r
+            }\r
+        }\r
+        return 0;\r
+    }\r
+\r
+    /** Count the number of elements in an array.\r
+     *  @deprecated  May silently underestimate\r
+     *               size if number is > 2 G.\r
+     */\r
+    public static int nElements(Object o) {\r
+        return (int) nLElements(o);\r
+    }\r
+\r
+    /** Count the number of elements in an array.\r
+     *  @deprecated  May silently underestimate\r
+     *               size if number is > 2 G.\r
+     */\r
+    public static long nLElements(Object o) {\r
+\r
+        if (o == null) {\r
+            return 0;\r
+        }\r
+\r
+        String classname = o.getClass().getName();\r
+        if (classname.charAt(1) == '[') {\r
+            int count = 0;\r
+            for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                count += nLElements(((Object[]) o)[i]);\r
+            }\r
+            return count;\r
+\r
+        } else if (classname.charAt(0) == '[') {\r
+            return Array.getLength(o);\r
+\r
+        } else {\r
+            return 1;\r
+        }\r
+    }\r
+\r
+    /** Try to create a deep clone of an Array or a standard clone of a scalar.\r
+     * The object may comprise arrays of\r
+     * any primitive type or any Object type which implements Cloneable.\r
+     * However, if the Object is some kind of collection, e.g., a Vector\r
+     * then only a shallow copy of that object is made.  I.e., deep refers\r
+     * only to arrays.\r
+     *\r
+     * @param o The object to be copied.\r
+     */\r
+    public static Object deepClone(Object o) {\r
+\r
+        if (o == null) {\r
+            return null;\r
+        }\r
+\r
+        String classname = o.getClass().getName();\r
+\r
+        // Is this an array?\r
+        if (classname.charAt(0) != '[') {\r
+            return genericClone(o);\r
+        }\r
+\r
+        // Check if this is a 1D primitive array.\r
+        if (classname.charAt(1) != '[' && classname.charAt(1) != 'L') {\r
+            try {\r
+                // Some compilers (SuperCede, e.g.) still\r
+                // think you have to catch this...\r
+                if (false) {\r
+                    throw new CloneNotSupportedException();\r
+                }\r
+                switch (classname.charAt(1)) {\r
+                    case 'B':\r
+                        return ((byte[]) o).clone();\r
+                    case 'Z':\r
+                        return ((boolean[]) o).clone();\r
+                    case 'C':\r
+                        return ((char[]) o).clone();\r
+                    case 'S':\r
+                        return ((short[]) o).clone();\r
+                    case 'I':\r
+                        return ((int[]) o).clone();\r
+                    case 'J':\r
+                        return ((long[]) o).clone();\r
+                    case 'F':\r
+                        return ((float[]) o).clone();\r
+                    case 'D':\r
+                        return ((double[]) o).clone();\r
+                    default:\r
+                        System.err.println("Unknown primtive array class:" + classname);\r
+                        return null;\r
+\r
+                }\r
+            } catch (CloneNotSupportedException e) {\r
+            }\r
+        }\r
+\r
+        // Get the base type.\r
+        int ndim = 1;\r
+        while (classname.charAt(ndim) == '[') {\r
+            ndim += 1;\r
+        }\r
+        Class baseClass;\r
+        if (classname.charAt(ndim) != 'L') {\r
+            baseClass = getBaseClass(o);\r
+        } else {\r
+            try {\r
+                baseClass = Class.forName(classname.substring(ndim + 1, classname.length() - 1));\r
+            } catch (ClassNotFoundException e) {\r
+                System.err.println("Internal error: class definition inconsistency: " + classname);\r
+                return null;\r
+            }\r
+        }\r
+\r
+        // Allocate the array but make all but the first dimension 0.\r
+        int[] dims = new int[ndim];\r
+        dims[0] = Array.getLength(o);\r
+        for (int i = 1; i < ndim; i += 1) {\r
+            dims[i] = 0;\r
+        }\r
+\r
+        Object copy = ArrayFuncs.newInstance(baseClass, dims);\r
+\r
+        // Now fill in the next level down by recursion.\r
+        for (int i = 0; i < dims[0]; i += 1) {\r
+            Array.set(copy, i, deepClone(Array.get(o, i)));\r
+        }\r
+\r
+        return copy;\r
+    }\r
+\r
+    /** Clone an Object if possible.\r
+     *\r
+     * This method returns an Object which is a clone of the\r
+     * input object.  It checks if the method implements the\r
+     * Cloneable interface and then uses reflection to invoke\r
+     * the clone method.  This can't be done directly since\r
+     * as far as the compiler is concerned the clone method for\r
+     * Object is protected and someone could implement Cloneable but\r
+     * leave the clone method protected.  The cloning can fail in a\r
+     * variety of ways which are trapped so that it returns null instead.\r
+     * This method will generally create a shallow clone.  If you\r
+     * wish a deep copy of an array the method deepClone should be used.\r
+     *\r
+     * @param o The object to be cloned.\r
+     */\r
+    public static Object genericClone(Object o) {\r
+\r
+        if (!(o instanceof Cloneable)) {\r
+            return null;\r
+        }\r
+\r
+        Class[] argTypes = new Class[0];\r
+        Object[] args = new Object[0];\r
+        Class type = o.getClass();\r
+\r
+        try {\r
+            return type.getMethod("clone", argTypes).invoke(o, args);\r
+        } catch (Exception e) {\r
+            if (type.isArray()) {\r
+                return deepClone(o);\r
+            }\r
+            // Implements cloneable, but does not\r
+            // apparently make clone public.\r
+            return null;\r
+        }\r
+    }\r
+\r
+    /** Copy one array into another.\r
+     * This function copies the contents of one array\r
+     * into a previously allocated array.\r
+     * The arrays must agree in type and size.\r
+     * @param original The array to be copied.\r
+     * @param copy     The array to be copied into.  This\r
+     *                 array must already be fully allocated.\r
+     */\r
+    public static void copyArray(Object original, Object copy) {\r
+        String oname = original.getClass().getName();\r
+        String cname = copy.getClass().getName();\r
+\r
+        if (!oname.equals(cname)) {\r
+            return;\r
+        }\r
+\r
+        if (oname.charAt(0) != '[') {\r
+            return;\r
+        }\r
+\r
+        if (oname.charAt(1) == '[') {\r
+            Object[] x = (Object[]) original;\r
+            Object[] y = (Object[]) copy;\r
+            if (x.length != y.length) {\r
+                return;\r
+            }\r
+            for (int i = 0; i < x.length; i += 1) {\r
+                copyArray(x, y);\r
+            }\r
+        }\r
+        int len = Array.getLength(original);\r
+\r
+        System.arraycopy(original, 0, copy, 0, len);\r
+    }\r
+\r
+    /** Find the dimensions of an object.\r
+     *\r
+     * This method returns an integer array with the dimensions\r
+     * of the object o which should usually be an array.\r
+     *\r
+     * It returns an array of dimension 0 for scalar objects\r
+     * and it returns -1 for dimension which have not been allocated,\r
+     *  e.g., int[][][] x = new int[100][][]; should return [100,-1,-1].\r
+     *\r
+     * @param o The object to get the dimensions of.\r
+     */\r
+    public static int[] getDimensions(Object o) {\r
+\r
+        if (o == null) {\r
+            return null;\r
+        }\r
+\r
+        String classname = o.getClass().getName();\r
+\r
+        int ndim = 0;\r
+\r
+        while (classname.charAt(ndim) == '[') {\r
+            ndim += 1;\r
+        }\r
+\r
+        int[] dimens = new int[ndim];\r
+\r
+        for (int i = 0; i < ndim; i += 1) {\r
+            dimens[i] = -1;  // So that we can distinguish a null from a 0 length.\r
+        }\r
+\r
+        for (int i = 0; i < ndim; i += 1) {\r
+            dimens[i] = java.lang.reflect.Array.getLength(o);\r
+            if (dimens[i] == 0) {\r
+                return dimens;\r
+            }\r
+            if (i != ndim - 1) {\r
+                o = ((Object[]) o)[0];\r
+                if (o == null) {\r
+                    return dimens;\r
+                }\r
+            }\r
+        }\r
+        return dimens;\r
+    }\r
+\r
+    /** This routine returns the base array of a multi-dimensional\r
+     *  array.  I.e., a one-d array of whatever the array is composed\r
+     *  of.  Note that arrays are not guaranteed to be rectangular,\r
+     *  so this returns o[0][0]....\r
+     */\r
+    public static Object getBaseArray(Object o) {\r
+        String cname = o.getClass().getName();\r
+        if (cname.charAt(1) == '[') {\r
+            return getBaseArray(((Object[]) o)[0]);\r
+        } else {\r
+            return o;\r
+        }\r
+    }\r
+\r
+    /** This routine returns the base class of an object.  This is just\r
+     * the class of the object for non-arrays.\r
+     */\r
+    public static Class getBaseClass(Object o) {\r
+\r
+        if (o == null) {\r
+            return Void.TYPE;\r
+        }\r
+\r
+        String className = o.getClass().getName();\r
+\r
+        int dims = 0;\r
+        while (className.charAt(dims) == '[') {\r
+            dims += 1;\r
+        }\r
+\r
+        if (dims == 0) {\r
+            return o.getClass();\r
+        }\r
+\r
+        char c = className.charAt(dims);\r
+        for (int i = 0; i < PrimitiveInfo.suffixes.length; i += 1) {\r
+            if (c == PrimitiveInfo.suffixes[i]) {\r
+                return PrimitiveInfo.classes[i];\r
+            }\r
+        }\r
+\r
+        if (c == 'L') {\r
+            try {\r
+                return Class.forName(className.substring(dims + 1, className.length() - 1));\r
+            } catch (ClassNotFoundException e) {\r
+                return null;\r
+            }\r
+        }\r
+        return null;\r
+    }\r
+\r
+    /** This routine returns the size of the base element of an array.\r
+     * @param o The array object whose base length is desired.\r
+     * @return the size of the object in bytes, 0 if null, or\r
+     * -1 if not a primitive array.\r
+     */\r
+    public static int getBaseLength(Object o) {\r
+\r
+        if (o == null) {\r
+            return 0;\r
+        }\r
+\r
+        String className = o.getClass().getName();\r
+\r
+        int dims = 0;\r
+\r
+        while (className.charAt(dims) == '[') {\r
+            dims += 1;\r
+        }\r
+\r
+        if (dims == 0) {\r
+            return -1;\r
+        }\r
+\r
+        char c = className.charAt(dims);\r
+        for (int i = 0; i < PrimitiveInfo.suffixes.length; i += 1) {\r
+            if (c == PrimitiveInfo.suffixes[i]) {\r
+                return PrimitiveInfo.sizes[i];\r
+            }\r
+        }\r
+        return -1;\r
+    }\r
+\r
+    /** Create an array and populate it with a test pattern.\r
+     *\r
+     * @param baseType  The base type of the array.  This is expected to\r
+     *                  be a numeric type, but this is not checked.\r
+     * @param dims      The desired dimensions.\r
+     * @return An array object populated with a simple test pattern.\r
+     */\r
+    public static Object generateArray(Class baseType, int[] dims) {\r
+\r
+        // Generate an array and populate it with a test pattern of\r
+        // data.\r
+\r
+        Object x = ArrayFuncs.newInstance(baseType, dims);\r
+        testPattern(x, (byte) 0);\r
+        return x;\r
+    }\r
+\r
+    /** Just create a simple pattern cycling through valid byte values.\r
+     * We use bytes because they can be cast to any other numeric type.\r
+     * @param o      The array in which the test pattern is to be set.\r
+     * @param start  The value for the first element.\r
+     */\r
+    public static byte testPattern(Object o, byte start) {\r
+\r
+        int[] dims = getDimensions(o);\r
+        if (dims.length > 1) {\r
+            for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                start = testPattern(((Object[]) o)[i], start);\r
+            }\r
+\r
+        } else if (dims.length == 1) {\r
+            for (int i = 0; i < dims[0]; i += 1) {\r
+                java.lang.reflect.Array.setByte(o, i, start);\r
+                start += 1;\r
+            }\r
+        }\r
+        return start;\r
+    }\r
+\r
+    /** Generate a description of an array (presumed rectangular).\r
+     * @param o The array to be described.\r
+     */\r
+    public static String arrayDescription(Object o) {\r
+\r
+        Class base = getBaseClass(o);\r
+        if (base == Void.TYPE) {\r
+            return "NULL";\r
+        }\r
+\r
+        int[] dims = getDimensions(o);\r
+\r
+        StringBuffer desc = new StringBuffer();\r
+\r
+        // Note that all instances Class describing a given class are\r
+        // the same so we can use == here.\r
+        boolean found = false;\r
+\r
+        for (int i = 0; i < PrimitiveInfo.classes.length; i += 1) {\r
+            if (base == PrimitiveInfo.classes[i]) {\r
+                found = true;\r
+                desc.append(PrimitiveInfo.types[i]);\r
+                break;\r
+            }\r
+        }\r
+\r
+        if (!found) {\r
+            desc.append(base.getName());\r
+        }\r
+\r
+        if (dims != null) {\r
+            desc.append("[");\r
+            for (int i = 0; i < dims.length; i += 1) {\r
+                desc.append("" + dims[i]);\r
+                if (i < dims.length - 1) {\r
+                    desc.append("][");\r
+                }\r
+            }\r
+            desc.append("]");\r
+        }\r
+        return new String(desc);\r
+    }\r
+\r
+    /** Examine the structure of an array in detail.\r
+     * @param o The array to be examined.\r
+     */\r
+    public static void examinePrimitiveArray(Object o) {\r
+        String className = o.getClass().getName();\r
+\r
+        // If we have a two-d array, or if the array is a one-d array\r
+        // of Objects, then recurse over the next dimension.  We handle\r
+        // Object specially because each element could itself be an array.\r
+        if (className.substring(0, 2).equals("[[")\r
+                || className.equals("[Ljava.lang.Object;")) {\r
+            System.out.println("[");\r
+            for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                examinePrimitiveArray(((Object[]) o)[i]);\r
+            }\r
+            System.out.print("]");\r
+        } else if (className.charAt(0) != '[') {\r
+            System.out.println(className);\r
+        } else {\r
+            System.out.println("[" + java.lang.reflect.Array.getLength(o) + "]"\r
+                    + className.substring(1));\r
+        }\r
+    }\r
+\r
+    /** Given an array of arbitrary dimensionality return\r
+     * the array flattened into a single dimension.\r
+     * @param input The input array.\r
+     */\r
+    public static Object flatten(Object input) {\r
+\r
+        int[] dimens = getDimensions(input);\r
+        if (dimens.length <= 1) {\r
+            return input;\r
+        }\r
+        int size = 1;\r
+        for (int i = 0; i < dimens.length; i += 1) {\r
+            size *= dimens[i];\r
+        }\r
+\r
+        Object flat = ArrayFuncs.newInstance(getBaseClass(input), size);\r
+\r
+        if (size == 0) {\r
+            return flat;\r
+        }\r
+\r
+        int offset = 0;\r
+        doFlatten(input, flat, offset);\r
+        return flat;\r
+    }\r
+\r
+    /** This routine does the actually flattening of multi-dimensional\r
+     * arrays.\r
+     * @param input  The input array to be flattened.\r
+     * @param output The flattened array.\r
+     * @param offset The current offset within the output array.\r
+     * @return       The number of elements within the array.\r
+     */\r
+    protected static int doFlatten(Object input, Object output, int offset) {\r
+\r
+        String classname = input.getClass().getName();\r
+        if (classname.charAt(0) != '[') {\r
+            throw new RuntimeException("Attempt to flatten non-array");\r
+        }\r
+        int size = Array.getLength(input);\r
+\r
+        if (classname.charAt(1) != '[') {\r
+            System.arraycopy(input, 0, output, offset, size);\r
+            return size;\r
+        }\r
+        int total = 0;\r
+        Object[] xx = (Object[]) input;\r
+        for (int i = 0; i < size; i += 1) {\r
+            int len = doFlatten(xx[i], output, offset + total);\r
+            total += len;\r
+        }\r
+        return total;\r
+    }\r
+\r
+    /** Curl an input array up into a multi-dimensional array.\r
+     *\r
+     * @param input The one dimensional array to be curled.\r
+     * @param dimens The desired dimensions\r
+     * @return The curled array.\r
+     */\r
+    public static Object curl(Object input, int[] dimens) {\r
+\r
+        if (input == null) {\r
+            return null;\r
+        }\r
+        String classname = input.getClass().getName();\r
+        if (classname.charAt(0) != '[' || classname.charAt(1) == '[') {\r
+            throw new RuntimeException("Attempt to curl non-1D array");\r
+        }\r
+\r
+        int size = Array.getLength(input);\r
+\r
+        int test = 1;\r
+        for (int i = 0; i < dimens.length; i += 1) {\r
+            test *= dimens[i];\r
+        }\r
+\r
+        if (test != size) {\r
+            throw new RuntimeException("Curled array does not fit desired dimensions");\r
+        }\r
+\r
+        Class base = getBaseClass(input);\r
+\r
+        Object newArray = ArrayFuncs.newInstance(base, dimens);\r
+\r
+        int offset = 0;\r
+\r
+        doCurl(input, newArray, dimens, offset);\r
+        return newArray;\r
+\r
+    }\r
+\r
+    /** Do the curling of the 1-d to multi-d array.\r
+     * @param input  The 1-d array to be curled.\r
+     * @param output The multi-dimensional array to be filled.\r
+     * @param dimens The desired output dimensions.\r
+     * @param offset The current offset in the input array.\r
+     * @return       The number of elements curled.\r
+     */\r
+    protected static int doCurl(Object input, Object output,\r
+            int[] dimens, int offset) {\r
+\r
+        if (dimens.length == 1) {\r
+            System.arraycopy(input, offset, output, 0, dimens[0]);\r
+            return dimens[0];\r
+        }\r
+\r
+        int total = 0;\r
+        int[] xdimens = new int[dimens.length - 1];\r
+        for (int i = 1; i < dimens.length; i += 1) {\r
+            xdimens[i - 1] = dimens[i];\r
+        }\r
+\r
+        for (int i = 0; i < dimens[0]; i += 1) {\r
+            total += doCurl(input, ((Object[]) output)[i], xdimens, offset + total);\r
+        }\r
+        return total;\r
+    }\r
+\r
+    /** Create an array of a type given by new type with\r
+     * the dimensionality given in array.\r
+     * @param array   A possibly multidimensional array to be converted.\r
+     * @param newType The desired output type.  This should be one of the\r
+     *                class descriptors for primitive numeric data, e.g., double.type.\r
+     */\r
+    public static Object mimicArray(Object array, Class newType) {\r
+\r
+        String classname = array.getClass().getName();\r
+        if (classname.charAt(0) != '[') {\r
+            return null;\r
+        }\r
+\r
+        int dims = 1;\r
+\r
+        while (classname.charAt(dims) == '[') {\r
+            dims += 1;\r
+        }\r
+\r
+        Object mimic;\r
+\r
+        if (dims > 1) {\r
+\r
+            Object[] xarray = (Object[]) array;\r
+            int[] dimens = new int[dims];\r
+            dimens[0] = xarray.length;  // Leave other dimensions at 0.\r
+\r
+\r
+            mimic = ArrayFuncs.newInstance(newType, dimens);\r
+\r
+            for (int i = 0; i < xarray.length; i += 1) {\r
+                Object temp = mimicArray(xarray[i], newType);\r
+                ((Object[]) mimic)[i] = temp;\r
+            }\r
+\r
+        } else {\r
+            mimic = ArrayFuncs.newInstance(newType, Array.getLength(array));\r
+        }\r
+\r
+        return mimic;\r
+    }\r
+\r
+    /** Convert an array to a specified type.  This method supports conversions\r
+     * only among the primitive numeric types.\r
+     * @param array   A possibly multidimensional array to be converted.\r
+     * @param newType The desired output type.  This should be one of the\r
+     *                class descriptors for primitive numeric data, e.g., double.type.\r
+     * @param preserve If set, and the requested type is the same as the\r
+     *                  original, then the original is returned.\r
+     */\r
+    public static Object convertArray(Object array, Class newType, boolean reuse) {\r
+\r
+        if (getBaseClass(array) == newType && reuse) {\r
+            return array;\r
+        } else {\r
+            return convertArray(array, newType);\r
+        }\r
+    }\r
+\r
+    /** Convert an array to a specified type.  This method supports conversions\r
+     * only among the primitive numeric types.\r
+     * @param array   A possibly multidimensional array to be converted.\r
+     * @param newType The desired output type.  This should be one of the\r
+     *                class descriptors for primitive numeric data, e.g., double.type.\r
+     */\r
+    public static Object convertArray(Object array, Class newType) {\r
+\r
+        /* We break this up into two steps so that users\r
+         * can reuse an array many times and only allocate a\r
+         * new array when needed.\r
+         */\r
+\r
+        /* First create the full new array. */\r
+        Object mimic = mimicArray(array, newType);\r
+        if (mimic == null) {\r
+            return mimic;\r
+        }\r
+\r
+        /* Now copy the info into the new array */\r
+        copyInto(array, mimic);\r
+\r
+        return mimic;\r
+    }\r
+\r
+    /** Copy an array into an array of a different type.\r
+     *  The dimensions and dimensionalities of the two\r
+     *  arrays should be the same.\r
+     *  @param array  The original array.\r
+     *  @param mimic  The array mimicking the original.\r
+     */\r
+    public static void copyInto(Object array, Object mimic) {\r
+\r
+        String classname = array.getClass().getName();\r
+        if (classname.charAt(0) != '[') {\r
+            return;\r
+        }\r
+\r
+        /* Do multidimensional arrays recursively */\r
+        if (classname.charAt(1) == '[') {\r
+\r
+            for (int i = 0; i < ((Object[]) array).length; i += 1) {\r
+                copyInto(((Object[]) array)[i], ((Object[]) mimic)[i]);\r
+            }\r
+\r
+        } else {\r
+\r
+            byte[] xbarr;\r
+            short[] xsarr;\r
+            char[] xcarr;\r
+            int[] xiarr;\r
+            long[] xlarr;\r
+            float[] xfarr;\r
+            double[] xdarr;\r
+\r
+            Class base = getBaseClass(array);\r
+            Class newType = getBaseClass(mimic);\r
+\r
+            if (base == byte.class) {\r
+                byte[] barr = (byte[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, barr.length);\r
+\r
+                } else if (newType == short.class) {\r
+                    xsarr = (short[]) mimic;\r
+                    for (int i = 0; i < barr.length; i += 1) {\r
+                        xsarr[i] = barr[i];\r
+                    }\r
+\r
+                } else if (newType == char.class) {\r
+                    xcarr = (char[]) mimic;\r
+                    for (int i = 0; i < barr.length; i += 1) {\r
+                        xcarr[i] = (char) barr[i];\r
+                    }\r
+\r
+                } else if (newType == int.class) {\r
+                    xiarr = (int[]) mimic;\r
+                    for (int i = 0; i < barr.length; i += 1) {\r
+                        xiarr[i] = barr[i];\r
+                    }\r
+\r
+                } else if (newType == long.class) {\r
+                    xlarr = (long[]) mimic;\r
+                    for (int i = 0; i < barr.length; i += 1) {\r
+                        xlarr[i] = barr[i];\r
+                    }\r
+\r
+                } else if (newType == float.class) {\r
+                    xfarr = (float[]) mimic;\r
+                    for (int i = 0; i < barr.length; i += 1) {\r
+                        xfarr[i] = barr[i];\r
+                    }\r
+\r
+                } else if (newType == double.class) {\r
+                    xdarr = (double[]) mimic;\r
+                    for (int i = 0; i < barr.length; i += 1) {\r
+                        xdarr[i] = barr[i];\r
+                    }\r
+                }\r
+\r
+            } else if (base == short.class) {\r
+                short[] sarr = (short[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    xbarr = (byte[]) mimic;\r
+                    for (int i = 0; i < sarr.length; i += 1) {\r
+                        xbarr[i] = (byte) sarr[i];\r
+                    }\r
+\r
+                } else if (newType == short.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, sarr.length);\r
+\r
+                } else if (newType == char.class) {\r
+                    xcarr = (char[]) mimic;\r
+                    for (int i = 0; i < sarr.length; i += 1) {\r
+                        xcarr[i] = (char) sarr[i];\r
+                    }\r
+\r
+                } else if (newType == int.class) {\r
+                    xiarr = (int[]) mimic;\r
+                    for (int i = 0; i < sarr.length; i += 1) {\r
+                        xiarr[i] = sarr[i];\r
+                    }\r
+\r
+                } else if (newType == long.class) {\r
+                    xlarr = (long[]) mimic;\r
+                    for (int i = 0; i < sarr.length; i += 1) {\r
+                        xlarr[i] = sarr[i];\r
+                    }\r
+\r
+                } else if (newType == float.class) {\r
+                    xfarr = (float[]) mimic;\r
+                    for (int i = 0; i < sarr.length; i += 1) {\r
+                        xfarr[i] = sarr[i];\r
+                    }\r
+\r
+                } else if (newType == double.class) {\r
+                    xdarr = (double[]) mimic;\r
+                    for (int i = 0; i < sarr.length; i += 1) {\r
+                        xdarr[i] = sarr[i];\r
+                    }\r
+                }\r
+\r
+            } else if (base == char.class) {\r
+                char[] carr = (char[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    xbarr = (byte[]) mimic;\r
+                    for (int i = 0; i < carr.length; i += 1) {\r
+                        xbarr[i] = (byte) carr[i];\r
+                    }\r
+\r
+                } else if (newType == short.class) {\r
+                    xsarr = (short[]) mimic;\r
+                    for (int i = 0; i < carr.length; i += 1) {\r
+                        xsarr[i] = (short) carr[i];\r
+                    }\r
+\r
+                } else if (newType == char.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, carr.length);\r
+\r
+                } else if (newType == int.class) {\r
+                    xiarr = (int[]) mimic;\r
+                    for (int i = 0; i < carr.length; i += 1) {\r
+                        xiarr[i] = carr[i];\r
+                    }\r
+\r
+                } else if (newType == long.class) {\r
+                    xlarr = (long[]) mimic;\r
+                    for (int i = 0; i < carr.length; i += 1) {\r
+                        xlarr[i] = carr[i];\r
+                    }\r
+\r
+                } else if (newType == float.class) {\r
+                    xfarr = (float[]) mimic;\r
+                    for (int i = 0; i < carr.length; i += 1) {\r
+                        xfarr[i] = carr[i];\r
+                    }\r
+\r
+                } else if (newType == double.class) {\r
+                    xdarr = (double[]) mimic;\r
+                    for (int i = 0; i < carr.length; i += 1) {\r
+                        xdarr[i] = carr[i];\r
+                    }\r
+                }\r
+\r
+            } else if (base == int.class) {\r
+                int[] iarr = (int[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    xbarr = (byte[]) mimic;\r
+                    for (int i = 0; i < iarr.length; i += 1) {\r
+                        xbarr[i] = (byte) iarr[i];\r
+                    }\r
+\r
+                } else if (newType == short.class) {\r
+                    xsarr = (short[]) mimic;\r
+                    for (int i = 0; i < iarr.length; i += 1) {\r
+                        xsarr[i] = (short) iarr[i];\r
+                    }\r
+\r
+                } else if (newType == char.class) {\r
+                    xcarr = (char[]) mimic;\r
+                    for (int i = 0; i < iarr.length; i += 1) {\r
+                        xcarr[i] = (char) iarr[i];\r
+                    }\r
+\r
+                } else if (newType == int.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, iarr.length);\r
+\r
+                } else if (newType == long.class) {\r
+                    xlarr = (long[]) mimic;\r
+                    for (int i = 0; i < iarr.length; i += 1) {\r
+                        xlarr[i] = iarr[i];\r
+                    }\r
+\r
+                } else if (newType == float.class) {\r
+                    xfarr = (float[]) mimic;\r
+                    for (int i = 0; i < iarr.length; i += 1) {\r
+                        xfarr[i] = iarr[i];\r
+                    }\r
+\r
+                } else if (newType == double.class) {\r
+                    xdarr = (double[]) mimic;\r
+                    for (int i = 0; i < iarr.length; i += 1) {\r
+                        xdarr[i] = iarr[i];\r
+                    }\r
+                }\r
+\r
+\r
+            } else if (base == long.class) {\r
+                long[] larr = (long[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    xbarr = (byte[]) mimic;\r
+                    for (int i = 0; i < larr.length; i += 1) {\r
+                        xbarr[i] = (byte) larr[i];\r
+                    }\r
+\r
+                } else if (newType == short.class) {\r
+                    xsarr = (short[]) mimic;\r
+                    for (int i = 0; i < larr.length; i += 1) {\r
+                        xsarr[i] = (short) larr[i];\r
+                    }\r
+\r
+                } else if (newType == char.class) {\r
+                    xcarr = (char[]) mimic;\r
+                    for (int i = 0; i < larr.length; i += 1) {\r
+                        xcarr[i] = (char) larr[i];\r
+                    }\r
+\r
+                } else if (newType == int.class) {\r
+                    xiarr = (int[]) mimic;\r
+                    for (int i = 0; i < larr.length; i += 1) {\r
+                        xiarr[i] = (int) larr[i];\r
+                    }\r
+\r
+                } else if (newType == long.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, larr.length);\r
+\r
+                } else if (newType == float.class) {\r
+                    xfarr = (float[]) mimic;\r
+                    for (int i = 0; i < larr.length; i += 1) {\r
+                        xfarr[i] = (float) larr[i];\r
+                    }\r
+\r
+                } else if (newType == double.class) {\r
+                    xdarr = (double[]) mimic;\r
+                    for (int i = 0; i < larr.length; i += 1) {\r
+                        xdarr[i] = (double) larr[i];\r
+                    }\r
+                }\r
+\r
+            } else if (base == float.class) {\r
+                float[] farr = (float[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    xbarr = (byte[]) mimic;\r
+                    for (int i = 0; i < farr.length; i += 1) {\r
+                        xbarr[i] = (byte) farr[i];\r
+                    }\r
+\r
+                } else if (newType == short.class) {\r
+                    xsarr = (short[]) mimic;\r
+                    for (int i = 0; i < farr.length; i += 1) {\r
+                        xsarr[i] = (short) farr[i];\r
+                    }\r
+\r
+                } else if (newType == char.class) {\r
+                    xcarr = (char[]) mimic;\r
+                    for (int i = 0; i < farr.length; i += 1) {\r
+                        xcarr[i] = (char) farr[i];\r
+                    }\r
+\r
+                } else if (newType == int.class) {\r
+                    xiarr = (int[]) mimic;\r
+                    for (int i = 0; i < farr.length; i += 1) {\r
+                        xiarr[i] = (int) farr[i];\r
+                    }\r
+\r
+                } else if (newType == long.class) {\r
+                    xlarr = (long[]) mimic;\r
+                    for (int i = 0; i < farr.length; i += 1) {\r
+                        xlarr[i] = (long) farr[i];\r
+                    }\r
+\r
+                } else if (newType == float.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, farr.length);\r
+\r
+                } else if (newType == double.class) {\r
+                    xdarr = (double[]) mimic;\r
+                    for (int i = 0; i < farr.length; i += 1) {\r
+                        xdarr[i] = farr[i];\r
+                    }\r
+                }\r
+\r
+\r
+            } else if (base == double.class) {\r
+                double[] darr = (double[]) array;\r
+\r
+                if (newType == byte.class) {\r
+                    xbarr = (byte[]) mimic;\r
+                    for (int i = 0; i < darr.length; i += 1) {\r
+                        xbarr[i] = (byte) darr[i];\r
+                    }\r
+\r
+                } else if (newType == short.class) {\r
+                    xsarr = (short[]) mimic;\r
+                    for (int i = 0; i < darr.length; i += 1) {\r
+                        xsarr[i] = (short) darr[i];\r
+                    }\r
+\r
+                } else if (newType == char.class) {\r
+                    xcarr = (char[]) mimic;\r
+                    for (int i = 0; i < darr.length; i += 1) {\r
+                        xcarr[i] = (char) darr[i];\r
+                    }\r
+\r
+                } else if (newType == int.class) {\r
+                    xiarr = (int[]) mimic;\r
+                    for (int i = 0; i < darr.length; i += 1) {\r
+                        xiarr[i] = (int) darr[i];\r
+                    }\r
+\r
+                } else if (newType == long.class) {\r
+                    xlarr = (long[]) mimic;\r
+                    for (int i = 0; i < darr.length; i += 1) {\r
+                        xlarr[i] = (long) darr[i];\r
+                    }\r
+\r
+                } else if (newType == float.class) {\r
+                    xfarr = (float[]) mimic;\r
+                    for (int i = 0; i < darr.length; i += 1) {\r
+                        xfarr[i] = (float) darr[i];\r
+                    }\r
+\r
+                } else if (newType == double.class) {\r
+                    System.arraycopy(array, 0, mimic, 0, darr.length);\r
+                }\r
+            }\r
+        }\r
+\r
+        return;\r
+\r
+    }\r
+\r
+    /** Allocate an array dynamically. The Array.newInstance method\r
+     *  does not throw an error when there is insufficient memory\r
+     *  and silently returns a null.\r
+     *  @param cl      The class of the array.\r
+     *  @param dim      The dimension of the array.\r
+     *  @return The allocated array.\r
+     *  @throws An OutOfMemoryError if insufficient space is available.\r
+     */\r
+    public static Object newInstance(Class cl, int dim) {\r
+\r
+        Object o = Array.newInstance(cl, dim);\r
+        if (o == null) {\r
+            String desc = cl + "[" + dim + "]";\r
+            throw new OutOfMemoryError("Unable to allocate array: " + desc);\r
+        }\r
+        return o;\r
+    }\r
+\r
+    /** Allocate an array dynamically. The Array.newInstance method\r
+     *  does not throw an error and silently returns a null.\r
+     *\r
+     *  @param cl      The class of the array.\r
+     *  @param dims     The dimensions of the array.\r
+     *  @return The allocated array.\r
+     *  @throws An OutOfMemoryError if insufficient space is available.\r
+     */\r
+    public static Object newInstance(Class cl, int[] dims) {\r
+\r
+        if (dims.length == 0) {\r
+            // Treat a scalar as a 1-d array of length 1\r
+            dims = new int[]{1};\r
+        }\r
+\r
+        Object o = Array.newInstance(cl, dims);\r
+        if (o == null) {\r
+            String desc = cl + "[";\r
+            String comma = "";\r
+            for (int i = 0; i < dims.length; i += 1) {\r
+                desc += comma + dims[i];\r
+                comma = ",";\r
+            }\r
+            desc += "]";\r
+            throw new OutOfMemoryError("Unable to allocate array: " + desc);\r
+        }\r
+        return o;\r
+    }\r
+\r
+    /** Are two objects equal?  Arrays have the standard object equals\r
+     *  method which only returns true if the two object are the same.\r
+     *  This method returns true if every element of the arrays match.\r
+     *  The inputs may be of any dimensionality.  The dimensionality\r
+     *  and dimensions of the arrays must match as well as any elements.\r
+     *  If the elements are non-primitive. non-array objects, then the\r
+     *  equals method is called for each element.\r
+     *  If both elements are multi-dimensional arrays, then\r
+     *  the method recurses.\r
+     */\r
+    public static boolean arrayEquals(Object x, Object y) {\r
+        return arrayEquals(x, y, 0, 0);\r
+    }\r
+\r
+    /** Are two objects equal?  Arrays have the standard object equals\r
+     *  method which only returns true if the two object are the same.\r
+     *  This method returns true if every element of the arrays match.\r
+     *  The inputs may be of any dimensionality.  The dimensionality\r
+     *  and dimensions of the arrays must match as well as any elements.\r
+     *  If the elements are non-primitive. non-array objects, then the\r
+     *  equals method is called for each element.\r
+     *  If both elements are multi-dimensional arrays, then\r
+     *  the method recurses.\r
+     */\r
+    public static boolean arrayEquals(Object x, Object y, double tolf, double told) {\r
+\r
+        // Handle the special cases first.\r
+        // We treat null == null so that two object arrays\r
+        // can match if they have matching null elements.\r
+        if (x == null && y == null) {\r
+            return true;\r
+        }\r
+\r
+        if (x == null || y == null) {\r
+            return false;\r
+        }\r
+\r
+        Class xClass = x.getClass();\r
+        Class yClass = y.getClass();\r
+\r
+        if (xClass != yClass) {\r
+            return false;\r
+        }\r
+\r
+        if (!xClass.isArray()) {\r
+            return x.equals(y);\r
+\r
+        } else {\r
+            if (xClass.equals(int[].class)) {\r
+                return Arrays.equals((int[]) x, (int[]) y);\r
+\r
+            } else if (xClass.equals(double[].class)) {\r
+                if (told == 0) {\r
+                    return Arrays.equals((double[]) x, (double[]) y);\r
+                } else {\r
+                    return doubleArrayEquals((double[]) x, (double[]) y, told);\r
+                }\r
+\r
+            } else if (xClass.equals(long[].class)) {\r
+                return Arrays.equals((long[]) x, (long[]) y);\r
+\r
+            } else if (xClass.equals(float[].class)) {\r
+                if (tolf == 0) {\r
+                    return Arrays.equals((float[]) x, (float[]) y);\r
+                } else {\r
+                    return floatArrayEquals((float[]) x, (float[]) y, (float) tolf);\r
+                }\r
+\r
+            } else if (xClass.equals(byte[].class)) {\r
+                return Arrays.equals((byte[]) x, (byte[]) y);\r
+\r
+            } else if (xClass.equals(short[].class)) {\r
+                return Arrays.equals((short[]) x, (short[]) y);\r
+\r
+            } else if (xClass.equals(char[].class)) {\r
+                return Arrays.equals((char[]) x, (char[]) y);\r
+\r
+            } else if (xClass.equals(boolean[].class)) {\r
+                return Arrays.equals((boolean[]) x, (boolean[]) y);\r
+\r
+            } else {\r
+                // Non-primitive and multidimensional arrays can be\r
+                // cast to Object[]\r
+                Object[] xo = (Object[]) x;\r
+                Object[] yo = (Object[]) y;\r
+                if (xo.length != yo.length) {\r
+                    return false;\r
+                }\r
+                for (int i = 0; i < xo.length; i += 1) {\r
+                    if (!arrayEquals(xo[i], yo[i], tolf, told)) {\r
+                        return false;\r
+                    }\r
+                }\r
+\r
+                return true;\r
+\r
+            }\r
+        }\r
+    }\r
+\r
+    /** Compare two double arrays using a given tolerance */\r
+    public static boolean doubleArrayEquals(double[] x, double[] y, double tol) {\r
+\r
+        for (int i = 0; i < x.length; i += 1) {\r
+            if (x[i] == 0) {\r
+                return y[i] == 0;\r
+            }\r
+            if (Math.abs((y[i] - x[i]) / x[i]) > tol) {\r
+                return false;\r
+            }\r
+        }\r
+        return true;\r
+    }\r
+\r
+    /** Compare two float arrays using a given tolerance */\r
+    public static boolean floatArrayEquals(float[] x, float[] y, float tol) {\r
+\r
+        for (int i = 0; i < x.length; i += 1) {\r
+            if (x[i] == 0) {\r
+                return y[i] == 0;\r
+            }\r
+            if (Math.abs((y[i] - x[i]) / x[i]) > tol) {\r
+                return false;\r
+            }\r
+        }\r
+        return true;\r
+    }\r
+\r
+    /** Dump an array on the given print steam */\r
+    public static void dumpArray(java.io.PrintStream p, Object arr) {\r
+        // Get the dimensionality and then dump.\r
+        if (arr == null) {\r
+            p.print("null ");\r
+        } else {\r
+            Class nm = arr.getClass();\r
+            if (nm.isArray()) {\r
+                p.print("[");\r
+                for (int i = 0; i < java.lang.reflect.Array.getLength(arr); i += 1) {\r
+                    dumpArray(p, java.lang.reflect.Array.get(arr, i));\r
+                }\r
+                p.print("]\n");\r
+            } else {\r
+                p.print(" " + arr.toString() + " ");\r
+            }\r
+        }\r
+    }\r
+}\r
diff --git a/src/nom/tam/util/AsciiFuncs.java b/src/nom/tam/util/AsciiFuncs.java
new file mode 100644 (file)
index 0000000..a74ea16
--- /dev/null
@@ -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 (file)
index 0000000..e8347cf
--- /dev/null
@@ -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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.]
+ * <p>
+ * 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 (file)
index 0000000..2a5890a
--- /dev/null
@@ -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.
+ * <p>
+ * 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).
+ * <p>
+ * 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 (file)
index 0000000..289ec15
--- /dev/null
@@ -0,0 +1,1150 @@
+package nom.tam.util;\r
+\r
+/* Copyright: Thomas McGlynn 1997-1999.\r
+ * This code may be used for any purpose, non-commercial\r
+ * or commercial so long as this copyright notice is retained\r
+ * in the source code or included in or referred to in any\r
+ * derived software.\r
+ */\r
+/** This class is intended for high performance I/O in scientific applications.\r
+ * It adds buffering to the RandomAccessFile and also\r
+ * provides efficient handling of arrays.  Primitive arrays\r
+ * may be written using a single method call.  Large buffers\r
+ * are used to minimize synchronization overheads since methods\r
+ * of this class are not synchronized.\r
+ * <p>\r
+ * Note that although this class supports most of the\r
+ * contract of RandomAccessFile it does not (and can not)\r
+ * extend that class since many of the methods of\r
+ * RandomAccessFile are final.  In practice this\r
+ * method works much like the StreamFilter classes.\r
+ * All methods are implemented in this class but\r
+ * some are simply delegated to an underlying RandomAccessFile member.\r
+ * <p>\r
+ * Testing and timing routines are available in\r
+ * the nom.tam.util.test.BufferedFileTester class.\r
+ *\r
+ *  Version 1.1 October 12, 2000: Fixed handling of EOF in array reads\r
+ *  so that a partial array will be returned when an EOF is detected.\r
+ *  Excess bytes that cannot be used to construct array elements will\r
+ *  be discarded (e.g., if there are 2 bytes left and the user is\r
+ *  reading an int array).\r
+ *  Version 1.2 December 8, 2002: Added getChannel method.\r
+ *  Version 1.3 March 2, 2007:  Added File based constructors.\r
+ *  Version 1.4 July 20, 2009: Added support for >2G Object reads.\r
+ *     This is still a bit problematic in that we do not support\r
+ *     primitive arrays larger than 2 GB/atomsize.  However except\r
+ *     in the case of bytes this is not currently a major issue.\r
+ *\r
+ */\r
+import java.io.RandomAccessFile;\r
+import java.io.File;\r
+import java.io.FileDescriptor;\r
+import java.io.IOException;\r
+import java.io.EOFException;\r
+\r
+public class BufferedFile\r
+        implements ArrayDataInput, ArrayDataOutput, RandomAccess {\r
+\r
+    /** The current offset into the buffer */\r
+    private int bufferOffset;\r
+    /** The number of valid characters in the buffer */\r
+    private int bufferLength;\r
+    /** The declared length of the buffer array */\r
+    private int bufferSize;\r
+    /** Counter used in reading arrays */\r
+    private long primitiveArrayCount;\r
+    /** The data buffer. */\r
+    private byte[] buffer;\r
+    /** The underlying access to the file system */\r
+    private RandomAccessFile raf;\r
+    /** The offset of the beginning of the current buffer */\r
+    private long fileOffset;\r
+    /** Is the buffer being used for input or output */\r
+    private boolean doingInput;\r
+\r
+    /** Create a read-only buffered file */\r
+    public BufferedFile(String filename) throws IOException {\r
+        this(filename, "r", 32768);\r
+    }\r
+\r
+    /** Create a buffered file with the given mode.\r
+     *  @param filename The file to be accessed.\r
+     *  @param mode     A string composed of "r" and "w" for\r
+     *                  read and write access.\r
+     */\r
+    public BufferedFile(String filename, String mode) throws IOException {\r
+        this(filename, mode, 32768);\r
+    }\r
+\r
+    /** Create a buffered file from a File descriptor */\r
+    public BufferedFile(File file) throws IOException {\r
+        this(file, "r", 32768);\r
+    }\r
+\r
+    /** Create a buffered file from a File descriptor */\r
+    public BufferedFile(File file, String mode) throws IOException {\r
+        this(file, mode, 32768);\r
+    }\r
+\r
+    /** Create a buffered file with the given mode and a specified\r
+     *  buffer size.\r
+     *  @param filename The file to be accessed.\r
+     *  @param mode     A string composed of "r" and "w" indicating\r
+     *                  read or write access.\r
+     *  @param buffer   The buffer size to be used. This should be\r
+     *                  substantially larger than 100 bytes and\r
+     *                  defaults to 32768 bytes in the other\r
+     *                  constructors.\r
+     */\r
+    public BufferedFile(String filename, String mode, int bufferSize) throws IOException {\r
+\r
+        File file = new File(filename);\r
+        initialize(file, mode, bufferSize);\r
+    }\r
+\r
+    /** Create a buffered file from a file descriptor */\r
+    public BufferedFile(File file, String mode, int bufferSize) throws IOException {\r
+        initialize(file, mode, bufferSize);\r
+    }\r
+\r
+    protected void initialize(File file, String mode, int bufferSize) throws IOException {\r
+\r
+        raf = new RandomAccessFile(file, mode);\r
+        buffer = new byte[bufferSize];\r
+        bufferOffset = 0;\r
+        bufferLength = 0;\r
+        fileOffset = 0;\r
+        this.bufferSize = bufferSize;\r
+\r
+    }\r
+\r
+    /** Create a buffered file using a mapped\r
+\r
+\r
+    /** Read an entire byte array.\r
+     *  Note BufferedFile will return a partially filled array\r
+     *  only at an end-of-file.\r
+     *  @param buf The array to be filled.\r
+     */\r
+    public int read(byte[] buf) throws IOException {\r
+        return read(buf, 0, buf.length);\r
+    }\r
+\r
+    /** Read into a segment of a byte array.\r
+     *  @param buf    The array to be filled.\r
+     *  @param offset The starting location for input.\r
+     *  @param length The number of bytes to be read.  Fewer bytes\r
+     *                will be read if an EOF is reached.\r
+     */\r
+    public int read(byte[] buf, int offset, int len) throws IOException {\r
+\r
+        checkBuffer(-1);\r
+        int total = 0;\r
+\r
+        // Ensure that the entire buffer is read.\r
+        while (len > 0) {\r
+\r
+            if (bufferOffset < bufferLength) {\r
+\r
+                int get = len;\r
+                if (bufferOffset + get > bufferLength) {\r
+                    get = bufferLength - bufferOffset;\r
+                }\r
+                System.arraycopy(buffer, bufferOffset, buf, offset, get);\r
+                len -= get;\r
+                bufferOffset += get;\r
+                offset += get;\r
+                total += get;\r
+                continue;\r
+\r
+            } else {\r
+\r
+                // This might be pretty long, but we know that the\r
+                // old buffer is exhausted.\r
+                try {\r
+                    if (len > bufferSize) {\r
+                        checkBuffer(bufferSize);\r
+                    } else {\r
+                        checkBuffer(len);\r
+                    }\r
+                } catch (EOFException e) {\r
+                    if (bufferLength > 0) {\r
+                        System.arraycopy(buffer, 0, buf, offset, bufferLength);\r
+                        total += bufferLength;\r
+                        bufferLength = 0;\r
+                    }\r
+                    if (total == 0) {\r
+                        throw e;\r
+                    } else {\r
+                        return total;\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        return total;\r
+    }\r
+\r
+    /** This should only be used when a small number of\r
+     * bytes is required (substantially smaller than\r
+     * bufferSize.\r
+     */\r
+    private void checkBuffer(int needBytes) throws IOException {\r
+\r
+        // Check if the buffer has some pending output.\r
+        if (!doingInput && bufferOffset > 0) {\r
+            flush();\r
+        }\r
+        doingInput = true;\r
+\r
+        if (bufferOffset + needBytes < bufferLength) {\r
+            return;\r
+        }\r
+        /* Move the last few bytes to the beginning of the buffer\r
+         * and read in enough data to fill the current demand.\r
+         */\r
+        int len = bufferLength - bufferOffset;\r
+\r
+        /* Note that new location that the beginning of the buffer\r
+         * corresponds to.\r
+         */\r
+        fileOffset += bufferOffset;\r
+        if (len > 0) {\r
+            System.arraycopy(buffer, bufferOffset, buffer, 0, len);\r
+        }\r
+        needBytes -= len;\r
+        bufferLength = len;\r
+        bufferOffset = 0;\r
+\r
+        while (needBytes > 0) {\r
+            len = raf.read(buffer, bufferLength, bufferSize - bufferLength);\r
+            if (len < 0) {\r
+                throw new EOFException();\r
+            }\r
+            needBytes -= len;\r
+            bufferLength += len;\r
+        }\r
+    }\r
+\r
+    /** Read a byte */\r
+    public int read() throws IOException {\r
+        checkBuffer(1);\r
+        bufferOffset += 1;\r
+        return buffer[bufferOffset - 1];\r
+    }\r
+\r
+    /** Skip from the current position.\r
+     *  @param offset The number of bytes from the\r
+     *                current position.  This may\r
+     *                be negative.\r
+     */\r
+    public long skip(long offset) throws IOException {\r
+\r
+        if (offset > 0 && fileOffset + bufferOffset + offset > raf.length()) {\r
+            offset = raf.length() - fileOffset - bufferOffset;\r
+            seek(raf.length());\r
+        } else if (fileOffset + bufferOffset + offset < 0) {\r
+            offset = -(fileOffset + bufferOffset);\r
+            seek(0);\r
+        } else {\r
+            seek(fileOffset + bufferOffset + offset);\r
+        }\r
+        return offset;\r
+    }\r
+\r
+    /** Move to the current offset from the beginning of the file.\r
+     *  A user may move past the end of file but this\r
+     *  does not extend the file unless data is written there.\r
+     */\r
+    public void seek(long offsetFromStart) throws IOException {\r
+\r
+        if (!doingInput) {\r
+            // Have to flush before a seek...\r
+            flush();\r
+        }\r
+\r
+        // Are we within the current buffer?\r
+        if (fileOffset <= offsetFromStart && offsetFromStart < fileOffset + bufferLength) {\r
+            bufferOffset = (int) (offsetFromStart - fileOffset);\r
+        } else {\r
+\r
+            // Seek to the desired location.\r
+            if (offsetFromStart < 0) {\r
+                offsetFromStart = 0;\r
+            }\r
+\r
+            fileOffset = offsetFromStart;\r
+            raf.seek(fileOffset);\r
+\r
+            // Invalidate the current buffer.\r
+            bufferLength = 0;\r
+            bufferOffset = 0;\r
+        }\r
+    }\r
+\r
+    /** Read a boolean\r
+     * @return a boolean generated from the next\r
+     *                     byte in the input.\r
+     */\r
+    public boolean readBoolean() throws IOException {\r
+        return convertToBoolean();\r
+    }\r
+\r
+    /** Get a boolean from the buffer */\r
+    private boolean convertToBoolean() throws IOException {\r
+        checkBuffer(1);\r
+        bufferOffset += 1;\r
+        return buffer[bufferOffset - 1] == 1;\r
+    }\r
+\r
+    /** Read a byte\r
+     *  @return the next byte in the input.\r
+     */\r
+    public byte readByte() throws IOException {\r
+        checkBuffer(1);\r
+        bufferOffset += 1;\r
+        return buffer[bufferOffset - 1];\r
+    }\r
+\r
+    /** Read an unsigned byte.\r
+     *  @return the unsigned value of the next byte as\r
+     *          an integer.\r
+     */\r
+    public int readUnsignedByte() throws IOException {\r
+        checkBuffer(1);\r
+        bufferOffset += 1;\r
+        return buffer[bufferOffset - 1] | 0x00ff;\r
+    }\r
+\r
+    /** Read an int\r
+     *  @return an integer read from the input.\r
+     */\r
+    public int readInt() throws IOException {\r
+        return convertToInt();\r
+    }\r
+\r
+    /** Get an integer value from the buffer */\r
+    private int convertToInt() throws IOException {\r
+        checkBuffer(4);\r
+        int x = bufferOffset;\r
+        int i = buffer[x] << 24 | (buffer[x + 1] & 0xFF) << 16 | (buffer[x + 2] & 0xFF) << 8 | (buffer[x + 3] & 0xFF);\r
+        bufferOffset += 4;\r
+        return i;\r
+    }\r
+\r
+    /** Read a short\r
+     *  @return a short read from the input.\r
+     */\r
+    public short readShort() throws IOException {\r
+        return convertToShort();\r
+    }\r
+\r
+    /** Get a short from the buffer */\r
+    private short convertToShort() throws IOException {\r
+        checkBuffer(2);\r
+        short s = (short) (buffer[bufferOffset] << 8 | (buffer[bufferOffset + 1] & 0xFF));\r
+        bufferOffset += 2;\r
+        return s;\r
+    }\r
+\r
+    /** Read an unsigned short.\r
+     *  @return an unsigned short value as an integer.\r
+     */\r
+    public int readUnsignedShort() throws IOException {\r
+        return readShort() & 0xFFFF;\r
+    }\r
+\r
+    /** Read a char\r
+     *  @return a char read from the input.\r
+     */\r
+    public char readChar() throws IOException {\r
+        return convertToChar();\r
+    }\r
+\r
+    /** Get a char from the buffer */\r
+    private char convertToChar() throws IOException {\r
+        checkBuffer(2);\r
+        char c = (char) (buffer[bufferOffset] << 8 | (buffer[bufferOffset + 1] & 0xFF));\r
+        bufferOffset += 2;\r
+        return c;\r
+    }\r
+\r
+    /** Read a long.\r
+     *  @return a long value read from the input.\r
+     */\r
+    public long readLong() throws IOException {\r
+        return convertToLong();\r
+    }\r
+\r
+    /** Get a long value from the buffer */\r
+    private long convertToLong() throws IOException {\r
+        checkBuffer(8);\r
+        int x = bufferOffset;\r
+\r
+        int i1 = buffer[x] << 24 | (buffer[x + 1] & 0xFF) << 16 | (buffer[x + 2] & 0xFF) << 8 | (buffer[x + 3] & 0xFF);\r
+        int i2 = buffer[x + 4] << 24 | (buffer[x + 5] & 0xFF) << 16 | (buffer[x + 6] & 0xFF) << 8 | (buffer[x + 7] & 0xFF);\r
+        bufferOffset += 8;\r
+\r
+        return (((long) i1) << 32) | (((long) i2) & 0x00000000ffffffffL);\r
+    }\r
+\r
+    /** Read a float.\r
+     *  @return a float value read from the input.\r
+     */\r
+    public float readFloat() throws IOException {\r
+        return Float.intBitsToFloat(convertToInt());\r
+    }\r
+\r
+    /** Read a double.\r
+     *  @return a double value read from the input.\r
+     */\r
+    public double readDouble() throws IOException {\r
+        return Double.longBitsToDouble(convertToLong());\r
+    }\r
+\r
+    /** Read a byte array fully.\r
+     *  Since the read method of this class reads an entire\r
+     *  buffer, the only difference with readFully is that\r
+     *  readFully will signal an EOF if the buffer cannot be filled.\r
+     */\r
+    public void readFully(byte[] b) throws IOException {\r
+        readFully(b, 0, b.length);\r
+    }\r
+\r
+    /** Read a byte array fully.\r
+     *  Since the read method of this class reads an entire\r
+     *  buffer, the only difference with readFully is that\r
+     *  readFully will signal an EOF if the buffer cannot be filled.\r
+     */\r
+    public void readFully(byte[] b, int off, int len) throws IOException {\r
+\r
+        if (off < 0 || len < 0 || off + len > b.length) {\r
+            throw new IOException("Attempt to read outside byte array");\r
+        }\r
+\r
+        if (read(b, off, len) < len) {\r
+            throw new EOFException();\r
+        }\r
+    }\r
+\r
+    /** Skip the number of bytes.\r
+     *  This differs from the skip method in that\r
+     *  it will throw an EOF if a forward skip cannot\r
+     *  be fully accomplished...  (However that isn't\r
+     *  supposed to happen with a random access file,\r
+     *  so there is probably no operational difference).\r
+     */\r
+    public int skipBytes(int toSkip) throws IOException {\r
+        return (int) skipBytes((long) toSkip);\r
+    }\r
+\r
+    public long skipBytes(long toSkip) throws IOException {\r
+\r
+        // Note that we allow negative skips...\r
+        if (skip(toSkip) < toSkip) {\r
+            throw new EOFException();\r
+        } else {\r
+            return toSkip;\r
+        }\r
+    }\r
+\r
+    /** Read a string encoded as a UTF.\r
+     *  @return the string.\r
+     */\r
+    public String readUTF() throws IOException {\r
+        checkBuffer(-1);\r
+        raf.seek(fileOffset + bufferOffset);\r
+        String utf = raf.readUTF();\r
+        fileOffset = raf.getFilePointer();\r
+\r
+        // Invalidate the buffer.\r
+        bufferLength = 0;\r
+        bufferOffset = 0;\r
+\r
+        return utf;\r
+    }\r
+\r
+    /** Read a line of input.\r
+     *  @return the next line.\r
+     */\r
+    public String readLine() throws IOException {\r
+\r
+        checkBuffer(-1);\r
+        raf.seek(fileOffset + bufferOffset);\r
+        String line = raf.readLine();\r
+        fileOffset = raf.getFilePointer();\r
+\r
+        // Invalidate the buffer.\r
+        bufferLength = 0;\r
+        bufferOffset = 0;\r
+\r
+        return line;\r
+    }\r
+\r
+    /** This routine provides efficient reading of arrays of any primitive type.\r
+     *  @deprecated  The readLArray(Object) routine should be used to\r
+     *               ensure that large arrays which read more than\r
+     *               two-gigabytes return the proper value.\r
+     *\r
+     *\r
+     *  @param o  The object to be read.  It must be an array of a primitive type,\r
+     *           or an array of Object's.\r
+     */\r
+    public int readArray(Object o) throws IOException {\r
+        return (int) readLArray(o);\r
+    }\r
+\r
+    /** This routine provides efficient reading of arrays of any primitive\r
+     *  type.\r
+     *  @param o  The object to be read.  It must be an arraof of a primtive type\r
+     *            (or any dimension), or an array of Objects which contains\r
+     *            pointers to primitive arrays or other object arrays.\r
+     */\r
+    public long readLArray(Object o) throws IOException {\r
+\r
+\r
+        // Note that we assume that only a single thread is\r
+        // doing a primitive Array read at any given time.  Otherwise\r
+        // primitiveArrayCount can be wrong and also the\r
+        // input data can be mixed up.  If this assumption is not\r
+        // true we need to synchronize this call.\r
+\r
+        primitiveArrayCount = 0;\r
+        return primitiveArrayRecurse(o);\r
+    }\r
+\r
+    protected long primitiveArrayRecurse(Object o) throws IOException {\r
+\r
+        if (o == null) {\r
+            return primitiveArrayCount;\r
+        }\r
+\r
+        String className = o.getClass().getName();\r
+\r
+        if (className.charAt(0) != '[') {\r
+            throw new IOException("Invalid object passed to BufferedDataInputStream.readArray:" + className);\r
+        }\r
+\r
+        // Is this a multidimensional array?  If so process recursively.\r
+        if (className.charAt(1) == '[') {\r
+            for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                primitiveArrayRecurse(((Object[]) o)[i]);\r
+            }\r
+        } else {\r
+\r
+            // This is a one-d array.  Process it using our special functions.\r
+            switch (className.charAt(1)) {\r
+                case 'Z':\r
+                    primitiveArrayCount += read((boolean[]) o, 0, ((boolean[]) o).length);\r
+                    break;\r
+                case 'B':\r
+                    int len = read((byte[]) o, 0, ((byte[]) o).length);\r
+                    break;\r
+                case 'C':\r
+                    primitiveArrayCount += read((char[]) o, 0, ((char[]) o).length);\r
+                    break;\r
+                case 'S':\r
+                    primitiveArrayCount += read((short[]) o, 0, ((short[]) o).length);\r
+                    break;\r
+                case 'I':\r
+                    primitiveArrayCount += read((int[]) o, 0, ((int[]) o).length);\r
+                    break;\r
+                case 'J':\r
+                    primitiveArrayCount += read((long[]) o, 0, ((long[]) o).length);\r
+                    break;\r
+                case 'F':\r
+                    primitiveArrayCount += read((float[]) o, 0, ((float[]) o).length);\r
+                    break;\r
+                case 'D':\r
+                    primitiveArrayCount += read((double[]) o, 0, ((double[]) o).length);\r
+                    break;\r
+                case 'L':\r
+\r
+                    // Handle an array of Objects by recursion.  Anything\r
+                    // else is an error.\r
+                    if (className.equals("[Ljava.lang.Object;")) {\r
+                        for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                            primitiveArrayRecurse(((Object[]) o)[i]);\r
+                        }\r
+                    } else {\r
+                        throw new IOException("Invalid object passed to BufferedFile.readPrimitiveArray: " + className);\r
+                    }\r
+                    break;\r
+                default:\r
+                    throw new IOException("Invalid object passed to BufferedDataInputStream.readArray: " + className);\r
+            }\r
+        }\r
+        return primitiveArrayCount;\r
+    }\r
+\r
+    public int read(boolean[] b) throws IOException {\r
+        return read(b, 0, b.length);\r
+    }\r
+\r
+    public int read(boolean[] b, int start, int length) throws IOException {\r
+\r
+        int i = start;\r
+        try {\r
+            for (; i < start + length; i += 1) {\r
+                b[i] = convertToBoolean();\r
+            }\r
+            return length;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, i, 1);\r
+        }\r
+    }\r
+\r
+    public int read(short[] s) throws IOException {\r
+        return read(s, 0, s.length);\r
+    }\r
+\r
+    public int read(short[] s, int start, int length) throws IOException {\r
+\r
+        int i = start;\r
+        try {\r
+            for (; i < start + length; i += 1) {\r
+                s[i] = convertToShort();\r
+            }\r
+            return length * 2;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, i, 2);\r
+        }\r
+    }\r
+\r
+    public int read(char[] c) throws IOException {\r
+        return read(c, 0, c.length);\r
+    }\r
+\r
+    public int read(char[] c, int start, int length) throws IOException {\r
+\r
+        int i = start;\r
+        try {\r
+            for (; i < start + length; i += 1) {\r
+                c[i] = convertToChar();\r
+            }\r
+            return length * 2;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, i, 2);\r
+        }\r
+    }\r
+\r
+    public int read(int[] i) throws IOException {\r
+        return read(i, 0, i.length);\r
+    }\r
+\r
+    public int read(int[] i, int start, int length) throws IOException {\r
+\r
+        int ii = start;\r
+        try {\r
+            for (; ii < start + length; ii += 1) {\r
+                i[ii] = convertToInt();\r
+            }\r
+            return length * 4;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, ii, 4);\r
+        }\r
+    }\r
+\r
+    public int read(long[] l) throws IOException {\r
+        return read(l, 0, l.length);\r
+    }\r
+\r
+    public int read(long[] l, int start, int length) throws IOException {\r
+\r
+        int i = start;\r
+        try {\r
+            for (; i < start + length; i += 1) {\r
+                l[i] = convertToLong();\r
+            }\r
+            return length * 8;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, i, 8);\r
+        }\r
+\r
+    }\r
+\r
+    public int read(float[] f) throws IOException {\r
+        return read(f, 0, f.length);\r
+    }\r
+\r
+    public int read(float[] f, int start, int length) throws IOException {\r
+\r
+        int i = start;\r
+        try {\r
+            for (; i < start + length; i += 1) {\r
+                f[i] = Float.intBitsToFloat(convertToInt());\r
+            }\r
+            return length * 4;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, i, 4);\r
+        }\r
+    }\r
+\r
+    public int read(double[] d) throws IOException {\r
+        return read(d, 0, d.length);\r
+    }\r
+\r
+    public int read(double[] d, int start, int length) throws IOException {\r
+\r
+        int i = start;\r
+        try {\r
+            for (; i < start + length; i += 1) {\r
+                d[i] = Double.longBitsToDouble(convertToLong());\r
+            }\r
+            return length * 8;\r
+        } catch (EOFException e) {\r
+            return eofCheck(e, start, i, 8);\r
+        }\r
+    }\r
+\r
+    /** See if an exception should be thrown during an array read. */\r
+    private int eofCheck(EOFException e, int start, int index, int length)\r
+            throws EOFException {\r
+        if (start == index) {\r
+            throw e;\r
+        } else {\r
+            return (index - start) * length;\r
+        }\r
+    }\r
+\r
+    /**** Output Routines ****/\r
+    private void needBuffer(int need) throws IOException {\r
+\r
+        if (doingInput) {\r
+\r
+            fileOffset += bufferOffset;\r
+            raf.seek(fileOffset);\r
+\r
+            doingInput = false;\r
+\r
+            bufferOffset = 0;\r
+            bufferLength = 0;\r
+        }\r
+\r
+        if (bufferOffset + need >= bufferSize) {\r
+            raf.write(buffer, 0, bufferOffset);\r
+            fileOffset += bufferOffset;\r
+            bufferOffset = 0;\r
+        }\r
+    }\r
+\r
+    public void write(int buf) throws IOException {\r
+        convertFromByte(buf);\r
+    }\r
+\r
+    public void write(byte[] buf) throws IOException {\r
+        write(buf, 0, buf.length);\r
+    }\r
+\r
+    public void write(byte[] buf, int offset, int length) throws IOException {\r
+\r
+        if (length < bufferSize) {\r
+            /* If we can use the buffer do so... */\r
+            needBuffer(length);\r
+            System.arraycopy(buf, offset, buffer, bufferOffset, length);\r
+            bufferOffset += length;\r
+        } else {\r
+            /* Otherwise flush the buffer and write the data directly.\r
+             * Make sure that we indicate that the buffer is clean when\r
+             * we're done.\r
+             */\r
+            flush();\r
+\r
+            raf.write(buf, offset, length);\r
+\r
+            fileOffset += length;\r
+\r
+            doingInput = false;\r
+            bufferOffset = 0;\r
+            bufferLength = 0;\r
+        }\r
+    }\r
+\r
+    /** Flush output buffer if necessary.\r
+     *  This method is not present in RandomAccessFile\r
+     *  but users may need to call flush to ensure\r
+     *  that data has been written.\r
+     */\r
+    public void flush() throws IOException {\r
+\r
+        if (!doingInput && bufferOffset > 0) {\r
+            raf.write(buffer, 0, bufferOffset);\r
+            fileOffset += bufferOffset;\r
+            bufferOffset = 0;\r
+            bufferLength = 0;\r
+        }\r
+    }\r
+\r
+    /** Clear up any pending output at cleanup.\r
+     */\r
+    protected void finalize() {\r
+        try {\r
+            if (getFD().valid()) {\r
+                flush();\r
+                close();\r
+            }\r
+        } catch (Exception e) {\r
+        }\r
+    }\r
+\r
+    /** Write a boolean value\r
+     * @param b  The value to be written.  Externally true is represented as\r
+     *           a byte of 1 and false as a byte value of 0.\r
+     */\r
+    public void writeBoolean(boolean b) throws IOException {\r
+        convertFromBoolean(b);\r
+    }\r
+\r
+    private void convertFromBoolean(boolean b) throws IOException {\r
+        needBuffer(1);\r
+        if (b) {\r
+            buffer[bufferOffset] = (byte) 1;\r
+        } else {\r
+            buffer[bufferOffset] = (byte) 0;\r
+        }\r
+        bufferOffset += 1;\r
+    }\r
+\r
+    /** Write a byte value.\r
+     */\r
+    public void writeByte(int b) throws IOException {\r
+        convertFromByte(b);\r
+    }\r
+\r
+    private void convertFromByte(int b) throws IOException {\r
+        needBuffer(1);\r
+        buffer[bufferOffset++] = (byte) b;\r
+    }\r
+\r
+    /** Write an integer value.\r
+     */\r
+    public void writeInt(int i) throws IOException {\r
+        convertFromInt(i);\r
+    }\r
+\r
+    private void convertFromInt(int i) throws IOException {\r
+\r
+        needBuffer(4);\r
+        buffer[bufferOffset++] = (byte) (i >>> 24);\r
+        buffer[bufferOffset++] = (byte) (i >>> 16);\r
+        buffer[bufferOffset++] = (byte) (i >>> 8);\r
+        buffer[bufferOffset++] = (byte) i;\r
+    }\r
+\r
+    /** Write a short value.\r
+     */\r
+    public void writeShort(int s) throws IOException {\r
+\r
+        convertFromShort(s);\r
+    }\r
+\r
+    private void convertFromShort(int s) throws IOException {\r
+        needBuffer(2);\r
+\r
+        buffer[bufferOffset++] = (byte) (s >>> 8);\r
+        buffer[bufferOffset++] = (byte) s;\r
+    }\r
+\r
+    /** Write a char value.\r
+     */\r
+    public void writeChar(int c) throws IOException {\r
+        convertFromChar(c);\r
+    }\r
+\r
+    private void convertFromChar(int c) throws IOException {\r
+        needBuffer(2);\r
+        buffer[bufferOffset++] = (byte) (c >>> 8);\r
+        buffer[bufferOffset++] = (byte) c;\r
+    }\r
+\r
+    /** Write a long value.\r
+     */\r
+    public void writeLong(long l) throws IOException {\r
+        convertFromLong(l);\r
+    }\r
+\r
+    private void convertFromLong(long l) throws IOException {\r
+        needBuffer(8);\r
+\r
+        buffer[bufferOffset++] = (byte) (l >>> 56);\r
+        buffer[bufferOffset++] = (byte) (l >>> 48);\r
+        buffer[bufferOffset++] = (byte) (l >>> 40);\r
+        buffer[bufferOffset++] = (byte) (l >>> 32);\r
+        buffer[bufferOffset++] = (byte) (l >>> 24);\r
+        buffer[bufferOffset++] = (byte) (l >>> 16);\r
+        buffer[bufferOffset++] = (byte) (l >>> 8);\r
+        buffer[bufferOffset++] = (byte) l;\r
+    }\r
+\r
+    /** Write a float value.\r
+     */\r
+    public void writeFloat(float f) throws IOException {\r
+        convertFromInt(Float.floatToIntBits(f));\r
+    }\r
+\r
+    /** Write a double value.\r
+     */\r
+    public void writeDouble(double d) throws IOException {\r
+        convertFromLong(Double.doubleToLongBits(d));\r
+    }\r
+\r
+    /** Write a string using the local protocol to convert char's to bytes.\r
+     *\r
+     * @param s   The string to be written.\r
+     */\r
+    public void writeBytes(String s) throws IOException {\r
+        write(s.getBytes(), 0, s.length());\r
+    }\r
+\r
+    /** Write a string as an array of chars.\r
+     */\r
+    public void writeChars(String s) throws IOException {\r
+\r
+        int len = s.length();\r
+        for (int i = 0; i < len; i += 1) {\r
+            convertFromChar(s.charAt(i));\r
+        }\r
+    }\r
+\r
+    /** Write a string as a UTF.\r
+     */\r
+    public void writeUTF(String s) throws IOException {\r
+        flush();\r
+        raf.writeUTF(s);\r
+        fileOffset = raf.getFilePointer();\r
+    }\r
+\r
+    /** This routine provides efficient writing of arrays of any primitive type.\r
+     * The String class is also handled but it is an error to invoke this\r
+     * method with an object that is not an array of these types.  If the\r
+     * array is multidimensional, then it calls itself recursively to write\r
+     * the entire array.  Strings are written using the standard\r
+     * 1 byte format (i.e., as in writeBytes).\r
+     *\r
+     * If the array is an array of objects, then write will\r
+     * be called for each element of the array.\r
+     *\r
+     * @param o  The object to be written.  It must be an array of a primitive\r
+     *           type, Object, or String.\r
+     */\r
+    public void writeArray(Object o) throws IOException {\r
+        String className = o.getClass().getName();\r
+\r
+        if (className.charAt(0) != '[') {\r
+            throw new IOException("Invalid object passed to BufferedFile.writeArray:" + className);\r
+        }\r
+\r
+        // Is this a multidimensional array?  If so process recursively.\r
+        if (className.charAt(1) == '[') {\r
+            for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                writeArray(((Object[]) o)[i]);\r
+            }\r
+        } else {\r
+\r
+            // This is a one-d array.  Process it using our special functions.\r
+            switch (className.charAt(1)) {\r
+                case 'Z':\r
+                    write((boolean[]) o, 0, ((boolean[]) o).length);\r
+                    break;\r
+                case 'B':\r
+                    write((byte[]) o, 0, ((byte[]) o).length);\r
+                    break;\r
+                case 'C':\r
+                    write((char[]) o, 0, ((char[]) o).length);\r
+                    break;\r
+                case 'S':\r
+                    write((short[]) o, 0, ((short[]) o).length);\r
+                    break;\r
+                case 'I':\r
+                    write((int[]) o, 0, ((int[]) o).length);\r
+                    break;\r
+                case 'J':\r
+                    write((long[]) o, 0, ((long[]) o).length);\r
+                    break;\r
+                case 'F':\r
+                    write((float[]) o, 0, ((float[]) o).length);\r
+                    break;\r
+                case 'D':\r
+                    write((double[]) o, 0, ((double[]) o).length);\r
+                    break;\r
+                case 'L':\r
+\r
+                    // Handle two exceptions: an array of strings, or an\r
+                    // array of objects. .\r
+                    if (className.equals("[Ljava.lang.String;")) {\r
+                        write((String[]) o, 0, ((String[]) o).length);\r
+                    } else if (className.equals("[Ljava.lang.Object;")) {\r
+                        for (int i = 0; i < ((Object[]) o).length; i += 1) {\r
+                            writeArray(((Object[]) o)[i]);\r
+                        }\r
+                    } else {\r
+                        throw new IOException("Invalid object passed to BufferedFile.write: " + className);\r
+                    }\r
+                    break;\r
+                default:\r
+                    throw new IOException("Invalid object passed to BufferedFile.write: " + className);\r
+            }\r
+        }\r
+\r
+    }\r
+\r
+    /** Write an array of booleans.\r
+     */\r
+    public void write(boolean[] b) throws IOException {\r
+        write(b, 0, b.length);\r
+    }\r
+\r
+    public void write(boolean[] b, int start, int length) throws IOException {\r
+        for (int i = start; i < start + length; i += 1) {\r
+            convertFromBoolean(b[i]);\r
+        }\r
+    }\r
+\r
+    /** Write an array of shorts.\r
+     */\r
+    public void write(short[] s) throws IOException {\r
+        write(s, 0, s.length);\r
+    }\r
+\r
+    public void write(short[] s, int start, int length) throws IOException {\r
+\r
+        for (int i = start; i < start + length; i += 1) {\r
+            convertFromShort(s[i]);\r
+        }\r
+    }\r
+\r
+    /** Write an array of char's.\r
+     */\r
+    public void write(char[] c) throws IOException {\r
+        write(c, 0, c.length);\r
+    }\r
+\r
+    public void write(char[] c, int start, int length) throws IOException {\r
+\r
+        for (int i = start; i < start + length; i += 1) {\r
+            convertFromChar(c[i]);\r
+        }\r
+    }\r
+\r
+    /** Write an array of int's.\r
+     */\r
+    public void write(int[] i) throws IOException {\r
+        write(i, 0, i.length);\r
+    }\r
+\r
+    public void write(int[] i, int start, int length) throws IOException {\r
+        for (int ii = start; ii < start + length; ii += 1) {\r
+            convertFromInt(i[ii]);\r
+        }\r
+    }\r
+\r
+    /** Write an array of longs.\r
+     */\r
+    public void write(long[] l) throws IOException {\r
+        write(l, 0, l.length);\r
+    }\r
+\r
+    public void write(long[] l, int start, int length) throws IOException {\r
+\r
+        for (int i = start; i < start + length; i += 1) {\r
+            convertFromLong(l[i]);\r
+        }\r
+    }\r
+\r
+    /** Write an array of floats.\r
+     */\r
+    public void write(float[] f) throws IOException {\r
+        write(f, 0, f.length);\r
+    }\r
+\r
+    public void write(float[] f, int start, int length) throws IOException {\r
+        for (int i = start; i < start + length; i += 1) {\r
+            convertFromInt(Float.floatToIntBits(f[i]));\r
+        }\r
+    }\r
+\r
+    /** Write an array of doubles.\r
+     */\r
+    public void write(double[] d) throws IOException {\r
+        write(d, 0, d.length);\r
+    }\r
+\r
+    public void write(double[] d, int start, int length) throws IOException {\r
+\r
+        for (int i = start; i < start + length; i += 1) {\r
+            convertFromLong(Double.doubleToLongBits(d[i]));\r
+        }\r
+    }\r
+\r
+    /** Write an array of Strings -- equivalent to calling writeBytes for each string.\r
+     */\r
+    public void write(String[] s) throws IOException {\r
+        write(s, 0, s.length);\r
+    }\r
+\r
+    public void write(String[] s, int start, int length) throws IOException {\r
+        for (int i = start; i < start + length; i += 1) {\r
+            writeBytes(s[i]);\r
+        }\r
+    }\r
+\r
+    /** Close the file */\r
+    public void close() throws IOException {\r
+        flush();\r
+        raf.close();\r
+    }\r
+\r
+    /** Get the file descriptor associated with\r
+     *  this stream.  Note that this returns the file\r
+     *  descriptor of the associated RandomAccessFile.\r
+     */\r
+    public FileDescriptor getFD() throws IOException {\r
+        return raf.getFD();\r
+    }\r
+\r
+    /** Get the channel associated with\r
+     *  this file.  Note that this returns the channel\r
+     *  of the associated RandomAccessFile.\r
+     *  Note that since the BufferedFile buffers the I/O's to the\r
+     *  underlying file, the offset of the channel may be\r
+     *  different than the offset of the BufferedFile.  This\r
+     *  is different than for a RandomAccessFile where the\r
+     *  offsets are guaranteed to be the same.\r
+     */\r
+    public java.nio.channels.FileChannel getChannel() {\r
+        return raf.getChannel();\r
+    }\r
+\r
+    /** Get the current length of the file.\r
+     */\r
+    public long length() throws IOException {\r
+        flush();\r
+        return raf.length();\r
+    }\r
+\r
+    /** Get the current offset into the file.\r
+     */\r
+    public long getFilePointer() {\r
+        return fileOffset + bufferOffset;\r
+    }\r
+\r
+    /** Set the length of the file.  This method calls\r
+     *  the method of the same name in RandomAccessFile which\r
+     *  is only available in JDK1.2 and greater.  This method\r
+     *  may be deleted for compilation with earlier versions.\r
+     *\r
+     * @param newLength The number of bytes at which the file\r
+     *                  is set.\r
+     *\r
+     */\r
+    public void setLength(long newLength) throws IOException {\r
+\r
+        flush();\r
+        raf.setLength(newLength);\r
+        if (newLength < fileOffset) {\r
+            fileOffset = newLength;\r
+        }\r
+\r
+    }\r
+}\r
diff --git a/src/nom/tam/util/ByteFormatter.java b/src/nom/tam/util/ByteFormatter.java
new file mode 100644 (file)
index 0000000..f704f32
--- /dev/null
@@ -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.
+ * <p>
+ * The methods in this class create no objects.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+     * <p>
+     * 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.
+     * <p>
+     * Recall that the binary representation of
+     * the float is of the form <tt>d = 0.bbbbbbbb x 2<sup>n</sup></tt>
+     * 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 <tt>10<sup>m</su> d</tt> is between
+     * <tt>2<sup>24</sup></tt> and <tt>>2<sup>32</sup></tt>.
+     * 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 (<tt>-m</tt>).
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * Recall that the binary representation of
+     * the double is of the form <tt>d = 0.bbbbbbbb x 2<sup>n</sup></tt>
+     * 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 <tt>10<sup>m</su> d</tt> is between
+     * <tt>2<sup>53</sup></tt> and <tt>>2<sup>63</sup></tt>.
+     * 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 (<tt>-m</tt>).
+     * 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 (file)
index 0000000..59de84e
--- /dev/null
@@ -0,0 +1,481 @@
+package nom.tam.util;\r
+\r
+import java.io.UnsupportedEncodingException;\r
+import java.util.logging.Level;\r
+import java.util.logging.Logger;\r
+\r
+/** This class provides routines\r
+ *  for efficient parsing of data stored in a byte array.\r
+ *  This routine is optimized (in theory at least!) for efficiency\r
+ *  rather than accuracy.  The values read in for doubles or floats\r
+ *  may differ in the last bit or so from the standard input\r
+ *  utilities, especially in the case where a float is specified\r
+ * as a very long string of digits (substantially longer than\r
+ * the precision of the type).\r
+ * <p>\r
+ * The get methods generally are available with or without a length\r
+ * parameter specified.  When a length parameter is specified only\r
+ * the bytes with the specified range from the current offset will\r
+ * be search for the number.  If no length is specified, the entire\r
+ * buffer from the current offset will be searched.\r
+ * <p>\r
+ * The getString method returns a string with leading and trailing\r
+ * white space left intact.  For all other get calls, leading\r
+ * white space is ignored.  If fillFields is set, then the get\r
+ * methods check that only white space follows valid data and a\r
+ * FormatException is thrown if that is not the case.  If\r
+ * fillFields is not set and valid data is found, then the\r
+ * methods return having read as much as possible.  E.g., for\r
+ * the sequence "T123.258E13", a getBoolean, getInteger and\r
+ * getFloat call would return true, 123, and 2.58e12 when\r
+ * called in succession.\r
+ *\r
+ */\r
+public class ByteParser {\r
+\r
+    /** Array being parsed */\r
+    private byte[] input;\r
+    /** Current offset into input. */\r
+    private int offset;\r
+    /** Length of last parsed value */\r
+    private int numberLength;\r
+    /** Did we find a sign last time we checked? */\r
+    private boolean foundSign;\r
+    /** Do we fill up fields? */\r
+    private boolean fillFields = false;\r
+\r
+    /** Construct a parser.\r
+     * @param input    The byte array to be parsed.\r
+     *                 Note that the array can be re-used by\r
+     *                 refilling its contents and resetting the offset.\r
+     */\r
+    public ByteParser(byte[] input) {\r
+        this.input = input;\r
+        this.offset = 0;\r
+    }\r
+\r
+    /** Set the buffer for the parser */\r
+    public void setBuffer(byte[] buf) {\r
+        this.input = buf;\r
+        this.offset = 0;\r
+    }\r
+\r
+    /** Get the buffer being used by the parser */\r
+    public byte[] getBuffer() {\r
+        return input;\r
+    }\r
+\r
+    /** Set the offset into the array.\r
+     * @param offset   The desired offset from the beginning\r
+     *                  of the array.\r
+     */\r
+    public void setOffset(int offset) {\r
+        this.offset = offset;\r
+    }\r
+\r
+    /** Do we require a field to completely fill up the specified\r
+     * length (with optional leading and trailing white space.\r
+    @param flag        Is filling required?\r
+     */\r
+    public void setFillFields(boolean flag) {\r
+        fillFields = flag;\r
+    }\r
+\r
+    /** Get the current offset\r
+    @return The current offset within the buffer.\r
+     */\r
+    public int getOffset() {\r
+        return offset;\r
+    }\r
+\r
+    /** Get the number of characters used to parse the previous\r
+     *  number (or the length of the previous String returned).\r
+     */\r
+    public int getNumberLength() {\r
+        return numberLength;\r
+    }\r
+\r
+    /** Read in the buffer until a double is read.  This will read\r
+     * the entire buffer if fillFields is set.\r
+     * @return The value found.\r
+     */\r
+    public double getDouble() throws FormatException {\r
+        return getDouble(input.length - offset);\r
+    }\r
+\r
+    /** Look for a double in the buffer.\r
+     * Leading spaces are ignored.\r
+     * @param length   The maximum number of characters\r
+     *                  used to parse this number.  If fillFields\r
+     *                  is specified then exactly only whitespace may follow\r
+     *                  a valid double value.\r
+     */\r
+    public double getDouble(int length) throws FormatException {\r
+\r
+        int startOffset = offset;\r
+\r
+        boolean error = true;\r
+\r
+        double number = 0;\r
+        int i = 0;\r
+\r
+        // Skip initial blanks.\r
+        length -= skipWhite(length);\r
+\r
+        if (length == 0) {\r
+            numberLength = offset - startOffset;\r
+            return 0;\r
+        }\r
+\r
+        double mantissaSign = checkSign();\r
+        if (foundSign) {\r
+            length -= 1;\r
+        }\r
+\r
+        // Look for the special strings NaN, Inf,\r
+        if (length >= 3\r
+                && (input[offset] == 'n' || input[offset] == 'N')\r
+                && (input[offset + 1] == 'a' || input[offset + 1] == 'A')\r
+                && (input[offset + 2] == 'n' || input[offset + 2] == 'N')) {\r
+\r
+            number = Double.NaN;\r
+            length -= 3;\r
+            offset += 3;\r
+\r
+            // Look for the longer string first then try the shorter.\r
+        } else if (length >= 8\r
+                && (input[offset] == 'i' || input[offset] == 'I')\r
+                && (input[offset + 1] == 'n' || input[offset + 1] == 'N')\r
+                && (input[offset + 2] == 'f' || input[offset + 2] == 'F')\r
+                && (input[offset + 3] == 'i' || input[offset + 3] == 'I')\r
+                && (input[offset + 4] == 'n' || input[offset + 4] == 'N')\r
+                && (input[offset + 5] == 'i' || input[offset + 5] == 'I')\r
+                && (input[offset + 6] == 't' || input[offset + 6] == 'T')\r
+                && (input[offset + 7] == 'y' || input[offset + 7] == 'Y')) {\r
+            number = Double.POSITIVE_INFINITY;\r
+            length -= 8;\r
+            offset += 8;\r
+\r
+        } else if (length >= 3\r
+                && (input[offset] == 'i' || input[offset] == 'I')\r
+                && (input[offset + 1] == 'n' || input[offset + 1] == 'N')\r
+                && (input[offset + 2] == 'f' || input[offset + 2] == 'F')) {\r
+            number = Double.POSITIVE_INFINITY;\r
+            length -= 3;\r
+            offset += 3;\r
+\r
+        } else {\r
+\r
+            number = getBareInteger(length);   // This will update offset\r
+            length -= numberLength;            // Set by getBareInteger\r
+\r
+            if (numberLength > 0) {\r
+                error = false;\r
+            }\r
+\r
+            // Check for fractional values after decimal\r
+            if (length > 0 && input[offset] == '.') {\r
+\r
+                offset += 1;\r
+                length -= 1;\r
+\r
+                double numerator = getBareInteger(length);\r
+                if (numerator > 0) {\r
+                    number += numerator / Math.pow(10., numberLength);\r
+                }\r
+                length -= numberLength;\r
+                if (numberLength > 0) {\r
+                    error = false;\r
+                }\r
+            }\r
+\r
+            if (error) {\r
+                offset = startOffset;\r
+                numberLength = 0;\r
+                throw new FormatException("Invalid real field");\r
+            }\r
+\r
+            // Look for an exponent\r
+            if (length > 0) {\r
+\r
+                // Our Fortran heritage means that we allow 'D' for the exponent indicator.\r
+                if (input[offset] == 'e' || input[offset] == 'E'\r
+                        || input[offset] == 'd' || input[offset] == 'D') {\r
+\r
+                    offset += 1;\r
+                    length -= 1;\r
+                    if (length > 0) {\r
+                        int sign = checkSign();\r
+                        if (foundSign) {\r
+                            length -= 1;\r
+                        }\r
+\r
+                        int exponent = (int) getBareInteger(length);\r
+\r
+                        // For very small numbers we try to miminize\r
+                        // effects of denormalization.\r
+                        if (exponent * sign > -300) {\r
+                            number *= Math.pow(10., exponent * sign);\r
+                        } else {\r
+                            number = 1.e-300 * (number * Math.pow(10., exponent * sign + 300));\r
+                        }\r
+                        length -= numberLength;\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        if (fillFields && length > 0) {\r
+\r
+            if (isWhite(length)) {\r
+                offset += length;\r
+            } else {\r
+                numberLength = 0;\r
+                offset = startOffset;\r
+                throw new FormatException("Non-blanks following real.");\r
+            }\r
+        }\r
+\r
+        numberLength = offset - startOffset;\r
+        return mantissaSign * number;\r
+    }\r
+\r
+    /** Get a floating point value from the buffer.  (see getDouble(int())\r
+     */\r
+    public float getFloat() throws FormatException {\r
+        return (float) getDouble(input.length - offset);\r
+    }\r
+\r
+    /** Get a floating point value in a region of the buffer */\r
+    public float getFloat(int length) throws FormatException {\r
+        return (float) getDouble(length);\r
+    }\r
+\r
+    /** Convert a region of the buffer to an integer */\r
+    public int getInt(int length) throws FormatException {\r
+        int startOffset = offset;\r
+\r
+        length -= skipWhite(length);\r
+        if (length == 0) {\r
+            numberLength = offset - startOffset;\r
+            return 0;\r
+        }\r
+\r
+        int number = 0;\r
+        boolean error = true;\r
+\r
+        int sign = checkSign();\r
+        if (foundSign) {\r
+            length -= 1;\r
+        }\r
+\r
+        while (length > 0 && input[offset] >= '0' && input[offset] <= '9') {\r
+            number = number * 10 + input[offset] - '0';\r
+            offset += 1;\r
+            length -= 1;\r
+            error = false;\r
+        }\r
+\r
+        if (error) {\r
+            numberLength = 0;\r
+            offset = startOffset;\r
+            throw new FormatException("Invalid Integer");\r
+        }\r
+\r
+        if (length > 0 && fillFields) {\r
+            if (isWhite(length)) {\r
+                offset += length;\r
+            } else {\r
+                numberLength = 0;\r
+                offset = startOffset;\r
+                throw new FormatException("Non-white following integer");\r
+            }\r
+        }\r
+\r
+        numberLength = offset - startOffset;\r
+        return sign * number;\r
+    }\r
+\r
+    /** Look for an integer at the beginning of the buffer */\r
+    public int getInt() throws FormatException {\r
+        return getInt(input.length - offset);\r
+    }\r
+\r
+    /** Look for a long in a specified region of the buffer */\r
+    public long getLong(int length) throws FormatException {\r
+\r
+        int startOffset = offset;\r
+\r
+        // Skip white space.\r
+        length -= skipWhite(length);\r
+        if (length == 0) {\r
+            numberLength = offset - startOffset;\r
+            return 0;\r
+        }\r
+\r
+        long number = 0;\r
+        boolean error = true;\r
+\r
+        long sign = checkSign();\r
+        if (foundSign) {\r
+            length -= 1;\r
+        }\r
+\r
+        while (length > 0 && input[offset] >= '0' && input[offset] <= '9') {\r
+            number = number * 10 + input[offset] - '0';\r
+            error = false;\r
+            offset += 1;\r
+            length -= 1;\r
+        }\r
+\r
+        if (error) {\r
+            numberLength = 0;\r
+            offset = startOffset;\r
+            throw new FormatException("Invalid long number");\r
+        }\r
+\r
+        if (length > 0 && fillFields) {\r
+            if (isWhite(length)) {\r
+                offset += length;\r
+            } else {\r
+                offset = startOffset;\r
+                numberLength = 0;\r
+                throw new FormatException("Non-white following long");\r
+            }\r
+        }\r
+        numberLength = offset - startOffset;\r
+        return sign * number;\r
+    }\r
+\r
+    /** Get a string\r
+     * @param length  The length of the string.\r
+     */\r
+    public String getString(int length) {\r
+\r
+        String s = AsciiFuncs.asciiString(input, offset, length);\r
+        offset += length;\r
+        numberLength = length;\r
+        return s;\r
+    }\r
+\r
+    /** Get a boolean value from the beginning of the buffer */\r
+    public boolean getBoolean() throws FormatException {\r
+        return getBoolean(input.length - offset);\r
+    }\r
+\r
+    /** Get a boolean value from a specified region of the buffer */\r
+    public boolean getBoolean(int length) throws FormatException {\r
+\r
+        int startOffset = offset;\r
+        length -= skipWhite(length);\r
+        if (length == 0) {\r
+            throw new FormatException("Blank boolean field");\r
+        }\r
+\r
+        boolean value = false;\r
+        if (input[offset] == 'T' || input[offset] == 't') {\r
+            value = true;\r
+        } else if (input[offset] != 'F' && input[offset] != 'f') {\r
+            numberLength = 0;\r
+            offset = startOffset;\r
+            throw new FormatException("Invalid boolean value");\r
+        }\r
+        offset += 1;\r
+        length -= 1;\r
+\r
+        if (fillFields && length > 0) {\r
+            if (isWhite(length)) {\r
+                offset += length;\r
+            } else {\r
+                numberLength = 0;\r
+                offset = startOffset;\r
+                throw new FormatException("Non-white following boolean");\r
+            }\r
+        }\r
+        numberLength = offset - startOffset;\r
+        return value;\r
+    }\r
+\r
+    /** Skip bytes in the buffer */\r
+    public void skip(int nBytes) {\r
+        offset += nBytes;\r
+    }\r
+\r
+    /** Get the integer value starting at the current position.\r
+     * This routine returns a double rather than an int/long\r
+     * to enable it to read very long integers (with reduced\r
+     * precision) such as 111111111111111111111111111111111111111111.\r
+     * Note that this routine does set numberLength.\r
+     *\r
+     * @param length   The maximum number of characters to use.\r
+     */\r
+    private double getBareInteger(int length) {\r
+\r
+        int startOffset = offset;\r
+        double number = 0;\r
+\r
+        while (length > 0 && input[offset] >= '0' && input[offset] <= '9') {\r
+\r
+            number *= 10;\r
+            number += input[offset] - '0';\r
+            offset += 1;\r
+            length -= 1;\r
+        }\r
+        numberLength = offset - startOffset;\r
+        return number;\r
+    }\r
+\r
+    /** Skip white space.  This routine skips with space in\r
+     * the input and returns the number of character skipped.\r
+     * White space is defined as ' ', '\t', '\n' or '\r'\r
+     *\r
+     * @param length The maximum number of characters to skip.\r
+     */\r
+    public int skipWhite(int length) {\r
+\r
+        int i;\r
+        for (i = 0; i < length; i += 1) {\r
+            if (input[offset + i] != ' ' && input[offset + i] != '\t'\r
+                    && input[offset + i] != '\n' && input[offset + i] != '\r') {\r
+                break;\r
+            }\r
+        }\r
+\r
+        offset += i;\r
+        return i;\r
+\r
+    }\r
+\r
+    /** Find the sign for a number .\r
+     * This routine looks for a sign (+/-) at the current location\r
+     * and return +1/-1 if one is found, or +1 if not.\r
+     * The foundSign boolean is set if a sign is found and offset is\r
+     * incremented.\r
+     */\r
+    private int checkSign() {\r
+\r
+        foundSign = false;\r
+\r
+        if (input[offset] == '+') {\r
+            foundSign = true;\r
+            offset += 1;\r
+            return 1;\r
+        } else if (input[offset] == '-') {\r
+            foundSign = true;\r
+            offset += 1;\r
+            return -1;\r
+        }\r
+\r
+        return 1;\r
+    }\r
+\r
+    /** Is a region blank?\r
+     * @param length The length of the region to be tested\r
+     */\r
+    private boolean isWhite(int length) {\r
+        int oldOffset = offset;\r
+        boolean value = skipWhite(length) == length;\r
+        offset = oldOffset;\r
+        return value;\r
+    }\r
+}\r
diff --git a/src/nom/tam/util/ColumnTable.java b/src/nom/tam/util/ColumnTable.java
new file mode 100644 (file)
index 0000000..20679d2
--- /dev/null
@@ -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:
+     * <ul>
+     * <li> arrays and sizes have different lengths.
+     * <li> an element of arrays is not a primitive array.
+     * <li> the size of an array is not divisible by the sizes entry.
+     * <li> the number of rows differs for the columns.
+     * </ul>
+     * @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 (file)
index 0000000..5b97f26
--- /dev/null
@@ -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 (file)
index 0000000..16312ec
--- /dev/null
@@ -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 (file)
index 0000000..4f98e38
--- /dev/null
@@ -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 (file)
index 0000000..c96090c
--- /dev/null
@@ -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 (file)
index 0000000..5d64712
--- /dev/null
@@ -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 (file)
index 0000000..7a0e5a9
--- /dev/null
@@ -0,0 +1,38 @@
+package nom.tam.util;\r
+\r
+/** This interface collects some information about Java primitives.\r
+ */\r
+public interface PrimitiveInfo {\r
+\r
+    /** Suffixes used for the classnames for primitive arrays. */\r
+    char[] suffixes = new char[]{'B', 'S', 'C', 'I', 'J', 'F', 'D', 'Z'};\r
+    /** Classes of the primitives. These should be in windening order\r
+     * (char is as always a problem).\r
+     */\r
+    Class[] classes = new Class[]{\r
+        byte.class, short.class, char.class, int.class,\r
+        long.class, float.class, double.class, boolean.class};\r
+    /** Is this a numeric class */\r
+    boolean[] isNumeric = new boolean[]{true, true, true, true, true, true, true, false};\r
+    /** Full names */\r
+    String[] types = new String[]{\r
+        "byte", "short", "char", "int",\r
+        "long", "float", "double", "boolean"\r
+    };\r
+    /** Sizes */\r
+    int[] sizes = new int[]{1, 2, 2, 4, 8, 4, 8, 1};\r
+    /** Index of first element of above arrays referring to a numeric type */\r
+    int FIRST_NUMERIC = 0;\r
+    /** Index of last element of above arrays referring to a numeric type */\r
+    int LAST_NUMERIC = 6;\r
+    int BYTE_INDEX = 0;\r
+    int SHORT_INDEX = 1;\r
+    int CHAR_INDEX = 2;\r
+    int INT_INDEX = 3;\r
+    int LONG_INDEX = 4;\r
+    int FLOAT_INDEX = 5;\r
+    int DOUBLE_INDEX = 6;\r
+    int BOOLEAN_INDEX = 7;\r
+}\r
+\r
+\r
diff --git a/src/nom/tam/util/RandomAccess.java b/src/nom/tam/util/RandomAccess.java
new file mode 100644 (file)
index 0000000..5ddbc62
--- /dev/null
@@ -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 (file)
index 0000000..554b6f4
--- /dev/null
@@ -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 (file)
index 0000000..434f1bc
--- /dev/null
@@ -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 (file)
index 0000000..2b21e3e
--- /dev/null
@@ -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 (file)
index 0000000..df5efb0
--- /dev/null
@@ -0,0 +1,364 @@
+/*\r
+ * ArrayFuncsTest.java\r
+ * JUnit based test\r
+ *\r
+ * Created on December 2, 2007, 7:19 PM\r
+ */\r
+package nom.tam.util.test;\r
+\r
+import junit.framework.*;\r
+import java.lang.reflect.*;\r
+import java.util.Arrays;\r
+import nom.tam.util.ArrayFuncs;\r
+\r
+/**\r
+ *\r
+ * @author Thomas McGlynn\r
+ */\r
+public class ArrayFuncsTest extends TestCase {\r
+\r
+    public ArrayFuncsTest(String testName) {\r
+        super(testName);\r
+    }\r
+\r
+    protected void setUp() throws Exception {\r
+    }\r
+\r
+    protected void tearDown() throws Exception {\r
+    }\r
+\r
+    /**\r
+     * Test of computeSize method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testComputeSize() {\r
+        System.out.println("computeSize");\r
+\r
+        Object o = null;\r
+\r
+        int expResult = 0;\r
+        int result = ArrayFuncs.computeSize(o);\r
+        assertEquals(expResult, result);\r
+        int[][] x = new int[2][3];\r
+        assertEquals(ArrayFuncs.computeSize(x), 24);\r
+        assertEquals(ArrayFuncs.computeSize(new double[3]), 24);\r
+        assertEquals(ArrayFuncs.computeSize("1234"), 4);\r
+        assertEquals(ArrayFuncs.computeSize(new Object()), 0);\r
+        assertEquals(ArrayFuncs.computeSize(new Double[5]), 0);\r
+        assertEquals(ArrayFuncs.computeSize(new Double[]{\r
+                    new Double(0), new Double(1), new Double(2)}), 24);\r
+        assertEquals(ArrayFuncs.computeLSize(x), 24);\r
+        assertEquals(ArrayFuncs.computeLSize(new double[3]), 24);\r
+        assertEquals(ArrayFuncs.computeLSize("1234"), 4);\r
+        assertEquals(ArrayFuncs.computeLSize(new Object()), 0);\r
+        assertEquals(ArrayFuncs.computeLSize(new Double[5]), 0);\r
+        assertEquals(ArrayFuncs.computeLSize(new Double[]{\r
+                    new Double(0), new Double(1), new Double(2)}), 24);\r
+    }\r
+\r
+    /**\r
+     * Test of nElements method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testNElements() {\r
+        System.out.println("nElements");\r
+\r
+        Object o = null;\r
+\r
+        assertEquals(ArrayFuncs.nElements(null), 0);\r
+        assertEquals(ArrayFuncs.nElements(new int[2][2][3]), 12);\r
+        assertEquals(ArrayFuncs.nLElements(null), 0);\r
+        assertEquals(ArrayFuncs.nLElements(new int[2][2][3]), 12);\r
+    }\r
+\r
+    /**\r
+     * Test of deepClone method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testDeepClone() {\r
+        int[][] test = {{0, 1}, {2, 3}, {4, 5}};\r
+        int[][] result = (int[][]) nom.tam.util.ArrayFuncs.deepClone(test);\r
+\r
+        for (int i = 0; i < test.length; i += 1) {\r
+            for (int j = 0; j < test[i].length; j += 1) {\r
+                assertEquals(test[i][j], result[i][j]);\r
+            }\r
+        }\r
+    }\r
+\r
+    public class CloneTest implements Cloneable {\r
+\r
+        public int value = 2;\r
+\r
+        public Object clone() {\r
+            try {\r
+                return super.clone();\r
+            } catch (Exception e) {\r
+            }\r
+            return null;\r
+        }\r
+\r
+        public boolean equals(Object x) {\r
+            return (x instanceof CloneTest)\r
+                    && (((CloneTest) x).value == this.value);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Test of genericClone method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testGenericClone() {\r
+        System.out.println("genericClone");\r
+\r
+        Object o = new int[]{1, 2, 3};\r
+\r
+        Object result = nom.tam.util.ArrayFuncs.genericClone(o);\r
+\r
+        int[] x = (int[]) o;\r
+        int[] y = (int[]) result;\r
+        for (int i = 0; i < x.length; i += 1) {\r
+            assertEquals(x[i], y[i]);\r
+        }\r
+        CloneTest xa = new CloneTest();\r
+        xa.value = 4;\r
+        Object ya = ArrayFuncs.genericClone(xa);\r
+        assertTrue(xa != ya);\r
+        assertTrue(xa.equals(ya));\r
+    }\r
+\r
+    /**\r
+     * Test of copyArray method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testCopyArray() {\r
+        System.out.println("copyArray");\r
+\r
+        double[] start = new double[]{1, 2, 3, 4, 5, 6};\r
+        double[] finish = new double[6];\r
+        ArrayFuncs.copyArray(start, finish);\r
+        assertTrue(ArrayFuncs.arrayEquals(start, finish));\r
+    }\r
+\r
+    /**\r
+     * Test of getDimensions method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testGetDimensions() {\r
+        System.out.println("getDimensions");\r
+\r
+        Object o = null;\r
+        int[] expResult = null;\r
+        int[] result = nom.tam.util.ArrayFuncs.getDimensions(o);\r
+        assertEquals(expResult, result);\r
+\r
+        assertEquals(ArrayFuncs.getDimensions(new Integer(0)).length, 0);\r
+        int[][] test = new int[2][3];\r
+        int[] dims = ArrayFuncs.getDimensions(test);\r
+        assertEquals(dims.length, 2);\r
+        assertEquals(dims[0], 2);\r
+        assertEquals(dims[1], 3);\r
+    }\r
+\r
+    /**\r
+     * Test of getBaseArray method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testGetBaseArray() {\r
+\r
+        int[][][] test = new int[2][3][4];\r
+        byte b = 0;\r
+        ArrayFuncs.testPattern(test, b);\r
+\r
+        assertEquals(ArrayFuncs.getBaseArray(test), test[0][0]);\r
+    }\r
+\r
+    /**\r
+     * Test of getBaseClass method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testGetBaseClass() {\r
+        System.out.println("getBaseClass");\r
+\r
+        assertEquals(ArrayFuncs.getBaseClass(new int[2][3]), int.class);\r
+        assertEquals(ArrayFuncs.getBaseClass(new String[3]), String.class);\r
+    }\r
+\r
+    /**\r
+     * Test of getBaseLength method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testGetBaseLength() {\r
+\r
+        assertEquals(ArrayFuncs.getBaseLength(new int[2][3]), 4);\r
+        assertEquals(ArrayFuncs.getBaseLength(new double[2][3]), 8);\r
+        assertEquals(ArrayFuncs.getBaseLength(new byte[2][3]), 1);\r
+        assertEquals(ArrayFuncs.getBaseLength(new short[2][3]), 2);\r
+        assertEquals(ArrayFuncs.getBaseLength(new int[2][3]), 4);\r
+        assertEquals(ArrayFuncs.getBaseLength(new char[2][3]), 2);\r
+        assertEquals(ArrayFuncs.getBaseLength(new float[2][3]), 4);\r
+        assertEquals(ArrayFuncs.getBaseLength(new boolean[2][3]), 1);\r
+        assertEquals(ArrayFuncs.getBaseLength(new Object[2][3]), -1);\r
+    }\r
+\r
+    /**\r
+     * Test of generateArray method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testGenerateArray() {\r
+        System.out.println("generateArray");\r
+\r
+        Class baseType = int.class;\r
+        int[] dims = {2, 3, 4};\r
+\r
+        Object result = nom.tam.util.ArrayFuncs.generateArray(baseType, dims);\r
+        assertEquals(result.getClass(), int[][][].class);\r
+        int[][][] x = (int[][][]) result;\r
+        assertEquals(x.length, 2);\r
+        assertEquals(x[0].length, 3);\r
+        assertEquals(x[0][0].length, 4);\r
+\r
+    }\r
+\r
+    /**\r
+     * Test of testPattern method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testTestPattern() {\r
+        System.out.println("testPattern");\r
+\r
+        byte start = 2;\r
+        int[] arr = new int[8];\r
+\r
+        byte expResult = 0;\r
+        byte result = nom.tam.util.ArrayFuncs.testPattern(arr, start);\r
+        assertEquals(result, (byte) (start + arr.length));\r
+        assertEquals(start, arr[0]);\r
+        assertEquals(start + arr.length - 1, arr[arr.length - 1]);\r
+    }\r
+\r
+    /**\r
+     * Test of flatten method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testFlatten() {\r
+        System.out.println("flatten");\r
+\r
+        int[][][] test = new int[2][3][4];\r
+\r
+        int[] result = (int[]) ArrayFuncs.flatten(test);\r
+        assertEquals(result.length, 24);\r
+    }\r
+\r
+    /**\r
+     * Test of curl method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testCurl() {\r
+        System.out.println("curl");\r
+\r
+        int[] dimens = new int[]{2, 3, 4};\r
+        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};\r
+\r
+        int[][][] res = (int[][][]) nom.tam.util.ArrayFuncs.curl(test, dimens);\r
+        assertEquals(res.length, 2);\r
+        assertEquals(res[0].length, 3);\r
+        assertEquals(res[0][0].length, 4);\r
+        assertEquals(res[0][0][0], 0);\r
+        assertEquals(res[0][0][3], 3);\r
+        assertEquals(res[1][2][3], 23);\r
+    }\r
+\r
+    /**\r
+     * Test of mimicArray method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testMimicArray() {\r
+        System.out.println("mimicArray");\r
+\r
+        int[][] array = new int[2][3];\r
+        Class newType = double.class;\r
+\r
+        double[][] result = (double[][]) nom.tam.util.ArrayFuncs.mimicArray(array, newType);\r
+        assertEquals(result.length, array.length);\r
+        assertEquals(result[0].length, array[0].length);\r
+    }\r
+\r
+    /**\r
+     * Test of convertArray method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testConvertArray() {\r
+        System.out.println("convertArray");\r
+\r
+        int[][] array = {{1, 2, 3}, {4, 5, 6}};\r
+        Class newType = double.class;\r
+\r
+        boolean reuse = true;\r
+        double[][] dres = (double[][]) ArrayFuncs.convertArray(array, newType, reuse);\r
+        assertEquals(dres.length, array.length);\r
+        assertEquals(dres[0].length, array[0].length);\r
+\r
+        newType = int.class;\r
+        int[][] ires = (int[][]) ArrayFuncs.convertArray(array, newType, true);\r
+        assertEquals(array, ires);\r
+\r
+        ires = (int[][]) ArrayFuncs.convertArray(array, newType, false);\r
+        assertNotSame(array, ires);\r
+        assertTrue(ArrayFuncs.arrayEquals(array, ires));\r
+    }\r
+\r
+    /**\r
+     * Test of copyInto method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testCopyInto() {\r
+        System.out.println("copyInto");\r
+\r
+        int[][] x = {{2, 3, 4}, {5, 6, 7}};\r
+        double[][] y = new double[2][3];\r
+\r
+        ArrayFuncs.copyInto(x, y);\r
+\r
+        assertEquals((double) x[0][0], y[0][0]);\r
+        assertEquals((double) x[1][2], y[1][2]);\r
+    }\r
+\r
+    /**\r
+     * Test of arrayEquals method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testArrayEquals() {\r
+        System.out.println("arrayEquals");\r
+\r
+        int[][] x = {{1, 2, 3}, {4, 5, 6}};\r
+        int[][] y = {{1, 2, 3}, {4, 5, 6}};\r
+        int[][] z = {{1, 2, 3}, {4, 5, 7}};\r
+        int[][] t = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};\r
+\r
+\r
+        assertTrue(ArrayFuncs.arrayEquals(null, null));\r
+        assertFalse(ArrayFuncs.arrayEquals(null, new int[2]));\r
+        assertTrue(ArrayFuncs.arrayEquals(x, y));\r
+        assertFalse(ArrayFuncs.arrayEquals(x, z));\r
+        assertFalse(ArrayFuncs.arrayEquals(x, t));\r
+        assertTrue(ArrayFuncs.arrayEquals(x[0], z[0]));\r
+    }\r
+\r
+    /**\r
+     * Test of doubleArrayEquals method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testDoubleArrayEquals() {\r
+\r
+        double x[] = {1, 2, 3};\r
+        double y[] = {1, 2, 3};\r
+        System.out.println("doubleArrayEquals");\r
+\r
+        double tol = 0.0;\r
+\r
+        assertTrue(ArrayFuncs.doubleArrayEquals(x, y, tol));\r
+        x[0] += 1.e-14;\r
+        assertFalse(ArrayFuncs.doubleArrayEquals(x, y, tol));\r
+        tol = 1.e-13;\r
+        assertTrue(ArrayFuncs.doubleArrayEquals(x, y, tol));\r
+    }\r
+\r
+    /**\r
+     * Test of floatArrayEquals method, of class nom.tam.util.ArrayFuncs.\r
+     */\r
+    public void testFloatArrayEquals() {\r
+        float x[] = {1f, 2f, 3f};\r
+        float y[] = {1f, 2f, 3f};\r
+        System.out.println("floatArrayEquals");\r
+\r
+        float tol = 0.0F;\r
+        assertTrue(ArrayFuncs.floatArrayEquals(x, y, tol));\r
+        x[0] += 1.e-6f;\r
+        assertFalse(ArrayFuncs.floatArrayEquals(x, y, tol));\r
+        tol = 1.e-5f;\r
+        assertTrue(ArrayFuncs.floatArrayEquals(x, y, tol));\r
+    }\r
+}\r
diff --git a/src/nom/tam/util/test/BigFileTest.java b/src/nom/tam/util/test/BigFileTest.java
new file mode 100644 (file)
index 0000000..81f40a3
--- /dev/null
@@ -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 (file)
index 0000000..55d1d77
--- /dev/null
@@ -0,0 +1,867 @@
+/* Copyright: Thomas McGlynn 1999.\r
+ * This code may be used for any purpose, non-commercial\r
+ * or commercial so long as this copyright notice is retained\r
+ * in the source code or included in or referred to in any\r
+ * derived software.\r
+ */\r
+package nom.tam.util.test;\r
+\r
+import org.junit.Test;\r
+import static org.junit.Assert.assertEquals;\r
+import junit.framework.JUnit4TestAdapter;\r
+\r
+import nom.tam.util.*;\r
+import nom.tam.util.ArrayFuncs;\r
+import java.io.*;\r
+\r
+/** This class provides runs tests of the\r
+ *  BufferedI/O classes: BufferedFile, BufferedDataInputStream\r
+ *  and BufferedDataOutputStream.  A limited comparison\r
+ *  to the standard I/O classes can also be made.\r
+ *  <p>\r
+ *  Input and output of all primitive scalar and array types is\r
+ *  tested, however input and output of String data is not.\r
+ *  Users may choose to test the BufferedFile class, the\r
+ *  BufferedDataXPUT classes array methods, the BufferedDataXPUT\r
+ *  classes using the methods of DataXput, the traditional\r
+ *  I/O classes, or any combination thereof.\r
+ */\r
+public class BufferedFileTester {\r
+\r
+    /** Usage: java nom.tam.util.test.BufferedFileTester file [dim [iter [flags]]]\r
+     *         where\r
+     *        file     is the file to be read and written.\r
+     *         dim     is the dimension of the arrays to be written.\r
+     *         iter    is the number of times each array is written.\r
+     *         flags   a string indicating what I/O to test\r
+     *                  O  -- test old I/O (RandomAccessFile and standard streams)\r
+     *                  R  -- BufferedFile (i.e., random access)\r
+     *                  S  -- BufferedDataXPutStream\r
+     *                  X  -- BufferedDataXPutStream using standard methods\r
+     */\r
+    public static void main(String[] args) throws Exception {\r
+\r
+        String filename = args[0];\r
+        int dim = 1000;\r
+        if (args.length > 1) {\r
+            dim = Integer.parseInt(args[1]);\r
+        }\r
+        int iter = 1;\r
+        if (args.length > 2) {\r
+            iter = Integer.parseInt(args[2]);\r
+        }\r
+\r
+        System.out.println("Allocating arrays.");\r
+        double[] db = new double[dim];\r
+        float[] fl = new float[dim];\r
+        int[] in = new int[dim];\r
+        long[] ln = new long[dim];\r
+        short[] sh = new short[dim];\r
+        byte[] by = new byte[dim];\r
+        char[] ch = new char[dim];\r
+        boolean[] bl = new boolean[dim];\r
+\r
+        System.out.println("Initializing arrays -- may take a while");\r
+        int sign = 1;\r
+        for (int i = 0; i < dim; i += 1) {\r
+\r
+\r
+            double x = sign * Math.pow(10., 20 * Math.random() - 10);\r
+            db[i] = x;\r
+            fl[i] = (float) x;\r
+\r
+            if (Math.abs(x) < 1) {\r
+                x = 1 / x;\r
+            }\r
+\r
+            in[i] = (int) x;\r
+            ln[i] = (long) x;\r
+            sh[i] = (short) x;\r
+            by[i] = (byte) x;\r
+            ch[i] = (char) x;\r
+            bl[i] = x > 0;\r
+\r
+            sign = -sign;\r
+        }\r
+\r
+        // Ensure special values are tested.\r
+\r
+        by[0] = Byte.MIN_VALUE;\r
+        by[1] = Byte.MAX_VALUE;\r
+        by[2] = 0;\r
+        ch[0] = Character.MIN_VALUE;\r
+        ch[1] = Character.MAX_VALUE;\r
+        ch[2] = 0;\r
+        sh[0] = Short.MAX_VALUE;\r
+        sh[1] = Short.MIN_VALUE;\r
+        sh[0] = 0;\r
+        in[0] = Integer.MAX_VALUE;\r
+        in[1] = Integer.MIN_VALUE;\r
+        in[2] = 0;\r
+        ln[0] = Long.MIN_VALUE;\r
+        ln[1] = Long.MAX_VALUE;\r
+        ln[2] = 0;\r
+        fl[0] = Float.MIN_VALUE;\r
+        fl[1] = Float.MAX_VALUE;\r
+        fl[2] = Float.POSITIVE_INFINITY;\r
+        fl[3] = Float.NEGATIVE_INFINITY;\r
+        fl[4] = Float.NaN;\r
+        fl[5] = 0;\r
+        db[0] = Double.MIN_VALUE;\r
+        db[1] = Double.MAX_VALUE;\r
+        db[2] = Double.POSITIVE_INFINITY;\r
+        db[3] = Double.NEGATIVE_INFINITY;\r
+        db[4] = Double.NaN;\r
+        db[5] = 0;\r
+\r
+        double[] db2 = new double[dim];\r
+        float[] fl2 = new float[dim];\r
+        int[] in2 = new int[dim];\r
+        long[] ln2 = new long[dim];\r
+        short[] sh2 = new short[dim];\r
+        byte[] by2 = new byte[dim];\r
+        char[] ch2 = new char[dim];\r
+        boolean[] bl2 = new boolean[dim];\r
+\r
+        int[][][][] multi = new int[10][10][10][10];\r
+        int[][][][] multi2 = new int[10][10][10][10];\r
+        for (int i = 0; i < 10; i += 1) {\r
+            multi[i][i][i][i] = i;\r
+        }\r
+\r
+        if (args.length < 4 || args[3].indexOf('O') >= 0) {\r
+            standardFileTest(filename, iter, in, in2);\r
+            standardStreamTest(filename, iter, in, in2);\r
+        }\r
+\r
+        if (args.length < 4 || args[3].indexOf('X') >= 0) {\r
+            buffStreamSimpleTest(filename, iter, in, in2);\r
+        }\r
+\r
+        if (args.length < 4 || args[3].indexOf('R') >= 0) {\r
+            bufferedFileTest(filename, iter, db, db2, fl, fl2, ln, ln2, in, in2, sh, sh2,\r
+                    ch, ch2, by, by2, bl, bl2, multi, multi2);\r
+        }\r
+\r
+\r
+        if (args.length < 4 || args[3].indexOf('S') >= 0) {\r
+            bufferedStreamTest(filename, iter, db, db2, fl, fl2, ln, ln2, in, in2, sh, sh2,\r
+                    ch, ch2, by, by2, bl, bl2, multi, multi2);\r
+        }\r
+    }\r
+\r
+    public static void standardFileTest(String filename, int iter, int[] in, int[] in2)\r
+            throws Exception {\r
+        System.out.println("Standard I/O library: java.io.RandomAccessFile");\r
+\r
+        RandomAccessFile f = new RandomAccessFile(filename, "rw");\r
+        int dim = in.length;\r
+        resetTime();\r
+        f.seek(0);\r
+        for (int j = 0; j < iter; j += 1) {\r
+            for (int i = 0; i < dim; i += 1) {\r
+                f.writeInt(in[i]);\r
+            }\r
+        }\r
+        System.out.println("  RAF Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+        f.seek(0);\r
+        resetTime();\r
+        for (int j = 0; j < iter; j += 1) {\r
+            for (int i = 0; i < dim; i += 1) {\r
+                in2[i] = f.readInt();\r
+            }\r
+        }\r
+        System.out.println("  RAF Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+\r
+\r
+        synchronized (f) {\r
+            f.seek(0);\r
+            for (int j = 0; j < iter; j += 1) {\r
+                for (int i = 0; i < dim; i += 1) {\r
+                    f.writeInt(in[i]);\r
+                }\r
+            }\r
+            System.out.println("  SyncRAF Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+            f.seek(0);\r
+            resetTime();\r
+            for (int j = 0; j < iter; j += 1) {\r
+                for (int i = 0; i < dim; i += 1) {\r
+                    in2[i] = f.readInt();\r
+                }\r
+            }\r
+        }\r
+        System.out.println("  SyncRAF Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+    }\r
+\r
+    public static void standardStreamTest(String filename, int iter, int[] in, int[] in2)\r
+            throws Exception {\r
+        System.out.println("Standard I/O library: java.io.DataXXputStream");\r
+        System.out.println("                      layered atop a BufferedXXputStream");\r
+\r
+        DataOutputStream f = new DataOutputStream(new BufferedOutputStream(\r
+                new FileOutputStream(filename), 32768));\r
+        resetTime();\r
+        int dim = in.length;\r
+        for (int j = 0; j < iter; j += 1) {\r
+            for (int i = 0; i < dim; i += 1) {\r
+                f.writeInt(in[i]);\r
+            }\r
+        }\r
+        f.flush();\r
+        f.close();\r
+        System.out.println("  DIS Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+\r
+        DataInputStream is = new DataInputStream(new BufferedInputStream(\r
+                new FileInputStream(filename), 32768));\r
+        resetTime();\r
+        for (int j = 0; j < iter; j += 1) {\r
+            for (int i = 0; i < dim; i += 1) {\r
+                in2[i] = is.readInt();\r
+            }\r
+        }\r
+        System.out.println("  DIS Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+\r
+\r
+        f = new DataOutputStream(new BufferedOutputStream(\r
+                new FileOutputStream(filename), 32768));\r
+        resetTime();\r
+        dim = in.length;\r
+        synchronized (f) {\r
+            for (int j = 0; j < iter; j += 1) {\r
+                for (int i = 0; i < dim; i += 1) {\r
+                    f.writeInt(in[i]);\r
+                }\r
+            }\r
+            f.flush();\r
+            f.close();\r
+            System.out.println("  DIS Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+\r
+            is = new DataInputStream(new BufferedInputStream(\r
+                    new FileInputStream(filename), 32768));\r
+            resetTime();\r
+            for (int j = 0; j < iter; j += 1) {\r
+                for (int i = 0; i < dim; i += 1) {\r
+                    in2[i] = is.readInt();\r
+                }\r
+            }\r
+        }\r
+        System.out.println("  DIS Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+    }\r
+\r
+    public static void buffStreamSimpleTest(String filename, int iter, int[] in, int[] in2)\r
+            throws Exception {\r
+\r
+        System.out.println("New libraries:  nom.tam.BufferedDataXXputStream");\r
+        System.out.println("                Using non-array I/O");\r
+        BufferedDataOutputStream f = new BufferedDataOutputStream(\r
+                new FileOutputStream(filename), 32768);\r
+        resetTime();\r
+        int dim = in.length;\r
+        for (int j = 0; j < iter; j += 1) {\r
+            for (int i = 0; i < dim; i += 1) {\r
+                f.writeInt(in[i]);\r
+            }\r
+        }\r
+        f.flush();\r
+        f.close();\r
+        System.out.println("  BDS Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+\r
+        BufferedDataInputStream is = new BufferedDataInputStream(new BufferedInputStream(\r
+                new FileInputStream(filename), 32768));\r
+        resetTime();\r
+        for (int j = 0; j < iter; j += 1) {\r
+            for (int i = 0; i < dim; i += 1) {\r
+                in2[i] = is.readInt();\r
+            }\r
+        }\r
+        System.out.println("  BDS Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+    }\r
+\r
+    public static void bufferedStreamTest(String filename, int iter, double[] db, double[] db2,\r
+            float[] fl, float[] fl2, long[] ln, long[] ln2,\r
+            int[] in, int[] in2, short[] sh, short[] sh2,\r
+            char[] ch, char[] ch2, byte[] by, byte[] by2,\r
+            boolean[] bl, boolean[] bl2,\r
+            int[][][][] multi, int[][][][] multi2) throws Exception {\r
+\r
+        int dim = db.length;\r
+\r
+        double ds = Math.random() - 0.5;\r
+        double ds2;\r
+        float fs = (float) (Math.random() - 0.5);\r
+        float fs2;\r
+        int is = (int) (1000000 * (Math.random() - 500000));\r
+        int is2;\r
+        long ls = (long) (100000000000L * (Math.random() - 50000000000L));\r
+        long ls2;\r
+        short ss = (short) (60000 * (Math.random() - 30000));\r
+        short ss2;\r
+        char cs = (char) (60000 * Math.random());\r
+        char cs2;\r
+        byte bs = (byte) (256 * Math.random() - 128);\r
+        byte bs2;\r
+        boolean bls = (Math.random() > 0.5);\r
+        boolean bls2;\r
+        System.out.println("New libraries: nom.tam.util.BufferedDataXXputStream");\r
+        System.out.println("               Using array I/O methods");\r
+\r
+        {\r
+            BufferedDataOutputStream f = new BufferedDataOutputStream(new FileOutputStream(filename));\r
+\r
+            resetTime();\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(db);\r
+            }\r
+            System.out.println("  BDS Dbl write: " + (8 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(fl);\r
+            }\r
+            System.out.println("  BDS Flt write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(in);\r
+            }\r
+            System.out.println("  BDS Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(ln);\r
+            }\r
+            System.out.println("  BDS Lng write: " + (8 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(sh);\r
+            }\r
+            System.out.println("  BDS Sht write: " + (2 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(ch);\r
+            }\r
+            System.out.println("  BDS Chr write: " + (2 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray((byte[]) by);\r
+            }\r
+            System.out.println("  BDS Byt write: " + (1 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.writeArray(bl);\r
+            }\r
+            System.out.println("  BDS Boo write: " + (1 * dim * iter) / (1000 * deltaTime()));\r
+\r
+            f.writeByte(bs);\r
+            f.writeChar(cs);\r
+            f.writeShort(ss);\r
+            f.writeInt(is);\r
+            f.writeLong(ls);\r
+            f.writeFloat(fs);\r
+            f.writeDouble(ds);\r
+            f.writeBoolean(bls);\r
+\r
+            f.writeArray(multi);\r
+            f.flush();\r
+            f.close();\r
+        }\r
+\r
+        {\r
+            BufferedDataInputStream f = new BufferedDataInputStream(new FileInputStream(filename));\r
+\r
+\r
+            resetTime();\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(db2);\r
+            }\r
+            System.out.println("  BDS Dbl read:  " + (8 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(fl2);\r
+            }\r
+            System.out.println("  BDS Flt read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(in2);\r
+            }\r
+            System.out.println("  BDS Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(ln2);\r
+            }\r
+            System.out.println("  BDS Lng read:  " + (8 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(sh2);\r
+            }\r
+            System.out.println("  BDS Sht read:  " + (2 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(ch2);\r
+            }\r
+            System.out.println("  BDS Chr read:  " + (2 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray((byte[]) by2);\r
+            }\r
+            System.out.println("  BDS Byt read:  " + (1 * dim * iter) / (1000 * deltaTime()));\r
+            for (int i = 0; i < iter; i += 1) {\r
+                f.readLArray(bl2);\r
+            }\r
+            System.out.println("  BDS Boo read:  " + (1 * dim * iter) / (1000 * deltaTime()));\r
+\r
+            bs2 = f.readByte();\r
+            cs2 = f.readChar();\r
+            ss2 = f.readShort();\r
+            is2 = f.readInt();\r
+            ls2 = f.readLong();\r
+            fs2 = f.readFloat();\r
+            ds2 = f.readDouble();\r
+            bls2 = f.readBoolean();\r
+\r
+            for (int i = 0; i < 10; i += 1) {\r
+                multi2[i][i][i][i] = 0;\r
+            }\r
+\r
+            // Now read only pieces of the multidimensional array.\r
+            for (int i = 0; i < 5; i += 1) {\r
+                System.out.println("Multiread:" + i);\r
+                // Skip the odd initial indices and\r
+                // read the evens.\r
+                f.skipBytes(4000);\r
+                f.readLArray(multi2[2 * i + 1]);\r
+            }\r
+            f.close();\r
+        }\r
+\r
+        System.out.println("Stream Verification:");\r
+        System.out.println("  An error should be reported for double and float NaN's");\r
+        System.out.println("  Arrays:");\r
+\r
+        for (int i = 0; i < dim; i += 1) {\r
+\r
+            if (db[i] != db2[i]) {\r
+                System.out.println("     Double error at " + i + " " + db[i] + " " + db2[i]);\r
+            }\r
+            if (fl[i] != fl2[i]) {\r
+                System.out.println("     Float error at " + i + " " + fl[i] + " " + fl2[i]);\r
+            }\r
+            if (in[i] != in2[i]) {\r
+                System.out.println("     Int error at " + i + " " + in[i] + " " + in2[i]);\r
+            }\r
+            if (ln[i] != ln2[i]) {\r
+                System.out.println("     Long error at " + i + " " + ln[i] + " " + ln2[i]);\r
+            }\r
+            if (sh[i] != sh2[i]) {\r
+                System.out.println("     Short error at " + i + " " + sh[i] + " " + sh2[i]);\r
+            }\r
+            if (ch[i] != ch2[i]) {\r
+                System.out.println("     Char error at " + i + " " + (int) ch[i] + " " + (int) ch2[i]);\r
+            }\r
+            if (by[i] != by2[i]) {\r
+                System.out.println("     Byte error at " + i + " " + by[i] + " " + by2[i]);\r
+            }\r
+            if (bl[i] != bl2[i]) {\r
+                System.out.println("     Bool error at " + i + " " + bl[i] + " " + bl2[i]);\r
+            }\r
+        }\r
+\r
+        System.out.println("  Scalars:");\r
+        // Check the scalars.\r
+        if (bls != bls2) {\r
+            System.out.println("     Bool Scalar mismatch:" + bls + " " + bls2);\r
+        }\r
+        if (bs != bs2) {\r
+            System.out.println("     Byte Scalar mismatch:" + bs + " " + bs2);\r
+        }\r
+        if (cs != cs2) {\r
+            System.out.println("     Char Scalar mismatch:" + (int) cs + " " + (int) cs2);\r
+        }\r
+        if (ss != ss2) {\r
+            System.out.println("     Short Scalar mismatch:" + ss + " " + ss2);\r
+        }\r
+        if (is != is2) {\r
+            System.out.println("     Int Scalar mismatch:" + is + " " + is2);\r
+        }\r
+        if (ls != ls2) {\r
+            System.out.println("     Long Scalar mismatch:" + ls + " " + ls2);\r
+        }\r
+        if (fs != fs2) {\r
+            System.out.println("     Float Scalar mismatch:" + fs + " " + fs2);\r
+        }\r
+        if (ds != ds2) {\r
+            System.out.println("     Double Scalar mismatch:" + ds + " " + ds2);\r
+        }\r
+\r
+        System.out.println("  Multi: odd rows should match");\r
+        for (int i = 0; i < 10; i += 1) {\r
+            System.out.println("      " + i + " " + multi[i][i][i][i] + " " + multi2[i][i][i][i]);\r
+        }\r
+        System.out.println("Done BufferedStream Tests");\r
+    }\r
+\r
+    public static void bufferedFileTest(String filename, int iter, double[] db, double[] db2,\r
+            float[] fl, float[] fl2, long[] ln, long[] ln2,\r
+            int[] in, int[] in2, short[] sh, short[] sh2,\r
+            char[] ch, char[] ch2, byte[] by, byte[] by2,\r
+            boolean[] bl, boolean[] bl2,\r
+            int[][][][] multi, int[][][][] multi2) throws Exception {\r
+\r
+\r
+        int dim = db.length;\r
+\r
+        double ds = Math.random() - 0.5;\r
+        double ds2;\r
+        float fs = (float) (Math.random() - 0.5);\r
+        float fs2;\r
+        int is = (int) (1000000 * (Math.random() - 500000));\r
+        int is2;\r
+        long ls = (long) (100000000000L * (Math.random() - 50000000000L));\r
+        long ls2;\r
+        short ss = (short) (60000 * (Math.random() - 30000));\r
+        short ss2;\r
+        char cs = (char) (60000 * Math.random());\r
+        char cs2;\r
+        byte bs = (byte) (256 * Math.random() - 128);\r
+        byte bs2;\r
+        boolean bls = (Math.random() > 0.5);\r
+        boolean bls2;\r
+\r
+        System.out.println("New libraries: nom.tam.util.BufferedFile");\r
+        System.out.println("               Using array I/O methods.");\r
+\r
+        BufferedFile f = new BufferedFile(filename, "rw");\r
+\r
+        resetTime();\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(db);\r
+        }\r
+        System.out.println("  BF  Dbl write: " + (8 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(fl);\r
+        }\r
+        System.out.println("  BF  Flt write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(in);\r
+        }\r
+        System.out.println("  BF  Int write: " + (4 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(ln);\r
+        }\r
+        System.out.println("  BF  Lng write: " + (8 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(sh);\r
+        }\r
+        System.out.println("  BF  Sht write: " + (2 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(ch);\r
+        }\r
+        System.out.println("  BF  Chr write: " + (2 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(by);\r
+        }\r
+        System.out.println("  BF  Byt write: " + (1 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.writeArray(bl);\r
+        }\r
+        System.out.println("  BF  Boo write: " + (1 * dim * iter) / (1000 * deltaTime()));\r
+\r
+        f.writeByte(bs);\r
+        f.writeChar(cs);\r
+        f.writeShort(ss);\r
+        f.writeInt(is);\r
+        f.writeLong(ls);\r
+        f.writeFloat(fs);\r
+        f.writeDouble(ds);\r
+        f.writeBoolean(bls);\r
+\r
+        f.writeArray(multi);\r
+        f.seek(0);\r
+\r
+\r
+        resetTime();\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(db2);\r
+        }\r
+        System.out.println("  BF  Dbl read:  " + (8 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(fl2);\r
+        }\r
+        System.out.println("  BF  Flt read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(in2);\r
+        }\r
+        System.out.println("  BF  Int read:  " + (4 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(ln2);\r
+        }\r
+        System.out.println("  BF  Lng read:  " + (8 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(sh2);\r
+        }\r
+        System.out.println("  BF  Sht read:  " + (2 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(ch2);\r
+        }\r
+        System.out.println("  BF  Chr read:  " + (2 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(by2);\r
+        }\r
+        System.out.println("  BF  Byt read:  " + (1 * dim * iter) / (1000 * deltaTime()));\r
+        for (int i = 0; i < iter; i += 1) {\r
+            f.readLArray(bl2);\r
+        }\r
+        System.out.println("  BF  Boo read:  " + (1 * dim * iter) / (1000 * deltaTime()));\r
+\r
+        bs2 = f.readByte();\r
+        cs2 = f.readChar();\r
+        ss2 = f.readShort();\r
+        is2 = f.readInt();\r
+        ls2 = f.readLong();\r
+        fs2 = f.readFloat();\r
+        ds2 = f.readDouble();\r
+        bls2 = f.readBoolean();\r
+\r
+        // Now read only pieces of the multidimensional array.\r
+        for (int i = 0; i < 5; i += 1) {\r
+            // Skip the odd initial indices and\r
+            // read the evens.\r
+            f.skipBytes(4000);\r
+            f.readLArray(multi2[2 * i + 1]);\r
+        }\r
+\r
+        System.out.println("BufferedFile Verification:");\r
+        System.out.println("  An error should be reported for double and float NaN's");\r
+        System.out.println("  Arrays:");\r
+\r
+        for (int i = 0; i < dim; i += 1) {\r
+\r
+            if (db[i] != db2[i]) {\r
+                System.out.println("     Double error at " + i + " " + db[i] + " " + db2[i]);\r
+            }\r
+            if (fl[i] != fl2[i]) {\r
+                System.out.println("     Float error at " + i + " " + fl[i] + " " + fl2[i]);\r
+            }\r
+            if (in[i] != in2[i]) {\r
+                System.out.println("     Int error at " + i + " " + in[i] + " " + in2[i]);\r
+            }\r
+            if (ln[i] != ln2[i]) {\r
+                System.out.println("     Long error at " + i + " " + ln[i] + " " + ln2[i]);\r
+            }\r
+            if (sh[i] != sh2[i]) {\r
+                System.out.println("     Short error at " + i + " " + sh[i] + " " + sh2[i]);\r
+            }\r
+            if (ch[i] != ch2[i]) {\r
+                System.out.println("     Char error at " + i + " " + (int) ch[i] + " " + (int) ch2[i]);\r
+            }\r
+            if (by[i] != by2[i]) {\r
+                System.out.println("     Byte error at " + i + " " + by[i] + " " + by2[i]);\r
+            }\r
+            if (bl[i] != bl2[i]) {\r
+                System.out.println("     Bool error at " + i + " " + bl[i] + " " + bl2[i]);\r
+            }\r
+        }\r
+\r
+        System.out.println("  Scalars:");\r
+        // Check the scalars.\r
+        if (bls != bls2) {\r
+            System.out.println("     Bool Scalar mismatch:" + bls + " " + bls2);\r
+        }\r
+        if (bs != bs2) {\r
+            System.out.println("     Byte Scalar mismatch:" + bs + " " + bs2);\r
+        }\r
+        if (cs != cs2) {\r
+            System.out.println("     Char Scalar mismatch:" + (int) cs + " " + (int) cs2);\r
+        }\r
+        if (ss != ss2) {\r
+            System.out.println("     Short Scalar mismatch:" + ss + " " + ss2);\r
+        }\r
+        if (is != is2) {\r
+            System.out.println("     Int Scalar mismatch:" + is + " " + is2);\r
+        }\r
+        if (ls != ls2) {\r
+            System.out.println("     Long Scalar mismatch:" + ls + " " + ls2);\r
+        }\r
+        if (fs != fs2) {\r
+            System.out.println("     Float Scalar mismatch:" + fs + " " + fs2);\r
+        }\r
+        if (ds != ds2) {\r
+            System.out.println("     Double Scalar mismatch:" + ds + " " + ds2);\r
+        }\r
+\r
+        System.out.println("  Multi: odd rows should match");\r
+        for (int i = 0; i < 10; i += 1) {\r
+            System.out.println("      " + i + " " + multi[i][i][i][i] + " " + multi2[i][i][i][i]);\r
+        }\r
+        System.out.println("Done BufferedFile Tests");\r
+    }\r
+    static long lastTime;\r
+\r
+    static void resetTime() {\r
+        lastTime = new java.util.Date().getTime();\r
+    }\r
+\r
+    static double deltaTime() {\r
+        long time = lastTime;\r
+        lastTime = new java.util.Date().getTime();\r
+        return (lastTime - time) / 1000.;\r
+    }\r
+\r
+    @Test\r
+    public void testBufferedFile() throws Exception {\r
+\r
+        double[][] td = new double[100][600];\r
+        for (int i = 0; i < 100; i += 1) {\r
+            for (int j = 0; j < 600; j += 1) {\r
+                td[i][j] = i + 2 * j;\r
+            }\r
+        }\r
+        int[][][] ti = new int[5][4][3];\r
+        for (int i = 0; i < 5; i += 1) {\r
+            for (int j = 0; j < 4; j += 1) {\r
+                for (int k = 0; k < 3; k += 1) {\r
+                    ti[i][j][k] = i * j * k;\r
+                }\r
+            }\r
+        }\r
+\r
+        float[][] tf = new float[10][];\r
+        for (int i = 0; i < 10; i += 1) {\r
+            tf[i] = new float[i];\r
+            for (int j = 0; j < i; j += 1) {\r
+                tf[i][j] = (float) Math.sin(i * j);\r
+            }\r
+        }\r
+\r
+        boolean[] tb = new boolean[100];\r
+        for (int i = 2; i < 100; i += 1) {\r
+            tb[i] = !tb[i - 1];\r
+        }\r
+\r
+        short[][] ts = new short[5][5];\r
+        ts[2][2] = 222;\r
+\r
+        byte[] tbyte = new byte[1024];\r
+        for (int i = 0; i < tbyte.length; i += 1) {\r
+            tbyte[i] = (byte) i;\r
+        }\r
+\r
+        char[] tc = new char[10];\r
+        tc[3] = 'c';\r
+\r
+        long[][][] tl0 = new long[1][1][1];\r
+        long[][][] tl1 = new long[1][1][0];\r
+\r
+        BufferedFile bf = new BufferedFile("jtest.fil", "rw");\r
+\r
+        bf.writeArray(td);\r
+        bf.writeArray(tf);\r
+        bf.writeArray(ti);\r
+        bf.writeArray(ts);\r
+        bf.writeArray(tb);\r
+        bf.writeArray(tbyte);\r
+        bf.writeArray(tc);\r
+        bf.writeArray(tl0);\r
+        bf.writeArray(tl1);\r
+        bf.writeArray(ts);\r
+\r
+        bf.close();\r
+\r
+        bf = new BufferedFile("jtest.fil", "r");\r
+\r
+        boolean thrown = false;\r
+\r
+        try {\r
+            bf.writeArray(td);\r
+        } catch (Exception e) {\r
+            thrown = true;\r
+        }\r
+        assertEquals("BufferedFile protections", true, thrown);\r
+        try {\r
+            bf.close();\r
+        } catch (Exception e) {\r
+        }\r
+\r
+        bf = new BufferedFile("jtest.fil", "r");\r
+\r
+        testArray(bf, "double", td);\r
+        testArray(bf, "float", tf);\r
+        testArray(bf, "int", ti);\r
+        testArray(bf, "short", ts);\r
+        testArray(bf, "bool", tb);\r
+        testArray(bf, "byte", tbyte);\r
+        testArray(bf, "char", tc);\r
+        testArray(bf, "long1", tl0);\r
+        testArray(bf, "longnull", tl1);\r
+        testArray(bf, "short2", ts);\r
+    }\r
+\r
+    @Test\r
+    public void testBufferedStreams() throws Exception {\r
+\r
+        double[][] td = new double[100][600];\r
+        for (int i = 0; i < 100; i += 1) {\r
+            for (int j = 0; j < 600; j += 1) {\r
+                td[i][j] = i + 2 * j;\r
+            }\r
+        }\r
+        int[][][] ti = new int[5][4][3];\r
+        for (int i = 0; i < 5; i += 1) {\r
+            for (int j = 0; j < 4; j += 1) {\r
+                for (int k = 0; k < 3; k += 1) {\r
+                    ti[i][j][k] = i * j * k;\r
+                }\r
+            }\r
+        }\r
+\r
+        float[][] tf = new float[10][];\r
+        for (int i = 0; i < 10; i += 1) {\r
+            tf[i] = new float[i];\r
+            for (int j = 0; j < i; j += 1) {\r
+                tf[i][j] = (float) Math.sin(i * j);\r
+            }\r
+        }\r
+\r
+        boolean[] tb = new boolean[100];\r
+        for (int i = 2; i < 100; i += 1) {\r
+            tb[i] = !tb[i - 1];\r
+        }\r
+\r
+        short[][] ts = new short[5][5];\r
+        ts[2][2] = 222;\r
+\r
+        byte[] tbyte = new byte[1024];\r
+        for (int i = 0; i < tbyte.length; i += 1) {\r
+            tbyte[i] = (byte) i;\r
+        }\r
+\r
+        char[] tc = new char[10];\r
+        tc[3] = 'c';\r
+\r
+        long[][][] tl0 = new long[1][1][1];\r
+        long[][][] tl1 = new long[1][1][0];\r
+\r
+        BufferedDataOutputStream bf = new BufferedDataOutputStream(\r
+                new FileOutputStream("jtest.fil"));\r
+\r
+        bf.writeArray(td);\r
+        bf.writeArray(tf);\r
+        bf.writeArray(ti);\r
+        bf.writeArray(ts);\r
+        bf.writeArray(tb);\r
+        bf.writeArray(tbyte);\r
+        bf.writeArray(tc);\r
+        bf.writeArray(tl0);\r
+        bf.writeArray(tl1);\r
+        bf.writeArray(ts);\r
+\r
+        bf.close();\r
+\r
+        BufferedDataInputStream bi = new BufferedDataInputStream(\r
+                new FileInputStream("jtest.fil"));\r
+\r
+        testArray(bi, "sdouble", td);\r
+        testArray(bi, "sfloat", tf);\r
+        testArray(bi, "sint", ti);\r
+        testArray(bi, "sshort", ts);\r
+        testArray(bi, "sbool", tb);\r
+        testArray(bi, "sbyte", tbyte);\r
+        testArray(bi, "schar", tc);\r
+        testArray(bi, "slong1", tl0);\r
+        testArray(bi, "slongnull", tl1);\r
+        testArray(bi, "sshort2", ts);\r
+    }\r
+\r
+    void testArray(ArrayDataInput bf, String label, Object array) throws Exception {\r
+        Object newArray = ArrayFuncs.mimicArray(array, ArrayFuncs.getBaseClass(array));\r
+        bf.readLArray(newArray);\r
+        boolean state = ArrayFuncs.arrayEquals(array, newArray);\r
+        assertEquals(label, true, state);\r
+    }\r
+}\r
+\r
+\r
diff --git a/src/nom/tam/util/test/ByteFormatParseTest.java b/src/nom/tam/util/test/ByteFormatParseTest.java
new file mode 100644 (file)
index 0000000..3c811ac
--- /dev/null
@@ -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 (file)
index 0000000..fcc7c08
--- /dev/null
@@ -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.
+ *  <p>
+ *  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.
+ *  <p>
+ *  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.
+ *  <p>
+ *  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());
+        }
+    }
+}