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