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 }