UTCTAIBulletinAFilesLoader.java

  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. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.SortedMap;
  28. import java.util.TreeMap;
  29. import java.util.regex.Matcher;
  30. import java.util.regex.Pattern;

  31. import org.hipparchus.util.FastMath;
  32. import org.orekit.annotation.DefaultDataContext;
  33. import org.orekit.data.AbstractSelfFeedingLoader;
  34. import org.orekit.data.DataContext;
  35. import org.orekit.data.DataLoader;
  36. import org.orekit.data.DataProvidersManager;
  37. import org.orekit.errors.OrekitException;
  38. import org.orekit.errors.OrekitMessages;

  39. /** Loader for UTC-TAI extracted from bulletin A files.
  40.  * <p>This class is a modified version of {@code BulletinAFileLoader}
  41.  * that only parses the TAI-UTC header line and checks the UT1-UTC column
  42.  * for discontinuities.
  43.  * </p>
  44.  * <p>
  45.  * Note that extracting UTC-TAI from bulletin A files is <em>NOT</em>
  46.  * recommended. There are known issues in some past bulletin A
  47.  * (for example bulletina-xix-001.txt from 2006-01-05 has a wrong year
  48.  * for last leap second and bulletina-xxi-053.txt from 2008-12-31 has an
  49.  * off by one value for TAI-UTC on MJD 54832). This is a known problem,
  50.  * and the Earth Orientation Department at USNO told us this TAI-UTC
  51.  * data was only provided as a convenience and this data should rather
  52.  * be sourced from other official files. As the bulletin A files are
  53.  * a record of past publications, they cannot modify archived bulletins,
  54.  * hence the errors above will remain forever. This UTC-TAI loader should
  55.  * therefore be used with great care.
  56.  * </p>
  57.  * <p>
  58.  * This class is immutable and hence thread-safe
  59.  * </p>
  60.  * @author Luc Maisonobe
  61.  * @since 7.1
  62.  */
  63. public class UTCTAIBulletinAFilesLoader extends AbstractSelfFeedingLoader
  64.         implements UTCTAIOffsetsLoader {

  65.     /**
  66.      * Build a loader for IERS bulletins A files. This constructor uses the {@link
  67.      * DataContext#getDefault() default data context}.
  68.      *
  69.      * @param supportedNames regular expression for supported files names
  70.      */
  71.     @DefaultDataContext
  72.     public UTCTAIBulletinAFilesLoader(final String supportedNames) {
  73.         this(supportedNames, DataContext.getDefault().getDataProvidersManager());
  74.     }

  75.     /**
  76.      * Build a loader for IERS bulletins A files.
  77.      *
  78.      * @param supportedNames regular expression for supported files names
  79.      * @param manager        provides access to the bulletin A files.
  80.      */
  81.     public UTCTAIBulletinAFilesLoader(final String supportedNames,
  82.                                       final DataProvidersManager manager) {
  83.         super(supportedNames, manager);
  84.     }

  85.     /** {@inheritDoc} */
  86.     @Override
  87.     public List<OffsetModel> loadOffsets() {

  88.         final Parser parser = new Parser();
  89.         this.feed(parser);
  90.         final SortedMap<Integer, Integer> taiUtc = parser.getTaiUtc();
  91.         final SortedMap<Integer, Double>  ut1Utc = parser.getUt1Utc();

  92.         // identify UT1-UTC discontinuities
  93.         final List<Integer> leapDays = new ArrayList<>();
  94.         Map.Entry<Integer, Double> previous = null;
  95.         for (final Map.Entry<Integer, Double> entry : ut1Utc.entrySet()) {
  96.             if (previous != null) {
  97.                 final double delta = entry.getValue() - previous.getValue();
  98.                 if (FastMath.abs(delta) > 0.5) {
  99.                     // discontinuity found between previous and current entry, a leap second has occurred
  100.                     leapDays.add(entry.getKey());
  101.                 }
  102.             }
  103.             previous = entry;
  104.         }

  105.         final List<OffsetModel> offsets = new ArrayList<>();

  106.         if (!taiUtc.isEmpty()) {

  107.             // find the start offset, before the first UT1-UTC entry
  108.             final Map.Entry<Integer, Integer> firstTaiMUtc = taiUtc.entrySet().iterator().next();
  109.             int offset = firstTaiMUtc.getValue();
  110.             final int refMJD = firstTaiMUtc.getKey();
  111.             for (final int leapMJD : leapDays) {
  112.                 if (leapMJD > refMJD) {
  113.                     break;
  114.                 }
  115.                 --offset;
  116.             }

  117.             // set all known time steps
  118.             for (final int leapMJD : leapDays) {
  119.                 offsets.add(new OffsetModel(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, leapMJD),
  120.                                             ++offset));
  121.             }

  122.             // check for missing time steps
  123.             for (final Map.Entry<Integer, Integer> refTaiMUtc : taiUtc.entrySet()) {
  124.                 final DateComponents refDC = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH,
  125.                                                                 refTaiMUtc.getKey() + 1);
  126.                 OffsetModel before = null;
  127.                 for (final OffsetModel o : offsets) {
  128.                     if (o.getStart().compareTo(refDC) < 0) {
  129.                         before = o;
  130.                     }
  131.                 }
  132.                 if (before != null) {
  133.                     if (refTaiMUtc.getValue() != (int) FastMath.rint(before.getOffset().toDouble())) {
  134.                         throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES,
  135.                                                   before.getStart(), refDC);
  136.                     }
  137.                 }
  138.             }

  139.             // make sure we stop the linear drift that was used before 1972
  140.             final DateComponents dc1972 = new DateComponents(1972, 1, 1);
  141.             if (offsets.isEmpty()) {
  142.                 offsets.add(0, new OffsetModel(dc1972, taiUtc.get(taiUtc.firstKey())));
  143.             } else {
  144.                 if (offsets.get(0).getStart().getYear() > 1972) {
  145.                     offsets.add(0,
  146.                                 new OffsetModel(dc1972,
  147.                                                 dc1972.getMJD(),
  148.                                                 offsets.get(0).getOffset().subtract(TimeOffset.SECOND),
  149.                                                 0));
  150.                 }
  151.             }

  152.         }

  153.         return offsets;

  154.     }

  155.     /** Internal class performing the parsing. */
  156.     private static class Parser implements DataLoader {

  157.         /** Regular expression matching blanks at start of line. */
  158.         private static final String LINE_START_REGEXP     = "^\\p{Blank}+";

  159.         /** Regular expression matching blanks at end of line. */
  160.         private static final String LINE_END_REGEXP       = "\\p{Blank}*$";

  161.         /** Regular expression matching integers. */
  162.         private static final String INTEGER_REGEXP        = "[-+]?\\p{Digit}+";

  163.         /** Regular expression matching real numbers. */
  164.         private static final String REAL_REGEXP           = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";

  165.         /** Regular expression matching an integer field to store. */
  166.         private static final String STORED_INTEGER_FIELD  = "\\p{Blank}*(" + INTEGER_REGEXP + ")";

  167.         /** regular expression matching a Modified Julian Day field to store. */
  168.         private static final String STORED_MJD_FIELD      = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";

  169.         /** Regular expression matching a real field to store. */
  170.         private static final String STORED_REAL_FIELD     = "\\p{Blank}+(" + REAL_REGEXP + ")";

  171.         /** Regular expression matching a real field to ignore. */
  172.         private static final String IGNORED_REAL_FIELD    = "\\p{Blank}+" + REAL_REGEXP;

  173.         /** Enum for files sections, in expected order.
  174.          * <p>The bulletin A weekly data files contain several sections,
  175.          * each introduced with some fixed header text and followed by tabular data.
  176.          * </p>
  177.          */
  178.         private enum Section {

  179.             /** Earth Orientation Parameters rapid service. */
  180.             // section 2 always contain rapid service data including error fields
  181.             //      COMBINED EARTH ORIENTATION PARAMETERS:
  182.             //
  183.             //                              IERS Rapid Service
  184.             //              MJD      x    error     y    error   UT1-UTC   error
  185.             //                       "      "       "      "        s        s
  186.             //   13  8 30  56534 0.16762 .00009 0.32705 .00009  0.038697 0.000019
  187.             //   13  8 31  56535 0.16669 .00010 0.32564 .00010  0.038471 0.000019
  188.             //   13  9  1  56536 0.16592 .00009 0.32410 .00010  0.038206 0.000024
  189.             //   13  9  2  56537 0.16557 .00009 0.32270 .00009  0.037834 0.000024
  190.             //   13  9  3  56538 0.16532 .00009 0.32147 .00010  0.037351 0.000024
  191.             //   13  9  4  56539 0.16488 .00009 0.32044 .00010  0.036756 0.000023
  192.             //   13  9  5  56540 0.16435 .00009 0.31948 .00009  0.036036 0.000024
  193.             EOP_RAPID_SERVICE("^ *COMBINED EARTH ORIENTATION PARAMETERS: *$",
  194.                               LINE_START_REGEXP +
  195.                               STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
  196.                               STORED_MJD_FIELD +
  197.                               IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
  198.                               IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
  199.                               STORED_REAL_FIELD  + IGNORED_REAL_FIELD +
  200.                               LINE_END_REGEXP),

  201.             /** Earth Orientation Parameters final values. */
  202.             // the first bulletin A of each month also includes final values for the
  203.             // period covering from day 2 of month m-2 to day 1 of month m-1.
  204.             //                                IERS Final Values
  205.             //                                 MJD        x        y      UT1-UTC
  206.             //                                            "        "         s
  207.             //             13  7  2           56475    0.1441   0.3901   0.05717
  208.             //             13  7  3           56476    0.1457   0.3895   0.05716
  209.             //             13  7  4           56477    0.1467   0.3887   0.05728
  210.             //             13  7  5           56478    0.1477   0.3875   0.05755
  211.             //             13  7  6           56479    0.1490   0.3862   0.05793
  212.             //             13  7  7           56480    0.1504   0.3849   0.05832
  213.             //             13  7  8           56481    0.1516   0.3835   0.05858
  214.             //             13  7  9           56482    0.1530   0.3822   0.05877
  215.             EOP_FINAL_VALUES("^ *IERS Final Values *$",
  216.                              LINE_START_REGEXP +
  217.                              STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
  218.                              STORED_MJD_FIELD +
  219.                              IGNORED_REAL_FIELD +
  220.                              IGNORED_REAL_FIELD +
  221.                              STORED_REAL_FIELD +
  222.                              LINE_END_REGEXP),

  223.             /** TAI-UTC part of the Earth Orientation Parameters prediction.. */
  224.             // section 3 always contain prediction data without error fields
  225.             //
  226.             //         PREDICTIONS:
  227.             //         The following formulas will not reproduce the predictions given below,
  228.             //         but may be used to extend the predictions beyond the end of this table.
  229.             //
  230.             //         x =  0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
  231.             //         y =  0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
  232.             //            UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
  233.             //
  234.             //         where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
  235.             //
  236.             //            TAI-UTC(MJD 56541) = 35.0
  237.             //         The accuracy may be estimated from the expressions:
  238.             //         S x,y = 0.00068 (MJD-56540)**0.80   S t = 0.00025 (MJD-56540)**0.75
  239.             //         Estimated accuracies are:  Predictions     10 d   20 d   30 d   40 d
  240.             //                                    Polar coord's  0.004  0.007  0.010  0.013
  241.             //                                    UT1-UTC        0.0014 0.0024 0.0032 0.0040
  242.             //
  243.             //                       MJD      x(arcsec)   y(arcsec)   UT1-UTC(sec)
  244.             //          2013  9  6  56541       0.1638      0.3185      0.03517
  245.             //          2013  9  7  56542       0.1633      0.3175      0.03420
  246.             //          2013  9  8  56543       0.1628      0.3164      0.03322
  247.             //          2013  9  9  56544       0.1623      0.3153      0.03229
  248.             //          2013  9 10  56545       0.1618      0.3142      0.03144
  249.             //          2013  9 11  56546       0.1612      0.3131      0.03071
  250.             //          2013  9 12  56547       0.1607      0.3119      0.03008
  251.             TAI_UTC("^ *PREDICTIONS: *$",
  252.                     LINE_START_REGEXP +
  253.                     "TAI-UTC\\(MJD *" +
  254.                     STORED_MJD_FIELD +
  255.                     "\\) *= *" +
  256.                     STORED_INTEGER_FIELD + "(?:\\.0*)?" +
  257.                     LINE_END_REGEXP),

  258.             /** Earth Orientation Parameters prediction. */
  259.             // section 3 always contain prediction data without error fields
  260.             //
  261.             //         PREDICTIONS:
  262.             //         The following formulas will not reproduce the predictions given below,
  263.             //         but may be used to extend the predictions beyond the end of this table.
  264.             //
  265.             //         x =  0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
  266.             //         y =  0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
  267.             //            UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
  268.             //
  269.             //         where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
  270.             //
  271.             //            TAI-UTC(MJD 56541) = 35.0
  272.             //         The accuracy may be estimated from the expressions:
  273.             //         S x,y = 0.00068 (MJD-56540)**0.80   S t = 0.00025 (MJD-56540)**0.75
  274.             //         Estimated accuracies are:  Predictions     10 d   20 d   30 d   40 d
  275.             //                                    Polar coord's  0.004  0.007  0.010  0.013
  276.             //                                    UT1-UTC        0.0014 0.0024 0.0032 0.0040
  277.             //
  278.             //                       MJD      x(arcsec)   y(arcsec)   UT1-UTC(sec)
  279.             //          2013  9  6  56541       0.1638      0.3185      0.03517
  280.             //          2013  9  7  56542       0.1633      0.3175      0.03420
  281.             //          2013  9  8  56543       0.1628      0.3164      0.03322
  282.             //          2013  9  9  56544       0.1623      0.3153      0.03229
  283.             //          2013  9 10  56545       0.1618      0.3142      0.03144
  284.             //          2013  9 11  56546       0.1612      0.3131      0.03071
  285.             //          2013  9 12  56547       0.1607      0.3119      0.03008
  286.             EOP_PREDICTION("^ *MJD *x\\(arcsec\\) *y\\(arcsec\\) *UT1-UTC\\(sec\\) *$",
  287.                            LINE_START_REGEXP +
  288.                            STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
  289.                            STORED_MJD_FIELD +
  290.                            IGNORED_REAL_FIELD +
  291.                            IGNORED_REAL_FIELD +
  292.                            STORED_REAL_FIELD +
  293.                            LINE_END_REGEXP);

  294.             /** Header pattern. */
  295.             private final Pattern header;

  296.             /** Data pattern. */
  297.             private final Pattern data;

  298.             /** Simple constructor.
  299.              * @param headerRegExp regular expression for header
  300.              * @param dataRegExp regular expression for data
  301.              */
  302.             Section(final String headerRegExp, final String dataRegExp) {
  303.                 this.header = Pattern.compile(headerRegExp);
  304.                 this.data   = Pattern.compile(dataRegExp);
  305.             }

  306.             /** Check if a line matches the section header.
  307.              * @param l line to check
  308.              * @return true if the line matches the header
  309.              */
  310.             public boolean matchesHeader(final String l) {
  311.                 return header.matcher(l).matches();
  312.             }

  313.             /** Get the data fields from a line.
  314.              * @param l line to parse
  315.              * @return extracted fields, or null if line does not match data format
  316.              */
  317.             public String[] getFields(final String l) {
  318.                 final Matcher matcher = data.matcher(l);
  319.                 if (matcher.matches()) {
  320.                     final String[] fields = new String[matcher.groupCount()];
  321.                     for (int i = 0; i < fields.length; ++i) {
  322.                         fields[i] = matcher.group(i + 1);
  323.                     }
  324.                     return fields;
  325.                 } else {
  326.                     return null;
  327.                 }
  328.             }

  329.         }

  330.         /** TAI-UTC history. */
  331.         private final SortedMap<Integer, Integer> taiUtc;

  332.         /** UT1-UTC history. */
  333.         private final SortedMap<Integer, Double> ut1Utc;

  334.         /** Current line number. */
  335.         private int lineNumber;

  336.         /** Current line. */
  337.         private String line;

  338.         /** Simple constructor.
  339.          */
  340.         Parser() {
  341.             this.taiUtc     = new TreeMap<>();
  342.             this.ut1Utc     = new TreeMap<>();
  343.             this.lineNumber = 0;
  344.         }

  345.         /** Get TAI-UTC history.
  346.          * @return TAI-UTC history
  347.          */
  348.         public SortedMap<Integer, Integer> getTaiUtc() {
  349.             return taiUtc;
  350.         }

  351.         /** Get UT1-UTC history.
  352.          * @return UT1-UTC history
  353.          */
  354.         public SortedMap<Integer, Double> getUt1Utc() {
  355.             return ut1Utc;
  356.         }

  357.         /** {@inheritDoc} */
  358.         @Override
  359.         public boolean stillAcceptsData() {
  360.             return true;
  361.         }

  362.         /** {@inheritDoc} */
  363.         @Override
  364.         public void loadData(final InputStream input, final String name)
  365.             throws IOException {

  366.             final List<Section> remaining = new ArrayList<>(Arrays.asList(Section.values()));
  367.             // set up a reader for line-oriented bulletin A files
  368.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {

  369.                 // loop over sections
  370.                 for (Section section = nextSection(remaining, reader);
  371.                      section != null;
  372.                      section = nextSection(remaining, reader)) {

  373.                     if (section == Section.TAI_UTC) {
  374.                         loadTaiUtc(section, reader, name);
  375.                     } else {
  376.                         // load the values
  377.                         loadTimeSteps(section, reader, name);
  378.                     }

  379.                     // remove the already parsed section from the list
  380.                     remaining.remove(section);

  381.                 }

  382.             }
  383.             lineNumber =  0;

  384.             // check that the mandatory sections have been parsed
  385.             if (remaining.contains(Section.EOP_RAPID_SERVICE) || remaining.contains(Section.EOP_PREDICTION)) {
  386.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  387.             }

  388.         }

  389.         /** Skip to next section header.
  390.          * @param sections sections to check for
  391.          * @param reader reader from where file content is obtained
  392.          * @return the next section or null if no section is found until end of file
  393.          * @exception IOException if data can't be read
  394.          */
  395.         private Section nextSection(final List<Section> sections, final BufferedReader reader)
  396.             throws IOException {

  397.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  398.                 ++lineNumber;
  399.                 for (Section section : sections) {
  400.                     if (section.matchesHeader(line)) {
  401.                         return section;
  402.                     }
  403.                 }
  404.             }

  405.             // we have reached end of file and not found a matching section header
  406.             return null;

  407.         }

  408.         /** Read TAI-UTC.
  409.          * @param section section to parse
  410.          * @param reader reader from where file content is obtained
  411.          * @param name name of the file (or zip entry)
  412.          * @exception IOException if data can't be read
  413.          */
  414.         private void loadTaiUtc(final Section section, final BufferedReader reader, final String name)
  415.             throws IOException {

  416.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  417.                 lineNumber++;
  418.                 final String[] fields = section.getFields(line);
  419.                 if (fields != null) {
  420.                     // we have found the single line we are looking for
  421.                     final int mjd    = Integer.parseInt(fields[0]);
  422.                     final int offset = Integer.parseInt(fields[1]);
  423.                     taiUtc.put(mjd, offset);
  424.                     return;
  425.                 }
  426.             }

  427.             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
  428.                                       name, lineNumber);

  429.         }

  430.         /** Read UT1-UTC.
  431.          * @param section section to parse
  432.          * @param reader reader from where file content is obtained
  433.          * @param name name of the file (or zip entry)
  434.          * @exception IOException if data can't be read
  435.          */
  436.         private void loadTimeSteps(final Section section, final BufferedReader reader, final String name)
  437.             throws IOException {

  438.             boolean inValuesPart = false;
  439.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  440.                 lineNumber++;
  441.                 final String[] fields = section.getFields(line);
  442.                 if (fields != null) {

  443.                     // we are within the values part
  444.                     inValuesPart = true;

  445.                     // this is a data line, build an entry from the extracted fields
  446.                     final int year  = Integer.parseInt(fields[0]);
  447.                     final int month = Integer.parseInt(fields[1]);
  448.                     final int day   = Integer.parseInt(fields[2]);
  449.                     final int mjd   = Integer.parseInt(fields[3]);
  450.                     final DateComponents dc = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
  451.                     if ((dc.getYear() % 100) != (year % 100) ||
  452.                             dc.getMonth() != month ||
  453.                             dc.getDay() != day) {
  454.                         throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
  455.                                                   name, year, month, day, mjd);
  456.                     }

  457.                     final double offset = Double.parseDouble(fields[4]);
  458.                     ut1Utc.put(mjd, offset);

  459.                 } else if (inValuesPart) {
  460.                     // we leave values part
  461.                     return;
  462.                 }
  463.             }

  464.             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
  465.                                       name, lineNumber);

  466.         }

  467.     }

  468. }