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