RinexNavigationParser.java

/* Copyright 2002-2025 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.files.rinex.navigation;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.Collections;
import java.util.InputMismatchException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.rinex.navigation.parsers.ephemeris.GlonassFdmaParser;
import org.orekit.files.rinex.navigation.parsers.RecordLineParser;
import org.orekit.files.rinex.navigation.parsers.ParseInfo;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.utils.ParsingUtils;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.GNSSDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.utils.units.Unit;

/**
 * Parser for RINEX navigation messages files.
 * <p>
 * This parser handles RINEX version from 2 to 4.02.
 * </p>
 * @see <a href="https://files.igs.org/pub/data/format/rinex2.txt">rinex 2.0</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex210.txt">rinex 2.10</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex211.pdf">rinex 2.11</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex301.pdf"> 3.01 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex302.pdf"> 3.02 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex303.pdf"> 3.03 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex304.pdf"> 3.04 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex305.pdf"> 3.05 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex_4.00.pdf"> 4.00 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex_4.01.pdf"> 4.01 navigation messages file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex_4.02.pdf"> 4.02 navigation messages file format</a>
 *
 * @author Bryan Cazabonne
 * @since 11.0
 *
 */
public class RinexNavigationParser {

    /** Converter for positions. */
    public static final Unit KM = Unit.KILOMETRE;

    /** Converter for velocities. */
    public static final Unit KM_PER_S = Unit.parse("km/s");

    /** Converter for accelerations. */
    public static final Unit KM_PER_S2 = Unit.parse("km/s²");

    /** Converter for velocities. */
    public static final Unit M_PER_S = Unit.parse("m/s");

    /** Converter for clock drift. */
    public static final Unit S_PER_S = Unit.parse("s/s");

    /** Converter for clock drift rate. */
    public static final Unit S_PER_S2 = Unit.parse("s/s²");

    /** Converter for ΔUT₁ first derivative. */
    public static final Unit S_PER_DAY = Unit.parse("s/d");

    /** Converter for ΔUT₁ second derivative. */
    public static final Unit S_PER_DAY2 = Unit.parse("s/d²");

    /** Converter for square root of semi-major axis. */
    public static final Unit SQRT_M = Unit.parse("√m");

    /** Converter for angular rates. */
    public static final Unit RAD_PER_S = Unit.parse("rad/s");

    /** Converter for angular accelerations. */
    public static final Unit RAD_PER_S2 = Unit.parse("rad/s²");

    /** Converter for rates of small angle. */
    public static final Unit AS_PER_DAY = Unit.parse("as/d");

    /** Converter for accelerations of small angles. */
    public static final Unit AS_PER_DAY2 = Unit.parse("as/d²");

    /** Total Electron Content. */
    public static final Unit TEC = Unit.TOTAL_ELECTRON_CONTENT_UNIT;

    /** System initials. */
    private static final String INITIALS = "GRECIJS";

    /** Set of time scales. */
    private final TimeScales timeScales;

    /**
     * Constructor.
     * <p>This constructor uses the {@link DataContext#getDefault() default data context}.</p>
     * @see #RinexNavigationParser(TimeScales)
     *
     */
    @DefaultDataContext
    public RinexNavigationParser() {
        this(DataContext.getDefault().getTimeScales());
    }

    /**
     * Constructor.
     * @param timeScales the set of time scales used for parsing dates.
     */
    public RinexNavigationParser(final TimeScales timeScales) {
        this.timeScales = timeScales;
    }

