RinexNavigationParser.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.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.Function;
import java.util.function.Predicate;

import org.hipparchus.util.FastMath;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.rinex.utils.parsing.RinexUtils;
import org.orekit.gnss.Frequency;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.gnss.TimeSystem;
import org.orekit.propagation.analytical.gnss.data.AbstractNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.BeidouCivilianNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.BeidouLegacyNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.BeidouSatelliteType;
import org.orekit.propagation.analytical.gnss.data.CivilianNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.GLONASSNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.GPSCivilianNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.GPSLegacyNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.GalileoNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.IRNSSNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.LegacyNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.QZSSCivilianNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.QZSSLegacyNavigationMessage;
import org.orekit.propagation.analytical.gnss.data.SBASNavigationMessage;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.GNSSDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.utils.Constants;
import org.orekit.utils.units.Unit;

/**
 * Parser for RINEX navigation messages files.
 * <p>
 * This parser handles RINEX version from 2 to 4.00.
 * </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>
 *
 * @author Bryan Cazabonne
 * @since 11.0
 *
 */
public class RinexNavigationParser {

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

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

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

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

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

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

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

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

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

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

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

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

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

    /** 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 pi = new ParseInfo(source.getName());

        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()) {
                    ++pi.lineNumber;
                    for (final LineParser candidate : candidateParsers) {
                        if (candidate.canHandle.test(line)) {
                            try {
                                candidate.parsingMethod.parse(line, pi);
                                candidateParsers = candidate.allowedNextProvider.apply(pi);
                                continue nextLine;
                            } catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
                                throw new OrekitException(e,
                                                          OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                          pi.lineNumber, source.getName(), line);
                            }
                        }
                    }
                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                              pi.lineNumber, source.getName(), line);
                }
        }

        if (!pi.headerParsed) {
            throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE, source.getName());
        }

        pi.closePendingMessage();

        return pi.file;

    }

    /** Transient data used for parsing a RINEX navigation messages file. */
    private class ParseInfo {

        /** Name of the data source. */
        private final String name;

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

        /** The corresponding navigation messages file object. */
        private RinexNavigation file;

        /** Number of initial spaces in broadcase orbits lines. */
        private int initialSpaces;

        /** Flag indicating header has been completely parsed. */
        private boolean headerParsed;

        /** Flag indicating the distinction between "alpha" and "beta" ionospheric coefficients. */
        private boolean isIonosphereAlphaInitialized;

        /** Satellite system line parser. */
        private SatelliteSystemLineParser systemLineParser;

        /** Current global line number. */
        private int lineNumber;

        /** Current line number within the navigation message. */
        private int messageLineNumber;

        /** Container for GPS navigation message. */
        private GPSLegacyNavigationMessage gpsLNav;

        /** Container for GPS navigation message. */
        private GPSCivilianNavigationMessage gpsCNav;

        /** Container for Galileo navigation message. */
        private GalileoNavigationMessage galileoNav;

        /** Container for Beidou navigation message. */
        private BeidouLegacyNavigationMessage beidouLNav;

        /** Container for Beidou navigation message. */
        private BeidouCivilianNavigationMessage beidouCNav;

        /** Container for QZSS navigation message. */
        private QZSSLegacyNavigationMessage qzssLNav;

        /** Container for QZSS navigation message. */
        private QZSSCivilianNavigationMessage qzssCNav;

        /** Container for IRNSS navigation message. */
        private IRNSSNavigationMessage irnssNav;

        /** Container for GLONASS navigation message. */
        private GLONASSNavigationMessage glonassNav;

        /** Container for SBAS navigation message. */
        private SBASNavigationMessage sbasNav;

        /** Container for System Time Offset message. */
        private SystemTimeOffsetMessage sto;

        /** Container for Earth Orientation Parameter message. */
        private EarthOrientationParameterMessage eop;

        /** Container for ionosphere Klobuchar message. */
        private IonosphereKlobucharMessage klobuchar;

        /** Container for ionosphere Nequick-G message. */
        private IonosphereNequickGMessage nequickG;

        /** Container for ionosphere BDGIM message. */
        private IonosphereBDGIMMessage bdgim;

        /** Constructor, build the ParseInfo object.
         * @param name name of the data source
         */
        ParseInfo(final String name) {
            // Initialize default values for fields
            this.name                         = name;
            this.timeScales                   = RinexNavigationParser.this.timeScales;
            this.isIonosphereAlphaInitialized = false;
            this.file                         = new RinexNavigation();

        }

        /** Ensure navigation message has been closed.
         */
        void closePendingMessage() {
            if (systemLineParser != null) {
                systemLineParser.closeMessage(this);
                systemLineParser = null;
            }

        }

    }

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

        /** Parser for version, file type and satellite system. */
        HEADER_VERSION(line -> RinexUtils.matchesLabel(line, "RINEX VERSION / TYPE"),
                       (line, pi) -> {
                           RinexUtils.parseVersionFileTypeSatelliteSystem(line, pi.name, pi.file.getHeader(),
                                                                                    2.0, 2.01, 2.10, 2.11,
                                                                                    3.01, 3.02, 3.03, 3.04, 3.05,
                                                                                    4.00);
                           pi.initialSpaces = pi.file.getHeader().getFormatVersion() < 3.0 ? 3 : 4;
                       },
                       LineParser::headerNext),

        /** Parser for generating program and emitting agency. */
        HEADER_PROGRAM(line -> RinexUtils.matchesLabel(line, "PGM / RUN BY / DATE"),
                       (line, pi) -> RinexUtils.parseProgramRunByDate(line, pi.lineNumber, pi.name, pi.timeScales, pi.file.getHeader()),
                       LineParser::headerNext),

        /** Parser for comments. */
        HEADER_COMMENT(line -> RinexUtils.matchesLabel(line, "COMMENT"),
                       (line, pi) -> RinexUtils.parseComment(pi.lineNumber, line, pi.file),
                       LineParser::headerNext),

        /** Parser for ionospheric correction parameters. */
        HEADER_ION_ALPHA(line -> RinexUtils.matchesLabel(line, "ION ALPHA"),
                         (line, pi) -> {

                             pi.file.getHeader().setIonosphericCorrectionType(IonosphericCorrectionType.GPS);

                             // Read coefficients
                             final double[] parameters = new double[4];
                             parameters[0] = RinexUtils.parseDouble(line, 2,  12);
                             parameters[1] = RinexUtils.parseDouble(line, 14, 12);
                             parameters[2] = RinexUtils.parseDouble(line, 26, 12);
                             parameters[3] = RinexUtils.parseDouble(line, 38, 12);
                             pi.file.setKlobucharAlpha(parameters);
                             pi.isIonosphereAlphaInitialized = true;

                         },
                         LineParser::headerNext),

        /** Parser for ionospheric correction parameters. */
        HEADER_ION_BETA(line -> RinexUtils.matchesLabel(line, "ION BETA"),
                        (line, pi) -> {

                            pi.file.getHeader().setIonosphericCorrectionType(IonosphericCorrectionType.GPS);

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

                        },
                        LineParser::headerNext),

        /** Parser for ionospheric correction parameters. */
        HEADER_IONOSPHERIC(line -> RinexUtils.matchesLabel(line, "IONOSPHERIC CORR"),
                           (line, pi) -> {

                               // ionospheric correction type
                               final IonosphericCorrectionType ionoType =
                                               IonosphericCorrectionType.valueOf(RinexUtils.parseString(line, 0, 3));
                               pi.file.getHeader().setIonosphericCorrectionType(ionoType);

                               // Read coefficients
                               final double[] parameters = new double[4];
                               parameters[0] = RinexUtils.parseDouble(line, 5,  12);
                               parameters[1] = RinexUtils.parseDouble(line, 17, 12);
                               parameters[2] = RinexUtils.parseDouble(line, 29, 12);
                               parameters[3] = RinexUtils.parseDouble(line, 41, 12);

                               // Verify if we are parsing Galileo ionospheric parameters
                               if (ionoType == IonosphericCorrectionType.GAL) {

                                   // We are parsing Galileo ionospheric parameters
                                   pi.file.setNeQuickAlpha(parameters);

                               } else {
                                   // We are parsing Klobuchar ionospheric parameters

                                   // Verify if we are parsing "alpha" or "beta" ionospheric parameters
                                   if (pi.isIonosphereAlphaInitialized) {

                                       // Ionospheric "beta" parameters
                                       pi.file.setKlobucharBeta(parameters);

                                   } else {

                                       // Ionospheric "alpha" parameters
                                       pi.file.setKlobucharAlpha(parameters);

                                       // Set the flag to true
                                       pi.isIonosphereAlphaInitialized = true;

                                   }

                               }

                           },
                           LineParser::headerNext),

