View Javadoc
1   package nom.tam.fits;
2   
3   import java.io.EOFException;
4   import java.io.IOException;
5   import java.io.PrintStream;
6   import java.math.BigDecimal;
7   import java.math.BigInteger;
8   import java.util.ArrayList;
9   import java.util.Arrays;
10  import java.util.Comparator;
11  import java.util.HashSet;
12  import java.util.List;
13  import java.util.Set;
14  import java.util.logging.Level;
15  import java.util.logging.Logger;
16  
17  import nom.tam.fits.FitsFactory.FitsSettings;
18  import nom.tam.fits.header.Bitpix;
19  import nom.tam.fits.header.Checksum;
20  import nom.tam.fits.header.IFitsHeader;
21  import nom.tam.fits.header.IFitsHeader.VALUE;
22  import nom.tam.fits.header.Standard;
23  import nom.tam.fits.utilities.FitsCheckSum;
24  import nom.tam.util.ArrayDataInput;
25  import nom.tam.util.ArrayDataOutput;
26  import nom.tam.util.AsciiFuncs;
27  import nom.tam.util.ComplexValue;
28  import nom.tam.util.Cursor;
29  import nom.tam.util.FitsIO;
30  import nom.tam.util.FitsInputStream;
31  import nom.tam.util.FitsOutput;
32  import nom.tam.util.HashedList;
33  import nom.tam.util.RandomAccess;
34  
35  /*
36   * #%L
37   * nom.tam FITS library
38   * %%
39   * Copyright (C) 2004 - 2024 nom-tam-fits
40   * %%
41   * This is free and unencumbered software released into the public domain.
42   *
43   * Anyone is free to copy, modify, publish, use, compile, sell, or
44   * distribute this software, either in source code form or as a compiled
45   * binary, for any purpose, commercial or non-commercial, and by any
46   * means.
47   *
48   * In jurisdictions that recognize copyright laws, the author or authors
49   * of this software dedicate any and all copyright interest in the
50   * software to the public domain. We make this dedication for the benefit
51   * of the public at large and to the detriment of our heirs and
52   * successors. We intend this dedication to be an overt act of
53   * relinquishment in perpetuity of all present and future rights to this
54   * software under copyright law.
55   *
56   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
57   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
58   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
59   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
60   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
61   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
62   * OTHER DEALINGS IN THE SOFTWARE.
63   * #L%
64   */
65  
66  import static nom.tam.fits.header.Standard.BITPIX;
67  import static nom.tam.fits.header.Standard.BLANKS;
68  import static nom.tam.fits.header.Standard.COMMENT;
69  import static nom.tam.fits.header.Standard.END;
70  import static nom.tam.fits.header.Standard.EXTEND;
71  import static nom.tam.fits.header.Standard.GCOUNT;
72  import static nom.tam.fits.header.Standard.GROUPS;
73  import static nom.tam.fits.header.Standard.HISTORY;
74  import static nom.tam.fits.header.Standard.NAXIS;
75  import static nom.tam.fits.header.Standard.NAXISn;
76  import static nom.tam.fits.header.Standard.PCOUNT;
77  import static nom.tam.fits.header.Standard.SIMPLE;
78  import static nom.tam.fits.header.Standard.TFIELDS;
79  import static nom.tam.fits.header.Standard.XTENSION;
80  import static nom.tam.fits.header.Standard.XTENSION_BINTABLE;
81  import static nom.tam.fits.header.extra.CXCExt.LONGSTRN;
82  
83  /**
84   * <p>
85   * Access and manipulate the header of a HDU. FITS headers serve more than a single purpose:
86   * </p>
87   * <ol>
88   * <li>provide an essential description of the type, size, and layout of the HDUs data segment</li>
89   * <li>describe the data as completely as possible via standardized (or conventional) keywords</li>
90   * <li>provide storage for additional user-specific key / value pairs</li>
91   * <li>allow for comments to aid human readability</li>
92   * </ol>
93   * <p>
94   * First and foremost headers provide a description of the data object that follows the header in the HDU. Some of that
95   * description is essential and critical to the integrity of the FITS file, such as the header keywords that describe
96   * the type, size, and layout of the data segment. This library will automatically populate the header with appropriate
97   * information using the mandatory keywords (such as <code>SIMPLE</code> or <code>XTENSION</code>, <code>BITPIX</code>,
98   * <code>NAXIS</code>, <code>NAXIS</code><i>n</i>, <code>PCOUNT</code>, <code>GCOUNT</code> keywords, as well as
99   * essential table column format descriptions). Users of the library should avoid overwriting these mandatory keywords
100  * manually, since they may corrupt the FITS file, rendering it unreadable.
101  * </p>
102  * <p>
103  * Beyond the keywords that describe the type, shape, and size of data, the library will not add further information to
104  * the header. The users of the library are responsible to complete the header description as necessary. This includes
105  * non-enssential data descriptions (such as <code>EXTNAME</code>, <code>BUNIT</code>, <code>OBSERVER</code>, or
106  * optional table column descriptors <code>TTYPE</code><i>n</i>, <code>TUNIT</code><i>n</i>, coordinate systems via the
107  * appropriate WCS keywords, or checksums). Users of the library are responsible for completing the data description
108  * using whatever standard or conventional keywords are available and appropriate. Please refer to the
109  * <a href="https://fits.gsfc.nasa.gov/fits_standard.html">FITS Standard</a> documentation to see what typical
110  * descriptions of data you might want to use.
111  * </p>
112  * <p>
113  * Last but not least, the header is also a place where FITS creators can store (nearly) arbitrary key/value pairs. In
114  * earlier versions of the FITS standard, header keywords were restricted to max. 8 upper case letters and numbers (plus
115  * hyphen and underscore), and no more than 70 character value fields. However, as of FITS 4.0 (and even before as a
116  * registered convention), string values of arbitrary length may be stored using the OGIP 1.0 long string convention,
117  * while the ESO <a href="https://fits.gsfc.nasa.gov/registry/hierarch_keyword.html">HIERARCH convention</a> allows
118  * keywords with more than 8 characters and hierarchical keywords. Support, conformance, and compliance to these
119  * conventions can be toggled by static settings in {@link FitsFactory} to user preference.
120  * </p>
121  * <p>
122  * As of version 1.16, we also support reserving space in headers for future additions using the
123  * {@link #ensureCardSpace(int)} method, also part of the FITS 4.0 standard. It allows users to finish populating
124  * headers <i>after</i> data that follows the header is already written -- a useful feature for recording data from
125  * streaming sources.
126  * </p>
127  */
128 @SuppressWarnings("deprecation")
129 public class Header implements FitsElement {
130 
131     /**
132      * The default character position to which comments should be aligned if possible (zero-based). The fITS standard
133      * requires that 'fixed-format' values are right-justified to byte 30 (index 29 in Java), and recommends a space
134      * after that before the comment. As such, comments should normally start at byte 30 (counted from 0). (We will add
135      * a space at that position before the '/' indicating the comment start)
136      */
137     public static final int DEFAULT_COMMENT_ALIGN = 30;
138 
139     /**
140      * The earliest position (zero-based) at which a comment may start for a regular key/value entry.
141      * 
142      * @deprecated We will disable changing alignment in the future because it may violate the standard for
143      *                 'fixed-format' header entries, and result in files that are unreadable by some other software.
144      *                 This constant will be obsoleted and removed.
145      */
146     public static final int MIN_COMMENT_ALIGN = 20;
147 
148     /**
149      * The largest (zero-based) comment alignment allowed that can still contain some meaningful comment (word)
150      * 
151      * @deprecated We will disable changing alignment in the future because it may violate the standard for
152      *                 'fixed-format' header entries, and result in files that are unreadable by some other software.
153      *                 This constant will be obsoleted and removed.
154      */
155     public static final int MAX_COMMENT_ALIGN = 70;
156 
157     /**
158      * The alignment position of card comments for a more pleasing visual experience. Comments will be aligned to this
159      * position, provided the lengths of all fields allow for it.
160      */
161     private static int commentAlign = DEFAULT_COMMENT_ALIGN;
162 
163     private static final Logger LOG = Logger.getLogger(Header.class.getName());
164 
165     private static final int MIN_NUMBER_OF_CARDS_FOR_VALID_HEADER = 4;
166 
167     /**
168      * The actual header data stored as a HashedList of HeaderCard's.
169      */
170     private final HashedList<HeaderCard> cards;
171 
172     /** Offset of this Header in the FITS file */
173     private long fileOffset;
174 
175     private List<HeaderCard> duplicates;
176 
177     private HashSet<String> dupKeys;
178 
179     /** Input descriptor last time header was read */
180     private ArrayDataInput input;
181 
182     /**
183      * The mimimum number of cards to write, including blank header space as described in the FITS 4.0 standard.
184      */
185     private int minCards;
186 
187     /**
188      * The number of bytes that this header occupied in file. (for re-writing).
189      */
190     private long readSize;
191 
192     /** The checksum calculated from the input stream */
193     private long streamSum = -1L;
194 
195     /**
196      * the sorter used to sort the header cards defore writing the header.
197      */
198     private Comparator<String> headerSorter;
199 
200     private BasicHDU<?> owner;
201 
202     /**
203      * Keyword checking mode when adding standardized keywords via the {@link IFitsHeader} interface.
204      * 
205      * @author Attila Kovacs
206      * 
207      * @since  1.19
208      */
209     public enum KeywordCheck {
210         /** No keyword checking will be performed. */
211         NONE,
212         /** Check only that the keyword is appropriate for the type of data contained in the associated HDU */
213         DATA_TYPE,
214         /**
215          * Strict checking, will refuse to set mandatory FITS keywords -- which should normally be set by the library
216          * alone.
217          */
218         STRICT
219     }
220 
221     /**
222      * The keyword checking mode used by the library until the user changes it it.
223      *
224      * @since 1.19
225      */
226     public static final KeywordCheck DEFAULT_KEYWORD_CHECK_POLICY = KeywordCheck.DATA_TYPE;
227 
228     private static KeywordCheck defaultKeyCheck = DEFAULT_KEYWORD_CHECK_POLICY;
229 
230     private KeywordCheck keyCheck = defaultKeyCheck;
231 
232     /**
233      * Create a header by reading the information from the input stream.
234      *
235      * @param  dis                    The input stream to read the data from.
236      *
237      * @return                        <CODE>null</CODE> if there was a problem with the header; otherwise return the
238      *                                    header read from the input stream.
239      *
240      * @throws TruncatedFileException if the stream ended prematurely
241      * @throws IOException            if the header could not be read.
242      */
243     public static Header readHeader(ArrayDataInput dis) throws TruncatedFileException, IOException {
244         Header myHeader = new Header();
245         try {
246             myHeader.read(dis);
247         } catch (EOFException e) {
248             // An EOF exception is thrown only if the EOF was detected
249             // when reading the first card. In this case we want
250             // to return a null.
251             return null;
252         }
253         return myHeader;
254     }
255 
256     /**
257      * please use {@link FitsFactory#setLongStringsEnabled(boolean)} instead.
258      *
259      * @param flag the new value for long-string enabling.
260      */
261     @Deprecated
262     public static void setLongStringsEnabled(boolean flag) {
263         FitsFactory.setLongStringsEnabled(flag);
264     }
265 
266     /** Create a new header with the required default keywords for a standalone header. */
267     public Header() {
268         cards = new HashedList<>();
269         headerSorter = new HeaderOrder();
270         duplicates = null;
271         clear();
272     }
273 
274     /**
275      * Create a header and populate it from the input stream
276      *
277      * @param  is                     The input stream where header information is expected.
278      *
279      * @throws IOException            if the header could not be read.
280      * @throws TruncatedFileException if the stream ended prematurely
281      */
282     public Header(ArrayDataInput is) throws TruncatedFileException, IOException {
283         this();
284         read(is);
285     }
286 
287     /**
288      * Create a header which points to the given data object.
289      *
290      * @param  o             The data object to be described.
291      *
292      * @throws FitsException if the data was not valid for this header.
293      */
294     public Header(Data o) throws FitsException {
295         this();
296         o.fillHeader(this);
297     }
298 
299     /**
300      * Create a header and initialize it with a vector of strings.
301      *
302      * @param newCards Card images to be placed in the header.
303      */
304     public Header(String[] newCards) {
305         this();
306         for (String newCard : newCards) {
307             cards.add(HeaderCard.create(newCard));
308         }
309     }
310 
311     void assignTo(BasicHDU<?> hdu) {
312         // if (owner != null) {
313         // throw new IllegalStateException("This header was already assigned to a HDU");
314         // }
315         this.owner = hdu;
316     }
317 
318     /**
319      * <p>
320      * Reserves header card space for populating at a later time. When written to a stream, the header will be large
321      * enough to hold at least the specified number of cards. If the header has fewer physical cards then the remaining
322      * space will be padded with blanks, leaving space for future additions, as specified by the FITS 4.0 standard for
323      * <a href="https://fits.gsfc.nasa.gov/registry/headerspace.html"> preallocated header space</a>.
324      * </p>
325      * <p>
326      * This method is also called by {@link #read(ArrayDataInput)}, with the number of cards (including reserved blank
327      * space) contained in the header input stream, in order to ensure that the header remains rewritable even if it is
328      * shortened by the removal of cards (explicitly, or because they were duplicates).
329      * </p>
330      * <p>
331      * A new setting always overrides prior ones. For example, calling this method with an argument that is %lt;=1 will
332      * eliminate (reset) any prior preallocated header space.
333      * </p>
334      *
335      * @param nCards the mimimum number of 80-character header records that is header must be able to support when
336      *                   written to a stream, including preallocated blank header space.
337      *
338      * @since        1.16
339      *
340      * @see          #getMinimumSize()
341      * @see          #write(ArrayDataOutput)
342      * @see          #read(ArrayDataInput)
343      * @see          #resetOriginalSize()
344      */
345     public void ensureCardSpace(int nCards) {
346         if (nCards < 1) {
347             nCards = 1;
348         }
349         minCards = nCards;
350     }
351 
352     /**
353      * Merges copies of all cards from another header, provided they are not readily present in this header. That is, it
354      * merges only the non-conflicting or distinct header entries from the designated source (in contrast to
355      * {@link #updateLines(Header)}). All comment cards are merged also (since these can always appear multiple times,
356      * so they do not conflict). The merged entries are added at the end of the header, in the same order as they appear
357      * in the source. The merged entries will be copies of the cards in the original, such that subsequent modifications
358      * to the source will not affect this header or vice versa.
359      * 
360      * @param source The header from which to inherit non-conflicting entries
361      * 
362      * @since        1.19
363      * 
364      * @see          #updateLines(Header)
365      */
366     public void mergeDistinct(Header source) {
367         seekTail();
368 
369         Cursor<String, HeaderCard> c = source.iterator();
370         while (c.hasNext()) {
371             HeaderCard card = c.next();
372             if (card.isCommentStyleCard() || !containsKey(card.getKey())) {
373                 if (card.getKey().equals(Standard.SIMPLE.key()) || card.getKey().equals(Standard.XTENSION.key())) {
374                     // Do not merge SIMPLE / XTENSION -- these are private matters...
375                     continue;
376                 }
377                 addLine(card.copy());
378             }
379         }
380     }
381 
382     /**
383      * Insert a new header card at the current position, deleting any prior occurence of the same card while maintaining
384      * the current position to point to after the newly inserted card.
385      *
386      * @param  fcard                    The card to be inserted.
387      * 
388      * @throws IllegalArgumentException if the current keyword checking mode does not allow the headercard with its
389      *                                      standard keyword in the header.
390      * 
391      * @see                             #setKeywordChecking(KeywordCheck)
392      */
393     public void addLine(HeaderCard fcard) throws IllegalArgumentException {
394         if (fcard == null) {
395             return;
396         }
397 
398         if (fcard.getStandardKey() != null) {
399             checkKeyword(fcard.getStandardKey());
400         }
401 
402         cursor().add(fcard);
403     }
404 
405     /**
406      * <p>
407      * Sets the built-in standard keyword checking mode. When populating the header using {@link IFitsHeader} keywords
408      * the library will check if the given keyword is appropriate for the type of HDU that the header represents, and
409      * will throw an {@link IllegalArgumentException} if the specified keyword is not allowed for that type of HDU.
410      * </p>
411      * <p>
412      * This method changes the keyword checking mode for this header instance only. If you want to change the mode for
413      * all newly created headers globally, use {@link #setDefaultKeywordChecking(KeywordCheck)} instead.
414      * </p>
415      * 
416      * @param mode The keyword checking mode to use.
417      * 
418      * @see        #getKeywordChecking()
419      * @see        HeaderCard#setValueCheckingPolicy(nom.tam.fits.HeaderCard.ValueCheck)
420      * 
421      * @since      1.19
422      */
423     public void setKeywordChecking(KeywordCheck mode) {
424         keyCheck = mode;
425     }
426 
427     /**
428      * Sets the default mode of built-in standard keyword checking mode for new headers. When populating the header
429      * using {@link IFitsHeader} keywords the library will check if the given keyword is appropriate for the type of HDU
430      * that the header represents, and will throw an {@link IllegalArgumentException} if the specified keyword is not
431      * allowed for that type of HDU.
432      * 
433      * @param mode The keyword checking policy to use.
434      * 
435      * @see        #setKeywordChecking(KeywordCheck)
436      * @see        #getKeywordChecking()
437      * @see        HeaderCard#setValueCheckingPolicy(nom.tam.fits.HeaderCard.ValueCheck)
438      * 
439      * @since      1.19
440      */
441     public static void setDefaultKeywordChecking(KeywordCheck mode) {
442         defaultKeyCheck = mode;
443     }
444 
445     /**
446      * Returns the current keyword checking mode.
447      * 
448      * @return the current keyword checking mode
449      * 
450      * @see    #setKeywordChecking(KeywordCheck)
451      * 
452      * @since  1.19
453      */
454     public final KeywordCheck getKeywordChecking() {
455         return keyCheck;
456     }
457 
458     private void checkKeyword(IFitsHeader keyword) throws IllegalArgumentException {
459         if (keyCheck == KeywordCheck.NONE || owner == null) {
460             return;
461         }
462 
463         if (keyCheck == KeywordCheck.STRICT
464                 && (keyword.status() == IFitsHeader.SOURCE.MANDATORY || keyword.status() == IFitsHeader.SOURCE.INTEGRAL)) {
465             throw new IllegalArgumentException("Keyword " + keyword + " should be set by the library only");
466         }
467 
468         switch (keyword.hdu()) {
469 
470         case PRIMARY:
471             if (!owner.canBePrimary()) {
472                 throw new IllegalArgumentException(
473                         "Keyword " + keyword + " is a primary keyword and may not be used in extensions");
474             }
475             return;
476         case EXTENSION:
477             if (owner instanceof RandomGroupsHDU) {
478                 throw new IllegalArgumentException(
479                         "Keyword " + keyword + " is an extension keyword but random groups may only be primary");
480             }
481             return;
482         case IMAGE:
483             if (owner instanceof ImageHDU || owner instanceof RandomGroupsHDU) {
484                 return;
485             }
486             break;
487         case GROUPS:
488             if (owner instanceof RandomGroupsHDU) {
489                 return;
490             }
491             break;
492         case TABLE:
493             if (owner instanceof TableHDU) {
494                 return;
495             }
496             break;
497         case ASCII_TABLE:
498             if (owner instanceof AsciiTableHDU) {
499                 return;
500             }
501             break;
502         case BINTABLE:
503             if (owner instanceof BinaryTableHDU) {
504                 return;
505             }
506             break;
507         default:
508             return;
509         }
510 
511         throw new IllegalArgumentException(
512                 "Keyword " + keyword.key() + " is not appropriate for " + owner.getClass().getName());
513     }
514 
515     /**
516      * Add or replace a key with the given boolean value and its standardized comment. If the value is not compatible
517      * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The
518      * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}.
519      *
520      * @param  key                      The header key.
521      * @param  val                      The boolean value.
522      *
523      * @return                          the new card that was added.
524      *
525      * @throws HeaderCardException      If the parameters cannot build a valid FITS card.
526      * @throws IllegalArgumentException If the keyword is invalid
527      *
528      * @see                             #addValue(String, Boolean, String)
529      */
530     public HeaderCard addValue(IFitsHeader key, Boolean val) throws HeaderCardException, IllegalArgumentException {
531         HeaderCard card = HeaderCard.create(key, val);
532         addLine(card);
533         return card;
534     }
535 
536     /**
537      * Add or replace a key with the given double value and its standardized comment. If the value is not compatible
538      * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The
539      * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}.
540      *
541      * @param  key                      The header key.
542      * @param  val                      The double value.
543      *
544      * @return                          the new card that was added.
545      *
546      * @throws HeaderCardException      If the parameters cannot build a valid FITS card.
547      * @throws IllegalArgumentException If the keyword is invalid
548      *
549      * @see                             #addValue(String, Number, String)
550      */
551     public HeaderCard addValue(IFitsHeader key, Number val) throws HeaderCardException, IllegalArgumentException {
552         HeaderCard card = HeaderCard.create(key, val);
553         addLine(card);
554         return card;
555     }
556 
557     /**
558      * Add or replace a key with the given string value and its standardized comment. If the value is not compatible
559      * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The
560      * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}.
561      *
562      * @param  key                      The header key.
563      * @param  val                      The string value.
564      *
565      * @return                          the new card that was added.
566      *
567      * @throws HeaderCardException      If the parameters cannot build a valid FITS card.
568      * @throws IllegalArgumentException If the keyword is invalid
569      *
570      * @see                             #addValue(String, String, String)
571      */
572     public HeaderCard addValue(IFitsHeader key, String val) throws HeaderCardException, IllegalArgumentException {
573         HeaderCard card = HeaderCard.create(key, val);
574         addLine(card);
575         return card;
576     }
577 
578     /**
579      * Add or replace a key with the given complex value and its standardized comment. If the value is not compatible
580      * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The
581      * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}.
582      *
583      * @param  key                      The header key.
584      * @param  val                      The complex value.
585      *
586      * @return                          the new card that was added.
587      *
588      * @throws HeaderCardException      If the parameters cannot build a valid FITS card.
589      * @throws IllegalArgumentException If the keyword is invalid
590      *
591      * @see                             #addValue(String, ComplexValue, String)
592      *
593      * @since                           1.17
594      */
595     public HeaderCard addValue(IFitsHeader key, ComplexValue val) throws HeaderCardException, IllegalArgumentException {
596         HeaderCard card = HeaderCard.create(key, val);
597         addLine(card);
598         return card;
599     }
600 
601     /**
602      * Add or replace a key with the given boolean value and comment. The new card will be placed at the current mark
603      * position, as set e.g. by {@link #findCard(String)}.
604      *
605      * @param  key                 The header key.
606      * @param  val                 The boolean value.
607      * @param  comment             A comment to append to the card.
608      *
609      * @return                     the new card that was added.
610      *
611      * @throws HeaderCardException If the parameters cannot build a valid FITS card.
612      *
613      * @see                        #addValue(IFitsHeader, Boolean)
614      * @see                        HeaderCard#HeaderCard(String, Boolean, String)
615      */
616     public HeaderCard addValue(String key, Boolean val, String comment) throws HeaderCardException {
617         HeaderCard hc = new HeaderCard(key, val, comment);
618         addLine(hc);
619         return hc;
620     }
621 
622     /**
623      * Add or replace a key with the given number value and comment. The value will be represented in the header card
624      * with use the native precision of the value or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS}, whichever
625      * fits in the available card space. Trailing zeroes will be ommitted. The new card will be placed at the current
626      * mark position, as set e.g. by {@link #findCard(String)}.
627      *
628      * @param  key                 The header key.
629      * @param  val                 The number value.
630      * @param  comment             A comment to append to the card.
631      *
632      * @return                     the new card that was added.
633      *
634      * @throws HeaderCardException If the parameters cannot build a valid FITS card.
635      *
636      * @see                        #addValue(String, Number, int, String)
637      * @see                        #addValue(IFitsHeader, Number)
638      * @see                        HeaderCard#HeaderCard(String, Number, String)
639      */
640     public HeaderCard addValue(String key, Number val, String comment) throws HeaderCardException {
641         HeaderCard hc = new HeaderCard(key, val, comment);
642         addLine(hc);
643         return hc;
644     }
645 
646     /**
647      * Add or replace a key with the given number value and comment, using up to the specified decimal places after the
648      * leading figure. Trailing zeroes will be ommitted. The new card will be placed at the current mark position, as
649      * set e.g. by {@link #findCard(String)}.
650      *
651      * @param  key                 The header key.
652      * @param  val                 The number value.
653      * @param  decimals            The number of decimal places to show after the leading figure, or
654      *                                 {@link nom.tam.util.FlexFormat#AUTO_PRECISION} to use the native precision of the
655      *                                 value or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS}, whichever fits
656      *                                 in the available card space.
657      * @param  comment             A comment to append to the card.
658      *
659      * @return                     the new card that was added.
660      *
661      * @throws HeaderCardException If the parameters cannot build a valid FITS card.
662      *
663      * @see                        #addValue(String, Number, String)
664      * @see                        HeaderCard#HeaderCard(String, Number, int, String)
665      */
666     public HeaderCard addValue(String key, Number val, int decimals, String comment) throws HeaderCardException {
667         HeaderCard hc = new HeaderCard(key, val, decimals, comment);
668         addLine(hc);
669         return hc;
670     }
671 
672     /**
673      * Add or replace a key with the given complex number value and comment. Trailing zeroes will be ommitted. The new
674      * card will be placed at the current mark position, as set e.g. by {@link #findCard(String)}.
675      *
676      * @param  key                 The header keyword.
677      * @param  val                 The complex number value.
678      * @param  comment             A comment to append to the card.
679      *
680      * @return                     the new card that was added.
681      *
682      * @throws HeaderCardException If the parameters cannot build a valid FITS card.
683      *
684      * @since                      1.16
685      *
686      * @see                        #addValue(String, ComplexValue, int, String)
687      * @see                        HeaderCard#HeaderCard(String, ComplexValue, String)
688      */
689     public HeaderCard addValue(String key, ComplexValue val, String comment) throws HeaderCardException {
690         HeaderCard hc = new HeaderCard(key, val, comment);
691         addLine(hc);
692         return hc;
693     }
694 
695     /**
696      * Add or replace a key with the given complex number value and comment, using up to the specified decimal places
697      * after the leading figure. Trailing zeroes will be ommitted. The new card will be placed at the current mark
698      * position, as set e.g. by {@link #findCard(String)}.
699      *
700      * @param  key                 The header keyword.
701      * @param  val                 The complex number value.
702      * @param  decimals            The number of decimal places to show after the leading figure, or
703      *                                 {@link nom.tam.util.FlexFormat#AUTO_PRECISION} to use the native precision of the
704      *                                 value, or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS}, whichever
705      *                                 fits in the available card space.
706      * @param  comment             A comment to append to the card.
707      *
708      * @return                     the new card that was added.
709      *
710      * @throws HeaderCardException If the parameters cannot build a valid FITS card.
711      *
712      * @since                      1.16
713      *
714      * @see                        #addValue(String, ComplexValue, String)
715      * @see                        HeaderCard#HeaderCard(String, ComplexValue, int, String)
716      */
717     public HeaderCard addValue(String key, ComplexValue val, int decimals, String comment) throws HeaderCardException {
718         HeaderCard hc = new HeaderCard(key, val, decimals, comment);
719         addLine(hc);
720         return hc;
721     }
722 
723     /**
724      * @deprecated                     Not supported by the FITS standard, so do not use. It was included due to a
725      *                                     misreading of the standard itself. We will remove this method in the future.
726      *
727      * @param      key                 The header key.
728      * @param      val                 The integer value.
729      * @param      comment             A comment to append to the card.
730      *
731      * @return                         the new card that was added.
732      *
733      * @throws     HeaderCardException If the parameters cannot build a valid FITS card.
734      *
735      * @since                          1.16
736      *
737      * @see                            #addValue(String, Number, String)
738      * @see                            HeaderCard#createHexValueCard(String, long)
739      * @see                            #getHexValue(String)
740      */
741     @Deprecated
742     public HeaderCard addHexValue(String key, long val, String comment) throws HeaderCardException {
743         HeaderCard hc = HeaderCard.createHexValueCard(key, val, comment);
744         addLine(hc);
745         return hc;
746     }
747 
748     /**
749      * Add or replace a key with the given string value and comment. The new card will be placed at the current mark
750      * position, as set e.g. by {@link #findCard(String)}.
751      *
752      * @param  key                 The header key.
753      * @param  val                 The string value.
754      * @param  comment             A comment to append to the card.
755      *
756      * @return                     the new card that was added.
757      *
758      * @throws HeaderCardException If the parameters cannot build a valid FITS card.
759      *
760      * @see                        #addValue(IFitsHeader, String)
761      * @see                        HeaderCard#HeaderCard(String, String, String)
762      */
763     public HeaderCard addValue(String key, String val, String comment) throws HeaderCardException {
764         HeaderCard hc = new HeaderCard(key, val, comment);
765         addLine(hc);
766         return hc;
767     }
768 
769     /**
770      * get a builder for filling the header cards using the builder pattern.
771      *
772      * @param  key the key for the first card.
773      *
774      * @return     the builder for header cards.
775      */
776     public HeaderCardBuilder card(IFitsHeader key) {
777         return new HeaderCardBuilder(this, key);
778     }
779 
780     /**
781      * Tests if the specified keyword is present in this table.
782      *
783      * @param  key the keyword to be found.
784      *
785      * @return     <code>true</code> if the specified keyword is present in this table; <code>false</code> otherwise.
786      */
787     public final boolean containsKey(IFitsHeader key) {
788         return cards.containsKey(key.key());
789     }
790 
791     /**
792      * Tests if the specified keyword is present in this table.
793      *
794      * @param  key the keyword to be found.
795      *
796      * @return     <code>true</code> if the specified keyword is present in this table; <code>false</code> otherwise.
797      */
798     public final boolean containsKey(String key) {
799         return cards.containsKey(key);
800     }
801 
802     /**
803      * Delete the card associated with the given key. Nothing occurs if the key is not found.
804      *
805      * @param key The header key.
806      */
807     public void deleteKey(IFitsHeader key) {
808         deleteKey(key.key());
809     }
810 
811     /**
812      * Delete the card associated with the given key. Nothing occurs if the key is not found.
813      *
814      * @param key The header key.
815      */
816     public void deleteKey(String key) {
817         // AK: This version will not move the current position to the deleted
818         // key
819         if (containsKey(key)) {
820             cards.remove(cards.get(key));
821         }
822     }
823 
824     /**
825      * Print the header to a given stream. Note that this method does not show reserved card space before the END
826      * keyword, and thus does not necessarily show the same layout as what would appear in a file.
827      *
828      * @param ps the stream to which the card images are dumped.
829      * 
830      * @see      #ensureCardSpace(int)
831      */
832     public void dumpHeader(PrintStream ps) {
833         Cursor<String, HeaderCard> iter = iterator();
834         while (iter.hasNext()) {
835             ps.println(iter.next());
836         }
837     }
838 
839     /**
840      * Returns the card associated with a given key. Unlike {@link #findCard(IFitsHeader)}, it does not alter the mark
841      * position at which new cards are added.
842      *
843      * @param  key the header key.
844      *
845      * @return     <CODE>null</CODE> if the keyword could not be found; return the HeaderCard object otherwise.
846      * 
847      * @see        #getCard(String)
848      * @see        #findCard(IFitsHeader)
849      * 
850      * @since      1.18.1
851      */
852     public HeaderCard getCard(IFitsHeader key) {
853         return this.getCard(key.key());
854     }
855 
856     /**
857      * Find the card associated with a given key. If found this sets the mark (cursor) to the card, otherwise it unsets
858      * the mark. The mark is where new cards will be added to the header by default. If you do not want to change the
859      * mark position, use {@link #getCard(IFitsHeader)} instead.
860      *
861      * @param  key The header key.
862      *
863      * @return     <CODE>null</CODE> if the keyword could not be found; return the HeaderCard object otherwise.
864      * 
865      * @see        #getCard(IFitsHeader)
866      * @see        #findCard(String)
867      */
868     public HeaderCard findCard(IFitsHeader key) {
869         return this.findCard(key.key());
870     }
871 
872     /**
873      * Returns the card associated with a given key. Unlike {@link #findCard(String)}, it does not alter the mark
874      * position at which new cards are added.
875      *
876      * @param  key the header key.
877      *
878      * @return     <CODE>null</CODE> if the keyword could not be found; return the HeaderCard object otherwise.
879      * 
880      * @see        #getCard(IFitsHeader)
881      * @see        #findCard(String)
882      * 
883      * @since      1.18.1
884      */
885     public HeaderCard getCard(String key) {
886         return cards.get(key);
887     }
888 
889     /**
890      * Finds the card associated with a given key, and returns it. If found this sets the mark (cursor) to just before
891      * the card, such that {@link #nextCard()} will return that very same card on the first subsequent call. If the
892      * header contains no matching entry, the mark is reset to the tail of the header (the same as {@link #seekTail()}).
893      * The mark determines where new cards will be added to the header by default. If you do not want to alter the mark
894      * position, use {@link #getCard(String)} instead.
895      * 
896      * @param  key the header key.
897      *
898      * @return     Returns the header entry for the given keyword, or <CODE>null</CODE> if the header has no such entry.
899      * 
900      * @see        #getCard(String)
901      * @see        #findCard(String)
902      */
903     public HeaderCard findCard(String key) {
904         HeaderCard card = cards.get(key);
905         if (card != null) {
906             cursor().setKey(key);
907         } else {
908             cursor().end();
909         }
910         return card;
911     }
912 
913     /************************************
914      * brief Collect the header cards that match a regular expression. This is useful if one needs to search for a
915      * keyword that is buried under some HIERARCH string conventions of unspecified depth. So to search for some key
916      * like "HIERARCH OBO SUBOBO MYOBO", which would appear with the key HIERARCH.OBO.SUBOBO.MYOBO in this FITS
917      * implementation, one could search with regex="HIER.*MYOBO" and find it, supposed FitsFactory.setUseHierarch(true)
918      * was called before creating the header.
919      * 
920      * @param  regex The generalized regular expression for the keyword search
921      * 
922      * @return       The list of header cards that match the regular expression.
923      * 
924      * @author       Richard J. Mathar
925      * 
926      * @since        1.19.1
927      */
928     public HeaderCard[] findCards(final String regex) {
929         /*
930          * The collection of header cards that match.
931          */
932         ArrayList<HeaderCard> crds = new ArrayList<>();
933 
934         /*
935          * position pointer to start of card stack and loop over all header cards
936          */
937         nom.tam.util.Cursor<String, HeaderCard> iter = iterator();
938         while (iter.hasNext()) {
939             final HeaderCard card = iter.next();
940             /*
941              * compare with regular expression and add to output list if it does
942              */
943             if (card.getKey().matches(regex)) {
944                 crds.add(card);
945             }
946         }
947 
948         HeaderCard[] tmp = new HeaderCard[crds.size()];
949         return crds.toArray(tmp);
950     } /* findCards */
951 
952     /**
953      * @deprecated     Use {@link #findCard(String)} or {@link #getCard(String)} instead. Find the card associated with
954      *                     a given key.
955      *
956      * @param      key The header key.
957      *
958      * @return         <CODE>null</CODE> if the keyword could not be found; return the card image otherwise.
959      */
960     @Deprecated
961     public String findKey(String key) {
962         HeaderCard card = findCard(key);
963         if (card == null) {
964             return null;
965         }
966         return card.toString();
967     }
968 
969     /**
970      * Get the bid decimal value associated with the given key.
971      * 
972      * @deprecated     The FITS header does not support decimal types beyond those that can be represented by a 64-bit
973      *                     IEEE double-precision floating point value.
974      *
975      * @param      key The header key.
976      *
977      * @return         The associated value or 0.0 if not found.
978      */
979     public final BigDecimal getBigDecimalValue(IFitsHeader key) {
980         return getBigDecimalValue(key.key());
981     }
982 
983     /**
984      * Get the big decimal value associated with the given key.
985      * 
986      * @deprecated     The FITS header does not support decimal types beyond those that can be represented by a 64-bit
987      *                     IEEE double-precision floating point value.
988      *
989      * @param      key The header key.
990      * @param      dft The default value to return if the key cannot be found.
991      *
992      * @return         the associated value.
993      */
994     public final BigDecimal getBigDecimalValue(IFitsHeader key, BigDecimal dft) {
995         return getBigDecimalValue(key.key(), dft);
996     }
997 
998     /**
999      * Get the big decimal value associated with the given key.
1000      * 
1001      * @deprecated     The FITS header does not support decimal types beyond those that can be represented by a 64-bit
1002      *                     IEEE double-precision floating point value.
1003      *
1004      * @param      key The header key.
1005      *
1006      * @return         The associated value or 0.0 if not found.
1007      */
1008     public final BigDecimal getBigDecimalValue(String key) {
1009         return getBigDecimalValue(key, BigDecimal.ZERO);
1010     }
1011 
1012     /**
1013      * Get the big decimal value associated with the given key.
1014      *
1015      * @deprecated     The FITS header does not support decimal types beyond those that can be represented by a 64-bit
1016      *                     IEEE double-precision floating point value.
1017      *
1018      * @param      key The header key.
1019      * @param      dft The default value to return if the key cannot be found.
1020      *
1021      * @return         the associated value.
1022      */
1023     public BigDecimal getBigDecimalValue(String key, BigDecimal dft) {
1024         HeaderCard fcard = getCard(key);
1025         if (fcard == null) {
1026             return dft;
1027         }
1028         return fcard.getValue(BigDecimal.class, dft);
1029     }
1030 
1031     /**
1032      * Get the big integer value associated with the given key.
1033      * 
1034      * @deprecated     The FITS header does not support integer types beyond those that can be represented by a 64-bit
1035      *                     integer.
1036      *
1037      * @param      key The header key.
1038      *
1039      * @return         the associated value or 0 if not found.
1040      */
1041     public final BigInteger getBigIntegerValue(IFitsHeader key) {
1042         return getBigIntegerValue(key.key());
1043     }
1044 
1045     /**
1046      * Get the big integer value associated with the given key, or return a default value.
1047      *
1048      * @deprecated     The FITS header does not support integer types beyond those that can be represented by a 64-bit
1049      *                     integer.
1050      *
1051      * @param      key The header key.
1052      * @param      dft The default value to be returned if the key cannot be found.
1053      *
1054      * @return         the associated value.
1055      */
1056     public final BigInteger getBigIntegerValue(IFitsHeader key, BigInteger dft) {
1057         return getBigIntegerValue(key.key(), dft);
1058     }
1059 
1060     /**
1061      * Get the big integer value associated with the given key.
1062      * 
1063      * @deprecated     The FITS header does not support integer types beyond those that can be represented by a 64-bit
1064      *                     integer.
1065      *
1066      * @param      key The header key.
1067      *
1068      * @return         The associated value or 0 if not found.
1069      */
1070     public final BigInteger getBigIntegerValue(String key) {
1071         return getBigIntegerValue(key, BigInteger.ZERO);
1072     }
1073 
1074     /**
1075      * Get the big integer value associated with the given key.
1076      * 
1077      * @deprecated     The FITS header does not support integer types beyond those that can be represented by a 64-bit
1078      *                     integer.
1079      *
1080      * @param      key The header key.
1081      * @param      dft The default value to be returned if the key cannot be found.
1082      *
1083      * @return         the associated value.
1084      */
1085     public BigInteger getBigIntegerValue(String key, BigInteger dft) {
1086         HeaderCard fcard = getCard(key);
1087         if (fcard == null) {
1088             return dft;
1089         }
1090         return fcard.getValue(BigInteger.class, dft);
1091     }
1092 
1093     /**
1094      * Get the complex number value associated with the given key.
1095      *
1096      * @param  key The header key.
1097      *
1098      * @return     The associated value or {@link ComplexValue#ZERO} if not found.
1099      *
1100      * @since      1.16
1101      *
1102      * @see        #getComplexValue(String, ComplexValue)
1103      * @see        HeaderCard#getValue(Class, Object)
1104      * @see        #addValue(String, ComplexValue, String)
1105      */
1106     public final ComplexValue getComplexValue(String key) {
1107         return getComplexValue(key, ComplexValue.ZERO);
1108     }
1109 
1110     /**
1111      * Get the complex number value associated with the given key, or return a default value.
1112      *
1113      * @param  key The header key.
1114      * @param  dft The default value to return if the key cannot be found.
1115      *
1116      * @return     the associated value.
1117      *
1118      * @since      1.16
1119      *
1120      * @see        #getComplexValue(String)
1121      * @see        HeaderCard#getValue(Class, Object)
1122      * @see        #addValue(String, ComplexValue, String)
1123      */
1124     public ComplexValue getComplexValue(String key, ComplexValue dft) {
1125         HeaderCard fcard = getCard(key);
1126         if (fcard == null) {
1127             return dft;
1128         }
1129         return fcard.getValue(ComplexValue.class, dft);
1130     }
1131 
1132     /**
1133      * Get the <CODE>boolean</CODE> value associated with the given key.
1134      *
1135      * @param  key The header key.
1136      *
1137      * @return     The value found, or false if not found or if the keyword is not a logical keyword.
1138      */
1139     public final boolean getBooleanValue(IFitsHeader key) {
1140         return getBooleanValue(key.key());
1141     }
1142 
1143     /**
1144      * Get the <CODE>boolean</CODE> value associated with the given key.
1145      *
1146      * @param  key The header key.
1147      * @param  dft The value to be returned if the key cannot be found or if the parameter does not seem to be a
1148      *                 boolean.
1149      *
1150      * @return     the associated value.
1151      */
1152     public final boolean getBooleanValue(IFitsHeader key, boolean dft) {
1153         return getBooleanValue(key.key(), dft);
1154     }
1155 
1156     /**
1157      * Get the <CODE>boolean</CODE> value associated with the given key.
1158      *
1159      * @param  key The header key.
1160      *
1161      * @return     The value found, or false if not found or if the keyword is not a logical keyword.
1162      */
1163     public final boolean getBooleanValue(String key) {
1164         return getBooleanValue(key, false);
1165     }
1166 
1167     /**
1168      * Get the <CODE>boolean</CODE> value associated with the given key.
1169      *
1170      * @param  key The header key.
1171      * @param  dft The value to be returned if the key cannot be found or if the parameter does not seem to be a
1172      *                 boolean.
1173      *
1174      * @return     the associated value.
1175      */
1176     public boolean getBooleanValue(String key, boolean dft) {
1177         HeaderCard fcard = getCard(key);
1178         if (fcard == null) {
1179             return dft;
1180         }
1181         return fcard.getValue(Boolean.class, dft).booleanValue();
1182     }
1183 
1184     /**
1185      * Get the n'th card image in the header
1186      *
1187      * @param      n the card index to get
1188      *
1189      * @return       the card image; return <CODE>null</CODE> if the n'th card does not exist.
1190      *
1191      * @deprecated   An iterator from {@link #iterator(int)} or {@link #iterator()} should be used for sequential access
1192      *                   to the header.
1193      */
1194     @Deprecated
1195     public String getCard(int n) {
1196         if (n >= 0 && n < cards.size()) {
1197             return cards.get(n).toString();
1198         }
1199         return null;
1200     }
1201 
1202     /**
1203      * Return the size of the data including any needed padding.
1204      *
1205      * @return the data segment size including any needed padding.
1206      */
1207     public long getDataSize() {
1208         return FitsUtil.addPadding(trueDataSize());
1209     }
1210 
1211     /**
1212      * Get the <CODE>double</CODE> value associated with the given key.
1213      *
1214      * @param  key The header key.
1215      *
1216      * @return     The associated value or 0.0 if not found.
1217      */
1218     public final double getDoubleValue(IFitsHeader key) {
1219         return getDoubleValue(key.key());
1220     }
1221 
1222     /**
1223      * Get the <CODE>double</CODE> value associated with the given key, or return a default value.
1224      *
1225      * @param  key The header key.
1226      * @param  dft The default value to return if the key cannot be found.
1227      *
1228      * @return     the associated value.
1229      */
1230     public final double getDoubleValue(IFitsHeader key, double dft) {
1231         return getDoubleValue(key.key(), dft);
1232     }
1233 
1234     /**
1235      * Get the <CODE>double</CODE> value associated with the given key.
1236      *
1237      * @param  key The header key.
1238      *
1239      * @return     The associated value or 0.0 if not found.
1240      */
1241     public final double getDoubleValue(String key) {
1242         return getDoubleValue(key, 0.0);
1243     }
1244 
1245     /**
1246      * Get the <CODE>double</CODE> value associated with the given key, or return a default value.
1247      *
1248      * @param  key The header key.
1249      * @param  dft The default value to return if the key cannot be found.
1250      *
1251      * @return     the associated value.
1252      */
1253     public double getDoubleValue(String key, double dft) {
1254         HeaderCard fcard = getCard(key);
1255         if (fcard == null) {
1256             return dft;
1257         }
1258         return fcard.getValue(Double.class, dft).doubleValue();
1259     }
1260 
1261     /**
1262      * <p>
1263      * Returns the list of duplicate cards in the order they appeared in the parsed header. You can access the first
1264      * occurence of each of every duplicated FITS keyword using the usual <code>Header.getValue()</code>, and find
1265      * further occurrences in the list returned here.
1266      * </p>
1267      * <p>
1268      * The FITS standared strongly discourages using the keywords multiple times with assigned values, and specifies
1269      * that the values of such keywords are undefined by definitions. Our library is thus far more tolerant than the
1270      * FITS standard, allowing you to access each and every value that was specified for the same keyword.
1271      * </p>
1272      * <p>
1273      * On the other hand FITS does not limit how many times you can add comment-style keywords to a header. If you must
1274      * used the same keyword multiple times in your header, you should consider using comment-style entries instead.
1275      * </p>
1276      *
1277      * @return the list of duplicate cards. Note that when the header is read in, only the last entry for a given
1278      *             keyword is retained in the active header. This method returns earlier cards that have been discarded
1279      *             in the order in which they were encountered in the header. It is possible for there to be many cards
1280      *             with the same keyword in this list.
1281      *
1282      * @see    #hadDuplicates()
1283      * @see    #getDuplicateKeySet()
1284      */
1285     public List<HeaderCard> getDuplicates() {
1286         return duplicates;
1287     }
1288 
1289     /**
1290      * Returns the set of keywords that had more than one value assignment in the parsed header.
1291      *
1292      * @return the set of header keywords that were assigned more than once in the same header, or <code>null</code> if
1293      *             there were no duplicate assignments.
1294      *
1295      * @see    #hadDuplicates()
1296      * @see    #getDuplicates()
1297      *
1298      * @since  1.17
1299      */
1300     public Set<String> getDuplicateKeySet() {
1301         return dupKeys;
1302     }
1303 
1304     @Override
1305     public long getFileOffset() {
1306         return fileOffset;
1307     }
1308 
1309     /**
1310      * Get the <CODE>float</CODE> value associated with the given key.
1311      *
1312      * @param  key The header key.
1313      *
1314      * @return     The associated value or 0.0 if not found.
1315      */
1316     public final float getFloatValue(IFitsHeader key) {
1317         return getFloatValue(key.key());
1318 
1319     }
1320 
1321     /**
1322      * Get the <CODE>float</CODE> value associated with the given key, or return a default value.
1323      * 
1324      * @return     the <CODE>float</CODE> value associated with the given key.
1325      *
1326      * @param  key The header key.
1327      * @param  dft The value to be returned if the key is not found.
1328      */
1329     public final float getFloatValue(IFitsHeader key, float dft) {
1330         return getFloatValue(key.key(), dft);
1331     }
1332 
1333     /**
1334      * Get the <CODE>float</CODE> value associated with the given key.
1335      *
1336      * @param  key The header key.
1337      *
1338      * @return     The associated value or 0.0 if not found.
1339      */
1340     public final float getFloatValue(String key) {
1341         return getFloatValue(key, 0.0F);
1342     }
1343 
1344     /**
1345      * Get the <CODE>float</CODE> value associated with the given key, or return a default value.
1346      * 
1347      * @return     the <CODE>float</CODE> value associated with the given key.
1348      *
1349      * @param  key The header key.
1350      * @param  dft The value to be returned if the key is not found.
1351      */
1352     public float getFloatValue(String key, float dft) {
1353         HeaderCard fcard = getCard(key);
1354         if (fcard == null) {
1355             return dft;
1356         }
1357         return fcard.getValue(Float.class, dft).floatValue();
1358     }
1359 
1360     /**
1361      * Get the <CODE>int</CODE> value associated with the given key.
1362      *
1363      * @param  key The header key.
1364      *
1365      * @return     The associated value or 0 if not found.
1366      */
1367     public final int getIntValue(IFitsHeader key) {
1368         return (int) getLongValue(key);
1369     }
1370 
1371     /**
1372      * Get the <CODE>int</CODE> value associated with the given key, or return a default value.
1373      * 
1374      * @return     the value associated with the key as an int.
1375      *
1376      * @param  key The header key.
1377      * @param  dft The value to be returned if the key is not found.
1378      */
1379     public final int getIntValue(IFitsHeader key, int dft) {
1380         return (int) getLongValue(key, dft);
1381     }
1382 
1383     /**
1384      * Get the <CODE>int</CODE> value associated with the given key.
1385      *
1386      * @param  key The header key.
1387      *
1388      * @return     The associated value or 0 if not found.
1389      */
1390     public final int getIntValue(String key) {
1391         return (int) getLongValue(key);
1392     }
1393 
1394     /**
1395      * Get the <CODE>int</CODE> value associated with the given key, or return a default value.
1396      * 
1397      * @return     the value associated with the key as an int.
1398      *
1399      * @param  key The header key.
1400      * @param  dft The value to be returned if the key is not found.
1401      */
1402     public int getIntValue(String key, int dft) {
1403         return (int) getLongValue(key, dft);
1404     }
1405 
1406     /**
1407      * Get the n'th key in the header.
1408      *
1409      * @param      n the index of the key
1410      *
1411      * @return       the card image; return <CODE>null</CODE> if the n'th key does not exist.
1412      *
1413      * @deprecated   An iterator from {@link #iterator(int)} or {@link #iterator()} should be used for sequential access
1414      *                   to the header.
1415      */
1416     @Deprecated
1417     public String getKey(int n) {
1418         if (n >= 0 && n < cards.size()) {
1419             return cards.get(n).getKey();
1420         }
1421         return null;
1422 
1423     }
1424 
1425     /**
1426      * Get the <CODE>long</CODE> value associated with the given key.
1427      *
1428      * @param  key The header key.
1429      *
1430      * @return     The associated value or 0 if not found.
1431      */
1432     public final long getLongValue(IFitsHeader key) {
1433         return getLongValue(key.key());
1434     }
1435 
1436     /**
1437      * Get the <CODE>long</CODE> value associated with the given key, or return a default value.
1438      *
1439      * @param  key The header key.
1440      * @param  dft The default value to be returned if the key cannot be found.
1441      *
1442      * @return     the associated value.
1443      */
1444     public final long getLongValue(IFitsHeader key, long dft) {
1445         return getLongValue(key.key(), dft);
1446     }
1447 
1448     /**
1449      * Get the <CODE>long</CODE> value associated with the given key.
1450      *
1451      * @param  key The header key.
1452      *
1453      * @return     The associated value or 0 if not found.
1454      */
1455     public final long getLongValue(String key) {
1456         return getLongValue(key, 0L);
1457     }
1458 
1459     /**
1460      * Get the <CODE>long</CODE> value associated with the given key, or return a default value.
1461      *
1462      * @param  key The header key.
1463      * @param  dft The default value to be returned if the key cannot be found.
1464      *
1465      * @return     the associated value.
1466      */
1467     public long getLongValue(String key, long dft) {
1468         HeaderCard fcard = getCard(key);
1469         if (fcard == null) {
1470             return dft;
1471         }
1472         return fcard.getValue(Long.class, dft).longValue();
1473     }
1474 
1475     /**
1476      * @deprecated     Not supported by the FITS standard, so do not use. It was included due to a misreading of the
1477      *                     standard itself.
1478      *
1479      * @param      key The header key.
1480      *
1481      * @return         The associated value or 0 if not found.
1482      *
1483      * @since          1.16
1484      *
1485      * @see            #getHexValue(String, long)
1486      * @see            HeaderCard#getHexValue()
1487      * @see            #addHexValue(String, long, String)
1488      */
1489     @Deprecated
1490     public final long getHexValue(String key) {
1491         return getHexValue(key, 0L);
1492     }
1493 
1494     /**
1495      * @deprecated     Not supported by the FITS standard, so do not use. It was included due to a misreading of the
1496      *                     standard itself.
1497      *
1498      * @param      key The header key.
1499      * @param      dft The default value to be returned if the key cannot be found.
1500      *
1501      * @return         the associated value.
1502      *
1503      * @since          1.16
1504      *
1505      * @see            #getHexValue(String)
1506      * @see            HeaderCard#getHexValue()
1507      * @see            #addHexValue(String, long, String)
1508      */
1509     public long getHexValue(String key, long dft) {
1510         HeaderCard fcard = getCard(key);
1511         if (fcard == null) {
1512             return dft;
1513         }
1514         try {
1515             return fcard.getHexValue();
1516         } catch (NumberFormatException e) {
1517             return dft;
1518         }
1519     }
1520 
1521     /**
1522      * Returns the nominal number of currently defined cards in this header. Each card can consist of one or more
1523      * 80-character wide header records.
1524      *
1525      * @return the number of nominal cards in the header
1526      *
1527      * @see    #getNumberOfPhysicalCards()
1528      */
1529     public int getNumberOfCards() {
1530         return cards.size();
1531     }
1532 
1533     /**
1534      * Returns the number of 80-character header records in this header, including an END marker (whether or not it is
1535      * currently contained).
1536      *
1537      * @return the number of physical cards in the header, including the END marker.
1538      *
1539      * @see    #getNumberOfCards()
1540      * @see    #getSize()
1541      */
1542     public int getNumberOfPhysicalCards() {
1543         int count = 0;
1544         for (HeaderCard card : cards) {
1545             count += card.cardSize();
1546         }
1547 
1548         // AK: Count the END card, which may not have been added yet...
1549         if (!containsKey(END)) {
1550             count++;
1551         }
1552 
1553         return count;
1554     }
1555 
1556     /**
1557      * Returns the minimum number of bytes that will be written by this header, either as the original byte size of a
1558      * header that was read, or else the minimum preallocated capacity after setting {@link #ensureCardSpace(int)}.
1559      *
1560      * @return the minimum byte size for this header. The actual header may take up more space than that (but never
1561      *             less!), depending on the number of cards contained.
1562      *
1563      * @since  1.16
1564      *
1565      * @see    #ensureCardSpace(int)
1566      * @see    #read(ArrayDataInput)
1567      */
1568     public long getMinimumSize() {
1569         return FitsUtil.addPadding((long) minCards * HeaderCard.FITS_HEADER_CARD_SIZE);
1570     }
1571 
1572     /**
1573      * @deprecated <i>for internal use</i>) It should be a private method in the future. Returns the original size of
1574      *                 the header in the stream from which it was read.
1575      *
1576      * @return     the size of the original header in bytes, or 0 if the header was not read from a stream.
1577      *
1578      * @see        #read(ArrayDataInput)
1579      * @see        #getMinimumSize()
1580      */
1581     @Deprecated
1582     public final long getOriginalSize() {
1583         return readSize;
1584     }
1585 
1586     @Override
1587     public final long getSize() {
1588         if (!isValidHeader()) {
1589             return 0;
1590         }
1591 
1592         return FitsUtil
1593                 .addPadding((long) Math.max(minCards, getNumberOfPhysicalCards()) * HeaderCard.FITS_HEADER_CARD_SIZE);
1594     }
1595 
1596     /**
1597      * Get the <CODE>String</CODE> value associated with the given standard key.
1598      *
1599      * @param  key The standard header key.
1600      *
1601      * @return     The associated value or null if not found or if the value is not a string.
1602      *
1603      * @see        #getStringValue(String)
1604      * @see        #getStringValue(IFitsHeader, String)
1605      */
1606     public final String getStringValue(IFitsHeader key) {
1607         return getStringValue(key.key());
1608     }
1609 
1610     /**
1611      * Get the <CODE>String</CODE> value associated with the given standard key, or return a default value.
1612      *
1613      * @param  key The standard header key.
1614      * @param  dft The default value.
1615      *
1616      * @return     The associated value or the default value if not found or if the value is not a string.
1617      *
1618      * @see        #getStringValue(String, String)
1619      * @see        #getStringValue(IFitsHeader)
1620      */
1621     public final String getStringValue(IFitsHeader key, String dft) {
1622         return getStringValue(key.key(), dft);
1623     }
1624 
1625     /**
1626      * Get the <CODE>String</CODE> value associated with the given key.
1627      *
1628      * @param  key The header key.
1629      *
1630      * @return     The associated value or null if not found or if the value is not a string.
1631      *
1632      * @see        #getStringValue(IFitsHeader)
1633      * @see        #getStringValue(String, String)
1634      */
1635     public final String getStringValue(String key) {
1636         return getStringValue(key, null);
1637     }
1638 
1639     /**
1640      * Get the <CODE>String</CODE> value associated with the given key, or return a default value.
1641      *
1642      * @param  key The header key.
1643      * @param  dft The default value.
1644      *
1645      * @return     The associated value or the default value if not found or if the value is not a string.
1646      *
1647      * @see        #getStringValue(IFitsHeader, String)
1648      * @see        #getStringValue(String)
1649      */
1650     public String getStringValue(String key, String dft) {
1651 
1652         HeaderCard fcard = getCard(key);
1653         if (fcard == null || !fcard.isStringValue()) {
1654             return dft;
1655         }
1656 
1657         return fcard.getValue();
1658     }
1659 
1660     /**
1661      * Checks if the header had duplicate assignments in the FITS.
1662      * 
1663      * @return Were duplicate header keys found when this record was read in?
1664      */
1665     public boolean hadDuplicates() {
1666         return duplicates != null;
1667     }
1668 
1669     /**
1670      * Adds a line to the header using the COMMENT style, i.e., no '=' in column 9. The comment text may be truncated to
1671      * fit into a single record, which is returned. Alternatively, you can split longer comments among multiple
1672      * consecutive cards of the same type by {@link #insertCommentStyleMultiline(String, String)}.
1673      *
1674      * @param  key     The comment style header keyword, or <code>null</code> for an empty comment line.
1675      * @param  comment A string comment to follow. Illegal characters will be replaced by '?' and the comment may be
1676      *                     truncated to fit into the card-space (71 characters).
1677      *
1678      * @return         The new card that was inserted, or <code>null</code> if the keyword itself was invalid or the
1679      *                     comment was <code>null</code>.
1680      *
1681      * @see            #insertCommentStyleMultiline(String, String)
1682      * @see            HeaderCard#createCommentStyleCard(String, String)
1683      */
1684     public HeaderCard insertCommentStyle(String key, String comment) {
1685         if (comment == null) {
1686             comment = "";
1687         } else if (comment.length() > HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH) {
1688             comment = comment.substring(0, HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH);
1689             LOG.warning("Truncated comment to fit card: [" + comment + "]");
1690         }
1691 
1692         try {
1693             HeaderCard hc = HeaderCard.createCommentStyleCard(key, HeaderCard.sanitize(comment));
1694             cursor().add(hc);
1695             return hc;
1696         } catch (HeaderCardException e) {
1697             LOG.log(Level.WARNING, "Ignoring comment card with invalid key [" + HeaderCard.sanitize(key) + "]", e);
1698             return null;
1699         }
1700     }
1701 
1702     /**
1703      * Adds a line to the header using the COMMENT style, i.e., no '=' in column 9. If the comment does not fit in a
1704      * single record, then it will be split (wrapped) among multiple consecutive records with the same keyword. Wrapped
1705      * lines will end with '&amp;' (not itself a standard) to indicate comment cards that might belong together.
1706      *
1707      * @param  key     The comment style header keyword, or <code>null</code> for an empty comment line.
1708      * @param  comment A string comment to follow. Illegal characters will be replaced by '?' and the comment may be
1709      *                     split among multiple records as necessary to be fully preserved.
1710      *
1711      * @return         The number of cards inserted.
1712      *
1713      * @since          1.16
1714      *
1715      * @see            #insertCommentStyle(String, String)
1716      * @see            #insertComment(String)
1717      * @see            #insertUnkeyedComment(String)
1718      * @see            #insertHistory(String)
1719      */
1720     public int insertCommentStyleMultiline(String key, String comment) {
1721 
1722         // Empty comments must have at least one space char to write at least one
1723         // comment card...
1724         if ((comment == null) || comment.isEmpty()) {
1725             comment = " ";
1726         }
1727 
1728         int n = 0;
1729 
1730         for (int from = 0; from < comment.length();) {
1731             int to = from + HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH;
1732             String part = null;
1733             if (to < comment.length()) {
1734                 part = comment.substring(from, --to) + "&";
1735             } else {
1736                 part = comment.substring(from);
1737             }
1738 
1739             if (insertCommentStyle(key, part) == null) {
1740                 return n;
1741             }
1742             from = to;
1743             n++;
1744         }
1745 
1746         return n;
1747     }
1748 
1749     /**
1750      * Adds one or more consecutive COMMENT records, wrapping the comment text as necessary.
1751      *
1752      * @param  value The comment.
1753      *
1754      * @return       The number of consecutive COMMENT cards that were inserted
1755      *
1756      * @see          #insertCommentStyleMultiline(String, String)
1757      * @see          #insertUnkeyedComment(String)
1758      * @see          #insertHistory(String)
1759      * @see          HeaderCard#createCommentCard(String)
1760      */
1761     public int insertComment(String value) {
1762         return insertCommentStyleMultiline(COMMENT.key(), value);
1763     }
1764 
1765     /**
1766      * Adds one or more consecutive comment records with no keyword (bytes 1-9 left blank), wrapping the comment text as
1767      * necessary.
1768      *
1769      * @param  value The comment.
1770      *
1771      * @return       The number of consecutive comment-style cards with no keyword (blank keyword) that were inserted.
1772      *
1773      * @since        1.16
1774      *
1775      * @see          #insertCommentStyleMultiline(String, String)
1776      * @see          #insertComment(String)
1777      * @see          #insertHistory(String)
1778      * @see          HeaderCard#createUnkeyedCommentCard(String)
1779      * @see          #insertBlankCard()
1780      */
1781     public int insertUnkeyedComment(String value) {
1782         return insertCommentStyleMultiline(BLANKS.key(), value);
1783     }
1784 
1785     /**
1786      * Adds a blank card into the header.
1787      *
1788      * @since 1.16
1789      *
1790      * @see   #insertUnkeyedComment(String)
1791      */
1792     public void insertBlankCard() {
1793         insertCommentStyle(null, null);
1794     }
1795 
1796     /**
1797      * Adds one or more consecutive a HISTORY records, wrapping the comment text as necessary.
1798      *
1799      * @param  value The history record.
1800      *
1801      * @return       The number of consecutive HISTORY cards that were inserted
1802      *
1803      * @see          #insertCommentStyleMultiline(String, String)
1804      * @see          #insertComment(String)
1805      * @see          #insertUnkeyedComment(String)
1806      * @see          HeaderCard#createHistoryCard(String)
1807      */
1808     public int insertHistory(String value) {
1809         return insertCommentStyleMultiline(HISTORY.key(), value);
1810     }
1811 
1812     /**
1813      * Returns a cursor-based iterator for this header's entries starting at the first entry.
1814      *
1815      * @return an iterator over the header cards
1816      */
1817     public Cursor<String, HeaderCard> iterator() {
1818         return cards.iterator(0);
1819     }
1820 
1821     /**
1822      * Returns a cursor-based iterator for this header's entries.
1823      * 
1824      * @deprecated       We should never use indexed access to the header. This function will be removed in 2.0.
1825      *
1826      * @return           an iterator over the header cards starting at an index
1827      *
1828      * @param      index the card index to start the iterator
1829      */
1830     @Deprecated
1831     public Cursor<String, HeaderCard> iterator(int index) {
1832         return cards.iterator(index);
1833     }
1834 
1835     /**
1836      * Return the iterator that represents the current position in the header. This provides a connection between
1837      * editing headers through Header add/append/update methods, and via Cursors, which can be used side-by-side while
1838      * maintaining desired card ordering. For the reverse direction ( translating iterator position to current position
1839      * in the header), we can just use findCard().
1840      *
1841      * @return the iterator representing the current position in the header.
1842      * 
1843      * @see    #iterator()
1844      */
1845     private Cursor<String, HeaderCard> cursor() {
1846         return cards.cursor();
1847     }
1848 
1849     /**
1850      * Move the cursor to the end of the header. Subsequently, all <code>addValue()</code> calls will add new cards to
1851      * the end of the header.
1852      * 
1853      * @return the cursor after it has been repositioned to the end
1854      * 
1855      * @since  1.18.1
1856      * 
1857      * @see    #seekTail()
1858      * @see    #findCard(String)
1859      * @see    #nextCard()
1860      */
1861     public Cursor<String, HeaderCard> seekHead() {
1862         Cursor<String, HeaderCard> c = cursor();
1863 
1864         while (c.hasPrev()) {
1865             c.prev();
1866         }
1867 
1868         return c;
1869     }
1870 
1871     /**
1872      * Move the cursor to the end of the header. Subsequently, all <code>addValue()</code> calls will add new cards to
1873      * the end of the header.
1874      * 
1875      * @return the cursor after it has been repositioned to the end
1876      * 
1877      * @since  1.18.1
1878      * 
1879      * @see    #seekHead()
1880      * @see    #findCard(String)
1881      */
1882     public Cursor<String, HeaderCard> seekTail() {
1883         cursor().end();
1884         return cursor();
1885     }
1886 
1887     /**
1888      * @deprecated               (<i>for internal use</i>) Normally we either want to write a Java object to FITS (in
1889      *                               which case we have the dataand want to make a header for it), or we read some data
1890      *                               from a FITS input. In either case, there is no benefit of exposing such a function
1891      *                               as this to the user.
1892      *
1893      * @return                   Create the data element corresponding to the current header
1894      *
1895      * @throws     FitsException if the header did not contain enough information to detect the type of the data
1896      */
1897     @Deprecated
1898     public Data makeData() throws FitsException {
1899         return FitsFactory.dataFactory(this);
1900     }
1901 
1902     /**
1903      * Returns the header card at the currently set mark position and increments the mark position by one. The mark
1904      * position determines the location at which new entries are added to the header. The mark is set either to just
1905      * prior a particular card (e.g. via {@link #findCard(IFitsHeader)}.
1906      * 
1907      * @return the next card in the Header using the built-in iterator
1908      * 
1909      * @see    #prevCard()
1910      * @see    #findCard(IFitsHeader)
1911      * @see    #findCard(String)
1912      * @see    #seekHead()
1913      */
1914     public HeaderCard nextCard() {
1915         if (cursor().hasNext()) {
1916             return cursor().next();
1917         }
1918         return null;
1919     }
1920 
1921     /**
1922      * Returns the header card prior to the currently set mark position and decrements the mark position by one. The
1923      * mark position determines the location at which new entries are added to the header. The mark is set either to
1924      * just prior a particular card (e.g. via {@link #findCard(IFitsHeader)}.
1925      * 
1926      * @return the next card in the Header using the built-in iterator
1927      * 
1928      * @see    #nextCard()
1929      * @see    #findCard(IFitsHeader)
1930      * @see    #findCard(String)
1931      * @see    #seekHead()
1932      * 
1933      * @since  1.18.1
1934      */
1935     public HeaderCard prevCard() {
1936         if (cursor().hasPrev()) {
1937             return cursor().prev();
1938         }
1939         return null;
1940     }
1941 
1942     /**
1943      * Create a header which points to the given data object.
1944      *
1945      * @param      o             The data object to be described.
1946      *
1947      * @throws     FitsException if the data was not valid for this header.
1948      *
1949      * @deprecated               Use the appropriate <code>Header</code> constructor instead. Will remove in a future
1950      *                               releae.
1951      */
1952     @Deprecated
1953     public void pointToData(Data o) throws FitsException {
1954         o.fillHeader(this);
1955     }
1956 
1957     /**
1958      * Remove all cards and reset the header to its default status.
1959      */
1960     private void clear() {
1961         cards.clear();
1962         duplicates = null;
1963         dupKeys = null;
1964         readSize = 0;
1965         fileOffset = -1;
1966         minCards = 0;
1967     }
1968 
1969     /**
1970      * Checks if the header is empty, that is if it contains no cards at all.
1971      *
1972      * @return <code>true</code> if the header contains no cards, otherwise <code>false</code>.
1973      *
1974      * @since  1.16
1975      */
1976     public boolean isEmpty() {
1977         return cards.isEmpty();
1978     }
1979 
1980     /**
1981      * <p>
1982      * Reads new header data from an input, discarding any prior content.
1983      * </p>
1984      * <p>
1985      * As of 1.16, the header is ensured to (re)write at least the same number of cards as before, padding with blanks
1986      * as necessary, unless the user resets the preallocated card space with a call to {@link #ensureCardSpace(int)}.
1987      * </p>
1988      *
1989      * @param  dis                    The input stream to read the data from.
1990      *
1991      * @throws TruncatedFileException the the stream ended prematurely
1992      * @throws IOException            if the operation failed
1993      *
1994      * @see                           #ensureCardSpace(int)
1995      */
1996     @Override
1997     public void read(ArrayDataInput dis) throws TruncatedFileException, IOException {
1998         // AK: Start afresh, in case the header had prior contents from before.
1999         clear();
2000 
2001         if (dis instanceof RandomAccess) {
2002             fileOffset = FitsUtil.findOffset(dis);
2003         } else {
2004             fileOffset = -1;
2005         }
2006 
2007         if (dis instanceof FitsInputStream) {
2008             ((FitsInputStream) dis).nextChecksum();
2009         }
2010         streamSum = -1L;
2011 
2012         int trailingBlanks = 0;
2013         minCards = 0;
2014 
2015         HeaderCardCountingArrayDataInput cardCountingArray = new HeaderCardCountingArrayDataInput(dis);
2016         try {
2017             for (;;) {
2018                 HeaderCard fcard = new HeaderCard(cardCountingArray);
2019                 minCards += fcard.cardSize();
2020 
2021                 // AK: Note, 'key' can never be null, as per contract of getKey(). So no need to check...
2022                 String key = fcard.getKey();
2023 
2024                 if (isEmpty()) {
2025                     checkFirstCard(key);
2026                 } else if (fcard.isBlank()) {
2027                     // AK: We don't add the trailing blank cards, but keep count of them.
2028                     // (esp. in case the aren't trailing...)
2029                     trailingBlanks++;
2030                     continue;
2031                 } else if (END.key().equals(key)) {
2032                     addLine(fcard);
2033                     break; // Out of reading the header.
2034                 } else if (LONGSTRN.key().equals(key)) {
2035                     // We don't check the value here. If the user
2036                     // wants to be sure that long strings are disabled,
2037                     // they can call setLongStringsEnabled(false) after
2038                     // reading the header.
2039                     FitsFactory.setLongStringsEnabled(true);
2040                 }
2041 
2042                 // AK: The preceding blank spaces were internal, not trailing
2043                 // so add them back in now...
2044                 for (int i = 0; i < trailingBlanks; i++) {
2045                     insertBlankCard();
2046                 }
2047                 trailingBlanks = 0;
2048 
2049                 if (cards.containsKey(key)) {
2050                     addDuplicate(cards.get(key));
2051                 }
2052 
2053                 addLine(fcard);
2054             }
2055         } catch (EOFException e) {
2056             // Normal end-of-file before END key...
2057             throw e;
2058         } catch (Exception e) {
2059             if (isEmpty() && FitsFactory.getAllowTerminalJunk()) {
2060                 // If this happened where we expect a new header to start, then
2061                 // treat is as if end-of-file if terminal junk is allowed
2062                 forceEOF(
2063                         "Junk detected where header was expected to start" + ((fileOffset > 0) ? ": at " + fileOffset : ""),
2064                         e);
2065             }
2066             if (e instanceof TruncatedFileException) {
2067                 throw (TruncatedFileException) e;
2068             }
2069             throw new IOException("Invalid FITS Header" + (isEmpty() ? e :
2070                     ":\n\n --> Try FitsFactory.setAllowTerminalJunk(true) prior to reading to work around.\n"), e);
2071         }
2072 
2073         if (fileOffset >= 0) {
2074             input = dis;
2075         }
2076 
2077         ensureCardSpace(cardCountingArray.getPhysicalCardsRead());
2078         readSize = FitsUtil.addPadding((long) minCards * HeaderCard.FITS_HEADER_CARD_SIZE);
2079 
2080         // Read to the end of the current FITS block.
2081         //
2082         try {
2083             dis.skipAllBytes(FitsUtil.padding(minCards * HeaderCard.FITS_HEADER_CARD_SIZE));
2084         } catch (EOFException e) {
2085             // No biggy. We got a complete header just fine, it's only that there was no
2086             // padding before EOF. We'll just log that, but otherwise keep going.
2087             LOG.log(Level.WARNING, "Premature end-of-file: no padding after header.", e);
2088         }
2089 
2090         if (dis instanceof FitsInputStream) {
2091             streamSum = ((FitsInputStream) dis).nextChecksum();
2092         }
2093 
2094         // AK: Log if the file ends before the expected end-of-header position.
2095         if (Fits.checkTruncated(dis)) {
2096             // No biggy. We got a complete header just fine, it's only that there was no
2097             // padding before EOF. We'll just log that, but otherwise keep going.
2098             LOG.warning("Premature end-of-file: no padding after header.");
2099         }
2100 
2101         // Move the cursor to after the last card -- this is where new cards will be added.
2102         seekTail();
2103     }
2104 
2105     /**
2106      * Returns the random-accessible input from which this header was read, or <code>null</code> if the header is not
2107      * associated with an input, or the input is not random accessible.
2108      * 
2109      * @return the random-accessible input associated with this header or <code>null</code>
2110      * 
2111      * @see    #read(ArrayDataInput)
2112      * 
2113      * @since  1.18.1
2114      */
2115     RandomAccess getRandomAccessInput() {
2116         return (input instanceof RandomAccess) ? (RandomAccess) input : null;
2117     }
2118 
2119     /**
2120      * Returns the checksum value calculated duting reading from a stream. It is only populated when reading from
2121      * {@link FitsInputStream} imputs, and never from other types of inputs. Valid values are greater or equal to zero.
2122      * Thus, the return value will be <code>-1L</code> to indicate an invalid (unpopulated) checksum.
2123      * 
2124      * @return the non-negative checksum calculated for the data read from a stream, or else <code>-1L</code> if the
2125      *             data was not read from the stream.
2126      * 
2127      * @see    FitsInputStream
2128      * @see    Data#getStreamChecksum()
2129      * 
2130      * @since  1.18.1
2131      */
2132     final long getStreamChecksum() {
2133         return streamSum;
2134     }
2135 
2136     /**
2137      * Forces an EOFException to be thrown when some other exception happened, essentially treating the exception to
2138      * force a normal end the reading of the header.
2139      *
2140      * @param  message      the message to log.
2141      * @param  cause        the exception encountered while reading the header
2142      *
2143      * @throws EOFException the EOFException we'll throw instead.
2144      */
2145     private void forceEOF(String message, Exception cause) throws EOFException {
2146         LOG.log(Level.WARNING, message, cause);
2147         throw new EOFException("Forced EOF at " + fileOffset + " due to: " + message);
2148     }
2149 
2150     /**
2151      * Delete a key.
2152      *
2153      * @param      key                 The header key.
2154      *
2155      * @throws     HeaderCardException if the operation failed
2156      *
2157      * @deprecated                     (<i>duplicate method</i>) Use {@link #deleteKey(String)} instead.
2158      */
2159     @Deprecated
2160     public void removeCard(String key) throws HeaderCardException {
2161         deleteKey(key);
2162     }
2163 
2164     @Override
2165     public boolean reset() {
2166         try {
2167             FitsUtil.reposition(input, fileOffset);
2168             return true;
2169         } catch (Exception e) {
2170             LOG.log(Level.WARNING, "Exception while repositioning " + input, e);
2171             return false;
2172         }
2173     }
2174 
2175     /**
2176      * @deprecated Use {@link #ensureCardSpace(int)} with 1 as the argument instead.
2177      *                 <p>
2178      *                 Resets any prior preallocated header space, such as was explicitly set by
2179      *                 {@link #ensureCardSpace(int)}, or when the header was read from a stream to ensure it remains
2180      *                 rewritable, if possible.
2181      *                 </p>
2182      *                 <p>
2183      *                 For headers read from a stream, this will affect {@link #rewriteable()}, so users should not call
2184      *                 this method unless they do not intend to {@link #rewrite()} this header into the original FITS.
2185      *                 </p>
2186      *
2187      * @see        #ensureCardSpace(int)
2188      * @see        #read(ArrayDataInput)
2189      * @see        #getMinimumSize()
2190      * @see        #rewriteable()
2191      * @see        #rewrite()
2192      */
2193     @Deprecated
2194     public final void resetOriginalSize() {
2195         ensureCardSpace(1);
2196     }
2197 
2198     @Override
2199     public void rewrite() throws FitsException, IOException {
2200         ArrayDataOutput dos = (ArrayDataOutput) input;
2201 
2202         if (!rewriteable()) {
2203             throw new FitsException("Invalid attempt to rewrite Header.");
2204         }
2205 
2206         FitsUtil.reposition(dos, fileOffset);
2207 
2208         write(dos);
2209         dos.flush();
2210     }
2211 
2212     @Override
2213     public boolean rewriteable() {
2214         long writeSize = FitsUtil
2215                 .addPadding((long) Math.max(minCards, getNumberOfPhysicalCards()) * HeaderCard.FITS_HEADER_CARD_SIZE);
2216         return fileOffset >= 0 && input instanceof ArrayDataOutput && writeSize == getOriginalSize();
2217     }
2218 
2219     /**
2220      * Set the BITPIX value for the header. The following values are permitted by FITS conventions:
2221      * <ul>
2222      * <li>8 -- signed byte data. Also used for tables.</li>
2223      * <li>16 -- signed short data.</li>
2224      * <li>32 -- signed int data.</li>
2225      * <li>64 -- signed long data.</li>
2226      * <li>-32 -- IEEE 32 bit floating point numbers.</li>
2227      * <li>-64 -- IEEE 64 bit floating point numbers.</li>
2228      * </ul>
2229      * 
2230      * @deprecated                          Use the safer {@link #setBitpix(Bitpix)} instead.
2231      *
2232      * @param      val                      The value set by the user.
2233      *
2234      * @throws     IllegalArgumentException if the value is not a valid BITPIX value.
2235      *
2236      * @see                                 #setBitpix(Bitpix)
2237      */
2238     @Deprecated
2239     public void setBitpix(int val) throws IllegalArgumentException {
2240         try {
2241             setBitpix(Bitpix.forValue(val));
2242         } catch (FitsException e) {
2243             throw new IllegalArgumentException("Invalid BITPIX value: " + val, e);
2244         }
2245     }
2246 
2247     /**
2248      * Sets a standard BITPIX value for the header.
2249      * 
2250      * @deprecated        (<i>for internall use</i>) Visibility will be reduced to the package level in the future.
2251      * 
2252      * @param      bitpix The predefined enum value, e.g. {@link Bitpix#INTEGER}.
2253      *
2254      * @since             1.16
2255      *
2256      * @see               #setBitpix(int)
2257      */
2258     @Deprecated
2259     public void setBitpix(Bitpix bitpix) {
2260         Cursor<String, HeaderCard> iter = iterator();
2261         iter.next();
2262         iter.add(bitpix.getHeaderCard());
2263     }
2264 
2265     /**
2266      * Overwite the default header card sorter.
2267      *
2268      * @param headerSorter the sorter tu use or null to disable sorting
2269      */
2270     public void setHeaderSorter(Comparator<String> headerSorter) {
2271         this.headerSorter = headerSorter;
2272     }
2273 
2274     /**
2275      * Set the value of the NAXIS keyword
2276      * 
2277      * @deprecated     (<i>for internal use</i>) Visibility will be reduced to the package level in the future.
2278      *
2279      * @param      val The dimensionality of the data.
2280      */
2281     @Deprecated
2282     public void setNaxes(int val) {
2283         Cursor<String, HeaderCard> iter = iterator();
2284         iter.setKey(BITPIX.key());
2285         if (iter.hasNext()) {
2286             iter.next();
2287         }
2288         iter.add(HeaderCard.create(NAXIS, val));
2289     }
2290 
2291     /**
2292      * Set the dimension for a given axis.
2293      *
2294      * @deprecated      (<i>for internal use</i>) Visibility will be reduced to the package level in the future.
2295      * 
2296      * @param      axis The axis being set.
2297      * @param      dim  The dimension
2298      */
2299     @Deprecated
2300     public void setNaxis(int axis, int dim) {
2301         Cursor<String, HeaderCard> iter = iterator();
2302         if (axis <= 0) {
2303             LOG.warning("setNaxis ignored because axis less than 0");
2304             return;
2305         }
2306         if (axis == 1) {
2307             iter.setKey(NAXIS.key());
2308         } else if (axis > 1) {
2309             iter.setKey(NAXISn.n(axis - 1).key());
2310         }
2311         if (iter.hasNext()) {
2312             iter.next();
2313         }
2314         iter.add(HeaderCard.create(NAXISn.n(axis), dim));
2315     }
2316 
2317     /**
2318      * Set the SIMPLE keyword to the given value.
2319      * 
2320      * @deprecated     (<i>for internall use</i>) Visibility will be reduced to the package level in the future.
2321      *
2322      * @param      val <code>true</code> for the primary header, otherwise <code>false</code>
2323      */
2324     @Deprecated
2325     public void setSimple(boolean val) {
2326         deleteKey(SIMPLE);
2327         deleteKey(XTENSION);
2328         deleteKey(EXTEND);
2329 
2330         Cursor<String, HeaderCard> iter = iterator();
2331 
2332         iter.add(HeaderCard.create(SIMPLE, val));
2333 
2334         // If we're flipping back to and from the primary header
2335         // we need to add in the EXTEND keyword whenever we become
2336         // a primary, because it's not permitted in the extensions
2337         // (at least not where it needs to be in the primary array).
2338         if (findCard(NAXIS) != null) {
2339             if (findCard(NAXISn.n(getIntValue(NAXIS))) != null) {
2340                 iter.next();
2341             }
2342         }
2343 
2344         iter.add(HeaderCard.create(EXTEND, true));
2345     }
2346 
2347     /**
2348      * Set the XTENSION keyword to the given value.
2349      * 
2350      * @deprecated                          (<i>for internall use</i>) Visibility will be reduced to the package level
2351      *                                          in the future.
2352      * 
2353      * @param      val                      The name of the extension.
2354      *
2355      * @throws     IllegalArgumentException if the string value contains characters that are not allowed in FITS
2356      *                                          headers, that is characters outside of the 0x20 thru 0x7E range.
2357      */
2358     @Deprecated
2359     public void setXtension(String val) throws IllegalArgumentException {
2360         deleteKey(SIMPLE);
2361         deleteKey(XTENSION);
2362         deleteKey(EXTEND);
2363         iterator().add(HeaderCard.create(XTENSION, val));
2364     }
2365 
2366     /**
2367      * @return     the number of cards in the header
2368      *
2369      * @deprecated use {@link #getNumberOfCards()}. The units of the size of the header may be unclear.
2370      */
2371     @Deprecated
2372     public int size() {
2373         return cards.size();
2374     }
2375 
2376     /**
2377      * Update a valued entry in the header, or adds a new header entry. If the header does not contain a prior entry for
2378      * the specific keyword, or if the keyword is a comment-style key, a new entry is added at the current editing
2379      * position. Otherwise, the matching existing entry is updated in situ.
2380      *
2381      * @param  key                 The key of the card to be replaced (or added).
2382      * @param  card                A new card
2383      *
2384      * @throws HeaderCardException if the operation failed
2385      */
2386     public void updateLine(IFitsHeader key, HeaderCard card) throws HeaderCardException {
2387         updateLine(key.key(), card);
2388     }
2389 
2390     private void updateValue(IFitsHeader key, Boolean value) {
2391         HeaderCard prior = cards.get(key.key());
2392         if (prior != null) {
2393             prior.setValue(value);
2394         } else {
2395             addValue(key, value);
2396         }
2397     }
2398 
2399     private void updateValue(IFitsHeader key, Number value) {
2400         HeaderCard prior = cards.get(key.key());
2401         if (prior != null) {
2402             prior.setValue(value);
2403         } else {
2404             addValue(key, value);
2405         }
2406     }
2407 
2408     private void updateValue(IFitsHeader key, String value) {
2409         HeaderCard prior = cards.get(key.key());
2410         if (prior != null) {
2411             prior.setValue(value);
2412         } else {
2413             addValue(key, value);
2414         }
2415     }
2416 
2417     /**
2418      * Update an existing card in situ, without affecting the current position, or else add a new card at the current
2419      * position.
2420      *
2421      * @param  key                 The key of the card to be replaced.
2422      * @param  card                A new card
2423      *
2424      * @throws HeaderCardException if the operation failed
2425      */
2426     public final void updateLine(String key, HeaderCard card) throws HeaderCardException {
2427         // Remove an existing card with the matching 'key' (even if that key
2428         // isn't the same
2429         // as the key of the card argument!)
2430         cards.update(key, card);
2431     }
2432 
2433     /**
2434      * Overwrite the lines in the header. Add the new PHDU header to the current one. If keywords appear twice, the new
2435      * value and comment overwrite the current contents. By Richard J Mathar.
2436      *
2437      * @param  newHdr              the list of new header data lines to replace the current ones.
2438      *
2439      * @throws HeaderCardException if the operation failed
2440      * 
2441      * @see                        #mergeDistinct(Header)
2442      */
2443     public void updateLines(final Header newHdr) throws HeaderCardException {
2444         Cursor<String, HeaderCard> j = newHdr.iterator();
2445 
2446         while (j.hasNext()) {
2447             HeaderCard card = j.next();
2448             if (card.isCommentStyleCard()) {
2449                 insertCommentStyle(card.getKey(), card.getComment());
2450             } else {
2451                 updateLine(card.getKey(), card);
2452             }
2453         }
2454     }
2455 
2456     /**
2457      * Writes a number of blank header records, for example to create preallocated blank header space as described by
2458      * the FITS 4.0 standard.
2459      *
2460      * @param  dos         the output stream to which the data is to be written.
2461      * @param  n           the number of blank records to add.
2462      *
2463      * @throws IOException if there was an error writing to the stream
2464      *
2465      * @since              1.16
2466      *
2467      * @see                #ensureCardSpace(int)
2468      */
2469     private void writeBlankCards(ArrayDataOutput dos, int n) throws IOException {
2470         byte[] blank = new byte[HeaderCard.FITS_HEADER_CARD_SIZE];
2471         Arrays.fill(blank, (byte) ' ');
2472 
2473         while (--n >= 0) {
2474             dos.write(blank);
2475         }
2476     }
2477 
2478     /**
2479      * Add required keywords, and removes conflicting ones depending on whether it is designated as a primary header or
2480      * not.
2481      *
2482      * @param  xType         The value for the XTENSION keyword, or <code>null</code> if primary HDU.
2483      *
2484      * @throws FitsException if there was an error trying to edit the header.
2485      *
2486      * @since                1.17
2487      *
2488      * @see                  #validate(FitsOutput)
2489      */
2490     void setRequiredKeys(String xType) throws FitsException {
2491 
2492         if (xType == null) {
2493             // Delete keys that cannot be in primary
2494             deleteKey(XTENSION);
2495 
2496             // Some FITS readers don't like the PCOUNT and GCOUNT keywords in the primary header
2497             if (!getBooleanValue(GROUPS, false)) {
2498                 deleteKey(PCOUNT);
2499                 deleteKey(GCOUNT);
2500             }
2501 
2502             // Make sure we have SIMPLE
2503             updateValue(SIMPLE, true);
2504         } else {
2505             // Delete keys that cannot be in extensions
2506             deleteKey(SIMPLE);
2507 
2508             // Some FITS readers don't like the EXTEND keyword in extensions.
2509             deleteKey(EXTEND);
2510 
2511             // Make sure we have XTENSION
2512             updateValue(XTENSION, xType);
2513         }
2514 
2515         // Make sure we have BITPIX
2516         updateValue(BITPIX, getIntValue(BITPIX, Bitpix.VALUE_FOR_INT));
2517 
2518         int naxes = getIntValue(NAXIS, 0);
2519         updateValue(NAXIS, naxes);
2520 
2521         for (int i = 1; i <= naxes; i++) {
2522             IFitsHeader naxisi = NAXISn.n(i);
2523             updateValue(naxisi, getIntValue(naxisi, 1));
2524         }
2525 
2526         if (xType == null) {
2527             updateValue(EXTEND, true);
2528         } else {
2529             updateValue(PCOUNT, getIntValue(PCOUNT, 0));
2530             updateValue(GCOUNT, getIntValue(GCOUNT, 1));
2531         }
2532     }
2533 
2534     /**
2535      * Validates this header by making it a proper primary or extension header. In both cases it means adding required
2536      * keywords if missing, and removing conflicting cards. Then ordering is checked and corrected as necessary and
2537      * ensures that the <code>END</code> card is at the tail.
2538      *
2539      * @param  asPrimary     <code>true</code> if this header is to be a primary FITS header
2540      *
2541      * @throws FitsException If there was an issue getting the header into proper form.
2542      *
2543      * @since                1.17
2544      */
2545     public void validate(boolean asPrimary) throws FitsException {
2546         setRequiredKeys(asPrimary ? null : getStringValue(XTENSION, "UNKNOWN"));
2547         validate();
2548     }
2549 
2550     /**
2551      * Validates the header making sure it has the required keywords and that the essential keywords appeat in the in
2552      * the required order
2553      *
2554      * @throws FitsException If there was an issue getting the header into proper form.
2555      */
2556     private void validate() throws FitsException {
2557         // Ensure that all cards are in the proper order.
2558         if (headerSorter != null) {
2559             cards.sort(headerSorter);
2560         }
2561 
2562         checkBeginning();
2563         checkEnd();
2564 
2565         updateChecksum();
2566     }
2567 
2568     private void updateChecksum() throws FitsException {
2569         if (containsKey(Checksum.CHECKSUM)) {
2570             HeaderCard dsum = getCard(Checksum.DATASUM);
2571             if (dsum != null) {
2572                 FitsCheckSum.setDatasum(this, dsum.getValue(Long.class, 0L));
2573             } else {
2574                 deleteKey(Checksum.CHECKSUM);
2575             }
2576         }
2577     }
2578 
2579     /**
2580      * (<i>for internal use</i>) Similar to {@link #write(ArrayDataOutput)}, but writes the header as is, without
2581      * ensuring that mandatory keys are present, and in the correct order, or that checksums are updated.
2582      * 
2583      * @param  out           The output file or stream to which to write
2584      * 
2585      * @throws FitsException if there was a violation of the FITS standard
2586      * @throws IOException   if the output was not accessible
2587      * 
2588      * @since                1.20.1
2589      *
2590      * @see                  #write(ArrayDataOutput)
2591      * @see                  #validate(boolean)
2592      */
2593     public void writeUnchecked(ArrayDataOutput out) throws FitsException, IOException {
2594         FitsSettings settings = FitsFactory.current();
2595         fileOffset = FitsUtil.findOffset(out);
2596 
2597         Cursor<String, HeaderCard> writeIterator = cards.iterator(0);
2598 
2599         int size = 0;
2600 
2601         while (writeIterator.hasNext()) {
2602             HeaderCard card = writeIterator.next();
2603             byte[] b = AsciiFuncs.getBytes(card.toString(settings));
2604             size += b.length;
2605 
2606             if (END.key().equals(card.getKey()) && minCards * HeaderCard.FITS_HEADER_CARD_SIZE > size) {
2607                 // AK: Add preallocated blank header space before the END key.
2608                 writeBlankCards(out, minCards - size / HeaderCard.FITS_HEADER_CARD_SIZE);
2609                 size = minCards * HeaderCard.FITS_HEADER_CARD_SIZE;
2610             }
2611 
2612             out.write(b);
2613         }
2614         FitsUtil.pad(out, size, (byte) ' ');
2615         out.flush();
2616     }
2617 
2618     @Override
2619     public void write(ArrayDataOutput out) throws FitsException {
2620         validate();
2621 
2622         try {
2623             writeUnchecked(out);
2624         } catch (IOException e) {
2625             throw new FitsException("IO Error writing header", e);
2626         }
2627     }
2628 
2629     private void addDuplicate(HeaderCard dup) {
2630         // AK: Don't worry about duplicates for comment-style cards in general.
2631         if (dup.isCommentStyleCard()) {
2632             return;
2633         }
2634 
2635         if (duplicates == null) {
2636             duplicates = new ArrayList<>();
2637             dupKeys = new HashSet<>();
2638         }
2639 
2640         if (!dupKeys.contains(dup.getKey())) {
2641             HeaderCardParser.getLogger().log(Level.WARNING, "Multiple occurrences of key:" + dup.getKey());
2642             dupKeys.add(dup.getKey());
2643         }
2644 
2645         duplicates.add(dup);
2646     }
2647 
2648     /**
2649      * Check if the given key is the next one available in the header.
2650      */
2651     private void cardCheck(Cursor<String, HeaderCard> iter, IFitsHeader key) throws FitsException {
2652         cardCheck(iter, key.key());
2653     }
2654 
2655     /**
2656      * Check if the given key is the next one available in the header.
2657      */
2658     private void cardCheck(Cursor<String, HeaderCard> iter, String key) throws FitsException {
2659         if (!iter.hasNext()) {
2660             throw new FitsException("Header terminates before " + key);
2661         }
2662         HeaderCard card = iter.next();
2663         if (!card.getKey().equals(key)) {
2664             throw new FitsException("Key " + key + " not found where expected." + "Found " + card.getKey());
2665         }
2666     }
2667 
2668     private void checkFirstCard(String key) throws FitsException {
2669         // AK: key cannot be null by the caller already, so checking for it makes dead code.
2670         if (!SIMPLE.key().equals(key) && !XTENSION.key().equals(key)) {
2671             throw new FitsException("Not a proper FITS header: " + HeaderCard.sanitize(key) + " at " + fileOffset);
2672         }
2673     }
2674 
2675     private void doCardChecks(Cursor<String, HeaderCard> iter, boolean isTable, boolean isExtension) throws FitsException {
2676         cardCheck(iter, BITPIX);
2677         cardCheck(iter, NAXIS);
2678         int nax = getIntValue(NAXIS);
2679 
2680         for (int i = 1; i <= nax; i++) {
2681             cardCheck(iter, NAXISn.n(i));
2682         }
2683         if (isExtension) {
2684             cardCheck(iter, PCOUNT);
2685             cardCheck(iter, GCOUNT);
2686             if (isTable) {
2687                 cardCheck(iter, TFIELDS);
2688             }
2689         }
2690         // This does not check for the EXTEND keyword which
2691         // if present in the primary array must immediately follow
2692         // the NAXISn.
2693     }
2694 
2695     /**
2696      * Ensure that the header begins with a valid set of keywords. Note that we do not check the values of these
2697      * keywords.
2698      */
2699     private void checkBeginning() throws FitsException {
2700         Cursor<String, HeaderCard> iter = iterator();
2701         if (!iter.hasNext()) {
2702             throw new FitsException("Empty Header");
2703         }
2704         HeaderCard card = iter.next();
2705         String key = card.getKey();
2706         if (!key.equals(SIMPLE.key()) && !key.equals(XTENSION.key())) {
2707             throw new FitsException("No SIMPLE or XTENSION at beginning of Header");
2708         }
2709         boolean isTable = false;
2710         boolean isExtension = false;
2711         if (key.equals(XTENSION.key())) {
2712             String value = card.getValue();
2713             if (value == null || value.isEmpty()) {
2714                 throw new FitsException("Empty XTENSION keyword");
2715             }
2716             isExtension = true;
2717             if (value.equals(XTENSION_BINTABLE) || value.equals("A3DTABLE") || value.equals("TABLE")) {
2718                 isTable = true;
2719             }
2720         }
2721         doCardChecks(iter, isTable, isExtension);
2722 
2723         Bitpix.fromHeader(this, false);
2724     }
2725 
2726     /**
2727      * Ensure that the header has exactly one END keyword in the appropriate location.
2728      */
2729     private void checkEnd() {
2730         // Ensure we have an END card only at the end of the
2731         // header.
2732         Cursor<String, HeaderCard> iter = iterator();
2733 
2734         HeaderCard card;
2735 
2736         while (iter.hasNext()) {
2737             card = iter.next();
2738             if (!card.isKeyValuePair() && card.getKey().equals(END.key())) {
2739                 iter.remove();
2740             }
2741         }
2742         // End cannot have a comment
2743         iter.add(HeaderCard.createCommentStyleCard(END.key(), null));
2744     }
2745 
2746     /**
2747      * Is this a valid header.
2748      *
2749      * @return <CODE>true</CODE> for a valid header, <CODE>false</CODE> otherwise.
2750      */
2751     // TODO retire?
2752     private boolean isValidHeader() {
2753         if (getNumberOfCards() < MIN_NUMBER_OF_CARDS_FOR_VALID_HEADER) {
2754             return false;
2755         }
2756         Cursor<String, HeaderCard> iter = iterator();
2757         String key = iter.next().getKey();
2758         if (!key.equals(SIMPLE.key()) && !key.equals(XTENSION.key())) {
2759             return false;
2760         }
2761         key = iter.next().getKey();
2762         if (!key.equals(BITPIX.key())) {
2763             return false;
2764         }
2765         key = iter.next().getKey();
2766         if (!key.equals(NAXIS.key())) {
2767             return false;
2768         }
2769         while (iter.hasNext()) {
2770             key = iter.next().getKey();
2771         }
2772         return key.equals(END.key());
2773     }
2774 
2775     /**
2776      * @deprecated Use {@link NullDataHDU} instead. Create a header for a null image.
2777      */
2778     @Deprecated
2779     void nullImage() {
2780         Cursor<String, HeaderCard> iter = iterator();
2781         iter.add(HeaderCard.create(SIMPLE, true));
2782         iter.add(Bitpix.BYTE.getHeaderCard());
2783         iter.add(HeaderCard.create(NAXIS, 0));
2784         iter.add(HeaderCard.create(EXTEND, true));
2785     }
2786 
2787     /**
2788      * Find the end of a set of keywords describing a column or axis (or anything else terminated by an index). This
2789      * routine leaves the header ready to add keywords after any existing keywords with the index specified. The user
2790      * should specify a prefix to a keyword that is guaranteed to be present.
2791      */
2792     Cursor<String, HeaderCard> positionAfterIndex(IFitsHeader prefix, int col) {
2793         String colnum = String.valueOf(col);
2794         cursor().setKey(prefix.n(col).key());
2795         if (cursor().hasNext()) {
2796             // Bug fix (references to forward) here by Laurent Borges
2797             boolean toFar = false;
2798             while (cursor().hasNext()) {
2799                 String key = cursor().next().getKey().trim();
2800                 // AK: getKey() cannot return null so no need to check.
2801                 if (key.length() <= colnum.length() || !key.substring(key.length() - colnum.length()).equals(colnum)) {
2802                     toFar = true;
2803                     break;
2804                 }
2805             }
2806             if (toFar) {
2807                 cursor().prev(); // Gone one too far, so skip back an element.
2808             }
2809         }
2810         return cursor();
2811     }
2812 
2813     /**
2814      * Replace the key with a new key. Typically this is used when deleting or inserting columns. If the convention of
2815      * the new keyword is not compatible with the existing value a warning message is logged but no exception is thrown
2816      * (at this point).
2817      *
2818      * @param  oldKey              The old header keyword.
2819      * @param  newKey              the new header keyword.
2820      *
2821      * @return                     <CODE>true</CODE> if the card was replaced.
2822      *
2823      * @throws HeaderCardException If <CODE>newKey</CODE> is not a valid FITS keyword.
2824      */
2825     boolean replaceKey(IFitsHeader oldKey, IFitsHeader newKey) throws HeaderCardException {
2826 
2827         if (oldKey.valueType() == VALUE.NONE) {
2828             throw new IllegalArgumentException("cannot replace comment-style " + oldKey.key());
2829         }
2830 
2831         HeaderCard card = getCard(oldKey);
2832         VALUE newType = newKey.valueType();
2833 
2834         if (card != null && oldKey.valueType() != newType && newType != VALUE.ANY) {
2835             Class<?> type = card.valueType();
2836             Exception e = null;
2837 
2838             // Check that the exisating cards value is compatible with the expected type of the new key.
2839             if (newType == VALUE.NONE) {
2840                 e = new IllegalArgumentException(
2841                         "comment-style " + newKey.key() + " cannot replace valued key " + oldKey.key());
2842             } else if (Boolean.class.isAssignableFrom(type) && newType != VALUE.LOGICAL) {
2843                 e = new IllegalArgumentException(newKey.key() + " cannot not support the existing boolean value.");
2844             } else if (String.class.isAssignableFrom(type) && newType != VALUE.STRING) {
2845                 e = new IllegalArgumentException(newKey.key() + " cannot not support the existing string value.");
2846             } else if (ComplexValue.class.isAssignableFrom(type) && newType != VALUE.COMPLEX) {
2847                 e = new IllegalArgumentException(newKey.key() + " cannot not support the existing complex value.");
2848             } else if (card.isDecimalType() && newType != VALUE.REAL && newType != VALUE.COMPLEX) {
2849                 e = new IllegalArgumentException(newKey.key() + " cannot not support the existing decimal values.");
2850             } else if (Number.class.isAssignableFrom(type) && newType != VALUE.REAL && newType != VALUE.INTEGER
2851                     && newType != VALUE.COMPLEX) {
2852                 e = new IllegalArgumentException(newKey.key() + " cannot not support the existing numerical value.");
2853             }
2854 
2855             if (e != null) {
2856                 LOG.log(Level.WARNING, e.getMessage(), e);
2857             }
2858         }
2859 
2860         return replaceKey(oldKey.key(), newKey.key());
2861     }
2862 
2863     /**
2864      * Replace the key with a new key. Typically this is used when deleting or inserting columns so that TFORMx ->
2865      * TFORMx-1
2866      *
2867      * @param     oldKey              The old header keyword.
2868      * @param     newKey              the new header keyword.
2869      *
2870      * @return                        <CODE>true</CODE> if the card was replaced.
2871      *
2872      * @exception HeaderCardException If <CODE>newKey</CODE> is not a valid FITS keyword. TODO should be private
2873      */
2874     boolean replaceKey(String oldKey, String newKey) throws HeaderCardException {
2875         HeaderCard oldCard = getCard(oldKey);
2876         if (oldCard == null) {
2877             return false;
2878         }
2879         if (!cards.replaceKey(oldKey, newKey)) {
2880             throw new HeaderCardException("Duplicate key [" + newKey + "] in replace");
2881         }
2882         try {
2883             oldCard.changeKey(newKey);
2884         } catch (IllegalArgumentException e) {
2885             throw new HeaderCardException("New key [" + newKey + "] is invalid or too long for existing value.", e);
2886         }
2887         return true;
2888     }
2889 
2890     /**
2891      * Calculate the unpadded size of the data segment from the header information.
2892      *
2893      * @return the unpadded data segment size.
2894      */
2895     private long trueDataSize() {
2896 
2897         // AK: No need to be too strict here. We can get a data size even if the
2898         // header isn't 100% to spec,
2899         // as long as the necessary keys are present. So, just check for the
2900         // required keys, and no more...
2901         if (!containsKey(BITPIX.key()) || !containsKey(NAXIS.key())) {
2902             return 0L;
2903         }
2904 
2905         int naxis = getIntValue(NAXIS, 0);
2906 
2907         // If there are no axes then there is no data.
2908         if (naxis == 0) {
2909             return 0L;
2910         }
2911 
2912         int[] axes = new int[naxis];
2913 
2914         for (int axis = 1; axis <= naxis; axis++) {
2915             axes[axis - 1] = getIntValue(NAXISn.n(axis), 0);
2916         }
2917 
2918         boolean isGroup = getBooleanValue(GROUPS, false);
2919 
2920         int pcount = getIntValue(PCOUNT, 0);
2921         int gcount = getIntValue(GCOUNT, 1);
2922 
2923         int startAxis = 0;
2924 
2925         if (isGroup && naxis > 1 && axes[0] == 0) {
2926             startAxis = 1;
2927         }
2928 
2929         long size = 1;
2930         for (int i = startAxis; i < naxis; i++) {
2931             size *= axes[i];
2932         }
2933 
2934         size += pcount;
2935         size *= gcount;
2936 
2937         // Now multiply by the number of bits per pixel and
2938         // convert to bytes.
2939         size *= Math.abs(getIntValue(BITPIX, 0)) / FitsIO.BITS_OF_1_BYTE;
2940 
2941         return size;
2942     }
2943 
2944     /**
2945      * <p>
2946      * Sets whether warnings about FITS standard violations are logged when a header is being read (parsed). Enabling
2947      * this feature can help identifying various standard violations in existing FITS headers, which nevertheless do not
2948      * prevent the successful reading of the header by this library.
2949      * </p>
2950      * <p>
2951      * If {@link FitsFactory#setAllowHeaderRepairs(boolean)} is set <code>false</code>, this will affect only minor
2952      * violations (e.g. a misplaced '=', missing space after '=', non-standard characters in header etc.), which
2953      * nevertheless do not interfere with the unamiguous parsing of the header information. More severe standard
2954      * violations, where some guessing may be required about the intent of some malformed header record, will throw
2955      * appropriate exceptions. If, however, {@link FitsFactory#setAllowHeaderRepairs(boolean)} is set <code>true</code>,
2956      * the parsing will throw fewer exceptions, and the additional issues may get logged as additional warning instead.
2957      *
2958      * @param value <code>true</code> if parser warnings about FITS standard violations when reading in existing FITS
2959      *                  headers are to be logged, otherwise <code>false</code>
2960      *
2961      * @see         #isParserWarningsEnabled()
2962      * @see         FitsFactory#setAllowHeaderRepairs(boolean)
2963      *
2964      * @since       1.16
2965      */
2966     public static void setParserWarningsEnabled(boolean value) {
2967         Level level = value ? Level.WARNING : Level.SEVERE;
2968         HeaderCardParser.getLogger().setLevel(level);
2969         Logger.getLogger(ComplexValue.class.getName()).setLevel(level);
2970     }
2971 
2972     /**
2973      * Checks whether warnings about FITS standard violations are logged when a header is being read (parsed).
2974      *
2975      * @return <code>true</code> if parser warnings about FITS standard violations when reading in existing FITS headers
2976      *             are enabled and logged, otherwise <code>false</code>
2977      *
2978      * @see    #setParserWarningsEnabled(boolean)
2979      *
2980      * @since  1.16
2981      */
2982     public static boolean isParserWarningsEnabled() {
2983         return !HeaderCardParser.getLogger().getLevel().equals(Level.SEVERE);
2984     }
2985 
2986     /**
2987      * Returns the current preferred alignment character position of inline header comments. This is the position at
2988      * which the '/' is placed for the inline comment. #deprecated
2989      *
2990      * @return The current alignment position for inline comments.
2991      *
2992      * @see    #setCommentAlignPosition(int)
2993      * 
2994      * @since  1.17
2995      */
2996     public static int getCommentAlignPosition() {
2997         return commentAlign;
2998     }
2999 
3000     /**
3001      * Sets a new alignment position for inline header comments.
3002      *
3003      * @param      pos                      [20:70] The character position to which inline comments should be aligned if
3004      *                                          possible.
3005      *
3006      * @throws     IllegalArgumentException if the position is outside of the allowed range.
3007      *
3008      * @see                                 #getCommentAlignPosition()
3009      * 
3010      * @deprecated                          Not recommended as it may violate the FITS standart for 'fixed-format'
3011      *                                          header entries, and make our FITS files unreadable by software that
3012      *                                          expects strict adherence to the standard. We will remove this feature in
3013      *                                          the future.
3014      * 
3015      * @since                               1.17
3016      */
3017     public static void setCommentAlignPosition(int pos) throws IllegalArgumentException {
3018         if (pos < Header.MIN_COMMENT_ALIGN || pos > Header.MAX_COMMENT_ALIGN) {
3019             throw new IllegalArgumentException(
3020                     "Comment alignment " + pos + " out of range (" + MIN_COMMENT_ALIGN + ":" + MAX_COMMENT_ALIGN + ").");
3021         }
3022         commentAlign = pos;
3023     }
3024 }