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