RinexClockParser.java

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.InputMismatchException;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.function.BiFunction;
import java.util.function.Function;

import org.hipparchus.exception.LocalizedCoreFormats;
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.OrekitMessages;
import org.orekit.files.rinex.AppliedDCBS;
import org.orekit.files.rinex.AppliedPCVS;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.utils.ParsingUtils;
import org.orekit.frames.Frame;
import org.orekit.gnss.IGSUtils;
import org.orekit.gnss.ObservationType;
import org.orekit.gnss.PredefinedObservationType;
import org.orekit.gnss.PredefinedTimeSystem;
import org.orekit.gnss.SatInSystem;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.gnss.TimeSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.utils.units.Unit;

/** A parser for the clock file from the IGS.
 * This parser handles versions 2.0 to 3.04 of the RINEX clock files.
 * <p> It is able to manage some mistakes in file writing and format compliance such as wrong date format,
 * misplaced header blocks or missing information. </p>
 * <p> A time system should be specified in the file. However, if it is not, default time system will be chosen
 * regarding the satellite system. If it is mixed or not specified, default time system will be UTC. </p>
 * <p> Caution, files with missing information in header can lead to wrong data dates and station positions.
 * It is advised to check the correctness and format compliance of the clock file to be parsed. </p>
 * @see <a href="https://files.igs.org/pub/data/format/rinex_clock300.txt"> 3.00 clock file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex_clock302.txt"> 3.02 clock file format</a>
 * @see <a href="https://files.igs.org/pub/data/format/rinex_clock304.txt"> 3.04 clock file format</a>
 *
 * @author Thomas Paulet
 * @since 11.0
 */
public class RinexClockParser {

    /** Millimeter unit. */
    private static final Unit MILLIMETER = Unit.parse("mm");

    /** Spaces delimiters. */
    private static final String SPACES = "\\s+";

    /** Mapping from frame identifier in the file to a {@link Frame}. */
    private final Function<? super String, ? extends Frame> frameBuilder;

    /** Mapper from string to the observation type.
     * @since 13.0
     */
    private final Function<? super String, ? extends ObservationType> typeBuilder;

    /** Mapper from string to time system.
     * @since 14.0
     */
    private final Function<? super String, ? extends TimeSystem> timeSystemBuilder;

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

    /** Create a clock file parser using default values.
     * <p>
     * This constructor uses the {@link DataContext#getDefault() default data context}
     * and {@link IGSUtils#guessFrame(String)}, it recognizes only {@link
     * org.orekit.gnss.PredefinedObservationType} and {@link org.orekit.gnss.PredefinedTimeSystem}.
     * </p>
     * @see #RinexClockParser(Function, Function, Function, TimeScales)
     */
    @DefaultDataContext
    public RinexClockParser() {
        this(IGSUtils::guessFrame,
             PredefinedObservationType::valueOf,
             PredefinedTimeSystem::parseTimeSystem,
             DataContext.getDefault().getTimeScales());
    }

