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