1   /* Copyright 2002-2024 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())) {
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             if (offsets.isEmpty()) {
155                 offsets.add(0, new OffsetModel(new DateComponents(1972, 1, 1), taiUtc.get(taiUtc.firstKey())));
156             } else {
157                 if (offsets.get(0).getStart().getYear() > 1972) {
158                     offsets.add(0, new OffsetModel(new DateComponents(1972, 1, 1),
159                                                 ((int) FastMath.rint(offsets.get(0).getOffset())) - 1));
160                 }
161             }
162 
163         }
164 
165         return offsets;
166 
167     }
168 
169     /** Internal class performing the parsing. */
170     private static class Parser implements DataLoader {
171 
172         /** Regular expression matching blanks at start of line. */
173         private static final String LINE_START_REGEXP     = "^\\p{Blank}+";
174 
175         /** Regular expression matching blanks at end of line. */
176         private static final String LINE_END_REGEXP       = "\\p{Blank}*$";
177 
178         /** Regular expression matching integers. */
179         private static final String INTEGER_REGEXP        = "[-+]?\\p{Digit}+";
180 
181         /** Regular expression matching real numbers. */
182         private static final String REAL_REGEXP           = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";
183 
184         /** Regular expression matching an integer field to store. */
185         private static final String STORED_INTEGER_FIELD  = "\\p{Blank}*(" + INTEGER_REGEXP + ")";
186 
187         /** regular expression matching a Modified Julian Day field to store. */
188         private static final String STORED_MJD_FIELD      = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";
189 
190         /** Regular expression matching a real field to store. */
191         private static final String STORED_REAL_FIELD     = "\\p{Blank}+(" + REAL_REGEXP + ")";
192 
193         /** Regular expression matching a real field to ignore. */
194         private static final String IGNORED_REAL_FIELD    = "\\p{Blank}+" + REAL_REGEXP;
195 
196         /** Enum for files sections, in expected order.
197          * <p>The bulletin A weekly data files contain several sections,
198          * each introduced with some fixed header text and followed by tabular data.
199          * </p>
200          */
201         private enum Section {
202 
203             /** Earth Orientation Parameters rapid service. */
204             // section 2 always contain rapid service data including error fields
205             //      COMBINED EARTH ORIENTATION PARAMETERS:
206             //
207             //                              IERS Rapid Service
208             //              MJD      x    error     y    error   UT1-UTC   error
209             //                       "      "       "      "        s        s
210             //   13  8 30  56534 0.16762 .00009 0.32705 .00009  0.038697 0.000019
211             //   13  8 31  56535 0.16669 .00010 0.32564 .00010  0.038471 0.000019
212             //   13  9  1  56536 0.16592 .00009 0.32410 .00010  0.038206 0.000024
213             //   13  9  2  56537 0.16557 .00009 0.32270 .00009  0.037834 0.000024
214             //   13  9  3  56538 0.16532 .00009 0.32147 .00010  0.037351 0.000024
215             //   13  9  4  56539 0.16488 .00009 0.32044 .00010  0.036756 0.000023
216             //   13  9  5  56540 0.16435 .00009 0.31948 .00009  0.036036 0.000024
217             EOP_RAPID_SERVICE("^ *COMBINED EARTH ORIENTATION PARAMETERS: *$",
218                               LINE_START_REGEXP +
219                               STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
220                               STORED_MJD_FIELD +
221                               IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
222                               IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
223                               STORED_REAL_FIELD  + IGNORED_REAL_FIELD +
224                               LINE_END_REGEXP),
225 
226             /** Earth Orientation Parameters final values. */
227             // the first bulletin A of each month also includes final values for the
228             // period covering from day 2 of month m-2 to day 1 of month m-1.
229             //                                IERS Final Values
230             //                                 MJD        x        y      UT1-UTC
231             //                                            "        "         s
232             //             13  7  2           56475    0.1441   0.3901   0.05717
233             //             13  7  3           56476    0.1457   0.3895   0.05716
234             //             13  7  4           56477    0.1467   0.3887   0.05728
235             //             13  7  5           56478    0.1477   0.3875   0.05755
236             //             13  7  6           56479    0.1490   0.3862   0.05793
237             //             13  7  7           56480    0.1504   0.3849   0.05832
238             //             13  7  8           56481    0.1516   0.3835   0.05858
239             //             13  7  9           56482    0.1530   0.3822   0.05877
240             EOP_FINAL_VALUES("^ *IERS Final Values *$",
241                              LINE_START_REGEXP +
242                              STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
243                              STORED_MJD_FIELD +
244                              IGNORED_REAL_FIELD +
245                              IGNORED_REAL_FIELD +
246                              STORED_REAL_FIELD +
247                              LINE_END_REGEXP),
248 
249             /** TAI-UTC part of the Earth Orientation Parameters prediction.. */
250             // section 3 always contain prediction data without error fields
251             //
252             //         PREDICTIONS:
253             //         The following formulas will not reproduce the predictions given below,
254             //         but may be used to extend the predictions beyond the end of this table.
255             //
256             //         x =  0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
257             //         y =  0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
258             //            UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
259             //
260             //         where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
261             //
262             //            TAI-UTC(MJD 56541) = 35.0
263             //         The accuracy may be estimated from the expressions:
264             //         S x,y = 0.00068 (MJD-56540)**0.80   S t = 0.00025 (MJD-56540)**0.75
265             //         Estimated accuracies are:  Predictions     10 d   20 d   30 d   40 d
266             //                                    Polar coord's  0.004  0.007  0.010  0.013
267             //                                    UT1-UTC        0.0014 0.0024 0.0032 0.0040
268             //
269             //                       MJD      x(arcsec)   y(arcsec)   UT1-UTC(sec)
270             //          2013  9  6  56541       0.1638      0.3185      0.03517
271             //          2013  9  7  56542       0.1633      0.3175      0.03420
272             //          2013  9  8  56543       0.1628      0.3164      0.03322
273             //          2013  9  9  56544       0.1623      0.3153      0.03229
274             //          2013  9 10  56545       0.1618      0.3142      0.03144
275             //          2013  9 11  56546       0.1612      0.3131      0.03071
276             //          2013  9 12  56547       0.1607      0.3119      0.03008
277             TAI_UTC("^ *PREDICTIONS: *$",
278                     LINE_START_REGEXP +
279                     "TAI-UTC\\(MJD *" +
280                     STORED_MJD_FIELD +
281                     "\\) *= *" +
282                     STORED_INTEGER_FIELD + "(?:\\.0*)?" +
283                     LINE_END_REGEXP),
284 
285             /** Earth Orientation Parameters prediction. */
286             // section 3 always contain prediction data without error fields
287             //
288             //         PREDICTIONS:
289             //         The following formulas will not reproduce the predictions given below,
290             //         but may be used to extend the predictions beyond the end of this table.
291             //
292             //         x =  0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
293             //         y =  0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
294             //            UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
295             //
296             //         where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
297             //
298             //            TAI-UTC(MJD 56541) = 35.0
299             //         The accuracy may be estimated from the expressions:
300             //         S x,y = 0.00068 (MJD-56540)**0.80   S t = 0.00025 (MJD-56540)**0.75
301             //         Estimated accuracies are:  Predictions     10 d   20 d   30 d   40 d
302             //                                    Polar coord's  0.004  0.007  0.010  0.013
303             //                                    UT1-UTC        0.0014 0.0024 0.0032 0.0040
304             //
305             //                       MJD      x(arcsec)   y(arcsec)   UT1-UTC(sec)
306             //          2013  9  6  56541       0.1638      0.3185      0.03517
307             //          2013  9  7  56542       0.1633      0.3175      0.03420
308             //          2013  9  8  56543       0.1628      0.3164      0.03322
309             //          2013  9  9  56544       0.1623      0.3153      0.03229
310             //          2013  9 10  56545       0.1618      0.3142      0.03144
311             //          2013  9 11  56546       0.1612      0.3131      0.03071
312             //          2013  9 12  56547       0.1607      0.3119      0.03008
313             EOP_PREDICTION("^ *MJD *x\\(arcsec\\) *y\\(arcsec\\) *UT1-UTC\\(sec\\) *$",
314                            LINE_START_REGEXP +
315                            STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
316                            STORED_MJD_FIELD +
317                            IGNORED_REAL_FIELD +
318                            IGNORED_REAL_FIELD +
319                            STORED_REAL_FIELD +
320                            LINE_END_REGEXP);
321 
322             /** Header pattern. */
323             private final Pattern header;
324 
325             /** Data pattern. */
326             private final Pattern data;
327 
328             /** Simple constructor.
329              * @param headerRegExp regular expression for header
330              * @param dataRegExp regular expression for data
331              */
332             Section(final String headerRegExp, final String dataRegExp) {
333                 this.header = Pattern.compile(headerRegExp);
334                 this.data   = Pattern.compile(dataRegExp);
335             }
336 
337             /** Check if a line matches the section header.
338              * @param l line to check
339              * @return true if the line matches the header
340              */
341             public boolean matchesHeader(final String l) {
342                 return header.matcher(l).matches();
343             }
344 
345             /** Get the data fields from a line.
346              * @param l line to parse
347              * @return extracted fields, or null if line does not match data format
348              */
349             public String[] getFields(final String l) {
350                 final Matcher matcher = data.matcher(l);
351                 if (matcher.matches()) {
352                     final String[] fields = new String[matcher.groupCount()];
353                     for (int i = 0; i < fields.length; ++i) {
354                         fields[i] = matcher.group(i + 1);
355                     }
356                     return fields;
357                 } else {
358                     return null;
359                 }
360             }
361 
362         }
363 
364         /** TAI-UTC history. */
365         private final SortedMap<Integer, Integer> taiUtc;
366 
367         /** UT1-UTC history. */
368         private final SortedMap<Integer, Double> ut1Utc;
369 
370         /** Current line number. */
371         private int lineNumber;
372 
373         /** Current line. */
374         private String line;
375 
376         /** Simple constructor.
377          */
378         Parser() {
379             this.taiUtc     = new TreeMap<>();
380             this.ut1Utc     = new TreeMap<>();
381             this.lineNumber = 0;
382         }
383 
384         /** Get TAI-UTC history.
385          * @return TAI-UTC history
386          */
387         public SortedMap<Integer, Integer> getTaiUtc() {
388             return taiUtc;
389         }
390 
391         /** Get UT1-UTC history.
392          * @return UT1-UTC history
393          */
394         public SortedMap<Integer, Double> getUt1Utc() {
395             return ut1Utc;
396         }
397 
398         /** {@inheritDoc} */
399         @Override
400         public boolean stillAcceptsData() {
401             return true;
402         }
403 
404         /** {@inheritDoc} */
405         @Override
406         public void loadData(final InputStream input, final String name)
407             throws IOException {
408 
409             final List<Section> remaining = new ArrayList<>(Arrays.asList(Section.values()));
410             // set up a reader for line-oriented bulletin A files
411             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
412 
413                 // loop over sections
414                 for (Section section = nextSection(remaining, reader);
415                      section != null;
416                      section = nextSection(remaining, reader)) {
417 
418                     if (section == Section.TAI_UTC) {
419                         loadTaiUtc(section, reader, name);
420                     } else {
421                         // load the values
422                         loadTimeSteps(section, reader, name);
423                     }
424 
425                     // remove the already parsed section from the list
426                     remaining.remove(section);
427 
428                 }
429 
430             }
431             lineNumber =  0;
432 
433             // check that the mandatory sections have been parsed
434             if (remaining.contains(Section.EOP_RAPID_SERVICE) || remaining.contains(Section.EOP_PREDICTION)) {
435                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
436             }
437 
438         }
439 
440         /** Skip to next section header.
441          * @param sections sections to check for
442          * @param reader reader from where file content is obtained
443          * @return the next section or null if no section is found until end of file
444          * @exception IOException if data can't be read
445          */
446         private Section nextSection(final List<Section> sections, final BufferedReader reader)
447             throws IOException {
448 
449             for (line = reader.readLine(); line != null; line = reader.readLine()) {
450                 ++lineNumber;
451                 for (Section section : sections) {
452                     if (section.matchesHeader(line)) {
453                         return section;
454                     }
455                 }
456             }
457 
458             // we have reached end of file and not found a matching section header
459             return null;
460 
461         }
462 
463         /** Read TAI-UTC.
464          * @param section section to parse
465          * @param reader reader from where file content is obtained
466          * @param name name of the file (or zip entry)
467          * @exception IOException if data can't be read
468          */
469         private void loadTaiUtc(final Section section, final BufferedReader reader, final String name)
470             throws IOException {
471 
472             for (line = reader.readLine(); line != null; line = reader.readLine()) {
473                 lineNumber++;
474                 final String[] fields = section.getFields(line);
475                 if (fields != null) {
476                     // we have found the single line we are looking for
477                     final int mjd    = Integer.parseInt(fields[0]);
478                     final int offset = Integer.parseInt(fields[1]);
479                     taiUtc.put(mjd, offset);
480                     return;
481                 }
482             }
483 
484             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
485                                       name, lineNumber);
486 
487         }
488 
489         /** Read UT1-UTC.
490          * @param section section to parse
491          * @param reader reader from where file content is obtained
492          * @param name name of the file (or zip entry)
493          * @exception IOException if data can't be read
494          */
495         private void loadTimeSteps(final Section section, final BufferedReader reader, final String name)
496             throws IOException {
497 
498             boolean inValuesPart = false;
499             for (line = reader.readLine(); line != null; line = reader.readLine()) {
500                 lineNumber++;
501                 final String[] fields = section.getFields(line);
502                 if (fields != null) {
503 
504                     // we are within the values part
505                     inValuesPart = true;
506 
507                     // this is a data line, build an entry from the extracted fields
508                     final int year  = Integer.parseInt(fields[0]);
509                     final int month = Integer.parseInt(fields[1]);
510                     final int day   = Integer.parseInt(fields[2]);
511                     final int mjd   = Integer.parseInt(fields[3]);
512                     final DateComponents dc = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
513                     if ((dc.getYear() % 100) != (year % 100) ||
514                             dc.getMonth() != month ||
515                             dc.getDay() != day) {
516                         throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
517                                                   name, year, month, day, mjd);
518                     }
519 
520                     final double offset = Double.parseDouble(fields[4]);
521                     ut1Utc.put(mjd, offset);
522 
523                 } else if (inValuesPart) {
524                     // we leave values part
525                     return;
526                 }
527             }
528 
529             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
530                                       name, lineNumber);
531 
532         }
533 
534     }
535 
536 }