SinexLoader.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.sinex;

  18. import java.io.BufferedInputStream;
  19. import java.io.BufferedReader;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.InputStreamReader;
  23. import java.nio.charset.StandardCharsets;
  24. import java.text.ParseException;
  25. import java.util.ArrayList;
  26. import java.util.Arrays;
  27. import java.util.Collections;
  28. import java.util.HashMap;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.SortedSet;
  32. import java.util.TreeSet;
  33. import java.util.regex.Matcher;
  34. import java.util.regex.Pattern;

  35. import org.hipparchus.exception.DummyLocalizable;
  36. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  37. import org.hipparchus.util.FastMath;
  38. import org.orekit.annotation.DefaultDataContext;
  39. import org.orekit.data.DataContext;
  40. import org.orekit.data.DataLoader;
  41. import org.orekit.data.DataProvidersManager;
  42. import org.orekit.data.DataSource;
  43. import org.orekit.errors.OrekitException;
  44. import org.orekit.errors.OrekitMessages;
  45. import org.orekit.files.sinex.Station.ReferenceSystem;
  46. import org.orekit.frames.EOPEntry;
  47. import org.orekit.frames.EopHistoryLoader;
  48. import org.orekit.frames.ITRFVersion;
  49. import org.orekit.gnss.SatelliteSystem;
  50. import org.orekit.gnss.TimeSystem;
  51. import org.orekit.time.AbsoluteDate;
  52. import org.orekit.time.ChronologicalComparator;
  53. import org.orekit.time.DateComponents;
  54. import org.orekit.time.TimeScale;
  55. import org.orekit.time.TimeScales;
  56. import org.orekit.time.TimeStamped;
  57. import org.orekit.utils.Constants;
  58. import org.orekit.utils.IERSConventions;
  59. import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
  60. import org.orekit.utils.units.Unit;

  61. /**
  62.  * Loader for Solution INdependent EXchange (SINEX) files.
  63.  * <p>
  64.  * The loader can be used to load several data types contained in Sinex files.
  65.  * The current supported data are: station coordinates, site eccentricities, EOP, and Difference Code Bias (DCB).
  66.  * Several instances of Sinex loader must be created in order to parse different data types.
  67.  * </p>
  68.  * <p>
  69.  * The parsing of EOP parameters for multiple files in different SinexLoader object, fed into the default DataContext
  70.  * might pose a problem in case validity dates are overlapping. As Sinex daily solution files provide a single EOP entry,
  71.  * the Sinex loader will add points at the limits of data dates (startDate, endDate) of the Sinex file, which in case of
  72.  * overlap will lead to inconsistencies in the final EOPHistory object. Multiple files can be parsed using a single SinexLoader
  73.  * with a regex to overcome this issue.
  74.  * </p>
  75.  * @author Bryan Cazabonne
  76.  * @since 10.3
  77.  */
  78. public class SinexLoader implements EopHistoryLoader {

  79.     /** Length of day. */
  80.     private static final String LOD = "LOD";

  81.     /** UT1-UTC. */
  82.     private static final String UT = "UT";

  83.     /** X polar motion. */
  84.     private static final String XPO = "XPO";

  85.     /** Y polar motion. */
  86.     private static final String YPO = "YPO";

  87.     /** Nutation correction in longitude. */
  88.     private static final String NUT_LN = "NUT_LN";

  89.     /** Nutation correction in obliquity. */
  90.     private static final String NUT_OB = "NUT_OB";

  91.     /** Nutation correction X. */
  92.     private static final String NUT_X = "NUT_X";

  93.     /** Nutation correction Y. */
  94.     private static final String NUT_Y = "NUT_Y";

  95.     /** 00:000:00000 epoch. */
  96.     private static final String DEFAULT_EPOCH_TWO_DIGITS = "00:000:00000";

  97.     /** 0000:000:00000 epoch. */
  98.     private static final String DEFAULT_EPOCH_FOUR_DIGITS = "0000:000:00000";

  99.     /** Pattern for delimiting regular expressions. */
  100.     private static final Pattern SEPARATOR = Pattern.compile(":");

  101.     /** Pattern for regular data. */
  102.     private static final Pattern PATTERN_SPACE = Pattern.compile("\\s+");

  103.     /** Pattern to check beginning of SINEX files.*/
  104.     private static final Pattern PATTERN_BEGIN = Pattern.compile("%=(?:SNX|BIA) \\d\\.\\d\\d ..." +
  105.                                                                  " (\\d{2,4}:\\d{3}:\\d{5}) ..." +
  106.                                                                  " (\\d{2,4}:\\d{3}:\\d{5}) (\\d{2,4}:\\d{3}:\\d{5})" +
  107.                                                                  " . .*");

  108.     /** List of all EOP parameter types. */
  109.     private static final List<String> EOP_TYPES = Arrays.asList(LOD, UT, XPO, YPO, NUT_LN, NUT_OB, NUT_X, NUT_Y);

  110.     /** Start time of the data used in the Sinex solution.*/
  111.     private AbsoluteDate startDate;

  112.     /** End time of the data used in the Sinex solution.*/
  113.     private AbsoluteDate endDate;

  114.     /** SINEX file creation date as extracted for the first line. */
  115.     private AbsoluteDate creationDate;

  116.     /** Station data.
  117.      * Key: Site code
  118.      */
  119.     private final Map<String, Station> stations;

  120.     /**
  121.      * DCB data.
  122.      * Key: Site code
  123.      */
  124.     private final Map<String, DcbStation> dcbStations;

  125.     /**
  126.      * DCB data.
  127.      * Key: Satellite PRN
  128.      */
  129.     private final Map<String, DcbSatellite> dcbSatellites;

  130.     /** DCB description. */
  131.     private final DcbDescription dcbDescription;

  132.     /** Data set. */
  133.     private Map<AbsoluteDate, SinexEopEntry> eop;

  134.     /** ITRF Version used for EOP parsing. */
  135.     private ITRFVersion itrfVersionEop;

  136.     /** Time scales. */
  137.     private final TimeScales scales;

  138.     /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
  139.      * default data context}.
  140.      * @param supportedNames regular expression for supported files names
  141.      * @see #SinexLoader(String, DataProvidersManager, TimeScales)
  142.      */
  143.     @DefaultDataContext
  144.     public SinexLoader(final String supportedNames) {
  145.         this(supportedNames,
  146.              DataContext.getDefault().getDataProvidersManager(),
  147.              DataContext.getDefault().getTimeScales());
  148.     }

  149.     /**
  150.      * Construct a loader by specifying the source of SINEX auxiliary data files.
  151.      * <p>
  152.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  153.      * possible to update the version using the {@link #setITRFVersion(int)}
  154.      * method.
  155.      * </p>
  156.      * @param supportedNames regular expression for supported files names
  157.      * @param dataProvidersManager provides access to auxiliary data.
  158.      * @param scales time scales
  159.      */
  160.     public SinexLoader(final String supportedNames,
  161.                        final DataProvidersManager dataProvidersManager,
  162.                        final TimeScales scales) {
  163.         // Common data
  164.         this.scales         = scales;
  165.         this.creationDate   = AbsoluteDate.FUTURE_INFINITY;
  166.         // DCB parameters
  167.         this.dcbDescription = new DcbDescription();
  168.         this.dcbStations    = new HashMap<>();
  169.         this.dcbSatellites  = new HashMap<>();
  170.         // EOP parameters
  171.         this.eop            = new HashMap<>();
  172.         this.itrfVersionEop = ITRFVersion.ITRF_2014;
  173.         // Station data
  174.         this.stations       = new HashMap<>();

  175.         // Read the file
  176.         dataProvidersManager.feed(supportedNames, new Parser());
  177.     }

  178.     /**
  179.      * Simple constructor. This constructor uses the {@link DataContext#getDefault()
  180.      * default data context}.
  181.      * <p>
  182.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  183.      * possible to update the version using the {@link #setITRFVersion(int)}
  184.      * method.
  185.      * </p>
  186.      * @param source source for the RINEX data
  187.      * @see #SinexLoader(String, DataProvidersManager, TimeScales)
  188.      */
  189.     @DefaultDataContext
  190.     public SinexLoader(final DataSource source) {
  191.         this(source, DataContext.getDefault().getTimeScales());
  192.     }

  193.     /**
  194.      * Loads SINEX from the given input stream using the specified auxiliary data.
  195.      * <p>
  196.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  197.      * possible to update the version using the {@link #setITRFVersion(int)}
  198.      * method.
  199.      * </p>
  200.      * @param source source for the RINEX data
  201.      * @param scales time scales
  202.      */
  203.     public SinexLoader(final DataSource source, final TimeScales scales) {
  204.         try {
  205.             // Common data
  206.             this.scales         = scales;
  207.             this.creationDate   = AbsoluteDate.FUTURE_INFINITY;
  208.             // EOP data
  209.             this.itrfVersionEop = ITRFVersion.ITRF_2014;
  210.             this.eop            = new HashMap<>();
  211.             // DCB data
  212.             this.dcbStations    = new HashMap<>();
  213.             this.dcbSatellites  = new HashMap<>();
  214.             this.dcbDescription = new DcbDescription();
  215.             // Station data
  216.             this.stations       = new HashMap<>();

  217.             // Read the file
  218.             try (InputStream         is  = source.getOpener().openStreamOnce();
  219.                  BufferedInputStream bis = new BufferedInputStream(is)) {
  220.                 new Parser().loadData(bis, source.getName());
  221.             }
  222.         } catch (IOException | ParseException ioe) {
  223.             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  224.         }
  225.     }

  226.     /**
  227.      * Set the ITRF version used in EOP entries processing.
  228.      * @param year Year of the ITRF Version used for parsing EOP.
  229.      * @since 11.2
  230.      */
  231.     public void setITRFVersion(final int year) {
  232.         this.itrfVersionEop = ITRFVersion.getITRFVersion(year);
  233.     }

  234.     /**
  235.      * Get the ITRF version used for the EOP entries processing.
  236.      * @return the ITRF Version used for the EOP processing.
  237.      * @since 11.2
  238.      */
  239.     public ITRFVersion getITRFVersion() {
  240.         return itrfVersionEop;
  241.     }

  242.     /**
  243.      * Get the creation date of the parsed SINEX file.
  244.      * @return SINEX file creation date as an AbsoluteDate
  245.      * @since 12.0
  246.      */
  247.     public AbsoluteDate getCreationDate() {
  248.         return creationDate;
  249.     }

  250.     /**
  251.      * Get the file epoch start time.
  252.      * @return the file epoch start time
  253.      * @since 12.0
  254.      */
  255.     public AbsoluteDate getFileEpochStartTime() {
  256.         return startDate;
  257.     }

  258.     /**
  259.      * Get the file epoch end time.
  260.      * @return the file epoch end time
  261.      * @since 12.0
  262.      */
  263.     public AbsoluteDate getFileEpochEndTime() {
  264.         return endDate;
  265.     }

  266.     /**
  267.      * Get the parsed station data.
  268.      * @return unmodifiable view of parsed station data
  269.      */
  270.     public Map<String, Station> getStations() {
  271.         return Collections.unmodifiableMap(stations);
  272.     }

  273.     /**
  274.      * Get the parsed EOP data.
  275.      * @return unmodifiable view of parsed station data
  276.      * @since 11.2
  277.      */
  278.     public Map<AbsoluteDate, SinexEopEntry> getParsedEop() {
  279.         return Collections.unmodifiableMap(eop);
  280.     }

  281.     /**
  282.      * Get the station corresponding to the given site code.
  283.      *
  284.      * @param siteCode site code
  285.      * @return the corresponding station
  286.      */
  287.     public Station getStation(final String siteCode) {
  288.         return stations.get(siteCode);
  289.     }

  290.     /** {@inheritDoc} */
  291.     @Override
  292.     public void fillHistory(final NutationCorrectionConverter converter,
  293.                             final SortedSet<EOPEntry> history) {
  294.         // Fill the history set with the content of the parsed data
  295.         // According to Sinex standard, data are given in UTC
  296.         history.addAll(getEopList(converter, scales.getUTC()));
  297.     }

  298.     /**
  299.      * Get the DCB data for a given station.
  300.      * @param siteCode site code
  301.      * @return DCB data for the station
  302.      * @since 12.0
  303.      */
  304.     public DcbStation getDcbStation(final String siteCode) {
  305.         return dcbStations.get(siteCode);
  306.     }

  307.     /**
  308.      * Get the DCB data for a given satellite identified by its PRN.
  309.      * @param prn the satellite PRN (e.g. "G01" for GPS 01)
  310.      * @return the DCB data for the satellite
  311.      * @since 12.0
  312.      */
  313.     public DcbSatellite getDcbSatellite(final String prn) {
  314.         return dcbSatellites.get(prn);
  315.     }

  316.     /** Parser for SINEX files. */
  317.     private class Parser implements DataLoader {

  318.         /** Start character of a comment line. */
  319.         private static final String COMMENT = "*";

  320.         /** {@inheritDoc} */
  321.         @Override
  322.         public boolean stillAcceptsData() {
  323.             // We load all SINEX files we can find
  324.             return true;
  325.         }

  326.         /** {@inheritDoc} */
  327.         @Override
  328.         public void loadData(final InputStream input, final String name)
  329.             throws IOException, ParseException {

  330.             // Useful parameters
  331.             int lineNumber            = 0;
  332.             String line               = null;
  333.             boolean inDcbDesc         = false;
  334.             boolean inDcbSol          = false;
  335.             boolean inId              = false;
  336.             boolean inAntenna         = false;
  337.             boolean inEcc             = false;
  338.             boolean inEpoch           = false;
  339.             boolean inEstimate        = false;
  340.             Vector3D position         = Vector3D.ZERO;
  341.             Vector3D velocity         = Vector3D.ZERO;
  342.             String startDateString    = "";
  343.             String endDateString      = "";
  344.             String creationDateString = "";

  345.             // According to Sinex standard, the epochs are given in UTC scale.
  346.             // Except for DCB files for which a TIME_SYSTEM key is present.
  347.             TimeScale scale    = scales.getUTC();

  348.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {

  349.                 // Loop on lines
  350.                 for (line = reader.readLine(); line != null; line = reader.readLine()) {
  351.                     ++lineNumber;
  352.                     // For now, only few keys are supported
  353.                     // They represent the minimum set of parameters that are interesting to consider in a SINEX file
  354.                     // Other keys can be added depending user needs

  355.                     // The first line is parsed in order to get the creation, start and end dates of the file
  356.                     if (lineNumber == 1) {
  357.                         final Matcher matcher = PATTERN_BEGIN.matcher(line);
  358.                         if (matcher.matches()) {

  359.                             creationDateString = matcher.group(1);
  360.                             startDateString    = matcher.group(2);
  361.                             endDateString      = matcher.group(3);
  362.                             creationDate       = stringEpochToAbsoluteDate(creationDateString, false, scale);

  363.                             if (startDate == null) {
  364.                                 // First data loading, needs to initialize the start and end dates for EOP history
  365.                                 startDate = stringEpochToAbsoluteDate(startDateString, true,  scale);
  366.                                 endDate   = stringEpochToAbsoluteDate(endDateString,   false, scale);
  367.                             }
  368.                         } else {
  369.                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  370.                                                       lineNumber, name, line);
  371.                         }
  372.                     } else {
  373.                         switch (line.trim()) {
  374.                             case "+SITE/ID" :
  375.                                 // Start of site id. data
  376.                                 inId = true;
  377.                                 break;
  378.                             case "-SITE/ID" :
  379.                                 // End of site id. data
  380.                                 inId = false;
  381.                                 break;
  382.                             case "+SITE/ANTENNA" :
  383.                                 // Start of site antenna data
  384.                                 inAntenna = true;
  385.                                 break;
  386.                             case "-SITE/ANTENNA" :
  387.                                 // End of site antenna data
  388.                                 inAntenna = false;
  389.                                 break;
  390.                             case "+SITE/ECCENTRICITY" :
  391.                                 // Start of antenna eccentricities data
  392.                                 inEcc = true;
  393.                                 break;
  394.                             case "-SITE/ECCENTRICITY" :
  395.                                 // End of antenna eccentricities data
  396.                                 inEcc = false;
  397.                                 break;
  398.                             case "+SOLUTION/EPOCHS" :
  399.                                 // Start of epoch data
  400.                                 inEpoch = true;
  401.                                 break;
  402.                             case "-SOLUTION/EPOCHS" :
  403.                                 // End of epoch data
  404.                                 inEpoch = false;
  405.                                 break;
  406.                             case "+SOLUTION/ESTIMATE" :
  407.                                 // Start of coordinates data
  408.                                 inEstimate = true;
  409.                                 break;
  410.                             case "-SOLUTION/ESTIMATE" :
  411.                                 // End of coordinates data
  412.                                 inEstimate = false;
  413.                                 break;
  414.                             case "+BIAS/DESCRIPTION" :
  415.                                 // Start of Bias description block data
  416.                                 inDcbDesc = true;
  417.                                 break;
  418.                             case "-BIAS/DESCRIPTION" :
  419.                                 // End of Bias description block data
  420.                                 inDcbDesc = false;
  421.                                 break;
  422.                             case "+BIAS/SOLUTION" :
  423.                                 // Start of Bias solution block data
  424.                                 inDcbSol = true;
  425.                                 break;
  426.                             case "-BIAS/SOLUTION" :
  427.                                 // End of Bias solution block data
  428.                                 inDcbSol = false;
  429.                                 break;
  430.                             default:
  431.                                 if (line.startsWith(COMMENT)) {
  432.                                     // ignore that line
  433.                                 } else {
  434.                                     // parsing data
  435.                                     if (inId) {
  436.                                         // read site id. data
  437.                                         final Station station = new Station();
  438.                                         station.setSiteCode(parseString(line, 1, 4));
  439.                                         station.setDomes(parseString(line, 9, 9));
  440.                                         // add the station to the map
  441.                                         addStation(station);
  442.                                     } else if (inAntenna) {

  443.                                         // read antenna type data
  444.                                         final Station station = getStation(parseString(line, 1, 4));

  445.                                         final AbsoluteDate start = stringEpochToAbsoluteDate(parseString(line, 16, 12), true, scale);
  446.                                         final AbsoluteDate end   = stringEpochToAbsoluteDate(parseString(line, 29, 12), false, scale);

  447.                                         // antenna type
  448.                                         final String type = parseString(line, 42, 20);

  449.                                         // special implementation for the first entry
  450.                                         if (station.getAntennaTypeTimeSpanMap().getSpansNumber() == 1) {
  451.                                             // we want null values outside validity limits of the station
  452.                                             station.addAntennaTypeValidBefore(type, end);
  453.                                             station.addAntennaTypeValidBefore(null, start);
  454.                                         } else {
  455.                                             station.addAntennaTypeValidBefore(type, end);
  456.                                         }

  457.                                     } else if (inEcc) {

  458.                                         // read antenna eccentricities data
  459.                                         final Station station = getStation(parseString(line, 1, 4));

  460.                                         final AbsoluteDate start = stringEpochToAbsoluteDate(parseString(line, 16, 12), true, scale);
  461.                                         final AbsoluteDate end   = stringEpochToAbsoluteDate(parseString(line, 29, 12), false, scale);

  462.                                         // reference system UNE or XYZ
  463.                                         station.setEccRefSystem(ReferenceSystem.getEccRefSystem(parseString(line, 42, 3)));

  464.                                         // eccentricity vector
  465.                                         final Vector3D eccStation = new Vector3D(parseDouble(line, 46, 8),
  466.                                                                                  parseDouble(line, 55, 8),
  467.                                                                                  parseDouble(line, 64, 8));

  468.                                         // special implementation for the first entry
  469.                                         if (station.getEccentricitiesTimeSpanMap().getSpansNumber() == 1) {
  470.                                             // we want null values outside validity limits of the station
  471.                                             station.addStationEccentricitiesValidBefore(eccStation, end);
  472.                                             station.addStationEccentricitiesValidBefore(null,       start);
  473.                                         } else {
  474.                                             station.addStationEccentricitiesValidBefore(eccStation, end);
  475.                                         }

  476.                                     } else if (inEpoch) {
  477.                                         // read epoch data
  478.                                         final Station station = getStation(parseString(line, 1, 4));
  479.                                         station.setValidFrom(stringEpochToAbsoluteDate(parseString(line, 16, 12), true, scale));
  480.                                         station.setValidUntil(stringEpochToAbsoluteDate(parseString(line, 29, 12), false, scale));
  481.                                     } else if (inEstimate) {
  482.                                         final Station       station     = getStation(parseString(line, 14, 4));
  483.                                         final AbsoluteDate  currentDate = stringEpochToAbsoluteDate(parseString(line, 27, 12), false, scale);
  484.                                         final String        dataType    = parseString(line, 7, 6);
  485.                                         // check if this station exists or if we are parsing EOP
  486.                                         if (station != null || EOP_TYPES.contains(dataType)) {
  487.                                             // switch on coordinates data
  488.                                             switch (dataType) {
  489.                                                 case "STAX":
  490.                                                     // station X coordinate
  491.                                                     final double x = parseDouble(line, 47, 22);
  492.                                                     position = new Vector3D(x, position.getY(), position.getZ());
  493.                                                     station.setPosition(position);
  494.                                                     break;
  495.                                                 case "STAY":
  496.                                                     // station Y coordinate
  497.                                                     final double y = parseDouble(line, 47, 22);
  498.                                                     position = new Vector3D(position.getX(), y, position.getZ());
  499.                                                     station.setPosition(position);
  500.                                                     break;
  501.                                                 case "STAZ":
  502.                                                     // station Z coordinate
  503.                                                     final double z = parseDouble(line, 47, 22);
  504.                                                     position = new Vector3D(position.getX(), position.getY(), z);
  505.                                                     station.setPosition(position);
  506.                                                     // set the reference epoch (identical for all coordinates)
  507.                                                     station.setEpoch(currentDate);
  508.                                                     // reset position vector
  509.                                                     position = Vector3D.ZERO;
  510.                                                     break;
  511.                                                 case "VELX":
  512.                                                     // station X velocity (value is in m/y)
  513.                                                     final double vx = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  514.                                                     velocity = new Vector3D(vx, velocity.getY(), velocity.getZ());
  515.                                                     station.setVelocity(velocity);
  516.                                                     break;
  517.                                                 case "VELY":
  518.                                                     // station Y velocity (value is in m/y)
  519.                                                     final double vy = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  520.                                                     velocity = new Vector3D(velocity.getX(), vy, velocity.getZ());
  521.                                                     station.setVelocity(velocity);
  522.                                                     break;
  523.                                                 case "VELZ":
  524.                                                     // station Z velocity (value is in m/y)
  525.                                                     final double vz = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  526.                                                     velocity = new Vector3D(velocity.getX(), velocity.getY(), vz);
  527.                                                     station.setVelocity(velocity);
  528.                                                     // reset position vector
  529.                                                     velocity = Vector3D.ZERO;
  530.                                                     break;
  531.                                                 case XPO:
  532.                                                     // X polar motion
  533.                                                     final double xPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
  534.                                                     getSinexEopEntry(currentDate).setxPo(xPo);
  535.                                                     break;
  536.                                                 case YPO:
  537.                                                     // Y polar motion
  538.                                                     final double yPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
  539.                                                     getSinexEopEntry(currentDate).setyPo(yPo);
  540.                                                     break;
  541.                                                 case LOD:
  542.                                                     // length of day
  543.                                                     final double lod = parseDoubleWithUnit(line, 40, 4, 47, 21);
  544.                                                     getSinexEopEntry(currentDate).setLod(lod);
  545.                                                     break;
  546.                                                 case UT:
  547.                                                     // delta time UT1-UTC
  548.                                                     final double dt = parseDoubleWithUnit(line, 40, 4, 47, 21);
  549.                                                     getSinexEopEntry(currentDate).setUt1MinusUtc(dt);
  550.                                                     break;
  551.                                                 case NUT_LN:
  552.                                                     // nutation correction in longitude
  553.                                                     final double nutLn = parseDoubleWithUnit(line, 40, 4, 47, 21);
  554.                                                     getSinexEopEntry(currentDate).setNutLn(nutLn);
  555.                                                     break;
  556.                                                 case NUT_OB:
  557.                                                     // nutation correction in obliquity
  558.                                                     final double nutOb = parseDoubleWithUnit(line, 40, 4, 47, 21);
  559.                                                     getSinexEopEntry(currentDate).setNutOb(nutOb);
  560.                                                     break;
  561.                                                 case NUT_X:
  562.                                                     // nutation correction X
  563.                                                     final double nutX = parseDoubleWithUnit(line, 40, 4, 47, 21);
  564.                                                     getSinexEopEntry(currentDate).setNutX(nutX);
  565.                                                     break;
  566.                                                 case NUT_Y:
  567.                                                     // nutation correction Y
  568.                                                     final double nutY = parseDoubleWithUnit(line, 40, 4, 47, 21);
  569.                                                     getSinexEopEntry(currentDate).setNutY(nutY);
  570.                                                     break;
  571.                                                 default:
  572.                                                     // ignore that field
  573.                                                     break;
  574.                                             }
  575.                                         }
  576.                                     } else if (inDcbDesc) {
  577.                                         // Determining the data type for the DCBDescription object
  578.                                         final String[] splitLine = PATTERN_SPACE.split(line.trim());
  579.                                         final String dataType = splitLine[0];
  580.                                         final String data = splitLine[1];
  581.                                         switch (dataType) {
  582.                                             case "OBSERVATION_SAMPLING":
  583.                                                 dcbDescription.setObservationSampling(Integer.parseInt(data));
  584.                                                 break;
  585.                                             case "PARAMETER_SPACING":
  586.                                                 dcbDescription.setParameterSpacing(Integer.parseInt(data));
  587.                                                 break;
  588.                                             case "DETERMINATION_METHOD":
  589.                                                 dcbDescription.setDeterminationMethod(data);
  590.                                                 break;
  591.                                             case "BIAS_MODE":
  592.                                                 dcbDescription.setBiasMode(data);
  593.                                                 break;
  594.                                             case "TIME_SYSTEM":
  595.                                                 if ("UTC".equals(data)) {
  596.                                                     dcbDescription.setTimeSystem(TimeSystem.UTC);
  597.                                                 } else if ("TAI".equals(data)) {
  598.                                                     dcbDescription.setTimeSystem(TimeSystem.TAI);
  599.                                                 } else {
  600.                                                     dcbDescription.setTimeSystem(TimeSystem.parseOneLetterCode(data));
  601.                                                 }
  602.                                                 scale = dcbDescription.getTimeSystem().getTimeScale(scales);
  603.                                                 // A time scale has been parsed, update start, end, and creation dates
  604.                                                 // to take into account the time scale
  605.                                                 startDate    = stringEpochToAbsoluteDate(startDateString,    true,  scale);
  606.                                                 endDate      = stringEpochToAbsoluteDate(endDateString,      false, scale);
  607.                                                 creationDate = stringEpochToAbsoluteDate(creationDateString, false, scale);
  608.                                                 break;
  609.                                             default:
  610.                                                 break;
  611.                                         }
  612.                                     } else if (inDcbSol) {

  613.                                         // Parsing the data present in a DCB file solution line.
  614.                                         // Most fields are used in the files provided by CDDIS.
  615.                                         // Station is empty for satellite measurements.
  616.                                         // The separator between columns is composed of spaces.

  617.                                         final String satellitePrn = parseString(line, 11, 3);
  618.                                         final String siteCode     = parseString(line, 15, 9);

  619.                                         // Parsing the line data.
  620.                                         final String obs1 = parseString(line, 25, 4);
  621.                                         final String obs2 = parseString(line, 30, 4);
  622.                                         final AbsoluteDate beginDate = stringEpochToAbsoluteDate(parseString(line, 35, 14), true, scale);
  623.                                         final AbsoluteDate finalDate = stringEpochToAbsoluteDate(parseString(line, 50, 14), false, scale);
  624.                                         final Unit unitDcb = Unit.parse(parseString(line, 65, 4));
  625.                                         final double valueDcb = unitDcb.toSI(Double.parseDouble(parseString(line, 70, 21)));

  626.                                         // Verifying if present
  627.                                         if (siteCode.equals("")) {
  628.                                             final String id = satellitePrn;
  629.                                             DcbSatellite dcbSatellite = getDcbSatellite(id);
  630.                                             if (dcbSatellite == null) {
  631.                                                 dcbSatellite = new DcbSatellite(id);
  632.                                                 dcbSatellite.setDescription(dcbDescription);
  633.                                             }
  634.                                             final Dcb dcb = dcbSatellite.getDcbData();
  635.                                             // Add the data to the DCB object.
  636.                                             dcb.addDcbLine(obs1, obs2, beginDate, finalDate, valueDcb);
  637.                                             // Adding the object to the HashMap if not present.
  638.                                             addDcbSatellite(dcbSatellite, id);
  639.                                         } else {
  640.                                             final String id = siteCode;
  641.                                             DcbStation dcbStation = getDcbStation(id);
  642.                                             if (dcbStation == null) {
  643.                                                 dcbStation = new DcbStation(id);
  644.                                                 dcbStation.setDescription(dcbDescription);
  645.                                             }
  646.                                             final SatelliteSystem satSystem = SatelliteSystem.parseSatelliteSystem(satellitePrn);
  647.                                             // Add the data to the DCB object.
  648.                                             final Dcb dcb = dcbStation.getDcbData(satSystem);
  649.                                             if (dcb == null) {
  650.                                                 dcbStation.addDcb(satSystem, new Dcb());
  651.                                             }
  652.                                             dcbStation.getDcbData(satSystem).addDcbLine(obs1, obs2, beginDate, finalDate, valueDcb);
  653.                                             // Adding the object to the HashMap if not present.
  654.                                             addDcbStation(dcbStation, id);
  655.                                         }

  656.                                     } else {
  657.                                         // not supported line, ignore it
  658.                                     }
  659.                                 }
  660.                                 break;
  661.                         }
  662.                     }
  663.                 }

  664.             } catch (NumberFormatException nfe) {
  665.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  666.                         lineNumber, name, line);
  667.             }

  668.         }

  669.         /** Extract a string from a line.
  670.          * @param line to parse
  671.          * @param start start index of the string
  672.          * @param length length of the string
  673.          * @return parsed string
  674.          */
  675.         private String parseString(final String line, final int start, final int length) {
  676.             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
  677.         }

  678.         /** Extract a double from a line.
  679.          * @param line to parse
  680.          * @param start start index of the real
  681.          * @param length length of the real
  682.          * @return parsed real
  683.          */
  684.         private double parseDouble(final String line, final int start, final int length) {
  685.             return Double.parseDouble(parseString(line, start, length));
  686.         }

  687.         /** Extract a double from a line and convert in SI unit.
  688.          * @param line to parse
  689.          * @param startUnit start index of the unit
  690.          * @param lengthUnit length of the unit
  691.          * @param startDouble start index of the real
  692.          * @param lengthDouble length of the real
  693.          * @return parsed double in SI unit
  694.          */
  695.         private double parseDoubleWithUnit(final String line, final int startUnit, final int lengthUnit,
  696.                                            final int startDouble, final int lengthDouble) {
  697.             final Unit unit = Unit.parse(parseString(line, startUnit, lengthUnit));
  698.             return unit.toSI(parseDouble(line, startDouble, lengthDouble));
  699.         }

  700.     }

  701.     /**
  702.      * Transform a String epoch to an AbsoluteDate.
  703.      * @param stringDate string epoch
  704.      * @param isStart true if epoch is a start validity epoch
  705.      * @param scale TimeScale for the computation of the dates
  706.      * @return the corresponding AbsoluteDate
  707.      */
  708.     private AbsoluteDate stringEpochToAbsoluteDate(final String stringDate, final boolean isStart, final TimeScale scale) {

  709.         // Deal with 00:000:00000 epochs
  710.         if (DEFAULT_EPOCH_TWO_DIGITS.equals(stringDate) || DEFAULT_EPOCH_FOUR_DIGITS.equals(stringDate)) {
  711.             // If its a start validity epoch, the file start date shall be used.
  712.             // For end validity epoch, future infinity is acceptable.
  713.             return isStart ? startDate : AbsoluteDate.FUTURE_INFINITY;
  714.         }

  715.         // Date components
  716.         final String[] fields = SEPARATOR.split(stringDate);

  717.         // Read fields
  718.         final int digitsYear = Integer.parseInt(fields[0]);
  719.         final int day        = Integer.parseInt(fields[1]);
  720.         final int secInDay   = Integer.parseInt(fields[2]);

  721.         // Data year
  722.         final int year;
  723.         if (digitsYear > 50 && digitsYear < 100) {
  724.             year = 1900 + digitsYear;
  725.         } else if (digitsYear < 100) {
  726.             year = 2000 + digitsYear;
  727.         } else {
  728.             year = digitsYear;
  729.         }

  730.         // Return an absolute date.
  731.         // Initialize to 1st January of the given year because
  732.         // sometimes day in equal to 0 in the file.
  733.         return new AbsoluteDate(new DateComponents(year, 1, 1), scale).
  734.                 shiftedBy(Constants.JULIAN_DAY * (day - 1)).
  735.                 shiftedBy(secInDay);

  736.     }

  737.     /**
  738.      * Add a new entry to the map of stations.
  739.      * @param station station entry to add
  740.      */
  741.     private void addStation(final Station station) {
  742.         // Check if the station already exists
  743.         if (stations.get(station.getSiteCode()) == null) {
  744.             stations.put(station.getSiteCode(), station);
  745.         }
  746.     }

  747.     /**
  748.      * Add a new entry to the map of stations DCB.
  749.      * @param dcb DCB entry
  750.      * @param siteCode site code
  751.      * @since 12.0
  752.      */
  753.     private void addDcbStation(final DcbStation dcb, final String siteCode) {
  754.         // Check if the DCB for the current station already exists
  755.         if (dcbStations.get(siteCode) == null) {
  756.             dcbStations.put(siteCode, dcb);
  757.         }
  758.     }

  759.     /**
  760.      * Add a new entry to the map of satellites DCB.
  761.      * @param dcb DCB entry
  762.      * @param prn satellite PRN (e.g. "G01" for GPS 01)
  763.      * @since 12.0
  764.      */
  765.     private void addDcbSatellite(final DcbSatellite dcb, final String prn) {
  766.         if (dcbSatellites.get(prn) == null) {
  767.             dcbSatellites.put(prn, dcb);
  768.         }
  769.     }

  770.     /**
  771.      * Get the EOP entry for the given epoch.
  772.      * @param date epoch
  773.      * @return the EOP entry corresponding to the epoch
  774.      */
  775.     private SinexEopEntry getSinexEopEntry(final AbsoluteDate date) {
  776.         eop.putIfAbsent(date, new SinexEopEntry(date));
  777.         return eop.get(date);
  778.     }

  779.     /**
  780.      * Converts parsed EOP lines a list of EOP entries.
  781.      * <p>
  782.      * The first read chronological EOP entry is duplicated at the start
  783.      * time of the data as read in the Sinex header. In addition, the last
  784.      * read chronological data is duplicated at the end time of the data.
  785.      * </p>
  786.      * @param converter converter to use for nutation corrections
  787.      * @param scale time scale of EOP entries
  788.      * @return a list of EOP entries
  789.      */
  790.     private List<EOPEntry> getEopList(final IERSConventions.NutationCorrectionConverter converter,
  791.                                       final TimeScale scale) {

  792.         // Initialize the list
  793.         final List<EOPEntry> eopEntries = new ArrayList<>();

  794.         // Convert the map of parsed EOP data to a sorted set
  795.         final SortedSet<SinexEopEntry> set = mapToSortedSet(eop);

  796.         // Loop on set
  797.         for (final SinexEopEntry entry : set) {
  798.             // Add to the list
  799.             eopEntries.add(entry.toEopEntry(converter, itrfVersionEop, scale));
  800.         }

  801.         // Add first entry to the start time of the data
  802.         eopEntries.add(copyEopEntry(startDate, set.first()).toEopEntry(converter, itrfVersionEop, scale));

  803.         // Add the last entry to the end time of the data
  804.         eopEntries.add(copyEopEntry(endDate, set.last()).toEopEntry(converter, itrfVersionEop, scale));

  805.         if (set.size() < 2) {
  806.             // there is only one entry in the Sinex file
  807.             // in order for interpolation to work, we need to add more dummy entries
  808.             eopEntries.add(copyEopEntry(startDate.shiftedBy(+1.0), set.first()).toEopEntry(converter, itrfVersionEop, scale));
  809.             eopEntries.add(copyEopEntry(endDate.shiftedBy(-1.0),   set.last()).toEopEntry(converter, itrfVersionEop, scale));
  810.         }

  811.         // Return
  812.         eopEntries.sort(new ChronologicalComparator());
  813.         return eopEntries;

  814.     }

  815.     /**
  816.      * Convert a map of TimeStamped instances to a sorted set.
  817.      * @param inputMap input map
  818.      * @param <T> type of TimeStamped
  819.      * @return corresponding sorted set, chronologically ordered
  820.      */
  821.     private <T extends TimeStamped> SortedSet<T> mapToSortedSet(final Map<AbsoluteDate, T> inputMap) {

  822.         // Create a sorted set, chronologically ordered
  823.         final SortedSet<T> set = new TreeSet<>(new ChronologicalComparator());

  824.         // Fill the set
  825.         for (final Map.Entry<AbsoluteDate, T> entry : inputMap.entrySet()) {
  826.             set.add(entry.getValue());
  827.         }

  828.         // Return the filled list
  829.         return set;

  830.     }

  831.     /**
  832.      * Copy an EOP entry.
  833.      * <p>
  834.      * The data epoch is updated.
  835.      * </p>
  836.      * @param date new epoch
  837.      * @param reference reference used for the data
  838.      * @return a copy of the reference with new epoch
  839.      */
  840.     private SinexEopEntry copyEopEntry(final AbsoluteDate date, final SinexEopEntry reference) {

  841.         // Initialize
  842.         final SinexEopEntry newEntry = new SinexEopEntry(date);

  843.         // Fill
  844.         newEntry.setLod(reference.getLod());
  845.         newEntry.setUt1MinusUtc(reference.getUt1MinusUtc());
  846.         newEntry.setxPo(reference.getXPo());
  847.         newEntry.setyPo(reference.getYPo());
  848.         newEntry.setNutX(reference.getNutX());
  849.         newEntry.setNutY(reference.getNutY());
  850.         newEntry.setNutLn(reference.getNutLn());
  851.         newEntry.setNutOb(reference.getNutOb());

  852.         // Return
  853.         return newEntry;

  854.     }

  855. }