/*
* @(#)FitsKeyword.java $Revision: 1.14 $ $Date: 2004/01/12 13:13:23 $
*
* Copyright (C) 2000 European Southern Observatory
* License: GNU General Public License version 2 or later
*/
package fr.unistra.pelican.util.jFits;
import java.text.*;
import java.util.*;
/** FitsKeyword class describes a single FITS header keyword as
* defined by the FITS standard (ref. NOST-1.2). The implementation
* also support the hierarchical keyword convension as defined by
* the ESO Data Interface Control Board. The name of a keyword
* is converted to uppercase and different hierarchical levels are
* separated by '.' i.e. the keyword 'HIERARCH ESO TEL NAME =' will
* get the name 'ESO.TEL.NAME'.
*
* @version $Revision: 1.14 $ $Date: 2004/01/12 13:13:23 $
* @author P.Grosbol, ESO, <pgrosbol@eso.org>
*/
public class FitsKeyword {
// Definition of FITS keyword types
public final static int NONE = 0;
public final static int COMMENT = 1;
public final static int STRING = 2;
public final static int BOOLEAN = 3;
public final static int INTEGER = 4;
public final static int REAL = 5;
public final static int DATE = 6;
private final byte NULL = 0x00;
private final byte SPACE = 0x20;
private final byte COMMA = 0x2C;
private final byte QUOTE = 0x27;
private final byte SLASH = 0x2F;
private final byte EQUAL = 0x3D;
private final byte MINUS = 0x2D;
private final byte UNDERSCORE = 0x5F;
private final byte A = 0x41;
private final byte Z = 0x5A;
private String name;
private int type = NONE;
private String kwCard; // Original FITS keyword string
private boolean validCard = false; // Is kwCard value valid ?
private Object value; // Value of keyword
private String comment; // Comment field of keyword
private boolean valueTruncated = false;
private final static SimpleDateFormat ISOLONG =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private final static SimpleDateFormat ISOSHORT =
new SimpleDateFormat("yyyy-MM-dd");
private final static SimpleDateFormat FITSDATE =
new SimpleDateFormat("dd/MM/yy");
private final static TimeZone TIMEZONE = TimeZone.getTimeZone("UTC");
/** Constructor for FitsKeyword class from a 80 character byte array.
*
* @param card byte array with 80 characters FITS header card
* @exception FitsException */
public FitsKeyword(byte[] card) throws FitsException {
int idx = 0; // index in FITS card
int idx_last = 0; // last index of value field
int idx_comm_first = 0; // first index of comment field
// if card is null or too short - add spaces
if (card == null || card.length < Fits.CARD) {
byte[] pc = new byte[Fits.CARD];
int n = 0;
while (n < card.length) {
pc[n] = card[n];
n++;
}
while (n < Fits.CARD) pc[n++] = SPACE;
card = pc;
}
/*
* B. Perret: ok we sometimes need to be meticulous but crashing about a keyword problem is not usefull.
* Most of the time it will have no impact... and if it has we can always throw an exception later if really needed.
*
*/
/*if ((card[0] != SPACE) && (card[0] != MINUS) && (card[0] != UNDERSCORE)
&& ((card[0] < A) || (Z < card[0]))) {
throw new FitsException("Illegal character",
FitsException.KEYWORD);
}*/
kwCard = new String(card, 0, Fits.CARD); // save keyword card
validCard = true;
name = kwCard.substring(0, 8); // get prime keyword name
String valueField = null;
comment = null;
if (name.startsWith("END ")) { // check if END card
throw new FitsException("END card", FitsException.ENDCARD);
}
if (name.startsWith("HISTORY ") // Comment keyword
|| name.startsWith("COMMENT ")
|| name.startsWith(" ")) {
type = COMMENT;
idx_comm_first = 8;
} else if (name.startsWith("HIERARCH")) { // Hierarchical keyword
StringBuffer hkw = new StringBuffer(Fits.CARD);
boolean found = false;
byte last = NULL;
idx = 8;
while ((idx<Fits.CARD) && (card[idx]!=EQUAL)) {
if (card[idx] != SPACE) {
if (last == SPACE && found) {
hkw.append('.');
}
found = true;
hkw.append((char) card[idx]);
}
last = card[idx++];
}
if (Fits.CARD <= idx) {
throw new FitsException("No equal-sign in HIERARCH keyword",
FitsException.KEYWORD);
}
name = hkw.toString();
} else if (card[8]==EQUAL) { // Prim keyword with value
idx = 8;
} else { // Comment keyword
type = COMMENT;
idx_comm_first = 8;
}
name = (name.trim()).toUpperCase(); // Force to uppercase
if (card[idx] == EQUAL) { // Keyword with value field
idx++;
while ((idx<Fits.CARD) && (card[idx]==SPACE)) idx++;
if (card[idx] == QUOTE) { // string value
idx_last = ++idx;
while ((idx_last < Fits.CARD-1)
&& ((card[idx_last] != QUOTE)
|| ((card[idx_last] == QUOTE)
&& (card[idx_last+1] == QUOTE)))) {
if (card[idx_last] == QUOTE) {
idx_last++;
}
idx_last++;
}
int n1 = idx; // convert two quotes to one
int n2 = idx;
boolean last_not_quote = true;
while (n1<idx_last) {
card[n2] = card[n1];
if (card[n1] == QUOTE) {
last_not_quote = !last_not_quote;
}
if (last_not_quote) {
n2++;
}
n1++;
}
valueField = (n1 == n2) ? kwCard.substring(idx, n2)
: new String(card, idx, n2-idx);
type = STRING;
n1 = name.lastIndexOf('.') + 1;
if (name.regionMatches(n1, "DATE", 0, 4)) {
SimpleDateFormat dateFormat = FITSDATE;
if (0<valueField.indexOf('-')) {
dateFormat = (0<valueField.indexOf('T'))
? ISOLONG : ISOSHORT;
}
dateFormat.setTimeZone(TIMEZONE);
value = dateFormat.parse(valueField, new ParsePosition(0));
type = DATE;
} else {
value = valueField.trim();
type = STRING;
}
} else {
idx_last = idx;
while ((idx_last < Fits.CARD)
&& (card[idx_last] != SPACE)
&& (card[idx_last] != SLASH)
&& (card[idx_last] != COMMA)) {
idx_last++;
}
try{
valueField = kwCard.substring(idx, idx_last);
}catch(StringIndexOutOfBoundsException e){
valueField = "";
System.err.println("FITS warning: incorrect card <"+ kwCard.substring(idx)+"> missing ending char. Value ignored and start praying and maybe it will work!");
}
try {
if (0<=valueField.indexOf('.')) {
value = new Double(valueField);
type = REAL;
} else {
value = new Integer(valueField);
type = INTEGER;
}
} catch (NumberFormatException e) {
value = new Boolean(valueField.regionMatches(true, 0,
"T", 0, 1));
type = BOOLEAN;
}
}
while ((idx_last < Fits.CARD) // find comment field
&& (card[idx_last] != SLASH)) {
idx_last++;
}
if ((idx_last < Fits.CARD) && (card[idx_last] == SLASH)) {
idx_comm_first = idx_last+1;
}
}
if (0<idx_comm_first) { // get keyword comment
comment = kwCard.substring(idx_comm_first, Math.min(Fits.CARD,kwCard.length()));
comment = comment.trim();
} else {
comment = new String("");
}
}
/** Constructor for FitsKeyword class from String.
*
* @param card String with 80 characters FITS header card
* @exception FitsException */
public FitsKeyword(String card) throws FitsException {
this(card.getBytes());
}
/** Constructor for FitsKeyword class specifying name and
* comment for a comment keyword'
*
* @param name String with name of keyword
* @param comment String with keyword comment */
public FitsKeyword(String name, String comment) {
setName(name);
this.comment = comment;
type = COMMENT;
validCard = false;
}
/** Constructor for FitsKeyword class specifying name, value and
* comment for a string keyword.
*
* @param name String with name of keyword
* @param value String value of keyword
* @param comment String with keyword comment */
public FitsKeyword(String name, String value, String comment) {
setName(name);
this.value = value;
this.comment = comment;
type = STRING;
validCard = false;
}
/** Constructor for FitsKeyword class specifying name, value and
* comment for a boolean keyword.
*
* @param name String with name of keyword
* @param value boolean value of keyword
* @param comment String with keyword comment */
public FitsKeyword(String name, boolean value, String comment) {
setName(name);
this.value = new Boolean(value);
this.comment = comment;
type = BOOLEAN;
validCard = false;
}
/** Constructor for FitsKeyword class specifying name, value and
* comment for an integer keyword.
*
* @param name String with name of keyword
* @param value int value of keyword
* @param comment String with keyword comment */
public FitsKeyword(String name, int value, String comment) {
setName(name);
this.value = new Integer(value);
this.comment = comment;
type = INTEGER;
validCard = false;
}
/** Constructor for FitsKeyword class specifying name, value and
* comment for a real keyword.
*
* @param name String with name of keyword
* @param value double value of keyword
* @param comment String with keyword comment */
public FitsKeyword(String name, double value, String comment) {
setName(name);
this.value = new Double(value);
this.comment = comment;
type = REAL;
validCard = false;
}
/** Constructor for FitsKeyword class specifying name, value and
* comment for a date keyword.
*
* @param name String with name of keyword
* @param value Date value of keyword
* @param comment String with keyword comment */
public FitsKeyword(String name, Date value, String comment) {
setName(name);
this.value = value;
this.comment = comment;
type = DATE;
validCard = false;
}
/** Method provides the value of a FITS keyword as boolean. For
* INTEGER type keywords, all none-zero values will return true.
* The method returns FALSE for all keyword types other than
* BOOLEAN, REAL and INTEGER. */
public final boolean getBool() {
if (type==BOOLEAN) {
return ((Boolean)value).booleanValue();
} else if (type==INTEGER) {
return (((Integer)value).intValue()!=0) ? true : false;
} else if (type==REAL) {
return (((Double)value).intValue()!=0) ? true : false;
}
return false;
}
/** Method provides the value of a FITS keyword as integer for
* keyword types INTEGER and REAL. Zero is returned for all
* other types. */
public final int getInt() {
if (type==INTEGER) {
return ((Integer)value).intValue();
} else if (type==REAL) {
return ((Double)value).intValue();
}
return 0;
}
/** Method provides the value of a FITS keyword as double for
* keyword types INTEGER and REAL. Zero is returned for all
* other types. */
public final double getReal() {
if (type==REAL) {
return ((Double)value).doubleValue();
} else if (type==INTEGER) {
return ((Integer)value).doubleValue();
}
return 0.0;
}
/** Method provides the value of a FITS keyword as a Date object for
* keywords of type DATE. For STRING type keywords the string is
* converted to a Date if possible otherwise a NULL pointer
* is returned. */
public final Date getDate() {
if (value == null) {
return null;
}
if (type == DATE) {
return (Date) value;
} else if (type == STRING) {
String str = (String) value;
SimpleDateFormat dateFormat = FITSDATE;
if (0<str.indexOf('-')) {
dateFormat = (0<str.indexOf('T')) ? ISOLONG : ISOSHORT;
}
dateFormat.setTimeZone(TIMEZONE);
return dateFormat.parse(str, new ParsePosition(0));
}
return null;
}
/** Method provides the value of a FITS keyword as a String. If
* not value field is defined NULL is returned. */
public final String getString() {
if (value == null) {
return null;
}
if (type == DATE) {
SimpleDateFormat dateFormat = ISOLONG;
dateFormat.setTimeZone(TIMEZONE);
return (dateFormat.format((Date) value, new StringBuffer(),
new FieldPosition(0))).toString();
}
return value.toString();
}
/** Set value field for keyword of STRING type. Note: the keyword
* type will be changed to STRING.
*
* @param value String with value of keyword value field
*/
public final void setValue(String value) {
this.value = value;
type = STRING;
validCard = false;
}
/** Set value field for keyword of BOOLEAN type. Note: the keyword
* type will be changed to BOOLEAN.
*
* @param value booelan with value of keyword value field
*/
public final void setValue(boolean value) {
this.value = new Boolean(value);
type = BOOLEAN;
validCard = false;
}
/** Set value field for keyword of INTEGER type. Note: the keyword
* type will be changed to INTEGER.
*
* @param value integer with value of keyword value field
*/
public final void setValue(int value) {
this.value = new Integer(value);
type = INTEGER;
validCard = false;
}
/** Set value field for keyword of REAL type. Note: the keyword
* type will be changed to REAL.
*
* @param value double with value of keyword value field
*/
public final void setValue(double value) {
this.value = new Double(value);
type = REAL;
validCard = false;
}
/** Set value field for keyword of DATE type. Note: the keyword
* type will be changed to DATE.
*
* @param value double with value of keyword value field
*/
public final void setValue(Date value) {
this.value = value;
type = DATE;
validCard = false;
}
/** Method generates an 80 character Sting of the keyword in FITS
* format. Note: fields may be truncated due the the 80
* char. limit. */
public String toString() {
int idx;
if (validCard) { // original FITS card valid
return kwCard;
}
StringBuffer card = new StringBuffer(80);
if ((name.length() < 9) && (name.indexOf('.') < 0)) { // Prime keyword
card.append(name);
idx = card.length();
while (idx++ < 8) card.append(" ");
} else { // Hierarchical keyword
card.append("HIERARCH ");
StringTokenizer stok = new StringTokenizer(name, ".");
while (stok.hasMoreTokens())
card.append(stok.nextToken() + " ");
}
String val = "' '";
switch (type) { // Generate keyword value string
case STRING :
StringBuffer sbuf = new StringBuffer((String) value);
if (0 <= ((String) value).indexOf('\'')) {
char[] ch = ((String) value).toCharArray();
sbuf = new StringBuffer(Fits.CARD);
for (int n=0; n<ch.length; n++) {
sbuf.append(ch[n]);
if (ch[n]=='\'') {
sbuf.append('\'');
}
}
}
while (sbuf.length()<8) {
sbuf.append(" ");
}
sbuf.insert(0, '\'');
sbuf.append('\'');
val = sbuf.toString();
break;
case INTEGER :
val = ((Integer) value).toString();
break;
case REAL :
val = ((Double) value).toString();
break;
case BOOLEAN :
if (((Boolean) value).booleanValue()) {
val = "T";
} else {
val = "F";
}
break;
case DATE :
SimpleDateFormat dateFormat = ISOLONG;
dateFormat.setTimeZone(TIMEZONE);
StringBuffer df =
new StringBuffer("'" +
dateFormat.format((Date) value,
new StringBuffer(),
new FieldPosition(0))
+ "'");
val = df.toString();
break;
case COMMENT :
card.append(comment);
break;
}
if (type!=COMMENT) {
card.append("= "); // append value of keyword
idx = val.length();
if ((card.length() < 11) && type!=STRING) {
while (idx++ < 20) card.append(" ");
}
card.append(val);
valueTruncated = false;
idx = card.length(); // finally add comment field
if (Fits.CARD < idx) { // check if name/value okay
card.setCharAt(Fits.CARD-1, '\'');
valueTruncated = true;
}
while (idx++ < 30) card.append(" ");
card.append(" / " + comment);
}
idx = card.length(); // ensure the card has 80 chars
if (Fits.CARD<idx) {
card.setLength(Fits.CARD);
} else {
while (idx++ < Fits.CARD) card.append(" ");
}
return card.toString();
}
/** Check if the keyword name or value fields were truncated
* by the last call of the toString method. */
public boolean isValueTruncated(){
return valueTruncated;
}
/** Check if FITS keyword is empty that is has all blank (' ') name
* and comment. */
public boolean isEmpty(){
return name.length()<1 && comment.length()<1 && value==null;
}
/** Check if the FITS keyword was modified since it was created
* from a FITS header card. */
public boolean isModified(){
return !validCard;
}
/** Get method to provide name of FITS keyword. */
public String getName(){
return name;
}
/** Set name of FITS keyword. */
public void setName(String name){
this.name = (name == null) ? "" : name.toUpperCase();
}
/** Get method to provide type of FITS keyword. */
public int getType(){
return type;
}
/** Get method to obtain comment of FITS keyword. */
public String getComment(){
return comment;
}
/** Set comment field of a FITS keyword
* @param comment String with the keyword comment. */
public void setComment(String comment){
this.comment = (comment == null) ? "" : comment;
}
}