UTCTAIBulletinAFilesLoader.java

  1. /* Copyright 2002-2023 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())) {
  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.             if (offsets.isEmpty()) {
  141.                 offsets.add(0, new OffsetModel(new DateComponents(1972, 1, 1), taiUtc.get(taiUtc.firstKey())));
  142.             } else {
  143.                 if (offsets.get(0).getStart().getYear() > 1972) {
  144.                     offsets.add(0, new OffsetModel(new DateComponents(1972, 1, 1),
  145.                                                 ((int) FastMath.rint(offsets.get(0).getOffset())) - 1));
  146.                 }
  147.             }

  148.         }

  149.         return offsets;

  150.     }

  151.     /** Internal class performing the parsing. */
  152.     private static class Parser implements DataLoader {

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

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

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

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

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

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

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

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

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

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

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

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

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

  290.             /** Header pattern. */
  291.             private final Pattern header;

  292.             /** Data pattern. */
  293.             private final Pattern data;

  294.             /** Simple constructor.
  295.              * @param headerRegExp regular expression for header
  296.              * @param dataRegExp regular expression for data
  297.              */
  298.             Section(final String headerRegExp, final String dataRegExp) {
  299.                 this.header = Pattern.compile(headerRegExp);
  300.                 this.data   = Pattern.compile(dataRegExp);
  301.             }

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

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

  325.         }

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

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

  330.         /** Current line number. */
  331.         private int lineNumber;

  332.         /** Current line. */
  333.         private String line;

  334.         /** Simple constructor.
  335.          */
  336.         Parser() {
  337.             this.taiUtc     = new TreeMap<>();
  338.             this.ut1Utc     = new TreeMap<>();
  339.             this.lineNumber = 0;
  340.         }

  341.         /** Get TAI-UTC history.
  342.          * @return TAI-UTC history
  343.          */
  344.         public SortedMap<Integer, Integer> getTaiUtc() {
  345.             return taiUtc;
  346.         }

  347.         /** Get UT1-UTC history.
  348.          * @return UT1-UTC history
  349.          */
  350.         public SortedMap<Integer, Double> getUt1Utc() {
  351.             return ut1Utc;
  352.         }

  353.         /** {@inheritDoc} */
  354.         @Override
  355.         public boolean stillAcceptsData() {
  356.             return true;
  357.         }

  358.         /** {@inheritDoc} */
  359.         @Override
  360.         public void loadData(final InputStream input, final String name)
  361.             throws IOException {

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

  365.                 // loop over sections
  366.                 for (Section section = nextSection(remaining, reader);
  367.                      section != null;
  368.                      section = nextSection(remaining, reader)) {

  369.                     if (section == Section.TAI_UTC) {
  370.                         loadTaiUtc(section, reader, name);
  371.                     } else {
  372.                         // load the values
  373.                         loadTimeSteps(section, reader, name);
  374.                     }

  375.                     // remove the already parsed section from the list
  376.                     remaining.remove(section);

  377.                 }

  378.             }
  379.             lineNumber =  0;

  380.             // check that the mandatory sections have been parsed
  381.             if (remaining.contains(Section.EOP_RAPID_SERVICE) || remaining.contains(Section.EOP_PREDICTION)) {
  382.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  383.             }

  384.         }

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

  393.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  394.                 ++lineNumber;
  395.                 for (Section section : sections) {
  396.                     if (section.matchesHeader(line)) {
  397.                         return section;
  398.                     }
  399.                 }
  400.             }

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

  403.         }

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

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

  423.             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
  424.                                       name, lineNumber);

  425.         }

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

  434.             boolean inValuesPart = false;
  435.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  436.                 lineNumber++;
  437.                 final String[] fields = section.getFields(line);
  438.                 if (fields != null) {

  439.                     // we are within the values part
  440.                     inValuesPart = true;

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

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

  455.                 } else if (inValuesPart) {
  456.                     // we leave values part
  457.                     return;
  458.                 }
  459.             }

  460.             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
  461.                                       name, lineNumber);

  462.         }

  463.     }

  464. }