001 /* 002 * @(#)FitsKeyword.java $Revision: 1.13 $ $Date: 2003/04/11 08:41:15 $ 003 * 004 * Copyright (C) 2000 European Southern Observatory 005 * License: GNU General Public License version 2 or later 006 */ 007 package org.eso.fits; 008 009 import java.lang.*; 010 import java.io.*; 011 import java.text.*; 012 import java.util.*; 013 014 /** FitsKeyword class describes a single FITS header keyword as 015 * defined by the FITS standard (ref. NOST-1.2). The implementation 016 * also support the hierarchical keyword convension as defined by 017 * the ESO Data Interface Control Board. The name of a keyword 018 * is converted to uppercase and different hierarchical levels are 019 * separated by '.' i.e. the keyword 'HIERARCH ESO TEL NAME =' will 020 * get the name 'ESO.TEL.NAME'. 021 * 022 * @version $Revision: 1.13 $ $Date: 2003/04/11 08:41:15 $ 023 * @author P.Grosbol, DMD/ESO, <pgrosbol@eso.org> 024 */ 025 public class FitsKeyword { 026 027 // Definition of FITS keyword types 028 public final static int NONE = 0; 029 public final static int COMMENT = 1; 030 public final static int STRING = 2; 031 public final static int BOOLEAN = 3; 032 public final static int INTEGER = 4; 033 public final static int REAL = 5; 034 public final static int DATE = 6; 035 036 private final byte NULL = 0x00; 037 private final byte SPACE = 0x20; 038 private final byte COMMA = 0x2C; 039 private final byte QUOTE = 0x27; 040 private final byte SLASH = 0x2F; 041 private final byte EQUAL = 0x3D; 042 private final byte MINUS = 0x2D; 043 private final byte UNDERSCORE = 0x5F; 044 private final byte A = 0x41; 045 private final byte Z = 0x5A; 046 047 private String name; 048 private int type = NONE; 049 private String kwCard; // Original FITS keyword string 050 private boolean validCard = false; // Is kwCard value valid ? 051 private Object value; // Value of keyword 052 private String comment; // Comment field of keyword 053 private boolean valueTruncated = false; 054 055 private final static SimpleDateFormat ISOLONG = 056 new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 057 private final static SimpleDateFormat ISOSHORT = 058 new SimpleDateFormat("yyyy-MM-dd"); 059 private final static SimpleDateFormat FITSDATE = 060 new SimpleDateFormat("dd/MM/yy"); 061 private final static TimeZone TIMEZONE = TimeZone.getTimeZone("UTC"); 062 063 /** Constructor for FitsKeyword class from a 80 character byte array. 064 * 065 * @param card byte array with 80 characters FITS header card 066 * @exception FitsException */ 067 public FitsKeyword(byte[] card) throws FitsException { 068 int idx = 0; // index in FITS card 069 int idx_last = 0; // last index of value field 070 int idx_comm_first = 0; // first index of comment field 071 072 // if card is null or too short - add spaces 073 if (card == null || card.length < Fits.CARD) { 074 byte[] pc = new byte[Fits.CARD]; 075 int n = 0; 076 while (n < card.length) { 077 pc[n] = card[n]; 078 n++; 079 } 080 while (n < Fits.CARD) pc[n++] = SPACE; 081 card = pc; 082 } 083 084 //BUGFIX: by John Talbot to support numbers in keywords of SDSS spectra 085 if ((card[0] != SPACE) && (card[0] != MINUS) && (card[0] != UNDERSCORE) && 086 ((card[0] < A) || (Z < card[0])) && 087 ((card[0] < '0') || ('9' < card[0]))) { 088 throw new FitsException("Illegal character " + 089 card[0] + ", " + card[1] + ", " + card[2] + ", " + card[3]+ ", " + card[4] + 090 (new String(card, 0, Fits.CARD)), FitsException.KEYWORD); 091 } 092 093 kwCard = new String(card, 0, Fits.CARD); // save keyword card 094 validCard = true; 095 name = kwCard.substring(0, 8); // get prime keyword name 096 String valueField = null; 097 comment = null; 098 099 if (name.startsWith("END ")) { // check if END card 100 throw new FitsException("END card", FitsException.ENDCARD); 101 } 102 103 if (name.startsWith("HISTORY ") // Comment keyword 104 || name.startsWith("COMMENT ") 105 || name.startsWith(" ")) { 106 type = COMMENT; 107 idx_comm_first = 8; 108 } else if (name.startsWith("HIERARCH")) { // Hierarchical keyword 109 StringBuffer hkw = new StringBuffer(Fits.CARD); 110 boolean found = false; 111 byte last = NULL; 112 idx = 8; 113 while ((idx<Fits.CARD) && (card[idx]!=EQUAL)) { 114 if (card[idx] != SPACE) { 115 if (last == SPACE && found) { 116 hkw.append('.'); 117 } 118 found = true; 119 hkw.append((char) card[idx]); 120 } 121 last = card[idx++]; 122 } 123 if (Fits.CARD <= idx) { 124 throw new FitsException("No equal-sign in HIERARCH keyword", 125 FitsException.KEYWORD); 126 } 127 name = hkw.toString(); 128 } else if (card[8]==EQUAL) { // Prim keyword with value 129 idx = 8; 130 } else { // Comment keyword 131 type = COMMENT; 132 idx_comm_first = 8; 133 } 134 135 name = (name.trim()).toUpperCase(); // Force to uppercase 136 137 if (card[idx] == EQUAL) { // Keyword with value field 138 idx++; 139 while ((idx<Fits.CARD) && (card[idx]==SPACE)) idx++; 140 if (card[idx] == QUOTE) { // string value 141 idx_last = ++idx; 142 while ((idx_last < Fits.CARD-1) 143 && ((card[idx_last] != QUOTE) 144 || ((card[idx_last] == QUOTE) 145 && (card[idx_last+1] == QUOTE)))) { 146 if (card[idx_last] == QUOTE) { 147 idx_last++; 148 } 149 idx_last++; 150 } 151 152 int n1 = idx; // convert two quotes to one 153 int n2 = idx; 154 boolean last_not_quote = true; 155 while (n1<idx_last) { 156 card[n2] = card[n1]; 157 if (card[n1] == QUOTE) { 158 last_not_quote = !last_not_quote; 159 } 160 if (last_not_quote) { 161 n2++; 162 } 163 n1++; 164 } 165 166 valueField = (n1 == n2) ? kwCard.substring(idx, n2) : new String(card, idx, n2-idx); 167 type = STRING; 168 n1 = name.lastIndexOf('.') + 1; 169 if (name.regionMatches(n1, "DATE", 0, 4)) { 170 SimpleDateFormat dateFormat = FITSDATE; 171 if (0<valueField.indexOf('-')) { 172 dateFormat = (0<valueField.indexOf('T')) ? ISOLONG : ISOSHORT; 173 } 174 dateFormat.setTimeZone(TIMEZONE); 175 value = dateFormat.parse(valueField, new ParsePosition(0)); 176 type = DATE; 177 } else { 178 value = valueField.trim(); 179 type = STRING; 180 } 181 } else { 182 idx_last = idx; 183 while ((idx_last < Fits.CARD) 184 && (card[idx_last] != SPACE) 185 && (card[idx_last] != SLASH) 186 && (card[idx_last] != COMMA)) { 187 idx_last++; 188 } 189 190 valueField = kwCard.substring(idx, idx_last); 191 try { 192 if (0<=valueField.indexOf('.')) { 193 value = new Double(valueField); 194 type = REAL; 195 } else { 196 value = new Integer(valueField); 197 type = INTEGER; 198 } 199 } catch (NumberFormatException e) { 200 value = new Boolean(valueField.regionMatches(true, 0, "T", 0, 1)); 201 type = BOOLEAN; 202 } 203 } 204 205 while ((idx_last < Fits.CARD) && (card[idx_last] != SLASH)) { // find comment field 206 idx_last++; 207 } 208 209 if ((idx_last < Fits.CARD) && (card[idx_last] == SLASH)) { 210 idx_comm_first = idx_last+1; 211 } 212 } 213 214 if (0<idx_comm_first) { // get keyword comment 215 comment = kwCard.substring(idx_comm_first, Fits.CARD); 216 comment = comment.trim(); 217 } else { 218 comment = new String(""); 219 } 220 } 221 222 /** Constructor for FitsKeyword class from String. 223 * 224 * @param card String with 80 characters FITS header card 225 * @exception FitsException */ 226 public FitsKeyword(String card) throws FitsException { 227 this(card.getBytes()); 228 } 229 230 /** Constructor for FitsKeyword class specifying name and 231 * comment for a comment keyword' 232 * 233 * @param name String with name of keyword 234 * @param comment String with keyword comment */ 235 public FitsKeyword(String name, String comment) { 236 setName(name); 237 this.comment = comment; 238 type = COMMENT; 239 validCard = false; 240 } 241 242 /** Constructor for FitsKeyword class specifying name, value and 243 * comment for a string keyword. 244 * 245 * @param name String with name of keyword 246 * @param value String value of keyword 247 * @param comment String with keyword comment */ 248 public FitsKeyword(String name, String value, String comment) { 249 setName(name); 250 this.value = value; 251 this.comment = comment; 252 type = STRING; 253 validCard = false; 254 } 255 256 /** Constructor for FitsKeyword class specifying name, value and 257 * comment for a boolean keyword. 258 * 259 * @param name String with name of keyword 260 * @param value boolean value of keyword 261 * @param comment String with keyword comment */ 262 public FitsKeyword(String name, boolean value, String comment) { 263 setName(name); 264 this.value = new Boolean(value); 265 this.comment = comment; 266 type = BOOLEAN; 267 validCard = false; 268 } 269 270 /** Constructor for FitsKeyword class specifying name, value and 271 * comment for an integer keyword. 272 * 273 * @param name String with name of keyword 274 * @param value int value of keyword 275 * @param comment String with keyword comment */ 276 public FitsKeyword(String name, int value, String comment) { 277 setName(name); 278 this.value = new Integer(value); 279 this.comment = comment; 280 type = INTEGER; 281 validCard = false; 282 } 283 284 /** Constructor for FitsKeyword class specifying name, value and 285 * comment for a real keyword. 286 * 287 * @param name String with name of keyword 288 * @param value double value of keyword 289 * @param comment String with keyword comment */ 290 public FitsKeyword(String name, double value, String comment) { 291 setName(name); 292 this.value = new Double(value); 293 this.comment = comment; 294 type = REAL; 295 validCard = false; 296 } 297 298 /** Constructor for FitsKeyword class specifying name, value and 299 * comment for a date keyword. 300 * 301 * @param name String with name of keyword 302 * @param value Date value of keyword 303 * @param comment String with keyword comment */ 304 public FitsKeyword(String name, Date value, String comment) { 305 setName(name); 306 this.value = value; 307 this.comment = comment; 308 type = DATE; 309 validCard = false; 310 } 311 312 /** Method provides the value of a FITS keyword as boolean. For 313 * INTEGER type keywords, all none-zero values will return true. 314 * The method returns FALSE for all keyword types other than 315 * BOOLEAN, REAL and INTEGER. */ 316 public final boolean getBool() { 317 if (type==BOOLEAN) { 318 return ((Boolean)value).booleanValue(); 319 } else if (type==INTEGER) { 320 return (((Integer)value).intValue()!=0) ? true : false; 321 } else if (type==REAL) { 322 return (((Double)value).intValue()!=0) ? true : false; 323 } 324 return false; 325 } 326 327 /** Method provides the value of a FITS keyword as integer for 328 * keyword types INTEGER and REAL. Zero is returned for all 329 * other types. */ 330 public final int getInt() { 331 if (type==INTEGER) { 332 return ((Integer)value).intValue(); 333 } else if (type==REAL) { 334 return ((Double)value).intValue(); 335 } 336 return 0; 337 } 338 339 /** Method provides the value of a FITS keyword as double for 340 * keyword types INTEGER and REAL. Zero is returned for all 341 * other types. */ 342 public final double getReal() { 343 if (type==REAL) { 344 return ((Double)value).doubleValue(); 345 } else if (type==INTEGER) { 346 return ((Integer)value).doubleValue(); 347 } 348 return 0.0; 349 } 350 351 /** Method provides the value of a FITS keyword as a Date object for 352 * keywords of type DATE. For STRING type keywords the string is 353 * converted to a Date if possible otherwise a NULL pointer 354 * is returned. */ 355 public final Date getDate() { 356 if (value == null) { 357 return null; 358 } 359 if (type == DATE) { 360 return (Date) value; 361 } else if (type == STRING) { 362 String str = (String) value; 363 SimpleDateFormat dateFormat = FITSDATE; 364 if (0<str.indexOf('-')) { 365 dateFormat = (0<str.indexOf('T')) ? ISOLONG : ISOSHORT; 366 } 367 dateFormat.setTimeZone(TIMEZONE); 368 return dateFormat.parse(str, new ParsePosition(0)); 369 } 370 return null; 371 } 372 373 /** Method provides the value of a FITS keyword as a String. If 374 * not value field is defined NULL is returned. */ 375 public final String getString() { 376 if (value == null) { 377 return null; 378 } 379 if (type == DATE) { 380 SimpleDateFormat dateFormat = ISOLONG; 381 dateFormat.setTimeZone(TIMEZONE); 382 return (dateFormat.format((Date) value, new StringBuffer(), 383 new FieldPosition(0))).toString(); 384 } 385 return value.toString(); 386 } 387 388 /** Set value field for keyword of STRING type. Note: the keyword 389 * type will be changed to STRING. 390 * 391 * @param value String with value of keyword value field 392 */ 393 public final void setValue(String value) { 394 this.value = value; 395 type = STRING; 396 validCard = false; 397 } 398 399 /** Set value field for keyword of BOOLEAN type. Note: the keyword 400 * type will be changed to BOOLEAN. 401 * 402 * @param value booelan with value of keyword value field 403 */ 404 public final void setValue(boolean value) { 405 this.value = new Boolean(value); 406 type = BOOLEAN; 407 validCard = false; 408 } 409 410 /** Set value field for keyword of INTEGER type. Note: the keyword 411 * type will be changed to INTEGER. 412 * 413 * @param value integer with value of keyword value field 414 */ 415 public final void setValue(int value) { 416 this.value = new Integer(value); 417 type = INTEGER; 418 validCard = false; 419 } 420 421 /** Set value field for keyword of REAL type. Note: the keyword 422 * type will be changed to REAL. 423 * 424 * @param value double with value of keyword value field 425 */ 426 public final void setValue(double value) { 427 this.value = new Double(value); 428 type = REAL; 429 validCard = false; 430 } 431 432 /** Set value field for keyword of DATE type. Note: the keyword 433 * type will be changed to DATE. 434 * 435 * @param value double with value of keyword value field 436 */ 437 public final void setValue(Date value) { 438 this.value = value; 439 type = DATE; 440 validCard = false; 441 } 442 443 /** Method generates an 80 character Sting of the keyword in FITS 444 * format. Note: fields may be truncated due the the 80 445 * char. limit. */ 446 public String toString() { 447 int idx; 448 449 if (validCard) { // original FITS card valid 450 return kwCard; 451 } 452 453 StringBuffer card = new StringBuffer(80); 454 455 if ((name.length() < 9) && (name.indexOf('.') < 0)) { // Prime keyword 456 card.append(name); 457 idx = card.length(); 458 while (idx++ < 8) card.append(" "); 459 } else { // Hierarchical keyword 460 card.append("HIERARCH "); 461 StringTokenizer stok = new StringTokenizer(name, "."); 462 while (stok.hasMoreTokens()) 463 card.append(stok.nextToken() + " "); 464 } 465 466 String val = "' '"; 467 switch (type) { // Generate keyword value string 468 case STRING : 469 StringBuffer sbuf = new StringBuffer((String) value); 470 if (0 <= ((String) value).indexOf('\'')) { 471 char[] ch = ((String) value).toCharArray(); 472 sbuf = new StringBuffer(Fits.CARD); 473 for (int n=0; n<ch.length; n++) { 474 sbuf.append(ch[n]); 475 if (ch[n]=='\'') { 476 sbuf.append('\''); 477 } 478 } 479 } 480 while (sbuf.length()<8) { 481 sbuf.append(" "); 482 } 483 sbuf.insert(0, '\''); 484 sbuf.append('\''); 485 val = sbuf.toString(); 486 break; 487 case INTEGER : 488 val = ((Integer) value).toString(); 489 break; 490 case REAL : 491 val = ((Double) value).toString(); 492 break; 493 case BOOLEAN : 494 if (((Boolean) value).booleanValue()) { 495 val = "T"; 496 } else { 497 val = "F"; 498 } 499 break; 500 case DATE : 501 SimpleDateFormat dateFormat = ISOLONG; 502 dateFormat.setTimeZone(TIMEZONE); 503 StringBuffer df = 504 new StringBuffer("'" + 505 dateFormat.format((Date) value, 506 new StringBuffer(), 507 new FieldPosition(0)) 508 + "'"); 509 val = df.toString(); 510 break; 511 case COMMENT : 512 card.append(comment); 513 break; 514 } 515 516 if (type!=COMMENT) { 517 card.append("= "); // append value of keyword 518 idx = val.length(); 519 if ((card.length() < 11) && type!=STRING) { 520 while (idx++ < 20) card.append(" "); 521 } 522 card.append(val); 523 524 valueTruncated = false; 525 idx = card.length(); // finally add comment field 526 if (Fits.CARD < idx) { // check if name/value okay 527 card.setCharAt(Fits.CARD-1, '\''); 528 valueTruncated = true; 529 } 530 while (idx++ < 30) card.append(" "); 531 card.append(" / " + comment); 532 } 533 534 idx = card.length(); // ensure the card has 80 chars 535 if (Fits.CARD<idx) { 536 card.setLength(Fits.CARD); 537 } else { 538 while (idx++ < Fits.CARD) card.append(" "); 539 } 540 return card.toString(); 541 } 542 543 /** Check if the keyword name or value fields were truncated 544 * by the last call of the toString method. */ 545 public boolean isValueTruncated(){ 546 return valueTruncated; 547 } 548 549 /** Check if FITS keyword is empty that is has all blank (' ') name 550 * and comment. */ 551 public boolean isEmpty(){ 552 return name.length()<1 && comment.length()<1 && value==null; 553 } 554 555 /** Check if the FITS keyword was modified since it was created 556 * from a FITS header card. */ 557 public boolean isModified(){ 558 return !validCard; 559 } 560 561 /** Get method to provide name of FITS keyword. */ 562 public String getName(){ 563 return name; 564 } 565 566 /** Set name of FITS keyword. */ 567 public void setName(String name){ 568 this.name = (name == null) ? "" : name.toUpperCase(); 569 } 570 571 /** Get method to provide type of FITS keyword. */ 572 public int getType(){ 573 return type; 574 } 575 576 /** Get method to obtain comment of FITS keyword. */ 577 public String getComment(){ 578 return comment; 579 } 580 581 /** Set comment field of a FITS keyword 582 * @param comment String with the keyword comment. */ 583 public void setComment(String comment){ 584 this.comment = (comment == null) ? "" : comment; 585 } 586 }