UTCTAIBulletinAFilesLoader.java

  1. /* Copyright 2002-2018 CS Systèmes d'Information
  2.  * Licensed to CS Systèmes d'Information (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.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.SortedMap;
  27. import java.util.TreeMap;
  28. import java.util.regex.Matcher;
  29. import java.util.regex.Pattern;

  30. import org.hipparchus.util.FastMath;
  31. import org.orekit.data.DataLoader;
  32. import org.orekit.data.DataProvidersManager;
  33. import org.orekit.errors.OrekitException;
  34. import org.orekit.errors.OrekitMessages;

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

  60.     /** Regular expression for supported files names. */
  61.     private final String supportedNames;

  62.     /** Build a loader for IERS bulletins A files.
  63.     * @param supportedNames regular expression for supported files names
  64.     */
  65.     public UTCTAIBulletinAFilesLoader(final String supportedNames) {
  66.         this.supportedNames = supportedNames;
  67.     }

  68.     /** {@inheritDoc} */
  69.     @Override
  70.     public List<OffsetModel> loadOffsets() throws OrekitException {

  71.         final Parser parser = new Parser();
  72.         DataProvidersManager.getInstance().feed(supportedNames, parser);
  73.         final SortedMap<Integer, Integer> taiUtc = parser.getTaiUtc();
  74.         final SortedMap<Integer, Double>  ut1Utc = parser.getUt1Utc();

  75.         // identify UT1-UTC discontinuities
  76.         final List<Integer> leapDays = new ArrayList<Integer>();
  77.         Map.Entry<Integer, Double> previous = null;
  78.         for (final Map.Entry<Integer, Double> entry : ut1Utc.entrySet()) {
  79.             if (previous != null) {
  80.                 final double delta = entry.getValue() - previous.getValue();
  81.                 if (FastMath.abs(delta) > 0.5) {
  82.                     // discontinuity found between previous and current entry, a leap second has occurred
  83.                     leapDays.add(entry.getKey());
  84.                 }
  85.             }
  86.             previous = entry;
  87.         }

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

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

  90.             // find the start offset, before the first UT1-UTC entry
  91.             final Map.Entry<Integer, Integer> firstTaiMUtc = taiUtc.entrySet().iterator().next();
  92.             int offset = firstTaiMUtc.getValue();
  93.             final int refMJD = firstTaiMUtc.getKey();
  94.             for (final int leapMJD : leapDays) {
  95.                 if (leapMJD > refMJD) {
  96.                     break;
  97.                 }
  98.                 --offset;
  99.             }

  100.             // set all known time steps
  101.             for (final int leapMJD : leapDays) {
  102.                 offsets.add(new OffsetModel(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, leapMJD),
  103.                                             ++offset));
  104.             }

  105.             // check for missing time steps
  106.             for (final Map.Entry<Integer, Integer> refTaiMUtc : taiUtc.entrySet()) {
  107.                 final DateComponents refDC = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH,
  108.                                                                 refTaiMUtc.getKey() + 1);
  109.                 OffsetModel before = null;
  110.                 for (final OffsetModel o : offsets) {
  111.                     if (o.getStart().compareTo(refDC) < 0) {
  112.                         before = o;
  113.                     }
  114.                 }
  115.                 if (before != null) {
  116.                     if (refTaiMUtc.getValue() != (int) FastMath.rint(before.getOffset())) {
  117.                         throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES,
  118.                                                   before.getStart(), refDC);
  119.                     }
  120.                 }
  121.             }

  122.             // make sure we stop the linear drift that was used before 1972
  123.             if (offsets.isEmpty()) {
  124.                 offsets.add(0, new OffsetModel(new DateComponents(1972, 1, 1), taiUtc.get(taiUtc.firstKey())));
  125.             } else {
  126.                 if (offsets.get(0).getStart().getYear() > 1972) {
  127.                     offsets.add(0, new OffsetModel(new DateComponents(1972, 1, 1),
  128.                                                 ((int) FastMath.rint(offsets.get(0).getOffset())) - 1));
  129.                 }
  130.             }

  131.         }

  132.         return offsets;

  133.     }

  134.     /** Internal class performing the parsing. */
  135.     private static class Parser implements DataLoader {

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

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

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

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

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

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

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

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

  152.         /** Enum for files sections, in expected order.
  153.          * <p>The bulletin A weekly data files contain several sections,
  154.          * each introduced with some fixed header text and followed by tabular data.
  155.          * </p>
  156.          */
  157.         private enum Section {

  158.             /** Earth Orientation Parameters rapid service. */
  159.             // section 2 always contain rapid service data including error fields
  160.             //      COMBINED EARTH ORIENTATION PARAMETERS:
  161.             //
  162.             //                              IERS Rapid Service
  163.             //              MJD      x    error     y    error   UT1-UTC   error
  164.             //                       "      "       "      "        s        s
  165.             //   13  8 30  56534 0.16762 .00009 0.32705 .00009  0.038697 0.000019
  166.             //   13  8 31  56535 0.16669 .00010 0.32564 .00010  0.038471 0.000019
  167.             //   13  9  1  56536 0.16592 .00009 0.32410 .00010  0.038206 0.000024
  168.             //   13  9  2  56537 0.16557 .00009 0.32270 .00009  0.037834 0.000024
  169.             //   13  9  3  56538 0.16532 .00009 0.32147 .00010  0.037351 0.000024
  170.             //   13  9  4  56539 0.16488 .00009 0.32044 .00010  0.036756 0.000023
  171.             //   13  9  5  56540 0.16435 .00009 0.31948 .00009  0.036036 0.000024
  172.             EOP_RAPID_SERVICE("^ *COMBINED EARTH ORIENTATION PARAMETERS: *$",
  173.                               LINE_START_REGEXP +
  174.                               STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
  175.                               STORED_MJD_FIELD +
  176.                               IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
  177.                               IGNORED_REAL_FIELD + IGNORED_REAL_FIELD +
  178.                               STORED_REAL_FIELD  + IGNORED_REAL_FIELD +
  179.                               LINE_END_REGEXP),

  180.            /** Earth Orientation Parameters final values. */
  181.            // the first bulletin A of each month also includes final values for the
  182.            // period covering from day 2 of month m-2 to day 1 of month m-1.
  183.            //                                IERS Final Values
  184.            //                                 MJD        x        y      UT1-UTC
  185.            //                                            "        "         s
  186.            //             13  7  2           56475    0.1441   0.3901   0.05717
  187.            //             13  7  3           56476    0.1457   0.3895   0.05716
  188.            //             13  7  4           56477    0.1467   0.3887   0.05728
  189.            //             13  7  5           56478    0.1477   0.3875   0.05755
  190.            //             13  7  6           56479    0.1490   0.3862   0.05793
  191.            //             13  7  7           56480    0.1504   0.3849   0.05832
  192.            //             13  7  8           56481    0.1516   0.3835   0.05858
  193.            //             13  7  9           56482    0.1530   0.3822   0.05877
  194.            EOP_FINAL_VALUES("^ *IERS Final Values *$",
  195.                             LINE_START_REGEXP +
  196.                             STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
  197.                             STORED_MJD_FIELD +
  198.                             IGNORED_REAL_FIELD +
  199.                             IGNORED_REAL_FIELD +
  200.                             STORED_REAL_FIELD +
  201.                             LINE_END_REGEXP),

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

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

  273.             /** Header pattern. */
  274.             private final Pattern header;

  275.             /** Data pattern. */
  276.             private final Pattern data;

  277.             /** Simple constructor.
  278.              * @param headerRegExp regular expression for header
  279.              * @param dataRegExp regular expression for data
  280.              */
  281.             Section(final String headerRegExp, final String dataRegExp) {
  282.                 this.header = Pattern.compile(headerRegExp);
  283.                 this.data   = Pattern.compile(dataRegExp);
  284.             }

  285.             /** Check if a line matches the section header.
  286.              * @param l line to check
  287.              * @return true if the line matches the header
  288.              */
  289.             public boolean matchesHeader(final String l) {
  290.                 return header.matcher(l).matches();
  291.             }

  292.             /** Get the data fields from a line.
  293.              * @param l line to parse
  294.              * @return extracted fields, or null if line does not match data format
  295.              */
  296.             public String[] getFields(final String l) {
  297.                 final Matcher matcher = data.matcher(l);
  298.                 if (matcher.matches()) {
  299.                     final String[] fields = new String[matcher.groupCount()];
  300.                     for (int i = 0; i < fields.length; ++i) {
  301.                         fields[i] = matcher.group(i + 1);
  302.                     }
  303.                     return fields;
  304.                 } else {
  305.                     return null;
  306.                 }
  307.             }

  308.         }

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

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

  313.         /** Current line number. */
  314.         private int lineNumber;

  315.         /** Current line. */
  316.         private String line;

  317.         /** Simple constructor.
  318.          */
  319.         Parser() {
  320.             this.taiUtc     = new TreeMap<Integer, Integer>();
  321.             this.ut1Utc     = new TreeMap<Integer, Double>();
  322.             this.lineNumber = 0;
  323.         }

  324.         /** Get TAI-UTC history.
  325.          * @return TAI-UTC history
  326.          */
  327.         public SortedMap<Integer, Integer> getTaiUtc() {
  328.             return taiUtc;
  329.         }

  330.         /** Get UT1-UTC history.
  331.          * @return UT1-UTC history
  332.          */
  333.         public SortedMap<Integer, Double> getUt1Utc() {
  334.             return ut1Utc;
  335.         }

  336.         /** {@inheritDoc} */
  337.         @Override
  338.         public boolean stillAcceptsData() {
  339.             return true;
  340.         }

  341.         /** {@inheritDoc} */
  342.         @Override
  343.         public void loadData(final InputStream input, final String name)
  344.             throws OrekitException, IOException {

  345.             // set up a reader for line-oriented bulletin A files
  346.             final BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));
  347.             lineNumber =  0;

  348.             // loop over sections
  349.             final List<Section> remaining = new ArrayList<Section>();
  350.             remaining.addAll(Arrays.asList(Section.values()));
  351.             for (Section section = nextSection(remaining, reader, name);
  352.                     section != null;
  353.                     section = nextSection(remaining, reader, name)) {

  354.                 if (section == Section.TAI_UTC) {
  355.                     loadTaiUtc(section, reader, name);
  356.                 } else {
  357.                     // load the values
  358.                     loadTimeSteps(section, reader, name);
  359.                 }

  360.                 // remove the already parsed section from the list
  361.                 remaining.remove(section);

  362.             }

  363.             // check that the mandatory sections have been parsed
  364.             if (remaining.contains(Section.EOP_RAPID_SERVICE) || remaining.contains(Section.EOP_PREDICTION)) {
  365.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  366.             }

  367.         }

  368.         /** Skip to next section header.
  369.          * @param sections sections to check for
  370.          * @param reader reader from where file content is obtained
  371.          * @param name name of the file (or zip entry)
  372.          * @return the next section or null if no section is found until end of file
  373.          * @exception IOException if data can't be read
  374.          */
  375.         private Section nextSection(final List<Section> sections, final BufferedReader reader, final String name)
  376.             throws IOException {

  377.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  378.                 ++lineNumber;
  379.                 for (Section section : sections) {
  380.                     if (section.matchesHeader(line)) {
  381.                         return section;
  382.                     }
  383.                 }
  384.             }

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

  387.         }

  388.         /** Read TAI-UTC.
  389.          * @param section section to parse
  390.          * @param reader reader from where file content is obtained
  391.          * @param name name of the file (or zip entry)
  392.          * @exception IOException if data can't be read
  393.          * @exception OrekitException if some data is missing or if some loader specific error occurs
  394.          */
  395.         private void loadTaiUtc(final Section section, final BufferedReader reader, final String name)
  396.             throws OrekitException, IOException {

  397.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  398.                 lineNumber++;
  399.                 final String[] fields = section.getFields(line);
  400.                 if (fields != null) {
  401.                     // we have found the single line we are looking for
  402.                     final int mjd    = Integer.parseInt(fields[0]);
  403.                     final int offset = Integer.parseInt(fields[1]);
  404.                     taiUtc.put(mjd, offset);
  405.                     return;
  406.                 }
  407.             }

  408.             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
  409.                                       name, lineNumber);

  410.         }

  411.         /** Read UT1-UTC.
  412.          * @param section section to parse
  413.          * @param reader reader from where file content is obtained
  414.          * @param name name of the file (or zip entry)
  415.          * @exception IOException if data can't be read
  416.          * @exception OrekitException if some data is missing or if some loader specific error occurs
  417.          */
  418.         private void loadTimeSteps(final Section section, final BufferedReader reader, final String name)
  419.             throws OrekitException, IOException {

  420.             boolean inValuesPart = false;
  421.             for (line = reader.readLine(); line != null; line = reader.readLine()) {
  422.                 lineNumber++;
  423.                 final String[] fields = section.getFields(line);
  424.                 if (fields != null) {

  425.                     // we are within the values part
  426.                     inValuesPart = true;

  427.                     // this is a data line, build an entry from the extracted fields
  428.                     final int year  = Integer.parseInt(fields[0]);
  429.                     final int month = Integer.parseInt(fields[1]);
  430.                     final int day   = Integer.parseInt(fields[2]);
  431.                     final int mjd   = Integer.parseInt(fields[3]);
  432.                     final DateComponents dc = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
  433.                     if ((dc.getYear() % 100) != (year % 100) ||
  434.                             dc.getMonth() != month ||
  435.                             dc.getDay() != day) {
  436.                         throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
  437.                                                   name, year, month, day, mjd);
  438.                     }

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

  441.                 } else if (inValuesPart) {
  442.                     // we leave values part
  443.                     return;
  444.                 }
  445.             }

  446.             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
  447.                                       name, lineNumber);

  448.         }

  449.     }

  450. }