    /**
     * Parse RINEX navigation messages.
     * @param source source providing the data to parse
     * @return a parsed  RINEX navigation messages file
     * @throws IOException if {@code reader} throws one
     */
    public RinexNavigation parse(final DataSource source) throws IOException {

        // initialize internal data structures
        final ParseInfo parseInfo = new ParseInfo(source.getName(), timeScales);

        Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
        try (Reader reader = source.getOpener().openReaderOnce();
             BufferedReader br = new BufferedReader(reader)) {
            nextLine:
                for (String line = br.readLine(); line != null; line = br.readLine()) {
                    parseInfo.setLine(line);
                    for (final LineParser candidate : candidateParsers) {
                        if (candidate.canHandle.test(parseInfo)) {
                            try {
                                candidate.parsingMethod.accept(parseInfo);
                                candidateParsers = candidate.allowedNextProvider.apply(parseInfo);
                                continue nextLine;
                            } catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
                                throw new OrekitException(e,
                                                          OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                          parseInfo.getLineNumber(), source.getName(), line);
                            }
                        }
                    }
                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                              parseInfo.getLineNumber(), source.getName(), line);
                }
        }

        return parseInfo.getCompletedFile();

    }

    /** Parsers for specific lines. */
    private enum LineParser {

        /** Parser for version, file type and satellite system. */
        HEADER_VERSION(pi -> pi.getHeader().matchFound(CommonLabel.VERSION, pi.getLine()),
                       pi -> {
                           pi.getHeader().parseVersionFileTypeSatelliteSystem(pi.getLine(), SatelliteSystem.GPS,
                                                                              pi.getName(),
                                                                              2.0, 2.01, 2.10, 2.11,
                                                                              3.01, 3.02, 3.03, 3.04, 3.05,
                                                                              4.00, 4.01, 4.02);
                           pi.setInitialSpaces(pi.getHeader().getFormatVersion() < 3.0 ? 3 : 4);
                       },
                       LineParser::headerNext),

        /** Parser for generating program and emitting agency. */
        HEADER_PROGRAM(pi -> pi.getHeader().matchFound(CommonLabel.PROGRAM, pi.getLine()),
                       pi -> pi.getHeader().parseProgramRunByDate(pi.getLine(), pi.getTimeScales()),
                       LineParser::headerNext),

        /** Parser for comments. */
        HEADER_COMMENT(pi -> pi.getHeader().matchFound(CommonLabel.COMMENT, pi.getLine()),
                       ParseInfo::parseComment,
                       LineParser::headerNext),

        /** Parser for ionospheric correction parameters. */
        HEADER_ION_ALPHA(pi -> pi.getHeader().matchFound(NavigationLabel.ION_ALPHA, pi.getLine()),
                         pi -> {

                             pi.setIonosphericCorrectionType(IonosphericCorrectionType.GPS);

                             // Read coefficients
                             final double[] parameters = new double[4];
                             parameters[0] = ParsingUtils.parseDouble(pi.getLine(), 2, 12);
                             parameters[1] = ParsingUtils.parseDouble(pi.getLine(), 14, 12);
                             parameters[2] = ParsingUtils.parseDouble(pi.getLine(), 26, 12);
                             parameters[3] = ParsingUtils.parseDouble(pi.getLine(), 38, 12);
                             pi.setKlobucharAlpha(parameters);

                         },
                         LineParser::headerNext),

        /** Parser for ionospheric correction parameters. */
        HEADER_ION_BETA(pi -> pi.getHeader().matchFound(NavigationLabel.ION_BETA, pi.getLine()),
                        pi -> {

                            pi.setIonosphericCorrectionType(IonosphericCorrectionType.GPS);

                            // Read coefficients
                            final double[] parameters = new double[4];
                            parameters[0] = ParsingUtils.parseDouble(pi.getLine(), 2, 12);
                            parameters[1] = ParsingUtils.parseDouble(pi.getLine(), 14, 12);
                            parameters[2] = ParsingUtils.parseDouble(pi.getLine(), 26, 12);
                            parameters[3] = ParsingUtils.parseDouble(pi.getLine(), 38, 12);
                            pi.setKlobucharBeta(parameters);

                        },
                        LineParser::headerNext),

        /** Parser for ionospheric correction parameters. */
        HEADER_IONOSPHERIC(pi -> pi.getHeader().matchFound(NavigationLabel.IONOSPHERIC_CORR, pi.getLine()),
                           pi -> {

                               // ionospheric correction type
                               final IonosphericCorrectionType ionoType =
                                               IonosphericCorrectionType.valueOf(ParsingUtils.parseString(pi.getLine(), 0, 3));
                               pi.setIonosphericCorrectionType(ionoType);
                               pi.setTimeMark(pi.getLine().charAt(54));

                               if (ionoType == IonosphericCorrectionType.GAL) {
                                   // We are parsing Galileo NeQuick G ionospheric parameters
                                   pi.setNeQuickAlpha(new double[] {
                                       ParsingUtils.parseDouble(pi.getLine(),  5, 12),
                                       ParsingUtils.parseDouble(pi.getLine(), 17, 12),
                                       ParsingUtils.parseDouble(pi.getLine(), 29, 12)
                                   });
                               } else {
                                   // We are parsing Klobuchar ionospheric parameters
                                   final double[] parameters = new double[] {
                                       ParsingUtils.parseDouble(pi.getLine(),  5, 12),
                                       ParsingUtils.parseDouble(pi.getLine(), 17, 12),
                                       ParsingUtils.parseDouble(pi.getLine(), 29, 12),
                                       ParsingUtils.parseDouble(pi.getLine(), 41, 12)
                                   };

                                   if (pi.getLine().charAt(3) == 'A') {
                                       // Ionospheric α parameters
                                       pi.setKlobucharAlpha(parameters);
                                   } else {
                                       // Ionospheric β parameters
                                       pi.setKlobucharBeta(parameters);
                                   }

                               }

                           },
                           LineParser::headerNext),

        /** Parser for corrections to transform the system time to UTC or to other time systems. */
        HEADER_DELTA_UTC(pi -> pi.getHeader().matchFound(NavigationLabel.DELTA_UTC, pi.getLine()),
                         pi -> {
                             // Read fields
                             final double a0      = ParsingUtils.parseDouble(pi.getLine(), 3, 19);
                             final double a1      = ParsingUtils.parseDouble(pi.getLine(), 22, 19);
                             final int    refTime = ParsingUtils.parseInt(pi.getLine(), 41, 9);
                             final int    refWeek = ParsingUtils.parseInt(pi.getLine(), 50, 9);

                             // convert date
                             final SatelliteSystem satSystem = pi.getHeader().getSatelliteSystem();
                             final AbsoluteDate    date      = new GNSSDate(refWeek, refTime, satSystem, pi.getTimeScales()).getDate();

                             // Add to the list
                             final TimeSystemCorrection tsc = new TimeSystemCorrection("GPUT", date, a0, a1, "", 0);
                             pi.getHeader().addTimeSystemCorrections(tsc);
                         },
                         LineParser::headerNext),

        /** Parser for corrections to transform the GLONASS system time to UTC or to other time systems. */
        HEADER_CORR_SYSTEM_TIME(pi -> pi.getHeader().matchFound(NavigationLabel.CORR_TO_SYSTEM_TIME, pi.getLine()),
                         pi -> {
                             // Read fields
                             final int year        = ParsingUtils.parseInt(pi.getLine(), 0, 6);
                             final int month       = ParsingUtils.parseInt(pi.getLine(), 6, 6);
                             final int day         = ParsingUtils.parseInt(pi.getLine(), 12, 6);
                             final double minusTau = ParsingUtils.parseDouble(pi.getLine(), 21, 19);

                             // convert date
                             final SatelliteSystem satSystem = SatelliteSystem.GLONASS;
                             final TimeScale       timeScale = satSystem.getObservationTimeScale().getTimeScale(pi.getTimeScales());
                             final AbsoluteDate    date      = new AbsoluteDate(year, month, day, timeScale);

                             // Add to the list
                             final TimeSystemCorrection tsc = new TimeSystemCorrection("GLUT", date, minusTau, 0.0, "", 0);
                             pi.getHeader().addTimeSystemCorrections(tsc);

                         },
                         LineParser::headerNext),

        /** Parser for corrections to transform the system time to UTC or to other time systems. */
        HEADER_TIME(pi -> pi.getHeader().matchFound(NavigationLabel.TIME_SYSTEM_CORR, pi.getLine()),
                    pi -> {

                        // Read fields
                        final String type    = ParsingUtils.parseString(pi.getLine(),  0, 4);
                        final double a0      = ParsingUtils.parseDouble(pi.getLine(),  5, 17);
                        final double a1      = ParsingUtils.parseDouble(pi.getLine(), 22, 16);
                        final int    refTime = ParsingUtils.parseInt(pi.getLine(),    38,  7);
                        final int    refWeek = ParsingUtils.parseInt(pi.getLine(),    46,  5);
                        final String satId   = ParsingUtils.parseString(pi.getLine(), 51,  5);
                        final int    utcId   = ParsingUtils.parseInt(pi.getLine(),    57,  2);

                        // convert date
                        final SatelliteSystem satSystem = pi.getHeader().getSatelliteSystem();
                        final AbsoluteDate    date;
                        if (satSystem == SatelliteSystem.GLONASS) {
                            date = null;
                        } else if (satSystem == SatelliteSystem.BEIDOU) {
                            date = new GNSSDate(refWeek, refTime, satSystem, pi.getTimeScales()).getDate();
                        } else {
                            // all other systems are converted to GPS week in Rinex files!
                            date = new GNSSDate(refWeek, refTime, SatelliteSystem.GPS, pi.getTimeScales()).getDate();
                        }

                        // Add to the list
                        final TimeSystemCorrection tsc = new TimeSystemCorrection(type, date, a0, a1, satId, utcId);
                        pi.getHeader().addTimeSystemCorrections(tsc);

                    },
                    LineParser::headerNext),

        /** Parser for leap seconds. */
        HEADER_LEAP_SECONDS(pi -> pi.getHeader().matchFound(CommonLabel.LEAP_SECONDS, pi.getLine()),
                            pi -> {
                                pi.getHeader().setLeapSecondsGNSS(ParsingUtils.parseInt(pi.getLine(), 0, 6));
                                pi.getHeader().setLeapSecondsFuture(ParsingUtils.parseInt(pi.getLine(), 6, 6));
                                pi.getHeader().setLeapSecondsWeekNum(ParsingUtils.parseInt(pi.getLine(), 12, 6));
                                pi.getHeader().setLeapSecondsDayNum(ParsingUtils.parseInt(pi.getLine(), 18, 6));
                            },
                            LineParser::headerNext),

        /** Parser for DOI.
         * @since 12.0
         */
        HEADER_DOI(pi -> pi.getHeader().matchFound(CommonLabel.DOI, pi.getLine()),
                   pi -> pi.getHeader().
                       setDoi(ParsingUtils.parseString(pi.getLine(), 0, pi.getHeader().getLabelIndex())),
                   LineParser::headerNext),

        /** Parser for license.
         * @since 12.0
         */
        HEADER_LICENSE(pi -> pi.getHeader().matchFound(CommonLabel.LICENSE, pi.getLine()),
                       pi -> pi.getHeader().
                           setLicense(ParsingUtils.parseString(pi.getLine(), 0, pi.getHeader().getLabelIndex())),
                       LineParser::headerNext),

        /** Parser for stationInformation.
         * @since 12.0
         */
        HEADER_STATION_INFORMATION(pi -> pi.getHeader().matchFound(CommonLabel.STATION_INFORMATION, pi.getLine()),
                                   pi -> pi.getHeader().
                                       setStationInformation(ParsingUtils.parseString(pi.getLine(), 0, pi.getHeader().getLabelIndex())),
                                   LineParser::headerNext),

        /** Parser for merged files.
         * @since 12.0
         */
        HEADER_MERGED_FILE(pi -> pi.getHeader().matchFound(NavigationLabel.MERGED_FILE, pi.getLine()),
                           pi -> pi.getHeader().setMergedFiles(ParsingUtils.parseInt(pi.getLine(), 0, 9)),
                           LineParser::headerNext),

       /** Parser for the end of pi.getHeader(). */
        HEADER_END(pi -> pi.getHeader().matchFound(CommonLabel.END, pi.getLine()),
                   pi -> {
                       // get rinex format version
                       final RinexNavigationHeader header = pi.getHeader();
                       final double version = pi.getHeader().getFormatVersion();

                       // check mandatory header fields
                       if (header.getRunByName() == null ||
                           version >= 4 && pi.getHeader().getLeapSecondsGNSS() < 0) {
                           throw new OrekitException(OrekitMessages.INCOMPLETE_HEADER, pi.getName());
                       }

                       pi.setHeaderParsed(true);

                   },
                   LineParser::navigationNext),

        /** Parser for navigation message space vehicle epoch and clock. */
        NAVIGATION_SV_EPOCH_CLOCK_RINEX_2(pi -> true,
                                          pi -> {
                                              pi.setRecordLineParser(RecordType.ORBIT,
                                                                     pi.getHeader().getSatelliteSystem(),
                                                                     ParsingUtils.parseInt(pi.getLine(), 0, 2),
                                                                     null, null);
                                              pi.getRecordLineParser().parseLine00();
                                          },
                                          LineParser::navigationNext),

        /** Parser for line 0 of ephemeris records. */
        EPH_LINE_00(pi -> INITIALS.indexOf(pi.getLine().charAt(0)) >= 0,
                       pi -> {

                           if (pi.getHeader().getFormatVersion() < 4) {
                               final SatelliteSystem system =
                                   SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(pi.getLine(), 0, 1));
                               final int             prn     = ParsingUtils.parseInt(pi.getLine(), 2, 2);
                               pi.setRecordLineParser(RecordType.ORBIT, system, prn, null, null);
                           }

                           // Read first line
                           pi.getRecordLineParser().parseLine00();

                       },
                       LineParser::navigationNext),

        /** Parser for line 0 of other records. */
        RECORD_LINE_00(pi -> pi.getLine().charAt(0) == ' ',
                       pi -> pi.getRecordLineParser().parseLine00(),
                       LineParser::navigationNext),

        /** Parser for message lines. */
        MESSAGE_LINE(pi -> RecordType.ORBIT.matches(pi.getLine()),
                     ParseInfo::parseRecordLine,
                     LineParser::navigationNext),

        /** Parser for navigation message type. */
        EPH_TYPE(pi -> RecordType.EPH.matches(pi.getLine()),
                 pi -> pi.setRecordLineParser(RecordType.ORBIT),
                 pi -> Collections.singleton(EPH_LINE_00)),

        /** Parser for system time offset message type. */
        STO_TYPE(pi -> RecordType.STO.matches(pi.getLine()),
                 pi -> pi.setRecordLineParser(RecordType.STO),
                 pi -> Collections.singleton(RECORD_LINE_00)),

        /** Parser for Earth orientation parameter message type. */
        EOP_TYPE(pi -> RecordType.EOP.matches(pi.getLine()),
                 pi -> pi.setRecordLineParser(RecordType.EOP),
                 pi -> Collections.singleton(RECORD_LINE_00)),

        /** Parser for ionosphere message type. */
        IONO_TYPE(pi -> RecordType.ION.matches(pi.getLine()),
                  pi -> {
                      final SatelliteSystem system =
                          SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(pi.getLine(), 6, 1));
                      final int             prn     = ParsingUtils.parseInt(pi.getLine(), 7, 2);
                      final String          type    = ParsingUtils.parseString(pi.getLine(), 10, 4);
                      final String          subtype = ParsingUtils.parseString(pi.getLine(), 15, 4);
                      pi.setRecordLineParser(RecordType.ION, system, prn, type, subtype);
                  },
                  pi -> Collections.singleton(RECORD_LINE_00));

        /** Predicate for identifying lines that can be parsed. */
        private final Predicate<ParseInfo> canHandle;

        /** Parsing method. */
        private final Consumer<ParseInfo> parsingMethod;

        /** Provider for next line parsers. */
        private final Function<ParseInfo, Iterable<LineParser>> allowedNextProvider;

        /** Simple constructor.
         * @param canHandle predicate for identifying lines that can be parsed
         * @param parsingMethod parsing method
         * @param allowedNextProvider supplier for allowed parsers for next line
         */
        LineParser(final Predicate<ParseInfo> canHandle,
                   final Consumer<ParseInfo> parsingMethod,
                   final Function<ParseInfo, Iterable<LineParser>> allowedNextProvider) {
            this.canHandle           = canHandle;
            this.parsingMethod       = parsingMethod;
            this.allowedNextProvider = allowedNextProvider;
        }

        /** Get the allowed parsers for next lines while parsing Rinex pi.getHeader().
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> headerNext(final ParseInfo parseInfo) {
            if (parseInfo.getHeader().getFormatVersion() < 3) {
                // Rinex 2.x header entries
                return Arrays.asList(HEADER_COMMENT, HEADER_PROGRAM,
                                     HEADER_ION_ALPHA, HEADER_ION_BETA,
                                     HEADER_DELTA_UTC, HEADER_CORR_SYSTEM_TIME,
                                     HEADER_LEAP_SECONDS, HEADER_END);
            } else if (parseInfo.getHeader().getFormatVersion() < 4) {
                // Rinex 3.x header entries
                return Arrays.asList(HEADER_COMMENT, HEADER_PROGRAM,
                                     HEADER_IONOSPHERIC, HEADER_TIME,
                                     HEADER_LEAP_SECONDS, HEADER_END);
            } else {
                // Rinex 4.x header entries
                return Arrays.asList(HEADER_COMMENT, HEADER_PROGRAM,
                                     HEADER_DOI, HEADER_LICENSE, HEADER_STATION_INFORMATION, HEADER_MERGED_FILE,
                                     HEADER_LEAP_SECONDS, HEADER_END);
            }
        }

        /** Get the allowed parsers for next lines while parsing navigation date.
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> navigationNext(final ParseInfo parseInfo) {
            final RecordLineParser mlp = parseInfo.getRecordLineParser();
            if (mlp != null) {
                if (mlp instanceof GlonassFdmaParser) {
                    // workaround for some invalid files that should nevertheless be parsed
                    // we have encountered in the wild merged files that claimed to be in 3.05 version
                    // and hence needed at least 4 broadcast GLONASS orbit lines (the fourth line was
                    // introduced in 3.05), but in fact only had 3 broadcast lines. We think they were
                    // merged from files in 3.04 or earlier format. In order to parse these files,
                    // we accept after the third line either another broadcast orbit line or a new message
                    if (parseInfo.getRecordLineNumber() < 3) {
                        return Collections.singleton(MESSAGE_LINE);
                    } else {
                        if (parseInfo.getHeader().getFormatVersion() < 4) {
                            return Arrays.asList(MESSAGE_LINE, EPH_LINE_00);
                        } else {
                            return Arrays.asList(MESSAGE_LINE, EPH_TYPE, STO_TYPE, EOP_TYPE, IONO_TYPE);
                        }
                    }
                } else {
                    return Collections.singleton(MESSAGE_LINE);
                }
            } else if (parseInfo.getHeader().getFormatVersion() < 3) {
                return Collections.singleton(NAVIGATION_SV_EPOCH_CLOCK_RINEX_2);
            } else if (parseInfo.getHeader().getFormatVersion() < 4) {
                return Collections.singleton(EPH_LINE_00);
            } else {
                return Arrays.asList(EPH_TYPE, STO_TYPE, EOP_TYPE, IONO_TYPE);
            }
        }

    }

}