Begin versioning.
[fits.git] / src / nom / tam / fits / Header.java
1 package nom.tam.fits;
2
3 /* Copyright: Thomas McGlynn 1997-1999.
4  * This code may be used for any purpose, non-commercial
5  * or commercial so long as this copyright notice is retained
6  * in the source code or included in or referred to in any
7  * derived software.
8  *
9  * Many thanks to David Glowacki (U. Wisconsin) for substantial
10  * improvements, enhancements and bug fixes.
11  */
12 import java.io.*;
13 import java.util.*;
14 import nom.tam.util.RandomAccess;
15 import nom.tam.util.*;
16
17 /** This class describes methods to access and manipulate the header
18  * for a FITS HDU. This class does not include code specific
19  * to particular types of HDU.
20  *
21  * As of version 1.1 this class supports the long keyword convention
22  * which allows long string keyword values to be split among multiple
23  * keywords
24  * <pre>
25  *    KEY        = 'ABC&'   /A comment
26  *    CONTINUE      'DEF&'  / Another comment
27  *    CONTINUE      'GHIJKL '
28  * </pre>
29  * The methods getStringValue(key), addValue(key,value,comment)
30  * and deleteCard(key) will get, create/update and delete long string
31  * values if the longStringsEnabled flag is set.  This flag is set
32  * automatically when a FITS header with a LONGSTRN card is found.
33  * The value is not checked.  It may also be set/unset using the
34  * static method setLongStringsEnabled(boolean). [So if a user wishes to ensure
35  * that it is not set, it should be unset after any header is read]
36  * When long strings are found in the FITS header users should be careful not to interpose
37  * new header cards within a long value sequence.
38  *
39  * When writing long strings, the comment is included in the last
40  * card.  If a user is writing long strings, a the keyword
41  * LONGSTRN = 'OGIP 1.0'
42  * should be added to the FITS header, but this is not done automatically
43  * for the user.
44  */
45 public class Header implements FitsElement {
46
47     /** The actual header data stored as a HashedList of
48      * HeaderCard's.
49      */
50     private HashedList cards = new HashedList();
51     /** This iterator allows one to run through the list.
52      */
53     private Cursor iter = cards.iterator(0);
54     /** Offset of this Header in the FITS file */
55     private long fileOffset = -1;
56     /** Number of cards in header last time it was read */
57     private int oldSize;
58     /** Input descriptor last time header was read */
59     private ArrayDataInput input;
60
61     /** Create an empty header */
62     public Header() {
63     }
64     /** Do we support long strings when reading/writing keywords */
65     private static boolean longStringsEnabled = false;
66
67     public static void setLongStringsEnabled(boolean flag) {
68         longStringsEnabled = flag;
69     }
70
71     public static boolean getLongStringsEnabled() {
72         return longStringsEnabled;
73     }
74
75     /** Create a header and populate it from the input stream
76      * @param is  The input stream where header information is expected.
77      */
78     public Header(ArrayDataInput is)
79             throws TruncatedFileException, IOException {
80         read(is);
81     }
82
83     /** Create a header and initialize it with a vector of strings.
84      * @param newCards Card images to be placed in the header.
85      */
86     public Header(String[] newCards) {
87
88         for (int i = 0; i < newCards.length; i += 1) {
89             HeaderCard card = new HeaderCard(newCards[i]);
90             if (card.getValue() == null) {
91                 cards.add(card);
92             } else {
93                 cards.add(card.getKey(), card);
94             }
95
96         }
97     }
98
99     /** Create a header which points to the
100      * given data object.
101      * @param o  The data object to be described.
102      * @exception FitsException if the data was not valid for this header.
103      */
104     public Header(Data o) throws FitsException {
105         o.fillHeader(this);
106     }
107
108     /** Create the data element corresponding to the current header */
109     public Data makeData() throws FitsException {
110         return FitsFactory.dataFactory(this);
111     }
112
113     /**
114      * Update a line in the header
115      * @param key The key of the card to be replaced.
116      * @param card A new card
117      */
118     public void updateLine(String key, HeaderCard card) throws HeaderCardException {
119         removeCard(key);
120         iter.add(key, card);
121     }
122
123     /**
124      * Overwrite the lines in the header.
125      * Add the new PHDU header to the current one. If keywords appear
126      * twice, the new value and comment overwrite the current contents.
127      *
128      * @param newHdr the list of new header data lines to replace the current
129      *               ones.
130      * @throws nom.tam.fits.HeaderCardException
131      * @author Richard J Mathar
132      * @since 2005-10-24
133      */
134     public void updateLines(final Header newHdr) throws nom.tam.fits.HeaderCardException {
135         Cursor j = newHdr.iterator();
136
137         while (j.hasNext()) {
138             HeaderCard nextHCard = (HeaderCard) j.next();
139             // updateLine() doesn't work with COMMENTs because
140             // this would allow only one COMMENT in total in each header
141             if (nextHCard.getKey().startsWith("COMMENT")) {
142                 insertComment(nextHCard.getComment());
143             } else {
144                 updateLine(nextHCard.getKey(), nextHCard);
145             }
146         }
147     }
148
149     /** Find the number of cards in the header */
150     public int getNumberOfCards() {
151         return cards.size();
152     }
153
154     /** Get an iterator over the header cards */
155     public Cursor iterator() {
156         return cards.iterator(0);
157     }
158
159     /** Get the offset of this header */
160     public long getFileOffset() {
161         return fileOffset;
162     }
163
164     /** Calculate the unpadded size of the data segment from
165      * the header information.
166      *
167      * @return the unpadded data segment size.
168      */
169     int trueDataSize() {
170
171         if (!isValidHeader()) {
172             return 0;
173         }
174
175
176         int naxis = getIntValue("NAXIS", 0);
177         int bitpix = getIntValue("BITPIX");
178
179         int[] axes = new int[naxis];
180
181         for (int axis = 1; axis <= naxis; axis += 1) {
182             axes[axis - 1] = getIntValue("NAXIS" + axis, 0);
183         }
184
185         boolean isGroup = getBooleanValue("GROUPS", false);
186
187         int pcount = getIntValue("PCOUNT", 0);
188         int gcount = getIntValue("GCOUNT", 1);
189
190         int startAxis = 0;
191
192         if (isGroup && naxis > 1 && axes[0] == 0) {
193             startAxis = 1;
194         }
195
196         int size = 1;
197         for (int i = startAxis; i < naxis; i += 1) {
198             size *= axes[i];
199         }
200
201         size += pcount;
202         size *= gcount;
203
204         // Now multiply by the number of bits per pixel and
205         // convert to bytes.
206         size *= Math.abs(getIntValue("BITPIX", 0)) / 8;
207
208         return size;
209     }
210
211     /** Return the size of the data including any needed padding.
212      * @return the data segment size including any needed padding.
213      */
214     public long getDataSize() {
215         return FitsUtil.addPadding(trueDataSize());
216     }
217
218     /** Get the size of the header in bytes */
219     public long getSize() {
220         return headerSize();
221     }
222
223     /** Return the size of the header data including padding.
224      * @return the header size including any needed padding.
225      */
226     int headerSize() {
227
228         if (!isValidHeader()) {
229             return 0;
230         }
231
232         return FitsUtil.addPadding(cards.size() * 80);
233     }
234
235     /** Is this a valid header.
236      * @return <CODE>true</CODE> for a valid header,
237      *          <CODE>false</CODE> otherwise.
238      */
239     boolean isValidHeader() {
240
241         if (getNumberOfCards() < 4) {
242             return false;
243         }
244         iter = iterator();
245
246         String key = ((HeaderCard) iter.next()).getKey();
247         if (!key.equals("SIMPLE") && !key.equals("XTENSION")) {
248             return false;
249         }
250         key = ((HeaderCard) iter.next()).getKey();
251         if (!key.equals("BITPIX")) {
252             return false;
253         }
254         key = ((HeaderCard) iter.next()).getKey();
255         if (!key.equals("NAXIS")) {
256             return false;
257         }
258         while (iter.hasNext()) {
259             key = ((HeaderCard) iter.next()).getKey();
260         }
261         if (!key.equals("END")) {
262             return false;
263         }
264         return true;
265
266     }
267
268     /** Find the card associated with a given key.
269      * If found this sets the mark to the card, otherwise it
270      * unsets the mark.
271      * @param key The header key.
272      * @return <CODE>null</CODE> if the keyword could not be found;
273      *          return the HeaderCard object otherwise.
274      */
275     public HeaderCard findCard(String key) {
276
277         HeaderCard card = (HeaderCard) cards.get(key);
278         if (card != null) {
279             iter.setKey(key);
280         }
281         return card;
282     }
283
284     /** Get the value associated with the key as an int.
285      * @param key The header key.
286      * @param dft The value to be returned if the key is not found.
287      */
288     public int getIntValue(String key, int dft) {
289         return (int) getLongValue(key, (long) dft);
290     }
291
292     /** Get the <CODE>int</CODE> value associated with the given key.
293      * @param key The header key.
294      * @return The associated value or 0 if not found.
295      */
296     public int getIntValue(String key) {
297         return (int) getLongValue(key);
298     }
299
300     /** Get the <CODE>long</CODE> value associated with the given key.
301      * @param key The header key.
302      * @return The associated value or 0 if not found.
303      */
304     public long getLongValue(String key) {
305         return getLongValue(key, 0L);
306     }
307
308     /** Get the <CODE>long</CODE> value associated with the given key.
309      * @param key   The header key.
310      * @param dft   The default value to be returned if the key cannot be found.
311      * @return the associated value.
312      */
313     public long getLongValue(String key, long dft) {
314
315         HeaderCard fcard = findCard(key);
316         if (fcard == null) {
317             return dft;
318         }
319
320         try {
321             String v = fcard.getValue();
322             if (v != null) {
323                 return Long.parseLong(v);
324             }
325         } catch (NumberFormatException e) {
326         }
327
328         return dft;
329     }
330
331     /** Get the <CODE>float</CODE> value associated with the given key.
332      * @param key The header key.
333      * @param dft The value to be returned if the key is not found.
334      */
335     public float getFloatValue(String key, float dft) {
336         return (float) getDoubleValue(key, dft);
337     }
338
339     /** Get the <CODE>float</CODE> value associated with the given key.
340      * @param key The header key.
341      * @return The associated value or 0.0 if not found.
342      */
343     public float getFloatValue(String key) {
344         return (float) getDoubleValue(key);
345     }
346
347     /** Get the <CODE>double</CODE> value associated with the given key.
348      * @param key The header key.
349      * @return The associated value or 0.0 if not found.
350      */
351     public double getDoubleValue(String key) {
352         return getDoubleValue(key, 0.);
353     }
354
355     /** Get the <CODE>double</CODE> value associated with the given key.
356      * @param key The header key.
357      * @param dft The default value to return if the key cannot be found.
358      * @return the associated value.
359      */
360     public double getDoubleValue(String key, double dft) {
361
362         HeaderCard fcard = findCard(key);
363         if (fcard == null) {
364             return dft;
365         }
366
367         try {
368             String v = fcard.getValue();
369             if (v != null) {
370                 return new Double(v).doubleValue();
371             }
372         } catch (NumberFormatException e) {
373         }
374
375         return dft;
376     }
377
378     /** Get the <CODE>boolean</CODE> value associated with the given key.
379      * @param The header key.
380      * @return The value found, or false if not found or if the
381      *         keyword is not a logical keyword.
382      */
383     public boolean getBooleanValue(String key) {
384         return getBooleanValue(key, false);
385     }
386
387     /** Get the <CODE>boolean</CODE> value associated with the given key.
388      * @param key The header key.
389      * @param dft The value to be returned if the key cannot be found
390      *            or if the parameter does not seem to be a boolean.
391      * @return the associated value.
392      */
393     public boolean getBooleanValue(String key, boolean dft) {
394
395         HeaderCard fcard = findCard(key);
396         if (fcard == null) {
397             return dft;
398         }
399
400         String val = fcard.getValue();
401         if (val == null) {
402             return dft;
403         }
404
405         if (val.equals("T")) {
406             return true;
407         } else if (val.equals("F")) {
408             return false;
409         } else {
410             return dft;
411         }
412     }
413
414     /** Get the <CODE>String</CODE> value associated with the given key.
415      *
416      * @param key The header key.
417      * @return The associated value or null if not found or if the value is not a string.
418      */
419     public String getStringValue(String key) {
420
421         HeaderCard fcard = findCard(key);
422         if (fcard == null || !fcard.isStringValue()) {
423             return null;
424         }
425
426         String val = fcard.getValue();
427         boolean append = longStringsEnabled
428                 && val != null && val.endsWith("&");
429         iter.next(); // skip the primary card.
430         while (append) {
431             HeaderCard nxt = (HeaderCard) iter.next();
432             if (nxt == null) {
433                 append = false;
434             } else {
435                 key = nxt.getKey();
436                 String comm = nxt.getComment();
437                 if (key == null || comm == null || !key.equals("CONTINUE")) {
438                     append = false;
439                 } else {
440                     comm = continueString(comm);
441                     if (comm != null) {
442                         comm = comm.substring(1, comm.length() - 1);
443                         val = val.substring(0, val.length() - 1) + comm;
444                         append = comm.endsWith("&");
445                     }
446                 }
447             }
448         }
449
450         return val;
451     }
452
453     /** Add a card image to the header.
454      * @param fcard The card to be added.
455      */
456     public void addLine(HeaderCard fcard) {
457
458         if (fcard != null) {
459             if (fcard.isKeyValuePair()) {
460                 iter.add(fcard.getKey(), fcard);
461             } else {
462                 iter.add(fcard);
463             }
464         }
465     }
466
467     /** Add a card image to the header.
468      * @param card The card to be added.
469      * @exception HeaderCardException If the card is not valid.
470      */
471     public void addLine(String card)
472             throws HeaderCardException {
473         addLine(new HeaderCard(card));
474     }
475
476     /** Create a header by reading the information from the input stream.
477      * @param dis The input stream to read the data from.
478      * @return <CODE>null</CODE> if there was a problem with the header;
479      *          otherwise return the header read from the input stream.
480      */
481     public static Header readHeader(ArrayDataInput dis)
482             throws TruncatedFileException, IOException {
483         Header myHeader = new Header();
484         try {
485             myHeader.read(dis);
486         } catch (EOFException e) {
487             // An EOF exception is thrown only if the EOF was detected
488             // when reading the first card.  In this case we want
489             // to return a null.
490             return null;
491         }
492         return myHeader;
493     }
494
495     /** Read a stream for header data.
496      * @param dis The input stream to read the data from.
497      * @return <CODE>null</CODE> if there was a problem with the header;
498      *          otherwise return the header read from the input stream.
499      */
500     public void read(ArrayDataInput dis)
501             throws TruncatedFileException, IOException {
502         if (dis instanceof RandomAccess) {
503             fileOffset = FitsUtil.findOffset(dis);
504         } else {
505             fileOffset = -1;
506         }
507
508         byte[] buffer = new byte[80];
509
510         boolean firstCard = true;
511         int count = 0;
512
513         try {
514             while (true) {
515
516                 int len;
517
518                 int need = 80;
519
520                 try {
521
522                     while (need > 0) {
523                         len = dis.read(buffer, 80 - need, need);
524                         count += 1;
525                         if (len == 0) {
526                             throw new TruncatedFileException();
527                         }
528                         need -= len;
529                     }
530                 } catch (EOFException e) {
531
532                     // Rethrow the EOF if we are at the beginning of the header,
533                     // otherwise we have a FITS error.
534                     //
535                     if (firstCard && need == 80) {
536                         throw e;
537                     }
538                     throw new TruncatedFileException(e.getMessage());
539                 }
540
541                 String cbuf = AsciiFuncs.asciiString(buffer);
542                 HeaderCard fcard = new HeaderCard(cbuf);
543
544                 if (firstCard) {
545
546                     String key = fcard.getKey();
547
548                     if (key == null || (!key.equals("SIMPLE") && !key.equals("XTENSION"))) {
549                         throw new IOException("Not FITS format at " + fileOffset + ":" + cbuf);
550                     }
551                     firstCard = false;
552                 }
553
554                 String key = fcard.getKey();
555                 if (key != null && cards.containsKey(key)) {
556                     System.err.println("Warning: multiple occurrences of key:" + key);
557                 }
558
559                 // We don't check the value here.  If the user
560                 // wants to be sure that long strings are disabled,
561                 // they can call setLongStringsEnabled(false) after
562                 // reading the header.
563                 if (key.equals("LONGSTRN")) {
564                     longStringsEnabled = true;
565                 }
566                 // save card
567                 addLine(fcard);
568                 if (cbuf.substring(0, 8).equals("END     ")) {
569                     break;  // Out of reading the header.
570                 }
571             }
572
573         } catch (EOFException e) {
574             throw e;
575
576         } catch (Exception e) {
577             if (!(e instanceof EOFException)) {
578                 // For compatibility with Java V5 we just add in the error message
579                 // rather than using using the cause mechanism.
580                 // Probably should update this when we can ignore Java 5.
581                 throw new IOException("Invalid FITS Header:"+ e);
582             }
583         }
584         if (fileOffset >= 0) {
585             oldSize = cards.size();
586             input = dis;
587         }
588
589         // Read to the end of the current FITS block.
590         //
591         try {
592             dis.skipBytes(FitsUtil.padding(count * 80));
593         } catch (IOException e) {
594             throw new TruncatedFileException(e.getMessage());
595         }
596     }
597
598     /** Find the card associated with a given key.
599      * @param key The header key.
600      * @return <CODE>null</CODE> if the keyword could not be found;
601      *          return the card image otherwise.
602      */
603     public String findKey(String key) {
604         HeaderCard card = findCard(key);
605         if (card == null) {
606             return null;
607         } else {
608             return card.toString();
609         }
610     }
611
612     /** Replace the key with a new key.  Typically this is used
613      * when deleting or inserting columns so that TFORMx -> TFORMx-1
614      * @param oldKey The old header keyword.
615      * @param newKey the new header keyword.
616      * @return <CODE>true</CODE> if the card was replaced.
617      * @exception HeaderCardException If <CODE>newKey</CODE> is not a
618      *            valid FITS keyword.
619      */
620     boolean replaceKey(String oldKey, String newKey)
621             throws HeaderCardException {
622
623         HeaderCard oldCard = findCard(oldKey);
624         if (oldCard == null) {
625             return false;
626         }
627         if (!cards.replaceKey(oldKey, newKey)) {
628             throw new HeaderCardException("Duplicate key in replace");
629         }
630
631         oldCard.setKey(newKey);
632
633         return true;
634     }
635
636     /** Write the current header (including any needed padding) to the
637      * output stream.
638      * @param dos The output stream to which the data is to be written.
639      * @exception FitsException if the header could not be written.
640      */
641     public void write(ArrayDataOutput dos) throws FitsException {
642
643         fileOffset = FitsUtil.findOffset(dos);
644
645         // Ensure that all cards are in the proper order.
646         cards.sort(new HeaderOrder());
647         checkBeginning();
648         checkEnd();
649         if (cards.size() <= 0) {
650             return;
651         }
652
653
654         Cursor iter = cards.iterator(0);
655
656         try {
657             while (iter.hasNext()) {
658                 HeaderCard card = (HeaderCard) iter.next();
659
660                 byte[] b = AsciiFuncs.getBytes(card.toString());
661                 dos.write(b);
662             }
663
664             FitsUtil.pad(dos, getNumberOfCards() * 80, (byte) ' ');
665         } catch (IOException e) {
666             throw new FitsException("IO Error writing header: " + e);
667         }
668         try {
669             dos.flush();
670         } catch (IOException e) {
671         }
672
673     }
674
675     /** Rewrite the header. */
676     public void rewrite() throws FitsException, IOException {
677
678         ArrayDataOutput dos = (ArrayDataOutput) input;
679
680         if (rewriteable()) {
681             FitsUtil.reposition(dos, fileOffset);
682             write(dos);
683             dos.flush();
684         } else {
685             throw new FitsException("Invalid attempt to rewrite Header.");
686         }
687     }
688
689     /** Reset the file pointer to the beginning of the header */
690     public boolean reset() {
691         try {
692             FitsUtil.reposition(input, fileOffset);
693             return true;
694         } catch (Exception e) {
695             return false;
696         }
697     }
698
699     /** Can the header be rewritten without rewriting the entire file? */
700     public boolean rewriteable() {
701
702         if (fileOffset >= 0
703                 && input instanceof ArrayDataOutput
704                 && (cards.size() + 35) / 36 == (oldSize + 35) / 36) {
705             return true;
706         } else {
707             return false;
708         }
709     }
710
711     /** Add or replace a key with the given boolean value and comment.
712      * @param key     The header key.
713      * @param val     The boolean value.
714      * @param comment A comment to append to the card.
715      * @exception HeaderCardException If the parameters cannot build a
716      *            valid FITS card.
717      */
718     public void addValue(String key, boolean val, String comment)
719             throws HeaderCardException {
720         removeCard(key);
721         iter.add(key, new HeaderCard(key, val, comment));
722     }
723
724     /** Add or replace a key with the given double value and comment.
725      * Note that float values will be promoted to doubles.
726      * @param key     The header key.
727      * @param val     The double value.
728      * @param comment A comment to append to the card.
729      * @exception HeaderCardException If the parameters cannot build a
730      *            valid FITS card.
731      */
732     public void addValue(String key, double val, String comment)
733             throws HeaderCardException {
734         removeCard(key);
735         iter.add(key, new HeaderCard(key, val, comment));
736     }
737
738     ;
739
740     /** Add or replace a key with the given string value and comment.
741      * @param key     The header key.
742      * @param val     The string value.
743      * @param comment A comment to append to the card.
744      * @exception HeaderCardException If the parameters cannot build a
745      *            valid FITS card.
746      */
747     public void addValue(String key, String val, String comment)
748             throws HeaderCardException {
749         removeCard(key);
750         // Remember that quotes get doubled in the value...
751         if (longStringsEnabled && val.replace("'", "''").length() > 68) {
752             addLongString(key, val, comment);
753         } else {
754             iter.add(key, new HeaderCard(key, val, comment));
755         }
756     }
757
758     /** Add or replace a key with the given long value and comment.
759      * Note that int's will be promoted to long's.
760      * @param key     The header key.
761      * @param val     The long value.
762      * @param comment A comment to append to the card.
763      * @exception HeaderCardException If the parameters cannot build a
764      *            valid FITS card.
765      */
766     public void addValue(String key, long val, String comment)
767             throws HeaderCardException {
768         removeCard(key);
769         iter.add(key, new HeaderCard(key, val, comment));
770     }
771
772     private int getAdjustedLength(String in, int max) {
773         // Find the longest string that we can use when
774         // we accommodate needing to double quotes.
775         int size = 0;
776         int i;
777         for (i = 0; i < in.length() && size < max; i += 1) {
778             if (in.charAt(i) == '\'') {
779                 size += 2;
780                 if (size > max) {
781                     break;  // Jumped over the edge
782                 }
783             } else {
784                 size += 1;
785             }
786         }
787         return i;
788     }
789
790     protected void addLongString(String key, String val, String comment)
791             throws HeaderCardException {
792         // We assume that we've made the test so that
793         // we need to write a long string.  We need to
794         // double the quotes in the string value.  addValue
795         // takes care of that for us, but we need to do it
796         // ourselves when we are extending into the comments.
797         // We also need to be careful that single quotes don't
798         // make the string too long and that we don't split
799         // in the middle of a quote.
800         int off = getAdjustedLength(val, 67);
801         String curr = val.substring(0, off) + '&';
802         // No comment here since we're using as much of the card as we can
803         addValue(key, curr, null);
804         val = val.substring(off);
805
806         while (val != null && val.length() > 0) {
807             off = getAdjustedLength(val, 67);
808             if (off < val.length()) {
809                 curr = "'" + val.substring(0, off).replace("'", "''") + "&'";
810                 val = val.substring(off);
811             } else {
812                 curr = "'" + val.replace("'", "''") + "' / " + comment;
813                 val = null;
814             }
815
816             iter.add(new HeaderCard("CONTINUE", null, curr));
817         }
818     }
819
820     /** Delete a key.
821      * @param key     The header key.
822      */
823     public void removeCard(String key)
824             throws HeaderCardException {
825
826         if (cards.containsKey(key)) {
827             iter.setKey(key);
828             if (iter.hasNext()) {
829                 HeaderCard hc = (HeaderCard) iter.next();
830                 String val = hc.getValue();
831                 boolean delExtensions =
832                         longStringsEnabled && val != null && val.endsWith("&");
833                 iter.remove();
834                 while (delExtensions) {
835                     hc = (HeaderCard) iter.next();
836                     if (hc == null) {
837                         delExtensions = false;
838                     } else {
839                         if (hc.getKey().equals("CONTINUE")) {
840                             String more = hc.getComment();
841                             more = continueString(more);
842                             if (more != null) {
843                                 iter.remove();
844                                 delExtensions = more.endsWith("&'");
845                             } else {
846                                 delExtensions = false;
847                             }
848                         } else {
849                             delExtensions = false;
850                         }
851                     }
852                 }
853             }
854         }
855     }
856
857     /** Look for the continuation part of a COMMENT.
858      *  The comment may also include a 'real' comment, e.g.,
859      *  <pre>
860      *  X = 'AB&'
861      *  CONTINUE 'CDEF' / ABC
862      *  </pre>
863      *  Here we are looking for just the 'CDEF' part of the CONTINUE card.
864      */
865     private String continueString(String input) {
866         if (input == null) {
867             return null;
868         }
869
870         input = input.trim();
871         if (input.length() < 2 || input.charAt(0) != '\'') {
872             return null;
873         }
874
875         for (int i = 1; i < input.length(); i += 1) {
876             char c = input.charAt(i);
877             if (c == '\'') {
878                 if (i < input.length() - 1 && input.charAt(i + 1) == c) {
879                     // consecutive quotes -> escaped single quote
880                     // Get rid of the extra quote.
881                     input = input.substring(0, i) + input.substring(i + 1);
882                     continue;  // Check the next character.
883                 } else {
884                     // Found closing apostrophe
885                     return input.substring(0, i + 1);
886                 }
887             }
888         }
889         // Never found a closing apostrophe.
890         return null;
891     }
892
893     /** Add a line to the header using the COMMENT style, i.e., no '='
894      * in column 9.
895      * @param header The comment style header.
896      * @param value  A string to follow the header.
897      * @exception HeaderCardException If the parameters cannot build a
898      *            valid FITS card.
899      */
900     public void insertCommentStyle(String header, String value) {
901         // Should just truncate strings, so we should never get
902         // an exception...
903
904         try {
905             iter.add(new HeaderCard(header, null, value));
906         } catch (HeaderCardException e) {
907             System.err.println("Impossible Exception for comment style:" + header + ":" + value);
908         }
909     }
910
911     /** Add a COMMENT line.
912      * @param value The comment.
913      * @exception HeaderCardException If the parameter is not a
914      *            valid FITS comment.
915      */
916     public void insertComment(String value)
917             throws HeaderCardException {
918         insertCommentStyle("COMMENT", value);
919     }
920
921     /** Add a HISTORY line.
922      * @param value The history record.
923      * @exception HeaderCardException If the parameter is not a
924      *            valid FITS comment.
925      */
926     public void insertHistory(String value)
927             throws HeaderCardException {
928         insertCommentStyle("HISTORY", value);
929     }
930
931     /** Delete the card associated with the given key.
932      * Nothing occurs if the key is not found.
933      *
934      * @param key The header key.
935      */
936     public void deleteKey(String key) {
937
938         iter.setKey(key);
939         if (iter.hasNext()) {
940             iter.next();
941             iter.remove();
942         }
943     }
944
945     /** Tests if the specified keyword is present in this table.
946      * @param key the keyword to be found.
947      * @return <CODE>true<CODE> if the specified keyword is present in this
948      *          table; <CODE>false<CODE> otherwise.
949      */
950     public final boolean containsKey(String key) {
951         return cards.containsKey(key);
952     }
953
954     /** Create a header for a null image.
955      */
956     void nullImage() {
957
958         iter = iterator();
959         try {
960             addValue("SIMPLE", true, "ntf::header:simple:2");
961             addValue("BITPIX", 8,    "ntf::header:bitpix:2");
962             addValue("NAXIS", 0,     "ntf::header:naxis:2");
963             addValue("EXTEND", true, "ntf::header:extend:2");
964         } catch (HeaderCardException e) {
965         }
966     }
967
968     /** Set the SIMPLE keyword to the given value.
969      * @param val The boolean value -- Should be true for FITS data.
970      */
971     public void setSimple(boolean val) {
972         deleteKey("SIMPLE");
973         deleteKey("XTENSION");
974
975         // If we're flipping back to and from the primary header
976         // we need to add in the EXTEND keyword whenever we become
977         // a primary, because it's not permitted in the extensions
978         // (at least not where it needs to be in the primary array).
979         if (findCard("NAXIS") != null) {
980             int nax = getIntValue("NAXIS");
981
982             iter = iterator();
983
984
985             if (findCard("NAXIS" + nax) != null) {
986                 HeaderCard hc = (HeaderCard) iter.next();
987                 try {
988                     removeCard("EXTEND");
989                     iter.add("EXTEND", new HeaderCard("EXTEND", true, "ntf::header:extend:1"));
990                 } catch (Exception e) {  // Ignore the exception
991                 }
992                 ;
993             }
994         }
995
996         iter = iterator();
997         try {
998             iter.add("SIMPLE",
999                     new HeaderCard("SIMPLE", val, "ntf::header:simple:1"));
1000         } catch (HeaderCardException e) {
1001             System.err.println("Impossible exception at setSimple " + e);
1002         }
1003     }
1004
1005     /** Set the XTENSION keyword to the given value.
1006      * @param val The name of the extension. "IMAGE" and "BINTABLE" are supported.
1007      */
1008     public void setXtension(String val) {
1009         deleteKey("SIMPLE");
1010         deleteKey("XTENSION");
1011         deleteKey("EXTEND");
1012         iter = iterator();
1013         try {
1014             iter.add("XTENSION",
1015                     new HeaderCard("XTENSION", val, "ntf::header:xtension:1"));
1016         } catch (HeaderCardException e) {
1017             System.err.println("Impossible exception at setXtension " + e);
1018         }
1019     }
1020
1021     /** Set the BITPIX value for the header.
1022      * @param val.  The following values are permitted by FITS conventions:
1023      * <ul>
1024      * <li> 8  -- signed bytes data.  Also used for tables.
1025      * <li> 16 -- signed short data.
1026      * <li> 32 -- signed int data.
1027      * <li> 64 -- signed long data.
1028      * <li> -32 -- IEEE 32 bit floating point numbers.
1029      * <li> -64 -- IEEE 64 bit floating point numbers.
1030      * </ul>
1031      */
1032     public void setBitpix(int val) {
1033         iter = iterator();
1034         iter.next();
1035         try {
1036             iter.add("BITPIX", new HeaderCard("BITPIX", val, "ntf::header:bitpix:1"));
1037         } catch (HeaderCardException e) {
1038             System.err.println("Impossible exception at setBitpix " + e);
1039         }
1040     }
1041
1042     /** Set the value of the NAXIS keyword
1043      * @param val The dimensionality of the data.
1044      */
1045     public void setNaxes(int val) {
1046         iter.setKey("BITPIX");
1047         if (iter.hasNext()) {
1048             iter.next();
1049         }
1050
1051         try {
1052             iter.add("NAXIS", new HeaderCard("NAXIS", val, "ntf::header:naxis:1"));
1053         } catch (HeaderCardException e) {
1054             System.err.println("Impossible exception at setNaxes " + e);
1055         }
1056     }
1057
1058     /** Set the dimension for a given axis.
1059      * @param axis The axis being set.
1060      * @param dim  The dimension
1061      */
1062     public void setNaxis(int axis, int dim) {
1063
1064         if (axis <= 0) {
1065             return;
1066         }
1067         if (axis == 1) {
1068             iter.setKey("NAXIS");
1069         } else if (axis > 1) {
1070             iter.setKey("NAXIS" + (axis - 1));
1071         }
1072         if (iter.hasNext()) {
1073             iter.next();
1074         }
1075         try {
1076             iter.add("NAXIS" + axis,
1077                     new HeaderCard("NAXIS" + axis, dim, "ntf::header:naxisN:1"));
1078
1079         } catch (HeaderCardException e) {
1080             System.err.println("Impossible exception at setNaxis " + e);
1081         }
1082     }
1083
1084     /** Ensure that the header begins with
1085      *  a valid set of keywords.  Note that we
1086      *  do not check the values of these keywords.
1087      */
1088     void checkBeginning() throws FitsException {
1089
1090         iter = iterator();
1091
1092         if (!iter.hasNext()) {
1093             throw new FitsException("Empty Header");
1094         }
1095         HeaderCard card = (HeaderCard) iter.next();
1096         String key = card.getKey();
1097         if (!key.equals("SIMPLE") && !key.equals("XTENSION")) {
1098             throw new FitsException("No SIMPLE or XTENSION at beginning of Header");
1099         }
1100         boolean isTable = false;
1101         boolean isExtension = false;
1102         if (key.equals("XTENSION")) {
1103             String value = card.getValue();
1104             if (value == null) {
1105                 throw new FitsException("Empty XTENSION keyword");
1106             }
1107
1108             isExtension = true;
1109
1110             if (value.equals("BINTABLE") || value.equals("A3DTABLE")
1111                     || value.equals("TABLE")) {
1112                 isTable = true;
1113             }
1114         }
1115
1116         cardCheck("BITPIX");
1117         cardCheck("NAXIS");
1118
1119         int nax = getIntValue("NAXIS");
1120         iter.next();
1121
1122         for (int i = 1; i <= nax; i += 1) {
1123             cardCheck("NAXIS" + i);
1124         }
1125
1126         if (isExtension) {
1127             cardCheck("PCOUNT");
1128             cardCheck("GCOUNT");
1129             if (isTable) {
1130                 cardCheck("TFIELDS");
1131             }
1132         }
1133         // This does not check for the EXTEND keyword which
1134         // if present in the primary array must immediately follow
1135         // the NAXISn.
1136     }
1137
1138     /** Check if the given key is the next one available in
1139      *  the header.
1140      */
1141     private void cardCheck(String key) throws FitsException {
1142
1143         if (!iter.hasNext()) {
1144             throw new FitsException("Header terminates before " + key);
1145         }
1146         HeaderCard card = (HeaderCard) iter.next();
1147         if (!card.getKey().equals(key)) {
1148             throw new FitsException("Key " + key + " not found where expected."
1149                     + "Found " + card.getKey());
1150         }
1151     }
1152
1153     /** Ensure that the header has exactly one END keyword in
1154      * the appropriate location.
1155      */
1156     void checkEnd() {
1157
1158         // Ensure we have an END card only at the end of the
1159         // header.
1160         //
1161         iter = iterator();
1162         HeaderCard card;
1163
1164         while (iter.hasNext()) {
1165             card = (HeaderCard) iter.next();
1166             if (!card.isKeyValuePair() && card.getKey().equals("END")) {
1167                 iter.remove();
1168             }
1169         }
1170         try {
1171             // End cannot have a comment
1172             iter.add(new HeaderCard("END", null, null));
1173         } catch (HeaderCardException e) {
1174         }
1175     }
1176
1177     /** Print the header to a given stream.
1178      * @param ps the stream to which the card images are dumped.
1179      */
1180     public void dumpHeader(PrintStream ps) {
1181         iter = iterator();
1182         while (iter.hasNext()) {
1183             ps.println(iter.next());
1184         }
1185     }
1186
1187     /***** Deprecated methods *******/
1188     /** Find the number of cards in the header
1189      * @deprecated see numberOfCards().  The units
1190      * of the size of the header may be unclear.
1191      */
1192     public int size() {
1193         return cards.size();
1194     }
1195
1196     /** Get the n'th card image in the header
1197      * @return the card image; return <CODE>null</CODE> if the n'th card
1198      *          does not exist.
1199      * @deprecated An iterator should be used for sequential
1200      *             access to the header.
1201      */
1202     public String getCard(int n) {
1203         if (n >= 0 && n < cards.size()) {
1204             iter = cards.iterator(n);
1205             HeaderCard c = (HeaderCard) iter.next();
1206             return c.toString();
1207         }
1208         return null;
1209     }
1210
1211     /** Get the n'th key in the header.
1212      * @return the card image; return <CODE>null</CODE> if the n'th key
1213      *          does not exist.
1214      * @deprecated An iterator should be used for sequential
1215      *             access to the header.
1216      */
1217     public String getKey(int n) {
1218
1219         String card = getCard(n);
1220         if (card == null) {
1221             return null;
1222         }
1223
1224         String key = card.substring(0, 8);
1225         if (key.charAt(0) == ' ') {
1226             return "";
1227         }
1228
1229
1230         if (key.indexOf(' ') >= 1) {
1231             key = key.substring(0, key.indexOf(' '));
1232         }
1233         return key;
1234     }
1235
1236     /** Create a header which points to the
1237      * given data object.
1238      * @param o  The data object to be described.
1239      * @exception FitsException if the data was not valid for this header.
1240      * @deprecated Use the appropriate Header constructor.
1241      */
1242     public void pointToData(Data o) throws FitsException {
1243         o.fillHeader(this);
1244     }
1245
1246     /** Find the end of a set of keywords describing a column or axis
1247      *  (or anything else terminated by an index.  This routine leaves
1248      *  the header ready to add keywords after any existing keywords
1249      *  with the index specified.  The user should specify a
1250      *  prefix to a keyword that is guaranteed to be present.
1251      */
1252     Cursor positionAfterIndex(String prefix, int col) {
1253         String colnum = "" + col;
1254
1255         iter.setKey(prefix + colnum);
1256
1257         if (iter.hasNext()) {
1258
1259             // Bug fix (references to forward) here by Laurent Borges
1260             boolean forward = false;
1261
1262             String key;
1263             while (iter.hasNext()) {
1264
1265                 key = ((HeaderCard) iter.next()).getKey().trim();
1266                 if (key == null
1267                         || key.length() <= colnum.length()
1268                         || !key.substring(key.length() - colnum.length()).equals(colnum)) {
1269                     forward = true;
1270                     break;
1271                 }
1272             }
1273             if (forward) {
1274                 iter.prev();   // Gone one too far, so skip back an element.
1275             }
1276         }
1277         return iter;
1278     }
1279
1280     /** Get the next card in the Header using the current iterator */
1281     public HeaderCard nextCard() {
1282         if (iter == null) {
1283             return null;
1284         }
1285         if (iter.hasNext()) {
1286             return (HeaderCard) iter.next();
1287         } else {
1288             return null;
1289         }
1290     }
1291
1292     /** Move after the EXTEND keyword in images.
1293      *  Used in bug fix noted by V. Forchi
1294      */
1295     void afterExtend() {
1296         if (findCard("EXTEND") != null) {
1297             nextCard();
1298         }
1299     }
1300 }