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