EopXmlLoader.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.frames;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedSet;
import java.util.function.Supplier;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.hipparchus.exception.LocalizedCoreFormats;
import org.orekit.data.DataProvidersManager;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeScale;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.units.Unit;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

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

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

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

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

    /**
     * Build a loader for IERS XML EOP files.
     *
     * @param supportedNames regular expression for supported files names
     * @param manager        provides access to the XML EOP files.
     * @param utcSupplier    UTC time scale.
     */
    EopXmlLoader(final String supportedNames,
                 final DataProvidersManager manager,
                 final Supplier<TimeScale> utcSupplier) {
        super(supportedNames, manager, utcSupplier);
    }

    /** {@inheritDoc} */
    public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
                            final SortedSet<EOPEntry> history) {
        final ItrfVersionProvider itrfVersionProvider = new ITRFVersionLoader(
                ITRFVersionLoader.SUPPORTED_NAMES,
                getDataProvidersManager());
        final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
        final EopParserLoader loader = new EopParserLoader(parser);
        this.feed(loader);
        history.addAll(loader.getEop());
    }

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

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

        /**
         * Simple constructor.
         *
         * @param converter           converter to use
         * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
         * @param utc                 time scale for parsing dates.
         */
        Parser(final IERSConventions.NutationCorrectionConverter converter,
               final ItrfVersionProvider itrfVersionProvider,
               final TimeScale utc) {
            super(converter, itrfVersionProvider, utc);
        }

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

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

                return history;

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

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

            // CHECKSTYLE: stop JavadocVariable check

            // elements and attributes used in both daily and finals data files
            private static final String MJD_ELT           = "MJD";
            private static final String LOD_ELT           = "LOD";
            private static final String X_ELT             = "X";
            private static final String Y_ELT             = "Y";
            private static final String X_RATE_ELT        = "x_rate";
            private static final String Y_RATE_ELT        = "y_rate";
            private static final String DPSI_ELT          = "dPsi";
            private static final String DEPSILON_ELT      = "dEpsilon";
            private static final String DX_ELT            = "dX";
            private static final String DY_ELT            = "dY";

            // elements and attributes specific to bulletinA, bulletinB and EOP C04 files
            private static final String DATA_ELT            = "data";
            private static final String PRODUCT_ATTR        = "product";
            private static final String BULLETIN_A_PROD     = "BulletinA";
            private static final String BULLETIN_B_PROD     = "BulletinB";
            private static final String EOP_C04_PROD_PREFIX = "EOP";
            private static final String EOP_C04_PROD_SUFFIX = "C04";

            // elements and attributes specific to daily data files
            private static final String DATA_EOP_ELT      = "dataEOP";
            private static final String TIME_SERIES_ELT   = "timeSeries";
            private static final String DATE_YEAR_ELT     = "dateYear";
            private static final String DATE_MONTH_ELT    = "dateMonth";
            private static final String DATE_DAY_ELT      = "dateDay";
            private static final String POLE_ELT          = "pole";
            private static final String UT_ELT            = "UT";
            private static final String UT1_U_UTC_ELT     = "UT1_UTC";
            private static final String NUTATION_ELT      = "nutation";
            private static final String SOURCE_ATTR       = "source";

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

            private boolean inBulletinA;
            private int     year;
            private int     month;
            private int     day;
            private int     mjd;
            private AbsoluteDate mjdDate;
            private double  dtu1;
            private double  lod;
            private double  x;
            private double  y;
            private double  xRate;
            private double  yRate;
            private double  dpsi;
            private double  deps;
            private double  dx;
            private double  dy;

            // CHECKSTYLE: resume JavadocVariable check

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

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

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

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

            /** Simple constructor.
             * @param name file name
             */
            EOPContentHandler(final String name) {
                this.name   = name;
                this.buffer = new StringBuilder();
            }

            /** {@inheritDoc} */
            @Override
            public void startDocument() {
                content       = DataFileContent.UNKNOWN;
                configuration = null;
            }

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

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

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

                if (content == DataFileContent.UNKNOWN) {
                    // try to identify file content
                    if (qName.equals(TIME_SERIES_ELT)) {
                        // the file contains final data
                        content = DataFileContent.DAILY;
                    } else if (qName.equals(FINALS_ELT)) {
                        // the file contains final data
                        content = DataFileContent.FINAL;
                    } else if (qName.equals(DATA_ELT)) {
                        final String product = atts.getValue(PRODUCT_ATTR);
                        if (product != null) {
                            if (product.startsWith(BULLETIN_A_PROD)) {
                                // the file contains bulletinA
                                content     = DataFileContent.BULLETIN_A;
                                inBulletinA = true;
                            } else if (product.startsWith(BULLETIN_B_PROD)) {
                                // the file contains bulletinB
                                content = DataFileContent.BULLETIN_B;
                            } else if (product.startsWith(EOP_C04_PROD_PREFIX) && product.endsWith(EOP_C04_PROD_SUFFIX)) {
                                // the file contains EOP C04
                                content = DataFileContent.EOP_C04;
                            }
                        }
                    }
                }

                if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
                    content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
                    startDailyElement(qName, atts);
                } else if (content == DataFileContent.FINAL) {
                    startFinalElement(qName);
                }

            }

            /** Handle end of an element in a daily data file.
             * @param qName name of the element
             * @param atts element attributes
             */
            private void startDailyElement(final String qName, final Attributes atts) {
                if (qName.equals(TIME_SERIES_ELT)) {
                    // reset EOP data
                    resetEOPData();
                } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
                    final String source = atts.getValue(SOURCE_ATTR);
                    if (source != null) {
                        inBulletinA = source.equals(BULLETIN_A_PROD);
                    }
                }
            }

            /** Handle end of an element in a final data file.
             * @param qName name of the element
             */
            private void startFinalElement(final String qName) {
                if (qName.equals(EOP_SET_ELT)) {
                    // reset EOP data
                    resetEOPData();
                } else if (qName.equals(BULLETIN_A_ELT)) {
                    inBulletinA = true;
                }
            }

            /** Reset EOP data.
             */
            private void resetEOPData() {
                inBulletinA = false;
                year        = -1;
                month       = -1;
                day         = -1;
                mjd         = -1;
                mjdDate     = null;
                dtu1        = Double.NaN;
                lod         = Double.NaN;
                x           = Double.NaN;
                y           = Double.NaN;
                xRate       = Double.NaN;
                yRate       = Double.NaN;
                dpsi        = Double.NaN;
                deps        = Double.NaN;
                dx          = Double.NaN;
                dy          = Double.NaN;
            }

            /** {@inheritDoc} */
            @Override
            public void endElement(final String uri, final String localName, final String qName) {
                if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
                    content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
                    endDailyElement(qName);
                } else if (content == DataFileContent.FINAL) {
                    endFinalElement(qName);
                }
            }

            /** Handle end of an element in a daily data file.
             * @param qName name of the element
             */
            private void endDailyElement(final String qName) {
                if (qName.equals(DATE_YEAR_ELT) && buffer.length() > 0) {
                    year = Integer.parseInt(buffer.toString());
                } else if (qName.equals(DATE_MONTH_ELT) && buffer.length() > 0) {
                    month = Integer.parseInt(buffer.toString());
                } else if (qName.equals(DATE_DAY_ELT) && buffer.length() > 0) {
                    day = Integer.parseInt(buffer.toString());
                } else if (qName.equals(MJD_ELT) && buffer.length() > 0) {
                    mjd     = Integer.parseInt(buffer.toString());
                    mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
                                               getUtc());
                } else if (qName.equals(UT1_M_UTC_ELT)) {
                    dtu1 = overwrite(dtu1, Unit.SECOND);
                } else if (qName.equals(LOD_ELT)) {
                    lod = overwrite(lod, MILLI_SECOND);
                } else if (qName.equals(X_ELT)) {
                    x = overwrite(x, Unit.ARC_SECOND);
                } else if (qName.equals(Y_ELT)) {
                    y = overwrite(y, Unit.ARC_SECOND);
                } else if (qName.equals(X_RATE_ELT)) {
                    xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
                } else if (qName.equals(Y_RATE_ELT)) {
                    yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
                } else if (qName.equals(DPSI_ELT)) {
                    dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
                } else if (qName.equals(DEPSILON_ELT)) {
                    deps = overwrite(deps, MILLI_ARC_SECOND);
                } else if (qName.equals(DX_ELT)) {
                    dx   = overwrite(dx, MILLI_ARC_SECOND);
                } else if (qName.equals(DY_ELT)) {
                    dy   = overwrite(dy, MILLI_ARC_SECOND);
                } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
                    inBulletinA = false;
                } else if (qName.equals(DATA_EOP_ELT)) {
                    checkDates();
                    if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
                        final double[] equinox;
                        final double[] nro;
                        if (Double.isNaN(dpsi)) {
                            nro = new double[] {
                                dx, dy
                            };
                            equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
                        } else {
                            equinox = new double[] {
                                dpsi, deps
                            };
                            nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
                        }
                        if (configuration == null || !configuration.isValid(mjd)) {
                            // get a configuration for current name and date range
                            configuration = getItrfVersionProvider().getConfiguration(name, mjd);
                        }
                        history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
                                                 equinox[0], equinox[1], nro[0], nro[1],
                                                 configuration.getVersion(), mjdDate));
                    }
                }
            }

            /** Handle end of an element in a final data file.
             * @param qName name of the element
             */
            private void endFinalElement(final String qName) {
                if (qName.equals(DATE_ELT) && buffer.length() > 0) {
                    final String[] fields = buffer.toString().split("-");
                    if (fields.length == 3) {
                        year  = Integer.parseInt(fields[0]);
                        month = Integer.parseInt(fields[1]);
                        day   = Integer.parseInt(fields[2]);
                    }
                } else if (qName.equals(MJD_ELT) && buffer.length() > 0) {
                    mjd     = Integer.parseInt(buffer.toString());
                    mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
                                               getUtc());
                } else if (qName.equals(UT1_U_UTC_ELT)) {
                    dtu1 = overwrite(dtu1, Unit.SECOND);
                } else if (qName.equals(LOD_ELT)) {
                    lod = overwrite(lod, MILLI_SECOND);
                } else if (qName.equals(X_ELT)) {
                    x = overwrite(x, Unit.ARC_SECOND);
                } else if (qName.equals(Y_ELT)) {
                    y = overwrite(y, Unit.ARC_SECOND);
                } else if (qName.equals(X_RATE_ELT)) {
                    xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
                } else if (qName.equals(Y_RATE_ELT)) {
                    yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
                } else if (qName.equals(DPSI_ELT)) {
                    dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
                } else if (qName.equals(DEPSILON_ELT)) {
                    deps = overwrite(deps, MILLI_ARC_SECOND);
                } else if (qName.equals(DX_ELT)) {
                    dx   = overwrite(dx, MILLI_ARC_SECOND);
                } else if (qName.equals(DY_ELT)) {
                    dy   = overwrite(dy, MILLI_ARC_SECOND);
                } else if (qName.equals(BULLETIN_A_ELT)) {
                    inBulletinA = false;
                } else if (qName.equals(EOP_SET_ELT)) {
                    checkDates();
                    if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
                        final double[] equinox;
                        final double[] nro;
                        if (Double.isNaN(dpsi)) {
                            nro = new double[] {
                                dx, dy
                            };
                            equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
                        } else {
                            equinox = new double[] {
                                dpsi, deps
                            };
                            nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
                        }
                        if (configuration == null || !configuration.isValid(mjd)) {
                            // get a configuration for current name and date range
                            configuration = getItrfVersionProvider().getConfiguration(name, mjd);
                        }
                        history.add(new EOPEntry(mjd, dtu1, lod, x, y, xRate, yRate,
                                                 equinox[0], equinox[1], nro[0], nro[1],
                                                 configuration.getVersion(), mjdDate));
                    }
                }
            }

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

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

            /** {@inheritDoc} */
            @Override
            public InputSource resolveEntity(final String publicId, final String systemId) {
                // disable external entities
                return new InputSource();
            }

        }

    }

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

        /** Unknown content. */
        UNKNOWN,

        /** Bulletin A data.
         * @since 12.0
         */
        BULLETIN_A,

        /** Bulletin B data.
         * @since 12.0
         */
        BULLETIN_B,

        /** EOP_C04 data.
         * @since 12.0
         */
        EOP_C04,

        /** Daily data. */
        DAILY,

        /** Final data. */
        FINAL

    }

}