RinexClockParser.java

  1. /* Copyright 2002-2022 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.gnss.clock;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.nio.file.Files;
  24. import java.nio.file.Paths;
  25. import java.util.ArrayList;
  26. import java.util.Arrays;
  27. import java.util.InputMismatchException;
  28. import java.util.List;
  29. import java.util.Locale;
  30. import java.util.Optional;
  31. import java.util.Scanner;
  32. import java.util.function.Function;
  33. import java.util.regex.Pattern;
  34. import java.util.stream.Stream;

  35. import org.orekit.annotation.DefaultDataContext;
  36. import org.orekit.data.DataContext;
  37. import org.orekit.errors.OrekitException;
  38. import org.orekit.errors.OrekitMessages;
  39. import org.orekit.frames.Frame;
  40. import org.orekit.gnss.AppliedDCBS;
  41. import org.orekit.gnss.AppliedPCVS;
  42. import org.orekit.gnss.ObservationType;
  43. import org.orekit.gnss.SatelliteSystem;
  44. import org.orekit.gnss.TimeSystem;
  45. import org.orekit.gnss.clock.RinexClock.ClockDataType;
  46. import org.orekit.gnss.clock.RinexClock.Receiver;
  47. import org.orekit.gnss.clock.RinexClock.ReferenceClock;
  48. import org.orekit.time.AbsoluteDate;
  49. import org.orekit.time.DateComponents;
  50. import org.orekit.time.TimeComponents;
  51. import org.orekit.time.TimeScale;
  52. import org.orekit.time.TimeScales;
  53. import org.orekit.utils.IERSConventions;

  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.     /**
  93.      * Create an clock file parser using default values.
  94.      *
  95.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  96.      *
  97.      * @see #RinexClockParser(Function)
  98.      */
  99.     @DefaultDataContext
  100.     public RinexClockParser() {
  101.         this(RinexClockParser::guessFrame);
  102.     }

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

  117.     /** Constructor, build the IGS clock file parser.
  118.      * @param frameBuilder is a function that can construct a frame from a clock file
  119.      *                     coordinate system string. The coordinate system can be
  120.      *                     any 5 character string e.g. ITR92, IGb08.
  121.      * @param timeScales   the set of time scales used for parsing dates.
  122.      */
  123.     public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder,
  124.                            final TimeScales timeScales) {

  125.         this.frameBuilder = frameBuilder;
  126.         this.timeScales   = timeScales;
  127.     }

  128.     /**
  129.      * Default string to {@link Frame} conversion for {@link #CLockFileParser()}.
  130.      *
  131.      * <p>This method uses the {@link DataContext#getDefault() default data context}.
  132.      *
  133.      * @param name of the frame.
  134.      * @return by default, return ITRF based on 2010 conventions,
  135.      *         with tidal effects considered during EOP interpolation.
  136.      * <p>If String matches to other already recorded frames, it will return the corresponding frame.</p>
  137.      * Already embedded frames are:
  138.      * <p> - ITRF96
  139.      */
  140.     @DefaultDataContext
  141.     private static Frame guessFrame(final String name) {
  142.         if (name.equals("ITRF96")) {
  143.             return DataContext.getDefault().getFrames()
  144.                               .getITRF(IERSConventions.IERS_1996, false);
  145.         } else {
  146.             return DataContext.getDefault().getFrames()
  147.                               .getITRF(IERSConventions.IERS_2010, false);
  148.         }
  149.     }

  150.     /**
  151.      * Parse an IGS clock file from an input stream using the UTF-8 charset.
  152.      *
  153.      * <p> This method creates a {@link BufferedReader} from the stream and as such this
  154.      * method may read more data than necessary from {@code stream} and the additional
  155.      * data will be lost. The other parse methods do not have this issue.
  156.      *
  157.      * @param stream to read the IGS clock file from
  158.      * @return a parsed IGS clock file
  159.      * @throws IOException if {@code stream} throws one
  160.      * @see #parse(String)
  161.      * @see #parse(BufferedReader, String)
  162.      */
  163.     public RinexClock parse(final InputStream stream) throws IOException {
  164.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
  165.             return parse(reader, stream.toString());
  166.         }
  167.     }

  168.     /**
  169.      * Parse an IGS clock file from a file on the local file system.
  170.      * @param fileName file name
  171.      * @return a parsed IGS clock file
  172.      * @throws IOException if one is thrown while opening or reading from {@code fileName}
  173.      * @see #parse(InputStream)
  174.      * @see #parse(BufferedReader, String)
  175.      */
  176.     public RinexClock parse(final String fileName) throws IOException {
  177.         try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName),
  178.                                                              StandardCharsets.UTF_8)) {
  179.             return parse(reader, fileName);
  180.         }
  181.     }

  182.     /**
  183.      * Parse an IGS clock file from a stream.
  184.      * @param reader containing the clock file
  185.      * @param fileName file name
  186.      * @return a parsed IGS clock file
  187.      * @throws IOException if {@code reader} throws one
  188.      * @see #parse(InputStream)
  189.      * @see #parse(String)
  190.      */
  191.     public RinexClock parse(final BufferedReader reader,
  192.                            final String fileName) throws IOException {

  193.         // initialize internal data structures
  194.         final ParseInfo pi = new ParseInfo();

  195.         int lineNumber = 0;
  196.         Stream<LineParser> candidateParsers = Stream.of(LineParser.HEADER_VERSION);
  197.         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  198.             ++lineNumber;
  199.             final String l = line;
  200.             final Optional<LineParser> selected = candidateParsers.filter(p -> p.canHandle(l)).findFirst();
  201.             if (selected.isPresent()) {
  202.                 try {
  203.                     selected.get().parse(line, pi);
  204.                 } catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
  205.                     throw new OrekitException(e,
  206.                                               OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  207.                                               lineNumber, fileName, line);
  208.                 }
  209.                 candidateParsers = selected.get().allowedNext();
  210.             } else {
  211.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  212.                                           lineNumber, fileName, line);
  213.             }
  214.         }

  215.         return pi.file;

  216.     }

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

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

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

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

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

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

  229.         /** Current reference clock list. */
  230.         private List<ReferenceClock> currentReferenceClocks;

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

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

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

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

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

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

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


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

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

  253.             /** {@inheritDoc} */
  254.             @Override
  255.             public void parse(final String line, final ParseInfo pi) {
  256.                 try (Scanner s1      = new Scanner(line);
  257.                      Scanner s2      = s1.useDelimiter(SPACES);
  258.                      Scanner scanner = s2.useLocale(Locale.US)) {

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

  261.                     // Throw exception if format version is not handled
  262.                     if (!HANDLED_VERSIONS.contains(version)) {
  263.                         throw new OrekitException(OrekitMessages.CLOCK_FILE_UNSUPPORTED_VERSION, version);
  264.                     }

  265.                     pi.file.setFormatVersion(version);

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

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

  269.                     // Check satellite if system is recorded
  270.                     if (!satelliteSystemString.equals("")) {
  271.                         // Record satellite system and default time system in clock file object
  272.                         final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(satelliteSystemString);
  273.                         pi.file.setSatelliteSystem(satelliteSystem);
  274.                         pi.file.setTimeScale(satelliteSystem.getDefaultTimeSystem(pi.timeScales));
  275.                     }
  276.                     // Set time scale to UTC by default
  277.                     if (pi.file.getTimeScale() == null) {
  278.                         pi.file.setTimeScale(pi.timeScales.getUTC());
  279.                     }
  280.                 }
  281.             }

  282.         },

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

  285.             /** {@inheritDoc} */
  286.             @Override
  287.             public void parse(final String line, final ParseInfo pi) {

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

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

  294.                 // Third element is date
  295.                 String dateString = "";

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

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

  299.                 } else {

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

  302.                 }

  303.                 parseDateTimeZone(dateString, pi);

  304.             }

  305.         },

  306.         /** Parser for comments. */
  307.         HEADER_COMMENT("^.+COMMENT( )*$") {

  308.             /** {@inheritDoc} */
  309.             @Override
  310.             public void parse(final String line, final ParseInfo pi) {

  311.                 if (pi.file.getFormatVersion() < 3.04) {
  312.                     pi.file.addComment(line.substring(0, 60).trim());
  313.                 } else {
  314.                     pi.file.addComment(line.substring(0, 65).trim());
  315.                 }
  316.             }

  317.         },

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

  320.             /** {@inheritDoc} */
  321.             @Override
  322.             public void parse(final String line, final ParseInfo pi) {
  323.                 try (Scanner s1      = new Scanner(line);
  324.                      Scanner s2      = s1.useDelimiter(SPACES);
  325.                      Scanner scanner = s2.useLocale(Locale.US)) {

  326.                     // First element of the line is satellite system code
  327.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(scanner.next());
  328.                     pi.currentSatelliteSystem = satelliteSystem;

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

  331.                     // Parse all observation types
  332.                     String currentObsType = scanner.next();
  333.                     while (!currentObsType.equals(SYS)) {
  334.                         final ObservationType obsType = ObservationType.valueOf(currentObsType);
  335.                         pi.file.addSystemObservationType(satelliteSystem, obsType);
  336.                         currentObsType = scanner.next();
  337.                     }
  338.                 }
  339.             }

  340.         },

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

  343.             /** {@inheritDoc} */
  344.             @Override
  345.             public void parse(final String line, final ParseInfo pi) {
  346.                 try (Scanner s1      = new Scanner(line);
  347.                      Scanner s2      = s1.useDelimiter(SPACES);
  348.                      Scanner scanner = s2.useLocale(Locale.US)) {

  349.                     // This is a continuation line, there are only observation types
  350.                     // Parse all observation types
  351.                     String currentObsType = scanner.next();
  352.                     while (!currentObsType.equals(SYS)) {
  353.                         final ObservationType obsType = ObservationType.valueOf(currentObsType);
  354.                         pi.file.addSystemObservationType(pi.currentSatelliteSystem, obsType);
  355.                         currentObsType = scanner.next();
  356.                     }
  357.                 }
  358.             }

  359.         },

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

  362.             /** {@inheritDoc} */
  363.             @Override
  364.             public void parse(final String line, final ParseInfo pi) {
  365.                 try (Scanner s1      = new Scanner(line);
  366.                      Scanner s2      = s1.useDelimiter(SPACES);
  367.                      Scanner scanner = s2.useLocale(Locale.US)) {

  368.                     // Only element is the time system code
  369.                     final TimeSystem timeSystem = TimeSystem.parseTimeSystem(scanner.next());
  370.                     final TimeScale timeScale = timeSystem.getTimeScale(pi.timeScales);
  371.                     pi.file.setTimeSystem(timeSystem);
  372.                     pi.file.setTimeScale(timeScale);
  373.                 }
  374.             }

  375.         },

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

  378.             /** {@inheritDoc} */
  379.             @Override
  380.             public void parse(final String line, final ParseInfo pi) {
  381.                 try (Scanner s1      = new Scanner(line);
  382.                      Scanner s2      = s1.useDelimiter(SPACES);
  383.                      Scanner scanner = s2.useLocale(Locale.US)) {

  384.                     // Only element is the number of leap seconds
  385.                     final int numberOfLeapSeconds = scanner.nextInt();
  386.                     pi.file.setNumberOfLeapSeconds(numberOfLeapSeconds);
  387.                 }
  388.             }

  389.         },

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

  392.             /** {@inheritDoc} */
  393.             @Override
  394.             public void parse(final String line, final ParseInfo pi) {
  395.                 try (Scanner s1      = new Scanner(line);
  396.                      Scanner s2      = s1.useDelimiter(SPACES);
  397.                      Scanner scanner = s2.useLocale(Locale.US)) {

  398.                     // Only element is the number of leap seconds GNSS
  399.                     final int numberOfLeapSecondsGNSS = scanner.nextInt();
  400.                     pi.file.setNumberOfLeapSecondsGNSS(numberOfLeapSecondsGNSS);
  401.                 }
  402.             }

  403.         },

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

  406.             /** {@inheritDoc} */
  407.             @Override
  408.             public void parse(final String line, final ParseInfo pi) {

  409.                 // First element is the related satellite system
  410.                 final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(line.substring(0, 1));

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

  413.                 // Third element is the source of the corrections
  414.                 String sourceDCBS = "";
  415.                 if (pi.file.getFormatVersion() < 3.04) {
  416.                     sourceDCBS = line.substring(19, 60).trim();
  417.                 } else {
  418.                     sourceDCBS = line.substring(22, 65).trim();
  419.                 }

  420.                 // Check if sought fields were not actually blanks
  421.                 if (!progDCBS.equals("")) {
  422.                     pi.file.addAppliedDCBS(new AppliedDCBS(satelliteSystem, progDCBS, sourceDCBS));
  423.                 }
  424.             }

  425.         },

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

  428.             /** {@inheritDoc} */
  429.             @Override
  430.             public void parse(final String line, final ParseInfo pi) {

  431.                 // First element is the related satellite system
  432.                 final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(line.substring(0, 1));

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

  435.                 // Third element is the source of the corrections
  436.                 String sourcePCVS = "";
  437.                 if (pi.file.getFormatVersion() < 3.04) {
  438.                     sourcePCVS = line.substring(19, 60).trim();
  439.                 } else {
  440.                     sourcePCVS = line.substring(22, 65).trim();
  441.                 }

  442.                 // Check if sought fields were not actually blanks
  443.                 if (!progPCVS.equals("") || !sourcePCVS.equals("")) {
  444.                     pi.file.addAppliedPCVS(new AppliedPCVS(satelliteSystem, progPCVS, sourcePCVS));
  445.                 }
  446.             }

  447.         },

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

  450.             /** {@inheritDoc} */
  451.             @Override
  452.             public void parse(final String line, final ParseInfo pi) {
  453.                 try (Scanner s1      = new Scanner(line);
  454.                      Scanner s2      = s1.useDelimiter(SPACES);
  455.                      Scanner scanner = s2.useLocale(Locale.US)) {

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

  458.                     // Loop over data types
  459.                     for (int i = 0; i < numberOfDifferentDataTypes; i++) {
  460.                         final ClockDataType dataType = ClockDataType.parseClockDataType(scanner.next());
  461.                         pi.file.addClockDataType(dataType);
  462.                     }
  463.                 }
  464.             }

  465.         },

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

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

  474.                     // First element is the station clock reference ID
  475.                     final String stationName = scanner.next();
  476.                     pi.file.setStationName(stationName);

  477.                     // Second element is the station clock reference identifier
  478.                     final String stationIdentifier = scanner.next();
  479.                     pi.file.setStationIdentifier(stationIdentifier);
  480.                 }
  481.             }

  482.         },

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

  485.             /** {@inheritDoc} */
  486.             @Override
  487.             public void parse(final String line, final ParseInfo pi) {
  488.                 if (pi.file.getFormatVersion() < 3.04) {
  489.                     pi.file.setExternalClockReference(line.substring(0, 60).trim());
  490.                 } else {
  491.                     pi.file.setExternalClockReference(line.substring(0, 65).trim());
  492.                 }
  493.             }

  494.         },

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

  497.             /** {@inheritDoc} */
  498.             @Override
  499.             public void parse(final String line, final ParseInfo pi) {

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

  503.                 // Then, the full name of the analysis center
  504.                 String analysisCenterName = "";
  505.                 if (pi.file.getFormatVersion() < 3.04) {
  506.                     analysisCenterName = line.substring(5, 60).trim();
  507.                 } else {
  508.                     analysisCenterName = line.substring(5, 65).trim();
  509.                 }
  510.                 pi.file.setAnalysisCenterName(analysisCenterName);
  511.             }

  512.         },

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

  515.             /** {@inheritDoc} */
  516.             @Override
  517.             public void parse(final String line, final ParseInfo pi) {
  518.                 try (Scanner s1      = new Scanner(line);
  519.                      Scanner s2      = s1.useDelimiter(SPACES);
  520.                      Scanner scanner = s2.useLocale(Locale.US)) {

  521.                     // Initialize current reference clock list corresponding to the period
  522.                     pi.currentReferenceClocks = new ArrayList<ReferenceClock>();

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

  525.                     if (scanner.hasNextInt()) {
  526.                         // Second element is the start epoch of the period
  527.                         final int startYear   = scanner.nextInt();
  528.                         final int startMonth  = scanner.nextInt();
  529.                         final int startDay    = scanner.nextInt();
  530.                         final int startHour   = scanner.nextInt();
  531.                         final int startMin    = scanner.nextInt();
  532.                         final double startSec = scanner.nextDouble();
  533.                         final AbsoluteDate startEpoch = new AbsoluteDate(startYear, startMonth, startDay,
  534.                                                                          startHour, startMin, startSec,
  535.                                                                          pi.file.getTimeScale());
  536.                         pi.referenceClockStartDate = startEpoch;

  537.                         // Third element is the end epoch of the period
  538.                         final int endYear   = scanner.nextInt();
  539.                         final int endMonth  = scanner.nextInt();
  540.                         final int endDay    = scanner.nextInt();
  541.                         final int endHour   = scanner.nextInt();
  542.                         final int endMin    = scanner.nextInt();
  543.                         double endSec       = 0.0;
  544.                         if (pi.file.getFormatVersion() < 3.04) {
  545.                             endSec = Double.parseDouble(line.substring(51, 60));
  546.                         } else {
  547.                             endSec = scanner.nextDouble();
  548.                         }
  549.                         final AbsoluteDate endEpoch = new AbsoluteDate(endYear, endMonth, endDay,
  550.                                                                        endHour, endMin, endSec,
  551.                                                                        pi.file.getTimeScale());
  552.                         pi.referenceClockEndDate = endEpoch;
  553.                     } else {
  554.                         pi.referenceClockStartDate = AbsoluteDate.PAST_INFINITY;
  555.                         pi.referenceClockEndDate = AbsoluteDate.FUTURE_INFINITY;
  556.                     }
  557.                 }
  558.             }

  559.         },

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

  562.             /** {@inheritDoc} */
  563.             @Override
  564.             public void parse(final String line, final ParseInfo pi) {
  565.                 try (Scanner s1      = new Scanner(line);
  566.                      Scanner s2      = s1.useDelimiter(SPACES);
  567.                      Scanner scanner = s2.useLocale(Locale.US)) {

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

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

  572.                     // Optionally, third element is an a priori clock constraint, by default equal to zero
  573.                     double clockConstraint = 0.0;
  574.                     if (scanner.hasNextDouble()) {
  575.                         clockConstraint = scanner.nextDouble();
  576.                     }

  577.                     // Add reference clock to current reference clock list
  578.                     final ReferenceClock referenceClock = new ReferenceClock(referenceName, clockID, clockConstraint,
  579.                                                                              pi.referenceClockStartDate, pi.referenceClockEndDate);
  580.                     pi.currentReferenceClocks.add(referenceClock);

  581.                     // Modify time span map of the reference clocks to accept the new reference clock
  582.                     pi.file.addReferenceClockList(pi.currentReferenceClocks, pi.referenceClockStartDate);
  583.                 }
  584.             }

  585.         },

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

  588.             /** {@inheritDoc} */
  589.             @Override
  590.             public void parse(final String line, final ParseInfo pi) {
  591.                 try (Scanner s1      = new Scanner(line);
  592.                      Scanner s2      = s1.useDelimiter(SPACES);
  593.                      Scanner scanner = s2.useLocale(Locale.US)) {

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

  596.                     // Second element is the frame linked to given receiver positions
  597.                     final String frameString = scanner.next();
  598.                     pi.file.setFrameName(frameString);
  599.                 }
  600.             }

  601.         },

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

  604.             /** {@inheritDoc} */
  605.             @Override
  606.             public void parse(final String line, final ParseInfo pi) {

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

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

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

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

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

  617.                 if (pi.file.getFormatVersion() < 3.04) {
  618.                     designator = line.substring(0, 4).trim();
  619.                     receiverIdentifier = line.substring(5, 25).trim();
  620.                     xString = line.substring(25, 36).trim();
  621.                     yString = line.substring(37, 48).trim();
  622.                     zString = line.substring(49, 60).trim();
  623.                 } else {
  624.                     designator = line.substring(0, 10).trim();
  625.                     receiverIdentifier = line.substring(10, 30).trim();
  626.                     xString = line.substring(30, 41).trim();
  627.                     yString = line.substring(42, 53).trim();
  628.                     zString = line.substring(54, 65).trim();
  629.                 }

  630.                 final double x = MILLIMETER * Double.parseDouble(xString);
  631.                 final double y = MILLIMETER * Double.parseDouble(yString);
  632.                 final double z = MILLIMETER * Double.parseDouble(zString);

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

  635.             }

  636.         },

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

  639.             /** {@inheritDoc} */
  640.             @Override
  641.             public void parse(final String line, final ParseInfo pi) {

  642.                     // Only element in the line is number of satellites, not used here.
  643.                     // Do nothing...
  644.             }

  645.         },

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

  648.             /** {@inheritDoc} */
  649.             @Override
  650.             public void parse(final String line, final ParseInfo pi) {
  651.                 try (Scanner s1      = new Scanner(line);
  652.                      Scanner s2      = s1.useDelimiter(SPACES);
  653.                      Scanner scanner = s2.useLocale(Locale.US)) {

  654.                     // Only PRN numbers are stored in these lines
  655.                     // Initialize first PRN number
  656.                     String prn = scanner.next();

  657.                     // Browse the line until its end
  658.                     while (!prn.equals("PRN")) {
  659.                         pi.file.addSatellite(prn);
  660.                         prn = scanner.next();
  661.                     }
  662.                 }
  663.             }

  664.         },

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

  667.             /** {@inheritDoc} */
  668.             @Override
  669.             public void parse(final String line, final ParseInfo pi) {
  670.                 // do nothing...
  671.             }

  672.             /** {@inheritDoc} */
  673.             @Override
  674.             public Stream<LineParser> allowedNext() {
  675.                 return Stream.of(CLOCK_DATA);
  676.             }
  677.         },

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

  680.             /** {@inheritDoc} */
  681.             @Override
  682.             public void parse(final String line, final ParseInfo pi) {
  683.                 try (Scanner s1      = new Scanner(line);
  684.                      Scanner s2      = s1.useDelimiter(SPACES);
  685.                      Scanner scanner = s2.useLocale(Locale.US)) {

  686.                     // Initialise current values
  687.                     pi.currentDataValues = new double[6];

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

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

  692.                     // Third element is data epoch
  693.                     final int year   = scanner.nextInt();
  694.                     final int month  = scanner.nextInt();
  695.                     final int day    = scanner.nextInt();
  696.                     final int hour   = scanner.nextInt();
  697.                     final int min    = scanner.nextInt();
  698.                     final double sec = scanner.nextDouble();
  699.                     pi.currentDateComponents = new DateComponents(year, month, day);
  700.                     pi.currentTimeComponents = new TimeComponents(hour, min, sec);

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

  703.                     // Get the values in this line, there are at most 2.
  704.                     // Some entries claim less values than there actually are.
  705.                     // All values are added to the set, regardless of their claimed number.
  706.                     int i = 0;
  707.                     while (scanner.hasNextDouble()) {
  708.                         pi.currentDataValues[i++] = scanner.nextDouble();
  709.                     }

  710.                     // Check if continuation line is required
  711.                     if (pi.currentNumberOfValues <= 2) {
  712.                         // No continuation line is required
  713.                         pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
  714.                                                                                        pi.currentName,
  715.                                                                                        pi.currentDateComponents,
  716.                                                                                        pi.currentTimeComponents,
  717.                                                                                        pi.currentNumberOfValues,
  718.                                                                                        pi.currentDataValues[0],
  719.                                                                                        pi.currentDataValues[1],
  720.                                                                                        0.0, 0.0, 0.0, 0.0));
  721.                     }
  722.                 }
  723.             }

  724.             /** {@inheritDoc} */
  725.             @Override
  726.             public Stream<LineParser> allowedNext() {
  727.                 return Stream.of(CLOCK_DATA, CLOCK_DATA_CONTINUATION);
  728.             }
  729.         },

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

  732.             /** {@inheritDoc} */
  733.             @Override
  734.             public void parse(final String line, final ParseInfo pi) {
  735.                 try (Scanner s1      = new Scanner(line);
  736.                      Scanner s2      = s1.useDelimiter(SPACES);
  737.                      Scanner scanner = s2.useLocale(Locale.US)) {

  738.                     // Get the values in this continuation line.
  739.                     // Some entries claim less values than there actually are.
  740.                     // All values are added to the set, regardless of their claimed number.
  741.                     int i = 2;
  742.                     while (scanner.hasNextDouble()) {
  743.                         pi.currentDataValues[i++] = scanner.nextDouble();
  744.                     }

  745.                     // Add clock data line
  746.                     pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
  747.                                                                                    pi.currentName,
  748.                                                                                    pi.currentDateComponents,
  749.                                                                                    pi.currentTimeComponents,
  750.                                                                                    pi.currentNumberOfValues,
  751.                                                                                    pi.currentDataValues[0],
  752.                                                                                    pi.currentDataValues[1],
  753.                                                                                    pi.currentDataValues[2],
  754.                                                                                    pi.currentDataValues[3],
  755.                                                                                    pi.currentDataValues[4],
  756.                                                                                    pi.currentDataValues[5]));

  757.                 }
  758.             }

  759.             /** {@inheritDoc} */
  760.             @Override
  761.             public Stream<LineParser> allowedNext() {
  762.                 return Stream.of(CLOCK_DATA);
  763.             }
  764.         };

  765.         /** Pattern for identifying line. */
  766.         private final Pattern pattern;

  767.         /** Simple constructor.
  768.          * @param lineRegexp regular expression for identifying line
  769.          */
  770.         LineParser(final String lineRegexp) {
  771.             pattern = Pattern.compile(lineRegexp);
  772.         }

  773.         /** Parse a line.
  774.          * @param line line to parse
  775.          * @param pi holder for transient data
  776.          */
  777.         public abstract void parse(String line, ParseInfo pi);

  778.         /** Get the allowed parsers for next line.
  779.          * <p>
  780.          * Because the standard only recommends an order for header keys,
  781.          * the default implementation of the method returns all the
  782.          * header keys. Specific implementations must overrides the method.
  783.          * </p>
  784.          * @return allowed parsers for next line
  785.          */
  786.         public Stream<LineParser> allowedNext() {
  787.             return Stream.of(HEADER_PROGRAM, HEADER_COMMENT, HEADER_SYSTEM_OBS, HEADER_SYSTEM_OBS_CONTINUATION, HEADER_TIME_SYSTEM, HEADER_LEAP_SECONDS,
  788.                              HEADER_LEAP_SECONDS_GNSS, HEADER_DCBS, HEADER_PCVS, HEADER_TYPES_OF_DATA, HEADER_STATIONS_NAME, HEADER_STATION_CLOCK_REF,
  789.                              HEADER_ANALYSIS_CENTER, HEADER_NUMBER_OF_CLOCK_REF, HEADER_ANALYSIS_CLOCK_REF, HEADER_NUMBER_OF_SOLN_STATIONS,
  790.                              HEADER_SOLN_STATIONS, HEADER_NUMBER_OF_SOLN_SATS, HEADER_PRN_LIST, HEADER_END);
  791.         }

  792.         /** Check if parser can handle line.
  793.          * @param line line to parse
  794.          * @return true if parser can handle the specified line
  795.          */
  796.         public boolean canHandle(final String line) {
  797.             return pattern.matcher(line).matches();
  798.         }

  799.         /** Parse existing date - time - zone formats.
  800.          * If zone field is not missing, a proper Orekit date can be created and set into clock file object.
  801.          * This feature depends on the date format.
  802.          * @param dateString the whole date - time - zone string
  803.          * @param pi holder for transient data
  804.          */
  805.         private static void parseDateTimeZone(final String dateString, final ParseInfo pi) {

  806.             String date = "";
  807.             String time = "";
  808.             String zone = "";
  809.             DateComponents dateComponents = null;
  810.             TimeComponents timeComponents = null;

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

  812.                 date = dateString.substring(0, 10).trim();
  813.                 time = dateString.substring(11, 16).trim();
  814.                 zone = dateString.substring(16).trim();

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

  816.                 date = dateString.substring(0, 8).trim();
  817.                 time = dateString.substring(9, 16).trim();
  818.                 zone = dateString.substring(16).trim();

  819.                 if (!zone.equals("")) {
  820.                     // Get date and time components
  821.                     dateComponents = new DateComponents(Integer.parseInt(date.substring(0, 4)),
  822.                                                         Integer.parseInt(date.substring(4, 6)),
  823.                                                         Integer.parseInt(date.substring(6, 8)));
  824.                     timeComponents = new TimeComponents(Integer.parseInt(time.substring(0, 2)),
  825.                                                         Integer.parseInt(time.substring(2, 4)),
  826.                                                         Integer.parseInt(time.substring(4, 6)));

  827.                 }

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

  829.                 date = dateString.substring(0, 11).trim();
  830.                 time = dateString.substring(11, 17).trim();
  831.                 zone = dateString.substring(17).trim();

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

  833.                 date = dateString.substring(0, 9).trim();
  834.                 time = dateString.substring(9, 15).trim();
  835.                 zone = dateString.substring(15).trim();

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

  837.                 date = dateString.substring(0, 11).trim();
  838.                 time = dateString.substring(11, 20).trim();

  839.             } else {
  840.                 // Format is not handled or date is missing. Do nothing...
  841.             }

  842.             pi.file.setCreationDateString(date);
  843.             pi.file.setCreationTimeString(time);
  844.             pi.file.setCreationTimeZoneString(zone);

  845.             if (dateComponents != null) {
  846.                 pi.file.setCreationDate(new AbsoluteDate(dateComponents,
  847.                                                          timeComponents,
  848.                                                          TimeSystem.parseTimeSystem(zone).getTimeScale(pi.timeScales)));
  849.             }
  850.         }
  851.     }

  852. }