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             BinaryTable clone = (BinaryTable) super.clone();
1586             clone.lock = new Object();
1587             return clone;
1588         } catch (CloneNotSupportedException e) {
1589             return null;
1590         }
1591     }
1592 
1593     /**
1594      * Returns an independent copy of the binary table.
1595      * 
1596      * @return               a new binary that tnat contains an exact copy of us, but is completely independent.
1597      * 
1598      * @throws FitsException if the table could not be copied
1599      * 
1600      * @since                1.18
1601      */
1602     public BinaryTable copy() throws FitsException {
1603         BinaryTable copy = clone();
1604 
1605         synchronized (lock) {
1606             if (table != null) {
1607                 copy.table = table.copy();
1608             }
1609             if (heap != null) {
1610                 copy.heap = heap.copy();
1611             }
1612 
1613             copy.columns = new ArrayList<>();
1614             for (ColumnDesc c : columns) {
1615                 c = c.clone();
1616                 copy.columns.add(c);
1617             }
1618         }
1619 
1620         return copy;
1621     }
1622 
1623     /**
1624      * (<i>for internal use</i>) Discards all variable-length arrays from this table, that is all data stored on the
1625      * heap, and resets all heap descritors to (0,0).
1626      * 
1627      * @since 1.19.1
1628      */
1629     protected void discardVLAs() {
1630         synchronized (lock) {
1631             for (int col = 0; col < columns.size(); col++) {
1632                 ColumnDesc c = columns.get(col);
1633 
1634                 if (c.isVariableSize()) {
1635                     for (int row = 0; row < nRow; row++) {
1636                         table.setElement(row, col, c.hasLongPointers() ? new long[2] : new int[2]);
1637                     }
1638                 }
1639             }
1640 
1641             heap = new FitsHeap(0);
1642         }
1643     }
1644 
1645     /**
1646      * Returns the number of bytes per regular table row
1647      * 
1648      * @return the number of bytes in a regular table row.
1649      */
1650     final int getRowBytes() {
1651         synchronized (lock) {
1652             return rowLen;
1653         }
1654     }
1655 
1656     /**
1657      * @deprecated               (<i>for internal use</i>) It may become a private method in the future.
1658      *
1659      * @param      table         the table to create the column data.
1660      *
1661      * @throws     FitsException if the data could not be created.
1662      */
1663     @Deprecated
1664     public static void createColumnDataFor(BinaryTable table) throws FitsException {
1665         synchronized (table) {
1666             table.createTable(table.nRow);
1667         }
1668     }
1669 
1670     /**
1671      * @deprecated                     (<i>for internal use</i>) It may be reduced to private visibility in the future.
1672      *                                     Parse the TDIMS value. If the TDIMS value cannot be deciphered a one-d array
1673      *                                     with the size given in arrsiz is returned.
1674      *
1675      * @param      tdims               The value of the TDIMSn card.
1676      *
1677      * @return                         An int array of the desired dimensions. Note that the order of the tdims is the
1678      *                                     inverse of the order in the TDIMS key.
1679      * 
1680      * @throws     HeaderCardException if the argument does not confirm to the FITS specification for the TDIMn keyword.
1681      */
1682     @Deprecated
1683     public static int[] parseTDims(String tdims) throws HeaderCardException {
1684         if (tdims == null) {
1685             return null;
1686         }
1687 
1688         // The TDIMs value should be of the form: "(i,j...)"
1689         int start = tdims.indexOf('(');
1690 
1691         if (start < 0) {
1692             return null;
1693         }
1694 
1695         int end = tdims.indexOf(')', start);
1696         if (end < 0) {
1697             end = tdims.length();
1698         }
1699 
1700         StringTokenizer st = new StringTokenizer(tdims.substring(start + 1, end), ",");
1701         int dim = st.countTokens();
1702 
1703         if (dim > 0) {
1704             int[] dims = new int[dim];
1705             for (int i = dim; --i >= 0;) {
1706                 try {
1707                     dims[i] = Integer.parseInt(st.nextToken().trim());
1708                 } catch (NumberFormatException e) {
1709                     throw new HeaderCardException("Invalid TDIMn value: '" + tdims + "'");
1710                 }
1711 
1712                 if (dims[i] < 0) {
1713                     throw new HeaderCardException("Invalid TDIMn value: '" + tdims + "'");
1714                 }
1715             }
1716             return dims;
1717         }
1718 
1719         return null;
1720     }
1721 
1722     /**
1723      * <p>
1724      * Adds a column of complex values stored as the specified decimal type of components in the FITS. While you can
1725      * also use {@link #addColumn(Object)} to add complex values, that method will always add them as 64-bit
1726      * double-precision values. So, this method is provided to allow users more control over how they want their complex
1727      * data be stored.
1728      * </p>
1729      * <p>
1730      * 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,
1731      * which can be changed by {@link ColumnDesc#name(String)} after.
1732      * </p>
1733      * 
1734      * @param  o             A {@link ComplexValue} or an array (possibly multi-dimensional) thereof.
1735      * @param  decimalType   <code>float.class</code> or <code>double.class</code> (all other values default to
1736      *                           <code>double.class</code>).
1737      * 
1738      * @return               the number of column in the table including the new column.
1739      * 
1740      * @throws FitsException if the object contains values other than {@link ComplexValue} types or if the array is not
1741      *                           suitable for storing in the FITS, e.g. because it is multi-dimensional but varying in
1742      *                           shape / size.
1743      * 
1744      * @since                1.18
1745      * 
1746      * @see                  #addColumn(Object)
1747      */
1748     public int addComplexColumn(Object o, Class<?> decimalType) throws FitsException {
1749         int col = columns.size();
1750         int eSize = addColumn(ArrayFuncs.complexToDecimals(o, decimalType));
1751         ColumnDesc c = columns.get(col);
1752         c.isComplex = true;
1753         c.setLegacyShape(c.fitsShape);
1754         return eSize;
1755     }
1756 
1757     /**
1758      * <p>
1759      * Adds a column of string values (one per row), optimized for storage size. Unlike {@link #addColumn(Object)},
1760      * which always store strings in fixed format, this method will automatically use variable-length columns for
1761      * storing the strings if their lengths vary sufficiently to make that form of storage more efficient, or if the
1762      * array contains nulls (which may be defined later).
1763      * </p>
1764      * <p>
1765      * 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,
1766      * which can be changed by {@link ColumnDesc#name(String)} after.
1767      * </p>
1768      * 
1769      * @param  o             A 1D string array, with 1 string element per table row. The array may contain
1770      *                           <code>null</code> entries, in which case variable-length columns will be used, since
1771      *                           these may be defined later...
1772      * 
1773      * @return               the number of column in the table including the new column.
1774      * 
1775      * @throws FitsException if the object contains values other than {@link ComplexValue} types or if the array is not
1776      *                           suitable for storing in the FITS, e.g. because it is multi-dimensional but varying in
1777      *                           shape / size.
1778      * 
1779      * @since                1.18
1780      * 
1781      * @see                  #addColumn(Object)
1782      */
1783     public int addStringColumn(String[] o) throws FitsException {
1784         checkRowCount(o);
1785 
1786         ColumnDesc c = new ColumnDesc(String.class);
1787 
1788         // Check if we should be using variable-length strings
1789         // (provided its a scalar string column with sufficiently varied strings sizes to make it worth..
1790         int min = FitsUtil.minStringLength(o);
1791         int max = FitsUtil.maxStringLength(o);
1792 
1793         if (max - min > 2 * ElementType.forClass(c.pointerClass()).size()) {
1794             c = ColumnDesc.createForVariableSize(String.class);
1795             return addVariableSizeColumn(o, c);
1796         }
1797 
1798         c = ColumnDesc.createForStrings(max);
1799         return addFlattenedColumn(o, o.length, c, false);
1800     }
1801 
1802     /**
1803      * <p>
1804      * Adds a column of bits. This uses much less space than if adding boolean values as logicals (the default behaviot
1805      * of {@link #addColumn(Object)}, since logicals take up 1 byte per element, whereas bits are really single bits.
1806      * </p>
1807      * <p>
1808      * 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,
1809      * which can be changed by {@link ColumnDesc#name(String)} after.
1810      * </p>
1811      * 
1812      * @param  o                        An any-dimensional array of <code>boolean</code> values.
1813      * 
1814      * @return                          the number of column in the table including the new column.
1815      * 
1816      * @throws IllegalArgumentException if the argument is not an array of <code>boolean</code> values.
1817      * @throws FitsException            if the object is not an array of <code>boolean</code> values.
1818      * 
1819      * @since                           1.18
1820      * 
1821      * @see                             #addColumn(Object)
1822      */
1823     public int addBitsColumn(Object o) throws FitsException {
1824         if (ArrayFuncs.getBaseClass(o) != boolean.class) {
1825             throw new IllegalArgumentException("Not an array of booleans: " + o.getClass());
1826         }
1827         return addColumn(o, false);
1828     }
1829 
1830     /**
1831      * <p>
1832      * Adds a new empty column to the table to the specification. This is useful when the user may want ot have more
1833      * control on how columns are configured before calling {@link #addRow(Object[])} to start populating. The new
1834      * column will be named as "Column <i>n</i>" (where <i>n</i> is the 1-based index of the column) by default, unless
1835      * already named otherwise.
1836      * </p>
1837      * <p>
1838      * 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,
1839      * which can be changed by {@link ColumnDesc#name(String)} after.
1840      * </p>
1841      * 
1842      * @param  descriptor            the column descriptor
1843      * 
1844      * @return                       the number of table columns after the addition
1845      * 
1846      * @throws IllegalStateException if the table already contains data rows that prevent the addition of empty
1847      *                                   comlumns.
1848      * 
1849      * @see                          #addRow(Object[])
1850      * @see                          ColumnDesc#name(String)
1851      */
1852     public int addColumn(ColumnDesc descriptor) throws IllegalStateException {
1853         synchronized (lock) {
1854             if (nRow != 0) {
1855                 throw new IllegalStateException("Cannot add empty columns to table already containing data rows");
1856             }
1857 
1858             descriptor.offset = rowLen;
1859             rowLen += descriptor.rowLen();
1860 
1861             if (descriptor.name() == null) {
1862                 // Set default column name;
1863                 descriptor.name(TableHDU.getDefaultColumnName(columns.size()));
1864             }
1865             columns.add(descriptor);
1866             return columns.size();
1867         }
1868     }
1869 
1870     /**
1871      * <p>
1872      * Adds a new column with the specified data array, with some default mappings. This method will always use
1873      * double-precision representation for {@link ComplexValue}-based data, and will represent <code>boolean</code>
1874      * based array data as one-byte-per element FITS logical values (for back compatibility). It will also store strings
1875      * as fixed sized (sized for the longest string element contained).
1876      * </p>
1877      * <p>
1878      * 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,
1879      * which can be changed by {@link ColumnDesc#name(String)} after.
1880      * </p>
1881      * <p>
1882      * If you want other complex-valued representations use {@link #addComplexColumn(Object, Class)} instead, and if you
1883      * want to pack <code>boolean</code>-based data more efficiently (using up to 8 times less space), use
1884      * {@link #addBitsColumn(Object)} instead, or else convert the column to bits afterwards using
1885      * {@link #convertToBits(int)}. And, if you want to allow storing strings more effiently in variable-length columns,
1886      * you should use {@link #addStringColumn(String[])} instead.
1887      * </p>
1888      * <p>
1889      * As of 1.18, the argument can be a boxed primitive for a coulmn containing a single scalar-valued entry (row).
1890      * </p>
1891      * 
1892      * @param o column data array
1893      * 
1894      * @see     #addVariableSizeColumn(Object)
1895      * @see     #addComplexColumn(Object, Class)
1896      * @see     #addBitsColumn(Object)
1897      * @see     #convertToBits(int)
1898      * @see     #addStringColumn(String[])
1899      * @see     ColumnDesc#name(String)
1900      */
1901     @Override
1902     public int addColumn(Object o) throws FitsException {
1903         return addColumn(o, true);
1904     }
1905 
1906     private int checkRowCount(Object o) throws FitsException {
1907         if (!o.getClass().isArray()) {
1908             throw new TableException("Not an array: " + o.getClass().getName());
1909         }
1910 
1911         int rows = Array.getLength(o);
1912 
1913         synchronized (lock) {
1914             if (columns.size() != 0 && rows != nRow) {
1915                 throw new TableException("Mismatched number of rows: " + rows + ", expected " + nRow);
1916             }
1917         }
1918 
1919         return rows;
1920     }
1921 
1922     /**
1923      * Like {@link #addColumn(Object)}, but allows specifying whether we use back compatible mode. This mainly just
1924      * affects how <code>boolean</code> arrays are stored (as logical bytes in compatibility mode, or as packed bits
1925      * otherwise).
1926      * 
1927      * @param o      The column data array
1928      * @param compat Whether to add the column in a back compatibility mode with versions prior to 1.18. If
1929      *                   <code>true</code> <code>boolean</code> arrays will stored as logical bytes, otherwise as packed
1930      *                   bits.
1931      */
1932     private int addColumn(Object o, boolean compat) throws FitsException {
1933         o = ArrayFuncs.objectToArray(o, compat);
1934 
1935         int rows = checkRowCount(o);
1936 
1937         ColumnDesc c = new ColumnDesc(ArrayFuncs.getBaseClass(o));
1938 
1939         if (ArrayFuncs.getBaseClass(o) == ComplexValue.class) {
1940             o = ArrayFuncs.complexToDecimals(o, double.class);
1941             c.isComplex = true;
1942         }
1943 
1944         try {
1945             int[] dim = ArrayFuncs.checkRegularArray(o, c.isNullAllowed());
1946 
1947             if (c.isString()) {
1948                 c.setStringLength(FitsUtil.maxStringLength(o));
1949             }
1950 
1951             if (c.isComplex) {
1952                 // Drop the railing 2 dimension, keep only outer dims...
1953                 dim = Arrays.copyOf(dim, dim.length - 1);
1954                 o = ArrayFuncs.flatten(o);
1955             }
1956 
1957             if (dim.length <= 1) {
1958                 c.setSingleton();
1959             } else {
1960                 int[] shape = new int[dim.length - 1];
1961                 System.arraycopy(dim, 1, shape, 0, shape.length);
1962                 c.setLegacyShape(shape);
1963                 o = ArrayFuncs.flatten(o);
1964             }
1965         } catch (IllegalArgumentException e) {
1966             c.setVariableSize(false);
1967             return addVariableSizeColumn(o, c);
1968         }
1969         // getBaseClass() prevents heterogeneous columns, so no need to catch ClassCastException here.
1970 
1971         return addFlattenedColumn(o, rows, c, compat);
1972     }
1973 
1974     /**
1975      * <p>
1976      * Adds a new variable-length data column, populating it with the specified data object. Unlike
1977      * {@link #addColumn(Object)} which will use fixed-size data storage provided the data allows it, this method forces
1978      * the use of variable-sized storage regardless of the data layout -- for example to accommodate addiing rows /
1979      * elements of different sized at a later time.
1980      * </p>
1981      * <p>
1982      * 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,
1983      * which can be changed by {@link ColumnDesc#name(String)} after.
1984      * </p>
1985      * 
1986      * @param  o             An array containing one entry per row. Multi-dimensional entries will be flattened to 1D
1987      *                           for storage on the heap.
1988      * 
1989      * @return               the number of table columns after the addition.
1990      * 
1991      * @throws FitsException if the column could not be created as requested.
1992      * 
1993      * @see                  #addColumn(Object)
1994      * @see                  #addColumn(ColumnDesc)
1995      * @see                  ColumnDesc#createForVariableSize(Class)
1996      * @see                  ColumnDesc#isVariableSize()
1997      * 
1998      * @since                1.18
1999      */
2000     public int addVariableSizeColumn(Object o) throws FitsException {
2001         Class<?> base = ArrayFuncs.getBaseClass(o);
2002         ColumnDesc c = ColumnDesc.createForVariableSize(base);
2003         return addVariableSizeColumn(o, c);
2004     }
2005 
2006     /**
2007      * Adds a new column with data directly, without performing any checks on the data. This should only be use
2008      * internally, after ansuring the data integrity and suitability for this table.
2009      * 
2010      * @param  o             the column data, whose integrity was verified previously
2011      * @param  rows          the number of rows the data contains (in flattened form)
2012      * @param  c             the new column's descriptor
2013      * 
2014      * @return               the number of table columns after the addition
2015      * 
2016      * @throws FitsException if the data is not the right type or format for internal storage.
2017      */
2018     private int addDirectColumn(Object o, int rows, ColumnDesc c) throws FitsException {
2019         synchronized (lock) {
2020             c.offset = rowLen;
2021             rowLen += c.rowLen();
2022 
2023             // Load any deferred data (we will not be able to do that once we alter the column structure)
2024             ensureData();
2025 
2026             // Set the default column name
2027             c.name(TableHDU.getDefaultColumnName(columns.size()));
2028 
2029             table.addColumn(o, c.getTableBaseCount());
2030             columns.add(c);
2031 
2032             if (nRow == 0) {
2033                 // Set the table row count to match first colum
2034                 nRow = rows;
2035             }
2036 
2037             return columns.size();
2038         }
2039     }
2040 
2041     private int addVariableSizeColumn(Object o, ColumnDesc c) throws FitsException {
2042         checkRowCount(o);
2043 
2044         Object[] array = (Object[]) o;
2045 
2046         o = Array.newInstance(c.pointerClass(), array.length * 2);
2047 
2048         for (int i = 0; i < array.length; i++) {
2049             boolean multi = c.isComplex() ? array[i] instanceof Object[][] : array[i] instanceof Object[];
2050 
2051             if (multi) {
2052                 boolean canBeComplex = false;
2053 
2054                 if (c.getFitsBase() == float.class || c.getFitsBase() == double.class) {
2055                     int[] dim = ArrayFuncs.getDimensions(array[i]);
2056                     if (dim[dim.length - 1] == 2) {
2057                         canBeComplex = true;
2058                     }
2059                 }
2060 
2061                 if (!canBeComplex && !c.warnedFlatten) {
2062                     LOG.warning("Table entries of " + array[i].getClass()
2063                             + " will be stored as 1D arrays in variable-length columns. "
2064                             + "Array shape(s) and intermittent null subarrays (if any) will be lost.");
2065 
2066                     c.warnedFlatten = true;
2067                 }
2068             }
2069 
2070             Object p = putOnHeap(c, array[i], null);
2071             System.arraycopy(p, 0, o, 2 * i, 2);
2072         }
2073 
2074         return addDirectColumn(o, array.length, c);
2075     }
2076 
2077     /**
2078      * Add a column where the data is already flattened.
2079      *
2080      * @param      o             The new column data. This should be a one-dimensional primitive array.
2081      * @param      dims          The dimensions of an element in the column, or null for singleton (scalar) columns
2082      *
2083      * @return                   the new column size
2084      *
2085      * @throws     FitsException if the array could not be flattened
2086      * 
2087      * @deprecated               (<i>for internal use</i>) No longer used, will be removed in the future
2088      */
2089     @Deprecated
2090     public int addFlattenedColumn(Object o, int... dims) throws FitsException {
2091         ColumnDesc c = new ColumnDesc(ArrayFuncs.getBaseClass(o));
2092 
2093         try {
2094             ArrayFuncs.checkRegularArray(o, c.isNullAllowed());
2095         } catch (IllegalArgumentException e) {
2096             throw new FitsException("Irregular array: " + o.getClass() + ": " + e.getMessage(), e);
2097         }
2098 
2099         if (c.isString()) {
2100             c.setStringLength(FitsUtil.maxStringLength(o));
2101         }
2102 
2103         int n = 1;
2104 
2105         c.setLegacyShape(dims);
2106         for (int dim : dims) {
2107             n *= dim;
2108         }
2109 
2110         int rows = Array.getLength(o) / n;
2111 
2112         return addFlattenedColumn(o, rows, c, true);
2113     }
2114 
2115     /**
2116      * Checks that a flattened column has a compatible size for storing in a fixed-width column. It will also log a
2117      * warning if the storage size of the object is zero.
2118      * 
2119      * @param  c             the column descriptor
2120      * @param  o             the column data
2121      * 
2122      * @throws FitsException if the data is not the right size for the column
2123      */
2124     private void checkFlattenedColumnSize(ColumnDesc c, Object o) throws FitsException {
2125         synchronized (lock) {
2126             if (c.getTableBaseCount() == 0) {
2127                 LOG.warning("Elements of column + " + columns.size() + " have zero storage size.");
2128             } else if (columns.size() > 0) {
2129                 // Check that the number of rows is consistent.
2130                 int l = Array.getLength(o);
2131                 if (nRow > 0 && l != nRow * c.getTableBaseCount()) {
2132                     throw new TableException(
2133                             "Mismatched element count " + l + ", expected " + (nRow * c.getTableBaseCount()));
2134                 }
2135             }
2136         }
2137     }
2138 
2139     /**
2140      * This function is needed since we had made addFlattenedColumn public so in principle a user might have called it
2141      * directly.
2142      *
2143      * @param  o             The new column data. This should be a one-dimensional primitive array.
2144      * @param  c             The column description
2145      *
2146      * @return               the new column size
2147      *
2148      * @throws FitsException if the data type, format, or element count is inconsistent with this table.
2149      */
2150     private int addFlattenedColumn(Object o, int rows, ColumnDesc c, boolean compat) throws FitsException {
2151         // For back compatibility this method will add boolean values as logicals always...
2152         if (compat) {
2153             c.isBits = false;
2154         }
2155 
2156         if (c.isBits) {
2157             // Special handling for bits, which have to be segmented into bytes...
2158             boolean[] bits = (boolean[]) o;
2159             o = FitsUtil.bitsToBytes(bits, bits.length / rows);
2160         } else {
2161             o = javaToFits1D(c, o);
2162         }
2163 
2164         checkFlattenedColumnSize(c, o);
2165 
2166         return addDirectColumn(o, rows, c);
2167     }
2168 
2169     /**
2170      * <p>
2171      * Adds a row to the table. If this is the first row in a new table, fixed-length columns will be created from the
2172      * data type automatically. If you want more control over the column formats, you may want to specify columns
2173      * beforehand such as:
2174      * </p>
2175      * 
2176      * <pre>
2177      *   BinaryTable table = new BinaryTable();
2178      *   
2179      *   // A column containing 64-bit floating point scalar values, 1 per row...
2180      *   table.addColumn(ColumnDesc.createForScalars(double.class));
2181      *   
2182      *   // A column containing 5x4 arrays of single-precision complex values...
2183      *   table.addColumn(ColumnDesc.createForArrays(ComplexValue.Float.class, 5, 4)
2184      *  
2185      *   // A column containing Strings of variable length using 32-bit heap pointers...
2186      *   table.addColumn(ColumnDesc.creatForVariableStrings(false);
2187      * </pre>
2188      * <p>
2189      * For scalar columns of primitive types, the argument may be the corresponding java boxed type (new style), or a
2190      * primitive array of 1 (old style). Thus, you can write either:
2191      * </p>
2192      * 
2193      * <pre>
2194      * table.addRow(1, 3.14159265);
2195      * </pre>
2196      * <p>
2197      * or,
2198      * </p>
2199      * 
2200      * <pre>
2201      *   table.addRow(new Object[] { new int[] {1}, new double[] {3.14159265} };
2202      * </pre>
2203      * 
2204      * @see #addColumn(ColumnDesc)
2205      */
2206     @Override
2207     public int addRow(Object[] o) throws FitsException {
2208         synchronized (lock) {
2209             if (columns.isEmpty()) {
2210                 for (Object element : o) {
2211                     if (element == null) {
2212                         throw new TableException("Prototype row may not contain null");
2213                     }
2214 
2215                     Class<?> cl = element.getClass();
2216 
2217                     if (cl.isArray()) {
2218                         if (cl.getComponentType().isPrimitive() && Array.getLength(element) == 1) {
2219                             // Primitives of 1 (e.g. short[1]) are wrapped and should be added as is.
2220                             addColumn(element);
2221                         } else {
2222                             // Wrap into array of 1, as leading dimension becomes the number of rows, which must be 1...
2223                             Object wrapped = Array.newInstance(element.getClass(), 1);
2224                             Array.set(wrapped, 0, element);
2225                             addColumn(wrapped);
2226                         }
2227                     } else {
2228                         addColumn(ArrayFuncs.objectToArray(element, true));
2229                     }
2230                 }
2231 
2232                 return 1;
2233             }
2234 
2235             if (o.length != columns.size()) {
2236                 throw new TableException("Mismatched row size: " + o.length + ", expected " + columns.size());
2237             }
2238 
2239             ensureData();
2240 
2241             Object[] flatRow = new Object[getNCols()];
2242 
2243             for (int i = 0; i < flatRow.length; i++) {
2244                 ColumnDesc c = columns.get(i);
2245                 if (c.isVariableSize()) {
2246                     flatRow[i] = putOnHeap(c, o[i], null);
2247                 } else {
2248                     flatRow[i] = javaToFits1D(c, ArrayFuncs.flatten(o[i]));
2249 
2250                     int nexp = c.getElementCount();
2251                     if (c.stringLength > 0) {
2252                         nexp *= c.stringLength;
2253                     }
2254 
2255                     if (Array.getLength(flatRow[i]) != nexp) {
2256                         throw new IllegalArgumentException("Mismatched element count for column " + i + ": got "
2257                                 + Array.getLength(flatRow[i]) + ", expected " + nexp);
2258                     }
2259                 }
2260             }
2261 
2262             table.addRow(flatRow);
2263             nRow++;
2264 
2265             return nRow;
2266         }
2267     }
2268 
2269     @Override
2270     public void deleteColumns(int start, int len) throws FitsException {
2271         synchronized (lock) {
2272             ensureData();
2273 
2274             table.deleteColumns(start, len);
2275 
2276             ArrayList<ColumnDesc> remain = new ArrayList<>(columns.size() - len);
2277             rowLen = 0;
2278 
2279             for (int i = 0; i < columns.size(); i++) {
2280                 if (i < start || i >= start + len) {
2281                     ColumnDesc c = columns.get(i);
2282                     c.offset = rowLen;
2283                     rowLen += c.rowLen();
2284                     remain.add(c);
2285                 }
2286             }
2287             columns = remain;
2288         }
2289     }
2290 
2291     @Override
2292     public void deleteRows(int row, int len) throws FitsException {
2293         synchronized (lock) {
2294             ensureData();
2295             table.deleteRows(row, len);
2296             nRow -= len;
2297         }
2298     }
2299 
2300     /**
2301      * Returns the Java type of elements returned or expected by the older srray-based access methods. It can be
2302      * confusing, because:
2303      * <ul>
2304      * <li>Columns with variable sized entries report <code>int.class</code> or <code>long.class</code> regardless of
2305      * data type.</li>
2306      * <li>Regular logical and bit columns bith report <code>boolean.class</code>.</li>
2307      * <li>Regular complex valued columns report <code>float.class</code> or <code>double.class</code>.</li>
2308      * </ul>
2309      * 
2310      * @return     the types in the table, not the underlying types (e.g., for varying length arrays or booleans).
2311      * 
2312      * @deprecated (<i>for internal use</i>) Ambiguous, use {@link ColumnDesc#getElementClass()} instead. Will remove in
2313      *                 the future.
2314      */
2315     @Deprecated
2316     public Class<?>[] getBases() {
2317         synchronized (lock) {
2318             return table.getBases();
2319         }
2320     }
2321 
2322     /**
2323      * <p>
2324      * Returns the data for a particular column in as an array of elements. See {@link #addColumn(Object)} for more
2325      * information about the format of data elements in general.
2326      * </p>
2327      * 
2328      * @param  col           The zero-based column index.
2329      * 
2330      * @return               an array of primitives (for scalar columns), or else an <code>Object[]</code> array, or
2331      *                           possibly <code>null</code>
2332      * 
2333      * @throws FitsException if the table could not be accessed
2334      * 
2335      * @see                  #setColumn(int, Object)
2336      * @see                  #getElement(int, int)
2337      * @see                  #getNCols()
2338      */
2339     @Override
2340     public Object getColumn(int col) throws FitsException {
2341         synchronized (lock) {
2342             ColumnDesc c = columns.get(col);
2343 
2344             if (!c.isVariableSize() && c.fitsDimension() == 0 && !c.isComplex()) {
2345                 return getFlattenedColumn(col);
2346             }
2347 
2348             ensureData();
2349 
2350             Object[] data = null;
2351 
2352             for (int i = 0; i < nRow; i++) {
2353                 Object e = getElement(i, col);
2354                 if (data == null) {
2355                     data = (Object[]) Array.newInstance(e.getClass(), nRow);
2356                 }
2357                 data[i] = e;
2358             }
2359 
2360             return data;
2361         }
2362     }
2363 
2364     /**
2365      * Returns the Java index of the first column by the specified name.
2366      * 
2367      * @param  name the name of the column (case sensitive).
2368      * 
2369      * @return      The column index, or else -1 if this table does not contain a column by the specified name.
2370      * 
2371      * @see         #getDescriptor(String)
2372      * @see         ColumnDesc#name(String)
2373      * 
2374      * @since       1.20
2375      */
2376     public int indexOf(String name) {
2377         for (int col = 0; col < columns.size(); col++) {
2378             if (name.equals(getDescriptor(col).name())) {
2379                 return col;
2380             }
2381         }
2382         return -1;
2383     }
2384 
2385     @Override
2386     protected ColumnTable<?> getCurrentData() {
2387         synchronized (lock) {
2388             return table;
2389         }
2390     }
2391 
2392     @Override
2393     public ColumnTable<?> getData() throws FitsException {
2394         return (ColumnTable<?>) super.getData();
2395     }
2396 
2397     /**
2398      * Returns the dimensions of elements in each column.
2399      * 
2400      * @return     an array of arrays with the dimensions of each column's data.
2401      * 
2402      * @see        ColumnDesc#getDimens()
2403      * 
2404      * @deprecated (<i>for internal use</i>) Use {@link ColumnDesc#getEntryShape()} to access the shape of Java elements
2405      *                 individually for columns instead. Not useful to users since it returns the dimensions of the
2406      *                 primitive storage types, which is not always the dimension of elements on the Java side (notably
2407      *                 for string entries).
2408      */
2409     @Deprecated
2410     public int[][] getDimens() {
2411         int[][] dimens = new int[columns.size()][];
2412         for (int i = 0; i < dimens.length; i++) {
2413             dimens[i] = columns.get(i).getDimens();
2414         }
2415         return dimens;
2416     }
2417 
2418     /**
2419      * @deprecated               (<i>for internal use</i>) It may be private in the future.
2420      * 
2421      * @return                   An array with flattened data, in which each column's data is represented by a 1D array
2422      * 
2423      * @throws     FitsException if the reading of the data failed.
2424      */
2425     @Deprecated
2426     public Object[] getFlatColumns() throws FitsException {
2427         ensureData();
2428         return table.getColumns();
2429     }
2430 
2431     /**
2432      * @deprecated               (<i>for internal use</i>) It may be reduced to private visibility in the future.
2433      * 
2434      * @return                   column in flattened format. This is sometimes useful for fixed-sized columns.
2435      *                               Variable-sized columns will still return an <code>Object[]</code> array in which
2436      *                               each entry is the variable-length data for a row.
2437      *
2438      * @param      col           the column to flatten
2439      *
2440      * @throws     FitsException if the column could not be flattened
2441      */
2442     @Deprecated
2443     public Object getFlattenedColumn(int col) throws FitsException {
2444         synchronized (lock) {
2445             if (!validColumn(col)) {
2446                 throw new TableException("Invalid column index " + col + " in table of " + getNCols() + " columns");
2447             }
2448 
2449             ColumnDesc c = columns.get(col);
2450             if (c.isVariableSize()) {
2451                 throw new TableException("Cannot flatten variable-sized column data");
2452             }
2453 
2454             ensureData();
2455 
2456             if (c.isBits()) {
2457                 boolean[] bits = new boolean[nRow * c.fitsCount];
2458                 for (int i = 0; i < nRow; i++) {
2459                     boolean[] seg = (boolean[]) fitsToJava1D(c, table.getElement(i, col), c.fitsCount, false);
2460                     System.arraycopy(seg, 0, bits, i * c.fitsCount, c.fitsCount);
2461                 }
2462                 return bits;
2463             }
2464 
2465             return fitsToJava1D(c, table.getColumn(col), 0, false);
2466         }
2467     }
2468 
2469     /**
2470      * <p>
2471      * Reserves space for future addition of rows at the end of the regular table. In effect, this pushes the heap to
2472      * start at an offset value, leaving a gap between the main table and the heap in the FITS file. If your table
2473      * contains variable-length data columns, you may also want to reserve extra heap space for these via
2474      * {@link #reserveHeapSpace(int)}.
2475      * </p>
2476      * <p>
2477      * Note, that (C)FITSIO, as of version 4.4.0, has no proper support for offset heaps, and so you may want to be
2478      * careful using this function as the resulting FITS files, while standard, may not be readable by other tools due
2479      * to their own lack of support. Note, however, that you may also use this function to undo an offset heap with an
2480      * argument &lt;=0;
2481      * </p>
2482      * 
2483      * @param rows The number of future rows fow which space should be reserved (relative to the current table size) for
2484      *                 future additions, or &lt;=0 to ensure that the heap always follows immediately after the main
2485      *                 table, e.g. for better (C)FITSIO interoperability.
2486      * 
2487      * @see        #reserveHeapSpace(int)
2488      * 
2489      * @since      1.19.1
2490      */
2491     public void reserveRowSpace(int rows) {
2492         synchronized (lock) {
2493             heapAddress = rows > 0 ? getRegularTableSize() + (long) rows * getRowBytes() : 0;
2494         }
2495     }
2496 
2497     /**
2498      * Reserves space in the file at the end of the heap for future heap growth (e.g. different/longer or new VLA
2499      * entries). You may generally want to call this along with {@link #reserveRowSpace(int)} if yuor table contains
2500      * variable-length columns, to ensure storage for future data in these. You may call with &lt;=0 to discards any
2501      * previously reserved space.
2502      * 
2503      * @param bytes The number of bytes of unused space to reserve at the end of the heap, e.g. for future modifications
2504      *                  or additions, when writing the data to file.
2505      * 
2506      * @see         #reserveRowSpace(int)
2507      * 
2508      * @since       1.19.1
2509      */
2510     public void reserveHeapSpace(int bytes) {
2511         synchronized (lock) {
2512             heapReserve = Math.max(0, bytes);
2513         }
2514     }
2515 
2516     /**
2517      * Returns the address of the heap from the star of the HDU in the file.
2518      * 
2519      * @return (bytes) the start of the heap area from the beginning of the HDU.
2520      */
2521     final long getHeapAddress() {
2522         synchronized (lock) {
2523             long tableSize = getRegularTableSize();
2524             return heapAddress > tableSize ? heapAddress : tableSize;
2525         }
2526     }
2527 
2528     /**
2529      * Returns the offset from the end of the main table
2530      * 
2531      * @return the offset to the heap
2532      */
2533     final long getHeapOffset() {
2534         return getHeapAddress() - getRegularTableSize();
2535     }
2536 
2537     /**
2538      * It returns the heap size for storing in the FITS, which is the larger of the actual space occupied by the current
2539      * heap, or the original heap size based on the header when the HDU was read from an input. In the former case it
2540      * will also include heap space reserved for future additions.
2541      * 
2542      * @return (byte) the size of the heap in the FITS file.
2543      * 
2544      * @see    #compact()
2545      * @see    #reserveHeapSpace(int)
2546      */
2547     private int getHeapSize() {
2548         synchronized (lock) {
2549             if (heap != null && heap.size() + heapReserve > heapFileSize) {
2550                 return heap.size() + heapReserve;
2551             }
2552             return heapFileSize;
2553         }
2554     }
2555 
2556     /**
2557      * @return the size of the heap -- including the offset from the end of the table data, and reserved space after.
2558      */
2559     long getParameterSize() {
2560         synchronized (lock) {
2561             return getHeapOffset() + getHeapSize();
2562         }
2563     }
2564 
2565     /**
2566      * Returns an empty row for the table. Such model rows are useful when low-level reading binary tables from an input
2567      * row-by-row. You can simply all {@link nom.tam.util.ArrayDataInput#readArrayFully(Object)} to populate it with
2568      * data from a stream. You may also use model rows to add additional rows to an existing table.
2569      * 
2570      * @return     a row that may be used for direct i/o to the table.
2571      * 
2572      * @deprecated (<i>for internal use</i>) Use {@link #getElement(int, int)} instead for low-level reading of tables
2573      *                 in deferred mode. Not recommended for uses because it requires a deep understanding of how data
2574      *                 (especially varialbe length columns) are represented in the FITS. Will reduce visibility to
2575      *                 private in the future.
2576      */
2577     @Deprecated
2578     public Object[] getModelRow() {
2579         Object[] modelRow = new Object[columns.size()];
2580         for (int i = 0; i < modelRow.length; i++) {
2581             ColumnDesc c = columns.get(i);
2582             if (c.fitsDimension() < 2) {
2583                 modelRow[i] = Array.newInstance(c.getTableBase(), c.getTableBaseCount());
2584             } else {
2585                 modelRow[i] = Array.newInstance(c.getTableBase(), c.fitsShape);
2586             }
2587         }
2588         return modelRow;
2589     }
2590 
2591     @Override
2592     public int getNCols() {
2593         return columns.size();
2594     }
2595 
2596     @Override
2597     public int getNRows() {
2598         synchronized (lock) {
2599             return nRow;
2600         }
2601     }
2602 
2603     /**
2604      * Reads a regular table element in the main table from the input. This method should never be called unless we have
2605      * a random-accessible input associated, which is a requirement for deferred read mode.
2606      * 
2607      * @param  o             The array element to populate
2608      * @param  c             the column descriptor
2609      * @param  row           the zero-based row index of the element
2610      * 
2611      * @throws IOException   If there was an I/O error accessing the input
2612      * @throws FitsException If there was some other error
2613      */
2614     private void readTableElement(Object o, ColumnDesc c, int row) throws IOException, FitsException {
2615         synchronized (lock) {
2616             @SuppressWarnings("resource")
2617             RandomAccess in = getRandomAccessInput();
2618 
2619             in.position(getFileOffset() + row * (long) rowLen + c.offset);
2620 
2621             if (c.isLogical()) {
2622                 in.readArrayFully(o);
2623             } else {
2624                 in.readImage(o);
2625             }
2626         }
2627     }
2628 
2629     /**
2630      * Returns an unprocessed element from the table as a 1D array of the elements that are stored in the regular table
2631      * data, whithout reslving heap references. That is this call will return flattened versions of multidimensional
2632      * arrays, and will return only the heap locator (offset and size) for variable-sized columns.
2633      * 
2634      * @return                   a particular element from the table but do no processing of this element (e.g.,
2635      *                               dimension conversion or extraction of variable length array elements/)
2636      *
2637      * @param      row           The row of the element.
2638      * @param      col           The column of the element.
2639      * 
2640      * @deprecated               (<i>for internal use</i>) Will reduce visibility in the future.
2641      *
2642      * @throws     FitsException if the operation failed
2643      */
2644     @Deprecated
2645     public Object getRawElement(int row, int col) throws FitsException {
2646         synchronized (lock) {
2647             if (!validRow(row) || !validColumn(col)) {
2648                 throw new TableException("No such element (" + row + "," + col + ")");
2649             }
2650 
2651             if (table == null) {
2652                 try {
2653                     ColumnDesc c = columns.get(col);
2654                     Object e = c.newInstance(1);
2655                     readTableElement(e, c, row);
2656                     return e;
2657                 } catch (IOException e) {
2658                     throw new FitsException("Error reading from input: " + e.getMessage(), e);
2659                 }
2660             }
2661 
2662             ensureData();
2663             return table.getElement(row, col);
2664         }
2665     }
2666 
2667     /**
2668      * Returns a table element as a Java array. Consider using the more Java-friendly {@link #get(int, int)} or one of
2669      * the scalar access methods with implicit type conversion support.
2670      * 
2671      * @see #get(int, int)
2672      * @see #getLogical(int, int)
2673      * @see #getNumber(int, int)
2674      * @see #getLong(int, int)
2675      * @see #getDouble(int, int)
2676      * @see #getString(int, int)
2677      */
2678     @Override
2679     public Object getElement(int row, int col) throws FitsException {
2680         return getElement(row, col, false);
2681     }
2682 
2683     /**
2684      * Returns a a table entry, with control over how FITS logical values are to be handled.
2685      * 
2686      * @param  row           zero-based row index
2687      * @param  col           zero-based column index
2688      * @param  isEnhanced    Whether logicals should be returned as {@link Boolean} (rather than <code>boolean</code>)
2689      *                           and complex values as {@link ComplexValue} (rather than <code>float[2]</code> or
2690      *                           <code>double[2]</code>), or arrays thereof. Methods prior to 1.18 should set this to
2691      *                           <code>false</code> for back compatible behavior.
2692      * 
2693      * @return               The entry as a primitive array, or {@link String}, {@link Boolean} or {@link ComplexValue},
2694      *                           or arrays thereof.
2695      * 
2696      * @throws FitsException If the requested element could not be accessed.
2697      */
2698     private Object getElement(int row, int col, boolean isEnhanced) throws FitsException {
2699         if (!validRow(row) || !validColumn(col)) {
2700             throw new TableException("No such element (" + row + "," + col + ")");
2701         }
2702 
2703         ColumnDesc c = columns.get(col);
2704         Object o = getRawElement(row, col);
2705 
2706         if (c.isVariableSize()) {
2707             return getFromHeap(c, o, isEnhanced);
2708         }
2709 
2710         o = fitsToJava1D(c, o, c.isBits() ? c.fitsCount : 0, isEnhanced);
2711 
2712         if (c.legacyShape.length > 1) {
2713             return ArrayFuncs.curl(o, c.legacyShape);
2714         }
2715 
2716         return o;
2717     }
2718 
2719     /**
2720      * Returns a table element as an array of the FITS storage type. Similar to the original
2721      * {@link #getElement(int, int)}, except that FITS logicals are returned as arrays of <code>Boolean</code> (rather
2722      * than <code>boolean</code>), bits are returned as arrays of <code>boolean</code>, and complex values are returned
2723      * as arrays of {@link ComplexValue} rather than arrays of <code>double[2]</code> or <code>float[2]</code>.
2724      * Singleton (scalar) table elements are not boxed to an enclosing Java type (unlike {@link #get(int, int)}), an
2725      * instead returned as arrays of just one element. For example, a single logical as a <code>Boolean[1]</code>, a
2726      * single float as a <code>float[1]</code> or a single double-precision complex value as
2727      * <code>ComplexValue[1]</code>.
2728      * 
2729      * @param  row zero-based row index
2730      * @param  col zero-based column index
2731      * 
2732      * @return     The table entry as an array of the stored Java type, without applying any type or quantization
2733      *                 conversions.
2734      * 
2735      * @see        #getArrayElementAs(int, int, Class)
2736      * @see        #get(int, int)
2737      * 
2738      * @since      1.20
2739      */
2740     public Object getArrayElement(int row, int col) {
2741         return getElement(row, col, true);
2742     }
2743 
2744     /**
2745      * <p>
2746      * Returns a numerical table element as an array of a specific underlying other numerical type. Similar
2747      * {@link #getArrayElement(int, int)} except that table entries are converted to the specified array type before
2748      * returning. If an integer-decimal conversion is involved, it will be performed through the column's quantizer (if
2749      * any) or else via a simple rounding as necessary.
2750      * </p>
2751      * <p>
2752      * For example, if you have an <code>short</code>-type column, and you want is an array of <code>double</code>
2753      * values that are represented by the 16-bit integers, then the conversion will use the column's quantizer scaling
2754      * and offset before returning the result either as an array of doubles, and the designated <code>short</code>
2755      * blanking values will be converted to NaNs.
2756      * </p>
2757      * 
2758      * @param  row                      zero-based row index
2759      * @param  col                      zero-based column index
2760      * @param  asType                   The desired underlying type, a primitive class or a {@link ComplexValue} type
2761      *                                      for appropriate numerical arrays (with a trailing Java dimension of 2 for
2762      *                                      the real/imaginary pairs).
2763      * 
2764      * @return                          An array of the desired type (e.g. <code>double[][]</code> if
2765      *                                      <code>asType</code> is <code>double.class</code> and the column contains 2D
2766      *                                      arrays of some numerical type).
2767      * 
2768      * @throws IllegalArgumentException if the numerical conversion is not possible for the given column type or if the
2769      *                                      type argument is not a supported numerical primitive or {@link ComplexValue}
2770      *                                      type.
2771      * 
2772      * @see                             #getArrayElement(int, int)
2773      * 
2774      * @since                           1.20
2775      */
2776     public Object getArrayElementAs(int row, int col, Class<?> asType) throws IllegalArgumentException {
2777         ColumnDesc c = getDescriptor(col);
2778         Object e = getElement(row, col, true);
2779         return asType.isAssignableFrom(c.getFitsBase()) ? e : ArrayFuncs.convertArray(e, asType, c.getQuantizer());
2780     }
2781 
2782     /**
2783      * <p>
2784      * Returns a table element using the usual Java boxing for primitive scalar (singleton) entries, or packaging
2785      * complex values as {@link ComplexValue}, or as appropriate primitive or object arrays. FITS string columns return
2786      * {@link String} values. Logical (<code>boolean</code> columns will return a {@link Boolean}, which may be
2787      * <code>null</code> if undefined (as per the FITS standard). Multibit FITS bits colums return arrays of
2788      * <code>boolean</code>.
2789      * </p>
2790      * <p>
2791      * As opposed to {@link #getElement(int, int)} scalar (singleton) values are not wrapped into primitive arrays, but
2792      * return either a singular object, such as a ({@link String}, or a {@link ComplexValue}, or a boxed Java primitive.
2793      * Thus, columns containing single <code>short</code> entries will return the selected element as a {@link Short},
2794      * or columns containing single <code>double</code> values will return the element as a {@link Double} and so on.
2795      * </p>
2796      * <p>
2797      * Array columns will return the expected arrays of primitive values, or arrays of one of the mentioned types. Note
2798      * however, that logical arrays are returned as arrays of {@link Boolean}, e.g. <code>Boolean[][]</code>, <b>not</b>
2799      * <code>boolean[][]</code>. This is because FITS allows <code>null</code> values for logicals beyond <code>
2800      * true</code> and <code>false</code>, which is reproduced by the boxed type, but not by the primitive type. FITS
2801      * columns of bits (generally preferrably to logicals if support for <code>null</code> values is not required) will
2802      * return arrays of <code>boolean</code>.
2803      * </p>
2804      * <p>
2805      * Columns containing multidimensional arrays, will return the expected multidimensional array of the above
2806      * mentioned types for the FITS storage type. You can then convert numerical arrays to other types as required for
2807      * your application via {@link ArrayFuncs#convertArray(Object, Class, Quantizer)}, including any appropriate
2808      * quantization for the colummn (see {@link ColumnDesc#getQuantizer()}).
2809      * </p>
2810      * 
2811      * @param  row           the zero-based row index
2812      * @param  col           the zero-based column index
2813      * 
2814      * @return               the element, either as a Java boxed type (for scalar entries), a singular Java Object, or
2815      *                           as a (possibly multi-dimensional) array of {@link String}, {@link Boolean},
2816      *                           {@link ComplexValue}, or primitives.
2817      * 
2818      * @throws FitsException if the element could not be obtained
2819      * 
2820      * @see                  #getNumber(int, int)
2821      * @see                  #getLogical(int, int)
2822      * @see                  #getString(int, int)
2823      * @see                  #getArrayElementAs(int, int, Class)
2824      * @see                  #set(int, int, Object)
2825      * 
2826      * @since                1.18
2827      */
2828     public Object get(int row, int col) throws FitsException {
2829         ColumnDesc c = columns.get(col);
2830         Object e = getElement(row, col, true);
2831         return (c.isSingleton() && e.getClass().isArray()) ? Array.get(e, 0) : e;
2832     }
2833 
2834     /**
2835      * Returns the numerical value, if possible, for scalar elements. Scalar numerical columns return the boxed type of
2836      * their primitive type. Thus, a column of <code>long</code> values will return {@link Long}, whereas a column of
2837      * <code>float</code> values will return a {@link Float}. Logical columns will return 1 if <code>true</code> or 0 if
2838      * <code>false</code>, or <code>null</code> if undefined. Array columns and other column types will throw an
2839      * exception.
2840      * 
2841      * @param  row                   the zero-based row index
2842      * @param  col                   the zero-based column index
2843      * 
2844      * @return                       the number value of the specified scalar entry
2845      * 
2846      * @throws FitsException         if the element could not be obtained
2847      * @throws ClassCastException    if the specified column in not a numerical scalar type.
2848      * @throws NumberFormatException if the it's a string column but the entry does not seem to be a number
2849      * 
2850      * @see                          #getDouble(int, int)
2851      * @see                          #getLong(int, int)
2852      * @see                          #get(int, int)
2853      * 
2854      * @since                        1.18
2855      */
2856     public final Number getNumber(int row, int col) throws FitsException, ClassCastException, NumberFormatException {
2857         Object o = get(row, col);
2858         if (o instanceof String) {
2859             try {
2860                 return Long.parseLong((String) o);
2861             } catch (NumberFormatException e) {
2862                 return Double.parseDouble((String) o);
2863             }
2864         }
2865         if (o instanceof Boolean) {
2866             return ((Boolean) o) ? 1 : 0;
2867         }
2868         return (Number) o;
2869     }
2870 
2871     /**
2872      * <p>
2873      * Returns the decimal value, if possible, of a scalar table entry. See {@link #getNumber(int, int)} for more
2874      * information on the conversion process.
2875      * </p>
2876      * <p>
2877      * Since version 1.20, if the column has a quantizer and stores integer elements, the conversion to double-precision
2878      * will account for the quantization of the column, if any, and will return NaN if the stored integer is the
2879      * designated blanking value (if any). To bypass quantization, you can use {@link #getNumber(int, int)} instead
2880      * followed by {@link Number#doubleValue()} to to get the stored integer values as a double.
2881      * </p>
2882      * 
2883      * @param  row                the zero-based row index
2884      * @param  col                the zero-based column index
2885      * 
2886      * @return                    the number value of the specified scalar entry
2887      * 
2888      * @throws FitsException      if the element could not be obtained
2889      * @throws ClassCastException if the specified column in not a numerical scalar type.
2890      * 
2891      * @see                       #getNumber(int, int)
2892      * @see                       #getLong(int, int)
2893      * @see                       #get(int, int)
2894      * @see                       ColumnDesc#getQuantizer()
2895      * 
2896      * @since                     1.18
2897      */
2898     public final double getDouble(int row, int col) throws FitsException, ClassCastException {
2899         Number n = getNumber(row, col);
2900 
2901         if (n == null) {
2902             return Double.NaN;
2903         }
2904 
2905         if (!(n instanceof Float || n instanceof Double)) {
2906             Quantizer q = getDescriptor(col).getQuantizer();
2907             if (q != null) {
2908                 return q.toDouble(n.longValue());
2909             }
2910         }
2911 
2912         return n.doubleValue();
2913     }
2914 
2915     /**
2916      * <p>
2917      * Returns a 64-bit integer value, if possible, of a scalar table entry. Boolean columns will return 1 if
2918      * <code>true</code> or 0 if <code>false</code>, or throw a {@link NullPointerException} if undefined. See
2919      * {@link #getNumber(int, int)} for more information on the conversion process of the stored data element.
2920      * </p>
2921      * <p>
2922      * Additionally, since version 1.20, if the column has a quantizer and stores floating-point elements, the
2923      * conversion to integer will include the quantization, and NaN values will be converted to the designated integer
2924      * blanking values. To bypass quantization, you can use {@link #getNumber(int, int)} instead followed by
2925      * {@link Number#longValue()} to to get the stored floating point values rounded directly to a long.
2926      * </p>
2927      * 
2928      * @param  row                   the zero-based row index
2929      * @param  col                   the zero-based column index
2930      * 
2931      * @return                       the 64-bit integer number value of the specified scalar table entry.
2932      * 
2933      * @throws FitsException         if the element could not be obtained
2934      * @throws ClassCastException    if the specified column in not a numerical scalar type.
2935      * @throws IllegalStateException if the column contains a undefined (blanking value), such as a {@link Double#NaN}
2936      *                                   when no quantizer is set for the column, or a {@link Boolean} <code>null</code>
2937      *                                   value.
2938      * 
2939      * @see                          #getNumber(int, int)
2940      * @see                          #getDouble(int, int)
2941      * @see                          #get(int, int)
2942      * 
2943      * @since                        1.18
2944      */
2945     public final long getLong(int row, int col) throws FitsException, ClassCastException, IllegalStateException {
2946         Number n = getNumber(row, col);
2947 
2948         if (n instanceof Float || n instanceof Double) {
2949             Quantizer q = getDescriptor(col).getQuantizer();
2950             if (q != null) {
2951                 return q.toLong(n.doubleValue());
2952             }
2953         }
2954 
2955         if (Double.isNaN(n.doubleValue())) {
2956             throw new IllegalStateException("Cannot convert NaN to long without Quantizer");
2957         }
2958         return n.longValue();
2959     }
2960 
2961     /**
2962      * Returns the boolean value, if possible, for scalar elements. It will will return<code>true</code>, or
2963      * <code>false</code>, or <code>null</code> if undefined. Numerical columns will return <code>null</code> if the
2964      * corresponding decimal value is NaN, or <code>false</code> if the value is 0, or else <code>true</code> for all
2965      * non-zero values (just like in C).
2966      * 
2967      * @param  row                the zero-based row index
2968      * @param  col                the zero-based column index
2969      * 
2970      * @return                    the boolean value of the specified scalar entry, or <code>null</code> if undefined.
2971      * 
2972      * @throws ClassCastException if the specified column in not a scalar boolean type.
2973      * @throws FitsException      if the element could not be obtained
2974      * 
2975      * @see                       #get(int, int)
2976      * 
2977      * @since                     1.18
2978      */
2979     @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null has specific meaning here")
2980     public final Boolean getLogical(int row, int col) throws FitsException, ClassCastException {
2981         Object o = get(row, col);
2982         if (o == null) {
2983             return null;
2984         }
2985 
2986         if (o instanceof Number) {
2987             Number n = (Number) o;
2988             if (Double.isNaN(n.doubleValue())) {
2989                 return null;
2990             }
2991             return n.longValue() != 0;
2992         }
2993 
2994         if (o instanceof Character) {
2995             char c = (Character) o;
2996             if (c == 'T' || c == 't' || c == '1') {
2997                 return true;
2998             }
2999             if (c == 'F' || c == 'f' || c == '0') {
3000                 return false;
3001             }
3002             return null;
3003         }
3004 
3005         if (o instanceof String) {
3006             return FitsUtil.parseLogical((String) o);
3007         }
3008 
3009         return (Boolean) o;
3010     }
3011 
3012     /**
3013      * Returns the string value, if possible, for scalar elements. All scalar columns will return the string
3014      * representation of their values, while <code>byte[]</code> and <code>char[]</code> are converted to appropriate
3015      * strings.
3016      * 
3017      * @param  row                the zero-based row index
3018      * @param  col                the zero-based column index
3019      * 
3020      * @return                    the string representatiof the specified table entry
3021      * 
3022      * @throws ClassCastException if the specified column contains array elements other than <code>byte[]</code> or
3023      *                                <code>char[]</code>
3024      * @throws FitsException      if the element could not be obtained
3025      * 
3026      * @see                       #get(int, int)
3027      * 
3028      * @since                     1.18
3029      */
3030     public final String getString(int row, int col) throws FitsException, ClassCastException {
3031         ColumnDesc c = columns.get(col);
3032         Object value = get(row, col);
3033 
3034         if (value == null) {
3035             return "null";
3036         }
3037 
3038         if (!value.getClass().isArray()) {
3039             return value.toString();
3040         }
3041 
3042         if (c.fitsDimension() > 1) {
3043             throw new ClassCastException("Cannot convert multi-dimensional array element to String");
3044         }
3045 
3046         if (value instanceof char[]) {
3047             return String.valueOf((char[]) value).trim();
3048         }
3049         if (value instanceof byte[]) {
3050             return AsciiFuncs.asciiString((byte[]) value).trim();
3051         }
3052 
3053         throw new ClassCastException("Cannot convert " + value.getClass().getName() + " to String.");
3054     }
3055 
3056     @Override
3057     public Object[] getRow(int row) throws FitsException {
3058         if (!validRow(row)) {
3059             throw new TableException("Invalid row index " + row + " in table of " + getNRows() + " rows");
3060         }
3061 
3062         Object[] data = new Object[columns.size()];
3063         for (int col = 0; col < data.length; col++) {
3064             data[col] = getElement(row, col);
3065         }
3066         return data;
3067     }
3068 
3069     /**
3070      * Returns the flattened (1D) size of elements in each column of this table. As of 1.18, this method returns a copy
3071      * ot the array used internally, which is safe to modify.
3072      * 
3073      * @return     an array with the byte sizes of each column
3074      * 
3075      * @deprecated (<i>for internal use</i>) Use {@link ColumnDesc#getElementCount()} instead. This one returns the
3076      *                 number of elements in the FITS representation, not in the java representation. For example, for
3077      *                 {@link String} entries, this returns the number of bytes stored, not the number of strings.
3078      *                 Similarly, for complex values it returns the number of components not the number of values.
3079      */
3080     @Deprecated
3081     public int[] getSizes() {
3082         int[] sizes = new int[columns.size()];
3083         for (int i = 0; i < sizes.length; i++) {
3084             sizes[i] = columns.get(i).getTableBaseCount();
3085         }
3086         return sizes;
3087     }
3088 
3089     /**
3090      * Returns the size of the regular table data, before the heap area.
3091      * 
3092      * @return the size of the regular table in bytes
3093      */
3094     private long getRegularTableSize() {
3095         synchronized (lock) {
3096             return (long) nRow * rowLen;
3097         }
3098     }
3099 
3100     @Override
3101     protected long getTrueSize() {
3102         return getRegularTableSize() + getParameterSize();
3103     }
3104 
3105     /**
3106      * Get the characters describing the base classes of the columns. As of 1.18, this method returns a copy ot the
3107      * array used internally, which is safe to modify.
3108      *
3109      * @return     An array of type characters (Java array types), one for each column.
3110      * 
3111      * @deprecated (<i>for internal use</i>) Use {@link ColumnDesc#getElementClass()} instead. Not very useful to users
3112      *                 since this returns the FITS primitive storage type for the data column.
3113      */
3114     @Deprecated
3115     public char[] getTypes() {
3116         char[] types = new char[columns.size()];
3117         for (int i = 0; i < columns.size(); i++) {
3118             types[i] = ElementType.forClass(columns.get(i).getTableBase()).type();
3119         }
3120         return types;
3121     }
3122 
3123     @Override
3124     public void setColumn(int col, Object o) throws FitsException {
3125         synchronized (lock) {
3126             ColumnDesc c = columns.get(col);
3127 
3128             if (c.isVariableSize()) {
3129                 Object[] array = (Object[]) o;
3130                 for (int i = 0; i < nRow; i++) {
3131                     Object p = putOnHeap(c, ArrayFuncs.flatten(array[i]), getRawElement(i, col));
3132                     setTableElement(i, col, p);
3133                 }
3134             } else {
3135                 setFlattenedColumn(col, o);
3136             }
3137         }
3138     }
3139 
3140     /**
3141      * Writes an element directly into the random accessible FITS file. Note, this call will not modify the table in
3142      * memory (if loaded). This method should never be called unless we have a valid encoder object that can handle the
3143      * writing, which is a requirement for deferred read mode.
3144      * 
3145      * @param  row         the zero-based row index
3146      * @param  col         the zero-based column index
3147      * @param  array       an array object containing primitive types, in FITS storage format. It may be
3148      *                         multi-dimensional.
3149      * 
3150      * @throws IOException the there was an error writing to the FITS output
3151      * 
3152      * @see                #setTableElement(int, int, Object)
3153      */
3154     @SuppressWarnings("resource")
3155     private void writeTableElement(int row, int col, Object array) throws IOException {
3156         synchronized (lock) {
3157             ColumnDesc c = columns.get(col);
3158             getRandomAccessInput().position(getFileOffset() + row * (long) rowLen + c.offset);
3159         }
3160         encoder.writeArray(array);
3161     }
3162 
3163     /**
3164      * Sets a table element to an array in the FITS storage format. If the data is in deferred mode it will write the
3165      * table entry directly into the file. Otherwise it will update the table entry in memory. For variable sized
3166      * column, the heap will always be updated in memory, so you may want to call {@link #rewrite()} when done updating
3167      * all entries.
3168      * 
3169      * @param  row           the zero-based row index
3170      * @param  col           the zero-based column index
3171      * @param  o             an array object containing primitive types, in FITS storage format. It may be
3172      *                           multi-dimensional.
3173      *
3174      * @throws FitsException if the array is invalid for the given column, or if the table could not be accessed in the
3175      *                           file / input.
3176      * 
3177      * @see                  #setTableElement(int, int, Object)
3178      * @see                  #getRawElement(int, int)
3179      */
3180     private void setTableElement(int row, int col, Object o) throws FitsException {
3181         synchronized (lock) {
3182             if (table == null) {
3183                 try {
3184                     writeTableElement(row, col, o);
3185                 } catch (IOException e) {
3186                     throw new FitsException(e.getMessage(), e);
3187                 }
3188             } else {
3189                 ensureData();
3190                 table.setElement(row, col, o);
3191             }
3192         }
3193     }
3194 
3195     /**
3196      * Consider using the more Java-friendly {@link #set(int, int, Object)} with implicit scalar type conversions.
3197      * 
3198      * @see #set(int, int, Object)
3199      */
3200     @Override
3201     public void setElement(int row, int col, Object o) throws FitsException {
3202         ColumnDesc c = columns.get(col);
3203         o = c.isVariableSize() ? putOnHeap(c, o, getRawElement(row, col)) : javaToFits1D(c, ArrayFuncs.flatten(o));
3204         setTableElement(row, col, o);
3205     }
3206 
3207     /**
3208      * <p>
3209      * The Swiss-army knife of setting table entries, including Java boxing, and with some support for automatic type
3210      * conversions. The argument may be one of the following type:
3211      * </p>
3212      * <ul>
3213      * <li>Scalar values -- any Java primitive with its boxed type, such as a {@link Double}, or a
3214      * {@link Character}.</li>
3215      * <li>A single {@link String} or {@link ComplexValue} object.
3216      * <li>An array (including multidimensional) of primitive types, or that of {@link Boolean}, {@link ComplexValue},
3217      * or {@link String}.</li>
3218      * </ul>
3219      * <p>
3220      * For array-type columns the argument needs to match the column type exactly. However, you may call
3221      * {@link ArrayFuncs#convertArray(Object, Class, Quantizer)} prior to setting values to convert arrays to the
3222      * desired numerical types, including the quantization that is appropriate for the column (see
3223      * {@link ColumnDesc#getQuantizer()}).
3224      * </p>
3225      * <p>
3226      * For scalar (single element) columns, automatic type conversions may apply, to make setting scalar columns more
3227      * flexible:
3228      * </p>
3229      * <ul>
3230      * <li>Any numerical column can take any {@link Number} value. The conversion is as if an explicit Java cast were
3231      * applied. For example, if setting a <code>double</code> value for a column of single <code>short</code> values it
3232      * as if a <code>(short)</code> cast were applied to the value.</li>
3233      * <li>Numerical colums can also take {@link Boolean} values which set the entry to 1, or 0, or to
3234      * {@link Double#isNaN()} (or the equivalent integer minimum value) if the argument is <code>null</code>. Numerical
3235      * columns can also set {@link String} values, by parsing the string according to the numerical type of the
3236      * column.</li>
3237      * <li>Logical columns can set {@link Boolean} values, including <code>null</code>values, but also any
3238      * {@link Number} type. In case of numbers, zero values map to <code>false</code> while definite non-zero values map
3239      * to <code>true</code>. {@link Double#isNaN()} maps to a <code>null</code> (or undefined) entry. Loginal columns
3240      * can be also set to the {@link String} values of 'true' or 'false', or to a {@link Character} of 'T'/'F' (or
3241      * equivalently '1'/'0') and 0 (undefined)</li>
3242      * <li>Singular string columns can be set to any scalar type owing to Java's {@link #toString()} method performing
3243      * the conversion, as long as the string representation fits into the size constraints (if any) for the string
3244      * column.</li>
3245      * </ul>
3246      * <p>
3247      * Additionally, scalar columns can take single-element array arguments, just like
3248      * {@link #setElement(int, int, Object)}.
3249      * </p>
3250      * 
3251      * @param  row                      the zero-based row index
3252      * @param  col                      the zero-based column index
3253      * @param  o                        the new value to set. For array columns this must match the Java array type
3254      *                                      exactly, but for scalar columns additional flexibility is provided for fuzzy
3255      *                                      type matching (see description above).
3256      * 
3257      * @throws FitsException            if the column could not be set
3258      * @throws IllegalArgumentException if the argument cannot be converted to a value for the specified column type.
3259      * 
3260      * @since                           1.18
3261      * 
3262      * @see                             #get(int, int)
3263      */
3264     public void set(int row, int col, Object o) throws FitsException, IllegalArgumentException {
3265         ColumnDesc c = columns.get(col);
3266 
3267         if (o == null) {
3268             // Only logicals and strings support 'null' values
3269             if (!c.isSingleton()) {
3270                 throw new TableException("No null values allowed for column of " + c.getLegacyBase() + " arrays.");
3271             } else if (c.isString()) {
3272                 setElement(row, col, "");
3273             } else {
3274                 setLogical(row, col, null);
3275             }
3276         } else if (o.getClass().isArray()) {
3277             Class<?> eType = ArrayFuncs.getBaseClass(o);
3278             if (!c.getFitsBase().isAssignableFrom(eType) && c.isNumeric()) {
3279                 o = ArrayFuncs.convertArray(o, c.getFitsBase(), c.getQuantizer());
3280             }
3281             setElement(row, col, o);
3282         } else if (o instanceof String) {
3283             setString(row, col, (String) o);
3284         } else if (!c.isSingleton()) {
3285             throw new TableException("Cannot set scalar values in non-scalar columns");
3286         } else if (c.isString()) {
3287             setElement(row, col, o.toString());
3288         } else if (o instanceof Boolean) {
3289             setLogical(row, col, (Boolean) o);
3290         } else if (o instanceof Character) {
3291             setCharacter(row, col, (Character) o);
3292         } else if (o instanceof Number) {
3293             setNumber(row, col, (Number) o);
3294         } else if (o instanceof ComplexValue) {
3295             setElement(row, col, o);
3296         } else {
3297             throw new IllegalArgumentException("Unsupported scalar type: " + o.getClass());
3298         }
3299     }
3300 
3301     /**
3302      * Sets a scalar table entry to the specified numerical value.
3303      * 
3304      * @param  row                the zero-based row index
3305      * @param  col                the zero-based column index
3306      * @param  value              the new number value
3307      * 
3308      * @throws ClassCastException if the specified column in not a numerical scalar type.
3309      * @throws FitsException      if the table element could not be altered
3310      * 
3311      * @see                       #getNumber(int, int)
3312      * @see                       #set(int, int, Object)
3313      * 
3314      * @since                     1.18
3315      */
3316     private void setNumber(int row, int col, Number value) throws FitsException, ClassCastException {
3317         ColumnDesc c = columns.get(col);
3318 
3319         // Already checked before calling...
3320         // if (!c.isSingleton()) {
3321         // throw new ClassCastException("Cannot set scalar value for array column " + col);
3322         // }
3323 
3324         if (c.isLogical()) {
3325             Boolean b = null;
3326             if (!Double.isNaN(value.doubleValue())) {
3327                 b = value.longValue() != 0;
3328             }
3329             setTableElement(row, col, new byte[] {FitsEncoder.byteForBoolean(b)});
3330             return;
3331         }
3332 
3333         Class<?> base = c.getLegacyBase();
3334 
3335         // quantize / unquantize as necessary...
3336         Quantizer q = c.getQuantizer();
3337 
3338         if (q != null) {
3339             boolean decimalBase = (base == float.class || base == double.class);
3340             boolean decimalValue = (value instanceof Float || value instanceof Double || value instanceof BigInteger
3341                     || value instanceof BigDecimal);
3342 
3343             if (decimalValue && !decimalBase) {
3344                 value = q.toLong(value.doubleValue());
3345             } else if (!decimalValue && decimalBase) {
3346                 value = q.toDouble(value.longValue());
3347             }
3348         }
3349 
3350         Object wrapped = null;
3351 
3352         if (base == byte.class) {
3353             wrapped = new byte[] {value.byteValue()};
3354         } else if (base == short.class) {
3355             wrapped = new short[] {value.shortValue()};
3356         } else if (base == int.class) {
3357             wrapped = new int[] {value.intValue()};
3358         } else if (base == long.class) {
3359             wrapped = new long[] {value.longValue()};
3360         } else if (base == float.class) {
3361             wrapped = new float[] {value.floatValue()};
3362         } else if (base == double.class) {
3363             wrapped = new double[] {value.doubleValue()};
3364         } else {
3365             // This could be a char based column...
3366             throw new ClassCastException("Cannot set number value for column of type " + base);
3367         }
3368 
3369         setTableElement(row, col, wrapped);
3370     }
3371 
3372     /**
3373      * Sets a boolean scalar table entry to the specified value.
3374      * 
3375      * @param  row                the zero-based row index
3376      * @param  col                the zero-based column index
3377      * @param  value              the new boolean value
3378      * 
3379      * @throws ClassCastException if the specified column in not a boolean scalar type.
3380      * @throws FitsException      if the table element could not be altered
3381      * 
3382      * @see                       #getLogical(int, int)
3383      * @see                       #set(int, int, Object)
3384      * 
3385      * @since                     1.18
3386      */
3387     private void setLogical(int row, int col, Boolean value) throws FitsException, ClassCastException {
3388         ColumnDesc c = columns.get(col);
3389 
3390         // Already checked before calling...
3391         // if (!c.isSingleton()) {
3392         // throw new ClassCastException("Cannot set scalar value for array column " + col);
3393         // }
3394 
3395         if (c.isLogical()) {
3396             setTableElement(row, col, new byte[] {FitsEncoder.byteForBoolean(value)});
3397         } else if (c.getLegacyBase() == char.class) {
3398             setTableElement(row, col, new char[] {value == null ? '\0' : (value ? 'T' : 'F')});
3399         } else {
3400             setNumber(row, col, value == null ? Double.NaN : (value ? 1 : 0));
3401         }
3402     }
3403 
3404     /**
3405      * Sets a Unicode character scalar table entry to the specified value.
3406      * 
3407      * @param  row                the zero-based row index
3408      * @param  col                the zero-based column index
3409      * @param  value              the new Unicode character value
3410      * 
3411      * @throws ClassCastException if the specified column in not a boolean scalar type.
3412      * @throws FitsException      if the table element could not be altered
3413      * 
3414      * @see                       #getString(int, int)
3415      * 
3416      * @since                     1.18
3417      */
3418     private void setCharacter(int row, int col, Character value) throws FitsException, ClassCastException {
3419         ColumnDesc c = columns.get(col);
3420 
3421         // Already checked before calling...
3422         // if (!c.isSingleton()) {
3423         // throw new IllegalArgumentException("Cannot set scalar value for array column " + col);
3424         // }
3425 
3426         if (c.isLogical()) {
3427             setLogical(row, col, FitsUtil.parseLogical(value.toString()));
3428         } else if (c.fitsBase == char.class) {
3429             setTableElement(row, col, new char[] {value});
3430         } else if (c.fitsBase == byte.class) {
3431             setTableElement(row, col, new byte[] {(byte) (value & FitsIO.BYTE_MASK)});
3432         } else {
3433             throw new ClassCastException("Cannot convert char value to " + c.fitsBase.getName());
3434         }
3435     }
3436 
3437     /**
3438      * Sets a table entry to the specified string value. Scalar column will attempt to parse the value, while
3439      * <code>byte[]</code> and <code>char[]</code> type columns will convert the string provided the string's length
3440      * does not exceed the entry size for these columns (the array elements will be padded with zeroes). Note, that
3441      * scalar <code>byte</code> columns will parse the string as a number (not as a single ASCII character).
3442      * 
3443      * @param  row                      the zero-based row index
3444      * @param  col                      the zero-based column index
3445      * @param  value                    the new boolean value
3446      * 
3447      * @throws ClassCastException       if the specified column is not a scalar type, and neither it is a
3448      *                                      <code>byte[]</code> or <code>char[]</code> column.
3449      * @throws IllegalArgumentException if the String is too long to contain in the column.
3450      * @throws NumberFormatException    if the numerical value could not be parsed.
3451      * @throws FitsException            if the table element could not be altered
3452      * 
3453      * @see                             #getString(int, int)
3454      * @see                             #set(int, int, Object)
3455      * 
3456      * @since                           1.18
3457      */
3458     private void setString(int row, int col, String value)
3459             throws FitsException, ClassCastException, IllegalArgumentException, NumberFormatException {
3460         ColumnDesc c = columns.get(col);
3461 
3462         // Already checked before calling...
3463         // if (!c.isSingleton()) {
3464         // throw new IllegalArgumentException("Cannot set scalar value for array column " + col);
3465         // }
3466 
3467         if (c.isLogical()) {
3468             setLogical(row, col, FitsUtil.parseLogical(value));
3469         } else if (value.length() == 1) {
3470             setCharacter(row, col, value.charAt(0));
3471         } else if (c.fitsDimension() > 1) {
3472             throw new ClassCastException("Cannot convert String to multi-dimensional array");
3473         } else if (c.fitsDimension() == 1) {
3474             if (c.fitsBase != char.class && c.fitsBase != byte.class) {
3475                 throw new ClassCastException("Cannot cast String to " + c.fitsBase.getName());
3476             }
3477             int len = c.isVariableSize() ? value.length() : c.fitsCount;
3478             if (value.length() > len) {
3479                 throw new IllegalArgumentException("String size " + value.length() + " exceeds entry size of " + len);
3480             }
3481             if (c.fitsBase == char.class) {
3482                 setTableElement(row, col, Arrays.copyOf(value.toCharArray(), len));
3483             } else {
3484                 setTableElement(row, col, FitsUtil.stringToByteArray(value, len));
3485             }
3486         } else {
3487             try {
3488                 setNumber(row, col, Long.parseLong(value));
3489             } catch (NumberFormatException e) {
3490                 setNumber(row, col, Double.parseDouble(value));
3491             }
3492         }
3493     }
3494 
3495     /**
3496      * @deprecated               (<i>for internal use</i>) It may be reduced to private visibility in the future. Sets a
3497      *                               column with the data already flattened.
3498      *
3499      * @param      col           The index of the column to be replaced.
3500      * @param      data          The new data array. This should be a one-d primitive array.
3501      *
3502      * @throws     FitsException Thrown if the type of length of the replacement data differs from the original.
3503      */
3504     @Deprecated
3505     public void setFlattenedColumn(int col, Object data) throws FitsException {
3506         synchronized (lock) {
3507             ensureData();
3508 
3509             Object oldCol = table.getColumn(col);
3510             if (data.getClass() != oldCol.getClass() || Array.getLength(data) != Array.getLength(oldCol)) {
3511                 throw new TableException("Replacement column mismatch at column:" + col);
3512             }
3513             table.setColumn(col, javaToFits1D(columns.get(col), data));
3514         }
3515     }
3516 
3517     @Override
3518     public void setRow(int row, Object[] data) throws FitsException {
3519         ensureData();
3520 
3521         if (data.length != getNCols()) {
3522             throw new TableException("Mismatched number of columns: " + data.length + ", expected " + getNCols());
3523         }
3524 
3525         for (int col = 0; col < data.length; col++) {
3526             set(row, col, data[col]);
3527         }
3528     }
3529 
3530     /**
3531      * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a
3532      *                 new HDU after column deletions, and then edit the new header as necessary to incorporate custom
3533      *                 entries. May be removed from the API in the future.
3534      */
3535     @Deprecated
3536     @Override
3537     public void updateAfterDelete(int oldNcol, Header hdr) throws FitsException {
3538         synchronized (lock) {
3539             hdr.addValue(Standard.NAXIS1, rowLen);
3540             int l = 0;
3541             for (ColumnDesc d : columns) {
3542                 d.offset = l;
3543                 l += d.rowLen();
3544             }
3545         }
3546     }
3547 
3548     @SuppressWarnings("resource")
3549     @Override
3550     public void write(ArrayDataOutput os) throws FitsException {
3551         synchronized (lock) {
3552             try {
3553                 if (isDeferred() && os == getRandomAccessInput()) {
3554                     // It it's a deferred mode re-write, then data were edited in place if at all,
3555                     // so we can skip the main table.
3556                     ((RandomAccess) os).skipAllBytes(getRegularTableSize());
3557                 } else {
3558                     // otherwise make sure we loaded all data before writing to the output
3559                     ensureData();
3560 
3561                     // Write the regular table (if any)
3562                     if (getRegularTableSize() > 0) {
3563                         table.write(os);
3564                     }
3565                 }
3566 
3567                 // Now check if we need to write the heap
3568                 if (getParameterSize() > 0) {
3569                     for (long rem = getHeapOffset(); rem > 0;) {
3570                         byte[] b = new byte[(int) Math.min(getHeapOffset(), 1 << Short.SIZE)];
3571                         os.write(b);
3572                         rem -= b.length;
3573                     }
3574 
3575                     getHeap().write(os);
3576 
3577                     if (heapReserve > 0) {
3578                         byte[] b = new byte[heapReserve];
3579                         os.write(b);
3580                     }
3581                 }
3582 
3583                 FitsUtil.pad(os, getTrueSize(), (byte) 0);
3584             } catch (IOException e) {
3585                 throw new FitsException("Unable to write table:" + e, e);
3586             }
3587         }
3588     }
3589 
3590     /**
3591      * Returns the heap offset component from a pointer.
3592      * 
3593      * @param  p the pointer, either a <code>int[2]</code> or a <code>long[2]</code>.
3594      * 
3595      * @return   the offset component from the pointer
3596      */
3597     private long getPointerOffset(Object p) {
3598         return (p instanceof long[]) ? ((long[]) p)[1] : ((int[]) p)[1];
3599     }
3600 
3601     /**
3602      * Returns the number of elements reported in a heap pointer.
3603      * 
3604      * @param  p the pointer, either a <code>int[2]</code> or a <code>long[2]</code>.
3605      * 
3606      * @return   the element count component from the pointer
3607      */
3608     private long getPointerCount(Object p) {
3609         return (p instanceof long[]) ? ((long[]) p)[0] : ((int[]) p)[0];
3610     }
3611 
3612     /**
3613      * Puts a FITS data array onto our heap, returning its locator pointer. The data will overwrite the previous heap
3614      * entry, if provided, so long as the new data fits in the same place. Otherwise the new data is placed at the end
3615      * of the heap.
3616      * 
3617      * @param  c             The column descriptor, specifying the data type
3618      * @param  o             The variable-length data
3619      * @param  oldPointer    The heap pointer, where this element was stored on the heap before, or <code>null</code> if
3620      *                           we aren't replacing an earlier entry.
3621      * 
3622      * @return               the heap pointer information, either <code>int[2]</code> or else a <code>long[2]</code>
3623      * 
3624      * @throws FitsException if the data could not be accessed in full from the heap.
3625      */
3626     private Object putOnHeap(ColumnDesc c, Object o, Object oldPointer) throws FitsException {
3627         return putOnHeap(getHeap(), c, o, oldPointer);
3628     }
3629 
3630     /**
3631      * Puts a FITS data array onto a specific heap, returning its locator pointer. The data will overwrite the previous
3632      * heap entry, if provided, so long as the new data fits in the same place. Otherwise the new data is placed at the
3633      * end of the heap.
3634      * 
3635      * @param  h             The heap object to use.
3636      * @param  c             The column descriptor, specifying the data type
3637      * @param  o             The variable-length data in Java form.
3638      * @param  oldPointer    The heap pointer, where this element was stored on the heap before, or <code>null</code> if
3639      *                           we aren't replacing an earlier entry.
3640      * 
3641      * @return               the heap pointer information, either <code>int[2]</code> or else a <code>long[2]</code>
3642      * 
3643      * @throws FitsException if the data could not be accessed in full from the heap.
3644      */
3645     private Object putOnHeap(FitsHeap h, ColumnDesc c, Object o, Object oldPointer) throws FitsException {
3646         // Flatten data for heap
3647         o = ArrayFuncs.flatten(o);
3648 
3649         // By default put data at the end of the heap;
3650         int off = h.size();
3651 
3652         // The number of Java elements is the same as the number of FITS elements, except for strings and complex
3653         // numbers
3654         int len = (c.isComplex() || c.isString()) ? -1 : Array.getLength(o);
3655 
3656         // Convert to FITS storage array
3657         o = javaToFits1D(c, o);
3658 
3659         // For complex values and strings, determine length from converted object....
3660         if (len < 0) {
3661             len = Array.getLength(o);
3662 
3663             // If complex in primitive 1D form, then length is half the number of elements.
3664             if (c.isComplex() && o.getClass().getComponentType().isPrimitive()) {
3665                 len >>>= 1;
3666             }
3667         }
3668 
3669         if (oldPointer != null) {
3670             if (len <= getPointerCount(oldPointer)) {
3671                 // Write data back at the old heap location
3672                 off = (int) getPointerOffset(oldPointer);
3673             }
3674         }
3675 
3676         h.putData(o, off);
3677 
3678         return c.hasLongPointers() ? new long[] {len, off} : new int[] {len, off};
3679     }
3680 
3681     /**
3682      * Returns a FITS data array from the heap
3683      * 
3684      * @param  c             The column descriptor, specifying the data type
3685      * @param  p             The heap pointer, either <code>int[2]</code> or else a <code>long[2]</code>
3686      * @param  isEnhanced    Whether logicals should be returned as {@link Boolean} (rather than <code>boolean</code>)
3687      *                           and complex values as {@link ComplexValue} (rather than <code>float[2]</code> or
3688      *                           <code>double[2]</code>), or arrays thereof. Methods prior to 1.18 should set this to
3689      *                           <code>false</code> for back compatible behavior.
3690      * 
3691      * @return               the FITS array object retrieved from the heap
3692      * 
3693      * @throws FitsException if the data could not be accessed in full from the heap.
3694      */
3695     protected Object getFromHeap(ColumnDesc c, Object p, boolean isEnhanced) throws FitsException {
3696         long len = getPointerCount(p);
3697         long off = getPointerOffset(p);
3698 
3699         if (off > Integer.MAX_VALUE || len > Integer.MAX_VALUE) {
3700             throw new FitsException("Data located beyond 32-bit accessible heap limit: off=" + off + ", len=" + len);
3701         }
3702 
3703         Object e = null;
3704 
3705         if (c.isComplex()) {
3706             e = Array.newInstance(c.getFitsBase(), (int) len, 2);
3707         } else {
3708             e = Array.newInstance(c.getFitsBase(), c.getFitsBaseCount((int) len));
3709         }
3710 
3711         readHeap(off, e);
3712 
3713         return fitsToJava1D(c, e, (int) len, isEnhanced);
3714     }
3715 
3716     /**
3717      * Convert Java arrays to their FITS representation. Transformation include boolean &rightarrow; 'T'/'F' or '\0';
3718      * Strings &rightarrow; byte arrays; variable length arrays &rightarrow; pointers (after writing data to heap).
3719      *
3720      * @param  c             The column descritor
3721      * @param  o             A one-dimensional Java array
3722      *
3723      * @return               An one-dimensional array with values as stored in FITS.
3724      * 
3725      * @throws FitsException if the operation failed
3726      */
3727     private static Object javaToFits1D(ColumnDesc c, Object o) throws FitsException {
3728 
3729         if (c.isBits()) {
3730             if (o instanceof Boolean && c.isSingleton()) {
3731                 // Scalar boxed boolean...
3732                 return FitsUtil.bitsToBytes(new boolean[] {(Boolean) o});
3733             }
3734             return FitsUtil.bitsToBytes((boolean[]) o);
3735         }
3736 
3737         if (c.isLogical()) {
3738             // Convert true/false to 'T'/'F', or null to '\0'
3739             return FitsUtil.booleansToBytes(o);
3740         }
3741 
3742         if (c.isComplex()) {
3743             if (o instanceof ComplexValue || o instanceof ComplexValue[]) {
3744                 return ArrayFuncs.complexToDecimals(o, c.fitsBase);
3745             }
3746         }
3747 
3748         if (c.isString()) {
3749             // Convert strings to array of bytes.
3750             if (o == null) {
3751                 if (c.isVariableSize()) {
3752                     return new byte[0];
3753                 }
3754 
3755                 return Array.newInstance(byte.class, c.fitsShape);
3756             }
3757 
3758             if (o instanceof String) {
3759                 int l = c.getStringLength();
3760                 if (l < 0) {
3761                     // Not fixed width, write the whole string.
3762                     l = ((String) o).length();
3763                 }
3764                 return FitsUtil.stringToByteArray((String) o, l);
3765             }
3766 
3767             if (c.isVariableSize() && c.delimiter != 0) {
3768                 // Write variable-length string arrays in delimited form
3769 
3770                 for (String s : (String[]) o) {
3771                     // We set the string length to that of the longest element + 1
3772                     c.setStringLength(Math.max(c.stringLength, s == null ? 1 : s.length() + 1));
3773                 }
3774 
3775                 return FitsUtil.stringsToDelimitedBytes((String[]) o, c.getStringLength(), c.delimiter);
3776             }
3777 
3778             // Fixed length substring array (not delimited).
3779             // For compatibility with tools that do not process array dimension, ASCII NULL should not
3780             // be used between components (permissible only at the end of all strings)
3781             return FitsUtil.stringsToByteArray((String[]) o, c.getStringLength(), FitsUtil.BLANK_SPACE);
3782         }
3783 
3784         return ArrayFuncs.objectToArray(o, true);
3785     }
3786 
3787     /**
3788      * Converts from the FITS representation of data to their basic Java array representation.
3789      *
3790      * @param  c             The column descritor
3791      * @param  o             A one-dimensional array of values as stored in FITS
3792      * @param  bits          A bit count for bit arrays (otherwise unused).
3793      * @param  isEnhanced    Whether logicals should be returned as {@link Boolean} (rather than <code>boolean</code>)
3794      *                           and complex values as {@link ComplexValue} (rather than <code>float[2]</code> or
3795      *                           <code>double[2]</code>), or arrays thereof. Methods prior to 1.18 should set this to
3796      *                           <code>false</code> for back compatible behavior.
3797      *
3798      * @return               A {@link String} or a one-dimensional array with the matched basic Java type
3799      * 
3800      * @throws FitsException if the operation failed
3801      */
3802     private Object fitsToJava1D(ColumnDesc c, Object o, int bits, boolean isEnhanced) {
3803 
3804         if (c.isBits()) {
3805             return FitsUtil.bytesToBits((byte[]) o, bits);
3806         }
3807 
3808         if (c.isLogical()) {
3809             return isEnhanced ? FitsUtil.bytesToBooleanObjects(o) : FitsUtil.byteToBoolean((byte[]) o);
3810         }
3811 
3812         if (c.isComplex() && isEnhanced) {
3813             return ArrayFuncs.decimalsToComplex(o);
3814         }
3815 
3816         if (c.isString()) {
3817             byte[] bytes = (byte[]) o;
3818 
3819             int len = c.getStringLength();
3820 
3821             if (c.isVariableSize()) {
3822                 if (c.delimiter != 0) {
3823                     // delimited array of strings
3824                     return FitsUtil.delimitedBytesToStrings(bytes, c.getStringLength(), c.delimiter);
3825                 }
3826             }
3827 
3828             // If fixed or variable length arrays of strings...
3829             if (c.isSingleton()) {
3830                 // Single fixed string -- get it all but trim trailing spaces
3831                 return FitsUtil.extractString(bytes, new ParsePosition(0), bytes.length, FitsUtil.ASCII_NULL);
3832             }
3833 
3834             // Array of fixed-length strings -- we trim trailing spaces in each component
3835             String[] s = new String[bytes.length / len];
3836             for (int i = 0; i < s.length; i++) {
3837                 s[i] = FitsUtil.extractString(bytes, new ParsePosition(i * len), len, FitsUtil.ASCII_NULL);
3838             }
3839             return s;
3840         }
3841 
3842         return o;
3843     }
3844 
3845     /**
3846      * Create a column table with the specified number of rows. This is used when we defer instantiation of the
3847      * ColumnTable until the user requests data from the table.
3848      * 
3849      * @param  rows          the number of rows to allocate
3850      * 
3851      * @throws FitsException if the operation failed
3852      */
3853     protected void createTable(int rows) throws FitsException {
3854         synchronized (lock) {
3855             int nfields = columns.size();
3856             Object[] data = new Object[nfields];
3857             int[] sizes = new int[nfields];
3858             for (int i = 0; i < nfields; i++) {
3859                 ColumnDesc c = columns.get(i);
3860                 sizes[i] = c.getTableBaseCount();
3861                 data[i] = c.newInstance(rows);
3862             }
3863 
3864             table = createColumnTable(data, sizes);
3865             nRow = rows;
3866         }
3867     }
3868 
3869     /**
3870      * Sets the input to use for reading (and possibly writing) this table. If the input implements
3871      * {@link ReadWriteAccess}, then it can be used for both reading and (re)writing the data, including editing in
3872      * deferred mode.
3873      * 
3874      * @param in The input from which we can read the table data.
3875      */
3876     private void setInput(ArrayDataInput in) {
3877         encoder = (in instanceof ReadWriteAccess) ? new FitsEncoder((ReadWriteAccess) in) : null;
3878     }
3879 
3880     @Override
3881     public void read(ArrayDataInput in) throws FitsException {
3882         setInput(in);
3883         super.read(in);
3884     }
3885 
3886     @Override
3887     protected void loadData(ArrayDataInput in) throws IOException, FitsException {
3888         synchronized (lock) {
3889             setInput(in);
3890             createTable(nRow);
3891             readTrueData(in);
3892         }
3893     }
3894 
3895     /**
3896      * Extracts a column descriptor from the FITS header for a given column index
3897      * 
3898      * @param  header        the FITS header containing the column description(s)
3899      * @param  col           zero-based column index
3900      * 
3901      * @return               the Descriptor for that column.
3902      * 
3903      * @throws FitsException if the header deswcription is invalid or incomplete
3904      */
3905     public static ColumnDesc getDescriptor(Header header, int col) throws FitsException {
3906         String tform = header.getStringValue(Standard.TFORMn.n(col + 1));
3907 
3908         if (tform == null) {
3909             throw new FitsException("Missing TFORM" + (col + 1));
3910         }
3911 
3912         int count = 1;
3913         char type = 0;
3914 
3915         ParsePosition pos = new ParsePosition(0);
3916 
3917         try {
3918             count = AsciiFuncs.parseInteger(tform, pos);
3919         } catch (Exception e) {
3920             // Keep going...
3921         }
3922 
3923         try {
3924             type = Character.toUpperCase(AsciiFuncs.extractChar(tform, pos));
3925         } catch (Exception e) {
3926             throw new FitsException("Missing data type in TFORM: [" + tform + "]");
3927         }
3928 
3929         ColumnDesc c = new ColumnDesc();
3930 
3931         if (header.containsKey(Standard.TTYPEn.n(col + 1))) {
3932             c.name(header.getStringValue(Standard.TTYPEn.n(col + 1)));
3933         }
3934 
3935         if (type == POINTER_INT || type == POINTER_LONG) {
3936             // Variable length column...
3937             c.setVariableSize(type == POINTER_LONG);
3938 
3939             // Get the data type...
3940             try {
3941                 type = Character.toUpperCase(AsciiFuncs.extractChar(tform, pos));
3942             } catch (Exception e) {
3943                 throw new FitsException("Missing variable-length data type in TFORM: [" + tform + "]");
3944             }
3945         }
3946 
3947         // The special types...
3948         if (type == 'C' || type == 'M') {
3949             c.isComplex = true;
3950         } else if (type == 'X') {
3951             c.isBits = true;
3952         }
3953 
3954         if (!c.setFitsType(type)) {
3955             throw new FitsException("Invalid type '" + type + "' in column:" + col);
3956         }
3957 
3958         if (!c.isVariableSize()) {
3959             // Fixed sized column...
3960             int[] dims = parseTDims(header.getStringValue(Standard.TDIMn.n(col + 1)));
3961 
3962             if (dims == null) {
3963                 c.setFitsShape((count == 1 && type != 'A') ? SINGLETON_SHAPE : new int[] {count});
3964                 c.stringLength = -1; // T.B.D. further below...
3965             } else {
3966                 c.setFitsShape(dims);
3967             }
3968         }
3969 
3970         if (c.isString()) {
3971             // For vairable-length columns or of TDIM was not defined determine substring length from TFORM.
3972             c.parseSubstringConvention(tform, pos, c.getStringLength() < 0);
3973         }
3974 
3975         // Force to use the count in the header, even if it does not match up with the dimension otherwise.
3976         c.fitsCount = count;
3977 
3978         c.quant = Quantizer.fromTableHeader(header, col);
3979         if (c.quant.isDefault()) {
3980             c.quant = null;
3981         }
3982 
3983         return c;
3984     }
3985 
3986     /**
3987      * Process one column from a FITS Header.
3988      * 
3989      * @throws FitsException if the operation failed
3990      */
3991     private int processCol(Header header, int col, int offset) throws FitsException {
3992         ColumnDesc c = getDescriptor(header, col);
3993         c.offset = offset;
3994         columns.add(c);
3995 
3996         return c.rowLen();
3997     }
3998 
3999     /**
4000      * @deprecated (<i>for internal use</i>) Used Only by {@link nom.tam.image.compression.hdu.CompressedTableData} so
4001      *                 it would make a better private method in there.. `
4002      */
4003     @Deprecated
4004     protected void addByteVaryingColumn() {
4005         addColumn(ColumnDesc.createForVariableSize(byte.class));
4006     }
4007 
4008     /**
4009      * @deprecated (<i>for internal use</i>) This method should have visibility reduced to private
4010      */
4011     @SuppressWarnings("javadoc")
4012     @Deprecated
4013     protected ColumnTable<?> createColumnTable(Object[] arrCol, int[] sizes) throws TableException {
4014         return new ColumnTable<>(arrCol, sizes);
4015     }
4016 
4017     /**
4018      * Returns the heap, after initializing it from the input as necessary
4019      * 
4020      * @return               the initialized heap
4021      * 
4022      * @throws FitsException if we had trouble initializing it from the input.
4023      */
4024     @SuppressWarnings("resource")
4025     private FitsHeap getHeap() throws FitsException {
4026         synchronized (lock) {
4027             if (heap == null) {
4028                 readHeap(getRandomAccessInput());
4029             }
4030             return heap;
4031         }
4032     }
4033 
4034     /**
4035      * Reads an array from the heap. Subclasses may override this, for example to provide read-only access to a related
4036      * table's heap area.
4037      * 
4038      * @param  offset        the heap offset
4039      * @param  array         the array to populate from the heap area
4040      * 
4041      * @throws FitsException if there was an issue accessing the heap
4042      */
4043     protected void readHeap(long offset, Object array) throws FitsException {
4044         getHeap().getData((int) offset, array);
4045     }
4046 
4047     /**
4048      * Read the heap which contains the data for variable length arrays. A. Kovacs (4/1/08) Separated heap reading, s.t.
4049      * the heap can be properly initialized even if in deferred read mode. columnToArray() checks and initializes the
4050      * heap as necessary.
4051      *
4052      * @param      input         stream to read from.
4053      *
4054      * @throws     FitsException if the heap could not be read from the stream
4055      * 
4056      * @deprecated               (<i>for internal use</i>) unused.
4057      */
4058     @Deprecated
4059     protected void readHeap(ArrayDataInput input) throws FitsException {
4060         synchronized (lock) {
4061             if (input instanceof RandomAccess) {
4062                 FitsUtil.reposition(input, getFileOffset() + getHeapAddress());
4063             }
4064 
4065             heap = new FitsHeap(heapFileSize);
4066             if (input != null) {
4067                 heap.read(input);
4068             }
4069         }
4070     }
4071 
4072     /**
4073      * Read table, heap and padding
4074      *
4075      * @param  i             the stream to read the data from.
4076      *
4077      * @throws FitsException if the reading failed
4078      */
4079     protected void readTrueData(ArrayDataInput i) throws FitsException {
4080         try {
4081             synchronized (lock) {
4082                 table.read(i);
4083                 i.skipAllBytes(getHeapOffset());
4084                 if (heap == null) {
4085                     readHeap(i);
4086                 }
4087             }
4088         } catch (IOException e) {
4089             throw new FitsException("Error reading binary table data:" + e, e);
4090         }
4091     }
4092 
4093     /**
4094      * Check if the column number is valid.
4095      *
4096      * @param  j The Java index (first=0) of the column to check.
4097      *
4098      * @return   <code>true</code> if the column is valid
4099      */
4100     protected boolean validColumn(int j) {
4101         return j >= 0 && j < getNCols();
4102     }
4103 
4104     /**
4105      * Check to see if this is a valid row.
4106      *
4107      * @param  i The Java index (first=0) of the row to check.
4108      *
4109      * @return   <code>true</code> if the row is valid
4110      */
4111     protected boolean validRow(int i) {
4112         return getNRows() > 0 && i >= 0 && i < getNRows();
4113     }
4114 
4115     /**
4116      * @deprecated (<i>for internal use</i>) Visibility should be reduced to protected.
4117      */
4118     @Deprecated
4119     @Override
4120     public void fillHeader(Header h) throws FitsException {
4121         fillHeader(h, true);
4122     }
4123 
4124     /**
4125      * Fills (updates) the essential header description of this table in the header, optionally updating the essential
4126      * column descriptions also if desired.
4127      * 
4128      * @param  h             The FITS header to populate
4129      * @param  updateColumns Whether to update the essential column descriptions also
4130      * 
4131      * @throws FitsException if there was an error accessing the header.
4132      */
4133     void fillHeader(Header h, boolean updateColumns) throws FitsException {
4134         h.deleteKey(Standard.SIMPLE);
4135         h.deleteKey(Standard.EXTEND);
4136 
4137         Standard.context(BinaryTable.class);
4138 
4139         Cursor<String, HeaderCard> c = h.iterator();
4140         c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_BINTABLE));
4141         c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue()));
4142         c.add(HeaderCard.create(Standard.NAXIS, 2));
4143 
4144         synchronized (lock) {
4145             c.add(HeaderCard.create(Standard.NAXIS1, rowLen));
4146             c.add(HeaderCard.create(Standard.NAXIS2, nRow));
4147         }
4148 
4149         if (h.getLongValue(Standard.PCOUNT, -1L) < getParameterSize()) {
4150             c.add(HeaderCard.create(Standard.PCOUNT, getParameterSize()));
4151         }
4152 
4153         c.add(HeaderCard.create(Standard.GCOUNT, 1));
4154         c.add(HeaderCard.create(Standard.TFIELDS, columns.size()));
4155 
4156         if (getHeapOffset() == 0) {
4157             h.deleteKey(Standard.THEAP);
4158         } else {
4159             c.add(HeaderCard.create(Standard.THEAP, getHeapAddress()));
4160         }
4161 
4162         if (updateColumns) {
4163             for (int i = 0; i < columns.size(); i++) {
4164                 c.setKey(Standard.TFORMn.n(i + 1).key());
4165                 fillForColumn(h, c, i);
4166             }
4167         }
4168 
4169         Standard.context(null);
4170     }
4171 
4172     /**
4173      * Update the header to reflect the details of a given column.
4174      *
4175      * @throws FitsException if the operation failed
4176      */
4177     void fillForColumn(Header header, Cursor<String, HeaderCard> hc, int col) throws FitsException {
4178         ColumnDesc c = columns.get(col);
4179 
4180         try {
4181             Standard.context(BinaryTable.class);
4182 
4183             if (c.name() != null) {
4184                 hc.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), c.name()));
4185             }
4186 
4187             hc.add(HeaderCard.create(Standard.TFORMn.n(col + 1), c.getTFORM()));
4188 
4189             String tdim = c.getTDIM();
4190             if (tdim != null) {
4191                 hc.add(HeaderCard.create(Standard.TDIMn.n(col + 1), tdim));
4192             }
4193 
4194             if (c.quant != null) {
4195                 c.quant.editTableHeader(header, col);
4196             }
4197 
4198         } finally {
4199             Standard.context(null);
4200         }
4201     }
4202 
4203     /**
4204      * Returns the column descriptor of a given column in this table
4205      * 
4206      * @param  column                         the zero-based column index
4207      * 
4208      * @return                                the column's descriptor
4209      * 
4210      * @throws ArrayIndexOutOfBoundsException if this table does not contain a column with that index.
4211      * 
4212      * @see                                   #getDescriptor(String)
4213      */
4214     public ColumnDesc getDescriptor(int column) throws ArrayIndexOutOfBoundsException {
4215         return columns.get(column);
4216     }
4217 
4218     /**
4219      * Returns the (first) column descriptor whose name matches the specified value.
4220      * 
4221      * @param  name The column name (case sensitive).
4222      * 
4223      * @return      The descriptor of the first column by that name, or <code>null</code> if the table contains no
4224      *                  column by that name.
4225      * 
4226      * @see         #getDescriptor(int)
4227      * @see         #indexOf(String)
4228      * 
4229      * @since       1.20
4230      */
4231     public ColumnDesc getDescriptor(String name) {
4232         int col = indexOf(name);
4233         return col < 0 ? null : getDescriptor(col);
4234     }
4235 
4236     /**
4237      * Converts a column from FITS logical values to bits. Null values (allowed in logical columns) will map to
4238      * <code>false</code>.
4239      *
4240      * @param  col The zero-based index of the column to be reset.
4241      *
4242      * @return     Whether the conversion was possible. *
4243      * 
4244      * @since      1.18
4245      */
4246     public boolean convertToBits(int col) {
4247         ColumnDesc c = columns.get(col);
4248 
4249         if (c.isBits) {
4250             return true;
4251         }
4252 
4253         if (c.base != boolean.class) {
4254             return false;
4255         }
4256 
4257         c.isBits = true;
4258         return true;
4259     }
4260 
4261     /**
4262      * Convert a column from float/double to float complex/double complex. This is only possible for certain columns.
4263      * The return status indicates if the conversion is possible.
4264      *
4265      * @param  index         The zero-based index of the column to be reset.
4266      *
4267      * @return               Whether the conversion is possible. *
4268      *
4269      * @throws FitsException if the operation failed
4270      * 
4271      * @since                1.18
4272      * 
4273      * @see                  ColumnDesc#isComplex()
4274      * @see                  #addComplexColumn(Object, Class)
4275      */
4276     public boolean setComplexColumn(int index) throws FitsException {
4277         synchronized (lock) {
4278             if (!validColumn(index)) {
4279                 return false;
4280             }
4281 
4282             ColumnDesc c = columns.get(index);
4283             if (c.isComplex()) {
4284                 return true;
4285             }
4286 
4287             if (c.base != float.class && c.base != double.class) {
4288                 return false;
4289             }
4290 
4291             if (!c.isVariableSize()) {
4292                 if (c.getLastFitsDim() != 2) {
4293                     return false;
4294                 }
4295                 // Set the column to complex
4296                 c.isComplex = true;
4297 
4298                 // Update the legacy (wrapped array) shape
4299                 c.setLegacyShape(c.fitsShape);
4300                 return true;
4301             }
4302 
4303             // We need to make sure that for every row, there are
4304             // an even number of elements so that we can
4305             // convert to an integral number of complex numbers.
4306             for (int i = 1; i < nRow; i++) {
4307                 if (getPointerCount(getRawElement(i, index)) % 2 != 0) {
4308                     return false;
4309                 }
4310             }
4311 
4312             // Halve the length component of array descriptors (2 reals = 1 complex)
4313             for (int i = 1; i < nRow; i++) {
4314                 Object p = getRawElement(i, index);
4315                 long len = getPointerCount(p) >>> 1;
4316                 if (c.hasLongPointers()) {
4317                     ((long[]) p)[0] = len;
4318                 } else {
4319                     ((int[]) p)[0] = (int) len;
4320                 }
4321                 setTableElement(i, index, p);
4322             }
4323 
4324             // Set the column to complex
4325             c.isComplex = true;
4326         }
4327 
4328         return true;
4329 
4330     }
4331 
4332     /**
4333      * Checks if this table contains a heap for storing variable length arrays (VLAs).
4334      * 
4335      * @return <code>true</code> if the table contains a heap, or else <code>false</code>.
4336      * 
4337      * @since  1.19.1
4338      */
4339     public final boolean containsHeap() {
4340         return getParameterSize() > 0;
4341     }
4342 
4343     /**
4344      * <p>
4345      * Defragments the heap area of this table, compacting the heap area, and returning the number of bytes by which the
4346      * heap size has been reduced. When tables with variable-sized columns are modified, the heap may retain old data as
4347      * columns are removed or elements get replaced with new data of different size. The data order in the heap may also
4348      * get jumbled, causing what would appear to be sequential reads to jump all over the heap space with the caching.
4349      * And, depending on how the heap was constructed in the first place, it may not be optimal for the row-after-row
4350      * table access that is the most typical use case.
4351      * </p>
4352      * <p>
4353      * This method rebuilds the heap by taking elements in table read order (by rows, and columns) and puts them on a
4354      * new heap.
4355      * </p>
4356      * <p>
4357      * For best squential read performance, you should defragment all tables that have been built column-by-column
4358      * before writing them to a FITS file. The only time defragmentation is really not needed is if the table was built
4359      * row-by-row, with no modifications to variable-length content after the fact.
4360      * </p>
4361      * 
4362      * @return               the number of bytes by which the heap has shrunk as a result of defragmentation.
4363      * 
4364      * @throws FitsException if there was an error accessing the heap or the main data table comntaining the heap
4365      *                           locators. In case of an error the table content may be left in a damaged state.
4366      * 
4367      * @see                  #compact()
4368      * @see                  #setElement(int, int, Object)
4369      * @see                  #addColumn(Object)
4370      * @see                  #deleteColumns(int, int)
4371      * @see                  #setColumn(int, Object)
4372      * 
4373      * @since                1.18
4374      */
4375     public long defragment() throws FitsException {
4376         if (!containsHeap()) {
4377             return 0L;
4378         }
4379 
4380         synchronized (lock) {
4381 
4382             int[] eSize = new int[columns.size()];
4383 
4384             for (int j = 0; j < columns.size(); j++) {
4385                 ColumnDesc c = columns.get(j);
4386                 if (c.isVariableSize()) {
4387                     eSize[j] = ElementType.forClass(c.getFitsBase()).size();
4388                 }
4389             }
4390 
4391             FitsHeap hp = getHeap();
4392             long oldSize = hp.size();
4393             FitsHeap compact = new FitsHeap(0);
4394 
4395             for (int i = 0; i < nRow; i++) {
4396                 for (int j = 0; j < columns.size(); j++) {
4397                     ColumnDesc c = columns.get(j);
4398                     if (c.isVariableSize()) {
4399                         Object p = getRawElement(i, j);
4400 
4401                         int len = (int) getPointerCount(p);
4402 
4403                         // Copy to new heap...
4404                         int pos = compact.copyFrom(hp, (int) getPointerOffset(p), c.getFitsBaseCount(len) * eSize[j]);
4405 
4406                         // Same length as before...
4407                         if (p instanceof long[]) {
4408                             ((long[]) p)[1] = pos;
4409                         } else {
4410                             ((int[]) p)[1] = pos;
4411                         }
4412 
4413                         // Update pointers in table
4414                         setTableElement(i, j, p);
4415                     }
4416                 }
4417             }
4418 
4419             heap = compact;
4420             return oldSize - compact.size();
4421         }
4422     }
4423 
4424     /**
4425      * Discard the information about the original heap size (if this table was read from an input), and instead use the
4426      * real size of the actual heap (plus reserved space around it) when writing to an output. Compacted tables may not
4427      * be re-writeable to the same file from which they were read, since they may be shorter than the original, but they
4428      * can always be written to a different file, which may at times be smaller than the original. It may be used along
4429      * with {@link #defragment()} to create FITS files with optimized storage from FITS files that may contain wasted
4430      * space.
4431      * 
4432      * @see   #defragment()
4433      * 
4434      * @since 1.19.1
4435      */
4436     public void compact() {
4437         synchronized (lock) {
4438             heapFileSize = 0;
4439         }
4440     }
4441 
4442     @Override
4443     public BinaryTableHDU toHDU() throws FitsException {
4444         Header h = new Header();
4445         fillHeader(h);
4446         return new BinaryTableHDU(h, this);
4447     }
4448 }