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    }