1 package nom.tam.fits.header;
2
3 import java.io.Serializable;
4 import java.util.HashMap;
5 import java.util.HashSet;
6 import java.util.Map;
7
8 import nom.tam.fits.HeaderCard;
9
10 /*
11 * #%L
12 * nom.tam FITS library
13 * %%
14 * Copyright (C) 1996 - 2024 nom-tam-fits
15 * %%
16 * This is free and unencumbered software released into the public domain.
17 *
18 * Anyone is free to copy, modify, publish, use, compile, sell, or
19 * distribute this software, either in source code form or as a compiled
20 * binary, for any purpose, commercial or non-commercial, and by any
21 * means.
22 *
23 * In jurisdictions that recognize copyright laws, the author or authors
24 * of this software dedicate any and all copyright interest in the
25 * software to the public domain. We make this dedication for the benefit
26 * of the public at large and to the detriment of our heirs and
27 * successors. We intend this dedication to be an overt act of
28 * relinquishment in perpetuity of all present and future rights to this
29 * software under copyright law.
30 *
31 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
33 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
34 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
35 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
36 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
37 * OTHER DEALINGS IN THE SOFTWARE.
38 * #L%
39 */
40
41 /**
42 * <p>
43 * A concrete implementation of standardized FITS header keywords. Users may instantiate this class or extend it to
44 * define commonly used keywords for their applications, and benefit for the extra checks they afford, and to avoid
45 * typos when using these.
46 * </p>
47 * <p>
48 * FITS keywords must be composed of uppper-case 'A'-'Z', digits, underscore ('_') and hyphen ('-') characters.
49 * Additionally, lower case 'n' may be used as a place-holder for a numerical index, and the keyword name may end with a
50 * lower-case 'a' to indicate that it may be used for/with alternate WCS coordinate systems. (We also allow '/' because
51 * some STScI keywords use it even though it violates the FITS standard.)
52 * </p>
53 *
54 * @since 1.19
55 */
56 public class FitsKey implements IFitsHeader, Serializable {
57
58 /** Generated serial version ID */
59 private static final long serialVersionUID = -8312303744399173040L;
60
61 /** Standard header comment */
62 private final String comment;
63
64 /** The type of HDU in which this keyword may appear */
65 private final HDU hdu;
66
67 /** The FITS keyword pattern that produces the actual keyword as it appears in the header. */
68 private final String key;
69
70 /** Documentation source for the keyword */
71 private final SOURCE status;
72
73 /** The type of value expected for this keyword */
74 private final VALUE valueType;
75
76 /** A list of known comment-style keyword, which do not take an assigned value */
77 private static HashSet<String> commentStyleKeys = new HashSet<>();
78
79 /**
80 * Creates a new standardized FITS keyword with the specific usage constraints. The keyword must be composed of
81 * uppper-case 'A'-'Z', digits, underscore ('_') and hyphen ('-') characters. Additionally, lower case 'n' may be
82 * used as a place-holder for a numerical index, and the keyword name may end with a lower-case 'a' to indicate that
83 * it may be used for/with alternate WCS coordinate systems. (We also allow '/' because some STScI keywords use it
84 * even though it violates the FITS standard.)
85 *
86 * @param headerName The keyword as it will appear in the FITS headers, usually a string with up to 8
87 * characters, containing uppper case letters (A-Z), digits (0-9), and/or
88 * underscore (<code>_</code>) or hyphen (<code>-</code>) characters for
89 * standard FITS keywords.
90 * @param status The convention that defines this keyword
91 * @param hdu the type of HDU this keyword may appear in
92 * @param valueType the type of value that may be associated with this keyword
93 * @param comment the standard comment to include with this keyword
94 *
95 * @throws IllegalArgumentException if the keyword name is invalid.
96 */
97 public FitsKey(String headerName, SOURCE status, HDU hdu, VALUE valueType, String comment)
98 throws IllegalArgumentException {
99 if (headerName.length() > HeaderCard.MAX_KEYWORD_LENGTH) {
100 throw new IllegalArgumentException(
101 "Keyword " + headerName + " exceeeds the FITS " + HeaderCard.MAX_KEYWORD_LENGTH + " character limit");
102 }
103
104 for (int i = 0; i < headerName.length(); i++) {
105 char c = headerName.charAt(i);
106
107 if (c >= 'A' && c <= 'Z') {
108 continue;
109 }
110 if (c >= '0' && c <= '9') {
111 continue;
112 }
113 if (c == '-' || c == '_' || c == '/') {
114 continue;
115 }
116 if (c == 'n') {
117 continue;
118 }
119 if (c == 'a' && (i + 1) == headerName.length()) {
120 continue;
121 }
122
123 throw new IllegalArgumentException("Invalid FITS keyword: " + headerName);
124 }
125
126 this.key = headerName;
127 this.status = status;
128 this.hdu = hdu;
129 this.valueType = valueType;
130 this.comment = comment;
131
132 if (valueType == VALUE.NONE) {
133 commentStyleKeys.add(headerName);
134 }
135 }
136
137 /**
138 * Creates a new standardized user-defined FITS keyword. The keyword will have source set to
139 * {@link IFitsHeader.SOURCE#UNKNOWN}. The keyword must be composed of uppper-case 'A'-'Z', digits, underscore ('_')
140 * and hyphen ('-') characters. Additionally, lower case 'n' may be used as a place-holder for a numerical index,
141 * and the keyword name may end with a lower-case 'a' to indicate that it may be used for/with alternate WCS
142 * coordinate systems. (We also allow '/' because some STScI keywords use it even though it violates the FITS
143 * standard.)
144 *
145 * @param headerName The keyword as it will appear in the FITS headers, usually a string with up to 8
146 * characters, containing uppper case letters (A-Z), digits (0-9), and/or
147 * underscore (<code>_</code>) or hyphen (<code>-</code>) characters for
148 * standard FITS keywords.
149 * @param hdu the type of HDU this keyword may appear in
150 * @param valueType the type of value that may be associated with this keyword
151 * @param comment the standard comment to include with this keyword
152 *
153 * @throws IllegalArgumentException if the keyword name is invalid.
154 *
155 * @since 1.19
156 */
157 public FitsKey(String headerName, HDU hdu, VALUE valueType, String comment) throws IllegalArgumentException {
158 this(headerName, SOURCE.UNKNOWN, hdu, valueType, comment);
159 }
160
161 /**
162 * Creates a new standardized user-defined FITS keyword. The keyword will have source set to
163 * {@link IFitsHeader.SOURCE#UNKNOWN} and HDY type to {@link IFitsHeader.HDU#ANY}. The keyword must be composed of
164 * uppper-case 'A'-'Z', digits, underscore ('_') and hyphen ('-') characters. Additionally, lower case 'n' may be
165 * used as a place-holder for a numerical index, and the keyword name may end with a lower-case 'a' to indicate that
166 * it may be used for/with alternate WCS coordinate systems. (We also allow '/' because some STScI keywords use it
167 * even though it violates the FITS standard.)
168 *
169 * @param headerName The keyword as it will appear in the FITS headers, usually a string with up to 8
170 * characters, containing uppper case letters (A-Z), digits (0-9), and/or
171 * underscore (<code>_</code>) or hyphen (<code>-</code>) characters for
172 * standard FITS keywords.
173 * @param valueType the type of value that may be associated with this keyword
174 * @param comment the standard comment to include with this keyword
175 *
176 * @throws IllegalArgumentException if the keyword name is invalid.
177 *
178 * @since 1.19
179 */
180 public FitsKey(String headerName, VALUE valueType, String comment) throws IllegalArgumentException {
181 this(headerName, HDU.ANY, valueType, comment);
182 }
183
184 @Override
185 public final FitsKey impl() {
186 return this;
187 }
188
189 @Override
190 public String comment() {
191 return comment;
192 }
193
194 @Override
195 public HDU hdu() {
196 return hdu;
197 }
198
199 @Override
200 public String key() {
201 if (key.endsWith("a")) {
202 return key.substring(0, key.length() - 1);
203 }
204 return key;
205 }
206
207 @Override
208 public SOURCE status() {
209 return status;
210 }
211
212 @Override
213 public VALUE valueType() {
214 return valueType;
215 }
216
217 /**
218 * (<i>for internal use</i>) Checks if a keywords is known to be a comment-style keyword. That is, it checks if the
219 * <code>key</code> argument matches any {@link IFitsHeader} constructed via this implementation with a
220 * <code>valueType</code> argument that was <code>null</code>, or if the key is empty.
221 *
222 * @param key the keyword to check
223 *
224 * @return <code>true</code> if the key is empty or if it matches any known {@link IFitsHeader} keywords
225 * implemented through this class that have valueType of <code>null</code>. Otherwise
226 * <code>false</code>.
227 *
228 * @since 1.17
229 */
230 public static boolean isCommentStyleKey(String key) {
231 return commentStyleKeys.contains(key) || key.trim().isEmpty();
232 }
233
234 /**
235 * A registry of all standard keys, for reusing the standards. We keep the registry outside of the enums that define
236 * keys to avoid circular dependency issues.
237 */
238 private static final Map<String, IFitsHeader> STANDARD_KEYS = new HashMap<>();
239
240 static void registerStandard(IFitsHeader key) throws IllegalArgumentException {
241 STANDARD_KEYS.put(key.key(), key);
242 }
243
244 /**
245 * Returns the standard FITS keyword that matches the specified actual key.
246 *
247 * @param key The key as it may appear in a FITS header, e.g. "CTYPE1A"
248 *
249 * @return The standard FITS keyword/pattern that matches, e.g. {@link WCS#CTYPEna}.
250 *
251 * @see IFitsHeader#extractIndices(String)
252 *
253 * @since 1.19
254 */
255 static IFitsHeader matchStandard(String key) {
256 int i = 0, l = key.length();
257 StringBuilder pattern = new StringBuilder();
258
259 // If ends with digit + letter, then it must alt coordinate if standard...
260 if (l > 1 && Character.isAlphabetic(key.charAt(l - 1)) && Character.isDigit(key.charAt(l - 2))) {
261 key = key.substring(0, --l);
262 }
263
264 // If the first digit is a number it may be a coordinate index
265 if (i < l && Character.isDigit(key.charAt(i))) {
266 pattern.append('n');
267 i++;
268
269 // If the second digit is a number it may be a coordinate index
270 if (i < l && Character.isDigit(key.charAt(i))) {
271 pattern.append('n');
272 i++;
273 }
274 }
275
276 // Replace sequence of digits with 'n'
277 while (i < l) {
278 char c = key.charAt(i);
279
280 if (Character.isDigit(c)) {
281 pattern.append('n');
282
283 // Skip successive digits.
284 while (i < l && Character.isDigit(key.charAt(i))) {
285 i++;
286 }
287 } else {
288 pattern.append(c);
289 i++;
290 }
291 }
292
293 return STANDARD_KEYS.get(pattern.toString());
294 }
295
296 /** We define this here with package level visibility for IFitsHeader */
297 static final int BASE_10 = 10;
298 }