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 }