Header.java
package nom.tam.fits;
/*
* #%L
* nom.tam FITS library
* %%
* Copyright (C) 2004 - 2021 nom-tam-fits
* %%
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
* #L%
*/
import static nom.tam.fits.header.Standard.BITPIX;
import static nom.tam.fits.header.Standard.BLANKS;
import static nom.tam.fits.header.Standard.COMMENT;
import static nom.tam.fits.header.Standard.CONTINUE;
import static nom.tam.fits.header.Standard.END;
import static nom.tam.fits.header.Standard.EXTEND;
import static nom.tam.fits.header.Standard.GCOUNT;
import static nom.tam.fits.header.Standard.GROUPS;
import static nom.tam.fits.header.Standard.HISTORY;
import static nom.tam.fits.header.Standard.NAXIS;
import static nom.tam.fits.header.Standard.NAXISn;
import static nom.tam.fits.header.Standard.PCOUNT;
import static nom.tam.fits.header.Standard.SIMPLE;
import static nom.tam.fits.header.Standard.TFIELDS;
import static nom.tam.fits.header.Standard.XTENSION;
import static nom.tam.fits.header.Standard.XTENSION_BINTABLE;
import static nom.tam.fits.header.extra.CXCExt.LONGSTRN;
import java.io.EOFException;
import java.io.IOException;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import nom.tam.fits.FitsFactory.FitsSettings;
import nom.tam.fits.header.Bitpix;
import nom.tam.fits.header.IFitsHeader;
import nom.tam.util.ArrayDataInput;
import nom.tam.util.ArrayDataOutput;
import nom.tam.util.AsciiFuncs;
import nom.tam.util.ComplexValue;
import nom.tam.util.Cursor;
import nom.tam.util.FitsIO;
import nom.tam.util.HashedList;
import nom.tam.util.LoggerHelper;
import nom.tam.util.RandomAccess;
/**
* This class describes methods to access and manipulate the header for a FITS
* HDU. This class does not include code specific to particular types of HDU. As
* of version 1.1 this class supports the long keyword convention which allows
* long string keyword values to be split among multiple keywords
*
* <pre>
* KEY = 'ABC&' /A comment
* CONTINUE 'DEF&' / Another comment
* CONTINUE 'GHIJKL '
* </pre>
*
* The methods getStringValue(key), addValue(key,value,comment) and
* deleteCard(key) will get, create/update and delete long string values if the
* longStringsEnabled flag is set. This flag is set automatically when a FITS
* header with a LONGSTRN card is found. The value is not checked. It may also
* be set/unset using the static method setLongStringsEnabled(boolean). [So if a
* user wishes to ensure that it is not set, it should be unset after any header
* is read] When long strings are found in the FITS header users should be
* careful not to interpose new header cards within a long value sequence. When
* writing long strings, the comment is included in the last card. If a user is
* writing long strings, a the keyword LONGSTRN = 'OGIP 1.0' should be added to
* the FITS header, but this is not done automatically for the user.
*/
public class Header implements FitsElement {
private static final Logger LOG = Logger.getLogger(Header.class.getName());
private static final int MIN_NUMBER_OF_CARDS_FOR_VALID_HEADER = 4;
/**
* The actual header data stored as a HashedList of HeaderCard's.
*/
private final HashedList<HeaderCard> cards;
/** Offset of this Header in the FITS file */
private long fileOffset;
private List<HeaderCard> duplicates;
/** Input descriptor last time header was read */
private ArrayDataInput input;
/**
* The mimimum number of cards to write, including blank header space as
* described in the FITS 4.0 standard.
*/
private int minCards;
/**
* The number of bytes that this header occupied in file.
* (for re-writing).
*/
private long readSize;
/**
* the sorter used to sort the header cards defore writing the header.
*/
private Comparator<String> headerSorter;
/**
* Create a header by reading the information from the input stream.
*
* @param dis
* The input stream to read the data from.
* @return <CODE>null</CODE> if there was a problem with the header;
* otherwise return the header read from the input stream.
* @throws TruncatedFileException
* if the stream ended prematurely
* @throws IOException
* if the header could not be read.
*/
public static Header readHeader(ArrayDataInput dis) throws TruncatedFileException, IOException {
Header myHeader = new Header();
try {
myHeader.read(dis);
} catch (EOFException e) {
// An EOF exception is thrown only if the EOF was detected
// when reading the first card. In this case we want
// to return a null.
return null;
}
return myHeader;
}
/**
* please use {@link FitsFactory#setLongStringsEnabled(boolean)} instead.
*
* @param flag
* the new value for long-string enabling.
*/
@Deprecated
public static void setLongStringsEnabled(boolean flag) {
FitsFactory.setLongStringsEnabled(flag);
}
/** Create an empty header */
public Header() {
this.cards = new HashedList<>();
this.headerSorter = new HeaderOrder();
this.duplicates = null;
clear();
}
/**
* Create a header and populate it from the input stream
*
* @param is
* The input stream where header information is expected.
* @throws IOException
* if the header could not be read.
* @throws TruncatedFileException
* if the stream ended prematurely
*/
public Header(ArrayDataInput is) throws TruncatedFileException, IOException {
this();
read(is);
}
/**
* Create a header which points to the given data object.
*
* @param o
* The data object to be described.
* @throws FitsException
* if the data was not valid for this header.
*/
public Header(Data o) throws FitsException {
this();
o.fillHeader(this);
}
/**
* Create a header and initialize it with a vector of strings.
*
* @param newCards
* Card images to be placed in the header.
*/
public Header(String[] newCards) {
this();
for (String newCard : newCards) {
this.cards.add(HeaderCard.create(newCard));
}
}
/**
* <p>
* Preallocates a minimum header card space. When written to a stream, the header will be large enough to
* hold at least the specified number of cards. If the header has fewer physical cards
* then the remaining space will be padded with blanks, leaving space for future additions, as specified
* by the FITS 4.0 standard for <a href="https://fits.gsfc.nasa.gov/registry/headerspace.html">
* preallocated header space</a>.
* </p>
* <p>
* This method is also called by {@link #read(ArrayDataInput)}, with the number of cards (including
* reserved blank space) contained in the header input stream, in order to ensure that the header remains
* rewritable even if it is shortened by the removal of cards (explicitly, or because they were
* duplicates).
* </p>
* <p>
* A new setting always overrides prior ones. For example, calling this method with an argument
* that is %lt;=1 will eliminate (reset) any prior preallocated header space.
* </p>
*
* @param nCards the mimimum number of 80-character header records that is header
* must be able to support when written to a stream, including
* preallocated blank header space.
*
* @since 1.16
*
* @see #getMinimumSize()
* @see #write(ArrayDataOutput)
* @see #read(ArrayDataInput)
* @see #resetOriginalSize()
*
*/
public void ensureCardSpace(int nCards) {
if (nCards < 1) {
nCards = 1;
}
this.minCards = nCards;
}
/**
* Insert a new header card at the current position, deleting any prior
* occurence of the same card while maintaining the current position to
* point to after the newly inserted card.
*
* @param fcard
* The card to be inserted.
*/
public void addLine(HeaderCard fcard) {
if (fcard != null) {
cursor().add(fcard);
}
}
/**
* Add or replace a key with the given boolean value and comment.
*
* @param key
* The header key.
* @param val
* The boolean value.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(String, Boolean, String)
*/
public HeaderCard addValue(IFitsHeader key, Boolean val) throws HeaderCardException {
return addValue(key.key(), val, key.comment());
}
/**
* Add or replace a key with the given double value and comment. Note that
* float values will be promoted to doubles.
*
* @param key
* The header key.
* @param val
* The double value.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(String, Number, String)
*/
public HeaderCard addValue(IFitsHeader key, Number val) throws HeaderCardException {
return addValue(key.key(), val, key.comment());
}
/**
* Add or replace a key with the given string value and comment.
*
* @param key
* The header key.
* @param val
* The string value.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(String, String, String)
*/
public HeaderCard addValue(IFitsHeader key, String val) throws HeaderCardException {
return addValue(key.key(), val, key.comment());
}
/**
* Add or replace a key with the given boolean value and comment.
*
* @param key
* The header key.
* @param val
* The boolean value.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(IFitsHeader, Boolean)
* @see HeaderCard#HeaderCard(String, Boolean, String)
*/
public HeaderCard addValue(String key, Boolean val, String comment) throws HeaderCardException {
HeaderCard hc = new HeaderCard(key, val, comment);
addLine(hc);
return hc;
}
/**
* Add or replace a key with the given number value and comment. The value will be represented in the
* header card with use the native precision of the value or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS},
* whichever fits in the available card space. Trailing zeroes will be ommitted.
*
* @param key
* The header key.
* @param val
* The number value.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(String, Number, int, String)
* @see #addValue(IFitsHeader, Number)
* @see HeaderCard#HeaderCard(String, Number, String)
*/
public HeaderCard addValue(String key, Number val, String comment) throws HeaderCardException {
HeaderCard hc = new HeaderCard(key, val, comment);
addLine(hc);
return hc;
}
/**
* Add or replace a key with the given number value and comment, using up to the specified decimal
* places after the leading figure. Trailing zeroes will be ommitted.
*
* @param key
* The header key.
* @param val
* The number value.
* @param decimals
* The number of decimal places to show after the leading figure, or {@link nom.tam.util.FlexFormat#AUTO_PRECISION}
* to use the native precision of the value or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS},
* whichever fits in the available card space.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(String, Number, String)
* @see HeaderCard#HeaderCard(String, Number, int, String)
*/
public HeaderCard addValue(String key, Number val, int decimals, String comment) throws HeaderCardException {
HeaderCard hc = new HeaderCard(key, val, decimals, comment);
addLine(hc);
return hc;
}
/**
* Add or replace a key with the given complex number value and comment. Trailing zeroes will be ommitted.
*
* @param key
* The header keyword.
* @param val
* The complex number value.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @since 1.16
*
* @see #addValue(String, ComplexValue, int, String)
* @see HeaderCard#HeaderCard(String, ComplexValue, String)
*/
public HeaderCard addValue(String key, ComplexValue val, String comment) throws HeaderCardException {
HeaderCard hc = new HeaderCard(key, val, comment);
addLine(hc);
return hc;
}
/**
* Add or replace a key with the given complex number value and comment, using up to the specified decimal
* places after the leading figure. Trailing zeroes will be ommitted.
*
* @param key
* The header keyword.
* @param val
* The complex number value.
* @param decimals
* The number of decimal places to show after the leading figure, or {@link nom.tam.util.FlexFormat#AUTO_PRECISION}
* to use the native precision of the value, or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS},
* whichever fits in the available card space.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @since 1.16
*
* @see #addValue(String, ComplexValue, String)
* @see HeaderCard#HeaderCard(String, ComplexValue, int, String)
*/
public HeaderCard addValue(String key, ComplexValue val, int decimals, String comment) throws HeaderCardException {
HeaderCard hc = new HeaderCard(key, val, decimals, comment);
addLine(hc);
return hc;
}
/**
* Add or replace a key with the given integer value in hexadecimal representation,
* and comment.
*
* @param key
* The header key.
* @param val
* The integer value.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @since 1.16
*
* @see #addValue(String, Number, String)
* @see HeaderCard#createHexValueCard(String, long)
* @see #getHexValue(String)
*/
public HeaderCard addHexValue(String key, long val, String comment) throws HeaderCardException {
HeaderCard hc = HeaderCard.createHexValueCard(key, val, comment);
addLine(hc);
return hc;
}
/**
* Add or replace a key with the given string value and comment.
*
* @param key
* The header key.
* @param val
* The string value.
* @param comment
* A comment to append to the card.
* @return the new card that was added.
* @throws HeaderCardException
* If the parameters cannot build a valid FITS card.
*
* @see #addValue(IFitsHeader, String)
* @see HeaderCard#HeaderCard(String, String, String)
*/
public HeaderCard addValue(String key, String val, String comment) throws HeaderCardException {
HeaderCard hc = new HeaderCard(key, val, comment);
addLine(hc);
return hc;
}
/**
* get a builder for filling the header cards using the builder pattern.
*
* @param key
* the key for the first card.
* @return the builder for header cards.
*/
public HeaderCardBuilder card(IFitsHeader key) {
return new HeaderCardBuilder(this, key);
}
/**
* Tests if the specified keyword is present in this table.
*
* @param key
* the keyword to be found.
* @return <code>true</code> if the specified keyword is present in this
* table; <code>false</code> otherwise.
*/
public final boolean containsKey(IFitsHeader key) {
return this.cards.containsKey(key.key());
}
/**
* Tests if the specified keyword is present in this table.
*
* @param key
* the keyword to be found.
* @return <code>true</code> if the specified keyword is present in this
* table; <code>false</code> otherwise.
*/
public final boolean containsKey(String key) {
return this.cards.containsKey(key);
}
/**
* Delete the card associated with the given key. Nothing occurs if the key
* is not found.
*
* @param key
* The header key.
*/
public void deleteKey(IFitsHeader key) {
deleteKey(key.key());
}
/**
* Delete the card associated with the given key. Nothing occurs if the key
* is not found.
*
* @param key
* The header key.
*/
public void deleteKey(String key) {
// AK: This version will not move the current position to the deleted
// key
if (containsKey(key)) {
this.cards.remove(this.cards.get(key));
}
}
/**
* Print the header to a given stream.
*
* @param ps
* the stream to which the card images are dumped.
*/
public void dumpHeader(PrintStream ps) {
Cursor<String, HeaderCard> iter = iterator();
while (iter.hasNext()) {
ps.println(iter.next());
}
}
/**
* Find the card associated with a given key. If found this sets the mark to
* the card, otherwise it unsets the mark.
*
* @param key
* The header key.
* @return <CODE>null</CODE> if the keyword could not be found; return the
* HeaderCard object otherwise.
*/
public HeaderCard findCard(IFitsHeader key) {
return this.findCard(key.key());
}
/**
* Find the card associated with a given key. If found this sets the mark to
* the card, otherwise it unsets the mark.
*
* @param key
* The header key.
* @return <CODE>null</CODE> if the keyword could not be found; return the
* HeaderCard object otherwise.
*/
public HeaderCard findCard(String key) {
HeaderCard card = this.cards.get(key);
if (card != null) {
cursor().setKey(key);
}
return card;
}
/**
* Find the card associated with a given key.
*
* @param key
* The header key.
* @return <CODE>null</CODE> if the keyword could not be found; return the
* card image otherwise.
*/
public String findKey(String key) {
HeaderCard card = findCard(key);
if (card == null) {
return null;
}
return card.toString();
}
/**
* Get the bid decimal value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0.0 if not found.
*/
public final BigDecimal getBigDecimalValue(IFitsHeader key) {
return getBigDecimalValue(key.key());
}
/**
* Get the big decimal value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to return if the key cannot be found.
* @return the associated value.
*/
public final BigDecimal getBigDecimalValue(IFitsHeader key, BigDecimal dft) {
return getBigDecimalValue(key.key(), dft);
}
/**
* Get the big decimal value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0.0 if not found.
*/
public final BigDecimal getBigDecimalValue(String key) {
return getBigDecimalValue(key, BigDecimal.ZERO);
}
/**
* Get the big decimal value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to return if the key cannot be found.
* @return the associated value.
*/
public BigDecimal getBigDecimalValue(String key, BigDecimal dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(BigDecimal.class, dft);
}
/**
* Get the big integer value associated with the given key.
*
* @param key
* The header key.
*
* @return the associated value or 0 if not found.
*/
public final BigInteger getBigIntegerValue(IFitsHeader key) {
return getBigIntegerValue(key.key());
}
/**
* Get the big integer value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to be returned if the key cannot be found.
* @return the associated value.
*/
public final BigInteger getBigIntegerValue(IFitsHeader key, BigInteger dft) {
return getBigIntegerValue(key.key(), dft);
}
/**
* Get the big integer value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0 if not found.
*/
public final BigInteger getBigIntegerValue(String key) {
return getBigIntegerValue(key, BigInteger.ZERO);
}
/**
* Get the big integer value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to be returned if the key cannot be found.
* @return the associated value.
*/
public BigInteger getBigIntegerValue(String key, BigInteger dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(BigInteger.class, dft);
}
/**
* Get the complex number value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or {@link ComplexValue#ZERO} if not found.
*
* @since 1.16
*
* @see #getComplexValue(String, ComplexValue)
* @see HeaderCard#getValue(Class, Object)
* @see #addValue(String, ComplexValue, String)
*/
public final ComplexValue getComplexValue(String key) {
return getComplexValue(key, ComplexValue.ZERO);
}
/**
* Get the complex number value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to return if the key cannot be found.
* @return the associated value.
*
* @since 1.16
*
* @see #getComplexValue(String)
* @see HeaderCard#getValue(Class, Object)
* @see #addValue(String, ComplexValue, String)
*/
public ComplexValue getComplexValue(String key, ComplexValue dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(ComplexValue.class, dft);
}
/**
* Get the <CODE>boolean</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The value found, or false if not found or if the keyword is not a
* logical keyword.
*/
public final boolean getBooleanValue(IFitsHeader key) {
return getBooleanValue(key.key());
}
/**
* Get the <CODE>boolean</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The value to be returned if the key cannot be found or if the
* parameter does not seem to be a boolean.
* @return the associated value.
*/
public final boolean getBooleanValue(IFitsHeader key, boolean dft) {
return getBooleanValue(key.key(), dft);
}
/**
* Get the <CODE>boolean</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The value found, or false if not found or if the keyword is not a
* logical keyword.
*/
public final boolean getBooleanValue(String key) {
return getBooleanValue(key, false);
}
/**
* Get the <CODE>boolean</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The value to be returned if the key cannot be found or if the
* parameter does not seem to be a boolean.
* @return the associated value.
*/
public boolean getBooleanValue(String key, boolean dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(Boolean.class, dft).booleanValue();
}
/**
* Get the n'th card image in the header
*
* @param n
* the card index to get
* @return the card image; return <CODE>null</CODE> if the n'th card does
* not exist.
* @deprecated An iterator from {@link #iterator(int)} or
* {@link #iterator()} should be used for sequential access to
* the header.
*/
@Deprecated
public String getCard(int n) {
if (n >= 0 && n < this.cards.size()) {
return this.cards.get(n).toString();
}
return null;
}
/**
* Return the size of the data including any needed padding.
*
* @return the data segment size including any needed padding.
*/
public long getDataSize() {
return FitsUtil.addPadding(trueDataSize());
}
/**
* Get the <CODE>double</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0.0 if not found.
*/
public final double getDoubleValue(IFitsHeader key) {
return getDoubleValue(key.key());
}
/**
* Get the <CODE>double</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to return if the key cannot be found.
* @return the associated value.
*/
public final double getDoubleValue(IFitsHeader key, double dft) {
return getDoubleValue(key.key(), dft);
}
/**
* Get the <CODE>double</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0.0 if not found.
*/
public final double getDoubleValue(String key) {
return getDoubleValue(key, 0.0);
}
/**
* Get the <CODE>double</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to return if the key cannot be found.
* @return the associated value.
*/
public double getDoubleValue(String key, double dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(Double.class, dft).doubleValue();
}
/**
* @return the list of duplicate cards. Note that when the header is read
* in, only the last entry for a given keyword is retained in the
* active header. This method returns earlier cards that have been
* discarded in the order in which they were encountered in the
* header. It is possible for there to be many cards with the same
* keyword in this list.
*/
public List<HeaderCard> getDuplicates() {
return this.duplicates;
}
/**
* @return Get the offset of this header
*/
@Override
public long getFileOffset() {
return this.fileOffset;
}
/**
* Get the <CODE>float</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0.0 if not found.
*/
public final float getFloatValue(IFitsHeader key) {
return getFloatValue(key.key());
}
/**
* @return the <CODE>float</CODE> value associated with the given key.
* @param key
* The header key.
* @param dft
* The value to be returned if the key is not found.
*/
public final float getFloatValue(IFitsHeader key, float dft) {
return getFloatValue(key.key(), dft);
}
/**
* Get the <CODE>float</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0.0 if not found.
*/
public final float getFloatValue(String key) {
return getFloatValue(key, 0.0F);
}
/**
* @return the <CODE>float</CODE> value associated with the given key.
* @param key
* The header key.
* @param dft
* The value to be returned if the key is not found.
*/
public float getFloatValue(String key, float dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(Float.class, dft).floatValue();
}
/**
* Get the <CODE>int</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0 if not found.
*/
public final int getIntValue(IFitsHeader key) {
return (int) getLongValue(key);
}
/**
* @return the value associated with the key as an int.
* @param key
* The header key.
* @param dft
* The value to be returned if the key is not found.
*/
public final int getIntValue(IFitsHeader key, int dft) {
return (int) getLongValue(key, dft);
}
/**
* Get the <CODE>int</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0 if not found.
*/
public final int getIntValue(String key) {
return (int) getLongValue(key);
}
/**
* @return the value associated with the key as an int.
* @param key
* The header key.
* @param dft
* The value to be returned if the key is not found.
*/
public int getIntValue(String key, int dft) {
return (int) getLongValue(key, dft);
}
/**
* Get the n'th key in the header.
*
* @param n
* the index of the key
* @return the card image; return <CODE>null</CODE> if the n'th key does not
* exist.
* @deprecated An iterator from {@link #iterator(int)} or
* {@link #iterator()} should be used for sequential access to
* the header.
*/
@Deprecated
public String getKey(int n) {
if (n >= 0 && n < this.cards.size()) {
return this.cards.get(n).getKey();
}
return null;
}
/**
* Get the <CODE>long</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0 if not found.
*/
public final long getLongValue(IFitsHeader key) {
return getLongValue(key.key());
}
/**
* Get the <CODE>long</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to be returned if the key cannot be found.
* @return the associated value.
*/
public final long getLongValue(IFitsHeader key, long dft) {
return getLongValue(key.key(), dft);
}
/**
* Get the <CODE>long</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0 if not found.
*/
public final long getLongValue(String key) {
return getLongValue(key, 0L);
}
/**
* Get the <CODE>long</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value to be returned if the key cannot be found.
* @return the associated value.
*/
public long getLongValue(String key, long dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
return fcard.getValue(Long.class, dft).longValue();
}
/**
* Get the <CODE>long</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or 0 if not found.
*
* @since 1.16
*
* @see #getHexValue(String, long)
* @see HeaderCard#getHexValue()
* @see #addHexValue(String, long, String)
*/
public final long getHexValue(String key) {
return getHexValue(key, 0L);
}
/**
* Get the <CODE>long</CODE> value stored in hexadecimal format under the specified key.
*
* @param key
* The header key.
* @param dft
* The default value to be returned if the key cannot be found.
* @return the associated value.
*
* @since 1.16
*
* @see #getHexValue(String)
* @see HeaderCard#getHexValue()
* @see #addHexValue(String, long, String)
*/
public long getHexValue(String key, long dft) {
HeaderCard fcard = findCard(key);
if (fcard == null) {
return dft;
}
try {
return fcard.getHexValue();
} catch (NumberFormatException e) {
return dft;
}
}
/**
* Returns the nominal number of currently defined cards in this header. Each card can
* consist of one or more 80-character wide header records.
*
* @return the number of nominal cards in the header
*
* @see #getNumberOfPhysicalCards()
*/
public int getNumberOfCards() {
return this.cards.size();
}
/**
* Returns the number of 80-character header records in this header, including
* an END marker (whether or not it is currently contained).
*
* @return the number of physical cards in the header, including the END marker.
*
* @see #getNumberOfCards()
* @see #getSize()
*/
public int getNumberOfPhysicalCards() {
int count = 0;
for (HeaderCard card : this.cards) {
count += card.cardSize();
}
// AK: Count the END card, which may not have been added yet...
if (!containsKey(END)) {
count++;
}
return count;
}
/**
* Returns the minimum number of bytes that will be written by this header, either
* as the original byte size of a header that was read, or else the minimum
* preallocated capacity after setting {@link #ensureCardSpace(int)}.
*
* @return the minimum byte size for this header. The actual header may take up
* more space than that (but never less!), depending on the number of cards
* contained.
*
* @since 1.16
*
* @see #ensureCardSpace(int)
* @see #read(ArrayDataInput)
*/
public long getMinimumSize() {
return FitsUtil.addPadding((long) this.minCards * HeaderCard.FITS_HEADER_CARD_SIZE);
}
/**
* Returns the original size of the header in the stream from which it was read.
*
* @return the size of the original header in bytes, or 0 if the header was not
* read from a stream.
*
* @see #read(ArrayDataInput)
* @see #getMinimumSize()
*/
public final long getOriginalSize() {
return readSize;
}
/**
* Returns the current byte size of this header.
*
* @return the size of the header in bytes, or 0 if the header is invalid.
*
* @see #getMinimumSize()
* @see #ensureCardSpace(int)
*/
@Override
public final long getSize() {
return headerSize();
}
/**
* Get the <CODE>String</CODE> value associated with the given standard key.
*
* @param key
* The standard header key.
* @return The associated value or null if not found or if the value is not
* a string.
*
* @see #getStringValue(String)
* @see #getStringValue(IFitsHeader, String)
*/
public final String getStringValue(IFitsHeader key) {
return getStringValue(key.key());
}
/**
* Get the <CODE>String</CODE> value associated with the given standard key.
*
* @param key
* The standard header key.
* @param dft
* The default value.
* @return The associated value or the default value if not found or if the value is not
* a string.
*
* @see #getStringValue(String, String)
* @see #getStringValue(IFitsHeader)
*
*/
public final String getStringValue(IFitsHeader key, String dft) {
return getStringValue(key.key(), dft);
}
/**
* Get the <CODE>String</CODE> value associated with the given key.
*
* @param key
* The header key.
* @return The associated value or null if not found or if the value is not
* a string.
*
* @see #getStringValue(IFitsHeader)
* @see #getStringValue(String, String)
*/
public final String getStringValue(String key) {
return getStringValue(key, null);
}
/**
* Get the <CODE>String</CODE> value associated with the given key.
*
* @param key
* The header key.
* @param dft
* The default value.
* @return The associated value or the default value if not found or if the value is not
* a string.
*
* @see #getStringValue(IFitsHeader, String)
* @see #getStringValue(String)
*/
public String getStringValue(String key, String dft) {
HeaderCard fcard = findCard(key);
if (fcard == null || !fcard.isStringValue()) {
return dft;
}
return fcard.getValue();
}
/**
* @return Were duplicate header keys found when this record was read in?
*/
public boolean hadDuplicates() {
return this.duplicates != null;
}
/**
* Adds a line to the header using the COMMENT style, i.e., no '=' in column
* 9. The comment text may be truncated to fit into a single record, which is
* returned. Alternatively, you can split longer comments among multiple consecutive
* cards of the same type by {@link #insertCommentStyleMultiline(String, String)}.
*
* @param key
* The comment style header keyword, or <code>null</code> for an
* empty comment line.
* @param comment
* A string comment to follow. Illegal characters will be replaced by '?' and the
* comment may be truncated to fit into the card-space (71 characters).
* @return The new card that was inserted, or <code>null</code> if the keyword itself was
* invalid or the comment was <code>null</code>.
*
* @see #insertCommentStyleMultiline(String, String)
* @see HeaderCard#createCommentStyleCard(String, String)
*
*/
public HeaderCard insertCommentStyle(String key, String comment) {
if (comment == null) {
comment = "";
} else if (comment.length() > HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH) {
comment = comment.substring(0, HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH);
LOG.warning("Truncated comment to fit card: [" + comment + "]");
}
try {
HeaderCard hc = HeaderCard.createCommentStyleCard(key, HeaderCard.sanitize(comment));
cursor().add(hc);
return hc;
} catch (HeaderCardException e) {
LOG.log(Level.WARNING, "Ignoring comment card with invalid key [" + HeaderCard.sanitize(key) + "]", e);
return null;
}
}
/**
* Adds a line to the header using the COMMENT style, i.e., no '=' in column
* 9. If the comment does not fit in a single record, then it will be split
* (wrapped) among multiple consecutive records with the same keyword. Wrapped
* lines will end with '&' (not itself a standard) to indicate comment cards
* that might belong together.
*
* @param key
* The comment style header keyword, or <code>null</code> for an
* empty comment line.
* @param comment
* A string comment to follow. Illegal characters will be replaced by '?' and the
* comment may be split among multiple records as necessary to be fully preserved.
* @return The number of cards inserted.
*
* @since 1.16
*
* @see #insertCommentStyle(String, String)
* @see #insertComment(String)
* @see #insertUnkeyedComment(String)
* @see #insertHistory(String)
*/
public int insertCommentStyleMultiline(String key, String comment) {
// Empty comments must have at least one space char to write at least one
// comment card...
if (comment == null) {
comment = " ";
} else if (comment.isEmpty()) {
comment = " ";
}
int n = 0;
for (int from = 0; from < comment.length();) {
int to = from + HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH;
String part = null;
if (to < comment.length()) {
part = comment.substring(from, --to) + "&";
} else {
part = comment.substring(from);
}
if (insertCommentStyle(key, part) == null) {
return n;
}
from = to;
n++;
}
return n;
}
/**
* Adds one or more consecutive COMMENT records, wrapping the comment text as necessary.
*
* @param value
* The comment.
* @return The number of consecutive COMMENT cards that were inserted
*
* @see #insertCommentStyleMultiline(String, String)
* @see #insertUnkeyedComment(String)
* @see #insertHistory(String)
* @see HeaderCard#createCommentCard(String)
*/
public int insertComment(String value) {
return insertCommentStyleMultiline(COMMENT.key(), value);
}
/**
* Adds one or more consecutive comment records with no keyword (bytes 1-9 left blank),
* wrapping the comment text as necessary.
*
* @param value
* The comment.
* @return The number of consecutive comment-style cards with no keyword (blank keyword) that were inserted.
*
* @since 1.16
*
* @see #insertCommentStyleMultiline(String, String)
* @see #insertComment(String)
* @see #insertHistory(String)
* @see HeaderCard#createUnkeyedCommentCard(String)
* @see #insertBlankCard()
*/
public int insertUnkeyedComment(String value) {
return insertCommentStyleMultiline(BLANKS.key(), value);
}
/**
* Adds a blank card into the header.
*
* @since 1.16
*
* @see #insertUnkeyedComment(String)
*/
public void insertBlankCard() {
insertCommentStyle(null, null);
}
/**
* Adds one or more consecutive a HISTORY records, wrapping the comment text as necessary.
*
* @param value
* The history record.
* @return The number of consecutive HISTORY cards that were inserted
*
* @see #insertCommentStyleMultiline(String, String)
* @see #insertComment(String)
* @see #insertUnkeyedComment(String)
* @see HeaderCard#createHistoryCard(String)
*/
public int insertHistory(String value) {
return insertCommentStyleMultiline(HISTORY.key(), value);
}
/** @return an iterator over the header cards */
public Cursor<String, HeaderCard> iterator() {
return this.cards.iterator(0);
}
/**
* @return an iterator over the header cards starting at an index
* @param index
* the card index to start the iterator
*/
public Cursor<String, HeaderCard> iterator(int index) {
return this.cards.iterator(index);
}
/**
* Return the iterator that represents the current position in the header.
* This provides a connection between editing headers through Header
* add/append/update methods, and via Cursors, which can be used
* side-by-side while maintaining desired card ordering. For the reverse
* direction ( translating iterator position to current position in the
* header), we can just use findCard().
*
* @return the iterator representing the current position in the header.
*/
private Cursor<String, HeaderCard> cursor() {
return this.cards.cursor();
}
/**
* @return Create the data element corresponding to the current header
* @throws FitsException
* if the header did not contain enough information to detect
* the type of the data
*/
public Data makeData() throws FitsException {
return FitsFactory.dataFactory(this);
}
/**
* @return the next card in the Header using the current iterator
*/
public HeaderCard nextCard() {
if (cursor().hasNext()) {
return cursor().next();
}
return null;
}
/**
* Create a header which points to the given data object.
*
* @param o
* The data object to be described.
* @throws FitsException
* if the data was not valid for this header.
* @deprecated Use the appropriate Header constructor.
*/
@Deprecated
public void pointToData(Data o) throws FitsException {
o.fillHeader(this);
}
/**
* Remove all cards and reset the header to its default status.
*
*/
private void clear() {
cards.clear();
duplicates = null;
readSize = 0;
fileOffset = -1;
minCards = 0;
}
/**
* Checks if the header is empty, that is if it contains no cards at all.
*
* @return <code>true</code> if the header contains no cards, otherwise <code>false</code>.
*
* @since 1.16
*/
public boolean isEmpty() {
return cards.isEmpty();
}
/**
* <p>
* Reads new header data from an input, discarding any prior content.
* </p>
* <p>
* As of 1.16, the header is ensured to (re)write at least the same number of
* cards as before, padding with blanks as necessary, unless the user resets the preallocated card
* space with a call to {@link #ensureCardSpace(int)}.
* </p>
*
* @param dis
* The input stream to read the data from.
* @throws TruncatedFileException
* the the stream ended prematurely
* @throws IOException
* if the operation failed
*
* @see #ensureCardSpace(int)
*/
@SuppressWarnings("deprecation")
@Override
public void read(ArrayDataInput dis) throws TruncatedFileException, IOException {
// AK: Start afresh, in case the header had prior contents from before.
clear();
if (dis instanceof RandomAccess) {
this.fileOffset = FitsUtil.findOffset(dis);
} else {
this.fileOffset = -1;
}
int trailingBlanks = 0;
HeaderCardCountingArrayDataInput cardCountingArray = new HeaderCardCountingArrayDataInput(dis);
try {
for (;;) {
HeaderCard fcard = new HeaderCard(cardCountingArray);
// AK: Note, 'key' can never be null, as per contract of getKey(). So no need to check...
String key = fcard.getKey();
if (isEmpty()) {
checkFirstCard(key);
} else if (fcard.isBlank()) {
// AK: We don't add the trailing blank cards, but keep count of them.
// (esp. in case the aren't trailing...)
trailingBlanks++;
continue;
} else if (END.key().equals(key)) {
addLine(fcard);
break; // Out of reading the header.
} else if (LONGSTRN.key().equals(key)) {
// We don't check the value here. If the user
// wants to be sure that long strings are disabled,
// they can call setLongStringsEnabled(false) after
// reading the header.
FitsFactory.setLongStringsEnabled(true);
}
// AK: The preceding blank spaces were internal, not trailing
// so add them back in now...
for (int i = 0; i < trailingBlanks; i++) {
insertBlankCard();
}
trailingBlanks = 0;
if (this.cards.containsKey(key)) {
addDuplicate(this.cards.get(key));
}
addLine(fcard);
}
} catch (EOFException e) {
// Normal end-of-file before END key...
throw e;
} catch (Exception e) {
if (isEmpty() && FitsFactory.getAllowTerminalJunk()) {
// If this happened where we expect a new header to start, then
// treat is as if end-of-file if terminal junk is allowed
forceEOF("Junk detected at " + this.fileOffset + ".", e);
}
if (e instanceof TruncatedFileException) {
throw (TruncatedFileException) e;
}
throw new IOException("Invalid FITS Header" + (isEmpty()
? "" : ":\n\n --> Try FitsFactory.setAllowTerminalJunk(true) prior to reading to work around.\n"), e);
}
if (this.fileOffset >= 0) {
this.input = dis;
}
ensureCardSpace(cardCountingArray.getPhysicalCardsRead());
readSize = FitsUtil.addPadding(this.minCards * HeaderCard.FITS_HEADER_CARD_SIZE);
// Read to the end of the current FITS block.
//
try {
dis.skipAllBytes(FitsUtil.padding(this.minCards * HeaderCard.FITS_HEADER_CARD_SIZE));
} catch (EOFException e) {
// No biggy. We got a complete header just fine, it's only that there was no
// padding before EOF. We'll just log that, but otherwise keep going.
LOG.log(Level.WARNING, "Premature end-of-file: no padding after header.", e);
}
// AK: Log if the file ends before the expected end-of-header position.
if (dis.checkTruncated()) {
// No biggy. We got a complete header just fine, it's only that there was no
// padding before EOF. We'll just log that, but otherwise keep going.
LOG.warning("Premature end-of-file: no padding after header.");
}
}
/**
* Forces an EOFException to be thrown when some other exception happened, essentially
* treating the exception to force a normal end the reading of the header.
*
* @param message the message to log.
* @param cause the exception encountered while reading the header
* @throws EOFException the EOFException we'll throw instead.
*/
private void forceEOF(String message, Exception cause) throws EOFException {
LOG.log(Level.WARNING, message, cause);
throw new EOFException("Forced EOF at " + this.fileOffset + " due to: " + message);
}
/**
* Delete a key.
*
* @param key
* The header key.
* @throws HeaderCardException
* if the operation failed
* @deprecated see {@link #deleteKey(String)}
*/
@Deprecated
public void removeCard(String key) throws HeaderCardException {
deleteKey(key);
}
/** Reset the file pointer to the beginning of the header */
@Override
public boolean reset() {
try {
FitsUtil.reposition(this.input, this.fileOffset);
return true;
} catch (Exception e) {
LOG.log(Level.WARNING, "Exception while repositioning " + this.input, e);
return false;
}
}
/**
* @deprecated Use {@link #ensureCardSpace(int)} with a 1 argument instead.
*
* <p>
* Resets any prior preallocated header space, such as was explicitly set by
* {@link #ensureCardSpace(int)}, or when the header was read from a stream
* to ensure it remains rewritable, if possible.
* </p>
* <p>
* For headers read from a stream, this will affect {@link #rewriteable()},
* so users should not call this method unless they do not intend to
* {@link #rewrite()} this header into the original FITS.
* </p>
*
* @see #ensureCardSpace(int)
* @see #read(ArrayDataInput)
* @see #getMinimumSize()
* @see #rewriteable()
* @see #rewrite()
*/
@Deprecated
public final void resetOriginalSize() {
ensureCardSpace(1);
}
/** Rewrite the header. */
@Override
public void rewrite() throws FitsException, IOException {
ArrayDataOutput dos = (ArrayDataOutput) this.input;
if (rewriteable()) {
FitsUtil.reposition(dos, this.fileOffset);
write(dos);
dos.flush();
} else {
throw new FitsException("Invalid attempt to rewrite Header.");
}
}
@Override
public boolean rewriteable() {
int writeSize = FitsUtil.addPadding(Math.max(minCards, getNumberOfPhysicalCards()) * HeaderCard.FITS_HEADER_CARD_SIZE);
return this.fileOffset >= 0 && this.input instanceof ArrayDataOutput && writeSize == getOriginalSize();
}
/**
* @deprecated Use the safer {@link #setBitpix(Bitpix)} instead.
*
* Set the BITPIX value for the header. The following values are permitted
* by FITS conventions:
* <ul>
* <li>8 -- signed byte data. Also used for tables.</li>
* <li>16 -- signed short data.</li>
* <li>32 -- signed int data.</li>
* <li>64 -- signed long data.</li>
* <li>-32 -- IEEE 32 bit floating point numbers.</li>
* <li>-64 -- IEEE 64 bit floating point numbers.</li>
* </ul>
*
* @param val
* The value set by the user.
* @throws IllegalArgumentException if the value is not a valid BITPIX value.
*
* @see #setBitpix(Bitpix)
*/
@Deprecated
public void setBitpix(int val) throws IllegalArgumentException {
try {
setBitpix(Bitpix.forValue(val));
} catch (FitsException e) {
throw new IllegalArgumentException("Invalid BITPIX value: " + val, e);
}
}
/**
* Sets a standard BITPIX value for the header.
*
* @param bitpix The predefined enum value, e.g. {@link Bitpix#INTEGER}.
* @since 1.16
*
* @see #setBitpix(int)
*/
public void setBitpix(Bitpix bitpix) {
Cursor<String, HeaderCard> iter = iterator();
iter.next();
iter.add(bitpix.getHeaderCard());
}
/**
* Overwite the default header card sorter.
*
* @param headerSorter
* the sorter tu use or null to disable sorting
*/
public void setHeaderSorter(Comparator<String> headerSorter) {
this.headerSorter = headerSorter;
}
/**
* Set the value of the NAXIS keyword
*
* @param val
* The dimensionality of the data.
*/
public void setNaxes(int val) {
Cursor<String, HeaderCard> iter = iterator();
iter.setKey(BITPIX.key());
if (iter.hasNext()) {
iter.next();
}
iter.add(HeaderCard.create(NAXIS, val));
}
/**
* Set the dimension for a given axis.
*
* @param axis
* The axis being set.
* @param dim
* The dimension
*/
public void setNaxis(int axis, int dim) {
Cursor<String, HeaderCard> iter = iterator();
if (axis <= 0) {
LOG.warning("setNaxis ignored because axis less than 0");
return;
}
if (axis == 1) {
iter.setKey(NAXIS.key());
} else if (axis > 1) {
iter.setKey(NAXISn.n(axis - 1).key());
}
if (iter.hasNext()) {
iter.next();
}
iter.add(HeaderCard.create(NAXISn.n(axis), dim));
}
/**
* Set the SIMPLE keyword to the given value.
*
* @param val
* The boolean value -- Should be true for FITS data.
*/
public void setSimple(boolean val) {
deleteKey(SIMPLE);
deleteKey(XTENSION);
Cursor<String, HeaderCard> iter = iterator();
// If we're flipping back to and from the primary header
// we need to add in the EXTEND keyword whenever we become
// a primary, because it's not permitted in the extensions
// (at least not where it needs to be in the primary array).
if (findCard(NAXIS) != null) {
int nax = getIntValue(NAXIS);
if (findCard(NAXISn.n(nax)) != null) {
iter.next();
deleteKey(EXTEND);
iter.add(HeaderCard.create(EXTEND, true));
}
}
iter.add(HeaderCard.create(SIMPLE, val));
}
/**
* Set the XTENSION keyword to the given value.
*
* @param val
* The name of the extension.
* @throws IllegalArgumentException
* if the string value contains characters that are not allowed in
* FITS headers, that is characters outside of the 0x20 thru 0x7E
* range.
*/
public void setXtension(String val) throws IllegalArgumentException {
deleteKey(SIMPLE);
deleteKey(XTENSION);
deleteKey(EXTEND);
Cursor<String, HeaderCard> iter = iterator();
iter.add(HeaderCard.create(XTENSION, val));
}
/**
* @return the number of cards in the header
* @deprecated use {@link #getNumberOfCards()}. The units of the size of the
* header may be unclear.
*/
@Deprecated
public int size() {
return this.cards.size();
}
/**
* Update a line in the header
*
* @param key
* The key of the card to be replaced.
* @param card
* A new card
* @throws HeaderCardException
* if the operation failed
*/
public void updateLine(IFitsHeader key, HeaderCard card) throws HeaderCardException {
deleteKey(key);
cursor().add(card);
}
/**
* Update an existing card in situ, without affecting the current position,
* or else add a new card at the current position.
*
* @param key
* The key of the card to be replaced.
* @param card
* A new card
* @throws HeaderCardException
* if the operation failed
*/
public final void updateLine(String key, HeaderCard card) throws HeaderCardException {
// Remove an existing card with the matching 'key' (even if that key
// isn't the same
// as the key of the card argument!)
this.cards.update(key, card);
}
/**
* Overwrite the lines in the header. Add the new PHDU header to the current
* one. If keywords appear twice, the new value and comment overwrite the
* current contents. By Richard J Mathar.
*
* @param newHdr
* the list of new header data lines to replace the current ones.
* @throws HeaderCardException
* if the operation failed
*/
public void updateLines(final Header newHdr) throws HeaderCardException {
Cursor<String, HeaderCard> j = newHdr.iterator();
while (j.hasNext()) {
HeaderCard nextHCard = j.next();
// updateLine() doesn't work with COMMENT and HISTORYs because
// this would allow only one COMMENT in total in each header
if (nextHCard.getKey().equals(COMMENT.key())) {
insertComment(nextHCard.getComment());
} else if (nextHCard.getKey().equals(HISTORY.key())) {
insertHistory(nextHCard.getComment());
} else {
updateLine(nextHCard.getKey(), nextHCard);
}
}
}
/**
* Writes a number of blank header records, for example to create preallocated
* blank header space as described by the FITS 4.0 standard.
*
* @param dos the output stream to which the data is to be written.
* @param n the number of blank records to add.
* @throws IOException if there was an error writing to the stream
*
* @since 1.16
*
* @see #ensureCardSpace(int)
*/
private void writeBlankCards(ArrayDataOutput dos, int n) throws IOException {
byte[] blank = new byte[HeaderCard.FITS_HEADER_CARD_SIZE];
Arrays.fill(blank, (byte) ' ');
while (--n >= 0) {
dos.write(blank);
}
}
/**
* Write the current header (including any needed padding) to the output
* stream.
*
* @param dos
* The output stream to which the data is to be written.
* @throws FitsException
* if the header could not be written.
*
*/
@Override
public void write(ArrayDataOutput dos) throws FitsException {
FitsSettings settings = FitsFactory.current();
this.fileOffset = FitsUtil.findOffset(dos);
// Ensure that all cards are in the proper order.
if (this.headerSorter != null) {
this.cards.sort(this.headerSorter);
}
checkBeginning();
checkEnd();
Cursor<String, HeaderCard> writeIterator = this.cards.iterator(0);
try {
int size = 0;
while (writeIterator.hasNext()) {
HeaderCard card = writeIterator.next();
byte[] b = AsciiFuncs.getBytes(card.toString(settings));
size += b.length;
if (END.key().equals(card.getKey()) && minCards * HeaderCard.FITS_HEADER_CARD_SIZE > size) {
// AK: Add preallocated blank header space before the END key.
writeBlankCards(dos, minCards - size / HeaderCard.FITS_HEADER_CARD_SIZE);
size = minCards;
}
dos.write(b);
}
FitsUtil.pad(dos, size, (byte) ' ');
dos.flush();
} catch (IOException e) {
throw new FitsException("IO Error writing header", e);
}
}
private void addDuplicate(HeaderCard dup) {
// AK: Don't worry about duplicates for comment-style cards in general.
if (dup.isCommentStyleCard() || CONTINUE.key().equals(dup.getKey())) {
return;
}
LOG.log(Level.WARNING, "Multiple occurrences of key:" + dup.getKey());
if (this.duplicates == null) {
this.duplicates = new ArrayList<>();
}
this.duplicates.add(dup);
}
/**
* Check if the given key is the next one available in the header.
*/
private void cardCheck(Cursor<String, HeaderCard> iter, IFitsHeader key) throws FitsException {
cardCheck(iter, key.key());
}
/**
* Check if the given key is the next one available in the header.
*/
private void cardCheck(Cursor<String, HeaderCard> iter, String key) throws FitsException {
if (!iter.hasNext()) {
throw new FitsException("Header terminates before " + key);
}
HeaderCard card = iter.next();
if (!card.getKey().equals(key)) {
throw new FitsException("Key " + key + " not found where expected." + "Found " + card.getKey());
}
}
private void checkFirstCard(String key) throws FitsException {
// AK: key cannot be null by the caller already, so checking for it makes dead code.
if (!SIMPLE.key().equals(key) && !XTENSION.key().equals(key)) {
throw new FitsException("Not a proper FITS header: " + HeaderCard.sanitize(key) + " at " + this.fileOffset);
}
}
private void doCardChecks(Cursor<String, HeaderCard> iter, boolean isTable, boolean isExtension) throws FitsException {
cardCheck(iter, BITPIX);
cardCheck(iter, NAXIS);
int nax = getIntValue(NAXIS);
for (int i = 1; i <= nax; i += 1) {
cardCheck(iter, NAXISn.n(i));
}
if (isExtension) {
cardCheck(iter, PCOUNT);
cardCheck(iter, GCOUNT);
if (isTable) {
cardCheck(iter, TFIELDS);
}
}
// This does not check for the EXTEND keyword which
// if present in the primary array must immediately follow
// the NAXISn.
}
/**
* Move after the EXTEND keyword in images. Used in bug fix noted by V.
* Forchi
*/
void afterExtend() {
if (findCard(EXTEND) != null) {
nextCard();
}
}
/**
* Ensure that the header begins with a valid set of keywords. Note that we
* do not check the values of these keywords.
*/
void checkBeginning() throws FitsException {
Cursor<String, HeaderCard> iter = iterator();
if (!iter.hasNext()) {
throw new FitsException("Empty Header");
}
HeaderCard card = iter.next();
String key = card.getKey();
if (!key.equals(SIMPLE.key()) && !key.equals(XTENSION.key())) {
throw new FitsException("No SIMPLE or XTENSION at beginning of Header");
}
boolean isTable = false;
boolean isExtension = false;
if (key.equals(XTENSION.key())) {
String value = card.getValue();
if (value == null || value.isEmpty()) {
throw new FitsException("Empty XTENSION keyword");
}
isExtension = true;
if (value.equals(XTENSION_BINTABLE) || value.equals("A3DTABLE") || value.equals("TABLE")) {
isTable = true;
}
}
doCardChecks(iter, isTable, isExtension);
Bitpix.fromHeader(this, false);
}
/**
* Ensure that the header has exactly one END keyword in the appropriate
* location.
*/
void checkEnd() {
// Ensure we have an END card only at the end of the
// header.
Cursor<String, HeaderCard> iter = iterator();
HeaderCard card;
while (iter.hasNext()) {
card = iter.next();
if (!card.isKeyValuePair() && card.getKey().equals(END.key())) {
iter.remove();
}
}
// End cannot have a comment
try {
iter.add(HeaderCard.createCommentStyleCard(END.key(), null));
} catch (HeaderCardException e) {
// Cannot happen.
}
}
/**
* Return the size of the header data including padding, or 0 if the header is invalid.
*
* @return the header size including any needed padding, or 0 if the header is invalid.
*
* @see #isValidHeader()
*/
int headerSize() {
if (!isValidHeader()) {
return 0;
}
return FitsUtil.addPadding(Math.max(minCards, getNumberOfPhysicalCards()) * HeaderCard.FITS_HEADER_CARD_SIZE);
}
/**
* Is this a valid header.
*
* @return <CODE>true</CODE> for a valid header, <CODE>false</CODE>
* otherwise.
*/
boolean isValidHeader() {
if (getNumberOfCards() < MIN_NUMBER_OF_CARDS_FOR_VALID_HEADER) {
return false;
}
Cursor<String, HeaderCard> iter = iterator();
String key = iter.next().getKey();
if (!key.equals(SIMPLE.key()) && !key.equals(XTENSION.key())) {
return false;
}
key = iter.next().getKey();
if (!key.equals(BITPIX.key())) {
return false;
}
key = iter.next().getKey();
if (!key.equals(NAXIS.key())) {
return false;
}
while (iter.hasNext()) {
key = iter.next().getKey();
}
return key.equals(END.key());
}
/**
* Create a header for a null image.
*/
void nullImage() {
Cursor<String, HeaderCard> iter = iterator();
iter.add(HeaderCard.create(SIMPLE, true));
iter.add(Bitpix.BYTE.getHeaderCard());
iter.add(HeaderCard.create(NAXIS, 0));
iter.add(HeaderCard.create(EXTEND, true));
}
/**
* Find the end of a set of keywords describing a column or axis (or
* anything else terminated by an index. This routine leaves the header
* ready to add keywords after any existing keywords with the index
* specified. The user should specify a prefix to a keyword that is
* guaranteed to be present.
*/
Cursor<String, HeaderCard> positionAfterIndex(IFitsHeader prefix, int col) {
String colnum = String.valueOf(col);
cursor().setKey(prefix.n(col).key());
if (cursor().hasNext()) {
// Bug fix (references to forward) here by Laurent Borges
boolean toFar = false;
while (cursor().hasNext()) {
String key = cursor().next().getKey().trim();
// AK: getKey() cannot return null so no need to check.
if (key.length() <= colnum.length() || !key.substring(key.length() - colnum.length()).equals(colnum)) {
toFar = true;
break;
}
}
if (toFar) {
cursor().prev(); // Gone one too far, so skip back an element.
}
}
return cursor();
}
/**
* Replace the key with a new key. Typically this is used when deleting or
* inserting columns so that TFORMx -> TFORMx-1
*
* @param oldKey
* The old header keyword.
* @param newKey
* the new header keyword.
* @return <CODE>true</CODE> if the card was replaced.
* @exception HeaderCardException
* If <CODE>newKey</CODE> is not a valid FITS keyword.
*/
boolean replaceKey(IFitsHeader oldKey, IFitsHeader newKey) throws HeaderCardException {
return replaceKey(oldKey.key(), newKey.key());
}
/**
* Replace the key with a new key. Typically this is used when deleting or
* inserting columns so that TFORMx -> TFORMx-1
*
* @param oldKey
* The old header keyword.
* @param newKey
* the new header keyword.
* @return <CODE>true</CODE> if the card was replaced.
* @exception HeaderCardException
* If <CODE>newKey</CODE> is not a valid FITS keyword.
*/
boolean replaceKey(String oldKey, String newKey) throws HeaderCardException {
HeaderCard oldCard = findCard(oldKey);
if (oldCard == null) {
return false;
}
if (!this.cards.replaceKey(oldKey, newKey)) {
throw new HeaderCardException("Duplicate key [" + newKey + "] in replace");
}
try {
oldCard.changeKey(newKey);
} catch (IllegalArgumentException e) {
throw new HeaderCardException("New key [" + newKey + "] is invalid or too long for existing value.", e);
}
return true;
}
/**
* Calculate the unpadded size of the data segment from the header
* information.
*
* @return the unpadded data segment size.
*/
long trueDataSize() {
// AK: No need to be too strict here. We can get a data size even if the
// header isn't 100% to spec,
// as long as the necessary keys are present. So, just check for the
// required keys, and no more...
if (!containsKey(BITPIX.key())) {
return 0L;
}
if (!containsKey(NAXIS.key())) {
return 0L;
}
int naxis = getIntValue(NAXIS, 0);
// If there are no axes then there is no data.
if (naxis == 0) {
return 0L;
}
int[] axes = new int[naxis];
for (int axis = 1; axis <= naxis; axis += 1) {
axes[axis - 1] = getIntValue(NAXISn.n(axis), 0);
}
boolean isGroup = getBooleanValue(GROUPS, false);
int pcount = getIntValue(PCOUNT, 0);
int gcount = getIntValue(GCOUNT, 1);
int startAxis = 0;
if (isGroup && naxis > 1 && axes[0] == 0) {
startAxis = 1;
}
long size = 1;
for (int i = startAxis; i < naxis; i += 1) {
size *= axes[i];
}
size += pcount;
size *= gcount;
// Now multiply by the number of bits per pixel and
// convert to bytes.
size *= Math.abs(getIntValue(BITPIX, 0)) / FitsIO.BITS_OF_1_BYTE;
return size;
}
/**
* <p>
* Sets whether warnings about FITS standard violations are logged when a header is being read (parsed).
* Enabling this feature can help identifying various standard violations in existing FITS headers,
* which nevertheless do not prevent the successful reading of the header by this library.
* </p>
* <p>
* If {@link FitsFactory#setAllowHeaderRepairs(boolean)} is set <code>false</code>, this will affect
* only minor violations (e.g. a misplaced '=', missing space after '=', non-standard characters
* in header etc.), which nevertheless do not interfere with the unamiguous parsing of the header
* information. More severe standard violations, where some guessing may be required about the
* intent of some malformed header record, will throw appropriate exceptions. If, however,
* {@link FitsFactory#setAllowHeaderRepairs(boolean)} is set <code>true</code>, the
* parsing will throw fewer exceptions, and the additional issues may get logged as
* additional warning instead.
*
* @param value <code>true</code> if parser warnings about FITS standard violations when reading in
* existing FITS headers are to be logged, otherwise <code>false</code>
*
* @see FitsFactory#setAllowHeaderRepairs(boolean)
*
* @since 1.16
*/
public static void setParserWarningsEnabled(boolean value) {
Level level = value ? Level.WARNING : Level.SEVERE;
LoggerHelper.getLogger(HeaderCardParser.class).setLevel(level);
LoggerHelper.getLogger(ComplexValue.class).setLevel(level);
}
/**
* Checks whether warnings about FITS standard violations are logged when a header is being read
* (parsed).
*
* @return <code>true</code> if parser warnings about FITS standard violations when reading in
* existing FITS headers are enabled and logged, otherwise <code>false</code>
*
* @see #setParserWarningsEnabled(boolean)
*
* @since 1.16
*/
public static boolean isParserWarningsEnabled() {
return !Logger.getLogger(HeaderCardParser.class.getName()).getLevel().equals(Level.SEVERE);
}
}