        /** Parser for corrections to transform the system time to UTC or to other time systems. */
        HEADER_DELTA_UTC(line -> RinexUtils.matchesLabel(line, "DELTA-UTC: A0,A1,T,W"),
                         (line, pi) -> {
                             // Read fields
                             final double a0      = RinexUtils.parseDouble(line, 3,  19);
                             final double a1      = RinexUtils.parseDouble(line, 22, 19);
                             final int    refTime = RinexUtils.parseInt(line, 41, 9);
                             final int    refWeek = RinexUtils.parseInt(line, 50, 9);

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

                             // Add to the list
                             final TimeSystemCorrection tsc = new TimeSystemCorrection("GPUT", date, a0, a1);
                             pi.file.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(line -> RinexUtils.matchesLabel(line, "CORR TO SYSTEM TIME"),
                         (line, pi) -> {
                             // Read fields
                             final int year        = RinexUtils.parseInt(line,  0, 6);
                             final int month       = RinexUtils.parseInt(line,  6, 6);
                             final int day         = RinexUtils.parseInt(line, 12, 6);
                             final double minusTau = RinexUtils.parseDouble(line, 21, 19);

                             // convert date
                             final SatelliteSystem satSystem = pi.file.getHeader().getSatelliteSystem();
                             final TimeScale       timeScale = satSystem.getObservationTimeScale().getTimeScale(pi.timeScales);
                             final AbsoluteDate    date      = new AbsoluteDate(year, month, day, timeScale);

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

                         },
                         LineParser::headerNext),

        /** Parser for corrections to transform the system time to UTC or to other time systems. */
        HEADER_TIME(line -> RinexUtils.matchesLabel(line, "TIME SYSTEM CORR"),
                    (line, pi) -> {

                        // Read fields
                        final String type    = RinexUtils.parseString(line, 0,  4);
                        final double a0      = RinexUtils.parseDouble(line, 5,  17);
                        final double a1      = RinexUtils.parseDouble(line, 22, 16);
                        final int    refTime = RinexUtils.parseInt(line, 38, 7);
                        final int    refWeek = RinexUtils.parseInt(line, 46, 5);

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

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

                    },
                    LineParser::headerNext),

        /** Parser for leap seconds. */
        HEADER_LEAP_SECONDS(line -> RinexUtils.matchesLabel(line, "LEAP SECONDS"),
                            (line, pi) -> pi.file.getHeader().setNumberOfLeapSeconds(RinexUtils.parseInt(line, 0, 6)),
                            LineParser::headerNext),

        /** Parser for DOI.
         * @since 12.0
         */
        HEADER_DOI(line -> RinexUtils.matchesLabel(line, "DOI"),
                   (line, pi) -> pi.file.getHeader().setDoi(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
                   LineParser::headerNext),

        /** Parser for license.
         * @since 12.0
         */
        HEADER_LICENSE(line -> RinexUtils.matchesLabel(line, "LICENSE OF USE"),
                       (line, pi) -> pi.file.getHeader().setLicense(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
                       LineParser::headerNext),

        /** Parser for stationInformation.
         * @since 12.0
         */
        HEADER_STATION_INFORMATION(line -> RinexUtils.matchesLabel(line, "STATION INFORMATION"),
                                   (line, pi) -> pi.file.getHeader().setStationInformation(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
                                   LineParser::headerNext),

        /** Parser for merged files.
         * @since 12.0
         */
        HEADER_MERGED_FILE(line -> RinexUtils.matchesLabel(line, "MERGED FILE"),
                           (line, pi) -> pi.file.getHeader().setMergedFiles(RinexUtils.parseInt(line, 0, 9)),
                           LineParser::headerNext),

       /** Parser for the end of header. */
        HEADER_END(line -> RinexUtils.matchesLabel(line, "END OF HEADER"),
                   (line, pi) -> {
                       // get rinex format version
                       final RinexNavigationHeader header = pi.file.getHeader();
                       final double version = header.getFormatVersion();

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

                       pi.headerParsed = true;

                   },
                   LineParser::navigationNext),

        /** Parser for navigation message space vehicle epoch and clock. */
        NAVIGATION_SV_EPOCH_CLOCK_RINEX_2(line -> true,
                                          (line, pi) -> {

                                              // Set the line number to 0
                                              pi.messageLineNumber = 0;

                                              // Initialize parser
                                              pi.closePendingMessage();
                                              pi.systemLineParser = SatelliteSystemLineParser.getParser(pi.file.getHeader().getSatelliteSystem(),
                                                                                                        null, pi, line);

                                              pi.systemLineParser.parseSvEpochSvClockLine(line, pi);

                                          },
                                          LineParser::navigationNext),

        /** Parser for navigation message space vehicle epoch and clock. */
        NAVIGATION_SV_EPOCH_CLOCK(line -> INITIALS.indexOf(line.charAt(0)) >= 0,
                                  (line, pi) -> {

                                      // Set the line number to 0
                                      pi.messageLineNumber = 0;

                                      if (pi.file.getHeader().getFormatVersion() < 4) {
                                          // Current satellite system
                                          final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1));

                                          // Initialize parser
                                          pi.closePendingMessage();
                                          pi.systemLineParser = SatelliteSystemLineParser.getParser(system, null, pi, line);
                                      }

                                      // Read first line
                                      pi.systemLineParser.parseSvEpochSvClockLine(line, pi);

                                  },
                                  LineParser::navigationNext),

        /** Parser for navigation message type. */
        EPH_TYPE(line -> line.startsWith("> EPH"),
                 (line, pi) -> {
                     final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 6, 1));
                     final String          type   = RinexUtils.parseString(line, 10, 4);
                     pi.closePendingMessage();
                     pi.systemLineParser = SatelliteSystemLineParser.getParser(system, type, pi, line);
                 },
                 pi -> Collections.singleton(NAVIGATION_SV_EPOCH_CLOCK)),

        /** Parser for broadcast orbit. */
        BROADCAST_ORBIT(line -> line.startsWith("   "),
                        (line, pi) -> {
                            switch (++pi.messageLineNumber) {
                                case 1: pi.systemLineParser.parseFirstBroadcastOrbit(line, pi);
                                break;
                                case 2: pi.systemLineParser.parseSecondBroadcastOrbit(line, pi);
                                break;
                                case 3: pi.systemLineParser.parseThirdBroadcastOrbit(line, pi);
                                break;
                                case 4: pi.systemLineParser.parseFourthBroadcastOrbit(line, pi);
                                break;
                                case 5: pi.systemLineParser.parseFifthBroadcastOrbit(line, pi);
                                break;
                                case 6: pi.systemLineParser.parseSixthBroadcastOrbit(line, pi);
                                break;
                                case 7: pi.systemLineParser.parseSeventhBroadcastOrbit(line, pi);
                                break;
                                case 8: pi.systemLineParser.parseEighthBroadcastOrbit(line, pi);
                                break;
                                case 9: pi.systemLineParser.parseNinthBroadcastOrbit(line, pi);
                                break;
                                default:
                                    // this should never happen
                                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                              pi.lineNumber, pi.name, line);
                            }

                        },
                        LineParser::navigationNext),

        /** Parser for system time offset message model. */
        STO_LINE_1(line -> true,
                   (line, pi) -> {
                       pi.sto.setTransmissionTime(Unit.SECOND.toSI(RinexUtils.parseDouble(line,  4, 19)));
                       pi.sto.setA0(Unit.SECOND.toSI(RinexUtils.parseDouble(line, 23, 19)));
                       pi.sto.setA1(S_PER_S.toSI(RinexUtils.parseDouble(line, 42, 19)));
                       pi.sto.setA2(S_PER_S2.toSI(RinexUtils.parseDouble(line, 61, 19)));
                       pi.file.addSystemTimeOffset(pi.sto);
                       pi.sto = null;
                   },
                   LineParser::navigationNext),

        /** Parser for system time offset message space vehicle epoch and clock. */
        STO_SV_EPOCH_CLOCK(line -> true,
                           (line, pi) -> {

                               pi.sto.setDefinedTimeSystem(TimeSystem.parseTwoLettersCode(RinexUtils.parseString(line, 24, 2)));
                               pi.sto.setReferenceTimeSystem(TimeSystem.parseTwoLettersCode(RinexUtils.parseString(line, 26, 2)));
                               final String sbas = RinexUtils.parseString(line, 43, 18);
                               pi.sto.setSbasId(sbas.length() > 0 ? SbasId.valueOf(sbas) : null);
                               final String utc = RinexUtils.parseString(line, 62, 18);
                               pi.sto.setUtcId(utc.length() > 0 ? UtcId.parseUtcId(utc) : null);

                               // TODO is the reference date relative to one or the other time scale?
                               final int year  = RinexUtils.parseInt(line, 4, 4);
                               final int month = RinexUtils.parseInt(line, 9, 2);
                               final int day   = RinexUtils.parseInt(line, 12, 2);
                               final int hours = RinexUtils.parseInt(line, 15, 2);
                               final int min   = RinexUtils.parseInt(line, 18, 2);
                               final int sec   = RinexUtils.parseInt(line, 21, 2);
                               pi.sto.setReferenceEpoch(new AbsoluteDate(year, month, day, hours, min, sec,
                                                                         pi.sto.getDefinedTimeSystem().getTimeScale(pi.timeScales)));

                           },
                           pi -> Collections.singleton(STO_LINE_1)),

