EopXmlLoader.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.frames;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.InputStreamReader;
  21. import java.nio.charset.StandardCharsets;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.List;
  25. import java.util.SortedSet;
  26. import java.util.function.Supplier;

  27. import javax.xml.parsers.ParserConfigurationException;
  28. import javax.xml.parsers.SAXParser;
  29. import javax.xml.parsers.SAXParserFactory;

  30. import org.hipparchus.exception.LocalizedCoreFormats;
  31. import org.orekit.data.DataProvidersManager;
  32. import org.orekit.errors.OrekitException;
  33. import org.orekit.errors.OrekitMessages;
  34. import org.orekit.time.AbsoluteDate;
  35. import org.orekit.time.DateComponents;
  36. import org.orekit.time.TimeScale;
  37. import org.orekit.utils.IERSConventions;
  38. import org.orekit.utils.units.Unit;
  39. import org.xml.sax.Attributes;
  40. import org.xml.sax.InputSource;
  41. import org.xml.sax.SAXException;
  42. import org.xml.sax.helpers.DefaultHandler;

  43. /** Loader for IERS EOP data in XML format (finals and EOPC04 files).
  44.  * <p>The XML EOP files are recognized thanks to their base names, which
  45.  * must match one of the the patterns {@code finals.2000A.*.xml} or
  46.  * {@code finals.*.xml} or {@code eopc04_*.xml} (or the same ending with
  47.  * {@.gz} for gzip-compressed files) where * stands for any string of characters.</p>
  48.  * <p>Files containing data (back to 1962) are available at IERS web site: <a
  49.  * href="https://datacenter.iers.org/products/eop/">IERS https data download</a>.</p>
  50.  * <p>
  51.  * This class is immutable and hence thread-safe
  52.  * </p>
  53.  * @author Luc Maisonobe
  54.  */
  55. class EopXmlLoader extends AbstractEopLoader implements EopHistoryLoader {

  56.     /** Millisecond unit. */
  57.     private static final Unit MILLI_SECOND = Unit.parse("ms");

  58.     /** Milli arcsecond unit. */
  59.     private static final Unit MILLI_ARC_SECOND = Unit.parse("mas");

  60.     /**Arcsecond per day unit.
  61.      * @since 12.0
  62.      */
  63.     private static final Unit ARC_SECOND_PER_DAY = Unit.parse("as/day");

  64.     /**
  65.      * Build a loader for IERS XML EOP files.
  66.      *
  67.      * @param supportedNames regular expression for supported files names
  68.      * @param manager        provides access to the XML EOP files.
  69.      * @param utcSupplier    UTC time scale.
  70.      */
  71.     EopXmlLoader(final String supportedNames,
  72.                  final DataProvidersManager manager,
  73.                  final Supplier<TimeScale> utcSupplier) {
  74.         super(supportedNames, manager, utcSupplier);
  75.     }

  76.     /** {@inheritDoc} */
  77.     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
  78.                             final SortedSet<EOPEntry> history) {
  79.         final ItrfVersionProvider itrfVersionProvider = new ITRFVersionLoader(
  80.                 ITRFVersionLoader.SUPPORTED_NAMES,
  81.                 getDataProvidersManager());
  82.         final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
  83.         final EopParserLoader loader = new EopParserLoader(parser);
  84.         this.feed(loader);
  85.         history.addAll(loader.getEop());
  86.     }

  87.     /** Internal class performing the parsing. */
  88.     static class Parser extends AbstractEopParser {

  89.         /** History entries. */
  90.         private List<EOPEntry> history;

  91.         /**
  92.          * Simple constructor.
  93.          *
  94.          * @param converter           converter to use
  95.          * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
  96.          * @param utc                 time scale for parsing dates.
  97.          */
  98.         Parser(final IERSConventions.NutationCorrectionConverter converter,
  99.                final ItrfVersionProvider itrfVersionProvider,
  100.                final TimeScale utc) {
  101.             super(converter, itrfVersionProvider, utc);
  102.         }

  103.         /** {@inheritDoc} */
  104.         @Override
  105.         public Collection<EOPEntry> parse(final InputStream input, final String name)
  106.             throws IOException, OrekitException {
  107.             try {
  108.                 this.history = new ArrayList<>();
  109.                 // set up a parser for line-oriented bulletin B files
  110.                 final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();

  111.                 // read all file, ignoring header
  112.                 parser.parse(new InputSource(new InputStreamReader(input, StandardCharsets.UTF_8)),
  113.                              new EOPContentHandler(name));

  114.                 return history;

  115.             } catch (SAXException | ParserConfigurationException e) {
  116.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE, e.getMessage());
  117.             }
  118.         }

  119.         /** Local content handler for XML EOP files. */
  120.         private class EOPContentHandler extends DefaultHandler {

  121.             // CHECKSTYLE: stop JavadocVariable check

  122.             // elements and attributes used in both daily and finals data files
  123.             private static final String MJD_ELT           = "MJD";
  124.             private static final String LOD_ELT           = "LOD";
  125.             private static final String X_ELT             = "X";
  126.             private static final String Y_ELT             = "Y";
  127.             private static final String X_RATE_ELT        = "x_rate";
  128.             private static final String Y_RATE_ELT        = "y_rate";
  129.             private static final String DPSI_ELT          = "dPsi";
  130.             private static final String DEPSILON_ELT      = "dEpsilon";
  131.             private static final String DX_ELT            = "dX";
  132.             private static final String DY_ELT            = "dY";

  133.             // elements and attributes specific to bulletinA, bulletinB and EOP C04 files
  134.             private static final String DATA_ELT            = "data";
  135.             private static final String PRODUCT_ATTR        = "product";
  136.             private static final String BULLETIN_A_PROD     = "BulletinA";
  137.             private static final String BULLETIN_B_PROD     = "BulletinB";
  138.             private static final String EOP_C04_PROD_PREFIX = "EOP";
  139.             private static final String EOP_C04_PROD_SUFFIX = "C04";

  140.             // elements and attributes specific to daily data files
  141.             private static final String DATA_EOP_ELT      = "dataEOP";
  142.             private static final String TIME_SERIES_ELT   = "timeSeries";
  143.             private static final String DATE_YEAR_ELT     = "dateYear";
  144.             private static final String DATE_MONTH_ELT    = "dateMonth";
  145.             private static final String DATE_DAY_ELT      = "dateDay";
  146.             private static final String POLE_ELT          = "pole";
  147.             private static final String UT_ELT            = "UT";
  148.             private static final String UT1_U_UTC_ELT     = "UT1_UTC";
  149.             private static final String NUTATION_ELT      = "nutation";
  150.             private static final String SOURCE_ATTR       = "source";

  151.             // elements and attributes specific to finals data files
  152.             private static final String FINALS_ELT        = "Finals";
  153.             private static final String DATE_ELT          = "date";
  154.             private static final String EOP_SET_ELT       = "EOPSet";
  155.             private static final String BULLETIN_A_ELT    = "bulletinA";
  156.             private static final String UT1_M_UTC_ELT     = "UT1-UTC";

  157.             private boolean inBulletinA;
  158.             private int     year;
  159.             private int     month;
  160.             private int     day;
  161.             private int     mjd;
  162.             private AbsoluteDate mjdDate;
  163.             private double  dtu1;
  164.             private double  lod;
  165.             private double  x;
  166.             private double  y;
  167.             private double  xRate;
  168.             private double  yRate;
  169.             private double  dpsi;
  170.             private double  deps;
  171.             private double  dx;
  172.             private double  dy;

  173.             // CHECKSTYLE: resume JavadocVariable check

  174.             /** File name. */
  175.             private final String name;

  176.             /** Buffer for read characters. */
  177.             private final StringBuilder buffer;

  178.             /** Indicator for daily data XML format or final data XML format. */
  179.             private DataFileContent content;

  180.             /** ITRF version configuration. */
  181.             private ITRFVersionLoader.ITRFVersionConfiguration configuration;

  182.             /** Simple constructor.
  183.              * @param name file name
  184.              */
  185.             EOPContentHandler(final String name) {
  186.                 this.name   = name;
  187.                 this.buffer = new StringBuilder();
  188.             }

  189.             /** {@inheritDoc} */
  190.             @Override
  191.             public void startDocument() {
  192.                 content       = DataFileContent.UNKNOWN;
  193.                 configuration = null;
  194.             }

  195.             /** {@inheritDoc} */
  196.             @Override
  197.             public void characters(final char[] ch, final int start, final int length) {
  198.                 buffer.append(ch, start, length);
  199.             }

  200.             /** {@inheritDoc} */
  201.             @Override
  202.             public void startElement(final String uri, final String localName,
  203.                                      final String qName, final Attributes atts) {

  204.                 // reset the buffer to empty
  205.                 buffer.delete(0, buffer.length());

  206.                 if (content == DataFileContent.UNKNOWN) {
  207.                     // try to identify file content
  208.                     if (qName.equals(TIME_SERIES_ELT)) {
  209.                         // the file contains final data
  210.                         content = DataFileContent.DAILY;
  211.                     } else if (qName.equals(FINALS_ELT)) {
  212.                         // the file contains final data
  213.                         content = DataFileContent.FINAL;
  214.                     } else if (qName.equals(DATA_ELT)) {
  215.                         final String product = atts.getValue(PRODUCT_ATTR);
  216.                         if (product != null) {
  217.                             if (product.startsWith(BULLETIN_A_PROD)) {
  218.                                 // the file contains bulletinA
  219.                                 content     = DataFileContent.BULLETIN_A;
  220.                                 inBulletinA = true;
  221.                             } else if (product.startsWith(BULLETIN_B_PROD)) {
  222.                                 // the file contains bulletinB
  223.                                 content = DataFileContent.BULLETIN_B;
  224.                             } else if (product.startsWith(EOP_C04_PROD_PREFIX) && product.endsWith(EOP_C04_PROD_SUFFIX)) {
  225.                                 // the file contains EOP C04
  226.                                 content = DataFileContent.EOP_C04;
  227.                             }
  228.                         }
  229.                     }
  230.                 }

  231.                 if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
  232.                     content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
  233.                     startDailyElement(qName, atts);
  234.                 } else if (content == DataFileContent.FINAL) {
  235.                     startFinalElement(qName);
  236.                 }

  237.             }

  238.             /** Handle end of an element in a daily data file.
  239.              * @param qName name of the element
  240.              * @param atts element attributes
  241.              */
  242.             private void startDailyElement(final String qName, final Attributes atts) {
  243.                 if (qName.equals(TIME_SERIES_ELT)) {
  244.                     // reset EOP data
  245.                     resetEOPData();
  246.                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
  247.                     final String source = atts.getValue(SOURCE_ATTR);
  248.                     if (source != null) {
  249.                         inBulletinA = source.equals(BULLETIN_A_PROD);
  250.                     }
  251.                 }
  252.             }

  253.             /** Handle end of an element in a final data file.
  254.              * @param qName name of the element
  255.              */
  256.             private void startFinalElement(final String qName) {
  257.                 if (qName.equals(EOP_SET_ELT)) {
  258.                     // reset EOP data
  259.                     resetEOPData();
  260.                 } else if (qName.equals(BULLETIN_A_ELT)) {
  261.                     inBulletinA = true;
  262.                 }
  263.             }

  264.             /** Reset EOP data.
  265.              */
  266.             private void resetEOPData() {
  267.                 inBulletinA = false;
  268.                 year        = -1;
  269.                 month       = -1;
  270.                 day         = -1;
  271.                 mjd         = -1;
  272.                 mjdDate     = null;
  273.                 dtu1        = Double.NaN;
  274.                 lod         = Double.NaN;
  275.                 x           = Double.NaN;
  276.                 y           = Double.NaN;
  277.                 xRate       = Double.NaN;
  278.                 yRate       = Double.NaN;
  279.                 dpsi        = Double.NaN;
  280.                 deps        = Double.NaN;
  281.                 dx          = Double.NaN;
  282.                 dy          = Double.NaN;
  283.             }

  284.             /** {@inheritDoc} */
  285.             @Override
  286.             public void endElement(final String uri, final String localName, final String qName) {
  287.                 if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
  288.                     content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
  289.                     endDailyElement(qName);
  290.                 } else if (content == DataFileContent.FINAL) {
  291.                     endFinalElement(qName);
  292.                 }
  293.             }

  294.             /** Handle end of an element in a daily data file.
  295.              * @param qName name of the element
  296.              */
  297.             private void endDailyElement(final String qName) {
  298.                 if (qName.equals(DATE_YEAR_ELT) && buffer.length() > 0) {
  299.                     year = Integer.parseInt(buffer.toString());
  300.                 } else if (qName.equals(DATE_MONTH_ELT) && buffer.length() > 0) {
  301.                     month = Integer.parseInt(buffer.toString());
  302.                 } else if (qName.equals(DATE_DAY_ELT) && buffer.length() > 0) {
  303.                     day = Integer.parseInt(buffer.toString());
  304.                 } else if (qName.equals(MJD_ELT) && buffer.length() > 0) {
  305.                     mjd     = Integer.parseInt(buffer.toString());
  306.                     mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
  307.                                                getUtc());
  308.                 } else if (qName.equals(UT1_M_UTC_ELT)) {
  309.                     dtu1 = overwrite(dtu1, Unit.SECOND);
  310.                 } else if (qName.equals(LOD_ELT)) {
  311.                     lod = overwrite(lod, MILLI_SECOND);
  312.                 } else if (qName.equals(X_ELT)) {
  313.                     x = overwrite(x, Unit.ARC_SECOND);
  314.                 } else if (qName.equals(Y_ELT)) {
  315.                     y = overwrite(y, Unit.ARC_SECOND);
  316.                 } else if (qName.equals(X_RATE_ELT)) {
  317.                     xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
  318.                 } else if (qName.equals(Y_RATE_ELT)) {
  319.                     yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
  320.                 } else if (qName.equals(DPSI_ELT)) {
  321.                     dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
  322.                 } else if (qName.equals(DEPSILON_ELT)) {
  323.                     deps = overwrite(deps, MILLI_ARC_SECOND);
  324.                 } else if (qName.equals(DX_ELT)) {
  325.                     dx   = overwrite(dx, MILLI_ARC_SECOND);
  326.                 } else if (qName.equals(DY_ELT)) {
  327.                     dy   = overwrite(dy, MILLI_ARC_SECOND);
  328.                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
  329.                     inBulletinA = false;
  330.                 } else if (qName.equals(DATA_EOP_ELT)) {
  331.                     checkDates();
  332.                     if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
  333.                         final double[] equinox;
  334.                         final double[] nro;
  335.                         if (Double.isNaN(dpsi)) {
  336.                             nro = new double[] {
  337.                                 dx, dy
  338.                             };
  339.                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
  340.                         } else {
  341.                             equinox = new double[] {
  342.                                 dpsi, deps
  343.                             };
  344.                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
  345.                         }
  346.                         if (configuration == null || !configuration.isValid(mjd)) {
  347.                             // get a configuration for current name and date range
  348.                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
  349.                         }
  350.                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
  351.                                                  equinox[0], equinox[1], nro[0], nro[1],
  352.                                                  configuration.getVersion(), mjdDate));
  353.                     }
  354.                 }
  355.             }

  356.             /** Handle end of an element in a final data file.
  357.              * @param qName name of the element
  358.              */
  359.             private void endFinalElement(final String qName) {
  360.                 if (qName.equals(DATE_ELT) && buffer.length() > 0) {
  361.                     final String[] fields = buffer.toString().split("-");
  362.                     if (fields.length == 3) {
  363.                         year  = Integer.parseInt(fields[0]);
  364.                         month = Integer.parseInt(fields[1]);
  365.                         day   = Integer.parseInt(fields[2]);
  366.                     }
  367.                 } else if (qName.equals(MJD_ELT) && buffer.length() > 0) {
  368.                     mjd     = Integer.parseInt(buffer.toString());
  369.                     mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
  370.                                                getUtc());
  371.                 } else if (qName.equals(UT1_U_UTC_ELT)) {
  372.                     dtu1 = overwrite(dtu1, Unit.SECOND);
  373.                 } else if (qName.equals(LOD_ELT)) {
  374.                     lod = overwrite(lod, MILLI_SECOND);
  375.                 } else if (qName.equals(X_ELT)) {
  376.                     x = overwrite(x, Unit.ARC_SECOND);
  377.                 } else if (qName.equals(Y_ELT)) {
  378.                     y = overwrite(y, Unit.ARC_SECOND);
  379.                 } else if (qName.equals(X_RATE_ELT)) {
  380.                     xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
  381.                 } else if (qName.equals(Y_RATE_ELT)) {
  382.                     yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
  383.                 } else if (qName.equals(DPSI_ELT)) {
  384.                     dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
  385.                 } else if (qName.equals(DEPSILON_ELT)) {
  386.                     deps = overwrite(deps, MILLI_ARC_SECOND);
  387.                 } else if (qName.equals(DX_ELT)) {
  388.                     dx   = overwrite(dx, MILLI_ARC_SECOND);
  389.                 } else if (qName.equals(DY_ELT)) {
  390.                     dy   = overwrite(dy, MILLI_ARC_SECOND);
  391.                 } else if (qName.equals(BULLETIN_A_ELT)) {
  392.                     inBulletinA = false;
  393.                 } else if (qName.equals(EOP_SET_ELT)) {
  394.                     checkDates();
  395.                     if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
  396.                         final double[] equinox;
  397.                         final double[] nro;
  398.                         if (Double.isNaN(dpsi)) {
  399.                             nro = new double[] {
  400.                                 dx, dy
  401.                             };
  402.                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
  403.                         } else {
  404.                             equinox = new double[] {
  405.                                 dpsi, deps
  406.                             };
  407.                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
  408.                         }
  409.                         if (configuration == null || !configuration.isValid(mjd)) {
  410.                             // get a configuration for current name and date range
  411.                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
  412.                         }
  413.                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, xRate, yRate,
  414.                                                  equinox[0], equinox[1], nro[0], nro[1],
  415.                                                  configuration.getVersion(), mjdDate));
  416.                     }
  417.                 }
  418.             }

  419.             /** Overwrite a value if it is not set or if we are in a bulletinB.
  420.              * @param oldValue old value to overwrite (may be NaN)
  421.              * @param units units of raw data
  422.              * @return a new value
  423.              */
  424.             private double overwrite(final double oldValue, final Unit units) {
  425.                 if (buffer.length() == 0) {
  426.                     // there is nothing to overwrite with
  427.                     return oldValue;
  428.                 } else if (inBulletinA && !Double.isNaN(oldValue)) {
  429.                     // the value is already set and bulletin A values have a low priority
  430.                     return oldValue;
  431.                 } else {
  432.                     // either the value is not set or it is a high priority bulletin B value
  433.                     return units.toSI(Double.parseDouble(buffer.toString()));
  434.                 }
  435.             }

  436.             /** Check if the year, month, day date and MJD date are consistent.
  437.              */
  438.             private void checkDates() {
  439.                 if (new DateComponents(year, month, day).getMJD() != mjd) {
  440.                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
  441.                                               name, year, month, day, mjd);
  442.                 }
  443.             }

  444.             /** {@inheritDoc} */
  445.             @Override
  446.             public InputSource resolveEntity(final String publicId, final String systemId) {
  447.                 // disable external entities
  448.                 return new InputSource();
  449.             }

  450.         }

  451.     }

  452.     /** Enumerate for data file content. */
  453.     private enum DataFileContent {

  454.         /** Unknown content. */
  455.         UNKNOWN,

  456.         /** Bulletin A data.
  457.          * @since 12.0
  458.          */
  459.         BULLETIN_A,

  460.         /** Bulletin B data.
  461.          * @since 12.0
  462.          */
  463.         BULLETIN_B,

  464.         /** EOP_C04 data.
  465.          * @since 12.0
  466.          */
  467.         EOP_C04,

  468.         /** Daily data. */
  469.         DAILY,

  470.         /** Final data. */
  471.         FINAL

  472.     }

  473. }