Commit b4abe7a1 authored by Ole Streicher's avatar Ole Streicher

Updated version 0.1+2017.07.31 from 'upstream/0.1+2017.07.31'

with Debian dir 009d257e782df7e0aac1daca14d7eaa368abefd6
parents 47bbe647 2a9f27b0
Extended column convention for FITS BINTABLE
--------------------------------------------
The BINTABLE extension type as described in the FITS Standard
(FITS Standard v3.0, sec 7.3) requires table column metadata
to be described using 8-character keywords of the form XXXXXnnn,
where XXXXX represents one of an open set of mandatory, reserved
or user-defined root keywords up to five characters in length,
for instance TFORM (mandatory), TUNIT (reserved), TUCD (user-defined).
The nnn part is an integer between 1 and 999 indicating the
index of the column to which the keyword in question refers.
Since the header syntax confines this indexed part of the keyword
to three digits, there is an upper limit of 999 columns in
BINTABLE extensions.
Note that the FITS/BINTABLE format does not entail any restriction on
the storage of column *data* beyond the 999 column limit in the data
part of the HDU, the problem is just that client software
cannot be informed about the layout of this data using the
header cards in the usual way.
In some cases it is desirable to store FITS tables with a column
count greater than 999. Whether that's a good idea is not within
the scope of this discussion.
To achieve this, I propose the following convention.
Definitions:
- 'BINTABLE columns' are those columns defined using the
FITS BINTABLE standard
- 'Data columns' are the columns to be encoded
- N_TOT is the total number of data columns to be stored
- Data columns with (1-based) indexes from 999 to N_TOT inclusive
are known as 'extended' columns. Their data is stored
within the 'container' column.
- BINTABLE column 999 is known as the 'container' column
It contains the byte data for all the 'extended' columns.
Convention:
- All column data (for columns 1 to N_TOT) is laid out in the data part
of the HDU in exactly the same way as if there were no 999-column
limit.
- The TFIELDS header is declared with the value 999.
- The container column is declared in the header with some
TFORM999 value corresponding to the total field length required
by all the extended columns ('B' is the obvious data type, but
any legal TFORM value that gives the right width MAY be used).
The byte count implied by TFORM999 MUST be equal to the
total byte count implied by all extended columns.
- Other XXXXX999 headers MAY optionally be declared to describe
the container column in accordance with the usual rules,
e.g. TTYPE999 to give it a name.
- The NAXIS1 header is declared in the usual way to give the width
of a table row in bytes. This is equal to the sum of
all the BINTABLE columns as usual. It is also equal to
the sum of all the data columns, which has the same value.
- Headers for Data columns 1-998 are declared as usual,
corresponding to BINTABLE columns 1-998.
- Keyword XT_ICOL indicates the index of the container column.
It MUST be present with the integer value 999 to indicate
that this convention is in use.
- Keyword XT_NCOL indicates the total number of data columns encoded.
It MUST be present with an integer value equal to N_TOT.
- Metadata for each extended column is encoded with keywords
of the form HIERARCH XT XXXXXnnnnn, where XXXXX
are the same keyword roots as used for normal BINTABLE extensions,
and nnnnn is a decimal number written as usual (no leading zeros,
as many digits are required). Thus the formats for data
columns 999, 1000, 1001 etc are declared with the keywords
HIERARCH XT TFORM999, HIERARCH XT TFORM1000, HIERARCH XT TFORM1001
etc. Note this uses the ESO HIERARCH convention described at
https://fits.gsfc.nasa.gov/registry/hierarch_keyword.html.
The "name space" token has been chosen as "XT" (extended table).
- This convention MUST NOT be used for N_TOT<=999.
The resulting HDU is a completely legal FITS BINTABLE extension.
Readers aware of this convention may use it to extract column
data and metadata beyond the 999-column limit.
Readers unaware of this convention will see 998 columns in their
intended form, and an additional (possibly large) column 999
which contains byte data but which cannot be easily interpreted.
An example header might look like this:
XTENSION= 'BINTABLE' / binary table extension
BITPIX = 8 / 8-bit bytes
NAXIS = 2 / 2-dimensional table
NAXIS1 = 9229 / width of table in bytes
NAXIS2 = 26 / number of rows in table
PCOUNT = 0 / size of special data area
GCOUNT = 1 / one data group
TFIELDS = 999 / number of columns
XT_ICOL = 999 / index of container column
XT_NCOL = 1204 / total columns including extended
TTYPE1 = 'posid_1 ' / label for column 1
TFORM1 = 'J ' / format for column 1
TTYPE2 = 'instrument_1' / label for column 2
TFORM2 = '4A ' / format for column 2
TTYPE3 = 'edge_code_1' / label for column 3
TFORM3 = 'I ' / format for column 3
TUCD3 = 'meta.code.qual'
...
TTYPE998= 'var_min_s_2' / label for column 998
TFORM998= 'D ' / format for column 998
TUNIT998= 'counts/s' / units for column 998
TTYPE999= 'XT_MORECOLS' / label for column 999
TFORM999= '813I ' / format for column 999
HIERARCH XT TTYPE999 = 'var_min_u_2' / label for column 999
HIERARCH XT TFORM999 = 'D' / format for column 999
HIERARCH XT TUNIT999 = 'counts/s' / units for column 999
HIERARCH XT TTYPE1000 = 'var_prob_h_2' / label for column 1000
HIERARCH XT TFORM1000 = 'D' / format for column 1000
...
HIERARCH XT TTYPE1203 = 'var_prob_w_2' / label for column 1203
HIERARCH XT TFORM1203 = 'D' / format for column 1203
HIERARCH XT TTYPE1204 = 'var_sigma_w_2' / label for column 1204
HIERARCH XT TFORM1204 = 'D' / format for column 1204
HIERARCH XT TUNIT1204 = 'counts/s' / units for column 1204
END
This general approach was suggested by William Pence on the FITSBITS
list in June 2012
(https://listmgr.nrao.edu/pipermail/fitsbits/2012-June/002367.html),
and by Francois-Xavier Pineau (CDS) in private conversation in 2016.
The details have been filled in by Mark Taylor (Bristol).
It was discussed in some detail on the FITSBITS list in July 2017
(https://listmgr.nrao.edu/pipermail/fitsbits/2017-July/002967.html)
--------------------------------------------------------------------
Note: a previous variant of this convention was proposed in which
the metadata for the extended columns was declared by extending
the numbering scheme using three-digit base-26 representations.
It was identical to the above, except that:
- Metadata for each extended column is encoded with keywords
of the form XXXXXaaa, where XXXXX are the same keyword roots
as used for normal BINTABLE extensions, and aaa is a 3-digit
value in base 26 using the characters 'A' (0 in base 26) to
'Z' (25 in base 26), and giving the 1-based data column index
minus 999. The sequence aaa MUST be exactly three characters
long (leading 'A's are required). Thus the formats for data
columns 999, 1000, 1001, etc are declared with the keywords
TFORMAAA, TFORMAAB, TFORMAAC etc.
This convention can therefore allow encoding of tables with data
column counts N_TOT up to 998+26^3 = 18574.
In that case the header looks identical to the previous example up
to TFORM999, but the remaining entries differ:
TTYPE998= 'var_min_s_2' / label for column 998
TFORM998= 'D ' / format for column 998
TUNIT998= 'counts/s' / units for column 998
TTYPE999= 'XT_MORECOLS' / label for column 999
TFORM999= '813I ' / format for column 999
TTYPEAAA= 'var_min_u_2' / label for column 999
TFORMAAA= 'D ' / format for column 999
TUNITAAA= 'counts/s' / units for column 999
TTYPEAAB= 'var_prob_h_2' / label for column 1000
TFORMAAB= 'D ' / format for column 1000
...
TTYPEAHW= 'var_prob_w_2' / label for column 1203
TFORMAHW= 'D ' / format for column 1203
TTYPEAHX= 'var_sigma_w_2' / label for column 1204
TFORMAHX= 'D ' / format for column 1204
TUNITAHX= 'counts/s' / units for column 1204
END
This variant was generally less favoured than the HIERARCH_based
one by participants in the FITSBITS discussion.
...@@ -49,9 +49,6 @@ public abstract class AbstractFitsTableWriter extends StreamStarTableWriter ...@@ -49,9 +49,6 @@ public abstract class AbstractFitsTableWriter extends StreamStarTableWriter
private static final Logger logger_ = private static final Logger logger_ =
Logger.getLogger( "uk.ac.starlink.fits" ); Logger.getLogger( "uk.ac.starlink.fits" );
/** Hard limit for FITS table columns (TTYPE1000 has too many chars). */
private static final int MAX_FITS_COLUMNS = 999;
/** /**
* Constructor. * Constructor.
* *
...@@ -147,12 +144,6 @@ public abstract class AbstractFitsTableWriter extends StreamStarTableWriter ...@@ -147,12 +144,6 @@ public abstract class AbstractFitsTableWriter extends StreamStarTableWriter
*/ */
public void writeTableHDU( StarTable table, FitsTableSerializer fitser, public void writeTableHDU( StarTable table, FitsTableSerializer fitser,
DataOutput out ) throws IOException { DataOutput out ) throws IOException {
int ncol = table.getColumnCount();
if ( ncol > MAX_FITS_COLUMNS ) {
throw new IOException( "Column count " + ncol
+ " exceeds FITS limit of "
+ MAX_FITS_COLUMNS );
}
try { try {
Header hdr = fitser.getHeader(); Header hdr = fitser.getHeader();
addMetadata( hdr ); addMetadata( hdr );
...@@ -167,9 +158,13 @@ public abstract class AbstractFitsTableWriter extends StreamStarTableWriter ...@@ -167,9 +158,13 @@ public abstract class AbstractFitsTableWriter extends StreamStarTableWriter
/** /**
* Provides a suitable serializer for a given table. * Provides a suitable serializer for a given table.
* Note this should throw an IOException if it can be determined that
* the submitted table cannot be written by this writer, for instance
* if it has too many columns.
* *
* @param table table to serialize * @param table table to serialize
* @return FITS serializer * @return FITS serializer
* @throws IOException if the table can't be written
*/ */
protected abstract FitsTableSerializer createSerializer( StarTable table ) protected abstract FitsTableSerializer createSerializer( StarTable table )
throws IOException; throws IOException;
......
This diff is collapsed.
package uk.ac.starlink.fits;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* ThreadLocal based on an InputFactory.
* This can dispense a BasicInput object private to the current thread.
* The close method will close all the BasicInput objects that this
* has created so far.
*
* @author Mark Taylor
* @since 30 Jun 2017
*/
public class BasicInputThreadLocal extends ThreadLocal<BasicInput>
implements Closeable {
private final InputFactory inputFact_;
private final boolean isSeq_;
private final List<BasicInput> inputs_;
/**
* Constructor.
*
* @param inputFact factory for BasicInput objects
* @param isSeq true if created inputs are sequential, false for random
*/
public BasicInputThreadLocal( InputFactory inputFact, boolean isSeq ) {
inputFact_ = inputFact;
isSeq_ = isSeq;
inputs_ = new ArrayList<BasicInput>();
}
@Override
protected BasicInput initialValue() {
BasicInput bi = createBasicInput();
inputs_.add( bi );
return bi;
}
/**
* Creates a BasicInput object without throwing an exception.
* If it fails, a dummy instance that will throw a suitable exception
* when in use is returned.
*
* @return new basic input
*/
private BasicInput createBasicInput() {
try {
return inputFact_.createInput( isSeq_ );
}
catch ( final IOException e ) {
return new FailureBasicInput( e, isSeq_ );
}
}
public synchronized void close() {
for ( Iterator<BasicInput> it = inputs_.iterator(); it.hasNext(); ) {
try {
it.next().close();
}
catch ( IOException e ) {
// never mind
}
it.remove();
}
}
/**
* BasicInput instance that responds to most method invocations
* by throwing a previously supplied exception.
*/
private static class FailureBasicInput implements BasicInput {
private final IOException err_;
private final boolean isSeq_;
/**
* Constructor.
*
* @param err exception on which to base thrown ones
* @param isSeq true if created inputs are sequential,
* false for random
*/
FailureBasicInput( IOException err, boolean isSeq ) {
isSeq_ = isSeq;
err_ = err;
}
public byte readByte() throws IOException {
throw failure();
}
public short readShort() throws IOException {
throw failure();
}
public int readInt() throws IOException {
throw failure();
}
public long readLong() throws IOException {
throw failure();
}
public float readFloat() throws IOException {
throw failure();
}
public double readDouble() throws IOException {
throw failure();
}
public void skip( long nbyte ) throws IOException {
throw failure();
}
public boolean isRandom() {
return isSeq_;
}
public void seek( long offset ) throws IOException {
throw failure();
}
public long getOffset() {
return 0;
}
public void close() {
}
private IOException failure() {
return (IOException)
new IOException( "Input creation failed" ).initCause( err_ );
}
}
}
package uk.ac.starlink.fits;
/**
* Understands how per-column metadata is stored in the headers
* of a FITS BINTABLE extension.
*
* @author Mark Taylor
* @since 21 Mar 2017
*/
public abstract class BintableColumnHeader {
/**
* Constructor.
*/
protected BintableColumnHeader() {
}
/**
* Gives the name of the actual FITS header card for the column
* managed by this object and a standard FITS BINTABLE base header name.
*
* @param stdName standard base name for the metadata item
* (for instance "TFORM" for TFORMnnn)
* @return complete FITS header card key name
*/
public abstract String getKeyName( String stdName );
/**
* Returns the string value of a header card
* for this object's column.
*
* @param cards header collection
* @param stdName standard base name for the metadata item
* (for instance "TFORM" for TFORMnnn)
* @return string value, or null for absent header
*/
public String getStringValue( HeaderCards cards, String stdName ) {
String key = getKeyName( stdName );
return key == null ? null : cards.getStringValue( key );
}
/**
* Returns the long integer value of a header card
* for this object's column.
*
* @param cards header collection
* @param stdName standard base name for the metadata item
* (for instance "TFORM" for TFORMnnn)
* @return long value, or null for absent header
*/
public Long getLongValue( HeaderCards cards, String stdName ) {
String key = getKeyName( stdName );
return key == null ? null : cards.getLongValue( key );
}
/**
* Returns the double precision value of a header card
* for this object's column.
*
* @param cards header collection
* @param stdName standard base name for the metadata item
* (for instance "TFORM" for TFORMnnn)
* @return double value, or null for absent header
*/
public Double getDoubleValue( HeaderCards cards, String stdName ) {
String key = getKeyName( stdName );
return key == null ? null : cards.getDoubleValue( key );
}
/**
* Indicates whether a given header card is present
* for this object's column.
*
* @param cards header collection
* @param stdName standard base name for the metadata item
* (for instance "TFORM" for TFORMnnn)
* @return true iff header is present
*/
public boolean containsKey( HeaderCards cards, String stdName ) {
String key = getKeyName( stdName );
return key != null && cards.containsKey( key );
}
/**
* Returns an instance of this class for use with standard FITS BINTABLE
* headers.
*
* @param jcol column index (first column has value 1)
* @return new instance
*/
public static BintableColumnHeader createStandardHeader( int jcol ) {
final String jcolStr = Integer.toString( jcol );
return new BintableColumnHeader() {
public String getKeyName( String stdName ) {
return stdName + jcolStr;
}
};
}
}
...@@ -32,6 +32,25 @@ import uk.ac.starlink.util.DataSource; ...@@ -32,6 +32,25 @@ import uk.ac.starlink.util.DataSource;
*/ */
public class ColFitsTableBuilder implements TableBuilder { public class ColFitsTableBuilder implements TableBuilder {
private final WideFits wide_;
/**
* Default constructor.
*/
public ColFitsTableBuilder() {
this( WideFits.DEFAULT );
}
/**
* Constructor.
*
* @param wide convention for representing extended columns;
* use null to avoid use of extended columns
*/
public ColFitsTableBuilder( WideFits wide ) {
wide_ = wide;
}
public String getFormatName() { public String getFormatName() {
return "colfits-basic"; return "colfits-basic";
} }
...@@ -72,6 +91,6 @@ public class ColFitsTableBuilder implements TableBuilder { ...@@ -72,6 +91,6 @@ public class ColFitsTableBuilder implements TableBuilder {
in.close(); in.close();
} }
return new ColFitsStarTable( datsrc, hdr, pos, false ); return new ColFitsStarTable( datsrc, hdr, pos, false, wide_ );
} }
} }
...@@ -25,6 +25,7 @@ import uk.ac.starlink.table.WrapperStarTable; ...@@ -25,6 +25,7 @@ import uk.ac.starlink.table.WrapperStarTable;
*/ */
public class ColFitsTableSerializer implements FitsTableSerializer { public class ColFitsTableSerializer implements FitsTableSerializer {
private final WideFits wide_;
private final ColumnStore[] colStores_; private final ColumnStore[] colStores_;
private final String[] colids_; private final String[] colids_;
private final int ncol_; private final int ncol_;
...@@ -37,9 +38,13 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -37,9 +38,13 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
* Constructor. * Constructor.
* *
* @param table table to serialize * @param table table to serialize
* @param wide convention for representing over-wide tables;
* null to avoid this convention
* @throws IOException if it won't be possible to write the given table
*/ */
public ColFitsTableSerializer( StarTable table ) public ColFitsTableSerializer( StarTable table, WideFits wide )
throws IOException { throws IOException {
wide_ = wide;
/* Prepare an array of column storage objects which know how to do /* Prepare an array of column storage objects which know how to do
* serial storage/retrieval of the data in a table column. */ * serial storage/retrieval of the data in a table column. */
...@@ -47,6 +52,7 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -47,6 +52,7 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
ncol_ = table.getColumnCount(); ncol_ = table.getColumnCount();
colStores_ = new ColumnStore[ ncol_ ]; colStores_ = new ColumnStore[ ncol_ ];
colids_ = new String[ ncol_ ]; colids_ = new String[ ncol_ ];
int nUseCol = 0;
for ( int icol = 0; icol < ncol_; icol++ ) { for ( int icol = 0; icol < ncol_; icol++ ) {
ColumnInfo info = table.getColumnInfo( icol ); ColumnInfo info = table.getColumnInfo( icol );
colids_[ icol ] = info.toString(); colids_[ icol ] = info.toString();
...@@ -54,7 +60,11 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -54,7 +60,11 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
if ( colStores_[ icol ] == null ) { if ( colStores_[ icol ] == null ) {
logger_.warning( "Can't serialize column " + info ); logger_.warning( "Can't serialize column " + info );
} }
else {
nUseCol++;
}
} }
FitsConstants.checkColumnCount( wide, nUseCol );
/* Store the table data into these storage objects. */ /* Store the table data into these storage objects. */
boolean ok = false; boolean ok = false;
...@@ -104,15 +114,29 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -104,15 +114,29 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
/* Work out the length of the single table row. */ /* Work out the length of the single table row. */
long size = 0L; long size = 0L;
long extSize = 0L;
int nUseCol = 0; int nUseCol = 0;
for ( int icol = 0; icol < ncol_; icol++ ) { for ( int icol = 0; icol < ncol_; icol++ ) {
ColumnStore colStore = colStores_[ icol ]; ColumnStore colStore = colStores_[ icol ];
if ( colStore != null ) { if ( colStore != null ) {
nUseCol++; nUseCol++;
size += colStore.getDataLength(); long leng = colStore.getDataLength();
size += leng;
if ( wide_ != null &&
nUseCol >= wide_.getContainerColumnIndex() ) {
extSize += leng;
}
} }
} }
/* Work out the number of standard and extended columns.
* This won't fail because of checks carried out in constructor. */
int nStdCol =
wide_ != null && nUseCol > wide_.getContainerColumnIndex()
? wide_.getContainerColumnIndex()
: nUseCol;
boolean hasExtCol = nUseCol > nStdCol;
/* Write the standard part of the FITS header. */ /* Write the standard part of the FITS header. */
Header hdr = new Header(); Header hdr = new Header();
hdr.addValue( "XTENSION", "BINTABLE", "binary table extension" ); hdr.addValue( "XTENSION", "BINTABLE", "binary table extension" );
...@@ -122,7 +146,7 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -122,7 +146,7 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
hdr.addValue( "NAXIS2", 1, "single-row table" ); hdr.addValue( "NAXIS2", 1, "single-row table" );
hdr.addValue( "PCOUNT", 0, "size of special data area" ); hdr.addValue( "PCOUNT", 0, "size of special data area" );
hdr.addValue( "GCOUNT", 1, "one data group" ); hdr.addValue( "GCOUNT", 1, "one data group" );
hdr.addValue( "TFIELDS", nUseCol, "number of columns" ); hdr.addValue( "TFIELDS", nStdCol, "number of columns" );
/* Add EXTNAME record containing table name. */ /* Add EXTNAME record containing table name. */
if ( tname_ != null && tname_.trim().length() > 0 ) { if ( tname_ != null && tname_.trim().length() > 0 ) {
...@@ -130,13 +154,27 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -130,13 +154,27 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
.addTrimmedValue( hdr, "EXTNAME", tname_, "table name" ); .addTrimmedValue( hdr, "EXTNAME", tname_, "table name" );
} }
/* Add extended column header information if applicable. */
if ( hasExtCol ) {
wide_.addExtensionHeader( hdr, nUseCol );
AbstractWideFits.logWideWrite( logger_, nStdCol, nUseCol );
}
/* Ask each ColumnStore to add header cards describing the data /* Ask each ColumnStore to add header cards describing the data
* cell it will write. */ * cell it will write. */
int jcol = 0; int jcol = 0;
for ( int icol = 0; icol < ncol_; icol++ ) { for ( int icol = 0; icol < ncol_; icol++ ) {
ColumnStore colStore = colStores_[ icol ]; ColumnStore colStore = colStores_[ icol ];
if ( colStore != null ) { if ( colStore != null ) {
colStore.addHeaderInfo( hdr, ++jcol ); jcol++;
if ( hasExtCol && jcol == nStdCol ) {
wide_.addContainerColumnHeader( hdr, extSize, nrow_ );
}
BintableColumnHeader colhead =
hasExtCol && jcol >= nStdCol
? wide_.createExtendedHeader( nStdCol, jcol )
: BintableColumnHeader.createStandardHeader( jcol );
colStore.addHeaderInfo( hdr, colhead, jcol );
} }
} }
return hdr; return hdr;
...@@ -213,22 +251,26 @@ public class ColFitsTableSerializer implements FitsTableSerializer { ...@@ -213,22 +251,26 @@ public class ColFitsTableSerializer implements FitsTableSerializer {
private static String getCardValue( ColumnStore colStore, String tcard ) { private static String getCardValue( ColumnStore colStore, String tcard ) {
Header hdr = new Header(); Header hdr = new Header();
try { int icol = 99;
int icol = 99; BintableColumnHeader colhead =
Level level = logger_.getLevel(); BintableColumnHeader.createStandardHeader( icol );
/* Avoid unwanted logging messages that might occur in case of /* Avoid unwanted logging messages that might occur in case of
* truncated card values. */ * truncated card values. */
logger_.setLevel( Level.SEVERE ); Level level = logger_.getLevel();
colStore.addHeaderInfo( hdr, icol ); logger_.setLevel( Level.SEVERE );
logger_.setLevel( level ); try {
String key = tcard + icol; colStore.addHeaderInfo( hdr, colhead, icol );
return hdr.containsKey( key )
? hdr.findCard( key ).getValue().trim()
: null;
} }
catch ( HeaderCardException e ) { catch ( HeaderCardException e ) {
throw new AssertionError( e ); throw new AssertionError( e );
} }
finally {
logger_.setLevel( level );
}
String key = colhead.getKeyName( tcard );
return hdr.containsKey( key )
? hdr.findCard( key ).getValue().trim()
: null;
} }
} }
...@@ -21,8 +21,25 @@ import uk.ac.starlink.table.StarTable; ...@@ -21,8 +21,25 @@ import uk.ac.starlink.table.StarTable;
*/ */
public class ColFitsTableWriter extends AbstractFitsTableWriter { public class ColFitsTableWriter extends AbstractFitsTableWriter {