        /** Parser for system time offset message type. */
        STO_TYPE(line -> line.startsWith("> STO"),
                 (line, pi) -> {
                     pi.closePendingMessage();
                     pi.sto = new SystemTimeOffsetMessage(SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 6, 1)),
                                                          RinexUtils.parseInt(line, 7, 2),
                                                          RinexUtils.parseString(line, 10, 4));
                 },
                 pi -> Collections.singleton(STO_SV_EPOCH_CLOCK)),

        /** Parser for Earth orientation parameter message model. */
        EOP_LINE_2(line -> true,
                   (line, pi) -> {
                       pi.eop.setTransmissionTime(Unit.SECOND.toSI(RinexUtils.parseDouble(line,  4, 19)));
                       pi.eop.setDut1(Unit.SECOND.toSI(RinexUtils.parseDouble(line, 23, 19)));
                       pi.eop.setDut1Dot(S_PER_DAY.toSI(RinexUtils.parseDouble(line, 42, 19)));
                       pi.eop.setDut1DotDot(S_PER_DAY2.toSI(RinexUtils.parseDouble(line, 61, 19)));
                       pi.file.addEarthOrientationParameter(pi.eop);
                       pi.eop = null;
                   },
                   LineParser::navigationNext),

        /** Parser for Earth orientation parameter message model. */
        EOP_LINE_1(line -> true,
                   (line, pi) -> {
                       pi.eop.setYp(Unit.ARC_SECOND.toSI(RinexUtils.parseDouble(line, 23, 19)));
                       pi.eop.setYpDot(AS_PER_DAY.toSI(RinexUtils.parseDouble(line, 42, 19)));
                       pi.eop.setYpDotDot(AS_PER_DAY2.toSI(RinexUtils.parseDouble(line, 61, 19)));
                   },
                   pi -> Collections.singleton(EOP_LINE_2)),

        /** Parser for Earth orientation parameter message space vehicle epoch and clock. */
        EOP_SV_EPOCH_CLOCK(line -> true,
                           (line, pi) -> {
                               final int year  = RinexUtils.parseInt(line, 4, 4);
                               final int month = RinexUtils.parseInt(line, 9, 2);
                               final int day   = RinexUtils.parseInt(line, 12, 2);
                               final int hours = RinexUtils.parseInt(line, 15, 2);
                               final int min   = RinexUtils.parseInt(line, 18, 2);
                               final int sec   = RinexUtils.parseInt(line, 21, 2);
                               pi.eop.setReferenceEpoch(new AbsoluteDate(year, month, day, hours, min, sec,
                                                                         pi.eop.getSystem().getObservationTimeScale().getTimeScale(pi.timeScales)));
                               pi.eop.setXp(Unit.ARC_SECOND.toSI(RinexUtils.parseDouble(line, 23, 19)));
                               pi.eop.setXpDot(AS_PER_DAY.toSI(RinexUtils.parseDouble(line, 42, 19)));
                               pi.eop.setXpDotDot(AS_PER_DAY2.toSI(RinexUtils.parseDouble(line, 61, 19)));
                           },
                           pi -> Collections.singleton(EOP_LINE_1)),

        /** Parser for Earth orientation parameter message type. */
        EOP_TYPE(line -> line.startsWith("> EOP"),
                 (line, pi) -> {
                     pi.closePendingMessage();
                     pi.eop = new EarthOrientationParameterMessage(SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 6, 1)),
                                                                   RinexUtils.parseInt(line, 7, 2),
                                                                   RinexUtils.parseString(line, 10, 4));
                 },
                 pi -> Collections.singleton(EOP_SV_EPOCH_CLOCK)),

        /** Parser for ionosphere Klobuchar message model. */
        KLOBUCHAR_LINE_2(line -> true,
                         (line, pi) -> {
                             pi.klobuchar.setBetaI(3, IonosphereKlobucharMessage.S_PER_SC_N[3].toSI(RinexUtils.parseDouble(line,  4, 19)));
                             pi.klobuchar.setRegionCode(RinexUtils.parseDouble(line, 23, 19) < 0.5 ?
                                                        RegionCode.WIDE_AREA : RegionCode.JAPAN);
                             pi.file.addKlobucharMessage(pi.klobuchar);
                             pi.klobuchar = null;
                         },
                         LineParser::navigationNext),

        /** Parser for ionosphere Klobuchar message model. */
        KLOBUCHAR_LINE_1(line -> true,
                         (line, pi) -> {
                             pi.klobuchar.setAlphaI(3, IonosphereKlobucharMessage.S_PER_SC_N[3].toSI(RinexUtils.parseDouble(line,  4, 19)));
                             pi.klobuchar.setBetaI(0, IonosphereKlobucharMessage.S_PER_SC_N[0].toSI(RinexUtils.parseDouble(line, 23, 19)));
                             pi.klobuchar.setBetaI(1, IonosphereKlobucharMessage.S_PER_SC_N[1].toSI(RinexUtils.parseDouble(line, 42, 19)));
                             pi.klobuchar.setBetaI(2, IonosphereKlobucharMessage.S_PER_SC_N[2].toSI(RinexUtils.parseDouble(line, 61, 19)));
                         },
                         pi -> Collections.singleton(KLOBUCHAR_LINE_2)),

        /** Parser for ionosphere Klobuchar message model. */
        KLOBUCHAR_LINE_0(line -> true,
                         (line, pi) -> {
                             final int year  = RinexUtils.parseInt(line, 4, 4);
                             final int month = RinexUtils.parseInt(line, 9, 2);
                             final int day   = RinexUtils.parseInt(line, 12, 2);
                             final int hours = RinexUtils.parseInt(line, 15, 2);
                             final int min   = RinexUtils.parseInt(line, 18, 2);
                             final int sec   = RinexUtils.parseInt(line, 21, 2);
                             pi.klobuchar.setTransmitTime(new AbsoluteDate(year, month, day, hours, min, sec,
                                                                           pi.klobuchar.getSystem().getObservationTimeScale().getTimeScale(pi.timeScales)));
                             pi.klobuchar.setAlphaI(0, IonosphereKlobucharMessage.S_PER_SC_N[0].toSI(RinexUtils.parseDouble(line, 23, 19)));
                             pi.klobuchar.setAlphaI(1, IonosphereKlobucharMessage.S_PER_SC_N[1].toSI(RinexUtils.parseDouble(line, 42, 19)));
                             pi.klobuchar.setAlphaI(2, IonosphereKlobucharMessage.S_PER_SC_N[2].toSI(RinexUtils.parseDouble(line, 61, 19)));
                         },
                         pi -> Collections.singleton(KLOBUCHAR_LINE_1)),

        /** Parser for ionosphere Nequick-G message model. */
        NEQUICK_LINE_1(line -> true,
                       (line, pi) -> {
                           pi.nequickG.setFlags((int) FastMath.rint(RinexUtils.parseDouble(line, 4, 19)));
                           pi.file.addNequickGMessage(pi.nequickG);
                           pi.nequickG = null;
                       },
                       LineParser::navigationNext),

        /** Parser for ionosphere Nequick-G message model. */
        NEQUICK_LINE_0(line -> true,
                       (line, pi) -> {
                           final int year  = RinexUtils.parseInt(line, 4, 4);
                           final int month = RinexUtils.parseInt(line, 9, 2);
                           final int day   = RinexUtils.parseInt(line, 12, 2);
                           final int hours = RinexUtils.parseInt(line, 15, 2);
                           final int min   = RinexUtils.parseInt(line, 18, 2);
                           final int sec   = RinexUtils.parseInt(line, 21, 2);
                           pi.nequickG.setTransmitTime(new AbsoluteDate(year, month, day, hours, min, sec,
                                                                        pi.nequickG.getSystem().getObservationTimeScale().getTimeScale(pi.timeScales)));
                           pi.nequickG.setAi0(IonosphereNequickGMessage.SFU.toSI(RinexUtils.parseDouble(line, 23, 19)));
                           pi.nequickG.setAi1(IonosphereNequickGMessage.SFU_PER_DEG.toSI(RinexUtils.parseDouble(line, 42, 19)));
                           pi.nequickG.setAi2(IonosphereNequickGMessage.SFU_PER_DEG2.toSI(RinexUtils.parseDouble(line, 61, 19)));
                       },
                       pi -> Collections.singleton(NEQUICK_LINE_1)),

        /** Parser for ionosphere BDGIM message model. */
        BDGIM_LINE_2(line -> true,
                     (line, pi) -> {
                         pi.bdgim.setAlphaI(7, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line,  4, 19)));
                         pi.bdgim.setAlphaI(8, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 23, 19)));
                         pi.file.addBDGIMMessage(pi.bdgim);
                         pi.bdgim = null;
                     },
                     LineParser::navigationNext),

        /** Parser for ionosphere BDGIM message model. */
        BDGIM_LINE_1(line -> true,
                     (line, pi) -> {
                         pi.bdgim.setAlphaI(3, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line,  4, 19)));
                         pi.bdgim.setAlphaI(4, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 23, 19)));
                         pi.bdgim.setAlphaI(5, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 42, 19)));
                         pi.bdgim.setAlphaI(6, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 61, 19)));
                     },
                     pi -> Collections.singleton(BDGIM_LINE_2)),

        /** Parser for ionosphere BDGIM message model. */
        BDGIM_LINE_0(line -> true,
                     (line, pi) -> {
                         final int year  = RinexUtils.parseInt(line, 4, 4);
                         final int month = RinexUtils.parseInt(line, 9, 2);
                         final int day   = RinexUtils.parseInt(line, 12, 2);
                         final int hours = RinexUtils.parseInt(line, 15, 2);
                         final int min   = RinexUtils.parseInt(line, 18, 2);
                         final int sec   = RinexUtils.parseInt(line, 21, 2);
                         pi.bdgim.setTransmitTime(new AbsoluteDate(year, month, day, hours, min, sec,
                                                                   pi.bdgim.getSystem().getObservationTimeScale().getTimeScale(pi.timeScales)));
                         pi.bdgim.setAlphaI(0, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 23, 19)));
                         pi.bdgim.setAlphaI(1, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 42, 19)));
                         pi.bdgim.setAlphaI(2, Unit.TOTAL_ELECTRON_CONTENT_UNIT.toSI(RinexUtils.parseDouble(line, 61, 19)));
                     },
                     pi -> Collections.singleton(BDGIM_LINE_1)),

        /** Parser for ionosphere message type. */
        IONO_TYPE(line -> line.startsWith("> ION"),
                  (line, pi) -> {
                      pi.closePendingMessage();
                      final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 6, 1));
                      final int             prn    = RinexUtils.parseInt(line, 7, 2);
                      final String          type   = RinexUtils.parseString(line, 10, 4);
                      if (system == SatelliteSystem.GALILEO) {
                          pi.nequickG = new IonosphereNequickGMessage(system, prn, type);
                      } else {
                          // in Rinex 4.00, tables A32 and A34 are ambiguous as both seem to apply
                          // to Beidou CNVX messages, we consider BDGIM is the proper model in this case
                          if (system == SatelliteSystem.BEIDOU && "CNVX".equals(type)) {
                              pi.bdgim = new IonosphereBDGIMMessage(system, prn, type);
                          } else {
                              pi.klobuchar = new IonosphereKlobucharMessage(system, prn, type);
                          }
                      }
                  },
                  pi -> Collections.singleton(pi.nequickG != null ? NEQUICK_LINE_0 : (pi.bdgim != null ? BDGIM_LINE_0 : KLOBUCHAR_LINE_0)));

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

        /** Parsing method. */
        private final ParsingMethod 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<String> canHandle, final ParsingMethod 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 header.
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> headerNext(final ParseInfo parseInfo) {
            if (parseInfo.file.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.file.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) {
            if (parseInfo.gpsLNav    != null || parseInfo.gpsCNav    != null || parseInfo.galileoNav != null ||
                parseInfo.beidouLNav != null || parseInfo.beidouCNav != null || parseInfo.qzssLNav   != null ||
                parseInfo.qzssCNav   != null || parseInfo.irnssNav   != null || parseInfo.sbasNav    != null) {
                return Collections.singleton(BROADCAST_ORBIT);
            } else if (parseInfo.glonassNav != null) {
                if (parseInfo.messageLineNumber < 3) {
                    return Collections.singleton(BROADCAST_ORBIT);
                } else {
                    // 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.file.getHeader().getFormatVersion() < 4) {
                        return Arrays.asList(BROADCAST_ORBIT, NAVIGATION_SV_EPOCH_CLOCK);
                    } else {
                        return Arrays.asList(BROADCAST_ORBIT, EPH_TYPE, STO_TYPE, EOP_TYPE, IONO_TYPE);
                    }
                }
            } else if (parseInfo.file.getHeader().getFormatVersion() < 3) {
                return Collections.singleton(NAVIGATION_SV_EPOCH_CLOCK_RINEX_2);
            } else if (parseInfo.file.getHeader().getFormatVersion() < 4) {
                return Collections.singleton(NAVIGATION_SV_EPOCH_CLOCK);
            } else {
                return Arrays.asList(EPH_TYPE, STO_TYPE, EOP_TYPE, IONO_TYPE);
            }
        }

    }

    /** Parsers for satellite system specific lines. */
    private enum SatelliteSystemLineParser {

        /** GPS legacy. */
        GPS_LNAV() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                if (pi.file.getHeader().getFormatVersion() < 3.0) {
                    parseSvEpochSvClockLineRinex2(line, pi.timeScales.getGPS(), pi.gpsLNav);
                } else {
                    parseSvEpochSvClockLine(line, pi.timeScales.getGPS(), pi.gpsLNav);
                }
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsLNav.setIODE(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.gpsLNav.setCrs(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.METRE));
                pi.gpsLNav.setDeltaN(parseBroadcastDouble3(line, pi.initialSpaces, RAD_PER_S));
                pi.gpsLNav.setM0(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsLNav.setCuc(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.gpsLNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.gpsLNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.gpsLNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsLNav.setTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.gpsLNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.gpsLNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.gpsLNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsLNav.setI0(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.gpsLNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.gpsLNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.gpsLNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                // iDot
                pi.gpsLNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                // Codes on L2 channel (ignored)
                // RinexUtils.parseDouble(line, 23, 19)
                // GPS week (to go with Toe)
                pi.gpsLNav.setWeek((int) RinexUtils.parseDouble(line, 42, 19));
                pi.gpsLNav.setDate(new GNSSDate(pi.gpsLNav.getWeek(),
                                               pi.gpsLNav.getTime(),
                                               SatelliteSystem.GPS,
                                               pi.timeScales).getDate());
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsLNav.setSvAccuracy(parseBroadcastDouble1(line, pi.initialSpaces, Unit.METRE));
                pi.gpsLNav.setSvHealth(parseBroadcastInt2(line, pi.initialSpaces));
                pi.gpsLNav.setTGD(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.SECOND));
                pi.gpsLNav.setIODC(parseBroadcastInt4(line,     pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsLNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.gpsLNav.setFitInterval(parseBroadcastInt2(line, pi.initialSpaces));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addGPSLegacyNavigationMessage(pi.gpsLNav);
                pi.gpsLNav = null;
            }

        },

        /** GPS civilian.
         * @since 12.0
         */
        GPS_CNAV() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getGPS(), pi.gpsCNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setADot(parseBroadcastDouble1(line, pi.initialSpaces, M_PER_S));
                pi.gpsCNav.setCrs(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.METRE));
                pi.gpsCNav.setDeltaN(parseBroadcastDouble3(line, pi.initialSpaces, RAD_PER_S));
                pi.gpsCNav.setM0(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setCuc(parseBroadcastDouble1(line,   pi.initialSpaces, Unit.RADIAN));
                pi.gpsCNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.gpsCNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.gpsCNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setTime(parseBroadcastDouble1(line,   pi.initialSpaces, Unit.SECOND));
                pi.gpsCNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.gpsCNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.gpsCNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setI0(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.gpsCNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.gpsCNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.gpsCNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                pi.gpsCNav.setDeltaN0Dot(parseBroadcastDouble2(line, pi.initialSpaces, RAD_PER_S2));
                pi.gpsCNav.setUraiNed0(parseBroadcastInt3(line, pi.initialSpaces));
                pi.gpsCNav.setUraiNed1(parseBroadcastInt4(line, pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setUraiEd(parseBroadcastInt1(line, pi.initialSpaces));
                pi.gpsCNav.setSvHealth(parseBroadcastInt2(line, pi.initialSpaces));
                pi.gpsCNav.setTGD(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                pi.gpsCNav.setUraiNed2(parseBroadcastInt4(line, pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.gpsCNav.setIscL1CA(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.gpsCNav.setIscL2C(parseBroadcastDouble2(line,  pi.initialSpaces, Unit.SECOND));
                pi.gpsCNav.setIscL5I5(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                pi.gpsCNav.setIscL5Q5(parseBroadcastDouble4(line, pi.initialSpaces, Unit.SECOND));
            }

            /** {@inheritDoc} */
            @Override
            public void parseEighthBroadcastOrbit(final String line, final ParseInfo pi) {
                if (pi.gpsCNav.isCnv2()) {
                    // in CNAV2 messages, there is an additional line for L1 CD and L1 CP inter signal delay
                    pi.gpsCNav.setIscL1CD(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                    pi.gpsCNav.setIscL1CP(parseBroadcastDouble2(line, pi.initialSpaces, Unit.SECOND));
                } else {
                    parseTransmissionTimeLine(line, pi);
                }
            }

            /** {@inheritDoc} */
            @Override
            public void parseNinthBroadcastOrbit(final String line, final ParseInfo pi) {
                parseTransmissionTimeLine(line, pi);
            }

            /** Parse transmission time line.
             * @param line line to parse
             * @param pi holder for transient data
             */
            private void parseTransmissionTimeLine(final String line, final ParseInfo pi) {
                pi.gpsCNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addGPSLegacyNavigationMessage(pi.gpsCNav);
                pi.gpsCNav = null;
            }

        },

        /** Galileo. */
        GALILEO() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getGPS(), pi.galileoNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.galileoNav.setIODNav(parseBroadcastInt1(line, pi.initialSpaces));
                pi.galileoNav.setCrs(parseBroadcastDouble2(line,       pi.initialSpaces, Unit.METRE));
                pi.galileoNav.setDeltaN(parseBroadcastDouble3(line,    pi.initialSpaces, RAD_PER_S));
                pi.galileoNav.setM0(parseBroadcastDouble4(line,        pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.galileoNav.setCuc(parseBroadcastDouble1(line,   pi.initialSpaces, Unit.RADIAN));
                pi.galileoNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.galileoNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.galileoNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.galileoNav.setTime(parseBroadcastDouble1(line,   pi.initialSpaces, Unit.SECOND));
                pi.galileoNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.galileoNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.galileoNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.galileoNav.setI0(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.galileoNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.galileoNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.galileoNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                // iDot
                pi.galileoNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                pi.galileoNav.setDataSource(parseBroadcastInt2(line, pi.initialSpaces));
                // GAL week (to go with Toe)
                pi.galileoNav.setWeek(parseBroadcastInt3(line, pi.initialSpaces));
                pi.galileoNav.setDate(new GNSSDate(pi.galileoNav.getWeek(),
                                                   pi.galileoNav.getTime(),
                                                   SatelliteSystem.GPS, // in Rinex files, week number is aligned to GPS week!
                                                   pi.timeScales).getDate());
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.galileoNav.setSisa(parseBroadcastDouble1(line, pi.initialSpaces, Unit.METRE));
                pi.galileoNav.setSvHealth(parseBroadcastDouble2(line, pi.initialSpaces, Unit.NONE));
                pi.galileoNav.setBGDE1E5a(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                pi.galileoNav.setBGDE5bE1(parseBroadcastDouble4(line, pi.initialSpaces, Unit.SECOND));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.galileoNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addGalileoNavigationMessage(pi.galileoNav);
                pi.galileoNav = null;
            }

        },

        /** Glonass. */
        GLONASS() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {

                if (pi.file.getHeader().getFormatVersion() < 3.0) {

                    pi.glonassNav.setPRN(RinexUtils.parseInt(line, 0, 2));

                    // Toc
                    final int    year  = RinexUtils.convert2DigitsYear(RinexUtils.parseInt(line,  3, 2));
                    final int    month = RinexUtils.parseInt(line,  6, 2);
                    final int    day   = RinexUtils.parseInt(line,  9, 2);
                    final int    hours = RinexUtils.parseInt(line, 12, 2);
                    final int    min   = RinexUtils.parseInt(line, 15, 2);
                    final double sec   = RinexUtils.parseDouble(line, 17, 5);
                    pi.glonassNav.setEpochToc(new AbsoluteDate(year, month, day, hours, min, sec,
                                                               pi.timeScales.getUTC()));

                    // clock
                    pi.glonassNav.setTauN(-RinexUtils.parseDouble(line, 22, 19));
                    pi.glonassNav.setGammaN(RinexUtils.parseDouble(line, 41, 19));
                    pi.glonassNav.setTime(fmod(RinexUtils.parseDouble(line, 60, 19), Constants.JULIAN_DAY));

                    // Set the ephemeris epoch (same as time of clock epoch)
                    pi.glonassNav.setDate(pi.glonassNav.getEpochToc());

                } else {
                    pi.glonassNav.setPRN(RinexUtils.parseInt(line, 1, 2));

                    // Toc
                    pi.glonassNav.setEpochToc(parsePrnSvEpochClock(line, pi.timeScales.getUTC()));

                    // clock
                    pi.glonassNav.setTauN(-RinexUtils.parseDouble(line, 23, 19));
                    pi.glonassNav.setGammaN(RinexUtils.parseDouble(line, 42, 19));
                    pi.glonassNav.setTime(fmod(RinexUtils.parseDouble(line, 61, 19), Constants.JULIAN_DAY));

                    // Set the ephemeris epoch (same as time of clock epoch)
                    pi.glonassNav.setDate(pi.glonassNav.getEpochToc());
                }

            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.glonassNav.setX(parseBroadcastDouble1(line, pi.initialSpaces, KM));
                pi.glonassNav.setXDot(parseBroadcastDouble2(line,    pi.initialSpaces, KM_PER_S));
                pi.glonassNav.setXDotDot(parseBroadcastDouble3(line, pi.initialSpaces, KM_PER_S2));
                pi.glonassNav.setHealth(parseBroadcastDouble4(line,  pi.initialSpaces, Unit.NONE));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.glonassNav.setY(parseBroadcastDouble1(line, pi.initialSpaces, KM));
                pi.glonassNav.setYDot(parseBroadcastDouble2(line,            pi.initialSpaces, KM_PER_S));
                pi.glonassNav.setYDotDot(parseBroadcastDouble3(line,         pi.initialSpaces, KM_PER_S2));
                pi.glonassNav.setFrequencyNumber(parseBroadcastDouble4(line, pi.initialSpaces, Unit.NONE));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.glonassNav.setZ(parseBroadcastDouble1(line, pi.initialSpaces, KM));
                pi.glonassNav.setZDot(parseBroadcastDouble2(line,    pi.initialSpaces, KM_PER_S));
                pi.glonassNav.setZDotDot(parseBroadcastDouble3(line, pi.initialSpaces, KM_PER_S2));
                if (pi.file.getHeader().getFormatVersion() < 3.045) {
                    pi.closePendingMessage();
                }
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.glonassNav.setStatusFlags(parseBroadcastDouble1(line, pi.initialSpaces, Unit.NONE));
                pi.glonassNav.setGroupDelayDifference(parseBroadcastDouble2(line, pi.initialSpaces, Unit.NONE));
                pi.glonassNav.setURA(parseBroadcastDouble3(line,                  pi.initialSpaces, Unit.NONE));
                pi.glonassNav.setHealthFlags(parseBroadcastDouble4(line,          pi.initialSpaces, Unit.NONE));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addGlonassNavigationMessage(pi.glonassNav);
                pi.glonassNav = null;
            }

        },

        /** QZSS legacy. */
        QZSS_LNAV() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getGPS(), pi.qzssLNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssLNav.setIODE(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssLNav.setCrs(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.METRE));
                pi.qzssLNav.setDeltaN(parseBroadcastDouble3(line, pi.initialSpaces, RAD_PER_S));
                pi.qzssLNav.setM0(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssLNav.setCuc(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.qzssLNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.qzssLNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.qzssLNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssLNav.setTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssLNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.qzssLNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.qzssLNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssLNav.setI0(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.qzssLNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.qzssLNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.qzssLNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                // iDot
                pi.qzssLNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                // Codes on L2 channel (ignored)
                // RinexUtils.parseDouble(line, 23, 19)
                // GPS week (to go with Toe)
                pi.qzssLNav.setWeek(parseBroadcastInt3(line, pi.initialSpaces));
                pi.qzssLNav.setDate(new GNSSDate(pi.qzssLNav.getWeek(),
                                                 pi.qzssLNav.getTime(),
                                                 SatelliteSystem.GPS, // in Rinex files, week number is aligned to GPS week!
                                                 pi.timeScales).getDate());
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssLNav.setSvAccuracy(parseBroadcastDouble1(line,  pi.initialSpaces, Unit.METRE));
                pi.qzssLNav.setSvHealth(parseBroadcastInt2(line, pi.initialSpaces));
                pi.qzssLNav.setTGD(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.SECOND));
                pi.qzssLNav.setIODC(parseBroadcastInt4(line,     pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssLNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssLNav.setFitInterval(parseBroadcastInt2(line, pi.initialSpaces));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addQZSSLegacyNavigationMessage(pi.qzssLNav);
                pi.qzssLNav = null;
            }

        },

        /** QZSS civilian.
         * @since 12.0
         */
        QZSS_CNAV() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getGPS(), pi.qzssCNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setADot(parseBroadcastDouble1(line, pi.initialSpaces, M_PER_S));
                pi.qzssCNav.setCrs(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.METRE));
                pi.qzssCNav.setDeltaN(parseBroadcastDouble3(line, pi.initialSpaces, RAD_PER_S));
                pi.qzssCNav.setM0(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setCuc(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.qzssCNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.qzssCNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.qzssCNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssCNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.qzssCNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.qzssCNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setI0(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.qzssCNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.qzssCNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.qzssCNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                pi.qzssCNav.setDeltaN0Dot(parseBroadcastDouble2(line, pi.initialSpaces, RAD_PER_S2));
                pi.qzssCNav.setUraiNed0(parseBroadcastInt3(line, pi.initialSpaces));
                pi.qzssCNav.setUraiNed1(parseBroadcastInt4(line, pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setUraiEd(parseBroadcastInt1(line, pi.initialSpaces));
                pi.qzssCNav.setSvHealth(parseBroadcastInt2(line, pi.initialSpaces));
                pi.qzssCNav.setTGD(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssCNav.setUraiNed2(parseBroadcastInt4(line, pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.qzssCNav.setIscL1CA(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssCNav.setIscL2C(parseBroadcastDouble2(line,  pi.initialSpaces, Unit.SECOND));
                pi.qzssCNav.setIscL5I5(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                pi.qzssCNav.setIscL5Q5(parseBroadcastDouble4(line, pi.initialSpaces, Unit.SECOND));
            }

            /** {@inheritDoc} */
            @Override
            public void parseEighthBroadcastOrbit(final String line, final ParseInfo pi) {
                if (pi.qzssCNav.isCnv2()) {
                    // in CNAV2 messages, there is an additional line for L1 CD and L1 CP inter signal delay
                    pi.qzssCNav.setIscL1CD(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                    pi.qzssCNav.setIscL1CP(parseBroadcastDouble2(line, pi.initialSpaces, Unit.SECOND));
                } else {
                    parseTransmissionTimeLine(line, pi);
                }
            }

            /** {@inheritDoc} */
            @Override
            public void parseNinthBroadcastOrbit(final String line, final ParseInfo pi) {
                parseTransmissionTimeLine(line, pi);
            }

            /** Parse transmission time line.
             * @param line line to parse
             * @param pi holder for transient data
             */
            private void parseTransmissionTimeLine(final String line, final ParseInfo pi) {
                pi.qzssCNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addQZSSCivilianNavigationMessage(pi.qzssCNav);
                pi.qzssCNav = null;
            }

        },

        /** Beidou legacy. */
        BEIDOU_D1_D2() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getBDT(), pi.beidouLNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouLNav.setAODE(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.beidouLNav.setCrs(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.METRE));
                pi.beidouLNav.setDeltaN(parseBroadcastDouble3(line, pi.initialSpaces, RAD_PER_S));
                pi.beidouLNav.setM0(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouLNav.setCuc(parseBroadcastDouble1(line,   pi.initialSpaces, Unit.RADIAN));
                pi.beidouLNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.beidouLNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.beidouLNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouLNav.setTime(parseBroadcastDouble1(line,   pi.initialSpaces, Unit.SECOND));
                pi.beidouLNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.beidouLNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.beidouLNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouLNav.setI0(parseBroadcastDouble1(line,       pi.initialSpaces, Unit.RADIAN));
                pi.beidouLNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.beidouLNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.beidouLNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                // iDot
                pi.beidouLNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                // BDT week (to go with Toe)
                pi.beidouLNav.setWeek(parseBroadcastInt3(line, pi.initialSpaces));
                pi.beidouLNav.setDate(new GNSSDate(pi.beidouLNav.getWeek(),
                                                   pi.beidouLNav.getTime(),
                                                   SatelliteSystem.BEIDOU,
                                                   pi.timeScales).getDate());
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouLNav.setSvAccuracy(parseBroadcastDouble1(line, pi.initialSpaces, Unit.METRE));
                // TODO SatH1
                pi.beidouLNav.setTGD1(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.SECOND));
                pi.beidouLNav.setTGD2(parseBroadcastDouble4(line,       pi.initialSpaces, Unit.SECOND));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouLNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.beidouLNav.setAODC(parseBroadcastDouble2(line,             pi.initialSpaces, Unit.SECOND));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addBeidouLegacyNavigationMessage(pi.beidouLNav);
                pi.beidouLNav = null;
            }

        },

        /** Beidou-3 CNAV. */
        BEIDOU_CNV_123() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getBDT(), pi.beidouCNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setADot(parseBroadcastDouble1(line, pi.initialSpaces, M_PER_S));
                pi.beidouCNav.setCrs(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.METRE));
                pi.beidouCNav.setDeltaN(parseBroadcastDouble3(line, pi.initialSpaces, RAD_PER_S));
                pi.beidouCNav.setM0(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setCuc(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.beidouCNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.beidouCNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.beidouCNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.beidouCNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.beidouCNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.beidouCNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setI0(parseBroadcastDouble1(line,       pi.initialSpaces, Unit.RADIAN));
                pi.beidouCNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.beidouCNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.beidouCNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                pi.beidouCNav.setDeltaN0Dot(parseBroadcastDouble2(line, pi.initialSpaces, RAD_PER_S2));
                switch (parseBroadcastInt3(line, pi.initialSpaces)) {
                    case 0 :
                        pi.beidouCNav.setSatelliteType(BeidouSatelliteType.RESERVED);
                        break;
                    case 1 :
                        pi.beidouCNav.setSatelliteType(BeidouSatelliteType.GEO);
                        break;
                    case 2 :
                        pi.beidouCNav.setSatelliteType(BeidouSatelliteType.IGSO);
                        break;
                    case 3 :
                        pi.beidouCNav.setSatelliteType(BeidouSatelliteType.MEO);
                        break;
                    default:
                        throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                  pi.lineNumber, pi.name, line);
                }
                pi.beidouCNav.setTime(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.SECOND));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setSisaiOe(parseBroadcastInt1(line, pi.initialSpaces));
                pi.beidouCNav.setSisaiOcb(parseBroadcastInt2(line, pi.initialSpaces));
                pi.beidouCNav.setSisaiOc1(parseBroadcastInt3(line, pi.initialSpaces));
                pi.beidouCNav.setSisaiOc2(parseBroadcastInt4(line, pi.initialSpaces));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                if (pi.beidouCNav.getSignal() == Frequency.B1C) {
                    pi.beidouCNav.setIscB1CD(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                    // field 2 is spare
                    pi.beidouCNav.setTgdB1Cp(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                    pi.beidouCNav.setTgdB2ap(parseBroadcastDouble4(line, pi.initialSpaces, Unit.SECOND));
                } else if (pi.beidouCNav.getSignal() == Frequency.B2A) {
                    // field 1 is spare
                    pi.beidouCNav.setIscB2AD(parseBroadcastDouble2(line, pi.initialSpaces, Unit.SECOND));
                    pi.beidouCNav.setTgdB1Cp(parseBroadcastDouble3(line, pi.initialSpaces, Unit.SECOND));
                    pi.beidouCNav.setTgdB2ap(parseBroadcastDouble4(line, pi.initialSpaces, Unit.SECOND));
                } else {
                    parseSismaiHealthIntegrity(line, pi);
                }
            }

            /** {@inheritDoc} */
            @Override
            public void parseEighthBroadcastOrbit(final String line, final ParseInfo pi) {
                if (pi.beidouCNav.getSignal() == Frequency.B2B) {
                    pi.beidouCNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                    pi.closePendingMessage();
                } else {
                    parseSismaiHealthIntegrity(line, pi);
                }
            }

            /** {@inheritDoc} */
            @Override
            public void parseNinthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.beidouCNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                // field 2 is spare
                // field 3 is spare
                pi.beidouCNav.setIODE(parseBroadcastInt4(line, pi.initialSpaces));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addBeidouCivilianNavigationMessage(pi.beidouCNav);
                pi.beidouCNav = null;
            }

            /**
             * Parse the SISMAI/Health/integrity line.
             * @param line line to read
             * @param pi holder for transient data
             */
            private void parseSismaiHealthIntegrity(final String line, final ParseInfo pi) {
                pi.beidouCNav.setSismai(parseBroadcastInt1(line, pi.initialSpaces));
                pi.beidouCNav.setHealth(parseBroadcastInt2(line, pi.initialSpaces));
                pi.beidouCNav.setIntegrityFlags(parseBroadcastInt3(line, pi.initialSpaces));
                pi.beidouCNav.setIODC(parseBroadcastInt4(line, pi.initialSpaces));
            }

        },

        /** SBAS. */
        SBAS() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {

                // parse PRN
                pi.sbasNav.setPRN(RinexUtils.parseInt(line, 1, 2));

                // Time scale (UTC for Rinex 3.01 and GPS for other RINEX versions)
                final int       version100 = (int) FastMath.rint(pi.file.getHeader().getFormatVersion() * 100);
                final TimeScale timeScale  = (version100 == 301) ? pi.timeScales.getUTC() : pi.timeScales.getGPS();

                pi.sbasNav.setEpochToc(parsePrnSvEpochClock(line, timeScale));
                pi.sbasNav.setAGf0(parseBroadcastDouble2(line, pi.initialSpaces, Unit.SECOND));
                pi.sbasNav.setAGf1(parseBroadcastDouble3(line, pi.initialSpaces, S_PER_S));
                pi.sbasNav.setTime(parseBroadcastDouble4(line, pi.initialSpaces, Unit.SECOND));

                // Set the ephemeris epoch (same as time of clock epoch)
                pi.sbasNav.setDate(pi.sbasNav.getEpochToc());

            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.sbasNav.setX(parseBroadcastDouble1(line, pi.initialSpaces, KM));
                pi.sbasNav.setXDot(parseBroadcastDouble2(line,    pi.initialSpaces, KM_PER_S));
                pi.sbasNav.setXDotDot(parseBroadcastDouble3(line, pi.initialSpaces, KM_PER_S2));
                pi.sbasNav.setHealth(parseBroadcastDouble4(line,  pi.initialSpaces, Unit.NONE));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.sbasNav.setY(parseBroadcastDouble1(line, pi.initialSpaces, KM));
                pi.sbasNav.setYDot(parseBroadcastDouble2(line,    pi.initialSpaces, KM_PER_S));
                pi.sbasNav.setYDotDot(parseBroadcastDouble3(line, pi.initialSpaces, KM_PER_S2));
                pi.sbasNav.setURA(parseBroadcastDouble4(line,     pi.initialSpaces, Unit.NONE));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.sbasNav.setZ(parseBroadcastDouble1(line, pi.initialSpaces, KM));
                pi.sbasNav.setZDot(parseBroadcastDouble2(line,    pi.initialSpaces, KM_PER_S));
                pi.sbasNav.setZDotDot(parseBroadcastDouble3(line, pi.initialSpaces, KM_PER_S2));
                pi.sbasNav.setIODN(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.NONE));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addSBASNavigationMessage(pi.sbasNav);
                pi.sbasNav = null;
            }

        },

        /** IRNSS. */
        IRNSS() {

            /** {@inheritDoc} */
            @Override
            public void parseSvEpochSvClockLine(final String line, final ParseInfo pi) {
                parseSvEpochSvClockLine(line, pi.timeScales.getIRNSS(), pi.irnssNav);
            }

            /** {@inheritDoc} */
            @Override
            public void parseFirstBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.irnssNav.setIODEC(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.irnssNav.setCrs(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.METRE));
                pi.irnssNav.setDeltaN(parseBroadcastDouble3(line,  pi.initialSpaces, RAD_PER_S));
                pi.irnssNav.setM0(parseBroadcastDouble4(line,      pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSecondBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.irnssNav.setCuc(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.irnssNav.setE(parseBroadcastDouble2(line,     pi.initialSpaces, Unit.NONE));
                pi.irnssNav.setCus(parseBroadcastDouble3(line,   pi.initialSpaces, Unit.RADIAN));
                pi.irnssNav.setSqrtA(parseBroadcastDouble4(line, pi.initialSpaces, SQRT_M));
            }

            /** {@inheritDoc} */
            @Override
            public void parseThirdBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.irnssNav.setTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.irnssNav.setCic(parseBroadcastDouble2(line,    pi.initialSpaces, Unit.RADIAN));
                pi.irnssNav.setOmega0(parseBroadcastDouble3(line, pi.initialSpaces, Unit.RADIAN));
                pi.irnssNav.setCis(parseBroadcastDouble4(line,    pi.initialSpaces, Unit.RADIAN));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.irnssNav.setI0(parseBroadcastDouble1(line, pi.initialSpaces, Unit.RADIAN));
                pi.irnssNav.setCrc(parseBroadcastDouble2(line,      pi.initialSpaces, Unit.METRE));
                pi.irnssNav.setPa(parseBroadcastDouble3(line,       pi.initialSpaces, Unit.RADIAN));
                pi.irnssNav.setOmegaDot(parseBroadcastDouble4(line, pi.initialSpaces, RAD_PER_S));
            }

            /** {@inheritDoc} */
            @Override
            public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
                // iDot
                pi.irnssNav.setIDot(parseBroadcastDouble1(line, pi.initialSpaces, RAD_PER_S));
                // IRNSS week (to go with Toe)
                pi.irnssNav.setWeek(parseBroadcastInt3(line, pi.initialSpaces));
                pi.irnssNav.setDate(new GNSSDate(pi.irnssNav.getWeek(),
                                                 pi.irnssNav.getTime(),
                                                 SatelliteSystem.GPS, // in Rinex files, week number is aligned to GPS week!
                                                 pi.timeScales).getDate());
            }

            /** {@inheritDoc} */
            @Override
            public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.irnssNav.setURA(parseBroadcastDouble1(line,      pi.initialSpaces, Unit.METRE));
                pi.irnssNav.setSvHealth(parseBroadcastDouble2(line, pi.initialSpaces, Unit.NONE));
                pi.irnssNav.setTGD(parseBroadcastDouble3(line,      pi.initialSpaces, Unit.SECOND));
            }

            /** {@inheritDoc} */
            @Override
            public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
                pi.irnssNav.setTransmissionTime(parseBroadcastDouble1(line, pi.initialSpaces, Unit.SECOND));
                pi.closePendingMessage();
            }

            /** {@inheritDoc} */
            @Override
            public void closeMessage(final ParseInfo pi) {
                pi.file.addIRNSSNavigationMessage(pi.irnssNav);
                pi.irnssNav = null;
            }

        };

        /** Get the parse for navigation message.
         * @param system satellite system
         * @param type message type (null for Rinex 3.x)
         * @param parseInfo container for transient data
         * @param line line being parsed
         * @return the satellite system line parser
         */
        private static SatelliteSystemLineParser getParser(final SatelliteSystem system, final String type,
                                                           final ParseInfo parseInfo, final String line) {
            switch (system) {
                case GPS :
                    if (type == null || type.equals(LegacyNavigationMessage.LNAV)) {
                        parseInfo.gpsLNav = new GPSLegacyNavigationMessage();
                        return GPS_LNAV;
                    } else if (type.equals(CivilianNavigationMessage.CNAV)) {
                        parseInfo.gpsCNav = new GPSCivilianNavigationMessage(false);
                        return GPS_CNAV;
                    } else if (type.equals(CivilianNavigationMessage.CNV2)) {
                        parseInfo.gpsCNav = new GPSCivilianNavigationMessage(true);
                        return GPS_CNAV;
                    }
                    break;
                case GALILEO :
                    if (type == null || type.equals("INAV") || type.equals("FNAV")) {
                        parseInfo.galileoNav = new GalileoNavigationMessage();
                        return GALILEO;
                    }
                    break;
                case GLONASS :
                    if (type == null || type.equals("FDMA")) {
                        parseInfo.glonassNav = new GLONASSNavigationMessage();
                        return GLONASS;
                    }
                    break;
                case QZSS :
                    if (type == null || type.equals(LegacyNavigationMessage.LNAV)) {
                        parseInfo.qzssLNav = new QZSSLegacyNavigationMessage();
                        return QZSS_LNAV;
                    } else if (type.equals(CivilianNavigationMessage.CNAV)) {
                        parseInfo.qzssCNav = new QZSSCivilianNavigationMessage(false);
                        return QZSS_CNAV;
                    } else if (type.equals(CivilianNavigationMessage.CNV2)) {
                        parseInfo.qzssCNav = new QZSSCivilianNavigationMessage(true);
                        return QZSS_CNAV;
                    }
                    break;
                case BEIDOU :
                    if (type == null ||
                        type.equals(BeidouLegacyNavigationMessage.D1) ||
                        type.equals(BeidouLegacyNavigationMessage.D2)) {
                        parseInfo.beidouLNav = new BeidouLegacyNavigationMessage();
                        return BEIDOU_D1_D2;
                    } else if (type.equals(BeidouCivilianNavigationMessage.CNV1)) {
                        parseInfo.beidouCNav = new BeidouCivilianNavigationMessage(Frequency.B1C);
                        return BEIDOU_CNV_123;
                    } else if (type.equals(BeidouCivilianNavigationMessage.CNV2)) {
                        parseInfo.beidouCNav = new BeidouCivilianNavigationMessage(Frequency.B2A);
                        return BEIDOU_CNV_123;
                    } else if (type.equals(BeidouCivilianNavigationMessage.CNV3)) {
                        parseInfo.beidouCNav = new BeidouCivilianNavigationMessage(Frequency.B2B);
                        return BEIDOU_CNV_123;
                    }
                    break;
                case IRNSS :
                    if (type == null || type.equals("LNAV")) {
                        parseInfo.irnssNav = new IRNSSNavigationMessage();
                        return IRNSS;
                    }
                    break;
                case SBAS :
                    if (type == null || type.equals("SBAS")) {
                        parseInfo.sbasNav = new SBASNavigationMessage();
                        return SBAS;
                    }
                    break;
                default:
                    // do nothing, handle error after the switch
            }
            throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                      parseInfo.lineNumber, parseInfo.name, line);
        }

        /**
         * Parse the SV/Epoch/Sv clock of the navigation message.
         * @param line line to read
         * @param timeScale time scale to use
         * @param message navigation message
         */
        protected void parseSvEpochSvClockLineRinex2(final String line, final TimeScale timeScale,
                                                     final AbstractNavigationMessage message) {
            // PRN
            message.setPRN(RinexUtils.parseInt(line, 0, 2));

            // Toc
            final int    year  = RinexUtils.convert2DigitsYear(RinexUtils.parseInt(line,  2, 3));
            final int    month = RinexUtils.parseInt(line,  5, 3);
            final int    day   = RinexUtils.parseInt(line,  8, 3);
            final int    hours = RinexUtils.parseInt(line, 11, 3);
            final int    min   = RinexUtils.parseInt(line, 14, 3);
            final double sec   = RinexUtils.parseDouble(line, 17, 5);
            message.setEpochToc(new AbsoluteDate( year, month, day, hours, min, sec, timeScale));

            // clock
            message.setAf0(RinexUtils.parseDouble(line, 22, 19));
            message.setAf1(RinexUtils.parseDouble(line, 41, 19));
            message.setAf2(RinexUtils.parseDouble(line, 60, 19));

        }

        /**
         * Parse the SV/Epoch/Sv clock of the navigation message.
         * @param line line to read
         * @param timeScale time scale to use
         * @param message navigation message
         */
        protected void parseSvEpochSvClockLine(final String line, final TimeScale timeScale,
                                               final AbstractNavigationMessage message) {
            // PRN
            message.setPRN(RinexUtils.parseInt(line, 1, 2));

            // Toc
            message.setEpochToc(parsePrnSvEpochClock(line, timeScale));

            // clock
            message.setAf0(RinexUtils.parseDouble(line, 23, 19));
            message.setAf1(RinexUtils.parseDouble(line, 42, 19));
            message.setAf2(RinexUtils.parseDouble(line, 61, 19));

        }

        /** Parse epoch field of a Sv/epoch/clock line.
         * @param line line to parse
         * @param timeScale time scale to use
         * @return parsed field
         */
        protected AbsoluteDate parsePrnSvEpochClock(final String line, final TimeScale timeScale) {
            final int year  = RinexUtils.parseInt(line, 4, 4);
            final int month = RinexUtils.parseInt(line, 9, 2);
            final int day   = RinexUtils.parseInt(line, 12, 2);
            final int hours = RinexUtils.parseInt(line, 15, 2);
            final int min   = RinexUtils.parseInt(line, 18, 2);
            final int sec   = RinexUtils.parseInt(line, 21, 2);
            return new AbsoluteDate(year, month, day, hours, min, sec, timeScale);
        }

        /** Parse double field 1 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @param unit unit to used for parsing the field
         * @return parsed field
         */
        protected double parseBroadcastDouble1(final String line, final int initialSpaces, final Unit unit) {
            return unit.toSI(RinexUtils.parseDouble(line, initialSpaces, 19));
        }

        /** Parse integer field 1 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @return parsed field
         */
        protected int parseBroadcastInt1(final String line, final int initialSpaces) {
            return (int) FastMath.rint(RinexUtils.parseDouble(line, initialSpaces, 19));
        }

        /** Parse double field 2 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @param unit unit to used for parsing the field
         * @return parsed field
         */
        protected double parseBroadcastDouble2(final String line, final int initialSpaces, final Unit unit) {
            return unit.toSI(RinexUtils.parseDouble(line, initialSpaces + 19, 19));
        }

        /** Parse integer field 2 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @return parsed field
         */
        protected int parseBroadcastInt2(final String line, final int initialSpaces) {
            return (int) FastMath.rint(RinexUtils.parseDouble(line, initialSpaces + 19, 19));
        }

        /** Parse double field 3 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @param unit unit to used for parsing the field
         * @return parsed field
         */
        protected double parseBroadcastDouble3(final String line, final int initialSpaces, final Unit unit) {
            return unit.toSI(RinexUtils.parseDouble(line, initialSpaces + 38, 19));
        }

        /** Parse integer field 3 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @return parsed field
         */
        protected int parseBroadcastInt3(final String line, final int initialSpaces) {
            return (int) FastMath.rint(RinexUtils.parseDouble(line, initialSpaces + 38, 19));
        }

        /** Parse double field 4 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @param unit unit to used for parsing the field
         * @return parsed field
         */
        protected double parseBroadcastDouble4(final String line, final int initialSpaces, final Unit unit) {
            return unit.toSI(RinexUtils.parseDouble(line, initialSpaces + 57, 19));
        }

        /** Parse integer field 4 of a broadcast orbit line.
         * @param line line to parse
         * @param initialSpaces number of initial spaces in the line
         * @return parsed field
         */
        protected int parseBroadcastInt4(final String line, final int initialSpaces) {
            return (int) FastMath.rint(RinexUtils.parseDouble(line, initialSpaces + 57, 19));
        }

        /**
         * Parse the SV/Epoch/Sv clock of the navigation message.
         * @param line line to read
         * @param pi holder for transient data
         */
        public abstract void parseSvEpochSvClockLine(String line, ParseInfo pi);

        /**
         * Parse the "BROADCASTORBIT - 1" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public abstract void parseFirstBroadcastOrbit(String line, ParseInfo pi);

        /**
         * Parse the "BROADCASTORBIT - 2" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public abstract void parseSecondBroadcastOrbit(String line, ParseInfo pi);

        /**
         * Parse the "BROADCASTORBIT - 3" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public abstract void parseThirdBroadcastOrbit(String line, ParseInfo pi);

        /**
         * Parse the "BROADCASTORBIT - 4" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public void parseFourthBroadcastOrbit(final String line, final ParseInfo pi) {
            // this should never be called (except by some tests that use reflection)
            throw new OrekitInternalError(null);
        }

        /**
         * Parse the "BROADCASTORBIT - 5" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public void parseFifthBroadcastOrbit(final String line, final ParseInfo pi) {
            // this should never be called (except by some tests that use reflection)
            throw new OrekitInternalError(null);
        }

        /**
         * Parse the "BROADCASTORBIT - 6" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public void parseSixthBroadcastOrbit(final String line, final ParseInfo pi) {
            // this should never be called (except by some tests that use reflection)
            throw new OrekitInternalError(null);
        }

        /**
         * Parse the "BROADCASTORBIT - 7" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public void parseSeventhBroadcastOrbit(final String line, final ParseInfo pi) {
            // this should never be called (except by some tests that use reflection)
            throw new OrekitInternalError(null);
        }

        /**
         * Parse the "BROADCASTORBIT - 8" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public void parseEighthBroadcastOrbit(final String line, final ParseInfo pi) {
            // this should never be called (except by some tests that use reflection)
            throw new OrekitInternalError(null);
        }

        /**
         * Parse the "BROADCASTORBIT - 9" line.
         * @param line line to read
         * @param pi holder for transient data
         */
        public void parseNinthBroadcastOrbit(final String line, final ParseInfo pi) {
            // this should never be called (except by some tests that use reflection)
            throw new OrekitInternalError(null);
        }

        /**
         * Close a message as last line was parsed.
         * @param pi holder for transient data
         */
        public abstract void closeMessage(ParseInfo pi);

        /**
         * Calculates the floating-point remainder of a / b.
         * <p>
         * fmod = a - x * b
         * where x = (int) a / b
         * </p>
         * @param a numerator
         * @param b denominator
         * @return the floating-point remainder of a / b
         */
        private static double fmod(final double a, final double b) {
            final double x = (int) (a / b);
            return a - x * b;
        }

    }

    /** Parsing method. */
    @FunctionalInterface
    private interface ParsingMethod {
        /** Parse a line.
         * @param line line to parse
         * @param parseInfo holder for transient data
         */
        void parse(String line, ParseInfo parseInfo);
    }

}