View Javadoc
1   package nom.tam.fits;
2   
3   /*-
4    * #%L
5    * nom.tam.fits
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.util.Arrays;
37  
38  import nom.tam.fits.header.Bitpix;
39  import nom.tam.fits.header.IFitsHeader;
40  import nom.tam.fits.header.Standard;
41  import nom.tam.util.ArrayDataInput;
42  import nom.tam.util.ArrayDataOutput;
43  import nom.tam.util.ArrayFuncs;
44  import nom.tam.util.ByteFormatter;
45  import nom.tam.util.ByteParser;
46  import nom.tam.util.Cursor;
47  import nom.tam.util.FormatException;
48  
49  import static nom.tam.fits.header.Standard.NAXIS1;
50  import static nom.tam.fits.header.Standard.NAXIS2;
51  import static nom.tam.fits.header.Standard.TBCOLn;
52  import static nom.tam.fits.header.Standard.TDMAXn;
53  import static nom.tam.fits.header.Standard.TDMINn;
54  import static nom.tam.fits.header.Standard.TFIELDS;
55  import static nom.tam.fits.header.Standard.TFORMn;
56  import static nom.tam.fits.header.Standard.TLMAXn;
57  import static nom.tam.fits.header.Standard.TLMINn;
58  import static nom.tam.fits.header.Standard.TNULLn;
59  
60  /**
61   * ASCII table data. ASCII tables are meant for human readability without any special tools. However, they are far less
62   * flexible or compact than {@link BinaryTable}. As such, users are generally discouraged from using this type of table
63   * to represent FITS table data. This class only supports scalar entries of type <code>int</code>, <code>long</code>,
64   * <code>float</code>, <code>double</code>, or else <code>String</code> types.
65   * 
66   * @see AsciiTableHDU
67   * @see BinaryTable
68   */
69  @SuppressWarnings("deprecation")
70  public class AsciiTable extends AbstractTableData {
71  
72      private static final int MAX_INTEGER_LENGTH = 10;
73  
74      private static final int FLOAT_MAX_LENGTH = 16;
75  
76      private static final int LONG_MAX_LENGTH = 20;
77  
78      private static final int INT_MAX_LENGTH = 10;
79  
80      private static final int DOUBLE_MAX_LENGTH = 24;
81  
82      /** Whether I10 columns should be treated as <code>int</code> provided that defined limits allow for it. */
83      private static boolean isI10PreferInt = true;
84  
85      // private static final Logger LOG = Logger.getLogger(AsciiTable.class.getName());
86  
87      /** The number of rows in the table */
88      private int nRows;
89  
90      /** The number of fields in the table */
91      private int nFields;
92  
93      /** The number of bytes in a row */
94      private int rowLen;
95  
96      /** The null string for the field */
97      private String[] nulls;
98  
99      /** The type of data in the field */
100     private Class<?>[] types;
101 
102     /** The offset from the beginning of the row at which the field starts */
103     private int[] offsets;
104 
105     /** The number of bytes in the field */
106     private int[] lengths;
107 
108     /** The byte buffer used to read/write the ASCII table */
109     private byte[] buffer;
110 
111     /** Markers indicating fields that are null */
112     private boolean[] isNull;
113 
114     /** Column names */
115     private String[] names;
116 
117     /**
118      * An array of arrays giving the data in the table in binary numbers
119      */
120     private Object[] data;
121 
122     /**
123      * The parser used to convert from buffer to data.
124      */
125     private ByteParser bp;
126 
127     /** The actual stream used to input data */
128     private ArrayDataInput currInput;
129 
130     /** Create an empty ASCII table */
131     public AsciiTable() {
132         data = new Object[0];
133         buffer = null;
134         nFields = 0;
135         nRows = 0;
136         rowLen = 0;
137         types = new Class[0];
138         lengths = new int[0];
139         offsets = new int[0];
140         nulls = new String[0];
141         names = new String[0];
142     }
143 
144     /**
145      * Creates an ASCII table given a header. For tables that contain integer-valued columns of format <code>I10</code>,
146      * the {@link #setI10PreferInt(boolean)} mayb be used to control whether to treat them as <code>int</code> or as
147      * <code>long</code> values (the latter is the default).
148      *
149      * @param      hdr           The header describing the table
150      *
151      * @throws     FitsException if the operation failed
152      * 
153      * @deprecated               (<i>for internal use</i>) Visibility may be reduced to the package level in the future.
154      */
155     @Deprecated
156     public AsciiTable(Header hdr) throws FitsException {
157         this(hdr, isI10PreferInt);
158     }
159 
160     /**
161      * <p>
162      * Create an ASCII table given a header, with custom integer handling support.
163      * </p>
164      * <p>
165      * The <code>preferInt</code> parameter controls how columns with format "<code>I10</code>" are handled; this is
166      * tricky because some, but not all, integers that can be represented in 10 characters can be represented as 32-bit
167      * integers. Setting it <code>true</code> may make it more likely to avoid unexpected type changes during
168      * round-tripping, but it also means that some (large number) data in I10 columns may be impossible to read.
169      * </p>
170      * 
171      * @param      hdr           The header describing the table
172      * @param      preferInt     if <code>true</code>, format "I10" columns will be assumed <code>int.class</code>,
173      *                               provided TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if
174      *                               <code>false</code>, I10 columns that have no clear indication of data range will be
175      *                               assumed <code>long.class</code>.
176      *
177      * @throws     FitsException if the operation failed
178      * 
179      * @deprecated               Use {@link #setI10PreferInt(boolean)} instead prior to reading ASCII tables.
180      */
181     @Deprecated
182     public AsciiTable(Header hdr, boolean preferInt) throws FitsException {
183         String ext = hdr.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE);
184 
185         if (!ext.equalsIgnoreCase(Standard.XTENSION_ASCIITABLE)) {
186             throw new FitsException("Not an ASCII table header (XTENSION = " + hdr.getStringValue(Standard.XTENSION) + ")");
187         }
188 
189         nRows = hdr.getIntValue(NAXIS2);
190         nFields = hdr.getIntValue(TFIELDS);
191         rowLen = hdr.getIntValue(NAXIS1);
192 
193         types = new Class[nFields];
194         offsets = new int[nFields];
195         lengths = new int[nFields];
196         nulls = new String[nFields];
197         names = new String[nFields];
198 
199         for (int i = 0; i < nFields; i++) {
200             names[i] = hdr.getStringValue(Standard.TTYPEn.n(i + 1), TableHDU.getDefaultColumnName(i));
201             offsets[i] = hdr.getIntValue(TBCOLn.n(i + 1)) - 1;
202             String s = hdr.getStringValue(TFORMn.n(i + 1));
203             if (offsets[i] < 0 || s == null) {
204                 throw new FitsException("Invalid Specification for column:" + (i + 1));
205             }
206             s = s.trim();
207             char c = s.charAt(0);
208             s = s.substring(1);
209             if (s.indexOf('.') > 0) {
210                 s = s.substring(0, s.indexOf('.'));
211             }
212 
213             try {
214                 lengths[i] = Integer.parseInt(s);
215             } catch (NumberFormatException e) {
216                 throw new HeaderCardException("Invalid " + TFORMn.n(i + 1).key() + " value: '" + s + "'");
217             }
218 
219             if (lengths[i] < 0) {
220                 throw new HeaderCardException("Invalid " + TFORMn.n(i + 1).key() + " value: '" + s + "'");
221             }
222 
223             switch (c) {
224             case 'A':
225                 types[i] = String.class;
226                 break;
227             case 'I':
228                 if (lengths[i] == MAX_INTEGER_LENGTH) {
229                     types[i] = guessI10Type(i, hdr, preferInt);
230                 } else {
231                     types[i] = lengths[i] > MAX_INTEGER_LENGTH ? long.class : int.class;
232                 }
233                 break;
234             case 'F':
235             case 'E':
236                 types[i] = float.class;
237                 break;
238             case 'D':
239                 types[i] = double.class;
240                 break;
241             default:
242                 throw new FitsException("could not parse column type of ascii table");
243             }
244 
245             nulls[i] = hdr.getStringValue(TNULLn.n(i + 1));
246             if (nulls[i] != null) {
247                 nulls[i] = nulls[i].trim();
248             }
249         }
250     }
251 
252     /**
253      * Creates an ASCII table from existing data in column-major format order.
254      *
255      * @param  columns       The data for scalar-valued columns. Each column must be an array of <code>int[]</code>,
256      *                           <code>long[]</code>, <code>float[]</code>, <code>double[]</code>, or else
257      *                           <code>String[]</code>, containing the same number of elements in each column (the
258      *                           number of rows).
259      * 
260      * @return               a new ASCII table with the data. The tables data may be partially independent from the
261      *                           argument. Modifications to the table data, or that to the argument have undefined
262      *                           effect on the other object. If it is important to decouple them, you can use a
263      *                           {@link ArrayFuncs#deepClone(Object)} of your original data as an argument.
264      * 
265      * @throws FitsException if the argument is not a suitable representation of FITS data in columns
266      * 
267      * @see                  BinaryTable#fromColumnMajor(Object[])
268      * 
269      * @since                1.19
270      */
271     public static AsciiTable fromColumnMajor(Object[] columns) throws FitsException {
272         AsciiTable t = new AsciiTable();
273         for (int i = 0; i < columns.length; i++) {
274             try {
275                 t.addColumn(columns[i]);
276             } catch (Exception e) {
277                 throw new FitsException("col[" + i + "]: " + e.getMessage(), e);
278             }
279         }
280         return t;
281     }
282 
283     void setColumnName(int col, String value)
284             throws IllegalArgumentException, IndexOutOfBoundsException, HeaderCardException {
285         HeaderCard.validateChars(value);
286         names[col] = value;
287     }
288 
289     /**
290      * Checks if the integer value of a specific key requires <code>long</code> value type to store.
291      *
292      * @param  h   the header
293      * @param  key the keyword to check
294      *
295      * @return     <code>true</code> if the keyword exists and has an integer value that is outside the range of
296      *                 <code>int</code>. Otherwise <code>false</code>
297      *
298      * @see        #guessI10Type(int, Header, boolean)
299      */
300     private boolean requiresLong(Header h, IFitsHeader key, Long dft) {
301         long l = h.getLongValue(key, dft);
302         if (l == dft) {
303             return false;
304         }
305 
306         return (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE);
307     }
308 
309     /**
310      * Guesses what type of values to use to return I10 type table values. Depending on the range of represented values
311      * I10 may fit into <code>int</code> types, or else require <code>long</code> type arrays. Therefore, the method
312      * checks for the presence of standard column limit keywords TLMINn/TLMAXn and TDMINn/TDMAXn and if these exist and
313      * are outside of the range of an <code>int</code> then the call will return <code>long.class</code>. If the header
314      * does not define the data limits (fully), it will return the class the caller prefers. Otherwise (data limits were
315      * defined and fit into the <code>int</code> range) <code>int.class</code> will be returned.
316      *
317      * @param  col       the 0-based table column index
318      * @param  h         the header
319      * @param  preferInt whether we prefer <code>int.class</code> over <code>long.class</code> in case the header does
320      *                       not provide us with a clue.
321      *
322      * @return           <code>long.class</code> if the data requires long or we prefer it. Othwerwise
323      *                       <code>int.class</code>
324      *
325      * @see              #AsciiTable(Header, boolean)
326      */
327     private Class<?> guessI10Type(int col, Header h, boolean preferInt) {
328         col++;
329 
330         if (requiresLong(h, TLMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TLMAXn.n(col), Long.MIN_VALUE)
331                 || requiresLong(h, TDMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TDMAXn.n(col), Long.MIN_VALUE)) {
332             return long.class;
333         }
334 
335         if ((h.containsKey(TLMINn.n(col)) || h.containsKey(TDMINn.n(col))) //
336                 && (h.containsKey(TLMAXn.n(col)) || h.containsKey(TDMAXn.n(col)))) {
337             // There are keywords defining both min/max values, and none of them require long types...
338             return int.class;
339         }
340 
341         return preferInt ? int.class : long.class;
342     }
343 
344     /**
345      * Return the data type in the specified column, such as <code>int.class</code> or <code>String.class</code>.
346      *
347      * @param  col The 0-based column index
348      *
349      * @return     the class of data in the specified column.
350      *
351      * @since      1.16
352      */
353     public final Class<?> getColumnType(int col) {
354         return types[col];
355     }
356 
357     int addColInfo(int col, Cursor<String, HeaderCard> iter) {
358         String tform = null;
359         if (types[col] == String.class) {
360             tform = "A" + lengths[col];
361         } else if (types[col] == int.class || types[col] == long.class) {
362             tform = "I" + lengths[col];
363         } else if (types[col] == float.class) {
364             tform = "E" + lengths[col] + ".0";
365         } else if (types[col] == double.class) {
366             tform = "D" + lengths[col] + ".0";
367         }
368 
369         Standard.context(AsciiTable.class);
370         if (names[col] != null) {
371             iter.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), names[col]));
372         }
373         iter.add(HeaderCard.create(Standard.TFORMn.n(col + 1), tform));
374         iter.add(HeaderCard.create(Standard.TBCOLn.n(col + 1), offsets[col] + 1));
375         Standard.context(null);
376         return lengths[col];
377     }
378 
379     @Override
380     public int addColumn(Object newCol) throws FitsException, IllegalArgumentException {
381         if (newCol == null) {
382             throw new FitsException("data is null");
383         }
384 
385         if (!newCol.getClass().isArray()) {
386             throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName());
387         }
388 
389         int maxLen = 1;
390         if (newCol instanceof String[]) {
391             String[] sa = (String[]) newCol;
392             for (String element : sa) {
393                 if (element != null && element.length() > maxLen) {
394                     maxLen = element.length();
395                 }
396             }
397         } else if (newCol instanceof double[]) {
398             maxLen = DOUBLE_MAX_LENGTH;
399         } else if (newCol instanceof int[]) {
400             maxLen = INT_MAX_LENGTH;
401         } else if (newCol instanceof long[]) {
402             maxLen = LONG_MAX_LENGTH;
403         } else if (newCol instanceof float[]) {
404             maxLen = FLOAT_MAX_LENGTH;
405         } else {
406             throw new FitsException(
407                     "No AsciiTable support for elements of " + newCol.getClass().getComponentType().getName());
408         }
409         addColumn(newCol, maxLen);
410 
411         // Invalidate the buffer
412         buffer = null;
413 
414         return nFields;
415     }
416 
417     /**
418      * Adds an ASCII table column with the specified ASCII text width for storing its elements.
419      *
420      * @param  newCol                   The new column data, which must be one of: <code>int[]</code>,
421      *                                      <code>long[]</code>, <code>float[]</code>, <code>double[]</code>, or else
422      *                                      <code>String[]</code>. If the table already contains data, the length of the
423      *                                      array must match the number of rows already contained in the table.
424      * @param  width                    the ASCII text width of the for the column entries (without the string
425      *                                      termination).
426      *
427      * @return                          the number of columns after this one is added.
428      *
429      * @throws IllegalArgumentException if the column data is not an array or the specified text <code>width</code> is
430      *                                      &le;1.
431      * @throws FitsException            if the column us of an unsupported data type or if the number of entries does
432      *                                      not match the number of rows already contained in the table.
433      * 
434      * @see                             #addColumn(Object)
435      */
436     public int addColumn(Object newCol, int width) throws FitsException, IllegalArgumentException {
437         if (width < 1) {
438             throw new IllegalArgumentException("Illegal ASCII column width: " + width);
439         }
440 
441         if (!newCol.getClass().isArray()) {
442             throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName());
443         }
444 
445         if (nFields > 0 && Array.getLength(newCol) != nRows) {
446             throw new FitsException(
447                     "Mismatched number of rows: expected " + nRows + ", got " + Array.getLength(newCol) + "rows.");
448         }
449 
450         if (nFields == 0) {
451             nRows = Array.getLength(newCol);
452         }
453 
454         Class<?> type = ArrayFuncs.getBaseClass(newCol);
455         if (type != int.class && type != long.class && type != float.class && type != double.class
456                 && type != String.class) {
457             throw new FitsException("No AsciiTable support for elements of " + type.getName());
458         }
459 
460         data = Arrays.copyOf(data, nFields + 1);
461         offsets = Arrays.copyOf(offsets, nFields + 1);
462         lengths = Arrays.copyOf(lengths, nFields + 1);
463         types = Arrays.copyOf(types, nFields + 1);
464         nulls = Arrays.copyOf(nulls, nFields + 1);
465         names = Arrays.copyOf(names, nFields + 1);
466 
467         data[nFields] = newCol;
468         offsets[nFields] = rowLen + 1;
469         lengths[nFields] = width;
470         types[nFields] = ArrayFuncs.getBaseClass(newCol);
471         names[nFields] = TableHDU.getDefaultColumnName(nFields);
472 
473         rowLen += width + 1;
474         if (isNull != null) {
475             boolean[] newIsNull = new boolean[nRows * (nFields + 1)];
476             // Fix the null pointers.
477             int add = 0;
478             for (int i = 0; i < isNull.length; i++) {
479                 if (i % nFields == 0) {
480                     add++;
481                 }
482                 if (isNull[i]) {
483                     newIsNull[i + add] = true;
484                 }
485             }
486             isNull = newIsNull;
487         }
488         nFields++;
489 
490         // Invalidate the buffer
491         buffer = null;
492 
493         return nFields;
494     }
495 
496     /**
497      * Beware that adding rows to ASCII tables may be very inefficient. Avoid addding more than a few rows if you can.
498      */
499     @Override
500     public int addRow(Object[] newRow) throws FitsException {
501         try {
502             // If there are no fields, then this is the
503             // first row. We need to add in each of the columns
504             // to get the descriptors set up.
505             if (nFields == 0) {
506                 for (Object element : newRow) {
507                     addColumn(element);
508                 }
509             } else {
510                 for (int i = 0; i < nFields; i++) {
511                     Object o = ArrayFuncs.newInstance(types[i], nRows + 1);
512                     System.arraycopy(data[i], 0, o, 0, nRows);
513                     System.arraycopy(newRow[i], 0, o, nRows, 1);
514                     data[i] = o;
515                 }
516                 nRows++;
517             }
518             // Invalidate the buffer
519             buffer = null;
520             return nRows;
521         } catch (Exception e) {
522             throw new FitsException("Error adding row:" + e.getMessage(), e);
523         }
524     }
525 
526     @Override
527     public void deleteColumns(int start, int len) throws FitsException {
528         ensureData();
529 
530         Object[] newData = new Object[nFields - len];
531         int[] newOffsets = new int[nFields - len];
532         int[] newLengths = new int[nFields - len];
533         Class<?>[] newTypes = new Class[nFields - len];
534         String[] newNulls = new String[nFields - len];
535 
536         // Copy in the initial stuff...
537         System.arraycopy(data, 0, newData, 0, start);
538         // Don't do the offsets here.
539         System.arraycopy(lengths, 0, newLengths, 0, start);
540         System.arraycopy(types, 0, newTypes, 0, start);
541         System.arraycopy(nulls, 0, newNulls, 0, start);
542 
543         // Copy in the final
544         System.arraycopy(data, start + len, newData, start, nFields - start - len);
545         // Don't do the offsets here.
546         System.arraycopy(lengths, start + len, newLengths, start, nFields - start - len);
547         System.arraycopy(types, start + len, newTypes, start, nFields - start - len);
548         System.arraycopy(nulls, start + len, newNulls, start, nFields - start - len);
549 
550         for (int i = start; i < start + len; i++) {
551             rowLen -= lengths[i] + 1;
552         }
553 
554         data = newData;
555         offsets = newOffsets;
556         lengths = newLengths;
557         types = newTypes;
558         nulls = newNulls;
559 
560         if (isNull != null) {
561             boolean found = false;
562 
563             boolean[] newIsNull = new boolean[nRows * (nFields - len)];
564             for (int i = 0; i < nRows; i++) {
565                 int oldOff = nFields * i;
566                 int newOff = (nFields - len) * i;
567                 for (int col = 0; col < start; col++) {
568                     newIsNull[newOff + col] = isNull[oldOff + col];
569                     found = found || isNull[oldOff + col];
570                 }
571                 for (int col = start + len; col < nFields; col++) {
572                     newIsNull[newOff + col - len] = isNull[oldOff + col];
573                     found = found || isNull[oldOff + col];
574                 }
575             }
576             if (found) {
577                 isNull = newIsNull;
578             } else {
579                 isNull = null;
580             }
581         }
582 
583         // Invalidate the buffer
584         buffer = null;
585 
586         nFields -= len;
587     }
588 
589     /**
590      * Beware that repeatedly deleting rows from ASCII tables may be very inefficient. Avoid calling this more than once
591      * (or a few times) if you can.
592      */
593     @Override
594     public void deleteRows(int start, int len) throws FitsException {
595         if (nRows == 0 || start < 0 || start >= nRows || len <= 0) {
596             return;
597         }
598         if (start + len > nRows) {
599             len = nRows - start;
600         }
601 
602         ensureData();
603 
604         for (int i = 0; i < nFields; i++) {
605             try {
606                 Object o = ArrayFuncs.newInstance(types[i], nRows - len);
607                 System.arraycopy(data[i], 0, o, 0, start);
608                 System.arraycopy(data[i], start + len, o, start, nRows - len - start);
609                 data[i] = o;
610             } catch (Exception e) {
611                 throw new FitsException("Error deleting row: " + e.getMessage(), e);
612             }
613 
614         }
615         nRows -= len;
616     }
617 
618     @Override
619     protected void loadData(ArrayDataInput in) throws IOException, FitsException {
620         currInput = in;
621 
622         if (buffer == null) {
623             getBuffer((long) nRows * rowLen, 0);
624         }
625 
626         data = new Object[nFields];
627         for (int i = 0; i < nFields; i++) {
628             data[i] = ArrayFuncs.newInstance(types[i], nRows);
629         }
630 
631         bp.setOffset(0);
632 
633         int rowOffset;
634         for (int i = 0; i < nRows; i++) {
635             rowOffset = rowLen * i;
636             for (int j = 0; j < nFields; j++) {
637                 try {
638                     if (!extractElement(rowOffset + offsets[j], lengths[j], data, j, i, nulls[j])) {
639                         if (isNull == null) {
640                             isNull = new boolean[nRows * nFields];
641                         }
642 
643                         isNull[j + i * nFields] = true;
644                     }
645                 } catch (ArrayIndexOutOfBoundsException e) {
646                     throw new FitsException("not enough data: " + e, e);
647                 }
648             }
649         }
650     }
651 
652     @Override
653     public void read(ArrayDataInput in) throws FitsException {
654         currInput = in;
655         super.read(in);
656     }
657 
658     /**
659      * Move an element from the buffer into a data array.
660      *
661      * @param  offset        The offset within buffer at which the element starts.
662      * @param  length        The number of bytes in the buffer for the element.
663      * @param  array         An array of objects, each of which is a simple array.
664      * @param  col           Which element of array is to be modified?
665      * @param  row           Which index into that element is to be modified?
666      * @param  nullFld       What string signifies a null element?
667      *
668      * @throws FitsException if the operation failed
669      */
670     private boolean extractElement(int offset, int length, Object[] array, int col, int row, String nullFld)
671             throws FitsException {
672 
673         bp.setOffset(offset);
674 
675         if (nullFld != null) {
676             String s = bp.getString(length);
677             if (s.trim().equals(nullFld)) {
678                 return false;
679             }
680             bp.skip(-length);
681         }
682         try {
683             if (array[col] instanceof String[]) {
684                 ((String[]) array[col])[row] = bp.getString(length);
685             } else if (array[col] instanceof int[]) {
686                 ((int[]) array[col])[row] = bp.getInt(length);
687             } else if (array[col] instanceof float[]) {
688                 ((float[]) array[col])[row] = bp.getFloat(length);
689             } else if (array[col] instanceof double[]) {
690                 ((double[]) array[col])[row] = bp.getDouble(length);
691             } else if (array[col] instanceof long[]) {
692                 ((long[]) array[col])[row] = bp.getLong(length);
693             } else {
694                 throw new FitsException("Invalid type for ASCII table conversion:" + array[col]);
695             }
696         } catch (FormatException e) {
697             throw new FitsException("Error parsing data at row,col:" + row + "," + col + "  ", e);
698         }
699         return true;
700     }
701 
702     @Override
703     protected void fillHeader(Header h) {
704         h.deleteKey(Standard.SIMPLE);
705         h.deleteKey(Standard.EXTEND);
706 
707         Standard.context(AsciiTable.class);
708 
709         Cursor<String, HeaderCard> c = h.iterator();
710         c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_ASCIITABLE));
711         c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue()));
712         c.add(HeaderCard.create(Standard.NAXIS, 2));
713         c.add(HeaderCard.create(Standard.NAXIS1, rowLen));
714         c.add(HeaderCard.create(Standard.NAXIS2, nRows));
715         c.add(HeaderCard.create(Standard.PCOUNT, 0));
716         c.add(HeaderCard.create(Standard.GCOUNT, 1));
717         c.add(HeaderCard.create(Standard.TFIELDS, nFields));
718 
719         for (int i = 0; i < nFields; i++) {
720             addColInfo(i, c);
721         }
722 
723         Standard.context(null);
724     }
725 
726     /**
727      * Read some data into the buffer.
728      */
729     private void getBuffer(long size, long offset) throws IOException, FitsException {
730 
731         if (currInput == null) {
732             throw new IOException("No stream open to read");
733         }
734 
735         if (size > Integer.MAX_VALUE) {
736             throw new FitsException("Cannot read ASCII table > 2 GB");
737         }
738 
739         buffer = new byte[(int) size];
740         if (offset != 0) {
741             FitsUtil.reposition(currInput, offset);
742         }
743         currInput.readFully(buffer);
744         bp = new ByteParser(buffer);
745     }
746 
747     /**
748      * <p>
749      * Returns the data for a particular column in as a flattened 1D array of elements. See {@link #addColumn(Object)}
750      * for more information about the format of data elements in general.
751      * </p>
752      * 
753      * @param  col           The 0-based column index.
754      * 
755      * @return               an array of primitives (for scalar columns), or else an <code>Object[]</code> array.
756      * 
757      * @throws FitsException if the table could not be accessed
758      *
759      * @see                  #setColumn(int, Object)
760      * @see                  #getElement(int, int)
761      * @see                  #getNCols()
762      */
763     @Override
764     public Object getColumn(int col) throws FitsException {
765         ensureData();
766         return data[col];
767     }
768 
769     @Override
770     protected Object[] getCurrentData() {
771         return data;
772     }
773 
774     @Override
775     public Object[] getData() throws FitsException {
776         return (Object[]) super.getData();
777     }
778 
779     @Override
780     public Object getElement(int row, int col) throws FitsException {
781         if (data != null) {
782             return singleElement(row, col);
783         }
784         return parseSingleElement(row, col);
785     }
786 
787     @Override
788     public int getNCols() {
789         return nFields;
790     }
791 
792     @Override
793     public int getNRows() {
794         return nRows;
795     }
796 
797     @Override
798     public Object[] getRow(int row) throws FitsException {
799 
800         if (data != null) {
801             return singleRow(row);
802         }
803         return parseSingleRow(row);
804     }
805 
806     /**
807      * Get the number of bytes in a row
808      *
809      * @return The number of bytes for a single row in the table.
810      */
811     public int getRowLen() {
812         return rowLen;
813     }
814 
815     @Override
816     protected long getTrueSize() {
817         return (long) nRows * rowLen;
818     }
819 
820     /**
821      * Checks if an element is <code>null</code>.
822      *
823      * @param  row The 0-based row
824      * @param  col The 0-based column
825      *
826      * @return     if the given element has been nulled.
827      */
828     public boolean isNull(int row, int col) {
829         if (isNull != null) {
830             return isNull[row * nFields + col];
831         }
832         return false;
833     }
834 
835     /**
836      * Read a single element from the table. This returns an array of dimension 1.
837      *
838      * @throws FitsException if the operation failed
839      */
840     private Object parseSingleElement(int row, int col) throws FitsException {
841 
842         Object[] res = new Object[1];
843         try {
844             getBuffer(lengths[col], getFileOffset() + (long) row * (long) rowLen + offsets[col]);
845         } catch (IOException e) {
846             buffer = null;
847             throw new FitsException("Unable to read element", e);
848         }
849         res[0] = ArrayFuncs.newInstance(types[col], 1);
850 
851         boolean success = extractElement(0, lengths[col], res, 0, 0, nulls[col]);
852         buffer = null;
853 
854         return success ? res[0] : null;
855     }
856 
857     /**
858      * Read a single row from the table. This returns a set of arrays of dimension 1.
859      *
860      * @throws FitsException if the operation failed
861      */
862     private Object[] parseSingleRow(int row) throws FitsException {
863 
864         Object[] res = new Object[nFields];
865 
866         try {
867             getBuffer(rowLen, getFileOffset() + (long) row * (long) rowLen);
868         } catch (IOException e) {
869             throw new FitsException("Unable to read row", e);
870         }
871 
872         for (int i = 0; i < nFields; i++) {
873             res[i] = ArrayFuncs.newInstance(types[i], 1);
874             if (!extractElement(offsets[i], lengths[i], res, i, 0, nulls[i])) {
875                 res[i] = null;
876             }
877         }
878 
879         // Invalidate buffer for future use.
880         buffer = null;
881         return res;
882     }
883 
884     @Override
885     public void setColumn(int col, Object newData) throws FitsException {
886         ensureData();
887         if (col < 0 || col >= nFields || newData.getClass() != data[col].getClass()
888                 || Array.getLength(newData) != Array.getLength(data[col])) {
889             throw new FitsException("Invalid column/column mismatch:" + col);
890         }
891         data[col] = newData;
892 
893         // Invalidate the buffer.
894         buffer = null;
895     }
896 
897     @Override
898     public void setElement(int row, int col, Object newData) throws FitsException {
899         ensureData();
900         try {
901             System.arraycopy(newData, 0, data[col], row, 1);
902         } catch (Exception e) {
903             throw new FitsException("Incompatible element:" + row + "," + col, e);
904         }
905         setNull(row, col, false);
906 
907         // Invalidate the buffer
908         buffer = null;
909 
910     }
911 
912     /**
913      * Mark (or unmark) an element as null. Note that if this FITS file is latter written out, a TNULL keyword needs to
914      * be defined in the corresponding header. This routine does not add an element for String columns.
915      *
916      * @param row  The 0-based row.
917      * @param col  The 0-based column.
918      * @param flag True if the element is to be set to null.
919      */
920     public void setNull(int row, int col, boolean flag) {
921         if (flag) {
922             if (isNull == null) {
923                 isNull = new boolean[nRows * nFields];
924             }
925             isNull[col + row * nFields] = true;
926         } else if (isNull != null) {
927             isNull[col + row * nFields] = false;
928         }
929 
930         // Invalidate the buffer
931         buffer = null;
932     }
933 
934     /**
935      * Set the null string for a columns. This is not a public method since we want users to call the method in
936      * AsciiTableHDU and update the header also.
937      */
938     void setNullString(int col, String newNull) {
939         if (col >= 0 && col < nulls.length) {
940             nulls[col] = newNull;
941         }
942     }
943 
944     @Override
945     public void setRow(int row, Object[] newData) throws FitsException {
946         if (row < 0 || row > nRows) {
947             throw new FitsException("Invalid row in setRow");
948         }
949         ensureData();
950         for (int i = 0; i < nFields; i++) {
951             try {
952                 System.arraycopy(newData[i], 0, data[i], row, 1);
953             } catch (Exception e) {
954                 throw new FitsException("Unable to modify row: incompatible data:" + row, e);
955             }
956             setNull(row, i, false);
957         }
958 
959         // Invalidate the buffer
960         buffer = null;
961 
962     }
963 
964     /**
965      * Extract a single element from a table. This returns an array of length 1.
966      */
967     private Object singleElement(int row, int col) {
968 
969         Object res = null;
970         if (isNull == null || !isNull[row * nFields + col]) {
971             res = ArrayFuncs.newInstance(types[col], 1);
972             System.arraycopy(data[col], row, res, 0, 1);
973         }
974         return res;
975     }
976 
977     /**
978      * Extract a single row from a table. This returns an array of Objects each of which is an array of length 1.
979      */
980     private Object[] singleRow(int row) {
981 
982         Object[] res = new Object[nFields];
983         for (int i = 0; i < nFields; i++) {
984             if (isNull == null || !isNull[row * nFields + i]) {
985                 res[i] = ArrayFuncs.newInstance(types[i], 1);
986                 System.arraycopy(data[i], row, res[i], 0, 1);
987             }
988         }
989         return res;
990     }
991 
992     /**
993      * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a
994      *                 new HDU and editing the header as necessary to incorporate custom entries. May be removed from
995      *                 the API in the future.
996      */
997     @Deprecated
998     @Override
999     public void updateAfterDelete(int oldNCol, Header hdr) throws FitsException {
1000 
1001         int offset = 0;
1002         for (int i = 0; i < nFields; i++) {
1003             offsets[i] = offset;
1004             hdr.addValue(TBCOLn.n(i + 1), offset + 1);
1005             offset += lengths[i] + 1;
1006         }
1007         for (int i = nFields; i < oldNCol; i++) {
1008             hdr.deleteKey(TBCOLn.n(i + 1));
1009         }
1010 
1011         hdr.addValue(NAXIS1, rowLen);
1012     }
1013 
1014     @Override
1015     public void write(ArrayDataOutput str) throws FitsException {
1016         // Make sure we have the data in hand.
1017         if (str != currInput) {
1018             ensureData();
1019         }
1020 
1021         // If buffer is still around we can just reuse it,
1022         // since nothing we've done has invalidated it.
1023         if (data == null) {
1024             throw new FitsException("Attempt to write undefined ASCII Table");
1025         }
1026 
1027         if ((long) nRows * rowLen > Integer.MAX_VALUE) {
1028             throw new FitsException("Cannot write ASCII table > 2 GB");
1029         }
1030 
1031         buffer = new byte[nRows * rowLen];
1032 
1033         bp = new ByteParser(buffer);
1034         for (int i = 0; i < buffer.length; i++) {
1035             buffer[i] = (byte) ' ';
1036         }
1037 
1038         ByteFormatter bf = new ByteFormatter();
1039 
1040         for (int i = 0; i < nRows; i++) {
1041 
1042             for (int j = 0; j < nFields; j++) {
1043                 int offset = i * rowLen + offsets[j];
1044                 int len = lengths[j];
1045                 if (isNull != null && isNull[i * nFields + j]) {
1046                     if (nulls[j] == null) {
1047                         throw new FitsException("No null value set when needed");
1048                     }
1049                     bf.format(nulls[j], buffer, offset, len);
1050                 } else if (types[j] == String.class) {
1051                     String[] s = (String[]) data[j];
1052                     bf.format(s[i], buffer, offset, len);
1053                 } else if (types[j] == int.class) {
1054                     int[] ia = (int[]) data[j];
1055                     bf.format(ia[i], buffer, offset, len);
1056                 } else if (types[j] == float.class) {
1057                     float[] fa = (float[]) data[j];
1058                     bf.format(fa[i], buffer, offset, len);
1059                 } else if (types[j] == double.class) {
1060                     double[] da = (double[]) data[j];
1061                     bf.format(da[i], buffer, offset, len);
1062                 } else if (types[j] == long.class) {
1063                     long[] la = (long[]) data[j];
1064                     bf.format(la[i], buffer, offset, len);
1065                 }
1066             }
1067         }
1068 
1069         // Now write the buffer.
1070         try {
1071             str.write(buffer);
1072             FitsUtil.pad(str, buffer.length, (byte) ' ');
1073         } catch (IOException e) {
1074             throw new FitsException("Error writing ASCII Table data", e);
1075         }
1076     }
1077 
1078     @Override
1079     public AsciiTableHDU toHDU() {
1080         Header h = new Header();
1081         fillHeader(h);
1082         return new AsciiTableHDU(h, this);
1083     }
1084 
1085     /**
1086      * <p>
1087      * Controls how columns with format "<code>I10</code>" are handled; this is tricky because some, but not all,
1088      * integers that can be represented in 10 characters form 32-bit integers. Setting it <code>true</code> may make it
1089      * more likely to avoid unexpected type changes during round-tripping, but it also means that some values in I10
1090      * columns may be impossible to read. The default behavior is to assume <code>true</code>, and thus to treat I10
1091      * columns as <code>int</code> values.
1092      * </p>
1093      * 
1094      * @param value if <code>true</code>, format "I10" columns will be assumed <code>int.class</code>, provided
1095      *                  TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if <code>false</code>, I10 columns
1096      *                  that have no clear indication of data range will be assumed <code>long.class</code>.
1097      *
1098      * @since       1.19
1099      * 
1100      * @see         AsciiTable#isI10PreferInt()
1101      */
1102     public static void setI10PreferInt(boolean value) {
1103         isI10PreferInt = value;
1104     }
1105 
1106     /**
1107      * Checks if I10 columns should be treated as containing 32-bit <code>int</code> values, rather than 64-bit
1108      * <code>long</code> values, when possible.
1109      * 
1110      * @return <code>true</code> if I10 columns should be treated as containing 32-bit <code>int</code> values,
1111      *             otherwise <code>false</code>.
1112      * 
1113      * @since  1.19
1114      * 
1115      * @see    #setI10PreferInt(boolean)
1116      */
1117     public static boolean isI10PreferInt() {
1118         return isI10PreferInt;
1119     }
1120 }