View Javadoc
1   package nom.tam.fits;
2   
3   /*-
4    * #%L
5    * nom.tam FITS library
6    * %%
7    * Copyright (C) 1996 - 2024 nom-tam-fits
8    * %%
9    * This is free and unencumbered software released into the public domain.
10   *
11   * Anyone is free to copy, modify, publish, use, compile, sell, or
12   * distribute this software, either in source code form or as a compiled
13   * binary, for any purpose, commercial or non-commercial, and by any
14   * means.
15   *
16   * In jurisdictions that recognize copyright laws, the author or authors
17   * of this software dedicate any and all copyright interest in the
18   * software to the public domain. We make this dedication for the benefit
19   * of the public at large and to the detriment of our heirs and
20   * successors. We intend this dedication to be an overt act of
21   * relinquishment in perpetuity of all present and future rights to this
22   * software under copyright law.
23   *
24   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30   * OTHER DEALINGS IN THE SOFTWARE.
31   * #L%
32   */
33  
34  import java.io.IOException;
35  import java.lang.reflect.Array;
36  import java.math.BigDecimal;
37  import java.math.BigInteger;
38  import java.text.DecimalFormat;
39  import java.text.ParsePosition;
40  import java.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.List;
43  import java.util.StringTokenizer;
44  import java.util.logging.Logger;
45  
46  import nom.tam.fits.header.Bitpix;
47  import nom.tam.fits.header.NonStandard;
48  import nom.tam.fits.header.Standard;
49  import nom.tam.util.ArrayDataInput;
50  import nom.tam.util.ArrayDataOutput;
51  import nom.tam.util.ArrayFuncs;
52  import nom.tam.util.AsciiFuncs;
53  import nom.tam.util.ColumnTable;
54  import nom.tam.util.ComplexValue;
55  import nom.tam.util.Cursor;
56  import nom.tam.util.FitsEncoder;
57  import nom.tam.util.FitsIO;
58  import nom.tam.util.Quantizer;
59  import nom.tam.util.RandomAccess;
60  import nom.tam.util.ReadWriteAccess;
61  import nom.tam.util.TableException;
62  import nom.tam.util.type.ElementType;
63  
64  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
65  
66  /**
67   * Table data for binary table HDUs. It has been thoroughly re-written for 1.18 to improve consistency, increase
68   * performance, make it easier to use, and to enhance.
69   * 
70   * @see BinaryTableHDU
71   * @see AsciiTable
72   */
73  @SuppressWarnings("deprecation")
74  public class BinaryTable extends AbstractTableData implements Cloneable {
75  
76      /** For fixed-length columns */
77      private static final char POINTER_NONE = 0;
78  
79      /** FITS 32-bit pointer type for variable-sized columns */
80      private static final char POINTER_INT = 'P';
81  
82      /** FITS 64-bit pointer type for variable-sized columns */
83      private static final char POINTER_LONG = 'Q';
84  
85      /** Shape firs singleton / scalar entries */
86      private static final int[] SINGLETON_SHAPE = new int[0];
87  
88      /** The substring convention marker */
89      private static final String SUBSTRING_MARKER = ":SSTR";
90  
91      /**
92       * Describes the data type and shape stored in a binary table column.
93       */
94      public static class ColumnDesc implements Cloneable {
95  
96          private boolean warnedFlatten;
97  
98          /** byte offset of element from row start */
99          private int offset;
100 
101         /** The number of primitive elements in the column */
102         private int fitsCount;
103 
104         /** The dimensions of the column */
105         private int[] fitsShape = SINGLETON_SHAPE;
106 
107         /** Shape on the Java side. Differs from the FITS TDIM shape for String and complex values. */
108         private int[] legacyShape = SINGLETON_SHAPE;
109 
110         /** Length of string elements */
111         private int stringLength = -1;
112 
113         /** The class array entries on the Java side. */
114         private Class<?> base;
115 
116         /** The FITS element class associated with the column. */
117         private Class<?> fitsBase;
118 
119         /** Heap pointer type actually used for locating variable-length column data on the heap */
120         private char pointerType;
121 
122         /**
123          * String component delimiter for substring arrays, for example as defined by the TFORM keyword that uses the
124          * substring array convention...
125          */
126         private byte delimiter;
127 
128         /**
129          * Is this a complex column. Each entry will be associated with a float[2] or double[2]
130          */
131         private boolean isComplex;
132 
133         /**
134          * Whether this column contains bit arrays. These take up to 8-times less space than logicals, which occupy a
135          * byte per value.
136          */
137         private boolean isBits;
138 
139         /**
140          * User defined column name
141          */
142         private String name;
143 
144         private Quantizer quant;
145 
146         /**
147          * Creates a new column descriptor with default settings and 32-bit integer heap pointers.
148          */
149         protected ColumnDesc() {
150         }
151 
152         /**
153          * Creates a new column descriptor with default settings, and the specified type of heap pointers
154          * 
155          * @param  type          The Java type of base elements that this column is designated to contain. For example
156          *                           <code>int.class</code> if the column will contain integers or arrays of integers.
157          * 
158          * @throws FitsException if the base type is not one that can be used in binary table columns.
159          */
160         private ColumnDesc(Class<?> type) throws FitsException {
161             this();
162 
163             base = type;
164 
165             if (base == boolean.class) {
166                 fitsBase = byte.class;
167                 isBits = true;
168             } else if (base == Boolean.class) {
169                 base = boolean.class;
170                 fitsBase = byte.class;
171             } else if (base == String.class) {
172                 fitsBase = byte.class;
173             } else if (base == ComplexValue.class) {
174                 base = double.class;
175                 fitsBase = double.class;
176                 isComplex = true;
177             } else if (base == ComplexValue.Float.class) {
178                 base = float.class;
179                 fitsBase = float.class;
180                 isComplex = true;
181             } else if (base.isPrimitive()) {
182                 fitsBase = type;
183                 if (base == char.class && FitsFactory.isUseUnicodeChars()) {
184                     LOG.warning("char[] will be written as 16-bit integers (type 'I'), not as a ASCII bytes (type 'A')"
185                             + " in the binary table. If that is not what you want, you should set FitsFactory.setUseUnicodeChars(false).");
186                     LOG.warning(
187                             "Future releases will disable Unicode support by default as it is not supported by the FITS standard."
188                                     + " If you do want it still, use FitsFactory.setUseUnicodeChars(true) explicitly to keep the non-standard "
189                                     + " behavior as is.");
190                 }
191             } else {
192                 throw new TableException("Columns of type " + base + " are not supported.");
193             }
194 
195         }
196 
197         /**
198          * Creates a new column descriptor for the specified boxed Java type, and fixed array shape. The type may be any
199          * primitive type, or else <code>String.class</code>, <code>Boolean.class</code> (for FITS logicals),
200          * <code>ComplexValue.class</code> or <code>ComplexValue.Float.class</code> (for complex values with 64-bit and
201          * 32-bit precision, respectively). Whereas {@link Boolean} type columns will be stored as FITS logicals (1
202          * element per byte), <code>boolean</code> types will be stored as packed bits (with up to 8 bits per byte).
203          * 
204          * @param  base          The Java type of base elements that this column is designated to contain. For example
205          *                           <code>int.class</code> if the column will contain integers or arrays of integers.
206          * @param  dim           the fixed dimensions of the table entries. For strings the trailing dimension must
207          *                           specify the fixed length of strings.
208          * 
209          * @throws FitsException if the base type is not one that can be used in binary table columns.
210          * 
211          * @see                  #createForScalars(Class)
212          * @see                  #createForStrings(int)
213          * @see                  #createForStrings(int, int[])
214          * @see                  #createForVariableSize(Class)
215          * 
216          * @since                1.18
217          */
218         public ColumnDesc(Class<?> base, int... dim) throws FitsException {
219             this(base);
220             setBoxedShape(dim);
221         }
222 
223         /**
224          * Sets a user-specified name for this column. The specified name will be used as the TTYPEn value for this
225          * column.
226          * 
227          * @param  value                    The new name for this column.
228          * 
229          * @return                          itself, to support builder patterns.
230          * 
231          * @throws IllegalArgumentException If the name contains characters outside of the ASCII range of 0x20 - 0x7F
232          *                                      allowed by FITS.
233          * 
234          * @see                             #name()
235          * @see                             #getDescriptor(String)
236          * @see                             #indexOf(String)
237          * @see                             #addColumn(ColumnDesc)
238          * 
239          * @since                           1.20
240          */
241         public ColumnDesc name(String value) throws IllegalArgumentException {
242             HeaderCard.validateChars(value);
243             this.name = value;
244             return this;
245         }
246 
247         /**
248          * Returns the name of this column, as it was stored or would be stored by a TTYPEn value in the FITS header.
249          * 
250          * @return the name of this column
251          * 
252          * @see    #name(String)
253          * 
254          * @since  1.20
255          */
256         public String name() {
257             return this.name;
258         }
259 
260         /**
261          * Returns the conversion between decimal and integer data representations for the column data.
262          * 
263          * @return the quantizer that converts between floating-point and integer data representations, which may be
264          *             <code>null</code>.
265          * 
266          * @see    #setQuantizer(Quantizer)
267          * 
268          * @since  1.20
269          */
270         public Quantizer getQuantizer() {
271             return quant;
272         }
273 
274         /**
275          * Sets the conversion between decimal and integer data representations for the column data. If the table is
276          * read from a FITS input, the column's quantizer is automatically set if the Table HDU's header defines any of
277          * the TSCALn, TZEROn, or TNULLn keywords for the column. Users can override that by specifying another quatizer
278          * to use for the column, or dicard qunatizing by calling this method with a <code>null</code>argument.
279          * 
280          * @param q the quantizer that converts between floating-point and integer data representations, or
281          *              <code>null</code> to not use any quantization, and instead rely on the generic rounding for
282          *              decimal-integer conversions for this column.
283          * 
284          * @see     #getQuantizer()
285          * 
286          * @since   1.20
287          */
288         public void setQuantizer(Quantizer q) {
289             this.quant = q;
290         }
291 
292         /**
293          * Recalculate the FITS element count based on the shape of the data
294          */
295         private void calcFitsCount() {
296             fitsCount = 1;
297             for (int size : fitsShape) {
298                 fitsCount *= size;
299             }
300         }
301 
302         /**
303          * Sets the shape of entries in the older Java array format of this library (used exclusively prior to 1.18).
304          * For complex columns, there is an extra <code>[2]</code> dimension, such that single complex values are stored
305          * as an array of <code>[2]</code>, and an array of <i>n</i> complex values are stored as arrays of
306          * <code>[n][2]</code>. Otherwise it's the same as {@link #setBoxedShape(int...)}.
307          * 
308          * @param dim The Java dimensions for legacy arrays, such as returned by
309          *                {@link BinaryTable#getElement(int, int)}
310          */
311         private void setLegacyShape(int... dim) {
312             legacyShape = dim;
313             calcFitsShape();
314             calcFitsCount();
315         }
316 
317         /**
318          * Sets the shape of entries as stored in the FITS header by hte TDIM keyword.
319          * 
320          * @param dim The dimensions for the TDIM keyword in Java order (outer dimensions first), which is the reverse
321          *                of FITS order (inner-dimensions first).
322          */
323         private void setFitsShape(int... dim) {
324             fitsShape = dim;
325             calcLegacyShape();
326             calcFitsCount();
327         }
328 
329         /**
330          * Sets the shape of boxed Java array entries. For complex columns, all single entries, including strings and
331          * complex values, have scalar shape <code>[]</code>, whereas an array of <i>n</i> have shape <code>[n]</code>.
332          * 
333          * @param dim The Java dimensions for legacy arrays, such as returned by
334          *                {@link BinaryTable#getElement(int, int)}
335          */
336         private void setBoxedShape(int... dim) {
337             if (isComplex()) {
338                 setFitsShape(dim);
339             } else {
340                 setLegacyShape(dim);
341             }
342         }
343 
344         /**
345          * Returns the maximum length of string elements contained in this colum, or -1 if this is not a string based
346          * column, or if there is no limit set to string size (e.g. in a variable-length column)
347          * 
348          * @return the maximum length of string values stored in this column, or -1 if it is not as string column, or if
349          *             its a string column containing strings of unconstrained variable length.
350          * 
351          * @see    #getEntryShape()
352          * 
353          * @since  1.18
354          */
355         public final int getStringLength() {
356             return stringLength;
357         }
358 
359         /**
360          * Sets the maximum length of string elements in this column.
361          *
362          * @param len The fixed string length in bytes.
363          */
364         private void setStringLength(int len) {
365             stringLength = len;
366 
367             if (!isVariableSize()) {
368                 calcFitsShape();
369                 calcFitsCount();
370             }
371         }
372 
373         /**
374          * Returns the string delimiter that separates packed substrings in variable-length string arrays.
375          * 
376          * @return the delimiter byte value (usually between 0x20 and 0x7e) or 0 if no delimiter was set.
377          * 
378          * @see    #getStringLength()
379          */
380         public final byte getStringDelimiter() {
381             return delimiter;
382         }
383 
384         /**
385          * Creates a new column descriptor for a non-string based scalar column. The type may be any primitive type, or
386          * <code>Boolean.class</code> (for FITS logicals), <code>ComplexValue.class</code> or
387          * <code>ComplexValue.Float.class</code> (for complex values with 64-bit and 32-bit precision, respectively).
388          * Whereas {@link Boolean} type columns will be stored as FITS logicals (1 element per byte),
389          * <code>boolean</code> types will be stored as packed bits (with up to 8 bits per byte).
390          * 
391          * @param  type                     The Java type of base elements that this column is designated to contain.
392          *                                      For example <code>int.class</code> if the column will contain integers
393          *                                      or arrays of integers. It must not be <code>String.class</code>. To
394          *                                      create scalar {@link String} columns use {@link #createForStrings(int)}
395          *                                      instead.
396          * 
397          * @return                          the new column descriptor.
398          * 
399          * @throws IllegalArgumentException if the type is <code>String.class</code>, for which you should be using
400          *                                      {@link #createForStrings(int)} instead.
401          * @throws FitsException            if the base type is not one that can be used in binary table columns.
402          * 
403          * @see                             #createForFixedArrays(Class, int[])
404          * @see                             #createForVariableSize(Class)
405          * 
406          * @since                           1.18
407          */
408         public static ColumnDesc createForScalars(Class<?> type) throws IllegalArgumentException, FitsException {
409             if (String.class.isAssignableFrom(type)) {
410                 throw new IllegalArgumentException("Use the createStrings(int) method for scalar strings.");
411             }
412             return new ColumnDesc(type, SINGLETON_SHAPE);
413         }
414 
415         /**
416          * Creates a new column descriptor for fixed-shape non-string arrays. The type may be any primitive type, or
417          * else <code>Boolean.class</code> (for FITS logicals), <code>ComplexValue.class</code> or
418          * <code>ComplexValue.Float.class</code> (for complex values with 64-bit and 32-bit precision, respectively).
419          * Whereas {@link Boolean} type columns will be stored as FITS logicals (1 element per byte),
420          * <code>boolean</code> types will be stored as packed bits (with up to 8 bits per byte).
421          * 
422          * @param  type                     The Java type of base elements that this column is designated to contain.
423          *                                      For example <code>int.class</code> if the column will contain integers
424          *                                      or arrays of integers. It must not be <code>String.class</code>. To
425          *                                      create scalar {@link String} columns use {@link #createForStrings(int)}
426          *                                      instead.
427          * @param  dim                      the fixed dimensions of the table entries. For strings the trailing
428          *                                      dimension must specify the fixed length of strings.
429          * 
430          * @return                          the new column descriptor.
431          * 
432          * @throws IllegalArgumentException if the type is <code>String.class</code>, for which you should be using
433          *                                      {@link #createForStrings(int, int[])} instead.
434          * @throws FitsException            if the base type is not one that can be used in binary table columns.
435          * 
436          * @see                             #createForScalars(Class)
437          * @see                             #createForStrings(int)
438          * @see                             #createForStrings(int, int[])
439          * @see                             #createForVariableSize(Class)
440          * 
441          * @since                           1.18
442          */
443         public static ColumnDesc createForFixedArrays(Class<?> type, int... dim)
444                 throws IllegalArgumentException, FitsException {
445             if (String.class.isAssignableFrom(type)) {
446                 throw new IllegalArgumentException("Use the createStrings(int) method for scalar strings.");
447             }
448             return new ColumnDesc(type, dim);
449         }
450 
451         /**
452          * Creates a new column descriptor for single string entries of fixed maximum length.
453          * 
454          * @param  len           The fixed string length in bytes.
455          * 
456          * @return               the new column descriptor
457          * 
458          * @throws FitsException if the base type is not one that can be used in binary table columns.
459          * 
460          * @see                  #createForScalars(Class)
461          * @see                  #createForStrings(int, int[])
462          * 
463          * @since                1.18
464          */
465         public static ColumnDesc createForStrings(int len) throws FitsException {
466             return createForStrings(len, SINGLETON_SHAPE);
467         }
468 
469         /**
470          * Creates a new column descriptor for arrays of string entries of fixed maximum length.
471          * 
472          * @param  len           The fixed string length in bytes.
473          * @param  outerDims     The shape of string arrays
474          * 
475          * @return               the new column descriptor
476          * 
477          * @throws FitsException if the base type is not one that can be used in binary table columns.
478          * 
479          * @see                  #createForVariableStringArrays(int)
480          * @see                  #createForStrings(int)
481          * 
482          * @since                1.18
483          */
484         public static ColumnDesc createForStrings(int len, int... outerDims) throws FitsException {
485             ColumnDesc c = new ColumnDesc(String.class);
486             c.setLegacyShape(outerDims);
487             c.setStringLength(len);
488             return c;
489         }
490 
491         /**
492          * Creates a new column descriptor for variable-length arrays of fixed-length string entries. Each string
493          * component will occupy exactly <code>len</code> bytes.
494          * 
495          * @param  len           The fixed string storage length in bytes.
496          * 
497          * @return               the new column descriptor
498          * 
499          * @throws FitsException if the column could not be created.
500          *
501          * @see                  #createForDelimitedStringArrays(byte)
502          * @see                  #createForStrings(int, int[])
503          * 
504          * @since                1.18
505          */
506         public static ColumnDesc createForVariableStringArrays(int len) throws FitsException {
507             ColumnDesc c = createForVariableSize(String.class);
508             c.setStringLength(len);
509             return c;
510         }
511 
512         /**
513          * Creates a new column descriptor for variable-length arrays of delimited string entries.
514          * 
515          * @param  delim         the byte value that delimits strings that are shorter than the storage length. It
516          *                           should be in the ASCII range of 0x20 through 0x7e.
517          * 
518          * @return               the new column descriptor
519          * 
520          * @throws FitsException if the column could not be created.
521          * 
522          * @see                  #createForDelimitedStringArrays(byte)
523          * @see                  #createForStrings(int, int[])
524          * 
525          * @since                1.18
526          */
527         public static ColumnDesc createForDelimitedStringArrays(byte delim) throws FitsException {
528             ColumnDesc c = createForVariableStringArrays(-1);
529             c.setStringDelimiter(delim);
530             return c;
531         }
532 
533         /**
534          * Creates a new column descriptor for variable length 1D arrays or strings. The type may be any primitive type,
535          * or else <code>String.class</code>, <code>Boolean.class</code> (for FITS logicals),
536          * <code>ComplexValue.class</code> or <code>ComplexValue.Float.class</code> (for complex values with 64-bit and
537          * 32-bit precision, respectively). Whereas {@link Boolean} type columns will be stored as FITS logicals (1
538          * element per byte), <code>boolean</code> types will be stored as packed bits (with up to 8 elements per byte).
539          * 
540          * @param  type          The Java type of base elements that this column is designated to contain. For example
541          *                           <code>int.class</code> if the column will contain integers or arrays of integers.
542          * 
543          * @return               the new column descriptor
544          * 
545          * @throws FitsException if the base type is not one that can be used in binary table columns.
546          * 
547          * @see                  #createForScalars(Class)
548          * @see                  #createForStrings(int)
549          * @see                  #createForStrings(int, int[])
550          * @see                  #ColumnDesc(Class, int[])
551          * 
552          * @since                1.18
553          */
554         public static ColumnDesc createForVariableSize(Class<?> type) throws FitsException {
555             ColumnDesc c = new ColumnDesc(type);
556             c.setVariableSize(false);
557             return c;
558         }
559 
560         /**
561          * Recalculate the legacy Java entry shape from the FITS shape (as stored by TDIM). Strings drop the last
562          * dimension from the FITS shape (which becomes the string length), while complex values add a dimension of
563          * <code>[2]</code> to the FITS shape, reflecting the shape of their real-valued components.
564          */
565         private void calcLegacyShape() {
566             if (isString()) {
567                 legacyShape = Arrays.copyOf(fitsShape, fitsShape.length - 1);
568                 stringLength = fitsShape[fitsShape.length - 1];
569             } else if (isComplex()) {
570                 legacyShape = Arrays.copyOf(fitsShape, fitsShape.length + 1);
571                 legacyShape[fitsShape.length] = 2;
572             } else {
573                 legacyShape = fitsShape;
574             }
575         }
576 
577         /**
578          * Recalculate the FITS storage shape (as reported by TDIM) from the legacy Java array shape
579          */
580         private void calcFitsShape() {
581             if (isString()) {
582                 fitsShape = Arrays.copyOf(legacyShape, legacyShape.length + 1);
583                 fitsShape[legacyShape.length] = stringLength;
584             } else if (isComplex()) {
585                 fitsShape = Arrays.copyOf(legacyShape, legacyShape.length - 1);
586             } else {
587                 fitsShape = legacyShape;
588             }
589         }
590 
591         /**
592          * Returns the size of table entries in their trailing dimension.
593          * 
594          * @return the number of elemental components in the trailing dimension of table entries.
595          * 
596          * @see    #getLeadingShape()
597          */
598         private int getLastFitsDim() {
599             return fitsShape[fitsShape.length - 1];
600         }
601 
602         @Override
603         public ColumnDesc clone() {
604             try {
605                 ColumnDesc copy = (ColumnDesc) super.clone();
606                 fitsShape = fitsShape.clone();
607                 legacyShape = legacyShape.clone();
608 
609                 // Model should not be changed...
610                 return copy;
611             } catch (CloneNotSupportedException e) {
612                 return null;
613             }
614         }
615 
616         /**
617          * Specifies that this columns contains single (not array) boxed entrie, such as single primitives, strings, or
618          * complex values.
619          */
620         private void setSingleton() {
621             setBoxedShape(SINGLETON_SHAPE);
622         }
623 
624         /**
625          * Checks if this column contains single (scalar / non-array) elements only, including single strings or single
626          * complex values.
627          * 
628          * @return <code>true</code> if the column contains individual elements of its type, or else <code>false</code>
629          *             if it contains arrays.
630          * 
631          * @since  1.18
632          */
633         public final boolean isSingleton() {
634             if (isVariableSize()) {
635                 return isString() ? (stringLength < 0 && delimiter == 0) : false;
636             }
637 
638             if (isComplex()) {
639                 return fitsShape.length == 0;
640             }
641 
642             return legacyShape.length == 0;
643         }
644 
645         /**
646          * Checks if this column contains logical values. FITS logicals can each hve <code>true</code>,
647          * <code>false</code> or <code>null</code> (undefined) values. It is the support for these undefined values that
648          * set it apart from typical booleans. Also, logicals are stored as one byte per element. So if using only
649          * <code>true</code>, <code>false</code> values without <code>null</code> bits will offer more compact storage
650          * (by up to a factor of 8). You can convert existing logical columns to bits via
651          * {@link BinaryTable#convertToBits(int)}.
652          * 
653          * @return <code>true</code> if this column contains logical values.
654          * 
655          * @see    #isBits()
656          * @see    BinaryTable#convertToBits(int)
657          * 
658          * @since  1.18
659          */
660         public final boolean isLogical() {
661             return base == Boolean.class || (base == boolean.class && !isBits);
662         }
663 
664         /**
665          * Checks if this column contains only true boolean values (bits). Unlike logicals, bits can have only
666          * <code>true</code>, <code>false</code> values with no support for <code>null</code> , but offer more compact
667          * storage (by up to a factor of 8) than logicals. You can convert existing logical columns to bits via
668          * {@link BinaryTable#convertToBits(int)}.
669          * 
670          * @return <code>true</code> if this column contains <code>true</code> / <code>false</code> bits only.
671          * 
672          * @see    #isLogical()
673          * @see    BinaryTable#convertToBits(int)
674          * 
675          * @since  1.18
676          */
677         public final boolean isBits() {
678             return base == boolean.class && isBits;
679         }
680 
681         /**
682          * Checks if this column stores ASCII strings.
683          * 
684          * @return <code>true</code> if this column contains only strings.
685          * 
686          * @see    #isVariableSize()
687          */
688         public final boolean isString() {
689             return base == String.class;
690         }
691 
692         /**
693          * Checks if this column contains complex values. You can convert suitable columns of <code>float</code> or
694          * <code>double</code> elements to complex using {@link BinaryTable#setComplexColumn(int)}, as long as the last
695          * dimension is 2, ir if the variable-length columns contain even-number of values exclusively.
696          * 
697          * @return <code>true</code> if this column contains complex values.
698          */
699         public final boolean isComplex() {
700             return isComplex;
701         }
702 
703         /**
704          * Checks if this column contains numerical values, such as any primitive number type (e.g.
705          * <code>nt.class</code> or <code>double.class</code>) or else a {@link ComplexValue} type. type.
706          * 
707          * @return <code>true</code> if this column contains numerical data, including complex-valued data. String,
708          *             bits, and FITS logicals are not numerical (but all other column types are).
709          * 
710          * @since  1.20
711          */
712         public final boolean isNumeric() {
713             return !isLogical() && !isBits() && !isString();
714         }
715 
716         /**
717          * Returns the Java array element type that is used in Java to represent data in this column. When accessing
718          * columns or their elements in the old way, through arrays, this is the type that arrays from the Java side
719          * will expect or provide. For example, when storing {@link String} values (regular or variable-sized), this
720          * will return <code>String.class</code>. Arrays returned by {@link BinaryTable#getColumn(int)},
721          * {@link BinaryTable#getRow(int)}, and {@link BinaryTable#getElement(int, int)} will return arrays of this
722          * type, and the equivalent methods for setting data will expect arrays of this type as their argument.
723          * 
724          * @return     the Java class, arrays of which, packaged data for this column on the Java side.
725          * 
726          * @deprecated Ambiguous, use {@link #getLegacyBase()} instead. It can be confusing since it is not clear if it
727          *                 refers to array element types used in FITS storage or on the java side when using the older
728          *                 array access, or if it refers to the class of entries in the main table, which may be heap
729          *                 pointers. It is also distinct from {@link #getElementClass()}, which returns the boxed type
730          *                 used by {@link BinaryTable#get(int, int)} or {@link BinaryTable#set(int, int, Object)}.
731          */
732         @Deprecated
733         public Class<?> getBase() {
734             return getLegacyBase();
735         }
736 
737         /**
738          * Returns the primitive type that is used to store the data for this column in the FITS representation. This is
739          * the class for the actual data type, whether regularly shaped (multidimensional) arrays or variable length
740          * arrays (on the heap). For example, when storing {@link String} values (regular or variable-sized), this will
741          * return <code>byte.class</code>.
742          * 
743          * @return the primitive class, in used for storing data in the FITS representation.
744          * 
745          * @see    #getLegacyBase()
746          * 
747          * @since  1.18
748          */
749         final Class<?> getFitsBase() {
750             return fitsBase;
751         }
752 
753         /**
754          * <p>
755          * Returns the Java array element type that is used in Java to represent data in this column for the legacy
756          * table access methods. When accessing columns or their elements in the old way, through arrays, this is the
757          * type that arrays from the Java side will expect or provide. For example, when storing complex values (regular
758          * or variable-sized), this will return <code>float.class</code> or <code>double.class</code>. Arrays returned
759          * by {@link BinaryTable#getColumn(int)}, {@link BinaryTable#getRow(int)}, and
760          * {@link BinaryTable#getElement(int, int)} will return arrays of this type.
761          * </p>
762          * <p>
763          * This is different from {@link #getElementClass()}, which in turn returns the boxed type of objects returned
764          * by {@link BinaryTable#get(int, int)}.
765          * 
766          * @return the Java class, arrays of which, packaged data for this column on the Java side.
767          * 
768          * @see    #getElementClass()
769          * 
770          * @since  1.18
771          */
772         public Class<?> getLegacyBase() {
773             return base;
774         }
775 
776         /**
777          * (<i>for internal use</i>) Returns the primitive data class which is used for storing entries in the main
778          * (regular) table. For variable-sized columns, this will be the heap pointer class, not the FITS data class.
779          * 
780          * @return the class in which main table entries are stored.
781          * 
782          * @see    #isVariableSize()
783          */
784         private Class<?> getTableBase() {
785             return isVariableSize() ? pointerClass() : getFitsBase();
786         }
787 
788         /**
789          * Returns the dimensions of elements in this column. As of 1.18, this method returns a copy ot the array used
790          * internally, which is safe to modify.
791          * 
792          * @return     an array with the element dimensions.
793          * 
794          * @deprecated (<i>for internal use</i>) Use {@link #getEntryShape()} instead. Not useful to users since it
795          *                 returns the dimensions of the primitive storage types, which is not always the dimension of
796          *                 table entries on the Java side.
797          */
798         @Deprecated
799         public int[] getDimens() {
800             return fitsShape.clone();
801         }
802 
803         /**
804          * (<i>for internal use</i>) The dimension of elements in the FITS representation.
805          * 
806          * @return the dimension of elements in the FITS representation. For example an array of string will be 2
807          *             (number of string, number of bytes per string).
808          * 
809          * @see    #getEntryDimension()
810          */
811         private int fitsDimension() {
812             return fitsShape.length;
813         }
814 
815         /**
816          * Returns the boxed Java type of elements stored in a column.
817          * 
818          * @return The java type of elements in the columns. For columns containing strings, FITS logicals, or complex
819          *             values it will be <code>String.class</code>, <code>Boolean.class</code> or
820          *             <code>ComplexValue.class</code> respectively. For all other column types the primitive class of
821          *             the elements contained (e.g. <code>char.class</code>, <code>float.class</code>) is returned.
822          * 
823          * @since  1.18
824          * 
825          * @see    ColumnDesc#getElementCount()
826          * @see    ColumnDesc#getEntryShape()
827          * @see    ColumnDesc#getLegacyBase()
828          */
829         public final Class<?> getElementClass() {
830             if (isLogical()) {
831                 return Boolean.class;
832             }
833             if (isComplex()) {
834                 return ComplexValue.class;
835             }
836             return base;
837         }
838 
839         /**
840          * Returns the dimensionality of the 'boxed' elements as returned by {@link BinaryTable#get(int, int)} or
841          * expected by {@link BinaryTable#set(int, int, Object)}. That is it returns the dimnesion of 'boxed' elements,
842          * such as strings or complex values, rather than the dimension of characters or real components stored in the
843          * FITS for these.
844          * 
845          * @return the number of array dimensions in the 'boxed' Java type for this column. Variable-sized columns will
846          *             always return 1.
847          * 
848          * @see    #getEntryShape()
849          * @see    #getElementCount()
850          * 
851          * @since  1.18
852          */
853         public final int getEntryDimension() {
854             if (isVariableSize()) {
855                 return 1;
856             }
857             return isString() ? legacyShape.length : fitsShape.length;
858         }
859 
860         /**
861          * Returns the array shape of the 'boxed' elements as returned by {@link BinaryTable#get(int, int)} or expected
862          * by {@link BinaryTable#set(int, int, Object)}. That is it returns the array shape of 'boxed' elements, such as
863          * strings or complex values, rather than the shape of characters or real components stored in the FITS for
864          * these.
865          * 
866          * @return the array sized along each of the dimensions in the 'boxed' Java type for this column, or
867          *             <code>null</code> if the data is stored as variable-sized one-dimensional arrays of the boxed
868          *             element type. (Note, that accordingly variable-length string columns containing single strings
869          *             will thus return <code>{1}</code>, not <code>null</code>).
870          * 
871          * @see    #getEntryShape()
872          * @see    #getElementCount()
873          * @see    #isVariableSize()
874          * 
875          * @since  1.18
876          */
877         public final int[] getEntryShape() {
878             if (isVariableSize()) {
879                 return null;
880             }
881 
882             if (isComplex) {
883                 return fitsShape.clone();
884             }
885 
886             return legacyShape.clone();
887         }
888 
889         /**
890          * Returns the number of primitive elements (sych as bytes) that constitute a Java element (such as a String) in
891          * this table.
892          * 
893          * @return The number of primitives per Java element in the column, that is 1 for columns of primitive types, 2
894          *             for complex-valued columns, or the number of bytes (characters) in a String element.
895          *             Variable-length strings will return -1.
896          * 
897          * @since  1.18
898          * 
899          * @see    #getElementCount()
900          * @see    #getLegacyBase()
901          */
902         public final int getElementWidth() {
903             if (isComplex()) {
904                 return 2;
905             }
906             if (isString()) {
907                 return getStringLength();
908             }
909             return 1;
910         }
911 
912         /**
913          * Returns the number of 'boxed' elements as returned by {@link BinaryTable#get(int, int)} or expected by
914          * {@link BinaryTable#set(int, int, Object)}. That is it returns the number of strings or complex values per
915          * table entry, rather than the number of of characters or real components stored in the FITS for these.
916          * 
917          * @return the number of array elements in the 'boxed' Java type for this column, or -1 if the column contains
918          *             elements of varying size.
919          * 
920          * @see    #getEntryShape()
921          * @see    #getEntryDimension()
922          * @see    #isVariableSize()
923          * 
924          * @since  1.18
925          */
926         public final int getElementCount() {
927             if (isVariableSize()) {
928                 return isString() ? 1 : -1;
929             }
930 
931             if (isString()) {
932                 return fitsCount / getStringLength();
933             }
934 
935             return fitsCount;
936         }
937 
938         /**
939          * Returns the number of primitive base elements for a given FITS element count.
940          * 
941          * @param  fitsLen the FITS element count, sucj a a number of integers, complex-values, or bits
942          * 
943          * @return         the number of Java primitives that will be used to represent the number of FITS values for
944          *                     this type of column.
945          * 
946          * @see            #getFitsBase()
947          */
948         private int getFitsBaseCount(int fitsLen) {
949             if (isBits) {
950                 return (fitsLen + Byte.SIZE - 1) / Byte.SIZE;
951             }
952             if (isComplex) {
953                 return fitsLen << 1;
954             }
955             return fitsLen;
956         }
957 
958         /**
959          * Returns the number of regular primitive table elements in this column. For example, variable-length columns
960          * will always return 2, and complex-valued columns will return twice the number of complex values stored in
961          * each table entry.
962          * 
963          * @return the number of primitive table elements
964          * 
965          * @since  1.18
966          */
967         public final int getTableBaseCount() {
968             if (isVariableSize()) {
969                 return 2;
970             }
971             return getFitsBaseCount(fitsCount);
972         }
973 
974         /**
975          * Checks if this column contains entries of different size. Data for variable length coulmns is stored on the
976          * heap as one-dimemnsional arrays. As such information about the 'shape' of data is lost when they are stored
977          * that way.
978          * 
979          * @return <code>true</code> if the column contains elements of variable size, or else <code>false</code> if all
980          *             entries have the same size and shape.
981          */
982         public final boolean isVariableSize() {
983             return pointerType != POINTER_NONE;
984         }
985 
986         /**
987          * @deprecated      (<i>for internal use</i>) This method should be private in the future.
988          * 
989          * @return          new instance of the array with space for the specified number of rows.
990          *
991          * @param      nRow the number of rows to allocate the array for
992          */
993         @Deprecated
994         public Object newInstance(int nRow) {
995             return ArrayFuncs.newInstance(getTableBase(), getTableBaseCount() * nRow);
996         }
997 
998         /**
999          * @deprecated (<i>for internal use</i>) It may be reduced to private visibility in the future. Returns the
1000          *                 number of bytes that each element occupies in its FITS serialized form in the stored row
1001          *                 data.
1002          * 
1003          * @return     the number of bytes an element occupies in the FITS binary table data representation
1004          */
1005         @Deprecated
1006         public int rowLen() {
1007             return getTableBaseCount() * ElementType.forClass(getTableBase()).size();
1008         }
1009 
1010         /**
1011          * Checks if this column used 64-bit heap pointers.
1012          * 
1013          * @return <code>true</code> if the column uses 64-bit heap pointers, otherwise <code>false</code>
1014          * 
1015          * @see    #createForVariableSize(Class)
1016          * 
1017          * @since  1.18
1018          */
1019         public boolean hasLongPointers() {
1020             return pointerType == POINTER_LONG;
1021         }
1022 
1023         /**
1024          * Returns the <code>TFORM</code><i>n</i> character code for the heap pointers in this column or 0 if this is
1025          * not a variable-sized column.
1026          * 
1027          * @return <code>int.class</code> or <code>long.class</code>
1028          */
1029         private char pointerType() {
1030             return pointerType;
1031         }
1032 
1033         /**
1034          * Returns the primitive class used for sotring heap pointers for this column
1035          * 
1036          * @return <code>int.class</code> or <code>long.class</code>
1037          */
1038         private Class<?> pointerClass() {
1039             return pointerType == POINTER_LONG ? long.class : int.class;
1040         }
1041 
1042         /**
1043          * Sets whether this column will contain variable-length data, rather than fixed-shape data.
1044          * 
1045          * @param useLongPointers <code>true</code> to use 64-bit heap pointers for variable-length arrays or else
1046          *                            <code>false</code> to use 32-bit pointers.
1047          */
1048         private void setVariableSize(boolean useLongPointers) {
1049             pointerType = useLongPointers ? POINTER_LONG : POINTER_INT;
1050             fitsCount = 2;
1051             fitsShape = new int[] {2};
1052             legacyShape = fitsShape;
1053             stringLength = -1;
1054         }
1055 
1056         /**
1057          * Sets a custom substring delimiter byte for variable length string arrays, between ASCII 0x20 and 0x7e. We
1058          * will however tolerate values outside of that range, but log an appropriate warning to alert users of the
1059          * violation of the standard. User's can either 'fix' it, or suppress the warning if they want to stick to their
1060          * guns.
1061          * 
1062          * @param delim the delimiter byte value, between ASCII 0x20 and 0x7e (inclusive).
1063          * 
1064          * @since       1.18
1065          */
1066         private void setStringDelimiter(byte delim) {
1067             if (delim < FitsUtil.MIN_ASCII_VALUE || delim > FitsUtil.MAX_ASCII_VALUE) {
1068                 LOG.warning("WARNING! Substring terminator byte " + (delim & FitsIO.BYTE_MASK)
1069                         + " outside of the conventional range of " + FitsUtil.MIN_ASCII_VALUE + " through "
1070                         + FitsUtil.MAX_ASCII_VALUE + " (inclusive)");
1071             }
1072             delimiter = delim;
1073         }
1074 
1075         /**
1076          * Checks if <code>null</code> array elements are permissible for this column. It is for strings (which map to
1077          * empty strings), and for logical columns, where they signify undefined values.
1078          * 
1079          * @return <code>true</code> if <code>null</code> entries are considered valid for this column.
1080          */
1081         private boolean isNullAllowed() {
1082             return isLogical() || isString();
1083         }
1084 
1085         /**
1086          * Parses the substring array convention from a TFORM value, to set string length (if desired) and a delimiter
1087          * in variable-length string arrays.
1088          * 
1089          * @param tform     the TFORM header value for this column
1090          * @param pos       the parse position immediately after the 'A'
1091          * @param setLength Whether to use the substring definition to specify the max string component length, for
1092          *                      example because it is not defined otherwise by TDIM.
1093          */
1094         private void parseSubstringConvention(String tform, ParsePosition pos, boolean setLength) {
1095 
1096             if (setLength) {
1097                 // Default string length...
1098                 setStringLength(isVariableSize() ? -1 : fitsCount);
1099             }
1100 
1101             // Parse substring array convention...
1102             if (pos.getIndex() >= tform.length()) {
1103                 return;
1104             }
1105 
1106             // Try 'rAw' format...
1107             try {
1108                 int len = AsciiFuncs.parseInteger(tform, pos);
1109                 if (setLength) {
1110                     setStringLength(len);
1111                 }
1112                 return;
1113             } catch (Exception e) {
1114                 // Keep going...
1115             }
1116 
1117             // Find if and where is the ":SSTR" marker in the format
1118             int iSub = tform.indexOf(SUBSTRING_MARKER, pos.getIndex());
1119             if (iSub < 0) {
1120                 // No substring definition...
1121                 return;
1122             }
1123 
1124             pos.setIndex(iSub + SUBSTRING_MARKER.length());
1125 
1126             // Set the substring width....
1127             try {
1128                 int len = AsciiFuncs.parseInteger(tform, pos);
1129                 if (setLength) {
1130                     setStringLength(len);
1131                 }
1132             } catch (Exception e) {
1133                 LOG.warning("WARNING! Could not parse substring length from TFORM: [" + tform + "]");
1134             }
1135 
1136             // Parse substring array convention...
1137             if (pos.getIndex() >= tform.length()) {
1138                 return;
1139             }
1140 
1141             if (AsciiFuncs.extractChar(tform, pos) != '/') {
1142                 return;
1143             }
1144 
1145             try {
1146                 setStringDelimiter((byte) AsciiFuncs.parseInteger(tform, pos));
1147             } catch (NumberFormatException e) {
1148                 // Warn if the delimiter is outside of the range supported by the convention.
1149                 LOG.warning("WARNING! Could not parse substring terminator from TFORM: [" + tform + "]");
1150             }
1151         }
1152 
1153         private void appendSubstringConvention(StringBuffer tform) {
1154             if (getStringLength() > 0) {
1155                 tform.append(SUBSTRING_MARKER);
1156                 tform.append(getStringLength());
1157 
1158                 if (delimiter != 0) {
1159                     tform.append('/');
1160                     tform.append(new DecimalFormat("000").format(delimiter & FitsIO.BYTE_MASK));
1161                 }
1162             }
1163         }
1164 
1165         /**
1166          * Returns the TFORM header value to use for this column.
1167          * 
1168          * @return               The TFORM value that describes this column
1169          * 
1170          * @throws FitsException If the column itself is invalid.
1171          */
1172         String getTFORM() throws FitsException {
1173 
1174             StringBuffer tform = new StringBuffer();
1175 
1176             tform.append(isVariableSize() ? "1" + pointerType() : fitsCount);
1177 
1178             if (base == int.class) {
1179                 tform.append('J');
1180             } else if (base == short.class) {
1181                 tform.append('I');
1182             } else if (base == byte.class) {
1183                 tform.append('B');
1184             } else if (base == char.class) {
1185                 if (FitsFactory.isUseUnicodeChars()) {
1186                     tform.append('I');
1187                 } else {
1188                     tform.append('A');
1189                 }
1190             } else if (base == float.class) {
1191                 tform.append(isComplex() ? 'C' : 'E');
1192             } else if (base == double.class) {
1193                 tform.append(isComplex() ? 'M' : 'D');
1194             } else if (base == long.class) {
1195                 tform.append('K');
1196             } else if (isLogical()) {
1197                 tform.append('L');
1198             } else if (isBits()) {
1199                 tform.append('X');
1200             } else if (isString()) {
1201                 tform.append('A');
1202                 if (isVariableSize()) {
1203                     appendSubstringConvention(tform);
1204                 }
1205             } else {
1206                 throw new FitsException("Invalid column data class:" + base);
1207             }
1208 
1209             return tform.toString();
1210         }
1211 
1212         /**
1213          * Returns the TDIM header value that descrives the shape of entries in this column
1214          * 
1215          * @return the TDIM header value to use, or <code>null</code> if this column is not suited for a TDIM entry for
1216          *             example because it is variable-sized, or because its entries are not multidimensional. .
1217          */
1218         String getTDIM() {
1219             if (isVariableSize()) {
1220                 return null;
1221             }
1222 
1223             if (fitsShape.length < 2) {
1224                 return null;
1225             }
1226 
1227             StringBuffer tdim = new StringBuffer();
1228             char prefix = '(';
1229             for (int i = fitsShape.length - 1; i >= 0; i--) {
1230                 tdim.append(prefix);
1231                 tdim.append(fitsShape[i]);
1232                 prefix = ',';
1233             }
1234             tdim.append(')');
1235             return tdim.toString();
1236         }
1237 
1238         private boolean setFitsType(char type) throws FitsException {
1239             switch (type) {
1240             case 'A':
1241                 fitsBase = byte.class;
1242                 base = String.class;
1243                 break;
1244 
1245             case 'X':
1246                 fitsBase = byte.class;
1247                 base = boolean.class;
1248                 break;
1249 
1250             case 'L':
1251                 fitsBase = byte.class;
1252                 base = boolean.class;
1253                 break;
1254 
1255             case 'B':
1256                 fitsBase = byte.class;
1257                 base = byte.class;
1258                 break;
1259 
1260             case 'I':
1261                 fitsBase = short.class;
1262                 base = short.class;
1263                 break;
1264 
1265             case 'J':
1266                 fitsBase = int.class;
1267                 base = int.class;
1268                 break;
1269 
1270             case 'K':
1271                 fitsBase = long.class;
1272                 base = long.class;
1273                 break;
1274 
1275             case 'E':
1276             case 'C':
1277                 fitsBase = float.class;
1278                 base = float.class;
1279                 break;
1280 
1281             case 'D':
1282             case 'M':
1283                 fitsBase = double.class;
1284                 base = double.class;
1285                 break;
1286 
1287             default:
1288                 return false;
1289             }
1290 
1291             return true;
1292         }
1293     }
1294 
1295     /**
1296      * The enclosing binary table's properties
1297      * 
1298      * @deprecated (<i>for internal use</i>) no longer used, and will be removed in the future.
1299      */
1300     @Deprecated
1301     protected static class SaveState {
1302         /**
1303          * Create a new saved state
1304          * 
1305          * @param      columns the column descriptions to save
1306          * @param      heap    the heap to save
1307          * 
1308          * @deprecated         (<i>for internal use</i>) no longer in use. Will remove in the future.
1309          */
1310         @Deprecated
1311         public SaveState(List<ColumnDesc> columns, FitsHeap heap) {
1312         }
1313     }
1314 
1315     /**
1316      * Our own Logger instance, for nothing various non-critical issues.
1317      */
1318     private static final Logger LOG = Logger.getLogger(BinaryTable.class.getName());
1319 
1320     /**
1321      * This is the area in which variable length column data lives.
1322      */
1323     private FitsHeap heap;
1324 
1325     /**
1326      * The heap start from the head of the HDU
1327      */
1328     private long heapAddress;
1329 
1330     /**
1331      * (bytes) Empty space to leave after the populated heap area for future additions.
1332      */
1333     private int heapReserve;
1334 
1335     /**
1336      * The original heap size (from the header)
1337      */
1338     private int heapFileSize;
1339 
1340     /**
1341      * A list describing each of the columns in the table
1342      */
1343     private List<ColumnDesc> columns;
1344 
1345     /**
1346      * The number of rows in the table.
1347      */
1348     private int nRow;
1349 
1350     /**
1351      * The length in bytes of each row.
1352      */
1353     private int rowLen;
1354 
1355     /**
1356      * Where the data is actually stored.
1357      */
1358     private ColumnTable<?> table;
1359 
1360     private FitsEncoder encoder;
1361 
1362     /**
1363      * Creates an empty binary table, which can be populated with columns / rows as desired.
1364      */
1365     public BinaryTable() {
1366         table = new ColumnTable<>();
1367         columns = new ArrayList<>();
1368         heap = new FitsHeap(0);
1369         nRow = 0;
1370         rowLen = 0;
1371     }
1372 
1373     /**
1374      * Creates a binary table from an existing column table. <b>WARNING!</b>, as of 1.18 we no longer use the column
1375      * data extra state to carry information about an enclosing class, because it is horribly bad practice. You should
1376      * not use this constructor to create imperfect copies of binary tables. Rather, use {@link #copy()} if you want to
1377      * create a new binary table, which properly inherits <b>ALL</b> of the properties of an original one. As for this
1378      * constructor, you should assume that it will not use anything beyond what's available in any generic vanilla
1379      * column table.
1380      *
1381      * @param      tab           the column table to create the binary table from. It must be a regular column table
1382      *                               that contains regular data of scalar or fixed 1D arrays only (not heap pointers).
1383      *                               No information beyond what a generic vanilla column table provides will be used.
1384      *                               Column tables don't store imensions for their elements, and don't have
1385      *                               variable-sized entries. Thus, if the table was the used in another binary table to
1386      *                               store flattened multidimensional data, we'll detect that data as 1D arrays. Andm if
1387      *                               the table was used to store heap pointers for variable length arrays, we'll detect
1388      *                               these as regular <code>int[2]</code> or <code>long[2]</code> values.
1389      * 
1390      * @deprecated               DO NOT USE -- it will be removed in the future.
1391      * 
1392      * @throws     FitsException if the table could not be copied and threw a {@link nom.tam.util.TableException}, which
1393      *                               is preserved as the cause.
1394      * 
1395      * @see                      #copy()
1396      */
1397     @Deprecated
1398     public BinaryTable(ColumnTable<?> tab) throws FitsException {
1399         this();
1400 
1401         table = new ColumnTable<>();
1402         nRow = tab.getNRows();
1403         columns = new ArrayList<>();
1404 
1405         for (int i = 0; i < tab.getNCols(); i++) {
1406             int n = tab.getElementSize(i);
1407             ColumnDesc c = new ColumnDesc(tab.getElementClass(i), n > 1 ? new int[] {n} : SINGLETON_SHAPE);
1408             addFlattenedColumn(tab.getColumn(i), nRow, c, true);
1409         }
1410 
1411     }
1412 
1413     /**
1414      * Creates a binary table from a given FITS header description. The table columns are initialized but no data will
1415      * be available, at least initially. Data may be loaded later (e.g. deferred read mode), provided the table is
1416      * associated to an input (usually only if this constructor is called from a {@link Fits} object reading an input).
1417      * When the table has an input configured via a {@link Fits} object, the table entries may be accessed in-situ in
1418      * the file while in deferred read mode, but operations affecting significant portions of the table (e.g. retrieving
1419      * all data via {@link #getData()} or accessing entire columns) may load the data in memory. You can also call
1420      * {@link #detach()} any time to force loading the data into memory, so that alterations after that will not be
1421      * reflected in the original file, at least not unitl {@link #rewrite()} is called explicitly.
1422      * 
1423      * @param      header        A FITS header describing what the binary table should look like.
1424      *
1425      * @throws     FitsException if the specified header is not usable for a binary table
1426      * 
1427      * @deprecated               (<i>for internal use</i>) This constructor should only be called from a {@link Fits}
1428      *                               object reading an input; visibility may be reduced to the package level in the
1429      *                               future.
1430      * 
1431      * @see                      #isDeferred()
1432      */
1433     @Deprecated
1434     public BinaryTable(Header header) throws FitsException {
1435         String ext = header.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
1436 
1437         if (!ext.equalsIgnoreCase(Standard.XTENSION_BINTABLE) && !ext.equalsIgnoreCase(NonStandard.XTENSION_A3DTABLE)) {
1438             throw new FitsException(
1439                     "Not a binary table header (XTENSION = " + header.getStringValue(Standard.XTENSION) + ")");
1440         }
1441 
1442         synchronized (this) {
1443             nRow = header.getIntValue(Standard.NAXIS2);
1444         }
1445 
1446         long tableSize = nRow * header.getLongValue(Standard.NAXIS1);
1447         long paramSizeL = header.getLongValue(Standard.PCOUNT);
1448         long heapOffsetL = header.getLongValue(Standard.THEAP, tableSize);
1449 
1450         // Subtract out the size of the regular table from
1451         // the heap offset.
1452         long heapSizeL = (tableSize + paramSizeL) - heapOffsetL;
1453 
1454         if (heapSizeL < 0) {
1455             throw new FitsException("Inconsistent THEAP and PCOUNT");
1456         }
1457         if (heapSizeL > Integer.MAX_VALUE) {
1458             throw new FitsException("Heap size > 2 GB");
1459         }
1460         if (heapSizeL == 0L) {
1461             // There is no heap. Forget the offset
1462             heapAddress = 0;
1463         }
1464 
1465         heapAddress = (int) heapOffsetL;
1466         heapFileSize = (int) heapSizeL;
1467 
1468         int nCol = header.getIntValue(Standard.TFIELDS);
1469 
1470         synchronized (this) {
1471             rowLen = 0;
1472 
1473             columns = new ArrayList<>();
1474             for (int col = 0; col < nCol; col++) {
1475                 rowLen += processCol(header, col, rowLen);
1476             }
1477 
1478             HeaderCard card = header.getCard(Standard.NAXIS1);
1479             card.setValue(rowLen);
1480         }
1481     }
1482 
1483     /**
1484      * Creates a binary table from existing table data int row-major format. That is the first array index is the row
1485      * index while the second array index is the column index.
1486      *
1487      * @param      rowColTable   Row / column array. Scalars elements are wrapped in arrays of 1, s.t. a single
1488      *                               <code>int</code> elements is stored as <code>int[1]</code> at its
1489      *                               <code>[row][col]</code> index.
1490      *
1491      * @throws     FitsException if the argument is not a suitable representation of data in rows.
1492      * 
1493      * @deprecated               The constructor is ambiguous, use {@link #fromRowMajor(Object[][])} instead. You can
1494      *                               have a column-major array that has no scalar primitives which would also be an
1495      *                               <code>Object[][]</code> and could be passed erroneously.
1496      */
1497     @Deprecated
1498     public BinaryTable(Object[][] rowColTable) throws FitsException {
1499         this();
1500         for (Object[] row : rowColTable) {
1501             addRow(row);
1502         }
1503     }
1504 
1505     /**
1506      * Creates a binary table from existing table data in row-major format. That is the first array index is the row
1507      * index while the second array index is the column index;
1508      *
1509      * @param  table         Row / column array. Scalars elements are wrapped in arrays of 1, s.t. a single
1510      *                           <code>int</code> elements is stored as <code>int[1]</code> at its
1511      *                           <code>[row][col]</code> index.
1512      * 
1513      * @return               a new binary table with the data. The tables data may be partially independent from the
1514      *                           argument. Modifications to the table data, or that to the argument have undefined
1515      *                           effect on the other object. If it is important to decouple them, you can use a
1516      *                           {@link ArrayFuncs#deepClone(Object)} of your original data as an argument.
1517      *
1518      * @throws FitsException if the argument is not a suitable representation of FITS data in rows.
1519      * 
1520      * @see                  #fromColumnMajor(Object[])
1521      * 
1522      * @since                1.18
1523      */
1524     public static BinaryTable fromRowMajor(Object[][] table) throws FitsException {
1525         BinaryTable tab = new BinaryTable();
1526         for (Object[] row : table) {
1527             tab.addRow(row);
1528         }
1529         return tab;
1530     }
1531 
1532     /**
1533      * Create a binary table from existing data in column-major format order.
1534      *
1535      * @param      columns       array of columns. The data for scalar entries is a primive array. For all else, the
1536      *                               entry is an <code>Object[]</code> array of sorts.
1537      * 
1538      * @throws     FitsException if the data for the columns could not be used as coulumns
1539      * 
1540      * @deprecated               The constructor is ambiguous, use {@link #fromColumnMajor(Object[])} instead. One could
1541      *                               call this method with any row-major <code>Object[][]</code> table by mistake.
1542      * 
1543      * @see                      #defragment()
1544      */
1545     @Deprecated
1546     public BinaryTable(Object[] columns) throws FitsException {
1547         this();
1548 
1549         for (Object element : columns) {
1550             addColumn(element);
1551         }
1552     }
1553 
1554     /**
1555      * Creates a binary table from existing data in column-major format order.
1556      *
1557      * @param  columns       array of columns. The data for scalar entries is a primive array. For all else, the entry
1558      *                           is an <code>Object[]</code> array of sorts.
1559      * 
1560      * @return               a new binary table with the data. The tables data may be partially independent from the
1561      *                           argument. Modifications to the table data, or that to the argument have undefined
1562      *                           effect on the other object. If it is important to decouple them, you can use a
1563      *                           {@link ArrayFuncs#deepClone(Object)} of your original data as an argument.
1564      * 
1565      * @throws FitsException if the argument is not a suitable representation of FITS data in rows.
1566      * 
1567      * @see                  #fromColumnMajor(Object[])
1568      * 
1569      * @since                1.18
1570      */
1571     public static BinaryTable fromColumnMajor(Object[] columns) throws FitsException {
1572         BinaryTable t = new BinaryTable();
1573         for (Object element : columns) {
1574             t.addColumn(element);
1575         }
1576         return t;
1577     }
1578 
1579     @Override
1580     protected BinaryTable clone() {
1581         try {
1582             return (BinaryTable) super.clone();
1583         } catch (CloneNotSupportedException e) {
1584             return null;
1585         }
1586     }
1587 
1588     /**
1589      * Returns an independent copy of the binary table.
1590      * 
1591      * @return               a new binary that tnat contains an exact copy of us, but is completely independent.
1592      * 
1593      * @throws FitsException if the table could not be copied
1594      * 
1595      * @since                1.18
1596      */
1597     public synchronized BinaryTable copy() throws FitsException {
1598         BinaryTable copy = clone();
1599 
1600         synchronized (copy) {
1601             if (table != null) {
1602                 copy.table = table.copy();
1603             }
1604             if (heap != null) {
1605                 copy.heap = heap.copy();
1606             }
1607 
1608             copy.columns = new ArrayList<>();
1609             for (ColumnDesc c : columns) {
1610                 c = c.clone();
1611                 copy.columns.add(c);
1612             }
1613         }
1614 
1615         return copy;
1616     }
1617 
1618     /**
1619      * (<i>for internal use</i>) Discards all variable-length arrays from this table, that is all data stored on the
1620      * heap, and resets all heap descritors to (0,0).
1621      * 
1622      * @since 1.19.1
1623      */
1624     protected synchronized void discardVLAs() {
1625         for (int col = 0; col < columns.size(); col++) {
1626             ColumnDesc c = columns.get(col);
1627 
1628             if (c.isVariableSize()) {
1629                 for (int row = 0; row < nRow; row++) {
1630                     table.setElement(row, col, c.hasLongPointers() ? new long[2] : new int[2]);
1631                 }
1632             }
1633         }
1634 
1635         heap = new FitsHeap(0);
1636     }
1637 
1638     /**
1639      * Returns the number of bytes per regular table row
1640      * 
1641      * @return the number of bytes in a regular table row.
1642      */
1643     final synchronized int getRowBytes() {
1644         return rowLen;
1645     }
1646 
1647     /**
1648      * @deprecated               (<i>for internal use</i>) It may become a private method in the future.
1649      *
1650      * @param      table         the table to create the column data.
1651      *
1652      * @throws     FitsException if the data could not be created.
1653      */
1654     @Deprecated
1655     public static void createColumnDataFor(BinaryTable table) throws FitsException {
1656         synchronized (table) {
1657             table.createTable(table.nRow);
1658         }
1659     }
1660 
1661     /**
1662      * @deprecated       (<i>for internal use</i>) It may be reduced to private visibility in the future. Parse the
1663      *                       TDIMS value. If the TDIMS value cannot be deciphered a one-d array with the size given in
1664      *                       arrsiz is returned.
1665      *
1666      * @param      tdims The value of the TDIMSn card.
1667      *
1668      * @return           An int array of the desired dimensions. Note that the order of the tdims is the inverse of the
1669      *                       order in the TDIMS key.
1670      */
1671     @Deprecated
1672     public static int[] parseTDims(String tdims) {
1673         if (tdims == null) {
1674             return null;
1675         }
1676 
1677         // The TDIMs value should be of the form: "(i,j...)"
1678         int start = tdims.indexOf('(');
1679 
1680         if (start < 0) {
1681             return null;
1682         }
1683 
1684         int end = tdims.indexOf(')', start);
1685         if (end < 0) {
1686             end = tdims.length();
1687         }
1688 
1689         StringTokenizer st = new StringTokenizer(tdims.substring(start + 1, end), ",");
1690         int dim = st.countTokens();
1691 
1692         if (dim > 0) {
1693             int[] dims = new int[dim];
1694             for (int i = dim; --i >= 0;) {
1695                 dims[i] = Integer.parseInt(st.nextToken().trim());
1696             }
1697             return dims;
1698         }
1699 
1700         return null;
1701     }
1702 
1703     /**
1704      * <p>
1705      * Adds a column of complex values stored as the specified decimal type of components in the FITS. While you can
1706      * also use {@link #addColumn(Object)} to add complex values, that method will always add them as 64-bit
1707      * double-precision values. So, this method is provided to allow users more control over how they want their complex
1708      * data be stored.
1709      * </p>
1710      * <p>
1711      * The new column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default,
1712      * which can be changed by {@link ColumnDesc#name(String)} after.
1713      * </p>
1714      * 
1715      * @param  o             A {@link ComplexValue} or an array (possibly multi-dimensional) thereof.
1716      * @param  decimalType   <code>float.class</code> or <code>double.class</code> (all other values default to
1717      *                           <code>double.class</code>).
1718      * 
1719      * @return               the number of column in the table including the new column.
1720      * 
1721      * @throws FitsException if the object contains values other than {@link ComplexValue} types or if the array is not
1722      *                           suitable for storing in the FITS, e.g. because it is multi-dimensional but varying in
1723      *                           shape / size.
1724      * 
1725      * @since                1.18
1726      * 
1727      * @see                  #addColumn(Object)
1728      */
1729     public int addComplexColumn(Object o, Class<?> decimalType) throws FitsException {
1730         int col = columns.size();
1731         int eSize = addColumn(ArrayFuncs.complexToDecimals(o, decimalType));
1732         ColumnDesc c = columns.get(col);
1733         c.isComplex = true;
1734         c.setLegacyShape(c.fitsShape);
1735         return eSize;
1736     }
1737 
1738     /**
1739      * <p>
1740      * Adds a column of string values (one per row), optimized for storage size. Unlike {@link #addColumn(Object)},
1741      * which always store strings in fixed format, this method will automatically use variable-length columns for
1742      * storing the strings if their lengths vary sufficiently to make that form of storage more efficient, or if the
1743      * array contains nulls (which may be defined later).
1744      * </p>
1745      * <p>
1746      * The new column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default,
1747      * which can be changed by {@link ColumnDesc#name(String)} after.
1748      * </p>
1749      * 
1750      * @param  o             A 1D string array, with 1 string element per table row. The array may contain
1751      *                           <code>null</code> entries, in which case variable-length columns will be used, since
1752      *                           these may be defined later...
1753      * 
1754      * @return               the number of column in the table including the new column.
1755      * 
1756      * @throws FitsException if the object contains values other than {@link ComplexValue} types or if the array is not
1757      *                           suitable for storing in the FITS, e.g. because it is multi-dimensional but varying in
1758      *                           shape / size.
1759      * 
1760      * @since                1.18
1761      * 
1762      * @see                  #addColumn(Object)
1763      */
1764     public int addStringColumn(String[] o) throws FitsException {
1765         checkRowCount(o);
1766 
1767         ColumnDesc c = new ColumnDesc(String.class);
1768 
1769         // Check if we should be using variable-length strings
1770         // (provided its a scalar string column with sufficiently varied strings sizes to make it worth..
1771         int min = FitsUtil.minStringLength(o);
1772         int max = FitsUtil.maxStringLength(o);
1773 
1774         if (max - min > 2 * ElementType.forClass(c.pointerClass()).size()) {
1775             c = ColumnDesc.createForVariableSize(String.class);
1776             return addVariableSizeColumn(o, c);
1777         }
1778 
1779         c = ColumnDesc.createForStrings(max);
1780         return addFlattenedColumn(o, o.length, c, false);
1781     }
1782 
1783     /**
1784      * <p>
1785      * Adds a column of bits. This uses much less space than if adding boolean values as logicals (the default behaviot
1786      * of {@link #addColumn(Object)}, since logicals take up 1 byte per element, whereas bits are really single bits.
1787      * </p>
1788      * <p>
1789      * The new column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default,
1790      * which can be changed by {@link ColumnDesc#name(String)} after.
1791      * </p>
1792      * 
1793      * @param  o                        An any-dimensional array of <code>boolean</code> values.
1794      * 
1795      * @return                          the number of column in the table including the new column.
1796      * 
1797      * @throws IllegalArgumentException if the argument is not an array of <code>boolean</code> values.
1798      * @throws FitsException            if the object is not an array of <code>boolean</code> values.
1799      * 
1800      * @since                           1.18
1801      * 
1802      * @see                             #addColumn(Object)
1803      */
1804     public int addBitsColumn(Object o) throws FitsException {
1805         if (ArrayFuncs.getBaseClass(o) != boolean.class) {
1806             throw new IllegalArgumentException("Not an array of booleans: " + o.getClass());
1807         }
1808         return addColumn(o, false);
1809     }
1810 
1811     /**
1812      * <p>
1813      * Adds a new empty column to the table to the specification. This is useful when the user may want ot have more
1814      * control on how columns are configured before calling {@link #addRow(Object[])} to start populating. The new
1815      * column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default, unless
1816      * already named otherwise.
1817      * </p>
1818      * <p>
1819      * The new column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default,
1820      * which can be changed by {@link ColumnDesc#name(String)} after.
1821      * </p>
1822      * 
1823      * @param  descriptor            the column descriptor
1824      * 
1825      * @return                       the number of table columns after the addition
1826      * 
1827      * @throws IllegalStateException if the table already contains data rows that prevent the addition of empty
1828      *                                   comlumns.
1829      * 
1830      * @see                          #addRow(Object[])
1831      * @see                          ColumnDesc#name(String)
1832      */
1833     public synchronized int addColumn(ColumnDesc descriptor) throws IllegalStateException {
1834         if (nRow != 0) {
1835             throw new IllegalStateException("Cannot add empty columns to table already containing data rows");
1836         }
1837 
1838         descriptor.offset = rowLen;
1839         rowLen += descriptor.rowLen();
1840 
1841         if (descriptor.name() == null) {
1842             // Set default column name;
1843             descriptor.name(TableHDU.getDefaultColumnName(columns.size()));
1844         }
1845         columns.add(descriptor);
1846         return columns.size();
1847     }
1848 
1849     /**
1850      * <p>
1851      * Adds a new column with the specified data array, with some default mappings. This method will always use
1852      * double-precision representation for {@link ComplexValue}-based data, and will represent <code>boolean</code>
1853      * based array data as one-byte-per element FITS logical values (for back compatibility). It will also store strings
1854      * as fixed sized (sized for the longest string element contained).
1855      * </p>
1856      * <p>
1857      * The new column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default,
1858      * which can be changed by {@link ColumnDesc#name(String)} after.
1859      * </p>
1860      * <p>
1861      * If you want other complex-valued representations use {@link #addComplexColumn(Object, Class)} instead, and if you
1862      * want to pack <code>boolean</code>-based data more efficiently (using up to 8 times less space), use
1863      * {@link #addBitsColumn(Object)} instead, or else convert the column to bits afterwards using
1864      * {@link #convertToBits(int)}. And, if you want to allow storing strings more effiently in variable-length columns,
1865      * you should use {@link #addStringColumn(String[])} instead.
1866      * </p>
1867      * <p>
1868      * As of 1.18, the argument can be a boxed primitive for a coulmn containing a single scalar-valued entry (row).
1869      * </p>
1870      * 
1871      * @see #addVariableSizeColumn(Object)
1872      * @see #addComplexColumn(Object, Class)
1873      * @see #addBitsColumn(Object)
1874      * @see #convertToBits(int)
1875      * @see #addStringColumn(String[])
1876      * @see ColumnDesc#name(String)
1877      */
1878     @Override
1879     public int addColumn(Object o) throws FitsException {
1880         return addColumn(o, true);
1881     }
1882 
1883     private synchronized int checkRowCount(Object o) throws FitsException {
1884         if (!o.getClass().isArray()) {
1885             throw new TableException("Not an array: " + o.getClass().getName());
1886         }
1887 
1888         int rows = Array.getLength(o);
1889 
1890         if (columns.size() != 0 && rows != nRow) {
1891             throw new TableException("Mismatched number of rows: " + rows + ", expected " + nRow);
1892         }
1893 
1894         return rows;
1895     }
1896 
1897     /**
1898      * Like {@link #addColumn(Object)}, but allows specifying whether we use back compatible mode. This mainly just
1899      * affects how <code>boolean</code> arrays are stored (as logical bytes in compatibility mode, or as packed bits
1900      * otherwise).
1901      * 
1902      * @param Whether to add the column in a back compatibility mode with versions prior to 1.18. If <code>true</code>
1903      *                    <code>boolean</code> arrays will stored as logical bytes, otherwise as packed bits.
1904      */
1905     private int addColumn(Object o, boolean compat) throws FitsException {
1906         o = ArrayFuncs.objectToArray(o, compat);
1907 
1908         int rows = checkRowCount(o);
1909 
1910         ColumnDesc c = new ColumnDesc(ArrayFuncs.getBaseClass(o));
1911 
1912         if (ArrayFuncs.getBaseClass(o) == ComplexValue.class) {
1913             o = ArrayFuncs.complexToDecimals(o, double.class);
1914             c.isComplex = true;
1915         }
1916 
1917         try {
1918             int[] dim = ArrayFuncs.checkRegularArray(o, c.isNullAllowed());
1919 
1920             if (c.isString()) {
1921                 c.setStringLength(FitsUtil.maxStringLength(o));
1922             }
1923 
1924             if (c.isComplex) {
1925                 // Drop the railing 2 dimension, keep only outer dims...
1926                 dim = Arrays.copyOf(dim, dim.length - 1);
1927                 o = ArrayFuncs.flatten(o);
1928             }
1929 
1930             if (dim.length <= 1) {
1931                 c.setSingleton();
1932             } else {
1933                 int[] shape = new int[dim.length - 1];
1934                 System.arraycopy(dim, 1, shape, 0, shape.length);
1935                 c.setLegacyShape(shape);
1936                 o = ArrayFuncs.flatten(o);
1937             }
1938         } catch (IllegalArgumentException e) {
1939             c.setVariableSize(false);
1940             return addVariableSizeColumn(o, c);
1941         }
1942         // getBaseClass() prevents heterogeneous columns, so no need to catch ClassCastException here.
1943 
1944         return addFlattenedColumn(o, rows, c, compat);
1945     }
1946 
1947     /**
1948      * <p>
1949      * Adds a new variable-length data column, populating it with the specified data object. Unlike
1950      * {@link #addColumn(Object)} which will use fixed-size data storage provided the data allows it, this method forces
1951      * the use of variable-sized storage regardless of the data layout -- for example to accommodate addiing rows /
1952      * elements of different sized at a later time.
1953      * </p>
1954      * <p>
1955      * The new column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default,
1956      * which can be changed by {@link ColumnDesc#name(String)} after.
1957      * </p>
1958      * 
1959      * @param  o             An array containing one entry per row. Multi-dimensional entries will be flattened to 1D
1960      *                           for storage on the heap.
1961      * 
1962      * @return               the number of table columns after the addition.
1963      * 
1964      * @throws FitsException if the column could not be created as requested.
1965      * 
1966      * @see                  #addColumn(Object)
1967      * @see                  #addColumn(ColumnDesc)
1968      * @see                  ColumnDesc#createForVariableSize(Class)
1969      * @see                  ColumnDesc#isVariableSize()
1970      * 
1971      * @since                1.18
1972      */
1973     public int addVariableSizeColumn(Object o) throws FitsException {
1974         Class<?> base = ArrayFuncs.getBaseClass(o);
1975         ColumnDesc c = ColumnDesc.createForVariableSize(base);
1976         return addVariableSizeColumn(o, c);
1977     }
1978 
1979     /**
1980      * Adds a new column with data directly, without performing any checks on the data. This should only be use
1981      * internally, after ansuring the data integrity and suitability for this table.
1982      * 
1983      * @param  o             the column data, whose integrity was verified previously
1984      * @param  rows          the number of rows the data contains (in flattened form)
1985      * @param  c             the new column's descriptor
1986      * 
1987      * @return               the number of table columns after the addition
1988      * 
1989      * @throws FitsException if the data is not the right type or format for internal storage.
1990      */
1991     private synchronized int addDirectColumn(Object o, int rows, ColumnDesc c) throws FitsException {
1992         c.offset = rowLen;
1993         rowLen += c.rowLen();
1994 
1995         // Load any deferred data (we will not be able to do that once we alter the column structure)
1996         ensureData();
1997 
1998         // Set the default column name
1999         c.name(TableHDU.getDefaultColumnName(columns.size()));
2000 
2001         table.addColumn(o, c.getTableBaseCount());
2002         columns.add(c);
2003 
2004         if (nRow == 0) {
2005             // Set the table row count to match first colum
2006             nRow = rows;
2007         }
2008 
2009         return columns.size();
2010     }
2011 
2012     private int addVariableSizeColumn(Object o, ColumnDesc c) throws FitsException {
2013         checkRowCount(o);
2014 
2015         Object[] array = (Object[]) o;
2016 
2017         o = Array.newInstance(c.pointerClass(), array.length * 2);
2018 
2019         for (int i = 0; i < array.length; i++) {
2020             boolean multi = c.isComplex() ? array[i] instanceof Object[][] : array[i] instanceof Object[];
2021 
2022             if (multi) {
2023                 boolean canBeComplex = false;
2024 
2025                 if (c.getFitsBase() == float.class || c.getFitsBase() == double.class) {
2026                     int[] dim = ArrayFuncs.getDimensions(array[i]);
2027                     if (dim[dim.length - 1] == 2) {
2028                         canBeComplex = true;
2029                     }
2030                 }
2031 
2032                 if (!canBeComplex && !c.warnedFlatten) {
2033                     LOG.warning("Table entries of " + array[i].getClass()
2034                             + " will be stored as 1D arrays in variable-length columns. "
2035                             + "Array shape(s) and intermittent null subarrays (if any) will be lost.");
2036 
2037                     c.warnedFlatten = true;
2038                 }
2039             }
2040 
2041             Object p = putOnHeap(c, array[i], null);
2042             System.arraycopy(p, 0, o, 2 * i, 2);
2043         }
2044 
2045         return addDirectColumn(o, array.length, c);
2046     }
2047 
2048     /**
2049      * Add a column where the data is already flattened.
2050      *
2051      * @param      o             The new column data. This should be a one-dimensional primitive array.
2052      * @param      dims          The dimensions of an element in the column, or null for singleton (scalar) columns
2053      *
2054      * @return                   the new column size
2055      *
2056      * @throws     FitsException if the array could not be flattened
2057      * 
2058      * @deprecated               (<i>for internal use</i>) No longer used, will be removed in the future
2059      */
2060     @Deprecated
2061     public int addFlattenedColumn(Object o, int... dims) throws FitsException {
2062         ColumnDesc c = new ColumnDesc(ArrayFuncs.getBaseClass(o));
2063 
2064         try {
2065             ArrayFuncs.checkRegularArray(o, c.isNullAllowed());
2066         } catch (IllegalArgumentException e) {
2067             throw new FitsException("Irregular array: " + o.getClass() + ": " + e.getMessage(), e);
2068         }
2069 
2070         if (c.isString()) {
2071             c.setStringLength(FitsUtil.maxStringLength(o));
2072         }
2073 
2074         int n = 1;
2075 
2076         c.setLegacyShape(dims);
2077         for (int dim : dims) {
2078             n *= dim;
2079         }
2080 
2081         int rows = Array.getLength(o) / n;
2082 
2083         return addFlattenedColumn(o, rows, c, true);
2084     }
2085 
2086     /**
2087      * Checks that a flattened column has a compatible size for storing in a fixed-width column. It will also log a
2088      * warning if the storage size of the object is zero.
2089      * 
2090      * @param  c             the column descriptor
2091      * @param  o             the column data
2092      * 
2093      * @throws FitsException if the data is not the right size for the column
2094      */
2095     private synchronized void checkFlattenedColumnSize(ColumnDesc c, Object o) throws FitsException {
2096         if (c.getTableBaseCount() == 0) {
2097             LOG.warning("Elements of column + " + columns.size() + " have zero storage size.");
2098         } else if (columns.size() > 0) {
2099             // Check that the number of rows is consistent.
2100             int l = Array.getLength(o);
2101             if (nRow > 0 && l != nRow * c.getTableBaseCount()) {
2102                 throw new TableException("Mismatched element count " + l + ", expected " + (nRow * c.getTableBaseCount()));
2103             }
2104         }
2105     }
2106 
2107     /**
2108      * This function is needed since we had made addFlattenedColumn public so in principle a user might have called it
2109      * directly.
2110      *
2111      * @param  o             The new column data. This should be a one-dimensional primitive array.
2112      * @param  c             The column description
2113      *
2114      * @return               the new column size
2115      *
2116      * @throws FitsException if the data type, format, or element count is inconsistent with this table.
2117      */
2118     private int addFlattenedColumn(Object o, int rows, ColumnDesc c, boolean compat) throws FitsException {
2119         // For back compatibility this method will add boolean values as logicals always...
2120         if (compat) {
2121             c.isBits = false;
2122         }
2123 
2124         if (c.isBits) {
2125             // Special handling for bits, which have to be segmented into bytes...
2126             boolean[] bits = (boolean[]) o;
2127             o = FitsUtil.bitsToBytes(bits, bits.length / rows);
2128         } else {
2129             o = javaToFits1D(c, o);
2130         }
2131 
2132         checkFlattenedColumnSize(c, o);
2133 
2134         return addDirectColumn(o, rows, c);
2135     }
2136 
2137     /**
2138      * <p>
2139      * Adds a row to the table. If this is the first row in a new table, fixed-length columns will be created from the
2140      * data type automatically. If you want more control over the column formats, you may want to specify columns
2141      * beforehand such as:
2142      * </p>
2143      * 
2144      * <pre>
2145      *   BinaryTable table = new BinaryTable();
2146      *   
2147      *   // A column containing 64-bit floating point scalar values, 1 per row...
2148      *   table.addColumn(ColumnDesc.createForScalars(double.class));
2149      *   
2150      *   // A column containing 5x4 arrays of single-precision complex values...
2151      *   table.addColumn(ColumnDesc.createForArrays(ComplexValue.Float.class, 5, 4)
2152      *  
2153      *   // A column containing Strings of variable length using 32-bit heap pointers...
2154      *   table.addColumn(ColumnDesc.creatForVariableStrings(false);
2155      * </pre>
2156      * <p>
2157      * For scalar columns of primitive types, the argument may be the corresponding java boxed type (new style), or a
2158      * primitive array of 1 (old style). Thus, you can write either:
2159      * </p>
2160      * 
2161      * <pre>
2162      * table.addRow(1, 3.14159265);
2163      * </pre>
2164      * <p>
2165      * or,
2166      * </p>
2167      * 
2168      * <pre>
2169      *   table.addRow(new Object[] { new int[] {1}, new double[] {3.14159265} };
2170      * </pre>
2171      * 
2172      * @see #addColumn(ColumnDesc)
2173      */
2174     @Override
2175     public synchronized int addRow(Object[] o) throws FitsException {
2176         if (columns.isEmpty()) {
2177             for (Object element : o) {
2178                 if (element == null) {
2179                     throw new TableException("Prototype row may not contain null");
2180                 }
2181 
2182                 Class<?> cl = element.getClass();
2183 
2184                 if (cl.isArray()) {
2185                     if (cl.getComponentType().isPrimitive() && Array.getLength(element) == 1) {
2186                         // Primitives of 1 (e.g. short[1]) are wrapped and should be added as is.
2187                         addColumn(element);
2188                     } else {
2189                         // Wrap into array of 1, as leading dimension becomes the number of rows, which must be 1...
2190                         Object wrapped = Array.newInstance(element.getClass(), 1);
2191                         Array.set(wrapped, 0, element);
2192                         addColumn(wrapped);
2193                     }
2194                 } else {
2195                     addColumn(ArrayFuncs.objectToArray(element, true));
2196                 }
2197             }
2198 
2199             return 1;
2200         }
2201 
2202         if (o.length != columns.size()) {
2203             throw new TableException("Mismatched row size: " + o.length + ", expected " + columns.size());
2204         }
2205 
2206         ensureData();
2207 
2208         Object[] flatRow = new Object[getNCols()];
2209 
2210         for (int i = 0; i < flatRow.length; i++) {
2211             ColumnDesc c = columns.get(i);
2212             if (c.isVariableSize()) {
2213                 flatRow[i] = putOnHeap(c, o[i], null);
2214             } else {
2215                 flatRow[i] = javaToFits1D(c, ArrayFuncs.flatten(o[i]));
2216 
2217                 int nexp = c.getElementCount();
2218                 if (c.stringLength > 0) {
2219                     nexp *= c.stringLength;
2220                 }
2221 
2222                 if (Array.getLength(flatRow[i]) != nexp) {
2223                     throw new IllegalArgumentException("Mismatched element count for column " + i + ": got "
2224                             + Array.getLength(flatRow[i]) + ", expected " + nexp);
2225                 }
2226             }
2227         }
2228 
2229         table.addRow(flatRow);
2230         nRow++;
2231 
2232         return nRow;
2233     }
2234 
2235     @Override
2236     public synchronized void deleteColumns(int start, int len) throws FitsException {
2237         ensureData();
2238 
2239         table.deleteColumns(start, len);
2240 
2241         ArrayList<ColumnDesc> remain = new ArrayList<>(columns.size() - len);
2242         rowLen = 0;
2243 
2244         for (int i = 0; i < columns.size(); i++) {
2245             if (i < start || i >= start + len) {
2246                 ColumnDesc c = columns.get(i);
2247                 c.offset = rowLen;
2248                 rowLen += c.rowLen();
2249                 remain.add(c);
2250             }
2251         }
2252         columns = remain;
2253     }
2254 
2255     @Override
2256     public synchronized void deleteRows(int row, int len) throws FitsException {
2257         ensureData();
2258         table.deleteRows(row, len);
2259         nRow -= len;
2260     }
2261 
2262     /**
2263      * Returns the Java type of elements returned or expected by the older srray-based access methods. It can be
2264      * confusing, because:
2265      * <ul>
2266      * <li>Columns with variable sized entries report <code>int.class</code> or <code>long.class</code> regardless of
2267      * data type.</li>
2268      * <li>Regular logical and bit columns bith report <code>boolean.class</code>.</li>
2269      * <li>Regular complex valued columns report <code>float.class</code> or <code>double.class</code>.</li>
2270      * </ul>
2271      * 
2272      * @return     the types in the table, not the underlying types (e.g., for varying length arrays or booleans).
2273      * 
2274      * @deprecated (<i>for internal use</i>) Ambiguous, use {@link ColumnDesc#getElementClass()} instead. Will remove in
2275      *                 the future.
2276      */
2277     @Deprecated
2278     public synchronized Class<?>[] getBases() {
2279         return table.getBases();
2280     }
2281 
2282     /**
2283      * <p>
2284      * Returns the data for a particular column in as an array of elements. See {@link #addColumn(Object)} for more
2285      * information about the format of data elements in general.
2286      * </p>
2287      * 
2288      * @param  col           The zero-based column index.
2289      * 
2290      * @return               an array of primitives (for scalar columns), or else an <code>Object[]</code> array, or
2291      *                           possibly <code>null</code>
2292      * 
2293      * @throws FitsException if the table could not be accessed
2294      * 
2295      * @see                  #setColumn(int, Object)
2296      * @see                  #getElement(int, int)
2297      * @see                  #getNCols()
2298      */
2299     @Override
2300     public synchronized Object getColumn(int col) throws FitsException {
2301         ColumnDesc c = columns.get(col);
2302 
2303         if (!c.isVariableSize() && c.fitsDimension() == 0 && !c.isComplex()) {
2304             return getFlattenedColumn(col);
2305         }
2306 
2307         ensureData();
2308 
2309         Object[] data = null;
2310 
2311         for (int i = 0; i < nRow; i++) {
2312             Object e = getElement(i, col);
2313             if (data == null) {
2314                 data = (Object[]) Array.newInstance(e.getClass(), nRow);
2315             }
2316             data[i] = e;
2317         }
2318 
2319         return data;
2320     }
2321 
2322     /**
2323      * Returns the Java index of the first column by the specified name.
2324      * 
2325      * @param  name the name of the column (case sensitive).
2326      * 
2327      * @return      The column index, or else -1 if this table does not contain a column by the specified name.
2328      * 
2329      * @see         #getDescriptor(String)
2330      * @see         ColumnDesc#name(String)
2331      * 
2332      * @since       1.20
2333      */
2334     public int indexOf(String name) {
2335         for (int col = 0; col < columns.size(); col++) {
2336             if (name.equals(getDescriptor(col).name())) {
2337                 return col;
2338             }
2339         }
2340         return -1;
2341     }
2342 
2343     @Override
2344     protected synchronized ColumnTable<?> getCurrentData() {
2345         return table;
2346     }
2347 
2348     @Override
2349     public ColumnTable<?> getData() throws FitsException {
2350         return (ColumnTable<?>) super.getData();
2351     }
2352 
2353     /**
2354      * Returns the dimensions of elements in each column.
2355      * 
2356      * @return     an array of arrays with the dimensions of each column's data.
2357      * 
2358      * @see        ColumnDesc#getDimens()
2359      * 
2360      * @deprecated (<i>for internal use</i>) Use {@link ColumnDesc#getEntryShape()} to access the shape of Java elements
2361      *                 individually for columns instead. Not useful to users since it returns the dimensions of the
2362      *                 primitive storage types, which is not always the dimension of elements on the Java side (notably
2363      *                 for string entries).
2364      */
2365     @Deprecated
2366     public int[][] getDimens() {
2367         int[][] dimens = new int[columns.size()][];
2368         for (int i = 0; i < dimens.length; i++) {
2369             dimens[i] = columns.get(i).getDimens();
2370         }
2371         return dimens;
2372     }
2373 
2374     /**
2375      * @deprecated               (<i>for internal use</i>) It may be private in the future.
2376      * 
2377      * @return                   An array with flattened data, in which each column's data is represented by a 1D array
2378      * 
2379      * @throws     FitsException if the reading of the data failed.
2380      */
2381     @Deprecated
2382     public Object[] getFlatColumns() throws FitsException {
2383         ensureData();
2384         return table.getColumns();
2385     }
2386 
2387     /**
2388      * @deprecated               (<i>for internal use</i>) It may be reduced to private visibility in the future.
2389      * 
2390      * @return                   column in flattened format. This is sometimes useful for fixed-sized columns.
2391      *                               Variable-sized columns will still return an <code>Object[]</code> array in which
2392      *                               each entry is the variable-length data for a row.
2393      *
2394      * @param      col           the column to flatten
2395      *
2396      * @throws     FitsException if the column could not be flattened
2397      */
2398     @Deprecated
2399     public synchronized Object getFlattenedColumn(int col) throws FitsException {
2400         if (!validColumn(col)) {
2401             throw new TableException("Invalid column index " + col + " in table of " + getNCols() + " columns");
2402         }
2403 
2404         ColumnDesc c = columns.get(col);
2405         if (c.isVariableSize()) {
2406             throw new TableException("Cannot flatten variable-sized column data");
2407         }
2408 
2409         ensureData();
2410 
2411         if (c.isBits()) {
2412             boolean[] bits = new boolean[nRow * c.fitsCount];
2413             for (int i = 0; i < nRow; i++) {
2414                 boolean[] seg = (boolean[]) fitsToJava1D(c, table.getElement(i, col), c.fitsCount, false);
2415                 System.arraycopy(seg, 0, bits, i * c.fitsCount, c.fitsCount);
2416             }
2417             return bits;
2418         }
2419 
2420         return fitsToJava1D(c, table.getColumn(col), 0, false);
2421     }
2422 
2423     /**
2424      * <p>
2425      * Reserves space for future addition of rows at the end of the regular table. In effect, this pushes the heap to
2426      * start at an offset value, leaving a gap between the main table and the heap in the FITS file. If your table
2427      * contains variable-length data columns, you may also want to reserve extra heap space for these via
2428      * {@link #reserveHeapSpace(int)}.
2429      * </p>
2430      * <p>
2431      * Note, that (C)FITSIO, as of version 4.4.0, has no proper support for offset heaps, and so you may want to be
2432      * careful using this function as the resulting FITS files, while standard, may not be readable by other tools due
2433      * to their own lack of support. Note, however, that you may also use this function to undo an offset heap with an
2434      * argument &lt;=0;
2435      * </p>
2436      * 
2437      * @param rows The number of future rows fow which space should be reserved (relative to the current table size) for
2438      *                 future additions, or &lt;=0 to ensure that the heap always follows immediately after the main
2439      *                 table, e.g. for better (C)FITSIO interoperability.
2440      * 
2441      * @see        #reserveHeapSpace(int)
2442      * 
2443      * @since      1.19.1
2444      */
2445     public synchronized void reserveRowSpace(int rows) {
2446         heapAddress = rows > 0 ? getRegularTableSize() + (long) rows * getRowBytes() : 0;
2447     }
2448 
2449     /**
2450      * Reserves space in the file at the end of the heap for future heap growth (e.g. different/longer or new VLA
2451      * entries). You may generally want to call this along with {@link #reserveRowSpace(int)} if yuor table contains
2452      * variable-length columns, to ensure storage for future data in these. You may call with &lt;=0 to discards any
2453      * previously reserved space.
2454      * 
2455      * @param bytes The number of bytes of unused space to reserve at the end of the heap, e.g. for future modifications
2456      *                  or additions, when writing the data to file.
2457      * 
2458      * @see         #reserveRowSpace(int)
2459      * 
2460      * @since       1.19.1
2461      */
2462     public synchronized void reserveHeapSpace(int bytes) {
2463         heapReserve = Math.max(0, bytes);
2464     }
2465 
2466     /**
2467      * Returns the address of the heap from the star of the HDU in the file.
2468      * 
2469      * @return (bytes) the start of the heap area from the beginning of the HDU.
2470      */
2471     final synchronized long getHeapAddress() {
2472         long tableSize = getRegularTableSize();
2473         return heapAddress > tableSize ? heapAddress : tableSize;
2474     }
2475 
2476     /**
2477      * Returns the offset from the end of the main table
2478      * 
2479      * @return the offset to the heap
2480      */
2481     final long getHeapOffset() {
2482         return getHeapAddress() - getRegularTableSize();
2483     }
2484 
2485     /**
2486      * It returns the heap size for storing in the FITS, which is the larger of the actual space occupied by the current
2487      * heap, or the original heap size based on the header when the HDU was read from an input. In the former case it
2488      * will also include heap space reserved for future additions.
2489      * 
2490      * @return (byte) the size of the heap in the FITS file.
2491      * 
2492      * @see    #compact()
2493      * @see    #reserveHeapSpace(int)
2494      */
2495     private synchronized int getHeapSize() {
2496         if (heap != null && heap.size() + heapReserve > heapFileSize) {
2497             return heap.size() + heapReserve;
2498         }
2499         return heapFileSize;
2500     }
2501 
2502     /**
2503      * @return the size of the heap -- including the offset from the end of the table data, and reserved space after.
2504      */
2505     synchronized long getParameterSize() {
2506         return getHeapOffset() + getHeapSize();
2507     }
2508 
2509     /**
2510      * Returns an empty row for the table. Such model rows are useful when low-level reading binary tables from an input
2511      * row-by-row. You can simply all {@link nom.tam.util.ArrayDataInput#readArrayFully(Object)} to populate it with
2512      * data from a stream. You may also use model rows to add additional rows to an existing table.
2513      * 
2514      * @return     a row that may be used for direct i/o to the table.
2515      * 
2516      * @deprecated (<i>for internal use</i>) Use {@link #getElement(int, int)} instead for low-level reading of tables
2517      *                 in deferred mode. Not recommended for uses because it requires a deep understanding of how data
2518      *                 (especially varialbe length columns) are represented in the FITS. Will reduce visibility to
2519      *                 private in the future.
2520      */
2521     @Deprecated
2522     public Object[] getModelRow() {
2523         Object[] modelRow = new Object[columns.size()];
2524         for (int i = 0; i < modelRow.length; i++) {
2525             ColumnDesc c = columns.get(i);
2526             if (c.fitsDimension() < 2) {
2527                 modelRow[i] = Array.newInstance(c.getTableBase(), c.getTableBaseCount());
2528             } else {
2529                 modelRow[i] = Array.newInstance(c.getTableBase(), c.fitsShape);
2530             }
2531         }
2532         return modelRow;
2533     }
2534 
2535     @Override
2536     public int getNCols() {
2537         return columns.size();
2538     }
2539 
2540     @Override
2541     public synchronized int getNRows() {
2542         return nRow;
2543     }
2544 
2545     /**
2546      * Reads a regular table element in the main table from the input. This method should never be called unless we have
2547      * a random-accessible input associated, which is a requirement for deferred read mode.
2548      * 
2549      * @param  o             The array element to populate
2550      * @param  c             the column descriptor
2551      * @param  row           the zero-based row index of the element
2552      * 
2553      * @throws IOException   If there was an I/O error accessing the input
2554      * @throws FitsException If there was some other error
2555      */
2556     private synchronized void readTableElement(Object o, ColumnDesc c, int row) throws IOException, FitsException {
2557         @SuppressWarnings("resource")
2558         RandomAccess in = getRandomAccessInput();
2559 
2560         synchronized (this) {
2561             in.position(getFileOffset() + row * (long) rowLen + c.offset);
2562         }
2563 
2564         if (c.isLogical()) {
2565             in.readArrayFully(o);
2566         } else {
2567             in.readImage(o);
2568         }
2569     }
2570 
2571     /**
2572      * Returns an unprocessed element from the table as a 1D array of the elements that are stored in the regular table
2573      * data, whithout reslving heap references. That is this call will return flattened versions of multidimensional
2574      * arrays, and will return only the heap locator (offset and size) for variable-sized columns.
2575      * 
2576      * @return                   a particular element from the table but do no processing of this element (e.g.,
2577      *                               dimension conversion or extraction of variable length array elements/)
2578      *
2579      * @param      row           The row of the element.
2580      * @param      col           The column of the element.
2581      * 
2582      * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future.
2583      *
2584      * @throws     FitsException if the operation failed
2585      */
2586     @Deprecated
2587     public synchronized Object getRawElement(int row, int col) throws FitsException {
2588         if (!validRow(row) || !validColumn(col)) {
2589             throw new TableException("No such element (" + row + "," + col + ")");
2590         }
2591 
2592         if (table == null) {
2593             try {
2594                 ColumnDesc c = columns.get(col);
2595                 Object e = c.newInstance(1);
2596                 readTableElement(e, c, row);
2597                 return e;
2598             } catch (IOException e) {
2599                 throw new FitsException("Error reading from input: " + e.getMessage(), e);
2600             }
2601         }
2602 
2603         ensureData();
2604         return table.getElement(row, col);
2605     }
2606 
2607     /**
2608      * Returns a table element as a Java array. Consider using the more Java-friendly {@link #get(int, int)} or one of
2609      * the scalar access methods with implicit type conversion support.
2610      * 
2611      * @see #get(int, int)
2612      * @see #getLogical(int, int)
2613      * @see #getNumber(int, int)
2614      * @see #getLong(int, int)
2615      * @see #getDouble(int, int)
2616      * @see #getString(int, int)
2617      */
2618     @Override
2619     public Object getElement(int row, int col) throws FitsException {
2620         return getElement(row, col, false);
2621     }
2622 
2623     /**
2624      * Returns a a table entry, with control over how FITS logical values are to be handled.
2625      * 
2626      * @param  row           zero-based row index
2627      * @param  col           zero-based column index
2628      * @param  isEnhanced    Whether logicals should be returned as {@link Boolean} (rather than <code>boolean</code>)
2629      *                           and complex values as {@link ComplexValue} (rather than <code>float[2]</code> or
2630      *                           <code>double[2]</code>), or arrays thereof. Methods prior to 1.18 should set this to
2631      *                           <code>false</code> for back compatible behavior.
2632      * 
2633      * @return               The entry as a primitive array, or {@link String}, {@link Boolean} or {@link ComplexValue},
2634      *                           or arrays thereof.
2635      * 
2636      * @throws FitsException If the requested element could not be accessed.
2637      */
2638     private Object getElement(int row, int col, boolean isEnhanced) throws FitsException {
2639         if (!validRow(row) || !validColumn(col)) {
2640             throw new TableException("No such element (" + row + "," + col + ")");
2641         }
2642 
2643         ColumnDesc c = columns.get(col);
2644         Object o = getRawElement(row, col);
2645 
2646         if (c.isVariableSize()) {
2647             return getFromHeap(c, o, isEnhanced);
2648         }
2649 
2650         o = fitsToJava1D(c, o, c.isBits() ? c.fitsCount : 0, isEnhanced);
2651 
2652         if (c.legacyShape.length > 1) {
2653             return ArrayFuncs.curl(o, c.legacyShape);
2654         }
2655 
2656         return o;
2657     }
2658 
2659     /**
2660      * Returns a table element as an array of the FITS storage type. Similar to the original
2661      * {@link #getElement(int, int)}, except that FITS logicals are returned as arrays of <code>Boolean</code> (rather
2662      * than <code>boolean</code>), bits are returned as arrays of <code>boolean</code>, and complex values are returned
2663      * as arrays of {@link ComplexValue} rather than arrays of <code>double[2]</code> or <code>float[2]</code>.
2664      * Singleton (scalar) table elements are not boxed to an enclosing Java type (unlike {@link #get(int, int)}), an
2665      * instead returned as arrays of just one element. For example, a single logical as a <code>Boolean[1]</code>, a
2666      * single float as a <code>float[1]</code> or a single double-precision complex value as
2667      * <code>ComplexValue[1]</code>.
2668      * 
2669      * @param  row zero-based row index
2670      * @param  col zero-based column index
2671      * 
2672      * @return     The table entry as an array of the stored Java type, without applying any type or quantization
2673      *                 conversions.
2674      * 
2675      * @see        #getArrayElementAs(int, int, Class)
2676      * @see        #get(int, int)
2677      * 
2678      * @since      1.20
2679      */
2680     public Object getArrayElement(int row, int col) {
2681         return getElement(row, col, true);
2682     }
2683 
2684     /**
2685      * <p>
2686      * Returns a numerical table element as an array of a specific underlying other numerical type. Similar
2687      * {@link #getArrayElement(int, int)} except that table entries are converted to the specified array type before
2688      * returning. If an integer-decimal conversion is involved, it will be performed through the column's quantizer (if
2689      * any) or else via a simple rounding as necessary.
2690      * </p>
2691      * <p>
2692      * For example, if you have an <code>short</code>-type column, and you want is an array of <code>double</code>
2693      * values that are represented by the 16-bit integers, then the conversion will use the column's quantizer scaling
2694      * and offset before returning the result either as an array of doubles, and the designated <code>short</code>
2695      * blanking values will be converted to NaNs.
2696      * </p>
2697      * 
2698      * @param  row                      zero-based row index
2699      * @param  col                      zero-based column index
2700      * @param  asType                   The desired underlying type, a primitive class or a {@link ComplexValue} type
2701      *                                      for appropriate numerical arrays (with a trailing Java dimension of 2 for
2702      *                                      the real/imaginary pairs).
2703      * 
2704      * @return                          An array of the desired type (e.g. <code>double[][]</code> if
2705      *                                      <code>asType</code> is <code>double.class</code> and the column contains 2D
2706      *                                      arrays of some numerical type).
2707      * 
2708      * @throws IllegalArgumentException if the numerical conversion is not possible for the given column type or if the
2709      *                                      type argument is not a supported numerical primitive or {@link ComplexValue}
2710      *                                      type.
2711      * 
2712      * @see                             #getArrayElement(int, int)
2713      * 
2714      * @since                           1.20
2715      */
2716     public Object getArrayElementAs(int row, int col, Class<?> asType) throws IllegalArgumentException {
2717         ColumnDesc c = getDescriptor(col);
2718         Object e = getElement(row, col, true);
2719         return asType.isAssignableFrom(c.getFitsBase()) ? e : ArrayFuncs.convertArray(e, asType, c.getQuantizer());
2720     }
2721 
2722     /**
2723      * <p>
2724      * Returns a table element using the usual Java boxing for primitive scalar (singleton) entries, or packaging
2725      * complex values as {@link ComplexValue}, or as appropriate primitive or object arrays. FITS string columns return
2726      * {@link String} values. Logical (<code>boolean</code> columns will return a {@link Boolean}, which may be
2727      * <code>null</code> if undefined (as per the FITS standard). Multibit FITS bits colums return arrays of
2728      * <code>boolean</code>.
2729      * </p>
2730      * <p>
2731      * As opposed to {@link #getElement(int, int)} scalar (singleton) values are not wrapped into primitive arrays, but
2732      * return either a singular object, such as a ({@link String}, or a {@link ComplexValue}, or a boxed Java primitive.
2733      * Thus, columns containing single <code>short</code> entries will return the selected element as a {@link Short},
2734      * or columns containing single <code>double</code> values will return the element as a {@link Double} and so on.
2735      * </p>
2736      * <p>
2737      * Array columns will return the expected arrays of primitive values, or arrays of one of the mentioned types. Note
2738      * however, that logical arrays are returned as arrays of {@link Boolean}, e.g. <code>Boolean[][]</code>, <b>not</b>
2739      * <code>boolean[][]</code>. This is because FITS allows <code>null</code> values for logicals beyond <code>
2740      * true</code> and <code>false</code>, which is reproduced by the boxed type, but not by the primitive type. FITS
2741      * columns of bits (generally preferrably to logicals if support for <code>null</code> values is not required) will
2742      * return arrays of <code>boolean</code>.
2743      * </p>
2744      * <p>
2745      * Columns containing multidimensional arrays, will return the expected multidimensional array of the above
2746      * mentioned types for the FITS storage type. You can then convert numerical arrays to other types as required for
2747      * your application via {@link ArrayFuncs#convertArray(Object, Class, Quantizer)}, including any appropriate
2748      * quantization for the colummn (see {@link ColumnDesc#getQuantizer()}).
2749      * </p>
2750      * 
2751      * @param  row           the zero-based row index
2752      * @param  col           the zero-based column index
2753      * 
2754      * @return               the element, either as a Java boxed type (for scalar entries), a singular Java Object, or
2755      *                           as a (possibly multi-dimensional) array of {@link String}, {@link Boolean},
2756      *                           {@link ComplexValue}, or primitives.
2757      * 
2758      * @throws FitsException if the element could not be obtained
2759      * 
2760      * @see                  #getNumber(int, int)
2761      * @see                  #getLogical(int, int)
2762      * @see                  #getString(int, int)
2763      * @see                  #getArrayElementAs(int, int, Class)
2764      * @see                  #set(int, int, Object)
2765      * 
2766      * @since                1.18
2767      */
2768     public Object get(int row, int col) throws FitsException {
2769         ColumnDesc c = columns.get(col);
2770         Object e = getElement(row, col, true);
2771         return (c.isSingleton() && e.getClass().isArray()) ? Array.get(e, 0) : e;
2772     }
2773 
2774     /**
2775      * Returns the numerical value, if possible, for scalar elements. Scalar numerical columns return the boxed type of
2776      * their primitive type. Thus, a column of <code>long</code> values will return {@link Long}, whereas a column of
2777      * <code>float</code> values will return a {@link Float}. Logical columns will return 1 if <code>true</code> or 0 if
2778      * <code>false</code>, or <code>null</code> if undefined. Array columns and other column types will throw an
2779      * exception.
2780      * 
2781      * @param  row                   the zero-based row index
2782      * @param  col                   the zero-based column index
2783      * 
2784      * @return                       the number value of the specified scalar entry
2785      * 
2786      * @throws FitsException         if the element could not be obtained
2787      * @throws ClassCastException    if the specified column in not a numerical scalar type.
2788      * @throws NumberFormatException if the it's a string column but the entry does not seem to be a number
2789      * 
2790      * @see                          #getDouble(int, int)
2791      * @see                          #getLong(int, int)
2792      * @see                          #get(int, int)
2793      * 
2794      * @since                        1.18
2795      */
2796     public final Number getNumber(int row, int col) throws FitsException, ClassCastException, NumberFormatException {
2797         Object o = get(row, col);
2798         if (o instanceof String) {
2799             try {
2800                 return Long.parseLong((String) o);
2801             } catch (NumberFormatException e) {
2802                 return Double.parseDouble((String) o);
2803             }
2804         }
2805         if (o instanceof Boolean) {
2806             return ((Boolean) o) ? 1 : 0;
2807         }
2808         return (Number) o;
2809     }
2810 
2811     /**
2812      * <p>
2813      * Returns the decimal value, if possible, of a scalar table entry. See {@link #getNumber(int, int)} for more
2814      * information on the conversion process.
2815      * </p>
2816      * <p>
2817      * Since version 1.20, if the column has a quantizer and stores integer elements, the conversion to double-precision
2818      * will account for the quantization of the column, if any, and will return NaN if the stored integer is the
2819      * designated blanking value (if any). To bypass quantization, you can use {@link #getNumber(int, int)} instead
2820      * followed by {@link Number#doubleValue()} to to get the stored integer values as a double.
2821      * </p>
2822      * 
2823      * @param  row                the zero-based row index
2824      * @param  col                the zero-based column index
2825      * 
2826      * @return                    the number value of the specified scalar entry
2827      * 
2828      * @throws FitsException      if the element could not be obtained
2829      * @throws ClassCastException if the specified column in not a numerical scalar type.
2830      * 
2831      * @see                       #getNumber(int, int)
2832      * @see                       #getLong(int, int)
2833      * @see                       #get(int, int)
2834      * @see                       ColumnDesc#getQuantizer()
2835      * 
2836      * @since                     1.18
2837      */
2838     public final double getDouble(int row, int col) throws FitsException, ClassCastException {
2839         Number n = getNumber(row, col);
2840 
2841         if (!(n instanceof Float || n instanceof Double)) {
2842             Quantizer q = getDescriptor(col).getQuantizer();
2843             if (q != null) {
2844                 return q.toDouble(n.longValue());
2845             }
2846         }
2847 
2848         return n == null ? Double.NaN : n.doubleValue();
2849     }
2850 
2851     /**
2852      * <p>
2853      * Returns a 64-bit integer value, if possible, of a scalar table entry. Boolean columns will return 1 if
2854      * <code>true</code> or 0 if <code>false</code>, or throw a {@link NullPointerException} if undefined. See
2855      * {@link #getNumber(int, int)} for more information on the conversion process of the stored data element.
2856      * </p>
2857      * <p>
2858      * Additionally, since version 1.20, if the column has a quantizer and stores floating-point elements, the
2859      * conversion to integer will include the quantization, and NaN values will be converted to the designated integer
2860      * blanking values. To bypass quantization, you can use {@link #getNumber(int, int)} instead followed by
2861      * {@link Number#longValue()} to to get the stored floating point values rounded directly to a long.
2862      * </p>
2863      * 
2864      * @param  row                   the zero-based row index
2865      * @param  col                   the zero-based column index
2866      * 
2867      * @return                       the 64-bit integer number value of the specified scalar table entry.
2868      * 
2869      * @throws FitsException         if the element could not be obtained
2870      * @throws ClassCastException    if the specified column in not a numerical scalar type.
2871      * @throws IllegalStateException if the column contains a undefined (blanking value), such as a {@link Double#NaN}
2872      *                                   when no quantizer is set for the column, or a {@link Boolean} <code>null</code>
2873      *                                   value.
2874      * 
2875      * @see                          #getNumber(int, int)
2876      * @see                          #getDouble(int, int)
2877      * @see                          #get(int, int)
2878      * 
2879      * @since                        1.18
2880      */
2881     public final long getLong(int row, int col) throws FitsException, ClassCastException, IllegalStateException {
2882         Number n = getNumber(row, col);
2883 
2884         if (n instanceof Float || n instanceof Double) {
2885             Quantizer q = getDescriptor(col).getQuantizer();
2886             if (q != null) {
2887                 return q.toLong(n.doubleValue());
2888             }
2889         }
2890 
2891         if (Double.isNaN(n.doubleValue())) {
2892             throw new IllegalStateException("Cannot convert NaN to long without Quantizer");
2893         }
2894         return n.longValue();
2895     }
2896 
2897     /**
2898      * Returns the boolean value, if possible, for scalar elements. It will will return<code>true</code>, or
2899      * <code>false</code>, or <code>null</code> if undefined. Numerical columns will return <code>null</code> if the
2900      * corresponding decimal value is NaN, or <code>false</code> if the value is 0, or else <code>true</code> for all
2901      * non-zero values (just like in C).
2902      * 
2903      * @param  row                the zero-based row index
2904      * @param  col                the zero-based column index
2905      * 
2906      * @return                    the boolean value of the specified scalar entry, or <code>null</code> if undefined.
2907      * 
2908      * @throws ClassCastException if the specified column in not a scalar boolean type.
2909      * @throws FitsException      if the element could not be obtained
2910      * 
2911      * @see                       #get(int, int)
2912      * 
2913      * @since                     1.18
2914      */
2915     @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null has specific meaning here")
2916     public final Boolean getLogical(int row, int col) throws FitsException, ClassCastException {
2917         Object o = get(row, col);
2918         if (o == null) {
2919             return null;
2920         }
2921 
2922         if (o instanceof Number) {
2923             Number n = (Number) o;
2924             if (Double.isNaN(n.doubleValue())) {
2925                 return null;
2926             }
2927             return n.longValue() != 0;
2928         }
2929 
2930         if (o instanceof Character) {
2931             char c = (Character) o;
2932             if (c == 'T' || c == 't' || c == '1') {
2933                 return true;
2934             }
2935             if (c == 'F' || c == 'f' || c == '0') {
2936                 return false;
2937             }
2938             return null;
2939         }
2940 
2941         if (o instanceof String) {
2942             return FitsUtil.parseLogical((String) o);
2943         }
2944 
2945         return (Boolean) o;
2946     }
2947 
2948     /**
2949      * Returns the string value, if possible, for scalar elements. All scalar columns will return the string
2950      * representation of their values, while <code>byte[]</code> and <code>char[]</code> are converted to appropriate
2951      * strings.
2952      * 
2953      * @param  row                the zero-based row index
2954      * @param  col                the zero-based column index
2955      * 
2956      * @return                    the string representatiof the specified table entry
2957      * 
2958      * @throws ClassCastException if the specified column contains array elements other than <code>byte[]</code> or
2959      *                                <code>char[]</code>
2960      * @throws FitsException      if the element could not be obtained
2961      * 
2962      * @see                       #get(int, int)
2963      * 
2964      * @since                     1.18
2965      */
2966     public final String getString(int row, int col) throws FitsException, ClassCastException {
2967         ColumnDesc c = columns.get(col);
2968         Object value = get(row, col);
2969 
2970         if (value == null) {
2971             return "null";
2972         }
2973 
2974         if (!value.getClass().isArray()) {
2975             return value.toString();
2976         }
2977 
2978         if (c.fitsDimension() > 1) {
2979             throw new ClassCastException("Cannot convert multi-dimensional array element to String");
2980         }
2981 
2982         if (value instanceof char[]) {
2983             return String.valueOf((char[]) value).trim();
2984         }
2985         if (value instanceof byte[]) {
2986             return AsciiFuncs.asciiString((byte[]) value).trim();
2987         }
2988 
2989         throw new ClassCastException("Cannot convert " + value.getClass().getName() + " to String.");
2990     }
2991 
2992     @Override
2993     public Object[] getRow(int row) throws FitsException {
2994         if (!validRow(row)) {
2995             throw new TableException("Invalid row index " + row + " in table of " + getNRows() + " rows");
2996         }
2997 
2998         Object[] data = new Object[columns.size()];
2999         for (int col = 0; col < data.length; col++) {
3000             data[col] = getElement(row, col);
3001         }
3002         return data;
3003     }
3004 
3005     /**
3006      * Returns the flattened (1D) size of elements in each column of this table. As of 1.18, this method returns a copy
3007      * ot the array used internally, which is safe to modify.
3008      * 
3009      * @return     an array with the byte sizes of each column
3010      * 
3011      * @deprecated (<i>for internal use</i>) Use {@link ColumnDesc#getElementCount()} instead. This one returns the
3012      *                 number of elements in the FITS representation, not in the java representation. For example, for
3013      *                 {@link String} entries, this returns the number of bytes stored, not the number of strings.
3014      *                 Similarly, for complex values it returns the number of components not the number of values.
3015      */
3016     @Deprecated
3017     public int[] getSizes() {
3018         int[] sizes = new int[columns.size()];
3019         for (int i = 0; i < sizes.length; i++) {
3020             sizes[i] = columns.get(i).getTableBaseCount();
3021         }
3022         return sizes;
3023     }
3024 
3025     /**
3026      * Returns the size of the regular table data, before the heap area.
3027      * 
3028      * @return the size of the regular table in bytes
3029      */
3030     private synchronized long getRegularTableSize() {
3031         return (long) nRow * rowLen;
3032     }
3033 
3034     @Override
3035     protected long getTrueSize() {
3036         return getRegularTableSize() + getParameterSize();
3037     }
3038 
3039     /**
3040      * Get the characters describing the base classes of the columns. As of 1.18, this method returns a copy ot the
3041      * array used internally, which is safe to modify.
3042      *
3043      * @return     An array of type characters (Java array types), one for each column.
3044      * 
3045      * @deprecated (<i>for internal use</i>) Use {@link ColumnDesc#getElementClass()} instead. Not very useful to users
3046      *                 since this returns the FITS primitive storage type for the data column.
3047      */
3048     @Deprecated
3049     public char[] getTypes() {
3050         char[] types = new char[columns.size()];
3051         for (int i = 0; i < columns.size(); i++) {
3052             types[i] = ElementType.forClass(columns.get(i).getTableBase()).type();
3053         }
3054         return types;
3055     }
3056 
3057     @Override
3058     public synchronized void setColumn(int col, Object o) throws FitsException {
3059         ColumnDesc c = columns.get(col);
3060 
3061         if (c.isVariableSize()) {
3062             Object[] array = (Object[]) o;
3063             for (int i = 0; i < nRow; i++) {
3064                 Object p = putOnHeap(c, ArrayFuncs.flatten(array[i]), getRawElement(i, col));
3065                 setTableElement(i, col, p);
3066             }
3067         } else {
3068             setFlattenedColumn(col, o);
3069         }
3070     }
3071 
3072     /**
3073      * Writes an element directly into the random accessible FITS file. Note, this call will not modify the table in
3074      * memory (if loaded). This method should never be called unless we have a valid encoder object that can handle the
3075      * writing, which is a requirement for deferred read mode.
3076      * 
3077      * @param  row         the zero-based row index
3078      * @param  col         the zero-based column index
3079      * @param  array       an array object containing primitive types, in FITS storage format. It may be
3080      *                         multi-dimensional.
3081      * 
3082      * @throws IOException the there was an error writing to the FITS output
3083      * 
3084      * @see                #setTableElement(int, int, Object)
3085      */
3086     @SuppressWarnings("resource")
3087     private void writeTableElement(int row, int col, Object array) throws IOException {
3088         synchronized (this) {
3089             ColumnDesc c = columns.get(col);
3090             getRandomAccessInput().position(getFileOffset() + row * (long) rowLen + c.offset);
3091         }
3092         encoder.writeArray(array);
3093     }
3094 
3095     /**
3096      * Sets a table element to an array in the FITS storage format. If the data is in deferred mode it will write the
3097      * table entry directly into the file. Otherwise it will update the table entry in memory. For variable sized
3098      * column, the heap will always be updated in memory, so you may want to call {@link #rewrite()} when done updating
3099      * all entries.
3100      * 
3101      * @param  row           the zero-based row index
3102      * @param  col           the zero-based column index
3103      * @param  o             an array object containing primitive types, in FITS storage format. It may be
3104      *                           multi-dimensional.
3105      *
3106      * @throws FitsException if the array is invalid for the given column, or if the table could not be accessed in the
3107      *                           file / input.
3108      * 
3109      * @see                  #setTableElement(int, int, Object)
3110      * @see                  #getRawElement(int, int)
3111      */
3112     private synchronized void setTableElement(int row, int col, Object o) throws FitsException {
3113         if (table == null) {
3114             try {
3115                 writeTableElement(row, col, o);
3116             } catch (IOException e) {
3117                 throw new FitsException(e.getMessage(), e);
3118             }
3119         } else {
3120             ensureData();
3121             table.setElement(row, col, o);
3122         }
3123     }
3124 
3125     /**
3126      * Consider using the more Java-friendly {@link #set(int, int, Object)} with implicit scalar type conversions.
3127      * 
3128      * @see #set(int, int, Object)
3129      */
3130     @Override
3131     public void setElement(int row, int col, Object o) throws FitsException {
3132         ColumnDesc c = columns.get(col);
3133         o = c.isVariableSize() ? putOnHeap(c, o, getRawElement(row, col)) : javaToFits1D(c, ArrayFuncs.flatten(o));
3134         setTableElement(row, col, o);
3135     }
3136 
3137     /**
3138      * <p>
3139      * The Swiss-army knife of setting table entries, including Java boxing, and with some support for automatic type
3140      * conversions. The argument may be one of the following type:
3141      * </p>
3142      * <ul>
3143      * <li>Scalar values -- any Java primitive with its boxed type, such as a {@link Double}, or a
3144      * {@link Character}.</li>
3145      * <li>A single {@link String} or {@link ComplexValue} object.
3146      * <li>An array (including multidimensional) of primitive types, or that of {@link Boolean}, {@link ComplexValue},
3147      * or {@link String}.</li>
3148      * </ul>
3149      * <p>
3150      * For array-type columns the argument needs to match the column type exactly. However, you may call
3151      * {@link ArrayFuncs#convertArray(Object, Class, Quantizer)} prior to setting values to convert arrays to the
3152      * desired numerical types, including the quantization that is appropriate for the column (see
3153      * {@link ColumnDesc#getQuantizer()}).
3154      * </p>
3155      * <p>
3156      * For scalar (single element) columns, automatic type conversions may apply, to make setting scalar columns more
3157      * flexible:
3158      * </p>
3159      * <ul>
3160      * <li>Any numerical column can take any {@link Number} value. The conversion is as if an explicit Java cast were
3161      * applied. For example, if setting a <code>double</code> value for a column of single <code>short</code> values it
3162      * as if a <code>(short)</code> cast were applied to the value.</li>
3163      * <li>Numerical colums can also take {@link Boolean} values which set the entry to 1, or 0, or to
3164      * {@link Double#isNaN()} (or the equivalent integer minimum value) if the argument is <code>null</code>. Numerical
3165      * columns can also set {@link String} values, by parsing the string according to the numerical type of the
3166      * column.</li>
3167      * <li>Logical columns can set {@link Boolean} values, including <code>null</code>values, but also any
3168      * {@link Number} type. In case of numbers, zero values map to <code>false</code> while definite non-zero values map
3169      * to <code>true</code>. {@link Double#isNaN()} maps to a <code>null</code> (or undefined) entry. Loginal columns
3170      * can be also set to the {@link String} values of 'true' or 'false', or to a {@link Character} of 'T'/'F' (or
3171      * equivalently '1'/'0') and 0 (undefined)</li>
3172      * <li>Singular string columns can be set to any scalar type owing to Java's {@link #toString()} method performing
3173      * the conversion, as long as the string representation fits into the size constraints (if any) for the string
3174      * column.</li>
3175      * </ul>
3176      * <p>
3177      * Additionally, scalar columns can take single-element array arguments, just like
3178      * {@link #setElement(int, int, Object)}.
3179      * </p>
3180      * 
3181      * @param  row                      the zero-based row index
3182      * @param  col                      the zero-based column index
3183      * @param  o                        the new value to set. For array columns this must match the Java array type
3184      *                                      exactly, but for scalar columns additional flexibility is provided for fuzzy
3185      *                                      type matching (see description above).
3186      * 
3187      * @throws FitsException            if the column could not be set
3188      * @throws IllegalArgumentException if the argument cannot be converted to a value for the specified column type.
3189      * 
3190      * @since                           1.18
3191      * 
3192      * @see                             #get(int, int)
3193      */
3194     public void set(int row, int col, Object o) throws FitsException, IllegalArgumentException {
3195         ColumnDesc c = columns.get(col);
3196 
3197         if (o == null) {
3198             // Only logicals and strings support 'null' values
3199             if (!c.isSingleton()) {
3200                 throw new TableException("No null values allowed for column of " + c.getLegacyBase() + " arrays.");
3201             } else if (c.isString()) {
3202                 setElement(row, col, "");
3203             } else {
3204                 setLogical(row, col, null);
3205             }
3206         } else if (o.getClass().isArray()) {
3207             Class<?> eType = ArrayFuncs.getBaseClass(o);
3208             if (!c.getFitsBase().isAssignableFrom(eType) && c.isNumeric()) {
3209                 o = ArrayFuncs.convertArray(o, c.getFitsBase(), c.getQuantizer());
3210             }
3211             setElement(row, col, o);
3212         } else if (o instanceof String) {
3213             setString(row, col, (String) o);
3214         } else if (!c.isSingleton()) {
3215             throw new TableException("Cannot set scalar values in non-scalar columns");
3216         } else if (c.isString()) {
3217             setElement(row, col, o.toString());
3218         } else if (o instanceof Boolean) {
3219             setLogical(row, col, (Boolean) o);
3220         } else if (o instanceof Character) {
3221             setCharacter(row, col, (Character) o);
3222         } else if (o instanceof Number) {
3223             setNumber(row, col, (Number) o);
3224         } else if (o instanceof ComplexValue) {
3225             setElement(row, col, o);
3226         } else {
3227             throw new IllegalArgumentException("Unsupported scalar type: " + o.getClass());
3228         }
3229     }
3230 
3231     /**
3232      * Sets a scalar table entry to the specified numerical value.
3233      * 
3234      * @param  row                the zero-based row index
3235      * @param  col                the zero-based column index
3236      * @param  value              the new number value
3237      * 
3238      * @throws ClassCastException if the specified column in not a numerical scalar type.
3239      * @throws FitsException      if the table element could not be altered
3240      * 
3241      * @see                       #getNumber(int, int)
3242      * @see                       #set(int, int, Object)
3243      * 
3244      * @since                     1.18
3245      */
3246     private void setNumber(int row, int col, Number value) throws FitsException, ClassCastException {
3247         ColumnDesc c = columns.get(col);
3248 
3249         // Already checked before calling...
3250         // if (!c.isSingleton()) {
3251         // throw new ClassCastException("Cannot set scalar value for array column " + col);
3252         // }
3253 
3254         if (c.isLogical()) {
3255             Boolean b = null;
3256             if (!Double.isNaN(value.doubleValue())) {
3257                 b = value.longValue() != 0;
3258             }
3259             setTableElement(row, col, new byte[] {FitsEncoder.byteForBoolean(b)});
3260             return;
3261         }
3262 
3263         Class<?> base = c.getLegacyBase();
3264 
3265         // quantize / unquantize as necessary...
3266         Quantizer q = c.getQuantizer();
3267 
3268         if (q != null) {
3269             boolean decimalBase = (base == float.class || base == double.class);
3270             boolean decimalValue = (value instanceof Float || value instanceof Double || value instanceof BigInteger
3271                     || value instanceof BigDecimal);
3272 
3273             if (decimalValue && !decimalBase) {
3274                 value = q.toLong(value.doubleValue());
3275             } else if (!decimalValue && decimalBase) {
3276                 value = q.toDouble(value.longValue());
3277             }
3278         }
3279 
3280         Object wrapped = null;
3281 
3282         if (base == byte.class) {
3283             wrapped = new byte[] {value.byteValue()};
3284         } else if (base == short.class) {
3285             wrapped = new short[] {value.shortValue()};
3286         } else if (base == int.class) {
3287             wrapped = new int[] {value.intValue()};
3288         } else if (base == long.class) {
3289             wrapped = new long[] {value.longValue()};
3290         } else if (base == float.class) {
3291             wrapped = new float[] {value.floatValue()};
3292         } else if (base == double.class) {
3293             wrapped = new double[] {value.doubleValue()};
3294         } else {
3295             // This could be a char based column...
3296             throw new ClassCastException("Cannot set number value for column of type " + base);
3297         }
3298 
3299         setTableElement(row, col, wrapped);
3300     }
3301 
3302     /**
3303      * Sets a boolean scalar table entry to the specified value.
3304      * 
3305      * @param  row                the zero-based row index
3306      * @param  col                the zero-based column index
3307      * @param  value              the new boolean value
3308      * 
3309      * @throws ClassCastException if the specified column in not a boolean scalar type.
3310      * @throws FitsException      if the table element could not be altered
3311      * 
3312      * @see                       #getLogical(int, int)
3313      * @see                       #set(int, int, Object)
3314      * 
3315      * @since                     1.18
3316      */
3317     private void setLogical(int row, int col, Boolean value) throws FitsException, ClassCastException {
3318         ColumnDesc c = columns.get(col);
3319 
3320         // Already checked before calling...
3321         // if (!c.isSingleton()) {
3322         // throw new ClassCastException("Cannot set scalar value for array column " + col);
3323         // }
3324 
3325         if (c.isLogical()) {
3326             setTableElement(row, col, new byte[] {FitsEncoder.byteForBoolean(value)});
3327         } else if (c.getLegacyBase() == char.class) {
3328             setTableElement(row, col, new char[] {value == null ? '\0' : (value ? 'T' : 'F')});
3329         } else {
3330             setNumber(row, col, value == null ? Double.NaN : (value ? 1 : 0));
3331         }
3332     }
3333 
3334     /**
3335      * Sets a Unicode character scalar table entry to the specified value.
3336      * 
3337      * @param  row                the zero-based row index
3338      * @param  col                the zero-based column index
3339      * @param  value              the new Unicode character value
3340      * 
3341      * @throws ClassCastException if the specified column in not a boolean scalar type.
3342      * @throws FitsException      if the table element could not be altered
3343      * 
3344      * @see                       #getString(int, int)
3345      * 
3346      * @since                     1.18
3347      */
3348     private void setCharacter(int row, int col, Character value) throws FitsException, ClassCastException {
3349         ColumnDesc c = columns.get(col);
3350 
3351         // Already checked before calling...
3352         // if (!c.isSingleton()) {
3353         // throw new IllegalArgumentException("Cannot set scalar value for array column " + col);
3354         // }
3355 
3356         if (c.isLogical()) {
3357             setLogical(row, col, FitsUtil.parseLogical(value.toString()));
3358         } else if (c.fitsBase == char.class) {
3359             setTableElement(row, col, new char[] {value});
3360         } else if (c.fitsBase == byte.class) {
3361             setTableElement(row, col, new byte[] {(byte) (value & FitsIO.BYTE_MASK)});
3362         } else {
3363             throw new ClassCastException("Cannot convert char value to " + c.fitsBase.getName());
3364         }
3365     }
3366 
3367     /**
3368      * Sets a table entry to the specified string value. Scalar column will attempt to parse the value, while
3369      * <code>byte[]</code> and <coce>char[]</code> type columns will convert the string provided the string's length
3370      * does not exceed the entry size for these columns (the array elements will be padded with zeroes). Note, that
3371      * scalar <code>byte</code> columns will parse the string as a number (not as a single ASCII character).
3372      * 
3373      * @param  row                      the zero-based row index
3374      * @param  col                      the zero-based column index
3375      * @param  value                    the new boolean value
3376      * 
3377      * @throws ClassCastException       if the specified column is not a scalar type, and neither it is a
3378      *                                      <code>byte[]</code> or <coce>char[]</code> column.
3379      * @throws IllegalArgumentException if the String is too long to contain in the column.
3380      * @throws NumberFormatException    if the numerical value could not be parsed.
3381      * @throws FitsException            if the table element could not be altered
3382      * 
3383      * @see                             #getString(int, int)
3384      * @see                             #set(int, int, Object)
3385      * 
3386      * @since                           1.18
3387      */
3388     private void setString(int row, int col, String value)
3389             throws FitsException, ClassCastException, IllegalArgumentException, NumberFormatException {
3390         ColumnDesc c = columns.get(col);
3391 
3392         // Already checked before calling...
3393         // if (!c.isSingleton()) {
3394         // throw new IllegalArgumentException("Cannot set scalar value for array column " + col);
3395         // }
3396 
3397         if (c.isLogical()) {
3398             setLogical(row, col, FitsUtil.parseLogical(value));
3399         } else if (value.length() == 1) {
3400             setCharacter(row, col, value.charAt(0));
3401         } else if (c.fitsDimension() > 1) {
3402             throw new ClassCastException("Cannot convert String to multi-dimensional array");
3403         } else if (c.fitsDimension() == 1) {
3404             if (c.fitsBase != char.class && c.fitsBase != byte.class) {
3405                 throw new ClassCastException("Cannot cast String to " + c.fitsBase.getName());
3406             }
3407             int len = c.isVariableSize() ? value.length() : c.fitsCount;
3408             if (value.length() > len) {
3409                 throw new IllegalArgumentException("String size " + value.length() + " exceeds entry size of " + len);
3410             }
3411             if (c.fitsBase == char.class) {
3412                 setTableElement(row, col, Arrays.copyOf(value.toCharArray(), len));
3413             } else {
3414                 setTableElement(row, col, FitsUtil.stringToByteArray(value, len));
3415             }
3416         } else {
3417             try {
3418                 setNumber(row, col, Long.parseLong(value));
3419             } catch (NumberFormatException e) {
3420                 setNumber(row, col, Double.parseDouble(value));
3421             }
3422         }
3423     }
3424 
3425     /**
3426      * @deprecated               (<i>for internal use</i>) It may be reduced to private visibility in the future. Sets a
3427      *                               column with the data already flattened.
3428      *
3429      * @param      col           The index of the column to be replaced.
3430      * @param      data          The new data array. This should be a one-d primitive array.
3431      *
3432      * @throws     FitsException Thrown if the type of length of the replacement data differs from the original.
3433      */
3434     @Deprecated
3435     public synchronized void setFlattenedColumn(int col, Object data) throws FitsException {
3436         ensureData();
3437 
3438         Object oldCol = table.getColumn(col);
3439         if (data.getClass() != oldCol.getClass() || Array.getLength(data) != Array.getLength(oldCol)) {
3440             throw new TableException("Replacement column mismatch at column:" + col);
3441         }
3442         table.setColumn(col, javaToFits1D(columns.get(col), data));
3443     }
3444 
3445     @Override
3446     public void setRow(int row, Object[] data) throws FitsException {
3447         ensureData();
3448 
3449         if (data.length != getNCols()) {
3450             throw new TableException("Mismatched number of columns: " + data.length + ", expected " + getNCols());
3451         }
3452 
3453         for (int col = 0; col < data.length; col++) {
3454             set(row, col, data[col]);
3455         }
3456     }
3457 
3458     /**
3459      * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a
3460      *                 new HDU after column deletions, and then edit the new header as necessary to incorporate custom
3461      *                 entries. May be removed from the API in the future.
3462      */
3463     @Deprecated
3464     @Override
3465     public synchronized void updateAfterDelete(int oldNcol, Header hdr) throws FitsException {
3466         hdr.addValue(Standard.NAXIS1, rowLen);
3467         int l = 0;
3468         for (ColumnDesc d : columns) {
3469             d.offset = l;
3470             l += d.rowLen();
3471         }
3472     }
3473 
3474     @SuppressWarnings("resource")
3475     @Override
3476     public void write(ArrayDataOutput os) throws FitsException {
3477         synchronized (this) {
3478 
3479             try {
3480                 if (isDeferred() && os == getRandomAccessInput()) {
3481                     // It it's a deferred mode re-write, then data were edited in place if at all,
3482                     // so we can skip the main table.
3483                     ((RandomAccess) os).skipAllBytes(getRegularTableSize());
3484                 } else {
3485                     // otherwise make sure we loaded all data before writing to the output
3486                     ensureData();
3487 
3488                     // Write the regular table (if any)
3489                     if (getRegularTableSize() > 0) {
3490                         table.write(os);
3491                     }
3492                 }
3493 
3494                 // Now check if we need to write the heap
3495                 if (getParameterSize() > 0) {
3496                     for (long rem = getHeapOffset(); rem > 0;) {
3497                         byte[] b = new byte[(int) Math.min(getHeapOffset(), 1 << Short.SIZE)];
3498                         os.write(b);
3499                         rem -= b.length;
3500                     }
3501 
3502                     getHeap().write(os);
3503 
3504                     if (heapReserve > 0) {
3505                         byte[] b = new byte[heapReserve];
3506                         os.write(b);
3507                     }
3508                 }
3509 
3510                 FitsUtil.pad(os, getTrueSize(), (byte) 0);
3511             } catch (IOException e) {
3512                 throw new FitsException("Unable to write table:" + e, e);
3513             }
3514         }
3515     }
3516 
3517     /**
3518      * Returns the heap offset component from a pointer.
3519      * 
3520      * @param  p the pointer, either a <code>int[2]</code> or a <code>long[2]</code>.
3521      * 
3522      * @return   the offset component from the pointer
3523      */
3524     private long getPointerOffset(Object p) {
3525         return (p instanceof long[]) ? ((long[]) p)[1] : ((int[]) p)[1];
3526     }
3527 
3528     /**
3529      * Returns the number of elements reported in a heap pointer.
3530      * 
3531      * @param  p the pointer, either a <code>int[2]</code> or a <code>long[2]</code>.
3532      * 
3533      * @return   the element count component from the pointer
3534      */
3535     private long getPointerCount(Object p) {
3536         return (p instanceof long[]) ? ((long[]) p)[0] : ((int[]) p)[0];
3537     }
3538 
3539     /**
3540      * Puts a FITS data array onto our heap, returning its locator pointer. The data will overwrite the previous heap
3541      * entry, if provided, so long as the new data fits in the same place. Otherwise the new data is placed at the end
3542      * of the heap.
3543      * 
3544      * @param  c             The column descriptor, specifying the data type
3545      * @param  o             The variable-length data
3546      * @param  p             The heap pointer, where this element was stored on the heap before, or <code>null</code> if
3547      *                           we aren't replacing an earlier entry.
3548      * 
3549      * @return               the heap pointer information, either <code>int[2]</code> or else a <code>long[2]</code>
3550      * 
3551      * @throws FitsException if the data could not be accessed in full from the heap.
3552      */
3553     private Object putOnHeap(ColumnDesc c, Object o, Object oldPointer) throws FitsException {
3554         return putOnHeap(getHeap(), c, o, oldPointer);
3555     }
3556 
3557     /**
3558      * Puts a FITS data array onto a specific heap, returning its locator pointer. The data will overwrite the previous
3559      * heap entry, if provided, so long as the new data fits in the same place. Otherwise the new data is placed at the
3560      * end of the heap.
3561      * 
3562      * @param  h             The heap object to use.
3563      * @param  c             The column descriptor, specifying the data type
3564      * @param  o             The variable-length data in Java form.
3565      * @param  p             The heap pointer, where this element was stored on the heap before, or <code>null</code> if
3566      *                           we aren't replacing an earlier entry.
3567      * 
3568      * @return               the heap pointer information, either <code>int[2]</code> or else a <code>long[2]</code>
3569      * 
3570      * @throws FitsException if the data could not be accessed in full from the heap.
3571      */
3572     private Object putOnHeap(FitsHeap h, ColumnDesc c, Object o, Object oldPointer) throws FitsException {
3573         // Flatten data for heap
3574         o = ArrayFuncs.flatten(o);
3575 
3576         // By default put data at the end of the heap;
3577         int off = h.size();
3578 
3579         // The number of Java elements is the same as the number of FITS elements, except for strings and complex
3580         // numbers
3581         int len = (c.isComplex() || c.isString()) ? -1 : Array.getLength(o);
3582 
3583         // Convert to FITS storage array
3584         o = javaToFits1D(c, o);
3585 
3586         // For complex values and strings, determine length from converted object....
3587         if (len < 0) {
3588             len = Array.getLength(o);
3589 
3590             // If complex in primitive 1D form, then length is half the number of elements.
3591             if (c.isComplex() && o.getClass().getComponentType().isPrimitive()) {
3592                 len >>>= 1;
3593             }
3594         }
3595 
3596         if (oldPointer != null) {
3597             if (len <= getPointerCount(oldPointer)) {
3598                 // Write data back at the old heap location
3599                 off = (int) getPointerOffset(oldPointer);
3600             }
3601         }
3602 
3603         h.putData(o, off);
3604 
3605         return c.hasLongPointers() ? new long[] {len, off} : new int[] {len, off};
3606     }
3607 
3608     /**
3609      * Returns a FITS data array from the heap
3610      * 
3611      * @param  c             The column descriptor, specifying the data type
3612      * @param  p             The heap pointer, either <code>int[2]</code> or else a <code>long[2]</code>
3613      * @param  isEnhanced    Whether logicals should be returned as {@link Boolean} (rather than <code>boolean</code>)
3614      *                           and complex values as {@link ComplexValue} (rather than <code>float[2]</code> or
3615      *                           <code>double[2]</code>), or arrays thereof. Methods prior to 1.18 should set this to
3616      *                           <code>false</code> for back compatible behavior.
3617      * 
3618      * @return               the FITS array object retrieved from the heap
3619      * 
3620      * @throws FitsException if the data could not be accessed in full from the heap.
3621      */
3622     protected Object getFromHeap(ColumnDesc c, Object p, boolean isEnhanced) throws FitsException {
3623         long len = getPointerCount(p);
3624         long off = getPointerOffset(p);
3625 
3626         if (off > Integer.MAX_VALUE || len > Integer.MAX_VALUE) {
3627             throw new FitsException("Data located beyond 32-bit accessible heap limit: off=" + off + ", len=" + len);
3628         }
3629 
3630         Object e = null;
3631 
3632         if (c.isComplex()) {
3633             e = Array.newInstance(c.getFitsBase(), (int) len, 2);
3634         } else {
3635             e = Array.newInstance(c.getFitsBase(), c.getFitsBaseCount((int) len));
3636         }
3637 
3638         readHeap(off, e);
3639 
3640         return fitsToJava1D(c, e, (int) len, isEnhanced);
3641     }
3642 
3643     /**
3644      * Convert Java arrays to their FITS representation. Transformation include boolean &rightarrow; 'T'/'F' or '\0';
3645      * Strings &rightarrow; byte arrays; variable length arrays &rightarrow; pointers (after writing data to heap).
3646      *
3647      * @param  c             The column descritor
3648      * @param  o             A one-dimensional Java array
3649      *
3650      * @return               An one-dimensional array with values as stored in FITS.
3651      * 
3652      * @throws FitsException if the operation failed
3653      */
3654     private static Object javaToFits1D(ColumnDesc c, Object o) throws FitsException {
3655 
3656         if (c.isBits()) {
3657             if (o instanceof Boolean && c.isSingleton()) {
3658                 // Scalar boxed boolean...
3659                 return FitsUtil.bitsToBytes(new boolean[] {(Boolean) o});
3660             }
3661             return FitsUtil.bitsToBytes((boolean[]) o);
3662         }
3663 
3664         if (c.isLogical()) {
3665             // Convert true/false to 'T'/'F', or null to '\0'
3666             return FitsUtil.booleansToBytes(o);
3667         }
3668 
3669         if (c.isComplex()) {
3670             if (o instanceof ComplexValue || o instanceof ComplexValue[]) {
3671                 return ArrayFuncs.complexToDecimals(o, c.fitsBase);
3672             }
3673         }
3674 
3675         if (c.isString()) {
3676             // Convert strings to array of bytes.
3677             if (o == null) {
3678                 if (c.isVariableSize()) {
3679                     return new byte[0];
3680                 }
3681 
3682                 return Array.newInstance(byte.class, c.fitsShape);
3683             }
3684 
3685             if (o instanceof String) {
3686                 int l = c.getStringLength();
3687                 if (l < 0) {
3688                     // Not fixed width, write the whole string.
3689                     l = ((String) o).length();
3690                 }
3691                 return FitsUtil.stringToByteArray((String) o, l);
3692             }
3693 
3694             if (c.isVariableSize() && c.delimiter != 0) {
3695                 // Write variable-length string arrays in delimited form
3696 
3697                 for (String s : (String[]) o) {
3698                     // We set the string length to that of the longest element + 1
3699                     c.setStringLength(Math.max(c.stringLength, s == null ? 1 : s.length() + 1));
3700                 }
3701 
3702                 return FitsUtil.stringsToDelimitedBytes((String[]) o, c.getStringLength(), c.delimiter);
3703             }
3704 
3705             // Fixed length substring array (not delimited).
3706             // For compatibility with tools that do not process array dimension, ASCII NULL should not
3707             // be used between components (permissible only at the end of all strings)
3708             return FitsUtil.stringsToByteArray((String[]) o, c.getStringLength(), FitsUtil.BLANK_SPACE);
3709         }
3710 
3711         return ArrayFuncs.objectToArray(o, true);
3712     }
3713 
3714     /**
3715      * Converts from the FITS representation of data to their basic Java array representation.
3716      *
3717      * @param  c             The column descritor
3718      * @param  o             A one-dimensional array of values as stored in FITS
3719      * @param  bits          A bit count for bit arrays (otherwise unused).
3720      * @param  isEnhanced    Whether logicals should be returned as {@link Boolean} (rather than <code>boolean</code>)
3721      *                           and complex values as {@link ComplexValue} (rather than <code>float[2]</code> or
3722      *                           <code>double[2]</code>), or arrays thereof. Methods prior to 1.18 should set this to
3723      *                           <code>false</code> for back compatible behavior.
3724      *
3725      * @return               A {@link String} or a one-dimensional array with the matched basic Java type
3726      * 
3727      * @throws FitsException if the operation failed
3728      */
3729     private Object fitsToJava1D(ColumnDesc c, Object o, int bits, boolean isEnhanced) {
3730 
3731         if (c.isBits()) {
3732             return FitsUtil.bytesToBits((byte[]) o, bits);
3733         }
3734 
3735         if (c.isLogical()) {
3736             return isEnhanced ? FitsUtil.bytesToBooleanObjects(o) : FitsUtil.byteToBoolean((byte[]) o);
3737         }
3738 
3739         if (c.isComplex() && isEnhanced) {
3740             return ArrayFuncs.decimalsToComplex(o);
3741         }
3742 
3743         if (c.isString()) {
3744             byte[] bytes = (byte[]) o;
3745 
3746             int len = c.getStringLength();
3747 
3748             if (c.isVariableSize()) {
3749                 if (c.delimiter != 0) {
3750                     // delimited array of strings
3751                     return FitsUtil.delimitedBytesToStrings(bytes, c.getStringLength(), c.delimiter);
3752                 }
3753             }
3754 
3755             // If fixed or variable length arrays of strings...
3756             if (c.isSingleton()) {
3757                 // Single fixed string -- get it all but trim trailing spaces
3758                 return FitsUtil.extractString(bytes, new ParsePosition(0), bytes.length, FitsUtil.ASCII_NULL);
3759             }
3760 
3761             // Array of fixed-length strings -- we trim trailing spaces in each component
3762             String[] s = new String[bytes.length / len];
3763             for (int i = 0; i < s.length; i++) {
3764                 s[i] = FitsUtil.extractString(bytes, new ParsePosition(i * len), len, FitsUtil.ASCII_NULL);
3765             }
3766             return s;
3767         }
3768 
3769         return o;
3770     }
3771 
3772     /**
3773      * Create a column table with the specified number of rows. This is used when we defer instantiation of the
3774      * ColumnTable until the user requests data from the table.
3775      * 
3776      * @param  rows          the number of rows to allocate
3777      * 
3778      * @throws FitsException if the operation failed
3779      */
3780     protected synchronized void createTable(int rows) throws FitsException {
3781         int nfields = columns.size();
3782         Object[] data = new Object[nfields];
3783         int[] sizes = new int[nfields];
3784         for (int i = 0; i < nfields; i++) {
3785             ColumnDesc c = columns.get(i);
3786             sizes[i] = c.getTableBaseCount();
3787             data[i] = c.newInstance(rows);
3788         }
3789 
3790         table = createColumnTable(data, sizes);
3791         nRow = rows;
3792     }
3793 
3794     /**
3795      * Sets the input to use for reading (and possibly writing) this table. If the input implements
3796      * {@link ReadWriteAccess}, then it can be used for both reading and (re)writing the data, including editing in
3797      * deferred mode.
3798      * 
3799      * @param in The input from which we can read the table data.
3800      */
3801     private void setInput(ArrayDataInput in) {
3802         encoder = (in instanceof ReadWriteAccess) ? new FitsEncoder((ReadWriteAccess) in) : null;
3803     }
3804 
3805     @Override
3806     public void read(ArrayDataInput in) throws FitsException {
3807         setInput(in);
3808         super.read(in);
3809     }
3810 
3811     @Override
3812     protected void loadData(ArrayDataInput in) throws IOException, FitsException {
3813         setInput(in);
3814         synchronized (this) {
3815             createTable(nRow);
3816         }
3817         readTrueData(in);
3818     }
3819 
3820     /**
3821      * Extracts a column descriptor from the FITS header for a given column index
3822      * 
3823      * @param  header        the FITS header containing the column description(s)
3824      * @param  col           zero-based column index
3825      * 
3826      * @return               the Descriptor for that column.
3827      * 
3828      * @throws FitsException if the header deswcription is invalid or incomplete
3829      */
3830     public static ColumnDesc getDescriptor(Header header, int col) throws FitsException {
3831         String tform = header.getStringValue(Standard.TFORMn.n(col + 1));
3832 
3833         if (tform == null) {
3834             throw new FitsException("Missing TFORM" + (col + 1));
3835         }
3836 
3837         int count = 1;
3838         char type = 0;
3839 
3840         ParsePosition pos = new ParsePosition(0);
3841 
3842         try {
3843             count = AsciiFuncs.parseInteger(tform, pos);
3844         } catch (Exception e) {
3845             // Keep going...
3846         }
3847 
3848         try {
3849             type = Character.toUpperCase(AsciiFuncs.extractChar(tform, pos));
3850         } catch (Exception e) {
3851             throw new FitsException("Missing data type in TFORM: [" + tform + "]");
3852         }
3853 
3854         ColumnDesc c = new ColumnDesc();
3855 
3856         if (header.containsKey(Standard.TTYPEn.n(col + 1))) {
3857             c.name(header.getStringValue(Standard.TTYPEn.n(col + 1)));
3858         }
3859 
3860         if (type == POINTER_INT || type == POINTER_LONG) {
3861             // Variable length column...
3862             c.setVariableSize(type == POINTER_LONG);
3863 
3864             // Get the data type...
3865             try {
3866                 type = Character.toUpperCase(AsciiFuncs.extractChar(tform, pos));
3867             } catch (Exception e) {
3868                 throw new FitsException("Missing variable-length data type in TFORM: [" + tform + "]");
3869             }
3870         }
3871 
3872         // The special types...
3873         if (type == 'C' || type == 'M') {
3874             c.isComplex = true;
3875         } else if (type == 'X') {
3876             c.isBits = true;
3877         }
3878 
3879         if (!c.setFitsType(type)) {
3880             throw new FitsException("Invalid type '" + type + "' in column:" + col);
3881         }
3882 
3883         if (!c.isVariableSize()) {
3884             // Fixed sized column...
3885             int[] dims = parseTDims(header.getStringValue(Standard.TDIMn.n(col + 1)));
3886 
3887             if (dims == null) {
3888                 c.setFitsShape((count == 1 && type != 'A') ? SINGLETON_SHAPE : new int[] {count});
3889                 c.stringLength = -1; // T.B.D. further below...
3890             } else {
3891                 c.setFitsShape(dims);
3892             }
3893         }
3894 
3895         if (c.isString()) {
3896             // For vairable-length columns or of TDIM was not defined determine substring length from TFORM.
3897             c.parseSubstringConvention(tform, pos, c.getStringLength() < 0);
3898         }
3899 
3900         // Force to use the count in the header, even if it does not match up with the dimension otherwise.
3901         c.fitsCount = count;
3902 
3903         c.quant = Quantizer.fromTableHeader(header, col);
3904         if (c.quant.isDefault()) {
3905             c.quant = null;
3906         }
3907 
3908         return c;
3909     }
3910 
3911     /**
3912      * Process one column from a FITS Header.
3913      * 
3914      * @throws FitsException if the operation failed
3915      */
3916     private int processCol(Header header, int col, int offset) throws FitsException {
3917         ColumnDesc c = getDescriptor(header, col);
3918         c.offset = offset;
3919         columns.add(c);
3920 
3921         return c.rowLen();
3922     }
3923 
3924     /**
3925      * @deprecated (<i>for internal use</i>) Used Only by {@link nom.tam.image.compression.hdu.CompressedTableData} so
3926      *                 it would make a better private method in there.. `
3927      */
3928     @Deprecated
3929     protected void addByteVaryingColumn() {
3930         addColumn(ColumnDesc.createForVariableSize(byte.class));
3931     }
3932 
3933     /**
3934      * @deprecated (<i>for internal use</i>) This method should have visibility reduced to private
3935      */
3936     @SuppressWarnings("javadoc")
3937     @Deprecated
3938     protected ColumnTable<?> createColumnTable(Object[] arrCol, int[] sizes) throws TableException {
3939         return new ColumnTable<>(arrCol, sizes);
3940     }
3941 
3942     /**
3943      * Returns the heap, after initializing it from the input as necessary
3944      * 
3945      * @return               the initialized heap
3946      * 
3947      * @throws FitsException if we had trouble initializing it from the input.
3948      */
3949     @SuppressWarnings("resource")
3950     private synchronized FitsHeap getHeap() throws FitsException {
3951         if (heap == null) {
3952             readHeap(getRandomAccessInput());
3953         }
3954         return heap;
3955     }
3956 
3957     /**
3958      * Reads an array from the heap. Subclasses may override this, for example to provide read-only access to a related
3959      * table's heap area.
3960      * 
3961      * @param  offset        the heap offset
3962      * @param  array         the array to populate from the heap area
3963      * 
3964      * @throws FitsException if there was an issue accessing the heap
3965      */
3966     protected void readHeap(long offset, Object array) throws FitsException {
3967         getHeap().getData((int) offset, array);
3968     }
3969 
3970     /**
3971      * Read the heap which contains the data for variable length arrays. A. Kovacs (4/1/08) Separated heap reading, s.t.
3972      * the heap can be properly initialized even if in deferred read mode. columnToArray() checks and initializes the
3973      * heap as necessary.
3974      *
3975      * @param      input         stream to read from.
3976      *
3977      * @throws     FitsException if the heap could not be read from the stream
3978      * 
3979      * @deprecated               (<i>for internal use</i>) unused.
3980      */
3981     @Deprecated
3982     protected synchronized void readHeap(ArrayDataInput input) throws FitsException {
3983         if (input instanceof RandomAccess) {
3984             FitsUtil.reposition(input, getFileOffset() + getHeapAddress());
3985         }
3986         heap = new FitsHeap(heapFileSize);
3987         if (input != null) {
3988             heap.read(input);
3989         }
3990     }
3991 
3992     /**
3993      * Read table, heap and padding
3994      *
3995      * @param  i             the stream to read the data from.
3996      *
3997      * @throws FitsException if the reading failed
3998      */
3999     protected synchronized void readTrueData(ArrayDataInput i) throws FitsException {
4000         try {
4001             table.read(i);
4002             i.skipAllBytes(getHeapOffset());
4003             if (heap == null) {
4004                 readHeap(i);
4005             }
4006         } catch (IOException e) {
4007             throw new FitsException("Error reading binary table data:" + e, e);
4008         }
4009     }
4010 
4011     /**
4012      * Check if the column number is valid.
4013      *
4014      * @param  j The Java index (first=0) of the column to check.
4015      *
4016      * @return   <code>true</code> if the column is valid
4017      */
4018     protected boolean validColumn(int j) {
4019         return j >= 0 && j < getNCols();
4020     }
4021 
4022     /**
4023      * Check to see if this is a valid row.
4024      *
4025      * @param  i The Java index (first=0) of the row to check.
4026      *
4027      * @return   <code>true</code> if the row is valid
4028      */
4029     protected boolean validRow(int i) {
4030         return getNRows() > 0 && i >= 0 && i < getNRows();
4031     }
4032 
4033     /**
4034      * @deprecated (<i>for internal use</i>) Visibility should be reduced to protected.
4035      */
4036     @Deprecated
4037     @Override
4038     public void fillHeader(Header h) throws FitsException {
4039         fillHeader(h, true);
4040     }
4041 
4042     /**
4043      * Fills (updates) the essential header description of this table in the header, optionally updating the essential
4044      * column descriptions also if desired.
4045      * 
4046      * @param  h             The FITS header to populate
4047      * @param  updateColumns Whether to update the essential column descriptions also
4048      * 
4049      * @throws FitsException if there was an error accessing the header.
4050      */
4051     void fillHeader(Header h, boolean updateColumns) throws FitsException {
4052         h.deleteKey(Standard.SIMPLE);
4053         h.deleteKey(Standard.EXTEND);
4054 
4055         Standard.context(BinaryTable.class);
4056 
4057         Cursor<String, HeaderCard> c = h.iterator();
4058         c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_BINTABLE));
4059         c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue()));
4060         c.add(HeaderCard.create(Standard.NAXIS, 2));
4061 
4062         synchronized (this) {
4063             c.add(HeaderCard.create(Standard.NAXIS1, rowLen));
4064             c.add(HeaderCard.create(Standard.NAXIS2, nRow));
4065         }
4066 
4067         if (h.getLongValue(Standard.PCOUNT, -1L) < getParameterSize()) {
4068             c.add(HeaderCard.create(Standard.PCOUNT, getParameterSize()));
4069         }
4070 
4071         c.add(HeaderCard.create(Standard.GCOUNT, 1));
4072         c.add(HeaderCard.create(Standard.TFIELDS, columns.size()));
4073 
4074         if (getHeapOffset() == 0) {
4075             h.deleteKey(Standard.THEAP);
4076         } else {
4077             c.add(HeaderCard.create(Standard.THEAP, getHeapAddress()));
4078         }
4079 
4080         if (updateColumns) {
4081             for (int i = 0; i < columns.size(); i++) {
4082                 c.setKey(Standard.TFORMn.n(i + 1).key());
4083                 fillForColumn(h, c, i);
4084             }
4085         }
4086 
4087         Standard.context(null);
4088     }
4089 
4090     /**
4091      * Update the header to reflect the details of a given column.
4092      *
4093      * @throws FitsException if the operation failed
4094      */
4095     void fillForColumn(Header header, Cursor<String, HeaderCard> hc, int col) throws FitsException {
4096         ColumnDesc c = columns.get(col);
4097 
4098         try {
4099             Standard.context(BinaryTable.class);
4100 
4101             if (c.name() != null) {
4102                 hc.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), c.name()));
4103             }
4104 
4105             hc.add(HeaderCard.create(Standard.TFORMn.n(col + 1), c.getTFORM()));
4106 
4107             String tdim = c.getTDIM();
4108             if (tdim != null) {
4109                 hc.add(HeaderCard.create(Standard.TDIMn.n(col + 1), tdim));
4110             }
4111 
4112             if (c.quant != null) {
4113                 c.quant.editTableHeader(header, col);
4114             }
4115 
4116         } finally {
4117             Standard.context(null);
4118         }
4119     }
4120 
4121     /**
4122      * Returns the column descriptor of a given column in this table
4123      * 
4124      * @param  column                         the zero-based column index
4125      * 
4126      * @return                                the column's descriptor
4127      * 
4128      * @throws ArrayIndexOutOfBoundsException if this table does not contain a column with that index.
4129      * 
4130      * @see                                   #getDescriptor(String)
4131      */
4132     public ColumnDesc getDescriptor(int column) throws ArrayIndexOutOfBoundsException {
4133         return columns.get(column);
4134     }
4135 
4136     /**
4137      * Returns the (first) column descriptor whose name matches the specified value.
4138      * 
4139      * @param  name The column name (case sensitive).
4140      * 
4141      * @return      The descriptor of the first column by that name, or <code>null</code> if the table contains no
4142      *                  column by that name.
4143      * 
4144      * @see         #getDescriptor(int)
4145      * @see         #indexOf(String)
4146      * 
4147      * @since       1.20
4148      */
4149     public ColumnDesc getDescriptor(String name) {
4150         int col = indexOf(name);
4151         return col < 0 ? null : getDescriptor(col);
4152     }
4153 
4154     /**
4155      * Converts a column from FITS logical values to bits. Null values (allowed in logical columns) will map to
4156      * <code>false</code>.
4157      *
4158      * @param  col The zero-based index of the column to be reset.
4159      *
4160      * @return     Whether the conversion was possible. *
4161      * 
4162      * @since      1.18
4163      */
4164     public boolean convertToBits(int col) {
4165         ColumnDesc c = columns.get(col);
4166 
4167         if (c.isBits) {
4168             return true;
4169         }
4170 
4171         if (c.base != boolean.class) {
4172             return false;
4173         }
4174 
4175         c.isBits = true;
4176         return true;
4177     }
4178 
4179     /**
4180      * Convert a column from float/double to float complex/double complex. This is only possible for certain columns.
4181      * The return status indicates if the conversion is possible.
4182      *
4183      * @param  index         The zero-based index of the column to be reset.
4184      *
4185      * @return               Whether the conversion is possible. *
4186      *
4187      * @throws FitsException if the operation failed
4188      * 
4189      * @since                1.18
4190      * 
4191      * @see                  ColumnDesc#isComplex()
4192      * @see                  #addComplexColumn(Object, Class)
4193      */
4194     public synchronized boolean setComplexColumn(int index) throws FitsException {
4195 
4196         if (!validColumn(index)) {
4197             return false;
4198         }
4199 
4200         ColumnDesc c = columns.get(index);
4201         if (c.isComplex()) {
4202             return true;
4203         }
4204 
4205         if (c.base != float.class && c.base != double.class) {
4206             return false;
4207         }
4208 
4209         if (!c.isVariableSize()) {
4210             if (c.getLastFitsDim() != 2) {
4211                 return false;
4212             }
4213             // Set the column to complex
4214             c.isComplex = true;
4215 
4216             // Update the legacy (wrapped array) shape
4217             c.setLegacyShape(c.fitsShape);
4218             return true;
4219         }
4220 
4221         // We need to make sure that for every row, there are
4222         // an even number of elements so that we can
4223         // convert to an integral number of complex numbers.
4224         for (int i = 1; i < nRow; i++) {
4225             if (getPointerCount(getRawElement(i, index)) % 2 != 0) {
4226                 return false;
4227             }
4228         }
4229 
4230         // Halve the length component of array descriptors (2 reals = 1 complex)
4231         for (int i = 1; i < nRow; i++) {
4232             Object p = getRawElement(i, index);
4233             long len = getPointerCount(p) >>> 1;
4234             if (c.hasLongPointers()) {
4235                 ((long[]) p)[0] = len;
4236             } else {
4237                 ((int[]) p)[0] = (int) len;
4238             }
4239             setTableElement(i, index, p);
4240         }
4241 
4242         // Set the column to complex
4243         c.isComplex = true;
4244 
4245         return true;
4246     }
4247 
4248     /**
4249      * Checks if this table contains a heap for storing variable length arrays (VLAs).
4250      * 
4251      * @return <code>true</code> if the table contains a heap, or else <code>false</code>.
4252      * 
4253      * @since  1.19.1
4254      */
4255     public final boolean containsHeap() {
4256         return getParameterSize() > 0;
4257     }
4258 
4259     /**
4260      * <p>
4261      * Defragments the heap area of this table, compacting the heap area, and returning the number of bytes by which the
4262      * heap size has been reduced. When tables with variable-sized columns are modified, the heap may retain old data as
4263      * columns are removed or elements get replaced with new data of different size. The data order in the heap may also
4264      * get jumbled, causing what would appear to be sequential reads to jump all over the heap space with the caching.
4265      * And, depending on how the heap was constructed in the first place, it may not be optimal for the row-after-row
4266      * table access that is the most typical use case.
4267      * </p>
4268      * <p>
4269      * This method rebuilds the heap by taking elements in table read order (by rows, and columns) and puts them on a
4270      * new heap.
4271      * </p>
4272      * <p>
4273      * For best squential read performance, you should defragment all tables that have been built column-by-column
4274      * before writing them to a FITS file. The only time defragmentation is really not needed is if the table was built
4275      * row-by-row, with no modifications to variable-length content after the fact.
4276      * </p>
4277      * 
4278      * @return               the number of bytes by which the heap has shrunk as a result of defragmentation.
4279      * 
4280      * @throws FitsException if there was an error accessing the heap or the main data table comntaining the heap
4281      *                           locators. In case of an error the table content may be left in a damaged state.
4282      * 
4283      * @see                  #compact()
4284      * @see                  #setElement(int, int, Object)
4285      * @see                  #addColumn(Object)
4286      * @see                  #deleteColumns(int, int)
4287      * @see                  #setColumn(int, Object)
4288      * 
4289      * @since                1.18
4290      */
4291     public synchronized long defragment() throws FitsException {
4292         if (!containsHeap()) {
4293             return 0L;
4294         }
4295 
4296         int[] eSize = new int[columns.size()];
4297 
4298         for (int j = 0; j < columns.size(); j++) {
4299             ColumnDesc c = columns.get(j);
4300             if (c.isVariableSize()) {
4301                 eSize[j] = ElementType.forClass(c.getFitsBase()).size();
4302             }
4303         }
4304 
4305         FitsHeap hp = getHeap();
4306         long oldSize = hp.size();
4307         FitsHeap compact = new FitsHeap(0);
4308 
4309         for (int i = 0; i < nRow; i++) {
4310             for (int j = 0; j < columns.size(); j++) {
4311                 ColumnDesc c = columns.get(j);
4312                 if (c.isVariableSize()) {
4313                     Object p = getRawElement(i, j);
4314 
4315                     int len = (int) getPointerCount(p);
4316 
4317                     // Copy to new heap...
4318                     int pos = compact.copyFrom(hp, (int) getPointerOffset(p), c.getFitsBaseCount(len) * eSize[j]);
4319 
4320                     // Same length as before...
4321                     if (p instanceof long[]) {
4322                         ((long[]) p)[1] = pos;
4323                     } else {
4324                         ((int[]) p)[1] = pos;
4325                     }
4326 
4327                     // Update pointers in table
4328                     setTableElement(i, j, p);
4329                 }
4330             }
4331         }
4332 
4333         heap = compact;
4334         return oldSize - compact.size();
4335     }
4336 
4337     /**
4338      * Discard the information about the original heap size (if this table was read from an input), and instead use the
4339      * real size of the actual heap (plus reserved space around it) when writing to an output. Compacted tables may not
4340      * be re-writeable to the same file from which they were read, since they may be shorter than the original, but they
4341      * can always be written to a different file, which may at times be smaller than the original. It may be used along
4342      * with {@link #defragment()} to create FITS files with optimized storage from FITS files that may contain wasted
4343      * space.
4344      * 
4345      * @see   #defragment()
4346      * 
4347      * @since 1.19.1
4348      */
4349     public synchronized void compact() {
4350         heapFileSize = 0;
4351     }
4352 
4353     @Override
4354     public BinaryTableHDU toHDU() throws FitsException {
4355         Header h = new Header();
4356         fillHeader(h);
4357         return new BinaryTableHDU(h, this);
4358     }
4359 }