EopCsvFilesLoader.java

/* Copyright 2023 Luc Maisonobe
 * 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.BufferedReader;
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 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.IERSConventions.NutationCorrectionConverter;
import org.orekit.utils.units.Unit;

/** Loader for EOP csv files (can be bulletin A, bulletin B, EOP C04…).
 * <p>
 * This class is immutable and hence thread-safe
 * </p>
 * @author Luc Maisonobe
 * @since 12.0
 */
class EopCsvFilesLoader extends AbstractEopLoader implements EopHistoryLoader {

    /** Separator. */
    private static final String SEPARATOR = ";";

    /** Header for MJD. */
    private static final String MJD = "MJD";

    /** Header for Year. */
    private static final String YEAR = "Year";

    /** Header for Month. */
    private static final String MONTH = "Month";

    /** Header for Day. */
    private static final String DAY = "Day";

    /** Header for x_pole. */
    private static final String X_POLE = "x_pole";

    /** Header for y_pole. */
    private static final String Y_POLE = "y_pole";

    /** Header for x_rate. */
    private static final String X_RATE = "x_rate";

    /** Header for y_rate. */
    private static final String Y_RATE = "y_rate";

    /** Header for UT1-UTC. */
    private static final String UT1_UTC = "UT1-UTC";

    /** Header for LOD. */
    private static final String LOD = "LOD";

    /** Header for dPsi. */
    private static final String DPSI = "dPsi";

    /** Header for dEpsilon. */
    private static final String DEPSILON = "dEpsilon";

    /** Header for dX. */
    private static final String DX = "dX";

    /** Header for dY. */
    private static final String DY = "dY";

    /** Converter for milliarcseconds. */
    private static final Unit MAS = Unit.parse("mas");

    /** Converter for milliarcseconds per day. */
    private static final Unit MAS_D = Unit.parse("mas/day");

    /** Converter for milliseconds. */
    private static final Unit MS = Unit.parse("ms");

    /** Build a loader for IERS EOP csv files.
     * @param supportedNames regular expression for supported files names
     * @param manager provides access to the EOP C04 files.
     * @param utcSupplier UTC time scale.
     */
    EopCsvFilesLoader(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 Parser parser = new Parser(converter, getUtc());
        final EopParserLoader loader = new EopParserLoader(parser);
        this.feed(loader);
        history.addAll(loader.getEop());
    }

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

        /** Configuration for ITRF versions. */
        private final ItrfVersionProvider itrfVersionProvider;

        /** Column number for MJD field. */
        private int mjdColumn;

        /** Column number for year field. */
        private int yearColumn;

        /** Column number for month field. */
        private int monthColumn;

        /** Column number for day field. */
        private int dayColumn;

        /** Column number for X pole field. */
        private int xPoleColumn;

        /** Column number for Y pole field. */
        private int yPoleColumn;

        /** Column number for X rate pole field. */
        private int xRatePoleColumn;

        /** Column number for Y rate pole field. */
        private int yRatePoleColumn;

        /** Column number for UT1-UTC field. */
        private int ut1Column;

        /** Column number for LOD field. */
        private int lodColumn;

        /** Column number for dX field. */
        private int dxColumn;

        /** Column number for dY field. */
        private int dyColumn;

        /** Column number for dPsi field. */
        private int dPsiColumn;

        /** Column number for dEpsilon field. */
        private int dEpsilonColumn;

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

        /** Simple constructor.
         * @param converter converter to use
         * @param utc       time scale for parsing dates.
         */
        Parser(final NutationCorrectionConverter converter,
               final TimeScale utc) {
            super(converter, null, utc);
            this.itrfVersionProvider = new ITRFVersionLoader(ITRFVersionLoader.SUPPORTED_NAMES,
                                                             getDataProvidersManager());
        }

