1 /* Copyright 2002-2025 CS GROUP
2 * Licensed to CS GROUP (CS) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * CS licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.orekit.time;
18
19 import java.io.BufferedReader;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.InputStreamReader;
23 import java.nio.charset.StandardCharsets;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.SortedMap;
29 import java.util.TreeMap;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import org.hipparchus.util.FastMath;
34 import org.orekit.annotation.DefaultDataContext;
35 import org.orekit.data.AbstractSelfFeedingLoader;
36 import org.orekit.data.DataContext;
37 import org.orekit.data.DataLoader;
38 import org.orekit.data.DataProvidersManager;
39 import org.orekit.errors.OrekitException;
40 import org.orekit.errors.OrekitMessages;
41
42 /** Loader for UTC-TAI extracted from bulletin A files.
43 * <p>This class is a modified version of {@code BulletinAFileLoader}
44 * that only parses the TAI-UTC header line and checks the UT1-UTC column
45 * for discontinuities.
46 * </p>
47 * <p>
48 * Note that extracting UTC-TAI from bulletin A files is <em>NOT</em>
49 * recommended. There are known issues in some past bulletin A
50 * (for example bulletina-xix-001.txt from 2006-01-05 has a wrong year
51 * for last leap second and bulletina-xxi-053.txt from 2008-12-31 has an
52 * off by one value for TAI-UTC on MJD 54832). This is a known problem,
53 * and the Earth Orientation Department at USNO told us this TAI-UTC
54 * data was only provided as a convenience and this data should rather
55 * be sourced from other official files. As the bulletin A files are
56 * a record of past publications, they cannot modify archived bulletins,
57 * hence the errors above will remain forever. This UTC-TAI loader should
58 * therefore be used with great care.
59 * </p>
60 * <p>
61 * This class is immutable and hence thread-safe
62 * </p>
63 * @author Luc Maisonobe
64 * @since 7.1
65 */
66 public class UTCTAIBulletinAFilesLoader extends AbstractSelfFeedingLoader
67 implements UTCTAIOffsetsLoader {
68
69 /**
70 * Build a loader for IERS bulletins A files. This constructor uses the {@link
71 * DataContext#getDefault() default data context}.
72 *
73 * @param supportedNames regular expression for supported files names
74 */
75 @DefaultDataContext
76 public UTCTAIBulletinAFilesLoader(final String supportedNames) {
77 this(supportedNames, DataContext.getDefault().getDataProvidersManager());
78 }
79
80 /**
81 * Build a loader for IERS bulletins A files.
82 *
83 * @param supportedNames regular expression for supported files names
84 * @param manager provides access to the bulletin A files.
85 */
86 public UTCTAIBulletinAFilesLoader(final String supportedNames,
87 final DataProvidersManager manager) {
88 super(supportedNames, manager);
89 }
90
91 /** {@inheritDoc} */
92 @Override
93 public List<OffsetModel> loadOffsets() {
94
95 final Parser parser = new Parser();
96 this.feed(parser);
97 final SortedMap<Integer, Integer> taiUtc = parser.getTaiUtc();
98 final SortedMap<Integer, Double> ut1Utc = parser.getUt1Utc();
99
100 // identify UT1-UTC discontinuities
101 final List<Integer> leapDays = new ArrayList<>();
102 Map.Entry<Integer, Double> previous = null;
103 for (final Map.Entry<Integer, Double> entry : ut1Utc.entrySet()) {
104 if (previous != null) {
105 final double delta = entry.getValue() - previous.getValue();
106 if (FastMath.abs(delta) > 0.5) {
107 // discontinuity found between previous and current entry, a leap second has occurred
108 leapDays.add(entry.getKey());
109 }
110 }
111 previous = entry;
112 }
113
114 final List<OffsetModel> offsets = new ArrayList<>();
115
116 if (!taiUtc.isEmpty()) {
117
118 // find the start offset, before the first UT1-UTC entry
119 final Map.Entry<Integer, Integer> firstTaiMUtc = taiUtc.entrySet().iterator().next();
120 int offset = firstTaiMUtc.getValue();
121 final int refMJD = firstTaiMUtc.getKey();
122 for (final int leapMJD : leapDays) {
123 if (leapMJD > refMJD) {
124 break;
125 }
126 --offset;
127 }
128
129 // set all known time steps
130 for (final int leapMJD : leapDays) {
131 offsets.add(new OffsetModel(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, leapMJD),
132 ++offset));
133 }
134
135 // check for missing time steps
136 for (final Map.Entry<Integer, Integer> refTaiMUtc : taiUtc.entrySet()) {
137 final DateComponents refDC = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH,
138 refTaiMUtc.getKey() + 1);
139 OffsetModel before = null;
140 for (final OffsetModel o : offsets) {
141 if (o.getStart().compareTo(refDC) < 0) {
142 before = o;
143 }
144 }
145 if (before != null) {
146 if (refTaiMUtc.getValue() != (int) FastMath.rint(before.getOffset().toDouble())) {
147 throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES,
148 before.getStart(), refDC);
149 }
150 }
151 }
152
153 // make sure we stop the linear drift that was used before 1972
154 final DateComponents dc1972 = new DateComponents(1972, 1, 1);
155 if (offsets.isEmpty()) {
156 offsets.add(0, new OffsetModel(dc1972, taiUtc.get(taiUtc.firstKey())));
157 } else {
158 if (offsets.get(0).getStart().getYear() > 1972) {
159 offsets.add(0,
160 new OffsetModel(dc1972,
161 dc1972.getMJD(),
162 offsets.get(0).getOffset().subtract(TimeOffset.SECOND),
163 0));
164 }
165 }
166
167 }
168
169 return offsets;
170
171 }
172
173 /** Internal class performing the parsing. */
174 private static class Parser implements DataLoader {
175
176 /** Regular expression matching blanks at start of line. */
177 private static final String LINE_START_REGEXP = "^\\p{Blank}+";
178
179 /** Regular expression matching blanks at end of line. */
180 private static final String LINE_END_REGEXP = "\\p{Blank}*$";
181
182 /** Regular expression matching integers. */
183 private static final String INTEGER_REGEXP = "[-+]?\\p{Digit}+";
184
185 /** Regular expression matching real numbers. */
186 private static final String REAL_REGEXP = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";
187
188 /** Regular expression matching an integer field to store. */
189 private static final String STORED_INTEGER_FIELD = "\\p{Blank}*(" + INTEGER_REGEXP + ")";
190
191 /** regular expression matching a Modified Julian Day field to store. */
192 private static final String STORED_MJD_FIELD = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";
193
194 /** Regular expression matching a real field to store. */
195 private static final String STORED_REAL_FIELD = "\\p{Blank}+(" + REAL_REGEXP + ")";
196
197 /** Regular expression matching a real field to ignore. */
198 private static final String IGNORED_REAL_FIELD = "\\p{Blank}+" + REAL_REGEXP;
199
200 /** Enum for files sections, in expected order.
201 * <p>The bulletin A weekly data files contain several sections,
202 * each introduced with some fixed header text and followed by tabular data.
203 * </p>
204 */
205 private enum Section {
206
207 /** Earth Orientation Parameters rapid service. */
208 // section 2 always contain rapid service data including error fields
209 // COMBINED EARTH ORIENTATION PARAMETERS:
210 //
211 // IERS Rapid Service
212 // MJD x error y error UT1-UTC error
213 // " " " " s s
214 // 13 8 30 56534 0.16762 .00009 0.32705 .00009 0.038697 0.000019
215 // 13 8 31 56535 0.16669 .00010 0.32564 .00010 0.038471 0.000019
216 // 13 9 1 56536 0.16592 .00009 0.32410 .00010 0.038206 0.000024
217 // 13 9 2 56537 0.16557 .00009 0.32270 .00009 0.037834 0.000024
218 // 13 9 3 56538 0.16532 .00009 0.32147 .00010 0.037351 0.000024
219 // 13 9 4 56539 0.16488 .00009 0.32044 .00010 0.036756 0.000023
220 // 13 9 5 56540 0.16435 .00009 0.31948 .00009 0.036036 0.000024
221 EOP_RAPID_SERVICE("^ *COMBINED EARTH ORIENTATION PARAMETERS: *$",
222 LINE_START_REGEXP +
223 STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
224 STORED_MJD_FIELD +
225 IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
226 IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
227 STORED_REAL_FIELD + IGNORED_REAL_FIELD +
228 LINE_END_REGEXP),
229
230 /** Earth Orientation Parameters final values. */
231 // the first bulletin A of each month also includes final values for the
232 // period covering from day 2 of month m-2 to day 1 of month m-1.
233 // IERS Final Values
234 // MJD x y UT1-UTC
235 // " " s
236 // 13 7 2 56475 0.1441 0.3901 0.05717
237 // 13 7 3 56476 0.1457 0.3895 0.05716
238 // 13 7 4 56477 0.1467 0.3887 0.05728
239 // 13 7 5 56478 0.1477 0.3875 0.05755
240 // 13 7 6 56479 0.1490 0.3862 0.05793
241 // 13 7 7 56480 0.1504 0.3849 0.05832
242 // 13 7 8 56481 0.1516 0.3835 0.05858
243 // 13 7 9 56482 0.1530 0.3822 0.05877
244 EOP_FINAL_VALUES("^ *IERS Final Values *$",
245 LINE_START_REGEXP +
246 STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
247 STORED_MJD_FIELD +
248 IGNORED_REAL_FIELD +
249 IGNORED_REAL_FIELD +
250 STORED_REAL_FIELD +
251 LINE_END_REGEXP),
252
253 /** TAI-UTC part of the Earth Orientation Parameters prediction.. */
254 // section 3 always contain prediction data without error fields
255 //
256 // PREDICTIONS:
257 // The following formulas will not reproduce the predictions given below,
258 // but may be used to extend the predictions beyond the end of this table.
259 //
260 // x = 0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
261 // y = 0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
262 // UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
263 //
264 // where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
265 //
266 // TAI-UTC(MJD 56541) = 35.0
267 // The accuracy may be estimated from the expressions:
268 // S x,y = 0.00068 (MJD-56540)**0.80 S t = 0.00025 (MJD-56540)**0.75
269 // Estimated accuracies are: Predictions 10 d 20 d 30 d 40 d
270 // Polar coord's 0.004 0.007 0.010 0.013
271 // UT1-UTC 0.0014 0.0024 0.0032 0.0040
272 //
273 // MJD x(arcsec) y(arcsec) UT1-UTC(sec)
274 // 2013 9 6 56541 0.1638 0.3185 0.03517
275 // 2013 9 7 56542 0.1633 0.3175 0.03420
276 // 2013 9 8 56543 0.1628 0.3164 0.03322
277 // 2013 9 9 56544 0.1623 0.3153 0.03229
278 // 2013 9 10 56545 0.1618 0.3142 0.03144
279 // 2013 9 11 56546 0.1612 0.3131 0.03071
280 // 2013 9 12 56547 0.1607 0.3119 0.03008
281 TAI_UTC("^ *PREDICTIONS: *$",
282 LINE_START_REGEXP +
283 "TAI-UTC\\(MJD *" +
284 STORED_MJD_FIELD +
285 "\\) *= *" +
286 STORED_INTEGER_FIELD + "(?:\\.0*)?" +
287 LINE_END_REGEXP),
288
289 /** Earth Orientation Parameters prediction. */
290 // section 3 always contain prediction data without error fields
291 //
292 // PREDICTIONS:
293 // The following formulas will not reproduce the predictions given below,
294 // but may be used to extend the predictions beyond the end of this table.
295 //
296 // x = 0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
297 // y = 0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
298 // UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
299 //
300 // where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
301 //
302 // TAI-UTC(MJD 56541) = 35.0
303 // The accuracy may be estimated from the expressions:
304 // S x,y = 0.00068 (MJD-56540)**0.80 S t = 0.00025 (MJD-56540)**0.75
305 // Estimated accuracies are: Predictions 10 d 20 d 30 d 40 d
306 // Polar coord's 0.004 0.007 0.010 0.013
307 // UT1-UTC 0.0014 0.0024 0.0032 0.0040
308 //
309 // MJD x(arcsec) y(arcsec) UT1-UTC(sec)
310 // 2013 9 6 56541 0.1638 0.3185 0.03517
311 // 2013 9 7 56542 0.1633 0.3175 0.03420
312 // 2013 9 8 56543 0.1628 0.3164 0.03322
313 // 2013 9 9 56544 0.1623 0.3153 0.03229
314 // 2013 9 10 56545 0.1618 0.3142 0.03144
315 // 2013 9 11 56546 0.1612 0.3131 0.03071
316 // 2013 9 12 56547 0.1607 0.3119 0.03008
317 EOP_PREDICTION("^ *MJD *x\\(arcsec\\) *y\\(arcsec\\) *UT1-UTC\\(sec\\) *$",
318 LINE_START_REGEXP +
319 STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
320 STORED_MJD_FIELD +
321 IGNORED_REAL_FIELD +
322 IGNORED_REAL_FIELD +
323 STORED_REAL_FIELD +
324 LINE_END_REGEXP);
325
326 /** Header pattern. */
327 private final Pattern header;
328
329 /** Data pattern. */
330 private final Pattern data;
331
332 /** Simple constructor.
333 * @param headerRegExp regular expression for header
334 * @param dataRegExp regular expression for data
335 */
336 Section(final String headerRegExp, final String dataRegExp) {
337 this.header = Pattern.compile(headerRegExp);
338 this.data = Pattern.compile(dataRegExp);
339 }
340
341 /** Check if a line matches the section header.
342 * @param l line to check
343 * @return true if the line matches the header
344 */
345 public boolean matchesHeader(final String l) {
346 return header.matcher(l).matches();
347 }
348
349 /** Get the data fields from a line.
350 * @param l line to parse
351 * @return extracted fields, or null if line does not match data format
352 */
353 public String[] getFields(final String l) {
354 final Matcher matcher = data.matcher(l);
355 if (matcher.matches()) {
356 final String[] fields = new String[matcher.groupCount()];
357 for (int i = 0; i < fields.length; ++i) {
358 fields[i] = matcher.group(i + 1);
359 }
360 return fields;
361 } else {
362 return null;
363 }
364 }
365
366 }
367
368 /** TAI-UTC history. */
369 private final SortedMap<Integer, Integer> taiUtc;
370
371 /** UT1-UTC history. */
372 private final SortedMap<Integer, Double> ut1Utc;
373
374 /** Current line number. */
375 private int lineNumber;
376
377 /** Current line. */
378 private String line;
379
380 /** Simple constructor.
381 */
382 Parser() {
383 this.taiUtc = new TreeMap<>();
384 this.ut1Utc = new TreeMap<>();
385 this.lineNumber = 0;
386 }
387
388 /** Get TAI-UTC history.
389 * @return TAI-UTC history
390 */
391 public SortedMap<Integer, Integer> getTaiUtc() {
392 return taiUtc;
393 }
394
395 /** Get UT1-UTC history.
396 * @return UT1-UTC history
397 */
398 public SortedMap<Integer, Double> getUt1Utc() {
399 return ut1Utc;
400 }
401
402 /** {@inheritDoc} */
403 @Override
404 public boolean stillAcceptsData() {
405 return true;
406 }
407
408 /** {@inheritDoc} */
409 @Override
410 public void loadData(final InputStream input, final String name)
411 throws IOException {
412
413 final List<Section> remaining = new ArrayList<>(Arrays.asList(Section.values()));
414 // set up a reader for line-oriented bulletin A files
415 try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
416
417 // loop over sections
418 for (Section section = nextSection(remaining, reader);
419 section != null;
420 section = nextSection(remaining, reader)) {
421
422 if (section == Section.TAI_UTC) {
423 loadTaiUtc(section, reader, name);
424 } else {
425 // load the values
426 loadTimeSteps(section, reader, name);
427 }
428
429 // remove the already parsed section from the list
430 remaining.remove(section);
431
432 }
433
434 }
435 lineNumber = 0;
436
437 // check that the mandatory sections have been parsed
438 if (remaining.contains(Section.EOP_RAPID_SERVICE) || remaining.contains(Section.EOP_PREDICTION)) {
439 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
440 }
441
442 }
443
444 /** Skip to next section header.
445 * @param sections sections to check for
446 * @param reader reader from where file content is obtained
447 * @return the next section or null if no section is found until end of file
448 * @exception IOException if data can't be read
449 */
450 private Section nextSection(final List<Section> sections, final BufferedReader reader)
451 throws IOException {
452
453 for (line = reader.readLine(); line != null; line = reader.readLine()) {
454 ++lineNumber;
455 for (Section section : sections) {
456 if (section.matchesHeader(line)) {
457 return section;
458 }
459 }
460 }
461
462 // we have reached end of file and not found a matching section header
463 return null;
464
465 }
466
467 /** Read TAI-UTC.
468 * @param section section to parse
469 * @param reader reader from where file content is obtained
470 * @param name name of the file (or zip entry)
471 * @exception IOException if data can't be read
472 */
473 private void loadTaiUtc(final Section section, final BufferedReader reader, final String name)
474 throws IOException {
475
476 for (line = reader.readLine(); line != null; line = reader.readLine()) {
477 lineNumber++;
478 final String[] fields = section.getFields(line);
479 if (fields != null) {
480 // we have found the single line we are looking for
481 final int mjd = Integer.parseInt(fields[0]);
482 final int offset = Integer.parseInt(fields[1]);
483 taiUtc.put(mjd, offset);
484 return;
485 }
486 }
487
488 throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
489 name, lineNumber);
490
491 }
492
493 /** Read UT1-UTC.
494 * @param section section to parse
495 * @param reader reader from where file content is obtained
496 * @param name name of the file (or zip entry)
497 * @exception IOException if data can't be read
498 */
499 private void loadTimeSteps(final Section section, final BufferedReader reader, final String name)
500 throws IOException {
501
502 boolean inValuesPart = false;
503 for (line = reader.readLine(); line != null; line = reader.readLine()) {
504 lineNumber++;
505 final String[] fields = section.getFields(line);
506 if (fields != null) {
507
508 // we are within the values part
509 inValuesPart = true;
510
511 // this is a data line, build an entry from the extracted fields
512 final int year = Integer.parseInt(fields[0]);
513 final int month = Integer.parseInt(fields[1]);
514 final int day = Integer.parseInt(fields[2]);
515 final int mjd = Integer.parseInt(fields[3]);
516 final DateComponents dc = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
517 if ((dc.getYear() % 100) != (year % 100) ||
518 dc.getMonth() != month ||
519 dc.getDay() != day) {
520 throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
521 name, year, month, day, mjd);
522 }
523
524 final double offset = Double.parseDouble(fields[4]);
525 ut1Utc.put(mjd, offset);
526
527 } else if (inValuesPart) {
528 // we leave values part
529 return;
530 }
531 }
532
533 throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
534 name, lineNumber);
535
536 }
537
538 }
539
540 }