RinexClockParser.java

  1. /* Copyright 2002-2023 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.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.Collections;
  28. import java.util.InputMismatchException;
  29. import java.util.List;
  30. import java.util.Locale;
  31. import java.util.Scanner;
  32. import java.util.function.Function;
  33. import java.util.regex.Pattern;

  34. import org.orekit.annotation.DefaultDataContext;
  35. import org.orekit.data.DataContext;
  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.ObservationType;
  45. import org.orekit.gnss.SatelliteSystem;
  46. import org.orekit.gnss.TimeSystem;
  47. import org.orekit.time.AbsoluteDate;
  48. import org.orekit.time.DateComponents;
  49. import org.orekit.time.TimeComponents;
  50. import org.orekit.time.TimeScale;
  51. import org.orekit.time.TimeScales;
  52. import org.orekit.utils.IERSConventions;

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

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

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

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

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

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

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

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

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

  194.         int lineNumber = 0;
  195.         Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
  196.         nextLine:
  197.         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  198.             ++lineNumber;
  199.             for (final LineParser candidate : candidateParsers) {
  200.                 if (candidate.canHandle(line)) {
  201.                     try {
  202.                         candidate.parse(line, pi);
  203.                         candidateParsers = candidate.allowedNext();
  204.                         continue nextLine;
  205.                     } catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
  206.                         throw new OrekitException(e,
  207.                                                   OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  208.                                                   lineNumber, fileName, line);
  209.                     }
  210.                 }
  211.             }

  212.             // no parsers found for this line
  213.             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  214.                                       lineNumber, fileName, line);

  215.         }

  216.         return pi.file;

  217.     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

  266.                     pi.file.setFormatVersion(version);

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

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

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

  283.         },

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

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

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

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

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

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

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

  300.                 } else {

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

  303.                 }

  304.                 parseDateTimeZone(dateString, pi);

  305.             }

  306.         },

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

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

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

  318.         },

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

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

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

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

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

  341.         },

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

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

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

  360.         },

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

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

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

  376.         },

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

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

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

  390.         },

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

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

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

  404.         },

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

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

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

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

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

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

  426.         },

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

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

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

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

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

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

  448.         },

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

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

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

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

  466.         },

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

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

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

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

  483.         },

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

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

  495.         },

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

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

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

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

  513.         },

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

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

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

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

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

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

  560.         },

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

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

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

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

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

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

  582.                     // Modify time span map of the reference clocks to accept the new reference clock
  583.                     pi.file.addReferenceClockList(pi.currentReferenceClocks, pi.referenceClockStartDate);
  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.                 // do nothing...
  672.             }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  758.                 }
  759.             }

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

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

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

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

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

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

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

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

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

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

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

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

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

  828.                 }

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

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

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

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

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

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

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

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

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

  853. }