        /** {@inheritDoc} */
        public Collection<EOPEntry> parse(final InputStream input, final String name)
            throws IOException, OrekitException {

            final List<EOPEntry> history = new ArrayList<>();

            // set up a reader for line-oriented csv files
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
                // reset parse info to start new file (do not clear history!)
                int lineNumber = 0;
                configuration  = null;

                // read all file
                for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                    ++lineNumber;

                    final boolean parsed;
                    if (lineNumber == 1) {
                        parsed = parseHeaderLine(line);
                    } else {
                        history.add(parseDataLine(line, name));
                        parsed = true;
                    }

                    if (!parsed) {
                        throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                lineNumber, name, line);
                    }
                }

                // check if we have read something
                if (lineNumber < 2) {
                    throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
                }
            }

            return history;
        }

        /** Parse the header line.
         * @param headerLine header line
         * @return true if line was parsed correctly
         */
        private boolean parseHeaderLine(final String headerLine) {

            // reset columns numbers
            mjdColumn       = -1;
            yearColumn      = -1;
            monthColumn     = -1;
            dayColumn       = -1;
            xPoleColumn     = -1;
            yPoleColumn     = -1;
            xRatePoleColumn = -1;
            yRatePoleColumn = -1;
            ut1Column       = -1;
            lodColumn       = -1;
            dxColumn        = -1;
            dyColumn        = -1;
            dPsiColumn      = -1;
            dEpsilonColumn  = -1;

            // split header fields
            final String[] fields = headerLine.split(SEPARATOR);

            // affect column numbers according to header fields
            for (int column = 0; column < fields.length; ++column) {
                switch (fields[column]) {
                    case MJD :
                        mjdColumn = column;
                        break;
                    case YEAR :
                        yearColumn = column;
                        break;
                    case MONTH :
                        monthColumn = column;
                        break;
                    case DAY :
                        dayColumn = column;
                        break;
                    case X_POLE :
                        xPoleColumn = column;
                        break;
                    case Y_POLE :
                        yPoleColumn = column;
                        break;
                    case X_RATE :
                        xRatePoleColumn = column;
                        break;
                    case Y_RATE :
                        yRatePoleColumn = column;
                        break;
                    case UT1_UTC :
                        ut1Column = column;
                        break;
                    case LOD :
                        lodColumn = column;
                        break;
                    case DX :
                        dxColumn = column;
                        break;
                    case DY :
                        dyColumn = column;
                        break;
                    case DPSI :
                        dPsiColumn = column;
                        break;
                    case DEPSILON :
                        dEpsilonColumn = column;
                        break;
                    default :
                        // ignored column
                }
            }

            // check all required files are present (we just allow pole rates to be missing)
            return mjdColumn >= 0 && yearColumn >= 0 && monthColumn >= 0 && dayColumn >= 0 &&
                   xPoleColumn >= 0 && yPoleColumn >= 0 && ut1Column >= 0 && lodColumn >= 0 &&
                   (dxColumn >= 0 && dyColumn >= 0 || dPsiColumn >= 0 && dEpsilonColumn >= 0);

        }

        /** Parse a data line.
         * @param line line to parse
         * @param name file name (for error messages)
         * @return parsed entry
         */
        private EOPEntry parseDataLine(final String line, final String name) {

            final String[] fields = line.split(SEPARATOR);

            // check date
            final DateComponents dc = new DateComponents(Integer.parseInt(fields[yearColumn]),
                                                         Integer.parseInt(fields[monthColumn]),
                                                         Integer.parseInt(fields[dayColumn]));
            final int    mjd   = Integer.parseInt(fields[mjdColumn]);
            if (dc.getMJD() != mjd) {
                throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
                                          name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
            }
            final AbsoluteDate date = new AbsoluteDate(dc, getUtc());

            if (configuration == null || !configuration.isValid(mjd)) {
                // get a configuration for current name and date range
                configuration = itrfVersionProvider.getConfiguration(name, mjd);
            }

            final double x     = parseField(fields, xPoleColumn,     MAS);
            final double y     = parseField(fields, yPoleColumn,     MAS);
            final double xRate = parseField(fields, xRatePoleColumn, MAS_D);
            final double yRate = parseField(fields, yRatePoleColumn, MAS_D);
            final double dtu1  = parseField(fields, ut1Column,       MS);
            final double lod   = parseField(fields, lodColumn,       MS);

            if (dxColumn >= 0) {
                // non-rotatin origin paradigm
                final double dx = parseField(fields, dxColumn, MAS);
                final double dy = parseField(fields, dyColumn, MAS);
                final double[] equinox = getConverter().toEquinox(date, dx, dy);
                return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
                                    equinox[0], equinox[1], dx, dy,
                                    configuration.getVersion(), date);
            } else {
                // equinox paradigm
                final double ddPsi      = parseField(fields, dPsiColumn,     MAS);
                final double dddEpsilon = parseField(fields, dEpsilonColumn, MAS);
                final double[] nro = getConverter().toNonRotating(date, ddPsi, dddEpsilon);
                return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
                                    ddPsi, dddEpsilon, nro[0], nro[1],
                                    configuration.getVersion(), date);
            }


        }

        /** Parse one field.
         * @param fields fields array to parse
         * @param index index in the field array (negative for ignored fields)
         * @param unit field unit
         * @return parsed and converted field
         */
        private double parseField(final String[] fields, final int index, final Unit unit) {
            return (index < 0 || index >= fields.length || fields[index].isEmpty()) ?
                   Double.NaN :
                   unit.toSI(Double.parseDouble(fields[index]));
        }

    }

}