RinexClockParser.java

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

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.Reader;
  22. import java.nio.file.Paths;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.Collections;
  26. import java.util.InputMismatchException;
  27. import java.util.List;
  28. import java.util.Locale;
  29. import java.util.Scanner;
  30. import java.util.function.Function;
  31. import java.util.regex.Pattern;

  32. import org.hipparchus.exception.LocalizedCoreFormats;
  33. import org.orekit.annotation.DefaultDataContext;
  34. import org.orekit.data.DataContext;
  35. import org.orekit.data.DataSource;
  36. import org.orekit.errors.OrekitException;
  37. import org.orekit.errors.OrekitMessages;
  38. import org.orekit.files.rinex.AppliedDCBS;
  39. import org.orekit.files.rinex.AppliedPCVS;
  40. import org.orekit.files.rinex.clock.RinexClock.ClockDataType;
  41. import org.orekit.files.rinex.clock.RinexClock.Receiver;
  42. import org.orekit.files.rinex.clock.RinexClock.ReferenceClock;
  43. import org.orekit.frames.Frame;
  44. import org.orekit.gnss.IGSUtils;
  45. import org.orekit.gnss.ObservationType;
  46. import org.orekit.gnss.PredefinedObservationType;
  47. import org.orekit.gnss.SatelliteSystem;
  48. import org.orekit.gnss.TimeSystem;
  49. import org.orekit.time.AbsoluteDate;
  50. import org.orekit.time.DateComponents;
  51. import org.orekit.time.TimeComponents;
  52. import org.orekit.time.TimeScale;
  53. import org.orekit.time.TimeScales;

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

  70.     /** Handled clock file format versions. */
  71.     private static final List<Double> HANDLED_VERSIONS = Arrays.asList(2.00, 3.00, 3.01, 3.02, 3.04);

  72.     /** Pattern for date format yyyy-mm-dd hh:mm. */
  73.     private static final Pattern DATE_PATTERN_1 = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}.*$");

  74.     /** Pattern for date format yyyymmdd hhmmss zone or YYYYMMDD  HHMMSS zone. */
  75.     private static final Pattern DATE_PATTERN_2 = Pattern.compile("^[0-9]{8}\\s{1,2}[0-9]{6}.*$");

  76.     /** Pattern for date format dd-MONTH-yyyy hh:mm zone or d-MONTH-yyyy hh:mm zone. */
  77.     private static final Pattern DATE_PATTERN_3 = Pattern.compile("^[0-9]{1,2}-[a-z,A-Z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}.*$");

  78.     /** Pattern for date format dd-MONTH-yy hh:mm zone or d-MONTH-yy hh:mm zone. */
  79.     private static final Pattern DATE_PATTERN_4 = Pattern.compile("^[0-9]{1,2}-[a-z,A-Z]{3}-[0-9]{2} [0-9]{2}:[0-9]{2}.*$");

  80.     /** Pattern for date format yyyy MONTH dd hh:mm:ss or yyyy MONTH d hh:mm:ss. */
  81.     private static final Pattern DATE_PATTERN_5 = Pattern.compile("^[0-9]{4} [a-z,A-Z]{3} [0-9]{1,2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*$");

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

  84.     /** SYS string for line browsing stop. */
  85.     private static final String SYS = "SYS";

  86.     /** One millimeter, in meters. */
  87.     private static final double MILLIMETER = 1.0e-3;

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

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

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

  96.     /** Create a clock file parser using default values.
  97.      * <p>
  98.      * This constructor uses the {@link DataContext#getDefault() default data context},
  99.      * and {@link IGSUtils#guessFrame} and recognizes only {@link PredefinedObservationType}.
  100.      * </p>
  101.      * @see #RinexClockParser(Function)
  102.      */
  103.     @DefaultDataContext
  104.     public RinexClockParser() {
  105.         this(IGSUtils::guessFrame);
  106.     }

  107.     /** Create a clock file parser and specify the frame builder.
  108.      * <p>
  109.      * This constructor uses the {@link DataContext#getDefault() default data context}
  110.      * and recognizes only {@link PredefinedObservationType}.
  111.      * </p>
  112.      * @param frameBuilder is a function that can construct a frame from a clock file
  113.      *                     coordinate system string. The coordinate system can be
  114.      *                     any 5 character string e.g. ITR92, IGb08.
  115.      * @see #RinexClockParser(Function, Function, TimeScales)
  116.      */
  117.     @DefaultDataContext
  118.     public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder) {
  119.         this(frameBuilder, PredefinedObservationType::valueOf,
  120.              DataContext.getDefault().getTimeScales());
  121.     }

  122.     /** Constructor, build the IGS clock file parser.
  123.      * @param frameBuilder is a function that can construct a frame from a clock file
  124.      *                     coordinate system string. The coordinate system can be
  125.      *                     any 5 character string e.g. ITR92, IGb08.
  126.      * @param typeBuilder mapper from string to observation type
  127.      * @param timeScales   the set of time scales used for parsing dates.
  128.      * @since 13.0
  129.      */
  130.     public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder,
  131.                             final Function<? super String, ? extends ObservationType> typeBuilder,
  132.                             final TimeScales timeScales) {
  133.         this.frameBuilder = frameBuilder;
  134.         this.typeBuilder  = typeBuilder;
  135.         this.timeScales   = timeScales;
  136.     }

  137.     /**
  138.      * Parse an IGS clock file from an input stream using the UTF-8 charset.
  139.      *
  140.      * <p> This method creates a {@link BufferedReader} from the stream and as such this
  141.      * method may read more data than necessary from {@code stream} and the additional
  142.      * data will be lost. The other parse methods do not have this issue.
  143.      *
  144.      * @param stream to read the IGS clock file from
  145.      * @return a parsed IGS clock file
  146.      * @see #parse(String)
  147.      * @see #parse(BufferedReader, String)
  148.      * @see #parse(DataSource)
  149.      */
  150.     public RinexClock parse(final InputStream stream) {
  151.         return parse(new DataSource("<stream>", () -> stream));
  152.     }

  153.     /**
  154.      * Parse an IGS clock file from a file on the local file system.
  155.      * @param fileName file name
  156.      * @return a parsed IGS clock file
  157.      * @see #parse(InputStream)
  158.      * @see #parse(BufferedReader, String)
  159.      * @see #parse(DataSource)
  160.      */
  161.     public RinexClock parse(final String fileName) {
  162.         return parse(new DataSource(Paths.get(fileName).toFile()));
  163.     }

  164.     /**
  165.      * Parse an IGS clock file from a stream.
  166.      * @param reader containing the clock file
  167.      * @param fileName file name
  168.      * @return a parsed IGS clock file
  169.      * @see #parse(InputStream)
  170.      * @see #parse(String)
  171.      * @see #parse(DataSource)
  172.      */
  173.     public RinexClock parse(final BufferedReader reader, final String fileName) {
  174.         return parse(new DataSource(fileName, () -> reader));
  175.     }

  176.     /** Parse an IGS clock file from a {@link DataSource}.
  177.      * @param source source for clock file
  178.      * @return a parsed IGS clock file
  179.      * @see #parse(InputStream)
  180.      * @see #parse(String)
  181.      * @see #parse(BufferedReader, String)
  182.      * @since 12.1
  183.      */
  184.     public RinexClock parse(final DataSource source) {

  185.         // initialize internal data structures
  186.         final ParseInfo pi = new ParseInfo();

  187.         try (Reader reader = source.getOpener().openReaderOnce();
  188.              BufferedReader br = new BufferedReader(reader)) {
  189.             pi.lineNumber = 0;
  190.             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
  191.             nextLine:
  192.             for (String line = br.readLine(); line != null; line = br.readLine()) {
  193.                 ++pi.lineNumber;
  194.                 for (final LineParser candidate : candidateParsers) {
  195.                     if (candidate.canHandle(line)) {
  196.                         try {
  197.                             candidate.parse(line, pi);
  198.                             candidateParsers = candidate.allowedNext();
  199.                             continue nextLine;
  200.                         } catch (StringIndexOutOfBoundsException |
  201.                             NumberFormatException | InputMismatchException e) {
  202.                             throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  203.                                                       pi.lineNumber, source.getName(), line);
  204.                         }
  205.                     }
  206.                 }

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

  210.             }

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

  214.         return pi.file;

  215.     }

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

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

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

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

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

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

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

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

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

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

  236.         /** Current data date components. */
  237.         private DateComponents currentDateComponents;

  238.         /** Current data time components. */
  239.         private TimeComponents currentTimeComponents;

  240.         /** Current data number of data values to follow. */
  241.         private int currentNumberOfValues;

  242.         /** Current data values. */
  243.         private double[] currentDataValues;

  244.         /** Constructor, build the ParseInfo object. */
  245.         protected ParseInfo () {
  246.             this.timeScales = RinexClockParser.this.timeScales;
  247.             this.file = new RinexClock(frameBuilder);
  248.             this.pendingReferenceClocks = new ArrayList<>();
  249.         }

  250.         /** Build an observation type.
  251.          * @param type observation type
  252.          * @return built type
  253.          */
  254.         ObservationType buildType(final String type) {
  255.             return RinexClockParser.this.typeBuilder.apply(type);
  256.         }

  257.     }


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

  260.         /** Parser for version, file type and satellite system. */
  261.         HEADER_VERSION("^.+RINEX VERSION / TYPE( )*$") {

  262.             /** {@inheritDoc} */
  263.             @Override
  264.             public void parse(final String line, final ParseInfo pi) {
  265.                 try (Scanner s1      = new Scanner(line);
  266.                      Scanner s2      = s1.useDelimiter(SPACES);
  267.                      Scanner scanner = s2.useLocale(Locale.US)) {

  268.                     // First element of the line is format version
  269.                     final double version = scanner.nextDouble();

  270.                     // Throw exception if format version is not handled
  271.                     if (!HANDLED_VERSIONS.contains(version)) {
  272.                         throw new OrekitException(OrekitMessages.CLOCK_FILE_UNSUPPORTED_VERSION, version);
  273.                     }

  274.                     pi.file.setFormatVersion(version);

  275.                     // Second element is clock file indicator, not used here

  276.                     // Last element is the satellite system, might be missing
  277.                     final String satelliteSystemString = line.substring(40, 45).trim();

  278.                     // Check satellite if system is recorded
  279.                     if (!satelliteSystemString.isEmpty()) {
  280.                         // Record satellite system and default time system in clock file object
  281.                         final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(satelliteSystemString);
  282.                         pi.file.setSatelliteSystem(satelliteSystem);
  283.                         if (satelliteSystem.getObservationTimeScale() != null) {
  284.                             pi.file.setTimeScale(satelliteSystem.getObservationTimeScale().getTimeScale(pi.timeScales));
  285.                         }
  286.                     }
  287.                     // Set time scale to UTC by default
  288.                     if (pi.file.getTimeScale() == null) {
  289.                         pi.file.setTimeScale(pi.timeScales.getUTC());
  290.                     }
  291.                 }
  292.             }

  293.         },

  294.         /** Parser for generating program and emiting agency. */
  295.         HEADER_PROGRAM("^.+PGM / RUN BY / DATE( )*$") {

  296.             /** {@inheritDoc} */
  297.             @Override
  298.             public void parse(final String line, final ParseInfo pi) {

  299.                 // First element of the name of the generating program
  300.                 final String programName = line.substring(0, 20).trim();
  301.                 pi.file.setProgramName(programName);

  302.                 // Second element is the name of the emiting agency
  303.                 final String agencyName = line.substring(20, 40).trim();
  304.                 pi.file.setAgencyName(agencyName);

  305.                 // Third element is date
  306.                 String dateString = "";

  307.                 if (pi.file.getFormatVersion() < 3.04) {

  308.                     // Date string location before 3.04 format version
  309.                     dateString = line.substring(40, 60);

  310.                 } else {

  311.                     // Date string location after 3.04 format version
  312.                     dateString = line.substring(42, 65);

  313.                 }

  314.                 parseDateTimeZone(dateString, pi);

  315.             }

  316.         },

  317.         /** Parser for comments. */
  318.         HEADER_COMMENT("^.+COMMENT( )*$") {

  319.             /** {@inheritDoc} */
  320.             @Override
  321.             public void parse(final String line, final ParseInfo pi) {

  322.                 if (pi.file.getFormatVersion() < 3.04) {
  323.                     pi.file.addComment(line.substring(0, 60).trim());
  324.                 } else {
  325.                     pi.file.addComment(line.substring(0, 65).trim());
  326.                 }
  327.             }

  328.         },

  329.         /** Parser for satellite system and related observation types. */
  330.         HEADER_SYSTEM_OBS("^[A-Z] .*SYS / # / OBS TYPES( )*$") {

  331.             /** {@inheritDoc} */
  332.             @Override
  333.             public void parse(final String line, final ParseInfo pi) {
  334.                 try (Scanner s1      = new Scanner(line);
  335.                      Scanner s2      = s1.useDelimiter(SPACES);
  336.                      Scanner scanner = s2.useLocale(Locale.US)) {

  337.                     // First element of the line is satellite system code
  338.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(scanner.next());
  339.                     pi.currentSatelliteSystem = satelliteSystem;

  340.                     // Second element is the number of different observation types
  341.                     scanner.nextInt();

  342.                     // Parse all observation types
  343.                     String currentObsType = scanner.next();
  344.                     while (!currentObsType.equals(SYS)) {
  345.                         pi.file.addSystemObservationType(satelliteSystem, pi.buildType(currentObsType));
  346.                         currentObsType = scanner.next();
  347.                     }
  348.                 }
  349.             }

  350.         },

  351.         /** Parser for continuation of satellite system and related observation types. */
  352.         HEADER_SYSTEM_OBS_CONTINUATION("^ .*SYS / # / OBS TYPES( )*$") {

  353.             /** {@inheritDoc} */
  354.             @Override
  355.             public void parse(final String line, final ParseInfo pi) {
  356.                 try (Scanner s1      = new Scanner(line);
  357.                      Scanner s2      = s1.useDelimiter(SPACES);
  358.                      Scanner scanner = s2.useLocale(Locale.US)) {

  359.                     // This is a continuation line, there are only observation types
  360.                     // Parse all observation types
  361.                     String currentObsType = scanner.next();
  362.                     while (!currentObsType.equals(SYS)) {
  363.                         pi.file.addSystemObservationType(pi.currentSatelliteSystem, pi.buildType(currentObsType));
  364.                         currentObsType = scanner.next();
  365.                     }
  366.                 }
  367.             }

  368.         },

  369.         /** Parser for data time system. */
  370.         HEADER_TIME_SYSTEM("^.+TIME SYSTEM ID( )*$") {

  371.             /** {@inheritDoc} */
  372.             @Override
  373.             public void parse(final String line, final ParseInfo pi) {
  374.                 try (Scanner s1      = new Scanner(line);
  375.                      Scanner s2      = s1.useDelimiter(SPACES);
  376.                      Scanner scanner = s2.useLocale(Locale.US)) {

  377.                     // Only element is the time system code
  378.                     final TimeSystem timeSystem = TimeSystem.parseTimeSystem(scanner.next());
  379.                     final TimeScale timeScale = timeSystem.getTimeScale(pi.timeScales);
  380.                     pi.file.setTimeSystem(timeSystem);
  381.                     pi.file.setTimeScale(timeScale);
  382.                 }
  383.             }

  384.         },

  385.         /** Parser for leap seconds. */
  386.         HEADER_LEAP_SECONDS("^.+LEAP SECONDS( )*$") {

  387.             /** {@inheritDoc} */
  388.             @Override
  389.             public void parse(final String line, final ParseInfo pi) {
  390.                 try (Scanner s1      = new Scanner(line);
  391.                      Scanner s2      = s1.useDelimiter(SPACES);
  392.                      Scanner scanner = s2.useLocale(Locale.US)) {

  393.                     // Only element is the number of leap seconds
  394.                     final int numberOfLeapSeconds = scanner.nextInt();
  395.                     pi.file.setNumberOfLeapSeconds(numberOfLeapSeconds);
  396.                 }
  397.             }

  398.         },

  399.         /** Parser for leap seconds GNSS. */
  400.         HEADER_LEAP_SECONDS_GNSS("^.+LEAP SECONDS GNSS( )*$") {

  401.             /** {@inheritDoc} */
  402.             @Override
  403.             public void parse(final String line, final ParseInfo pi) {
  404.                 try (Scanner s1      = new Scanner(line);
  405.                      Scanner s2      = s1.useDelimiter(SPACES);
  406.                      Scanner scanner = s2.useLocale(Locale.US)) {

  407.                     // Only element is the number of leap seconds GNSS
  408.                     final int numberOfLeapSecondsGNSS = scanner.nextInt();
  409.                     pi.file.setNumberOfLeapSecondsGNSS(numberOfLeapSecondsGNSS);
  410.                 }
  411.             }

  412.         },

  413.         /** Parser for applied differencial code bias corrections. */
  414.         HEADER_DCBS("^.+SYS / DCBS APPLIED( )*$") {

  415.             /** {@inheritDoc} */
  416.             @Override
  417.             public void parse(final String line, final ParseInfo pi) {
  418.                 // First element, if present, is the related satellite system
  419.                 final String system = line.substring(0, 1);
  420.                 if (!" ".equals(system)) {
  421.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(system);

  422.                     // Second element is the program name
  423.                     final String progDCBS = line.substring(2, 20).trim();

  424.                     // Third element is the source of the corrections
  425.                     String sourceDCBS = "";
  426.                     if (pi.file.getFormatVersion() < 3.04) {
  427.                         sourceDCBS = line.substring(19, 60).trim();
  428.                     } else {
  429.                         sourceDCBS = line.substring(22, 65).trim();
  430.                     }

  431.                     // Check if sought fields were not actually blanks
  432.                     if (!progDCBS.isEmpty()) {
  433.                         pi.file.addAppliedDCBS(new AppliedDCBS(satelliteSystem, progDCBS, sourceDCBS));
  434.                     }
  435.                 }
  436.             }

  437.         },

  438.         /** Parser for applied phase center variation corrections. */
  439.         HEADER_PCVS("^.+SYS / PCVS APPLIED( )*$") {

  440.             /** {@inheritDoc} */
  441.             @Override
  442.             public void parse(final String line, final ParseInfo pi) {

  443.                 // First element, if present, is the related satellite system
  444.                 final String system = line.substring(0, 1);
  445.                 if (!" ".equals(system)) {
  446.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(system);

  447.                     // Second element is the program name
  448.                     final String progPCVS = line.substring(2, 20).trim();

  449.                     // Third element is the source of the corrections
  450.                     String sourcePCVS = "";
  451.                     if (pi.file.getFormatVersion() < 3.04) {
  452.                         sourcePCVS = line.substring(19, 60).trim();
  453.                     } else {
  454.                         sourcePCVS = line.substring(22, 65).trim();
  455.                     }

  456.                     // Check if sought fields were not actually blanks
  457.                     if (!progPCVS.isEmpty() || !sourcePCVS.isEmpty()) {
  458.                         pi.file.addAppliedPCVS(new AppliedPCVS(satelliteSystem, progPCVS, sourcePCVS));
  459.                     }
  460.                 }
  461.             }

  462.         },

  463.         /** Parser for the different clock data types that are stored in the file. */
  464.         HEADER_TYPES_OF_DATA("^.+# / TYPES OF DATA( )*$") {

  465.             /** {@inheritDoc} */
  466.             @Override
  467.             public void parse(final String line, final ParseInfo pi) {
  468.                 try (Scanner s1      = new Scanner(line);
  469.                      Scanner s2      = s1.useDelimiter(SPACES);
  470.                      Scanner scanner = s2.useLocale(Locale.US)) {

  471.                     // First element is the number of different types of data
  472.                     final int numberOfDifferentDataTypes = scanner.nextInt();

  473.                     // Loop over data types
  474.                     for (int i = 0; i < numberOfDifferentDataTypes; i++) {
  475.                         final ClockDataType dataType = ClockDataType.parseClockDataType(scanner.next());
  476.                         pi.file.addClockDataType(dataType);
  477.                     }
  478.                 }
  479.             }

  480.         },

  481.         /** Parser for the station with reference clock. */
  482.         HEADER_STATIONS_NAME("^.+STATION NAME / NUM( )*$") {

  483.             /** {@inheritDoc} */
  484.             @Override
  485.             public void parse(final String line, final ParseInfo pi) {
  486.                 try (Scanner s1      = new Scanner(line);
  487.                      Scanner s2      = s1.useDelimiter(SPACES);
  488.                      Scanner scanner = s2.useLocale(Locale.US)) {

  489.                     // First element is the station clock reference ID
  490.                     final String stationName = scanner.next();
  491.                     pi.file.setStationName(stationName);

  492.                     // Second element is the station clock reference identifier
  493.                     final String stationIdentifier = scanner.next();
  494.                     pi.file.setStationIdentifier(stationIdentifier);
  495.                 }
  496.             }

  497.         },

  498.         /** Parser for the reference clock in case of calibration data. */
  499.         HEADER_STATION_CLOCK_REF("^.+STATION CLK REF( )*$") {

  500.             /** {@inheritDoc} */
  501.             @Override
  502.             public void parse(final String line, final ParseInfo pi) {
  503.                 if (pi.file.getFormatVersion() < 3.04) {
  504.                     pi.file.setExternalClockReference(line.substring(0, 60).trim());
  505.                 } else {
  506.                     pi.file.setExternalClockReference(line.substring(0, 65).trim());
  507.                 }
  508.             }

  509.         },

  510.         /** Parser for the analysis center. */
  511.         HEADER_ANALYSIS_CENTER("^.+ANALYSIS CENTER( )*$") {

  512.             /** {@inheritDoc} */
  513.             @Override
  514.             public void parse(final String line, final ParseInfo pi) {

  515.                 // First element is IGS AC designator
  516.                 final String analysisCenterID = line.substring(0, 3).trim();
  517.                 pi.file.setAnalysisCenterID(analysisCenterID);

  518.                 // Then, the full name of the analysis center
  519.                 String analysisCenterName = "";
  520.                 if (pi.file.getFormatVersion() < 3.04) {
  521.                     analysisCenterName = line.substring(5, 60).trim();
  522.                 } else {
  523.                     analysisCenterName = line.substring(5, 65).trim();
  524.                 }
  525.                 pi.file.setAnalysisCenterName(analysisCenterName);
  526.             }

  527.         },

  528.         /** Parser for the number of reference clocks over a period. */
  529.         HEADER_NUMBER_OF_CLOCK_REF("^.+# OF CLK REF( )*$") {

  530.             /** {@inheritDoc} */
  531.             @Override
  532.             public void parse(final String line, final ParseInfo pi) {
  533.                 try (Scanner s1      = new Scanner(line);
  534.                      Scanner s2      = s1.useDelimiter(SPACES);
  535.                      Scanner scanner = s2.useLocale(Locale.US)) {

  536.                     if (!pi.pendingReferenceClocks.isEmpty()) {
  537.                         // Modify time span map of the reference clocks to accept the pending reference clock
  538.                         pi.file.addReferenceClockList(pi.pendingReferenceClocks,
  539.                                                       pi.referenceClockStartDate);
  540.                         pi.pendingReferenceClocks = new ArrayList<>();
  541.                     }

  542.                     // First element is the number of reference clocks corresponding to the period
  543.                     scanner.nextInt();

  544.                     if (scanner.hasNextInt()) {
  545.                         // Second element is the start epoch of the period
  546.                         final int startYear   = scanner.nextInt();
  547.                         final int startMonth  = scanner.nextInt();
  548.                         final int startDay    = scanner.nextInt();
  549.                         final int startHour   = scanner.nextInt();
  550.                         final int startMin    = scanner.nextInt();
  551.                         final double startSec = scanner.nextDouble();
  552.                         final AbsoluteDate startEpoch = new AbsoluteDate(startYear, startMonth, startDay,
  553.                                                                          startHour, startMin, startSec,
  554.                                                                          pi.file.getTimeScale());
  555.                         pi.referenceClockStartDate = startEpoch;

  556.                         // Third element is the end epoch of the period
  557.                         final int endYear   = scanner.nextInt();
  558.                         final int endMonth  = scanner.nextInt();
  559.                         final int endDay    = scanner.nextInt();
  560.                         final int endHour   = scanner.nextInt();
  561.                         final int endMin    = scanner.nextInt();
  562.                         double endSec       = 0.0;
  563.                         if (pi.file.getFormatVersion() < 3.04) {
  564.                             endSec = Double.parseDouble(line.substring(51, 60));
  565.                         } else {
  566.                             endSec = scanner.nextDouble();
  567.                         }
  568.                         final AbsoluteDate endEpoch = new AbsoluteDate(endYear, endMonth, endDay,
  569.                                                                        endHour, endMin, endSec,
  570.                                                                        pi.file.getTimeScale());
  571.                         pi.referenceClockEndDate = endEpoch;
  572.                     } else {
  573.                         pi.referenceClockStartDate = AbsoluteDate.PAST_INFINITY;
  574.                         pi.referenceClockEndDate = AbsoluteDate.FUTURE_INFINITY;
  575.                     }
  576.                 }
  577.             }

  578.         },

  579.         /** Parser for the reference clock over a period. */
  580.         HEADER_ANALYSIS_CLOCK_REF("^.+ANALYSIS CLK REF( )*$") {

  581.             /** {@inheritDoc} */
  582.             @Override
  583.             public void parse(final String line, final ParseInfo pi) {
  584.                 try (Scanner s1      = new Scanner(line);
  585.                      Scanner s2      = s1.useDelimiter(SPACES);
  586.                      Scanner scanner = s2.useLocale(Locale.US)) {

  587.                     // First element is the name of the receiver/satellite embedding the reference clock
  588.                     final String referenceName = scanner.next();

  589.                     // Second element is the reference clock ID
  590.                     final String clockID = scanner.next();

  591.                     // Optionally, third element is an a priori clock constraint, by default equal to zero
  592.                     double clockConstraint = 0.0;
  593.                     if (scanner.hasNextDouble()) {
  594.                         clockConstraint = scanner.nextDouble();
  595.                     }

  596.                     // Add reference clock to current reference clock list
  597.                     final ReferenceClock referenceClock = new ReferenceClock(referenceName, clockID, clockConstraint,
  598.                                                                              pi.referenceClockStartDate, pi.referenceClockEndDate);
  599.                     pi.pendingReferenceClocks.add(referenceClock);

  600.                 }
  601.             }

  602.         },

  603.         /** Parser for the number of stations embedded in the file and the related frame. */
  604.         HEADER_NUMBER_OF_SOLN_STATIONS("^.+SOLN STA / TRF( )*$") {

  605.             /** {@inheritDoc} */
  606.             @Override
  607.             public void parse(final String line, final ParseInfo pi) {
  608.                 try (Scanner s1      = new Scanner(line);
  609.                      Scanner s2      = s1.useDelimiter(SPACES);
  610.                      Scanner scanner = s2.useLocale(Locale.US)) {

  611.                     // First element is the number of receivers embedded in the file
  612.                     scanner.nextInt();

  613.                     // Second element is the frame linked to given receiver positions
  614.                     final String frameString = scanner.next();
  615.                     pi.file.setFrameName(frameString);
  616.                 }
  617.             }

  618.         },

  619.         /** Parser for the stations embedded in the file and the related positions. */
  620.         HEADER_SOLN_STATIONS("^.+SOLN STA NAME / NUM( )*$") {

  621.             /** {@inheritDoc} */
  622.             @Override
  623.             public void parse(final String line, final ParseInfo pi) {

  624.                 // First element is the receiver designator
  625.                 String designator = line.substring(0, 10).trim();

  626.                 // Second element is the receiver identifier
  627.                 String receiverIdentifier = line.substring(10, 30).trim();

  628.                 // Third element if X coordinates, in millimeters in the file frame.
  629.                 String xString = "";

  630.                 // Fourth element if Y coordinates, in millimeters in the file frame.
  631.                 String yString = "";

  632.                 // Fifth element if Z coordinates, in millimeters in the file frame.
  633.                 String zString = "";

  634.                 if (pi.file.getFormatVersion() < 3.04) {
  635.                     designator = line.substring(0, 4).trim();
  636.                     receiverIdentifier = line.substring(5, 25).trim();
  637.                     xString = line.substring(25, 36).trim();
  638.                     yString = line.substring(37, 48).trim();
  639.                     zString = line.substring(49, 60).trim();
  640.                 } else {
  641.                     designator = line.substring(0, 10).trim();
  642.                     receiverIdentifier = line.substring(10, 30).trim();
  643.                     xString = line.substring(30, 41).trim();
  644.                     yString = line.substring(42, 53).trim();
  645.                     zString = line.substring(54, 65).trim();
  646.                 }

  647.                 final double x = MILLIMETER * Double.parseDouble(xString);
  648.                 final double y = MILLIMETER * Double.parseDouble(yString);
  649.                 final double z = MILLIMETER * Double.parseDouble(zString);

  650.                 final Receiver receiver = new Receiver(designator, receiverIdentifier, x, y, z);
  651.                 pi.file.addReceiver(receiver);

  652.             }

  653.         },

  654.         /** Parser for the number of satellites embedded in the file. */
  655.         HEADER_NUMBER_OF_SOLN_SATS("^.+# OF SOLN SATS( )*$") {

  656.             /** {@inheritDoc} */
  657.             @Override
  658.             public void parse(final String line, final ParseInfo pi) {

  659.                     // Only element in the line is number of satellites, not used here.
  660.                     // Do nothing...
  661.             }

  662.         },

  663.         /** Parser for the satellites embedded in the file. */
  664.         HEADER_PRN_LIST("^.+PRN LIST( )*$") {

  665.             /** {@inheritDoc} */
  666.             @Override
  667.             public void parse(final String line, final ParseInfo pi) {
  668.                 try (Scanner s1      = new Scanner(line);
  669.                      Scanner s2      = s1.useDelimiter(SPACES);
  670.                      Scanner scanner = s2.useLocale(Locale.US)) {

  671.                     // Only PRN numbers are stored in these lines
  672.                     // Initialize first PRN number
  673.                     String prn = scanner.next();

  674.                     // Browse the line until its end
  675.                     while (!prn.equals("PRN")) {
  676.                         pi.file.addSatellite(prn);
  677.                         prn = scanner.next();
  678.                     }
  679.                 }
  680.             }

  681.         },

  682.         /** Parser for the end of header. */
  683.         HEADER_END("^.+END OF HEADER( )*$") {

  684.             /** {@inheritDoc} */
  685.             @Override
  686.             public void parse(final String line, final ParseInfo pi) {
  687.                 if (!pi.pendingReferenceClocks.isEmpty()) {
  688.                     // Modify time span map of the reference clocks to accept the pending reference clock
  689.                     pi.file.addReferenceClockList(pi.pendingReferenceClocks, pi.referenceClockStartDate);
  690.                 }
  691.             }

  692.             /** {@inheritDoc} */
  693.             @Override
  694.             public Iterable<LineParser> allowedNext() {
  695.                 return Collections.singleton(CLOCK_DATA);
  696.             }
  697.         },

  698.         /** Parser for a clock data line. */
  699.         CLOCK_DATA("(^AR |^AS |^CR |^DR |^MS ).+$") {

  700.             /** {@inheritDoc} */
  701.             @Override
  702.             public void parse(final String line, final ParseInfo pi) {
  703.                 try (Scanner s1      = new Scanner(line);
  704.                      Scanner s2      = s1.useDelimiter(SPACES);
  705.                      Scanner scanner = s2.useLocale(Locale.US)) {

  706.                     // Initialise current values
  707.                     pi.currentDataValues = new double[6];

  708.                     // First element is clock data type
  709.                     pi.currentDataType = ClockDataType.parseClockDataType(scanner.next());

  710.                     // Second element is receiver/satellite name
  711.                     pi.currentName = scanner.next();

  712.                     // Third element is data epoch
  713.                     final int year   = scanner.nextInt();
  714.                     final int month  = scanner.nextInt();
  715.                     final int day    = scanner.nextInt();
  716.                     final int hour   = scanner.nextInt();
  717.                     final int min    = scanner.nextInt();
  718.                     final double sec = scanner.nextDouble();
  719.                     pi.currentDateComponents = new DateComponents(year, month, day);
  720.                     pi.currentTimeComponents = new TimeComponents(hour, min, sec);

  721.                     // Fourth element is number of data values
  722.                     pi.currentNumberOfValues = scanner.nextInt();

  723.                     // Get the values in this line, there are at most 2.
  724.                     // Some entries claim less values than there actually are.
  725.                     // All values are added to the set, regardless of their claimed number.
  726.                     int i = 0;
  727.                     while (scanner.hasNextDouble()) {
  728.                         pi.currentDataValues[i++] = scanner.nextDouble();
  729.                     }

  730.                     // Check if continuation line is required
  731.                     if (pi.currentNumberOfValues <= 2) {
  732.                         // No continuation line is required
  733.                         pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
  734.                                                                                        pi.currentName,
  735.                                                                                        pi.currentDateComponents,
  736.                                                                                        pi.currentTimeComponents,
  737.                                                                                        pi.currentNumberOfValues,
  738.                                                                                        pi.currentDataValues[0],
  739.                                                                                        pi.currentDataValues[1],
  740.                                                                                        0.0, 0.0, 0.0, 0.0));
  741.                     }
  742.                 }
  743.             }

  744.             /** {@inheritDoc} */
  745.             @Override
  746.             public Iterable<LineParser> allowedNext() {
  747.                 return Arrays.asList(CLOCK_DATA, CLOCK_DATA_CONTINUATION);
  748.             }
  749.         },

  750.         /** Parser for a continuation clock data line. */
  751.         CLOCK_DATA_CONTINUATION("^   .+") {

  752.             /** {@inheritDoc} */
  753.             @Override
  754.             public void parse(final String line, final ParseInfo pi) {
  755.                 try (Scanner s1      = new Scanner(line);
  756.                      Scanner s2      = s1.useDelimiter(SPACES);
  757.                      Scanner scanner = s2.useLocale(Locale.US)) {

  758.                     // Get the values in this continuation line.
  759.                     // Some entries claim less values than there actually are.
  760.                     // All values are added to the set, regardless of their claimed number.
  761.                     int i = 2;
  762.                     while (scanner.hasNextDouble()) {
  763.                         pi.currentDataValues[i++] = scanner.nextDouble();
  764.                     }

  765.                     // Add clock data line
  766.                     pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
  767.                                                                                    pi.currentName,
  768.                                                                                    pi.currentDateComponents,
  769.                                                                                    pi.currentTimeComponents,
  770.                                                                                    pi.currentNumberOfValues,
  771.                                                                                    pi.currentDataValues[0],
  772.                                                                                    pi.currentDataValues[1],
  773.                                                                                    pi.currentDataValues[2],
  774.                                                                                    pi.currentDataValues[3],
  775.                                                                                    pi.currentDataValues[4],
  776.                                                                                    pi.currentDataValues[5]));

  777.                 }
  778.             }

  779.             /** {@inheritDoc} */
  780.             @Override
  781.             public Iterable<LineParser> allowedNext() {
  782.                 return Collections.singleton(CLOCK_DATA);
  783.             }
  784.         };

  785.         /** Pattern for identifying line. */
  786.         private final Pattern pattern;

  787.         /** Simple constructor.
  788.          * @param lineRegexp regular expression for identifying line
  789.          */
  790.         LineParser(final String lineRegexp) {
  791.             pattern = Pattern.compile(lineRegexp);
  792.         }

  793.         /** Parse a line.
  794.          * @param line line to parse
  795.          * @param pi holder for transient data
  796.          */
  797.         public abstract void parse(String line, ParseInfo pi);

  798.         /** Get the allowed parsers for next line.
  799.          * <p>
  800.          * Because the standard only recommends an order for header keys,
  801.          * the default implementation of the method returns all the
  802.          * header keys. Specific implementations must overrides the method.
  803.          * </p>
  804.          * @return allowed parsers for next line
  805.          */
  806.         public Iterable<LineParser> allowedNext() {
  807.             return Arrays.asList(HEADER_PROGRAM, HEADER_COMMENT, HEADER_SYSTEM_OBS, HEADER_SYSTEM_OBS_CONTINUATION, HEADER_TIME_SYSTEM, HEADER_LEAP_SECONDS,
  808.                                  HEADER_LEAP_SECONDS_GNSS, HEADER_DCBS, HEADER_PCVS, HEADER_TYPES_OF_DATA, HEADER_STATIONS_NAME, HEADER_STATION_CLOCK_REF,
  809.                                  HEADER_ANALYSIS_CENTER, HEADER_NUMBER_OF_CLOCK_REF, HEADER_ANALYSIS_CLOCK_REF, HEADER_NUMBER_OF_SOLN_STATIONS,
  810.                                  HEADER_SOLN_STATIONS, HEADER_NUMBER_OF_SOLN_SATS, HEADER_PRN_LIST, HEADER_END);
  811.         }

  812.         /** Check if parser can handle line.
  813.          * @param line line to parse
  814.          * @return true if parser can handle the specified line
  815.          */
  816.         public boolean canHandle(final String line) {
  817.             return pattern.matcher(line).matches();
  818.         }

  819.         /** Parse existing date - time - zone formats.
  820.          * If zone field is not missing, a proper Orekit date can be created and set into clock file object.
  821.          * This feature depends on the date format.
  822.          * @param dateString the whole date - time - zone string
  823.          * @param pi holder for transient data
  824.          */
  825.         private static void parseDateTimeZone(final String dateString, final ParseInfo pi) {

  826.             String date = "";
  827.             String time = "";
  828.             String zone = "";
  829.             DateComponents dateComponents = null;
  830.             TimeComponents timeComponents = null;

  831.             if (DATE_PATTERN_1.matcher(dateString).matches()) {

  832.                 date = dateString.substring(0, 10).trim();
  833.                 time = dateString.substring(11, 16).trim();
  834.                 zone = dateString.substring(16).trim();

  835.             } else if (DATE_PATTERN_2.matcher(dateString).matches()) {

  836.                 date = dateString.substring(0, 8).trim();
  837.                 time = dateString.substring(9, 16).trim();
  838.                 zone = dateString.substring(16).trim();

  839.                 if (!zone.isEmpty()) {
  840.                     // Get date and time components
  841.                     dateComponents = new DateComponents(Integer.parseInt(date.substring(0, 4)),
  842.                                                         Integer.parseInt(date.substring(4, 6)),
  843.                                                         Integer.parseInt(date.substring(6, 8)));
  844.                     timeComponents = new TimeComponents(Integer.parseInt(time.substring(0, 2)),
  845.                                                         Integer.parseInt(time.substring(2, 4)),
  846.                                                         Integer.parseInt(time.substring(4, 6)));

  847.                 }

  848.             } else if (DATE_PATTERN_3.matcher(dateString).matches()) {

  849.                 date = dateString.substring(0, 11).trim();
  850.                 time = dateString.substring(11, 17).trim();
  851.                 zone = dateString.substring(17).trim();

  852.             } else if (DATE_PATTERN_4.matcher(dateString).matches()) {

  853.                 date = dateString.substring(0, 9).trim();
  854.                 time = dateString.substring(9, 15).trim();
  855.                 zone = dateString.substring(15).trim();

  856.             } else if (DATE_PATTERN_5.matcher(dateString).matches()) {

  857.                 date = dateString.substring(0, 11).trim();
  858.                 time = dateString.substring(11, 20).trim();

  859.             } else {
  860.                 // Format is not handled or date is missing. Do nothing...
  861.             }

  862.             pi.file.setCreationDateString(date);
  863.             pi.file.setCreationTimeString(time);
  864.             pi.file.setCreationTimeZoneString(zone);

  865.             if (dateComponents != null) {
  866.                 pi.file.setCreationDate(new AbsoluteDate(dateComponents,
  867.                                                          timeComponents,
  868.                                                          TimeSystem.parseTimeSystem(zone).getTimeScale(pi.timeScales)));
  869.             }
  870.         }
  871.     }

  872. }