RinexClockParser.java

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

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

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

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

  75.     /** Pattern for date format dd-MONTH-yyyy hh:mm zone or d-MONTH-yyyy hh:mm zone. */
  76.     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}.*$");

  77.     /** Pattern for date format dd-MONTH-yy hh:mm zone or d-MONTH-yy hh:mm zone. */
  78.     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}.*$");

  79.     /** Pattern for date format yyyy MONTH dd hh:mm:ss or yyyy MONTH d hh:mm:ss. */
  80.     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}.*$");

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

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

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

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

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

  91.     /** Create a clock file parser using default values.
  92.      * <p>
  93.      * This constructor uses the {@link DataContext#getDefault() default data context},
  94.      * and {@link IGSUtils#guessFrame}.
  95.      * </p>
  96.      * @see #RinexClockParser(Function)
  97.      */
  98.     @DefaultDataContext
  99.     public RinexClockParser() {
  100.         this(IGSUtils::guessFrame);
  101.     }

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

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

  126.     /**
  127.      * Parse an IGS clock file from an input stream using the UTF-8 charset.
  128.      *
  129.      * <p> This method creates a {@link BufferedReader} from the stream and as such this
  130.      * method may read more data than necessary from {@code stream} and the additional
  131.      * data will be lost. The other parse methods do not have this issue.
  132.      *
  133.      * @param stream to read the IGS clock file from
  134.      * @return a parsed IGS clock file
  135.      * @see #parse(String)
  136.      * @see #parse(BufferedReader, String)
  137.      * @see #parse(DataSource)
  138.      */
  139.     public RinexClock parse(final InputStream stream) {
  140.         return parse(new DataSource("<stream>", () -> stream));
  141.     }

  142.     /**
  143.      * Parse an IGS clock file from a file on the local file system.
  144.      * @param fileName file name
  145.      * @return a parsed IGS clock file
  146.      * @see #parse(InputStream)
  147.      * @see #parse(BufferedReader, String)
  148.      * @see #parse(DataSource)
  149.      */
  150.     public RinexClock parse(final String fileName) {
  151.         return parse(new DataSource(Paths.get(fileName).toFile()));
  152.     }

  153.     /**
  154.      * Parse an IGS clock file from a stream.
  155.      * @param reader containing the clock file
  156.      * @param fileName file name
  157.      * @return a parsed IGS clock file
  158.      * @see #parse(InputStream)
  159.      * @see #parse(String)
  160.      * @see #parse(DataSource)
  161.      */
  162.     public RinexClock parse(final BufferedReader reader, final String fileName) {
  163.         return parse(new DataSource(fileName, () -> reader));
  164.     }

  165.     /** Parse an IGS clock file from a {@link DataSource}.
  166.      * @param source source for clock file
  167.      * @return a parsed IGS clock file
  168.      * @see #parse(InputStream)
  169.      * @see #parse(String)
  170.      * @see #parse(BufferedReader, String)
  171.      * @since 12.1
  172.      */
  173.     public RinexClock parse(final DataSource source) {

  174.         // initialize internal data structures
  175.         final ParseInfo pi = new ParseInfo();

  176.         try (Reader reader = source.getOpener().openReaderOnce();
  177.              BufferedReader br = new BufferedReader(reader)) {
  178.             pi.lineNumber = 0;
  179.             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
  180.             nextLine:
  181.             for (String line = br.readLine(); line != null; line = br.readLine()) {
  182.                 ++pi.lineNumber;
  183.                 for (final LineParser candidate : candidateParsers) {
  184.                     if (candidate.canHandle(line)) {
  185.                         try {
  186.                             candidate.parse(line, pi);
  187.                             candidateParsers = candidate.allowedNext();
  188.                             continue nextLine;
  189.                         } catch (StringIndexOutOfBoundsException |
  190.                             NumberFormatException | InputMismatchException e) {
  191.                             throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  192.                                                       pi.lineNumber, source.getName(), line);
  193.                         }
  194.                     }
  195.                 }

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

  199.             }

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

  203.         return pi.file;

  204.     }

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

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

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

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

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

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

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

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

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

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

  225.         /** Current data date components. */
  226.         private DateComponents currentDateComponents;

  227.         /** Current data time components. */
  228.         private TimeComponents currentTimeComponents;

  229.         /** Current data number of data values to follow. */
  230.         private int currentNumberOfValues;

  231.         /** Current data values. */
  232.         private double[] currentDataValues;

  233.         /** Constructor, build the ParseInfo object. */
  234.         protected ParseInfo () {
  235.             this.timeScales = RinexClockParser.this.timeScales;
  236.             this.file = new RinexClock(frameBuilder);
  237.             this.pendingReferenceClocks = new ArrayList<>();
  238.         }
  239.     }


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

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

  244.             /** {@inheritDoc} */
  245.             @Override
  246.             public void parse(final String line, final ParseInfo pi) {
  247.                 try (Scanner s1      = new Scanner(line);
  248.                      Scanner s2      = s1.useDelimiter(SPACES);
  249.                      Scanner scanner = s2.useLocale(Locale.US)) {

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

  252.                     // Throw exception if format version is not handled
  253.                     if (!HANDLED_VERSIONS.contains(version)) {
  254.                         throw new OrekitException(OrekitMessages.CLOCK_FILE_UNSUPPORTED_VERSION, version);
  255.                     }

  256.                     pi.file.setFormatVersion(version);

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

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

  260.                     // Check satellite if system is recorded
  261.                     if (!satelliteSystemString.isEmpty()) {
  262.                         // Record satellite system and default time system in clock file object
  263.                         final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(satelliteSystemString);
  264.                         pi.file.setSatelliteSystem(satelliteSystem);
  265.                         if (satelliteSystem.getObservationTimeScale() != null) {
  266.                             pi.file.setTimeScale(satelliteSystem.getObservationTimeScale().getTimeScale(pi.timeScales));
  267.                         }
  268.                     }
  269.                     // Set time scale to UTC by default
  270.                     if (pi.file.getTimeScale() == null) {
  271.                         pi.file.setTimeScale(pi.timeScales.getUTC());
  272.                     }
  273.                 }
  274.             }

  275.         },

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

  278.             /** {@inheritDoc} */
  279.             @Override
  280.             public void parse(final String line, final ParseInfo pi) {

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

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

  287.                 // Third element is date
  288.                 String dateString = "";

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

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

  292.                 } else {

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

  295.                 }

  296.                 parseDateTimeZone(dateString, pi);

  297.             }

  298.         },

  299.         /** Parser for comments. */
  300.         HEADER_COMMENT("^.+COMMENT( )*$") {

  301.             /** {@inheritDoc} */
  302.             @Override
  303.             public void parse(final String line, final ParseInfo pi) {

  304.                 if (pi.file.getFormatVersion() < 3.04) {
  305.                     pi.file.addComment(line.substring(0, 60).trim());
  306.                 } else {
  307.                     pi.file.addComment(line.substring(0, 65).trim());
  308.                 }
  309.             }

  310.         },

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

  313.             /** {@inheritDoc} */
  314.             @Override
  315.             public void parse(final String line, final ParseInfo pi) {
  316.                 try (Scanner s1      = new Scanner(line);
  317.                      Scanner s2      = s1.useDelimiter(SPACES);
  318.                      Scanner scanner = s2.useLocale(Locale.US)) {

  319.                     // First element of the line is satellite system code
  320.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(scanner.next());
  321.                     pi.currentSatelliteSystem = satelliteSystem;

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

  324.                     // Parse all observation types
  325.                     String currentObsType = scanner.next();
  326.                     while (!currentObsType.equals(SYS)) {
  327.                         final ObservationType obsType = ObservationType.valueOf(currentObsType);
  328.                         pi.file.addSystemObservationType(satelliteSystem, obsType);
  329.                         currentObsType = scanner.next();
  330.                     }
  331.                 }
  332.             }

  333.         },

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

  336.             /** {@inheritDoc} */
  337.             @Override
  338.             public void parse(final String line, final ParseInfo pi) {
  339.                 try (Scanner s1      = new Scanner(line);
  340.                      Scanner s2      = s1.useDelimiter(SPACES);
  341.                      Scanner scanner = s2.useLocale(Locale.US)) {

  342.                     // This is a continuation line, there are only observation types
  343.                     // Parse all observation types
  344.                     String currentObsType = scanner.next();
  345.                     while (!currentObsType.equals(SYS)) {
  346.                         final ObservationType obsType = ObservationType.valueOf(currentObsType);
  347.                         pi.file.addSystemObservationType(pi.currentSatelliteSystem, obsType);
  348.                         currentObsType = scanner.next();
  349.                     }
  350.                 }
  351.             }

  352.         },

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

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

  361.                     // Only element is the time system code
  362.                     final TimeSystem timeSystem = TimeSystem.parseTimeSystem(scanner.next());
  363.                     final TimeScale timeScale = timeSystem.getTimeScale(pi.timeScales);
  364.                     pi.file.setTimeSystem(timeSystem);
  365.                     pi.file.setTimeScale(timeScale);
  366.                 }
  367.             }

  368.         },

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

  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 number of leap seconds
  378.                     final int numberOfLeapSeconds = scanner.nextInt();
  379.                     pi.file.setNumberOfLeapSeconds(numberOfLeapSeconds);
  380.                 }
  381.             }

  382.         },

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

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

  391.                     // Only element is the number of leap seconds GNSS
  392.                     final int numberOfLeapSecondsGNSS = scanner.nextInt();
  393.                     pi.file.setNumberOfLeapSecondsGNSS(numberOfLeapSecondsGNSS);
  394.                 }
  395.             }

  396.         },

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

  399.             /** {@inheritDoc} */
  400.             @Override
  401.             public void parse(final String line, final ParseInfo pi) {
  402.                 // First element, if present, is the related satellite system
  403.                 final String system = line.substring(0, 1);
  404.                 if (!" ".equals(system)) {
  405.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(system);

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

  408.                     // Third element is the source of the corrections
  409.                     String sourceDCBS = "";
  410.                     if (pi.file.getFormatVersion() < 3.04) {
  411.                         sourceDCBS = line.substring(19, 60).trim();
  412.                     } else {
  413.                         sourceDCBS = line.substring(22, 65).trim();
  414.                     }

  415.                     // Check if sought fields were not actually blanks
  416.                     if (!progDCBS.isEmpty()) {
  417.                         pi.file.addAppliedDCBS(new AppliedDCBS(satelliteSystem, progDCBS, sourceDCBS));
  418.                     }
  419.                 }
  420.             }

  421.         },

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

  424.             /** {@inheritDoc} */
  425.             @Override
  426.             public void parse(final String line, final ParseInfo pi) {

  427.                 // First element, if present, is the related satellite system
  428.                 final String system = line.substring(0, 1);
  429.                 if (!" ".equals(system)) {
  430.                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(system);

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

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

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

  446.         },

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

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

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

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

  464.         },

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

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

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

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

  481.         },

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

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

  493.         },

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

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

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

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

  511.         },

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

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

  520.                     if (!pi.pendingReferenceClocks.isEmpty()) {
  521.                         // Modify time span map of the reference clocks to accept the pending reference clock
  522.                         pi.file.addReferenceClockList(pi.pendingReferenceClocks,
  523.                                                       pi.referenceClockStartDate);
  524.                         pi.pendingReferenceClocks = new ArrayList<>();
  525.                     }

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

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

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

  562.         },

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

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

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

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

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

  580.                     // Add reference clock to current reference clock list
  581.                     final ReferenceClock referenceClock = new ReferenceClock(referenceName, clockID, clockConstraint,
  582.                                                                              pi.referenceClockStartDate, pi.referenceClockEndDate);
  583.                     pi.pendingReferenceClocks.add(referenceClock);

  584.                 }
  585.             }

  586.         },

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

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

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

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

  602.         },

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

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

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

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

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

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

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

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

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

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

  636.             }

  637.         },

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

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

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

  646.         },

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

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

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

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

  665.         },

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

  668.             /** {@inheritDoc} */
  669.             @Override
  670.             public void parse(final String line, final ParseInfo pi) {
  671.                 if (!pi.pendingReferenceClocks.isEmpty()) {
  672.                     // Modify time span map of the reference clocks to accept the pending reference clock
  673.                     pi.file.addReferenceClockList(pi.pendingReferenceClocks, pi.referenceClockStartDate);
  674.                 }
  675.             }

  676.             /** {@inheritDoc} */
  677.             @Override
  678.             public Iterable<LineParser> allowedNext() {
  679.                 return Collections.singleton(CLOCK_DATA);
  680.             }
  681.         },

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

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

  690.                     // Initialise current values
  691.                     pi.currentDataValues = new double[6];

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

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

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

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

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

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

  728.             /** {@inheritDoc} */
  729.             @Override
  730.             public Iterable<LineParser> allowedNext() {
  731.                 return Arrays.asList(CLOCK_DATA, CLOCK_DATA_CONTINUATION);
  732.             }
  733.         },

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

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

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

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

  761.                 }
  762.             }

  763.             /** {@inheritDoc} */
  764.             @Override
  765.             public Iterable<LineParser> allowedNext() {
  766.                 return Collections.singleton(CLOCK_DATA);
  767.             }
  768.         };

  769.         /** Pattern for identifying line. */
  770.         private final Pattern pattern;

  771.         /** Simple constructor.
  772.          * @param lineRegexp regular expression for identifying line
  773.          */
  774.         LineParser(final String lineRegexp) {
  775.             pattern = Pattern.compile(lineRegexp);
  776.         }

  777.         /** Parse a line.
  778.          * @param line line to parse
  779.          * @param pi holder for transient data
  780.          */
  781.         public abstract void parse(String line, ParseInfo pi);

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

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

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

  810.             String date = "";
  811.             String time = "";
  812.             String zone = "";
  813.             DateComponents dateComponents = null;
  814.             TimeComponents timeComponents = null;

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

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

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

  820.                 date = dateString.substring(0, 8).trim();
  821.                 time = dateString.substring(9, 16).trim();
  822.                 zone = dateString.substring(16).trim();

  823.                 if (!zone.isEmpty()) {
  824.                     // Get date and time components
  825.                     dateComponents = new DateComponents(Integer.parseInt(date.substring(0, 4)),
  826.                                                         Integer.parseInt(date.substring(4, 6)),
  827.                                                         Integer.parseInt(date.substring(6, 8)));
  828.                     timeComponents = new TimeComponents(Integer.parseInt(time.substring(0, 2)),
  829.                                                         Integer.parseInt(time.substring(2, 4)),
  830.                                                         Integer.parseInt(time.substring(4, 6)));

  831.                 }

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

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

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

  837.                 date = dateString.substring(0, 9).trim();
  838.                 time = dateString.substring(9, 15).trim();
  839.                 zone = dateString.substring(15).trim();

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

  841.                 date = dateString.substring(0, 11).trim();
  842.                 time = dateString.substring(11, 20).trim();

  843.             } else {
  844.                 // Format is not handled or date is missing. Do nothing...
  845.             }

  846.             pi.file.setCreationDateString(date);
  847.             pi.file.setCreationTimeString(time);
  848.             pi.file.setCreationTimeZoneString(zone);

  849.             if (dateComponents != null) {
  850.                 pi.file.setCreationDate(new AbsoluteDate(dateComponents,
  851.                                                          timeComponents,
  852.                                                          TimeSystem.parseTimeSystem(zone).getTimeScale(pi.timeScales)));
  853.             }
  854.         }
  855.     }

  856. }