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) != ' ') {
+            &n