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.*; import nom.tam.util.*; import java.lang.reflect.Array; import java.util.Vector; /** This class defines the methods for accessing FITS binary table data. */ public class BinaryTable extends Data { /** This is the area in which variable length column data lives. */ byte[] hashArea = new byte[0]; /** Pointer to the next available byte in the hashArea. */ int hashPtr = 0; /** The columns of data in the binary table. */ Vector columns; /** The sizes of each column (in number of entries per row) */ int[] sizes; /** The dimensions of each column. */ int[][] dimens; /** An example of the structure of a row */ Object[] modelRow; /** Where the data is actually stored. */ ColumnTable table; /** Create a null binary table data segment. */ public BinaryTable() throws FitsException { columns = new Vector(); try { table = new ColumnTable(new Object[0], new int[0]); } catch (TableException e) { throw new FitsException("Unable to create table:"+e); } dataArray = table; dimens = new int[0][]; sizes = new int[0]; modelRow = new Object[0]; } /** Create a binary table from given header information. * * @param header A header describing what the binary * table should look like. */ public BinaryTable(Header myHeader) throws FitsException { BinaryTableHeaderParser parser = new BinaryTableHeaderParser(myHeader); modelRow = parser.getModelRow(); int naxis2 = myHeader.getIntValue("NAXIS2"); int nfields = myHeader.getIntValue("TFIELDS"); columns = new Vector(nfields); sizes = new int[nfields]; dimens = new int[nfields][]; modelRow = parser.getModelRow(); int size = useModelRow(modelRow, naxis2); Object[] arrCol= new Object[nfields]; columns.copyInto(arrCol); try { table = new ColumnTable(arrCol, sizes); } catch (TableException e) { throw new FitsException("Unable to create table:"+e); } int hsize = myHeader.trueDataSize(); // If the computed size of the data array is less than the size that // the header indicates for the table, then there must be a hash // area for variable length data. if (size < hsize) { hashArea = new byte[hsize-size]; hashPtr = hashArea.length; } else if (size > hsize) { // This implies an error in the user's value of NAXIS1 (or a bug!) throw new FitsException("Size inconsistency in header and data: Check NAXIS1 and PCOUNT"); } } /** Use an example row to determine what the table * should look like. * @param model An example of a row. Each element of model should * have the structure of the corresponding element of the row. * @param nrow The number of rows in the table. * @return The size of the table in bytes. */ protected int useModelRow(Object[] model, int nrow) { int totalsize = 0; for (int col=0; col < model.length; col += 1) { Class base = ArrayFuncs.getBaseClass(model[col]); dimens[col] = ArrayFuncs.getDimensions(model[col]); int size = 1; for (int dim=0; dim < dimens[col].length; dim += 1) { size *= dimens[col][dim]; } sizes[col] = size; Object array = Array.newInstance(base, size*nrow); columns.addElement(array); totalsize += size*nrow*ArrayFuncs.getBaseLength(model[col]); } return totalsize; } /** Create a binary table from existing data in row order. * * @param data The data used to initialize the binary table. */ public BinaryTable(Object[][] data) throws FitsException { modelRow = data[0]; dimens = new int[modelRow.length][]; sizes = new int[modelRow.length]; columns = new Vector(modelRow.length); useModelRow(modelRow, data.length); hashPtr = 0; // We've set up the structure but now we need to go through and // move information from data to the arrays in // column. rowToColumn(data); Object[] ocols = new Object[columns.size()]; columns.copyInto(ocols); try { table = new ColumnTable(ocols, sizes); } catch (TableException e) { throw new FitsException("Error creating ColumnTable"); } } /** Convert the data from a row to a flattened column format. * @param data The table data in row/column format. */ private void rowToColumn(Object[][] data) { for (int col=0; col<modelRow.length; col += 1) { Object column = columns.elementAt(col); for (int row=0; row<data.length; row += 1) { System.arraycopy( ArrayFuncs.flatten(data[row][col]), 0, column, row*sizes[col], sizes[col] ); } } } /** Get a given row * @param row The index of the row to be returned. * @return A row of data. */ public Object[] getRow(int row) throws FitsException { if (!validRow(row)) { throw new FitsException("Invalid row"); } Object[] data = new Object[modelRow.length]; for (int col=0; col<modelRow.length; col += 1) { data[col] = ArrayFuncs.curl(table.getElement(row,col), dimens[col]); } return data; } /** Replace a row in the table. * @param row The index of the row to be replaced. * @param data The new values for the row. * @exception FitsException Thrown if the new row cannot * match the existing data. */ public void setRow(int row, Object data[]) throws FitsException { if (data.length != getNcol()) { throw new FitsException("Updated row size does not agree with table"); } Object[] ydata = new Object[data.length]; for (int col=0; col<data.length; col += 1) { ydata[col] = ArrayFuncs.flatten(data[col]); } try { table.setRow(row, ydata); } catch (TableException e) { throw new FitsException("Error modifying table: "+e); } } /** Replace a column in the table. * @param col The index of the column to be replaced. * @param xcol The new data for the column * @exception FitsException Thrown if the data does not match * the current column description. */ public void setColumn(int col, Object[] xcol) throws FitsException { int nrow = xcol.length; if (nrow != getNrow()) { throw new FitsException("Replacement column had wrong number of rows"); } int[] dims = ArrayFuncs.getDimensions(xcol[0]); int size = 1; for (int dim=0; dim<dims.length; dim += 1) { size *= dims[dim]; } if (size != sizes[col]) { throw new FitsException("Replacement column has size mismatch"); } Object x = columns.elementAt(col); if (ArrayFuncs.getBaseClass(xcol) != ArrayFuncs.getBaseClass(x)) { throw new FitsException("Replactment column has type mismatch"); } for (int row=0; row < nrow; row += 1) { System.arraycopy(ArrayFuncs.flatten(xcol[row]), 0, x, row*size, size); } } /** Set a column with the data already flattened. * * @param col The index of the column to be replaced. * @param data The new data array. This should be a one-d * primitive array. * @exception FitsException Thrown if the type of length of * the replacement data differs from the * original. */ public void setFlattenedColumn (int col, Object data) throws FitsException { Object x = columns.elementAt(col); if ((x.getClass() != data.getClass()) || (Array.getLength(x) != Array.getLength(data))) { throw new FitsException("Replacement column mismatch"); } // Copy the new data into the array. System.arraycopy(data, 0, x, 0, sizes[col]); } /** Get a given column * @param col The index of the column. */ public Object[] getColumn(int col) throws FitsException { if (!validColumn(col)) { throw new FitsException("Invalid column"); } int[] dims = new int[dimens[col].length+1]; System.arraycopy(dimens[col], 0, dims, 0, dimens[col].length); dims[dimens[col].length] = getNrow(); return (Object[]) ArrayFuncs.curl(columns.elementAt(col),dims); } /** Get a column in flattened format. * For large tables getting a column in standard format can be * inefficient because a separate object is needed for * each row. Leaving the data in flattened format means * that only a single object is created. * @param col */ public Object getFlattenedColumn(int col) throws FitsException { if (!validColumn(col) ) { throw new FitsException("Invalid column"); } return columns.elementAt(col); } /** Get a particular element from the table. * @param i The row of the element. * @param j The column of the element. */ public Object getElement(int i, int j) throws FitsException { if (!validRow(i) || !validColumn(j)) { throw new FitsException("No such element"); } return table.getElement(i,j); } /** Add a row at the end of the table. * @param o An array of objects instantiating the data. These * should have the same structure as any existing rows. */ public void addRow(Object[] o) throws FitsException { Vector newColumns = new Vector(columns.size()); for (int col=0; col<columns.size(); col += 1) { Object oldArray = columns.elementAt(col); int olen = Array.getLength(oldArray); Class obase = ArrayFuncs.getBaseClass(oldArray); Object newArray = Array.newInstance(obase,olen+sizes[col]); System.arraycopy(oldArray, 0, newArray, 0, olen); System.arraycopy( ArrayFuncs.flatten(o[col]), 0, newArray, olen, sizes[col] ); } columns = newColumns; Object[] arrCol = new Object[columns.size()]; columns.copyInto(arrCol); try { table = new ColumnTable(arrCol, sizes); } catch (TableException e) { throw new FitsException("Unable to modify table:"+e); } } /** Add a column to the end of a table. * @param o An array of identically structured objects with the * same number of elements as other columns in the table. */ public void addColumn(Object[] o) throws FitsException { int[] dims = ArrayFuncs.getDimensions(o[0]); addFlattenedColumn(ArrayFuncs.flatten(o), dims); } /** Add a column where the data is already flattened. * @param o The new column data. This should be a one-dimensional * primitive array. * @param dimens The dimensions of one row of the column. */ public void addFlattenedColumn(Object o, int[] dims) throws FitsException { int[] newsizes = new int[sizes.length + 1]; int[][] newdimens = new int[dimens.length + 1][]; Object[] newmodel = new Object[modelRow.length + 1]; int size = 1; for (int dim=0; dim < dims.length; dim += 1) { size *= dims[dim]; } for (int col=0; col< sizes.length; col += 1) { newsizes[col] = sizes[col]; newdimens[col] = dimens[col]; newmodel[col] = modelRow[col]; } sizes = newsizes; dimens = newdimens; modelRow = newmodel; sizes[sizes.length-1] = size; dimens[sizes.length-1] = dims; modelRow[sizes.length-1] = Array.newInstance(ArrayFuncs.getBaseClass(o), dims); columns.addElement(o); Object[] arrCol = new Object[columns.size()]; columns.copyInto(arrCol); try { table = new ColumnTable(arrCol, sizes); } catch (TableException e) { throw new FitsException("Unable to modify table:"+e); } dataArray = table; } /** Get the number of rows in the table */ public int getNrow() { return table.getNrow(); } /** Get the number of columns in the table. */ public int getNcol() { return table.getNcol(); } /** Check to see if this is a valid row. * @param i The Java index (first=0) of the row to check. */ protected boolean validRow(int i) { if (getNrow() > 0 && i >= 0 && i <getNrow()) { return true; } else { return false; } } /** Check if the column number is valid. * * @param j The Java index (first=0) of the column to check. */ protected boolean validColumn(int j) { return (j >= 0 && j < getNcol()); } /** Replace a single element within the table. * * @param i The row of the data. * @param j The column of the data. * @param o The replacement data. */ public void setElement(int i, int j, Object o) throws FitsException{ try { table.setElement(i, j, ArrayFuncs.flatten(o)); } catch (TableException e) { throw new FitsException("Error modifying table:"+e); } } /** This routine makes sure the hash area is large * enough to fill a given request. If not it reallocates * the hash area, and copies the old data into the new * area. * @param need The number of bytes needed for the current * hash area request. */ protected void expandHashArea(int need) { if (hashPtr+need > hashArea.length) { int newlen = (int)((hashPtr+need)*1.5); if (newlen < 16384) { newlen = 16384; } byte[] newHash = new byte[newlen]; System.arraycopy(hashArea, 0, newHash, 0, hashPtr); hashArea = newHash; } } /** Write the data including any hash data. * * @param o The output stream. */ protected void writeTrueData(BufferedDataOutputStream o) throws FitsException { try { table.write(o); o.write(hashArea, 0, hashPtr); } catch (IOException e) { throw new FitsException("Error writing binary table data:"+ e); } } public void read(BufferedDataInputStream i) throws FitsException { /** Read the data associated with the HDU including the hash area if present. * @param i The input stream */ readTrueData(i); } protected void readTrueData(BufferedDataInputStream i) throws FitsException { int len; try { len = table.read(i); if (hashArea.length > 0) { i.readPrimitiveArray(hashArea); } len += hashArea.length; if (len %2880 != 0) { byte[] padding = new byte[2880-len%2880]; i.readPrimitiveArray(padding); } } catch (IOException e) { throw new FitsException("Error reading binary table data:"+e); } } /** Get the size of the data in the HDU sans padding. */ public int getTrueSize() { return super.getTrueSize() + hashPtr; } /** Add a variable length column to the data. * * @param data The data comprising the variable length column. * This should be a two dimensional primitive array * (3D for complex data). * Note that it is declared as a one dimensional object * array to make access convenient. Any 2-d array can * be cast to a 1-d array of objects. * @return A column describing the variable format data. * This column can then be added to the table using * other functions. */ public Column addVarData(Object[] data) throws FitsException { int size = ArrayFuncs.computeSize(data); int baseLength = ArrayFuncs.getBaseLength(data); // Check if the data is complex. If so then the third dimension // should be two. if (data instanceof Object[][]) { baseLength *= 2; } int offset = hashPtr; expandHashArea(size); ByteArrayOutputStream bo = new ByteArrayOutputStream(size); try { BufferedDataOutputStream o = new BufferedDataOutputStream(bo); o.writePrimitiveArray(data); o.flush(); o.close(); } catch (IOException e) { throw new FitsException("Unable to write variable column length data"); } System.arraycopy(bo.toByteArray(), 0, hashArea, hashPtr, size); hashPtr += size; int nrow = data.length; int[][] pointers = new int[nrow][2]; int myMax = 0; for (int i=0; i<nrow; i += 1) { int rowLength = Array.getLength(data[i]); pointers[i][0] = rowLength; pointers[i][1] = offset; offset += rowLength*baseLength; if (rowLength > myMax) { myMax = rowLength; } } Column newColumn = new Column(); newColumn.setData(pointers); return newColumn; } /** Get the data from a variable length column as a two-d primitive array. * * @param col The index of the column to be returned. * @param newArray The array to be filled with variable length data. * This is passed to the BinaryTable class rather than * created here so that we can handle complex data properly. * This will be a two or three dimensional array where the * first dimension is the number of rows in the table. * @param baseClass The base class of the array. It should be one * of the primitive types, e.g, Integer.TYPE. */ public Object getVarData(int col, Class baseClass, boolean complex) throws FitsException { if (col < 0 || col >= getNcol()) { throw new FitsException("Invalid column specified for variable length extraction"); } int[] dims; if (complex) { dims = new int[3]; dims[2] = 0; } else { dims = new int[2]; } dims[0] = getNrow(); dims[1] = 0; Object[] newArray = (Object[])Array.newInstance(baseClass, dims); int baseLength = ArrayFuncs.getBaseLength(newArray); if (complex) { baseLength *= 2; } int offset = 0; BufferedDataInputStream inp = new BufferedDataInputStream( new ByteArrayInputStream(hashArea)); int[] ptrs = (int[])table.getColumn(col); for (int i=0; i<getNrow(); i += 1) { if (ptrs[2*i+1] < offset) { inp = new BufferedDataInputStream(new ByteArrayInputStream(hashArea)); offset = 0; } try { inp.skipBytes(ptrs[2*i+1]-offset); int[] xdims; if (complex) { xdims = new int[2]; xdims[0] = ptrs[2*i]; xdims[1] = 2; } else { xdims = new int[1]; xdims[0] = ptrs[2*i]; } newArray[i] = Array.newInstance(baseClass,xdims); inp.readPrimitiveArray(newArray[i]); offset += baseLength * ptrs[2*i]; } catch (IOException e) { throw new FitsException("Error decoding hash area at offset="+offset+ ". Exception: Exception "+e); } } return newArray; } public void write(BufferedDataOutputStream os) throws FitsException { int len; try { // First write the table. len = table.write(os); // Now any variable length data. if (hashPtr> 0) { os.write(hashArea); } len += hashPtr; // Check and see if any padding needs to be appended. if (len%2880 != 0) { byte[] pad = new byte[2880 - len%2880]; for (int i=0; i<pad.length; i += 1) { pad[i] = 0; } os.write(pad); } } catch (IOException e) { throw new FitsException("Unable to write table:"+e); } } public int getHeapSize() { return hashPtr; } public int[][] getDimens() { return dimens; } public Class[] getBases() { return table.getBases(); } public char[] getTypes() { return table.getTypes(); } public int[] getSizes() { return sizes; } }