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