    /** Constructor, build the IGS clock file parser.
     * @param frameBuilder      is a function that can construct a frame from a clock file
     *                          coordinate system string. The coordinate system can be
     *                          any 5 characters string e.g., ITR92, IGb08.
     * @param typeBuilder       mapper from string to the observation type
     * @param timeSystemBuilder mapper from string to time system (useful for user-defined time systems)
     * @param timeScales        the set of time scales used for parsing dates.
     * @since 14.0
     */
    public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder,
                            final Function<? super String, ? extends ObservationType> typeBuilder,
                            final Function<? super String, ? extends TimeSystem> timeSystemBuilder,
                            final TimeScales timeScales) {
        this.frameBuilder      = frameBuilder;
        this.typeBuilder       = typeBuilder;
        this.timeSystemBuilder = timeSystemBuilder;
        this.timeScales        = timeScales;
    }

    /** Parse an IGS clock file from a {@link DataSource}.
     * @param source source for clock file
     * @return a parsed IGS clock file
     * @since 12.1
     */
    public RinexClock parse(final DataSource source) {

        Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.VERSION);

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

        try (Reader reader = source.getOpener().openReaderOnce();
             BufferedReader br = new BufferedReader(reader)) {
            ++parseInfo.lineNumber;
            nextLine:
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                for (final LineParser candidate : candidateParsers) {
                    if (candidate.canHandle.apply(parseInfo.file.getHeader(), line)) {
                        try {
                            candidate.parsingMethod.parse(line, parseInfo);
                            ++parseInfo.lineNumber;
                            candidateParsers = candidate.allowedNextProvider.apply(parseInfo);
                            continue nextLine;
                        } catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
                            throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                      parseInfo.lineNumber, source.getName(), line);
                        }
                    }
                }

                // no parsers found for this line
                throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                          parseInfo.lineNumber, source.getName(), line);

            }

        } catch (IOException ioe) {
            throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
        }

        return parseInfo.file;

    }

    /** Transient data used for parsing a clock file. */
    private class ParseInfo {

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

        /** Mapping from frame identifier in the file to a {@link Frame}.
         * @since 14.0
         */
        private final Function<? super String, ? extends Frame> frameBuilder;

        /** Mapper from string to the observation type.
         * @since 14.0
         */
        private final Function<? super String, ? extends ObservationType> typeBuilder;

        /** Mapper from string to time system.
         * @since 14.0
         */
        private final Function<? super String, ? extends TimeSystem> timeSystemBuilder;

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

        /** Current line number of the navigation message. */
        private int lineNumber;

        /** The corresponding clock file object. */
        private final RinexClock file;

        /** Current satellite system for observation type parsing. */
        private SatelliteSystem currentSatelliteSystem;

        /** Remaining number of observation types. */
        private int remainingObsTypes;

        /** Indicator for completed header. */
        private boolean headerCompleted;

        /** Current start date for reference clocks. */
        private AbsoluteDate referenceClockStartDate;

        /** Current end date for reference clocks. */
        private AbsoluteDate referenceClockEndDate;

        /** Pending reference clocks list. */
        private List<ReferenceClock> pendingReferenceClocks;

        /** Number of stations. */
        private int nbStations;

        /** Number of satellites. */
        private int nbSatellites;

        /** Current clock data type. */
        private ClockDataType currentDataType;

        /** Current receiver/satellite name. */
        private String currentName;

        /** Date if the clock data line. */
        private AbsoluteDate date;

        /** Total number of values to parse. */
        private int totalValues;

        /** Index of next value to parse. */
        private int valueIndex;

        /** Current data values. */
        private final double[] values;

        /** Constructor, build the ParseInfo object.
         * @param name name of the data source
         */
        ParseInfo(final String name) {
            this.name                   = name;
            this.frameBuilder           = RinexClockParser.this.frameBuilder;
            this.typeBuilder            = RinexClockParser.this.typeBuilder;
            this.timeSystemBuilder      = RinexClockParser.this.timeSystemBuilder;
            this.timeScales             = RinexClockParser.this.timeScales;
            this.file                   = new RinexClock();
            this.lineNumber             = 0;
            this.pendingReferenceClocks = new ArrayList<>();
            this.values                 = new double[6];

            // reset the default values set by header constructor
            this.file.getHeader().setProgramName(null);
            this.file.getHeader().setRunByName(null);
            this.file.getHeader().setCreationDateComponents(null);

        }

    }


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

        /** Parser for version, file type and satellite system. */
        VERSION((header, line) -> header.matchFound(CommonLabel.VERSION, line),
                (line, parseInfo) ->  {
                    final RinexClockHeader header = parseInfo.file.getHeader();
                    header.parseVersionFileTypeSatelliteSystem(line, null, parseInfo.name,
                                                               2.00, 3.00, 3.01, 3.02, 3.04);
                    if (header.getFormatVersion() < 3.0) {
                        // before 3.0, only GPS system was used
                        header.setSatelliteSystem(SatelliteSystem.GPS);
                        header.setTimeSystem(PredefinedTimeSystem.GPS);
                        header.setTimeScale(parseInfo.timeSystemBuilder.
                                            apply(PredefinedTimeSystem.GPS.getKey()).
                                            getTimeScale(parseInfo.timeScales));
                    }
                },
                LineParser::headerNext),

        /** Parser for generating program and emiting agency. */
        PROGRAM((header, line) -> header.matchFound(CommonLabel.PROGRAM, line),
                (line, parseInfo) -> parseInfo.file.getHeader().parseProgramRunByDate(line, parseInfo.timeScales),
                LineParser::headerNext),

        /** Parser for comments. */
        COMMENT((header, line) -> header.matchFound(CommonLabel.COMMENT, line),
                (line, parseInfo) -> ParsingUtils.parseComment(parseInfo.lineNumber, line, parseInfo.file),
                LineParser::commentNext),

        /** Parser for satellite system and related observation types. */
        SYS_NB_TYPES_OF_OBSERV((header, line) -> header.matchFound(CommonLabel.SYS_NB_TYPES_OF_OBSERV, line),
                               (line, parseInfo) -> {
                                   final RinexClockHeader header = parseInfo.file.getHeader();
                                   if (parseInfo.remainingObsTypes == 0) {
                                       // we are starting a new satellite system
                                       parseInfo.currentSatelliteSystem =
                                           SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(line, 0, 1));
                                       parseInfo.remainingObsTypes = ParsingUtils.parseInt(line, 3, 3);
                                   }
                                   for (int i = 0;
                                        i < (header.isBefore304() ? 13 : 14) && parseInfo.remainingObsTypes > 0;
                                        ++i) {
                                       parseInfo.remainingObsTypes--;
                                       final String obsType = ParsingUtils.parseString(line,
                                                                                       (header.isBefore304() ? 7 : 8) + 4 * i,
                                                                                       3);
                                       header.addSystemObservationType(parseInfo.currentSatelliteSystem,
                                                                       parseInfo.typeBuilder.apply(obsType));
                                   }
                               },
                               LineParser::sysObsTypesNext),

        /** Parser for time system identifier. */
        TIME_SYSTEM_ID((header, line) -> header.matchFound(ClockLabel.TIME_SYSTEM_ID, line),
                       (line, parseInfo) -> {
                           final RinexClockHeader header = parseInfo.file.getHeader();
                           final TimeSystem timeSystem = parseInfo.timeSystemBuilder.
                                                         apply(ParsingUtils.parseString(line, 3, 3));
                           header.setTimeSystem(timeSystem);
                           header.setTimeScale(timeSystem.getTimeScale(parseInfo.timeScales));
                       },
                       LineParser::headerNext),

        /** Parser for leap seconds separating UTC and TAI. */
        LEAP_SECONDS((header, line) -> header.matchFound(CommonLabel.LEAP_SECONDS, line),
                     (line, parseInfo) -> parseInfo.file.getHeader().
                                          setLeapSecondsTAI(ParsingUtils.parseInt(line, 0, 6)),
                     LineParser::headerNext),

        /** Parser for leap seconds separating UTC and GNSS. */
        LEAP_SECONDS_GNSS((header, line) -> header.matchFound(ClockLabel.LEAP_SECONDS_GNSS, line),
                          (line, parseInfo) -> parseInfo.file.getHeader().
                                               setLeapSecondsGNSS(ParsingUtils.parseInt(line, 0, 6)),
                          LineParser::headerNext),

        /** Parser for differential code bias corrections. */
        SYS_DCBS_APPLIED((header, line) -> header.matchFound(CommonLabel.SYS_DCBS_APPLIED, line),
                         (line, parseInfo) -> {
                             // we added small margins to the character indices here because some files
                             // do NOT respect the format
                             // the reference example in table A17 of the rinex 3.04 specification itself exhibits
                             // errors (the source field is shifted 2 characters to the left wrt. the specification,
                             // i.e. this line in table A17 is consistent with the rinex clock 3.02 specification,
                             // not with the rinex clock 3.04 specification…).
                             final RinexClockHeader header = parseInfo.file.getHeader();
                             final SatelliteSystem satelliteSystem =
                                 SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(line, 0, 1),
                                                                      header.getSatelliteSystem());
                             header.addAppliedDCBS(new AppliedDCBS(satelliteSystem,
                                                                   ParsingUtils.parseString(line,  2, 18),
                                                                   ParsingUtils.parseString(line, 20,
                                                                                            header.getLabelIndex() - 20)));
                         },
                         LineParser::headerNext),

        /** Parser for phase center variations corrections. */
        SYS_PCVS_APPLIED((header, line) -> header.matchFound(CommonLabel.SYS_PCVS_APPLIED, line),
                         (line, parseInfo) -> {
                             // we added small margins to the character indices here because some files
                             // do NOT respect the format
                             // the reference example in table A17 of the rinex 3.04 specification itself exhibits
                             // errors (the source field is shifted 2 characters to the left wrt. the specification,
                             // i.e. this line in table A17 is consistent with the rinex clock 3.02 specification,
                             // not with the rinex clock 3.04 specification…).
                             final RinexClockHeader header = parseInfo.file.getHeader();
                             final SatelliteSystem satelliteSystem =
                                     SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(line, 0, 1),
                                                                          header.getSatelliteSystem());
                             header.addAppliedPCVS(new AppliedPCVS(satelliteSystem,
                                                                   ParsingUtils.parseString(line,  2, 18),
                                                                   ParsingUtils.parseString(line, 20,
                                                                                            header.getLabelIndex() - 20)));
                         },
                         LineParser::headerNext),

        /** Parser for the different clock data types that are stored in the file. */
        NB_TYPES_OF_DATA((header, line) -> header.matchFound(ClockLabel.NB_TYPES_OF_DATA, line),
                         (line, parseInfo) -> {
                             final int n = ParsingUtils.parseInt(line, 0, 6);
                             if (n < 1) {
                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                           parseInfo.lineNumber, parseInfo.name, line);
                             }
                             for (int i = 0; i < n; i++) {
                                 final String type = ParsingUtils.parseString(line, 10 + i * 6, 4);
                                 try {
                                     parseInfo.file.getHeader().addClockDataType(ClockDataType.valueOf(type));
                                 } catch (IllegalArgumentException iae) {
                                     throw new OrekitException(OrekitMessages.UNKNOWN_CLOCK_DATA_TYPE, type);
                                 }
                             }
                         },
                         LineParser::headerNext),

        /** Parser for the station with reference clock. */
        STATION_NAME_NUM((header, line) -> header.matchFound(ClockLabel.STATION_NAME_NUM, line),
                         (line, parseInfo) -> {
                             // we use a Scanner here instead of relying on the character indices
                             // because some files do NOT respect the format
                             // the reference example in table A18 of the rinex 3.04 specification itself exhibits
                             // errors (the DOMES number field is shifted 5 characters to the left wrt. the
                             // specification, i.e. this line in table A18 is consistent with the rinex clock 3.02
                             // specification, not with the rinex clock 3.04 specification…).
                             // We fall back to a Scanner, which should handle all format versions properly
                             final RinexClockHeader header = parseInfo.file.getHeader();
                             try (Scanner s1      = new Scanner(line.substring(0, header.getLabelIndex()));
                                  Scanner s2      = s1.useDelimiter(SPACES);
                                  Scanner scanner = s2.useLocale(Locale.US)) {
                                 header.setStationName(scanner.next());
                                 header.setStationIdentifier(scanner.next());
                             }
                         },
                         LineParser::headerNext),

        /** Parser for the reference clock in case of calibration data. */
        STATION_CLK_REF((header, line) -> header.matchFound(ClockLabel.STATION_CLK_REF, line),
                        (line, parseInfo) ->  {
                            final RinexClockHeader header = parseInfo.file.getHeader();
                            if (header.isBefore304()) {
                                header.setExternalClockReference(line.substring(0, 60).trim());
                            } else {
                                header.setExternalClockReference(line.substring(0, 65).trim());
                            }
                        },
                        LineParser::headerNext),

        /** Parser for the analysis center. */
        ANALYSIS_CENTER((header, line) -> header.matchFound(ClockLabel.ANALYSIS_CENTER, line),
                        (line, parseInfo) -> {
                            final RinexClockHeader header = parseInfo.file.getHeader();

                            // First element is IGS AC designator
                            header.setAnalysisCenterID(ParsingUtils.parseString(line, 0, 3));

                            // Then, the full name of the analysis center
                            if (header.isBefore304()) {
                                header.setAnalysisCenterName(ParsingUtils.parseString(line, 5, 55));
                            } else {
                                header.setAnalysisCenterName(ParsingUtils.parseString(line, 5, 60));
                            }
                        },
                        LineParser::headerNext),

        /** Parser for the number of reference clocks over a period. */
        NB_OF_CLK_REF((header, line) -> header.matchFound(ClockLabel.NB_OF_CLK_REF, line),
                      (line, parseInfo) -> {
                          final RinexClockHeader header = parseInfo.file.getHeader();
                          if (!parseInfo.pendingReferenceClocks.isEmpty()) {
                              // Modify time span map of the reference clocks to accept the pending reference clock
                              header.addReferenceClockList(parseInfo.pendingReferenceClocks,
                                                           parseInfo.referenceClockStartDate,
                                                           parseInfo.referenceClockEndDate);
                              parseInfo.pendingReferenceClocks = new ArrayList<>();
                          }

                          final String startStop = line.substring(7, header.getLabelIndex()).trim();
                          if (startStop.isEmpty()) {
                              // no start/stop epoch the record applies to the whole file
                              parseInfo.referenceClockStartDate = AbsoluteDate.PAST_INFINITY;
                              parseInfo.referenceClockEndDate   = AbsoluteDate.FUTURE_INFINITY;
                          } else {
                              // we use a Scanner here instead of relying on the character indices
                              // because some files do NOT respect the format
                              // the reference example in table A17 of the rinex 3.04 specification itself exhibits
                              // errors (the seconds field in start date is shifted 1 character to the left wrt.
                              // the specification, the first few fields in end date are shifted 2 characters to the
                              // left wrt. the specification, and the seconds field in end date is shifted 3 characters
                              // to the left wrt. specification, i.e. this line in table A17 is consistent with
                              // the rinex clock 3.02 specification, not with the rinex clock 3.04 specification…).
                              // we were not able to find any real 3.04 files that have non-empty dates on clock ref lines.
                              // We fall back to a Scanner, which should handle all format versions properly
                              try (Scanner s1      = new Scanner(startStop);
                                   Scanner s2      = s1.useDelimiter(SPACES);
                                   Scanner scanner = s2.useLocale(Locale.US)) {
                                  final TimeScale ts = header.getFormatVersion() < 3.02 ?
                                                       parseInfo.timeScales.getGPS() : header.getTimeScale();
                                  parseInfo.referenceClockStartDate =
                                      new AbsoluteDate(scanner.nextInt(), scanner.nextInt(), scanner.nextInt(),
                                                       scanner.nextInt(), scanner.nextInt(), scanner.nextDouble(),
                                                       ts);
                                  parseInfo.referenceClockEndDate =
                                      new AbsoluteDate(scanner.nextInt(), scanner.nextInt(), scanner.nextInt(),
                                                       scanner.nextInt(), scanner.nextInt(), scanner.nextDouble(),
                                                       ts);
                              }
                          }
                      },
                      LineParser::headerNext),

        /** Parser for the reference clock over a period. */
        ANALYSIS_CLK_REF((header, line) -> header.matchFound(ClockLabel.ANALYSIS_CLK_REF, line),
                         (line, parseInfo) -> {

                             // First element is the name of the receiver/satellite embedding the reference clock
                             final int length = parseInfo.file.getHeader().isBefore304() ? 4 : 9;
                             final String referenceName = ParsingUtils.parseString(line, 0, length);

                             // Second element is the reference clock ID
                             final String clockID = ParsingUtils.parseString(line, length + 1, 20);

                             // Optionally, third element is an a priori clock constraint, by default equal to zero
                             double clockConstraint = ParsingUtils.parseDouble(line, length + 36, 19);
                             if (Double.isNaN(clockConstraint)) {
                                 clockConstraint = 0.0;
                             }

                             // Add reference clock to current reference clock list
                             final ReferenceClock referenceClock =
                                 new ReferenceClock(referenceName, clockID, clockConstraint,
                                                    parseInfo.referenceClockStartDate,
                                                    parseInfo.referenceClockEndDate);
                             parseInfo.pendingReferenceClocks.add(referenceClock);
                         },
                         LineParser::headerNext),

        /** Parser for the number of stations embedded in the file and the related frame. */
        NB_OF_SOLN_STA_TRF((header, line) -> header.matchFound(ClockLabel.NB_OF_SOLN_STA_TRF, line),
                           (line, parseInfo) -> {
                               final RinexClockHeader header = parseInfo.file.getHeader();
                               parseInfo.nbStations = ParsingUtils.parseInt(line, 0, 6);
                               final String complete = ParsingUtils.parseString(line, 10, header.isBefore304() ? 50 : 55);
                               int first = 0;
                               while (first < complete.length() && complete.charAt(first) == ' ') {
                                   ++first;
                               }
                               int last = first;
                               while (last < complete.length() &&
                                      Character.isLetterOrDigit(complete.charAt(last))) {
                                   ++last;
                               }
                               final String frameName = complete.substring(first, last);
                               header.setFrameName(frameName);
                               header.setFrame(parseInfo.frameBuilder.apply(frameName));
                           },
                           LineParser::headerNext),

        /** Parser for the stations embedded in the file and the related positions. */
        SOLN_STA_NAME_NUM((header, line) -> header.matchFound(ClockLabel.SOLN_STA_NAME_NUM, line),
                          (line, parseInfo) -> {
                              final int    length     = parseInfo.file.getHeader().isBefore304() ? 4 : 9;
                              final String designator = ParsingUtils.parseString(line, 0, length);
                              final String identifier = ParsingUtils.parseString(line, length + 1, 20);
                              final double x          = MILLIMETER.toSI(ParsingUtils.parseLong(line, length + 21, 11));
                              final double y          = MILLIMETER.toSI(ParsingUtils.parseLong(line, length + 33, 11));
                              final double z          = MILLIMETER.toSI(ParsingUtils.parseLong(line, length + 45, 11));
                              final Receiver receiver = new Receiver(designator, identifier, x, y, z);
                              parseInfo.file.getHeader().addReceiver(receiver);
                          },
                          LineParser::headerNext),

        /** Parser for the number of satellites embedded in the file. */
        NB_OF_SOLN_SATS((header, line) -> header.matchFound(ClockLabel.NB_OF_SOLN_SATS, line),
                        (line, parseInfo) -> parseInfo.nbSatellites = ParsingUtils.parseInt(line, 0, 6),
                        LineParser::headerNext),

        /** Parser for the satellites embedded in the file. */
        PRN_LIST((header, line) -> header.matchFound(ClockLabel.PRN_LIST, line),
                 (line, parseInfo) -> {
                     final RinexClockHeader header = parseInfo.file.getHeader();
                     final int nMax = header.isBefore304() ? 15 : 16;
                     for (int i = 0; i < nMax; ++i) {
                         final String prn = ParsingUtils.parseString(line, 4 * i, 3);
                         if (prn.isEmpty()) {
                             break;
                         } else {
                             header.addSatellite(new SatInSystem(prn));
                         }
                     }
                 },
                 LineParser::headerNext),

        /** Parser for the end of header. */
        HEADER_END((header, line) -> header.matchFound(CommonLabel.END, line),
                   (line, parseInfo) -> {
                       final RinexClockHeader header = parseInfo.file.getHeader();
                       if (!parseInfo.pendingReferenceClocks.isEmpty()) {
                           // Modify time span map of the reference clocks to accept the pending reference clock
                           header.addReferenceClockList(parseInfo.pendingReferenceClocks,
                                                        parseInfo.referenceClockStartDate,
                                                        parseInfo.referenceClockEndDate);
                       }
                       if (header.getTimeSystem() == null) {
                           if (header.getMergedSystem() == null ||
                               header.getMergedSystem() == SatelliteSystem.MIXED) {
                               if (header.getFormatVersion() >= 3.0) {
                                   throw new OrekitException(OrekitMessages.MISSING_TIME_SYSTEM_DEFINITION, parseInfo.name);
                               }
                           } else {
                               // we force the systems using the satellite list
                               header.setTimeSystem(PredefinedTimeSystem.
                                                    parseOneLetterCode(String.valueOf(header.getMergedSystem().getKey())));
                               header.setTimeScale(header.getTimeSystem().getTimeScale(parseInfo.timeScales));
                           }
                       }
                       if (parseInfo.nbStations != header.getNumberOfReceivers()) {
                           throw new OrekitException(OrekitMessages.WRONG_STATIONS_NUMBER,
                                                     parseInfo.name, parseInfo.nbStations,
                                                     header.getNumberOfReceivers());
                       }
                       if (parseInfo.nbSatellites != header.getNumberOfSatellites()) {
                           throw new OrekitException(OrekitMessages.WRONG_SATELLITES_NUMBER,
                                                     parseInfo.name, parseInfo.nbSatellites,
                                                     header.getNumberOfSatellites());
                       }
                       parseInfo.headerCompleted = true;
                   },
                   LineParser::headerEndNext),

        /** Parser for a clock data line. */
        CLOCK_DATA((header, line) -> line.charAt(0) != ' ',
                   (line, parseInfo) -> {

                       final RinexClockHeader header = parseInfo.file.getHeader();
                       try {
                           parseInfo.currentDataType = ClockDataType.valueOf(line.substring(0, 2));
                       } catch (IllegalArgumentException iae) {
                           throw new OrekitException(OrekitMessages.UNKNOWN_CLOCK_DATA_TYPE, line.substring(0, 2));
                       }

                       // Second element is receiver/satellite name
                       final int length = header.isBefore304() ? 4 : 9;
                       parseInfo.currentName = ParsingUtils.parseString(line, 3, length);

                       // Third element is data epoch
                       final int startI = header.isBefore304() ?  8 : 13;
                       final int startD = header.isBefore304() ? 24 : 29;
                       parseInfo.date =
                               new AbsoluteDate(ParsingUtils.parseInt(line, startI, 4),
                                                ParsingUtils.parseInt(line, startI +  4, 4),
                                                ParsingUtils.parseInt(line, startI +  7, 3),
                                                ParsingUtils.parseInt(line, startI + 10, 3),
                                                ParsingUtils.parseInt(line, startI + 13, 3),
                                                ParsingUtils.parseDouble(line, startD, 10),
                                                header.getTimeScale());

                       // Fourth element is number of data values
                       parseInfo.totalValues = ParsingUtils.parseInt(line, startD + 11, 2);
                       parseInfo.valueIndex  = 0;

                       // Get the values in this line
                       Arrays.fill(parseInfo.values, 0.0);
                       int start = header.isBefore304() ?  40 : 45;
                       while (parseInfo.valueIndex < FastMath.min(2, parseInfo.totalValues)) {
                           parseInfo.values[parseInfo.valueIndex++] = ParsingUtils.parseDouble(line, start, 19);
                           start += header.isBefore304() ? 20 : 21;
                       }

                       // Check if continuation line is required
                       if (parseInfo.valueIndex == parseInfo.totalValues) {
                           // No continuation line is required
                           parseInfo.file.addClockData(parseInfo.currentName,
                                                       new ClockDataLine(parseInfo.currentDataType,
                                                                         parseInfo.currentName,
                                                                         parseInfo.date,
                                                                         parseInfo.totalValues,
                                                                         parseInfo.values[0],
                                                                         parseInfo.values[1],
                                                                         parseInfo.values[2],
                                                                         parseInfo.values[3],
                                                                         parseInfo.values[4],
                                                                         parseInfo.values[5]));
                       }
                   },
                   LineParser::dataNext),

        /** Parser for a continuation clock data line. */
        CLOCK_DATA_CONTINUATION((header, line) -> true,
                                (line, parseInfo) -> {
                                    final RinexClockHeader header = parseInfo.file.getHeader();
                                    int start = header.isBefore304() ? 0 : 3;
                                    while (parseInfo.valueIndex < parseInfo.totalValues) {
                                        parseInfo.values[parseInfo.valueIndex++] =
                                            ParsingUtils.parseDouble(line, start, 19);
                                        start += header.isBefore304() ? 20 : 21;
                                    }
                                    parseInfo.file.addClockData(parseInfo.currentName,
                                                                new ClockDataLine(parseInfo.currentDataType,
                                                                                  parseInfo.currentName, parseInfo.date,
                                                                                  parseInfo.totalValues,
                                                                                  parseInfo.values[0],
                                                                                  parseInfo.values[1],
                                                                                  parseInfo.values[2],
                                                                                  parseInfo.values[3],
                                                                                  parseInfo.values[4],
                                                                                  parseInfo.values[5]));
                                },
                                LineParser::dataNext);

        /** Predicate for identifying lines that can be parsed. */
        private final BiFunction<RinexClockHeader, String, Boolean> 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 BiFunction<RinexClockHeader, String, Boolean> 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().isBefore304()) {
                return Arrays.asList(PROGRAM, COMMENT, SYS_NB_TYPES_OF_OBSERV, TIME_SYSTEM_ID,
                                     LEAP_SECONDS, SYS_DCBS_APPLIED, SYS_PCVS_APPLIED,
                                     NB_TYPES_OF_DATA, STATION_NAME_NUM, STATION_CLK_REF,
                                     ANALYSIS_CENTER, NB_OF_CLK_REF, ANALYSIS_CLK_REF,
                                     NB_OF_SOLN_STA_TRF, SOLN_STA_NAME_NUM, NB_OF_SOLN_SATS,
                                     PRN_LIST, HEADER_END);
            } else {
                return Arrays.asList(PROGRAM, COMMENT, SYS_NB_TYPES_OF_OBSERV, TIME_SYSTEM_ID,
                                     LEAP_SECONDS, LEAP_SECONDS_GNSS, SYS_DCBS_APPLIED, SYS_PCVS_APPLIED,
                                     NB_TYPES_OF_DATA, STATION_NAME_NUM, STATION_CLK_REF,
                                     ANALYSIS_CENTER, NB_OF_CLK_REF, ANALYSIS_CLK_REF,
                                     NB_OF_SOLN_STA_TRF, SOLN_STA_NAME_NUM, NB_OF_SOLN_SATS,
                                     PRN_LIST, HEADER_END);
            }
        }

        /** Get the allowed parsers for next lines while parsing comments.
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> commentNext(final ParseInfo parseInfo) {
            return parseInfo.headerCompleted ? headerEndNext(parseInfo) : headerNext(parseInfo);
        }

        /** Get the allowed parsers for next lines while parsing types of observations.
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> sysObsTypesNext(final ParseInfo parseInfo) {
            return parseInfo.remainingObsTypes > 0 ?
                   Collections.singletonList(SYS_NB_TYPES_OF_OBSERV) :
                   headerNext(parseInfo);
        }


        /** Get the allowed parsers for next lines while parsing header end.
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> headerEndNext(final ParseInfo parseInfo) {
            return Collections.singleton(CLOCK_DATA);
        }

        /** Get the allowed parsers for next lines while parsing data.
         * @param parseInfo holder for transient data
         * @return allowed parsers for next line
         */
        private static Iterable<LineParser> dataNext(final ParseInfo parseInfo) {
            return parseInfo.valueIndex < parseInfo.totalValues ?
                   Collections.singleton(LineParser.CLOCK_DATA_CONTINUATION) :
                   Collections.singleton(LineParser.CLOCK_DATA);
        }

    }

    /** 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);
    }

}