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.models.earth.displacement.PsdCorrection;
  52. import org.orekit.time.AbsoluteDate;
  53. import org.orekit.time.ChronologicalComparator;
  54. import org.orekit.time.DateComponents;
  55. import org.orekit.time.TimeScale;
  56. import org.orekit.time.TimeScales;
  57. import org.orekit.time.TimeStamped;
  58. import org.orekit.utils.Constants;
  59. import org.orekit.utils.IERSConventions;
  60. import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
  61. import org.orekit.utils.units.Unit;

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

  80.     /** Station X position coordinate.
  81.      * @since 12.1
  82.      */
  83.     private static final String STAX = "STAX";

  84.     /** Station Y position coordinate.
  85.      * @since 12.1
  86.      */
  87.     private static final String STAY = "STAY";

  88.     /** Station Z position coordinate.
  89.      * @since 12.1
  90.      */
  91.     private static final String STAZ = "STAZ";

  92.     /** Station X velocity coordinate.
  93.      * @since 12.1
  94.      */
  95.     private static final String VELX = "VELX";

  96.     /** Station Y velocity coordinate.
  97.      * @since 12.1
  98.      */
  99.     private static final String VELY = "VELY";

  100.     /** Station Z velocity coordinate.
  101.      * @since 12.1
  102.      */
  103.     private static final String VELZ = "VELZ";

  104.     /** Post-Seismic Deformation amplitude for exponential correction along East direction.
  105.      * @since 12.1
  106.      */
  107.     private static final String AEXP_E = "AEXP_E";

  108.     /** Post-Seismic Deformation relaxation time for exponential correction along East direction.
  109.      * @since 12.1
  110.      */
  111.     private static final String TEXP_E = "TEXP_E";

  112.     /** Post-Seismic Deformation amplitude for logarithmic correction along East direction.
  113.      * @since 12.1
  114.      */
  115.     private static final String ALOG_E = "ALOG_E";

  116.     /** Post-Seismic Deformation relaxation time for logarithmic correction along East direction.
  117.      * @since 12.1
  118.      */
  119.     private static final String TLOG_E = "TLOG_E";

  120.     /** Post-Seismic Deformation amplitude for exponential correction along North direction.
  121.      * @since 12.1
  122.      */
  123.     private static final String AEXP_N = "AEXP_N";

  124.     /** Post-Seismic Deformation relaxation time for exponential correction along North direction.
  125.      * @since 12.1
  126.      */
  127.     private static final String TEXP_N = "TEXP_N";

  128.     /** Post-Seismic Deformation amplitude for logarithmic correction along North direction.
  129.      * @since 12.1
  130.      */
  131.     private static final String ALOG_N = "ALOG_N";

  132.     /** Post-Seismic Deformation relaxation time for logarithmic correction along North direction.
  133.      * @since 12.1
  134.      */
  135.     private static final String TLOG_N = "TLOG_N";

  136.     /** Post-Seismic Deformation amplitude for exponential correction along up direction.
  137.      * @since 12.1
  138.      */
  139.     private static final String AEXP_U = "AEXP_U";

  140.     /** Post-Seismic Deformation relaxation time for exponential correction along up direction.
  141.      * @since 12.1
  142.      */
  143.     private static final String TEXP_U = "TEXP_U";

  144.     /** Post-Seismic Deformation amplitude for logarithmic correction along up direction.
  145.      * @since 12.1
  146.      */
  147.     private static final String ALOG_U = "ALOG_U";

  148.     /** Post-Seismic Deformation relaxation time for logarithmic correction along up direction.
  149.      * @since 12.1
  150.      */
  151.     private static final String TLOG_U = "TLOG_U";

  152.     /** Length of day. */
  153.     private static final String LOD = "LOD";

  154.     /** UT1-UTC. */
  155.     private static final String UT = "UT";

  156.     /** X polar motion. */
  157.     private static final String XPO = "XPO";

  158.     /** Y polar motion. */
  159.     private static final String YPO = "YPO";

  160.     /** Nutation correction in longitude. */
  161.     private static final String NUT_LN = "NUT_LN";

  162.     /** Nutation correction in obliquity. */
  163.     private static final String NUT_OB = "NUT_OB";

  164.     /** Nutation correction X. */
  165.     private static final String NUT_X = "NUT_X";

  166.     /** Nutation correction Y. */
  167.     private static final String NUT_Y = "NUT_Y";

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

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

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

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

  176.     /** Pattern to check beginning of SINEX files.*/
  177.     private static final Pattern PATTERN_BEGIN = Pattern.compile("%=(?:SNX|BIA) \\d\\.\\d\\d ..." +
  178.                                                                  " (\\d{2,4}:\\d{3}:\\d{5}) ..." +
  179.                                                                  " (\\d{2,4}:\\d{3}:\\d{5}) (\\d{2,4}:\\d{3}:\\d{5})" +
  180.                                                                  " . .*");

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

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

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

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

  189.     /** Station data.
  190.      * Key: Site code
  191.      */
  192.     private final Map<String, Station> stations;

  193.     /**
  194.      * DCB data.
  195.      * Key: Site code
  196.      */
  197.     private final Map<String, DcbStation> dcbStations;

  198.     /**
  199.      * DCB data.
  200.      * Key: Satellite PRN
  201.      */
  202.     private final Map<String, DcbSatellite> dcbSatellites;

  203.     /** DCB description. */
  204.     private final DcbDescription dcbDescription;

  205.     /** Data set. */
  206.     private final Map<AbsoluteDate, SinexEopEntry> eop;

  207.     /** ITRF Version used for EOP parsing. */
  208.     private ITRFVersion itrfVersionEop;

  209.     /** Time scales. */
  210.     private final TimeScales scales;

  211.     /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
  212.      * default data context}.
  213.      * @param supportedNames regular expression for supported files names
  214.      * @see #SinexLoader(String, DataProvidersManager, TimeScales)
  215.      */
  216.     @DefaultDataContext
  217.     public SinexLoader(final String supportedNames) {
  218.         this(supportedNames,
  219.              DataContext.getDefault().getDataProvidersManager(),
  220.              DataContext.getDefault().getTimeScales());
  221.     }

  222.     /**
  223.      * Construct a loader by specifying the source of SINEX auxiliary data files.
  224.      * <p>
  225.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  226.      * possible to update the version using the {@link #setITRFVersion(int)}
  227.      * method.
  228.      * </p>
  229.      * @param supportedNames regular expression for supported files names
  230.      * @param dataProvidersManager provides access to auxiliary data.
  231.      * @param scales time scales
  232.      */
  233.     public SinexLoader(final String supportedNames,
  234.                        final DataProvidersManager dataProvidersManager,
  235.                        final TimeScales scales) {
  236.         // Common data
  237.         this.scales         = scales;
  238.         this.creationDate   = AbsoluteDate.FUTURE_INFINITY;
  239.         // DCB parameters
  240.         this.dcbDescription = new DcbDescription();
  241.         this.dcbStations    = new HashMap<>();
  242.         this.dcbSatellites  = new HashMap<>();
  243.         // EOP parameters
  244.         this.eop            = new HashMap<>();
  245.         this.itrfVersionEop = ITRFVersion.ITRF_2014;
  246.         // Station data
  247.         this.stations       = new HashMap<>();

  248.         // Read the file
  249.         dataProvidersManager.feed(supportedNames, new Parser());
  250.     }

  251.     /**
  252.      * Simple constructor. This constructor uses the {@link DataContext#getDefault()
  253.      * default data context}.
  254.      * <p>
  255.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  256.      * possible to update the version using the {@link #setITRFVersion(int)}
  257.      * method.
  258.      * </p>
  259.      * @param source source for the RINEX data
  260.      * @see #SinexLoader(String, DataProvidersManager, TimeScales)
  261.      */
  262.     @DefaultDataContext
  263.     public SinexLoader(final DataSource source) {
  264.         this(source, DataContext.getDefault().getTimeScales());
  265.     }

  266.     /**
  267.      * Loads SINEX from the given input stream using the specified auxiliary data.
  268.      * <p>
  269.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  270.      * possible to update the version using the {@link #setITRFVersion(int)}
  271.      * method.
  272.      * </p>
  273.      * @param source source for the RINEX data
  274.      * @param scales time scales
  275.      */
  276.     public SinexLoader(final DataSource source, final TimeScales scales) {
  277.         try {
  278.             // Common data
  279.             this.scales         = scales;
  280.             this.creationDate   = AbsoluteDate.FUTURE_INFINITY;
  281.             // EOP data
  282.             this.itrfVersionEop = ITRFVersion.ITRF_2014;
  283.             this.eop            = new HashMap<>();
  284.             // DCB data
  285.             this.dcbStations    = new HashMap<>();
  286.             this.dcbSatellites  = new HashMap<>();
  287.             this.dcbDescription = new DcbDescription();
  288.             // Station data
  289.             this.stations       = new HashMap<>();

  290.             // Read the file
  291.             try (InputStream         is  = source.getOpener().openStreamOnce();
  292.                  BufferedInputStream bis = new BufferedInputStream(is)) {
  293.                 new Parser().loadData(bis, source.getName());
  294.             }
  295.         } catch (IOException | ParseException ioe) {
  296.             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  297.         }
  298.     }

  299.     /**
  300.      * Set the ITRF version used in EOP entries processing.
  301.      * @param year Year of the ITRF Version used for parsing EOP.
  302.      * @since 11.2
  303.      */
  304.     public void setITRFVersion(final int year) {
  305.         this.itrfVersionEop = ITRFVersion.getITRFVersion(year);
  306.     }

  307.     /**
  308.      * Get the ITRF version used for the EOP entries processing.
  309.      * @return the ITRF Version used for the EOP processing.
  310.      * @since 11.2
  311.      */
  312.     public ITRFVersion getITRFVersion() {
  313.         return itrfVersionEop;
  314.     }

  315.     /**
  316.      * Get the creation date of the parsed SINEX file.
  317.      * @return SINEX file creation date as an AbsoluteDate
  318.      * @since 12.0
  319.      */
  320.     public AbsoluteDate getCreationDate() {
  321.         return creationDate;
  322.     }

  323.     /**
  324.      * Get the file epoch start time.
  325.      * @return the file epoch start time
  326.      * @since 12.0
  327.      */
  328.     public AbsoluteDate getFileEpochStartTime() {
  329.         return startDate;
  330.     }

  331.     /**
  332.      * Get the file epoch end time.
  333.      * @return the file epoch end time
  334.      * @since 12.0
  335.      */
  336.     public AbsoluteDate getFileEpochEndTime() {
  337.         return endDate;
  338.     }

  339.     /**
  340.      * Get the parsed station data.
  341.      * @return unmodifiable view of parsed station data
  342.      */
  343.     public Map<String, Station> getStations() {
  344.         return Collections.unmodifiableMap(stations);
  345.     }

  346.     /**
  347.      * Get the parsed EOP data.
  348.      * @return unmodifiable view of parsed station data
  349.      * @since 11.2
  350.      */
  351.     public Map<AbsoluteDate, SinexEopEntry> getParsedEop() {
  352.         return Collections.unmodifiableMap(eop);
  353.     }

  354.     /**
  355.      * Get the station corresponding to the given site code.
  356.      *
  357.      * @param siteCode site code
  358.      * @return the corresponding station
  359.      */
  360.     public Station getStation(final String siteCode) {
  361.         return stations.get(siteCode);
  362.     }

  363.     /** {@inheritDoc} */
  364.     @Override
  365.     public void fillHistory(final NutationCorrectionConverter converter,
  366.                             final SortedSet<EOPEntry> history) {
  367.         // Fill the history set with the content of the parsed data
  368.         // According to Sinex standard, data are given in UTC
  369.         history.addAll(getEopList(converter, scales.getUTC()));
  370.     }

  371.     /**
  372.      * Get the DCB data for a given station.
  373.      * @param siteCode site code
  374.      * @return DCB data for the station
  375.      * @since 12.0
  376.      */
  377.     public DcbStation getDcbStation(final String siteCode) {
  378.         return dcbStations.get(siteCode);
  379.     }

  380.     /**
  381.      * Get the DCB data for a given satellite identified by its PRN.
  382.      * @param prn the satellite PRN (e.g. "G01" for GPS 01)
  383.      * @return the DCB data for the satellite
  384.      * @since 12.0
  385.      */
  386.     public DcbSatellite getDcbSatellite(final String prn) {
  387.         return dcbSatellites.get(prn);
  388.     }

  389.     /** Parser for SINEX files. */
  390.     private class Parser implements DataLoader {

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

  393.         /** Station x position coordinate.
  394.          * @since 12.1
  395.          */
  396.         private double px;

  397.         /** Station y position coordinate.
  398.          * @since 12.1
  399.          */
  400.         private double py;

  401.         /** Station z position coordinate.
  402.          * @since 12.1
  403.          */
  404.         private double pz;

  405.         /** Station x velocity coordinate.
  406.          * @since 12.1
  407.          */
  408.         private double vx;

  409.         /** Station y velocity coordinate.
  410.          * @since 12.1
  411.          */
  412.         private double vy;

  413.         /** Station z velocity coordinate.
  414.          * @since 12.1
  415.          */
  416.         private double vz;

  417.         /** Correction axis.
  418.          * @since 12.1
  419.          */
  420.         private PsdCorrection.Axis axis;

  421.         /** Correction time evolution.
  422.          * @since 12.1
  423.          */
  424.         private PsdCorrection.TimeEvolution evolution;

  425.         /** Correction amplitude.
  426.          * @since 12.1
  427.          */
  428.         private double amplitude;

  429.         /** Correction relaxation time.
  430.          * @since 12.1
  431.          */
  432.         private double relaxationTime;

  433.         /** Simple constructor.
  434.          */
  435.         Parser() {
  436.             resetPosition();
  437.             resetVelocity();
  438.             resetPsdCorrection();
  439.         }

  440.         /** {@inheritDoc} */
  441.         @Override
  442.         public boolean stillAcceptsData() {
  443.             // We load all SINEX files we can find
  444.             return true;
  445.         }

  446.         /** {@inheritDoc} */
  447.         @Override
  448.         public void loadData(final InputStream input, final String name)
  449.             throws IOException, ParseException {

  450.             // Useful parameters
  451.             int lineNumber            = 0;
  452.             String line               = null;
  453.             boolean inDcbDesc         = false;
  454.             boolean inDcbSol          = false;
  455.             boolean inId              = false;
  456.             boolean inAntenna         = false;
  457.             boolean inEcc             = false;
  458.             boolean inEpoch           = false;
  459.             boolean inEstimate        = false;
  460.             String startDateString    = "";
  461.             String endDateString      = "";
  462.             String creationDateString = "";


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

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

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

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

  477.                             creationDateString = matcher.group(1);
  478.                             startDateString    = matcher.group(2);
  479.                             endDateString      = matcher.group(3);
  480.                             creationDate       = stringEpochToAbsoluteDate(creationDateString, false, scale);

  481.                             if (startDate == null) {
  482.                                 // First data loading, needs to initialize the start and end dates for EOP history
  483.                                 startDate = stringEpochToAbsoluteDate(startDateString, true,  scale);
  484.                                 endDate   = stringEpochToAbsoluteDate(endDateString,   false, scale);
  485.                             }
  486.                         } else {
  487.                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  488.                                                       lineNumber, name, line);
  489.                         }
  490.                     } else {
  491.                         switch (line.trim()) {
  492.                             case "+SITE/ID" :
  493.                                 // Start of site id. data
  494.                                 inId = true;
  495.                                 break;
  496.                             case "-SITE/ID" :
  497.                                 // End of site id. data
  498.                                 inId = false;
  499.                                 break;
  500.                             case "+SITE/ANTENNA" :
  501.                                 // Start of site antenna data
  502.                                 inAntenna = true;
  503.                                 break;
  504.                             case "-SITE/ANTENNA" :
  505.                                 // End of site antenna data
  506.                                 inAntenna = false;
  507.                                 break;
  508.                             case "+SITE/ECCENTRICITY" :
  509.                                 // Start of antenna eccentricities data
  510.                                 inEcc = true;
  511.                                 break;
  512.                             case "-SITE/ECCENTRICITY" :
  513.                                 // End of antenna eccentricities data
  514.                                 inEcc = false;
  515.                                 break;
  516.                             case "+SOLUTION/EPOCHS" :
  517.                                 // Start of epoch data
  518.                                 inEpoch = true;
  519.                                 break;
  520.                             case "-SOLUTION/EPOCHS" :
  521.                                 // End of epoch data
  522.                                 inEpoch = false;
  523.                                 break;
  524.                             case "+SOLUTION/ESTIMATE" :
  525.                                 // Start of coordinates data
  526.                                 inEstimate = true;
  527.                                 break;
  528.                             case "-SOLUTION/ESTIMATE" :
  529.                                 // End of coordinates data
  530.                                 inEstimate = false;
  531.                                 break;
  532.                             case "+BIAS/DESCRIPTION" :
  533.                                 // Start of Bias description block data
  534.                                 inDcbDesc = true;
  535.                                 break;
  536.                             case "-BIAS/DESCRIPTION" :
  537.                                 // End of Bias description block data
  538.                                 inDcbDesc = false;
  539.                                 break;
  540.                             case "+BIAS/SOLUTION" :
  541.                                 // Start of Bias solution block data
  542.                                 inDcbSol = true;
  543.                                 break;
  544.                             case "-BIAS/SOLUTION" :
  545.                                 // End of Bias solution block data
  546.                                 inDcbSol = false;
  547.                                 break;
  548.                             default:
  549.                                 if (line.startsWith(COMMENT)) {
  550.                                     // ignore that line
  551.                                 } else {
  552.                                     // parsing data
  553.                                     if (inId) {
  554.                                         // read site id. data
  555.                                         final Station station = new Station();
  556.                                         station.setSiteCode(parseString(line, 1, 4));
  557.                                         station.setDomes(parseString(line, 9, 9));
  558.                                         // add the station to the map
  559.                                         addStation(station);
  560.                                     } else if (inAntenna) {

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

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

  565.                                         // antenna type
  566.                                         final String type = parseString(line, 42, 20);

  567.                                         // special implementation for the first entry
  568.                                         if (station.getAntennaTypeTimeSpanMap().getSpansNumber() == 1) {
  569.                                             // we want null values outside validity limits of the station
  570.                                             station.addAntennaTypeValidBefore(type, end);
  571.                                             station.addAntennaTypeValidBefore(null, start);
  572.                                         } else {
  573.                                             station.addAntennaTypeValidBefore(type, end);
  574.                                         }

  575.                                     } else if (inEcc) {

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

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

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

  582.                                         // eccentricity vector
  583.                                         final Vector3D eccStation = new Vector3D(parseDouble(line, 46, 8),
  584.                                                                                  parseDouble(line, 55, 8),
  585.                                                                                  parseDouble(line, 64, 8));

  586.                                         // special implementation for the first entry
  587.                                         if (station.getEccentricitiesTimeSpanMap().getSpansNumber() == 1) {
  588.                                             // we want null values outside validity limits of the station
  589.                                             station.addStationEccentricitiesValidBefore(eccStation, end);
  590.                                             station.addStationEccentricitiesValidBefore(null,       start);
  591.                                         } else {
  592.                                             station.addStationEccentricitiesValidBefore(eccStation, end);
  593.                                         }

  594.                                     } else if (inEpoch) {
  595.                                         // read epoch data
  596.                                         final Station station = getStation(parseString(line, 1, 4));
  597.                                         station.setValidFrom(stringEpochToAbsoluteDate(parseString(line, 16, 12), true, scale));
  598.                                         station.setValidUntil(stringEpochToAbsoluteDate(parseString(line, 29, 12), false, scale));
  599.                                     } else if (inEstimate) {
  600.                                         final Station       station     = getStation(parseString(line, 14, 4));
  601.                                         final AbsoluteDate  currentDate = stringEpochToAbsoluteDate(parseString(line, 27, 12), false, scale);
  602.                                         final String        dataType    = parseString(line, 7, 6);
  603.                                         // check if this station exists or if we are parsing EOP
  604.                                         if (station != null || EOP_TYPES.contains(dataType)) {
  605.                                             // switch on coordinates data
  606.                                             switch (dataType) {
  607.                                                 case STAX:
  608.                                                     // station X coordinate
  609.                                                     px = parseDouble(line, 47, 22);
  610.                                                     finalizePositionIfComplete(station, currentDate);
  611.                                                     break;
  612.                                                 case STAY:
  613.                                                     // station Y coordinate
  614.                                                     py = parseDouble(line, 47, 22);
  615.                                                     finalizePositionIfComplete(station, currentDate);
  616.                                                     break;
  617.                                                 case STAZ:
  618.                                                     // station Z coordinate
  619.                                                     pz = parseDouble(line, 47, 22);
  620.                                                     finalizePositionIfComplete(station, currentDate);
  621.                                                     break;
  622.                                                 case VELX:
  623.                                                     // station X velocity (value is in m/y)
  624.                                                     vx = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  625.                                                     finalizeVelocityIfComplete(station);
  626.                                                     break;
  627.                                                 case VELY:
  628.                                                     // station Y velocity (value is in m/y)
  629.                                                     vy = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  630.                                                     finalizeVelocityIfComplete(station);
  631.                                                     break;
  632.                                                 case VELZ:
  633.                                                     // station Z velocity (value is in m/y)
  634.                                                     vz = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  635.                                                     finalizeVelocityIfComplete(station);
  636.                                                     break;
  637.                                                 case AEXP_E:
  638.                                                     // amplitude of exponential correction for Post-Seismic Deformation
  639.                                                     evolution = PsdCorrection.TimeEvolution.EXP;
  640.                                                     axis      = PsdCorrection.Axis.EAST;
  641.                                                     amplitude = parseDouble(line, 47, 22);
  642.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  643.                                                     break;
  644.                                                 case TEXP_E:
  645.                                                     // relaxation toime of exponential correction for Post-Seismic Deformation
  646.                                                     evolution = PsdCorrection.TimeEvolution.EXP;
  647.                                                     axis           = PsdCorrection.Axis.EAST;
  648.                                                     relaxationTime = parseDouble(line, 47, 22) * Constants.JULIAN_YEAR;
  649.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  650.                                                     break;
  651.                                                 case ALOG_E:
  652.                                                     // amplitude of exponential correction for Post-Seismic Deformation
  653.                                                     evolution = PsdCorrection.TimeEvolution.LOG;
  654.                                                     axis      = PsdCorrection.Axis.EAST;
  655.                                                     amplitude = parseDouble(line, 47, 22);
  656.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  657.                                                     break;
  658.                                                 case TLOG_E:
  659.                                                     // relaxation toime of exponential correction for Post-Seismic Deformation
  660.                                                     evolution      = PsdCorrection.TimeEvolution.LOG;
  661.                                                     axis           = PsdCorrection.Axis.EAST;
  662.                                                     relaxationTime = parseDouble(line, 47, 22) * Constants.JULIAN_YEAR;
  663.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  664.                                                     break;
  665.                                                 case AEXP_N:
  666.                                                     // amplitude of exponential correction for Post-Seismic Deformation
  667.                                                     evolution = PsdCorrection.TimeEvolution.EXP;
  668.                                                     axis      = PsdCorrection.Axis.NORTH;
  669.                                                     amplitude = parseDouble(line, 47, 22);
  670.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  671.                                                     break;
  672.                                                 case TEXP_N:
  673.                                                     // relaxation toime of exponential correction for Post-Seismic Deformation
  674.                                                     evolution      = PsdCorrection.TimeEvolution.EXP;
  675.                                                     axis           = PsdCorrection.Axis.NORTH;
  676.                                                     relaxationTime = parseDouble(line, 47, 22) * Constants.JULIAN_YEAR;
  677.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  678.                                                     break;
  679.                                                 case ALOG_N:
  680.                                                     // amplitude of exponential correction for Post-Seismic Deformation
  681.                                                     evolution = PsdCorrection.TimeEvolution.LOG;
  682.                                                     axis      = PsdCorrection.Axis.NORTH;
  683.                                                     amplitude = parseDouble(line, 47, 22);
  684.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  685.                                                     break;
  686.                                                 case TLOG_N:
  687.                                                     // relaxation toime of exponential correction for Post-Seismic Deformation
  688.                                                     evolution      = PsdCorrection.TimeEvolution.LOG;
  689.                                                     axis           = PsdCorrection.Axis.NORTH;
  690.                                                     relaxationTime = parseDouble(line, 47, 22) * Constants.JULIAN_YEAR;
  691.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  692.                                                     break;
  693.                                                 case AEXP_U:
  694.                                                     // amplitude of exponential correction for Post-Seismic Deformation
  695.                                                     evolution = PsdCorrection.TimeEvolution.EXP;
  696.                                                     axis      = PsdCorrection.Axis.UP;
  697.                                                     amplitude = parseDouble(line, 47, 22);
  698.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  699.                                                     break;
  700.                                                 case TEXP_U:
  701.                                                     // relaxation toime of exponential correction for Post-Seismic Deformation
  702.                                                     evolution      = PsdCorrection.TimeEvolution.EXP;
  703.                                                     axis           = PsdCorrection.Axis.UP;
  704.                                                     relaxationTime = parseDouble(line, 47, 22) * Constants.JULIAN_YEAR;
  705.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  706.                                                     break;
  707.                                                 case ALOG_U:
  708.                                                     // amplitude of exponential correction for Post-Seismic Deformation
  709.                                                     evolution = PsdCorrection.TimeEvolution.LOG;
  710.                                                     axis      = PsdCorrection.Axis.UP;
  711.                                                     amplitude = parseDouble(line, 47, 22);
  712.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  713.                                                     break;
  714.                                                 case TLOG_U:
  715.                                                     // relaxation toime of exponential correction for Post-Seismic Deformation
  716.                                                     evolution      = PsdCorrection.TimeEvolution.LOG;
  717.                                                     axis           = PsdCorrection.Axis.UP;
  718.                                                     relaxationTime = parseDouble(line, 47, 22) * Constants.JULIAN_YEAR;
  719.                                                     finalizePsdCorrectionIfComplete(station, currentDate);
  720.                                                     break;
  721.                                                 case XPO:
  722.                                                     // X polar motion
  723.                                                     final double xPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
  724.                                                     getSinexEopEntry(currentDate).setxPo(xPo);
  725.                                                     break;
  726.                                                 case YPO:
  727.                                                     // Y polar motion
  728.                                                     final double yPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
  729.                                                     getSinexEopEntry(currentDate).setyPo(yPo);
  730.                                                     break;
  731.                                                 case LOD:
  732.                                                     // length of day
  733.                                                     final double lod = parseDoubleWithUnit(line, 40, 4, 47, 21);
  734.                                                     getSinexEopEntry(currentDate).setLod(lod);
  735.                                                     break;
  736.                                                 case UT:
  737.                                                     // delta time UT1-UTC
  738.                                                     final double dt = parseDoubleWithUnit(line, 40, 4, 47, 21);
  739.                                                     getSinexEopEntry(currentDate).setUt1MinusUtc(dt);
  740.                                                     break;
  741.                                                 case NUT_LN:
  742.                                                     // nutation correction in longitude
  743.                                                     final double nutLn = parseDoubleWithUnit(line, 40, 4, 47, 21);
  744.                                                     getSinexEopEntry(currentDate).setNutLn(nutLn);
  745.                                                     break;
  746.                                                 case NUT_OB:
  747.                                                     // nutation correction in obliquity
  748.                                                     final double nutOb = parseDoubleWithUnit(line, 40, 4, 47, 21);
  749.                                                     getSinexEopEntry(currentDate).setNutOb(nutOb);
  750.                                                     break;
  751.                                                 case NUT_X:
  752.                                                     // nutation correction X
  753.                                                     final double nutX = parseDoubleWithUnit(line, 40, 4, 47, 21);
  754.                                                     getSinexEopEntry(currentDate).setNutX(nutX);
  755.                                                     break;
  756.                                                 case NUT_Y:
  757.                                                     // nutation correction Y
  758.                                                     final double nutY = parseDoubleWithUnit(line, 40, 4, 47, 21);
  759.                                                     getSinexEopEntry(currentDate).setNutY(nutY);
  760.                                                     break;
  761.                                                 default:
  762.                                                     // ignore that field
  763.                                                     break;
  764.                                             }
  765.                                         }
  766.                                     } else if (inDcbDesc) {
  767.                                         // Determining the data type for the DCBDescription object
  768.                                         final String[] splitLine = PATTERN_SPACE.split(line.trim());
  769.                                         final String dataType = splitLine[0];
  770.                                         final String data = splitLine[1];
  771.                                         switch (dataType) {
  772.                                             case "OBSERVATION_SAMPLING":
  773.                                                 dcbDescription.setObservationSampling(Integer.parseInt(data));
  774.                                                 break;
  775.                                             case "PARAMETER_SPACING":
  776.                                                 dcbDescription.setParameterSpacing(Integer.parseInt(data));
  777.                                                 break;
  778.                                             case "DETERMINATION_METHOD":
  779.                                                 dcbDescription.setDeterminationMethod(data);
  780.                                                 break;
  781.                                             case "BIAS_MODE":
  782.                                                 dcbDescription.setBiasMode(data);
  783.                                                 break;
  784.                                             case "TIME_SYSTEM":
  785.                                                 if ("UTC".equals(data)) {
  786.                                                     dcbDescription.setTimeSystem(TimeSystem.UTC);
  787.                                                 } else if ("TAI".equals(data)) {
  788.                                                     dcbDescription.setTimeSystem(TimeSystem.TAI);
  789.                                                 } else {
  790.                                                     dcbDescription.setTimeSystem(TimeSystem.parseOneLetterCode(data));
  791.                                                 }
  792.                                                 scale = dcbDescription.getTimeSystem().getTimeScale(scales);
  793.                                                 // A time scale has been parsed, update start, end, and creation dates
  794.                                                 // to take into account the time scale
  795.                                                 startDate    = stringEpochToAbsoluteDate(startDateString,    true,  scale);
  796.                                                 endDate      = stringEpochToAbsoluteDate(endDateString,      false, scale);
  797.                                                 creationDate = stringEpochToAbsoluteDate(creationDateString, false, scale);
  798.                                                 break;
  799.                                             default:
  800.                                                 break;
  801.                                         }
  802.                                     } else if (inDcbSol) {

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

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

  809.                                         // Parsing the line data.
  810.                                         final String obs1 = parseString(line, 25, 4);
  811.                                         final String obs2 = parseString(line, 30, 4);
  812.                                         final AbsoluteDate beginDate = stringEpochToAbsoluteDate(parseString(line, 35, 14), true, scale);
  813.                                         final AbsoluteDate finalDate = stringEpochToAbsoluteDate(parseString(line, 50, 14), false, scale);
  814.                                         final Unit unitDcb = Unit.parse(parseString(line, 65, 4));
  815.                                         final double valueDcb = unitDcb.toSI(Double.parseDouble(parseString(line, 70, 21)));

  816.                                         // Verifying if present
  817.                                         if (siteCode.isEmpty()) {
  818.                                             DcbSatellite dcbSatellite = getDcbSatellite(satellitePrn);
  819.                                             if (dcbSatellite == null) {
  820.                                                 dcbSatellite = new DcbSatellite(satellitePrn);
  821.                                                 dcbSatellite.setDescription(dcbDescription);
  822.                                             }
  823.                                             final Dcb dcb = dcbSatellite.getDcbData();
  824.                                             // Add the data to the DCB object.
  825.                                             dcb.addDcbLine(obs1, obs2, beginDate, finalDate, valueDcb);
  826.                                             // Adding the object to the HashMap if not present.
  827.                                             addDcbSatellite(dcbSatellite, satellitePrn);
  828.                                         } else {
  829.                                             DcbStation dcbStation = getDcbStation(siteCode);
  830.                                             if (dcbStation == null) {
  831.                                                 dcbStation = new DcbStation(siteCode);
  832.                                                 dcbStation.setDescription(dcbDescription);
  833.                                             }
  834.                                             final SatelliteSystem satSystem = SatelliteSystem.parseSatelliteSystem(satellitePrn);
  835.                                             // Add the data to the DCB object.
  836.                                             final Dcb dcb = dcbStation.getDcbData(satSystem);
  837.                                             if (dcb == null) {
  838.                                                 dcbStation.addDcb(satSystem, new Dcb());
  839.                                             }
  840.                                             dcbStation.getDcbData(satSystem).addDcbLine(obs1, obs2, beginDate, finalDate, valueDcb);
  841.                                             // Adding the object to the HashMap if not present.
  842.                                             addDcbStation(dcbStation, siteCode);
  843.                                         }

  844.                                     } else {
  845.                                         // not supported line, ignore it
  846.                                     }
  847.                                 }
  848.                                 break;
  849.                         }
  850.                     }
  851.                 }

  852.             } catch (NumberFormatException nfe) {
  853.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  854.                         lineNumber, name, line);
  855.             }

  856.         }

  857.         /** Extract a string from a line.
  858.          * @param line to parse
  859.          * @param start start index of the string
  860.          * @param length length of the string
  861.          * @return parsed string
  862.          */
  863.         private String parseString(final String line, final int start, final int length) {
  864.             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
  865.         }

  866.         /** Extract a double from a line.
  867.          * @param line to parse
  868.          * @param start start index of the real
  869.          * @param length length of the real
  870.          * @return parsed real
  871.          */
  872.         private double parseDouble(final String line, final int start, final int length) {
  873.             return Double.parseDouble(parseString(line, start, length));
  874.         }

  875.         /** Extract a double from a line and convert in SI unit.
  876.          * @param line to parse
  877.          * @param startUnit start index of the unit
  878.          * @param lengthUnit length of the unit
  879.          * @param startDouble start index of the real
  880.          * @param lengthDouble length of the real
  881.          * @return parsed double in SI unit
  882.          */
  883.         private double parseDoubleWithUnit(final String line, final int startUnit, final int lengthUnit,
  884.                                            final int startDouble, final int lengthDouble) {
  885.             final Unit unit = Unit.parse(parseString(line, startUnit, lengthUnit));
  886.             return unit.toSI(parseDouble(line, startDouble, lengthDouble));
  887.         }

  888.         /** Finalize station position if complete.
  889.          * @param station station
  890.          * @param epoch coordinates epoch
  891.          * @since 12.1
  892.          */
  893.         private void finalizePositionIfComplete(final Station station, final AbsoluteDate epoch) {
  894.             if (!Double.isNaN(px + py + pz)) {
  895.                 // all coordinates are available, position is complete
  896.                 station.setPosition(new Vector3D(px, py, pz));
  897.                 station.setEpoch(epoch);
  898.                 resetPosition();
  899.             }
  900.         }

  901.         /** Reset position.
  902.          * @since 12.1
  903.          */
  904.         private void resetPosition() {
  905.             px = Double.NaN;
  906.             py = Double.NaN;
  907.             pz = Double.NaN;
  908.         }

  909.         /** Finalize station velocity if complete.
  910.          * @param station station
  911.          * @since 12.1
  912.          */
  913.         private void finalizeVelocityIfComplete(final Station station) {
  914.             if (!Double.isNaN(vx + vy + vz)) {
  915.                 // all coordinates are available, velocity is complete
  916.                 station.setVelocity(new Vector3D(vx, vy, vz));
  917.                 resetVelocity();
  918.             }
  919.         }

  920.         /** Reset velocity.
  921.          * @since 12.1
  922.          */
  923.         private void resetVelocity() {
  924.             vx = Double.NaN;
  925.             vy = Double.NaN;
  926.             vz = Double.NaN;
  927.         }

  928.         /** Finalize a Post-Seismic Deformation correction model if complete.
  929.          * @param station station
  930.          * @param epoch coordinates epoch
  931.          * @since 12.1
  932.          */
  933.         private void finalizePsdCorrectionIfComplete(final Station station, final AbsoluteDate epoch) {
  934.             if (!Double.isNaN(amplitude + relaxationTime)) {
  935.                 // both amplitude and relaxation time are available, correction is complete
  936.                 final PsdCorrection correction = new PsdCorrection(axis, evolution, epoch, amplitude, relaxationTime);
  937.                 station.addPsdCorrectionValidAfter(correction, epoch);
  938.                 resetPsdCorrection();
  939.             }
  940.         }

  941.         /** Reset Post-Seismic Deformation correction model.
  942.          * @since 12.1
  943.          */
  944.         private void resetPsdCorrection() {
  945.             axis           = null;
  946.             evolution      = null;
  947.             amplitude      = Double.NaN;
  948.             relaxationTime = Double.NaN;
  949.         }

  950.     }

  951.     /**
  952.      * Transform a String epoch to an AbsoluteDate.
  953.      * @param stringDate string epoch
  954.      * @param isStart true if epoch is a start validity epoch
  955.      * @param scale TimeScale for the computation of the dates
  956.      * @return the corresponding AbsoluteDate
  957.      */
  958.     private AbsoluteDate stringEpochToAbsoluteDate(final String stringDate, final boolean isStart, final TimeScale scale) {

  959.         // Deal with 00:000:00000 epochs
  960.         if (DEFAULT_EPOCH_TWO_DIGITS.equals(stringDate) || DEFAULT_EPOCH_FOUR_DIGITS.equals(stringDate)) {
  961.             // If its a start validity epoch, the file start date shall be used.
  962.             // For end validity epoch, future infinity is acceptable.
  963.             return isStart ? startDate : AbsoluteDate.FUTURE_INFINITY;
  964.         }

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

  967.         // Read fields
  968.         final int digitsYear = Integer.parseInt(fields[0]);
  969.         final int day        = Integer.parseInt(fields[1]);
  970.         final int secInDay   = Integer.parseInt(fields[2]);

  971.         // Data year
  972.         final int year;
  973.         if (digitsYear > 50 && digitsYear < 100) {
  974.             year = 1900 + digitsYear;
  975.         } else if (digitsYear < 100) {
  976.             year = 2000 + digitsYear;
  977.         } else {
  978.             year = digitsYear;
  979.         }

  980.         // Return an absolute date.
  981.         // Initialize to 1st January of the given year because
  982.         // sometimes day in equal to 0 in the file.
  983.         return new AbsoluteDate(new DateComponents(year, 1, 1), scale).
  984.                 shiftedBy(Constants.JULIAN_DAY * (day - 1)).
  985.                 shiftedBy(secInDay);

  986.     }

  987.     /**
  988.      * Add a new entry to the map of stations.
  989.      * @param station station entry to add
  990.      */
  991.     private void addStation(final Station station) {
  992.         // Check if the station already exists
  993.         stations.putIfAbsent(station.getSiteCode(), station);
  994.     }

  995.     /**
  996.      * Add a new entry to the map of stations DCB.
  997.      * @param dcb DCB entry
  998.      * @param siteCode site code
  999.      * @since 12.0
  1000.      */
  1001.     private void addDcbStation(final DcbStation dcb, final String siteCode) {
  1002.         // Check if the DCB for the current station already exists
  1003.         dcbStations.putIfAbsent(siteCode, dcb);
  1004.     }

  1005.     /**
  1006.      * Add a new entry to the map of satellites DCB.
  1007.      * @param dcb DCB entry
  1008.      * @param prn satellite PRN (e.g. "G01" for GPS 01)
  1009.      * @since 12.0
  1010.      */
  1011.     private void addDcbSatellite(final DcbSatellite dcb, final String prn) {
  1012.         dcbSatellites.putIfAbsent(prn, dcb);
  1013.     }

  1014.     /**
  1015.      * Get the EOP entry for the given epoch.
  1016.      * @param date epoch
  1017.      * @return the EOP entry corresponding to the epoch
  1018.      */
  1019.     private SinexEopEntry getSinexEopEntry(final AbsoluteDate date) {
  1020.         eop.putIfAbsent(date, new SinexEopEntry(date));
  1021.         return eop.get(date);
  1022.     }

  1023.     /**
  1024.      * Converts parsed EOP lines a list of EOP entries.
  1025.      * <p>
  1026.      * The first read chronological EOP entry is duplicated at the start
  1027.      * time of the data as read in the Sinex header. In addition, the last
  1028.      * read chronological data is duplicated at the end time of the data.
  1029.      * </p>
  1030.      * @param converter converter to use for nutation corrections
  1031.      * @param scale time scale of EOP entries
  1032.      * @return a list of EOP entries
  1033.      */
  1034.     private List<EOPEntry> getEopList(final IERSConventions.NutationCorrectionConverter converter,
  1035.                                       final TimeScale scale) {

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

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

  1040.         // Loop on set
  1041.         for (final SinexEopEntry entry : set) {
  1042.             // Add to the list
  1043.             eopEntries.add(entry.toEopEntry(converter, itrfVersionEop, scale));
  1044.         }

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

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

  1049.         if (set.size() < 2) {
  1050.             // there is only one entry in the Sinex file
  1051.             // in order for interpolation to work, we need to add more dummy entries
  1052.             eopEntries.add(copyEopEntry(startDate.shiftedBy(+1.0), set.first()).toEopEntry(converter, itrfVersionEop, scale));
  1053.             eopEntries.add(copyEopEntry(endDate.shiftedBy(-1.0),   set.last()).toEopEntry(converter, itrfVersionEop, scale));
  1054.         }

  1055.         // Return
  1056.         eopEntries.sort(new ChronologicalComparator());
  1057.         return eopEntries;

  1058.     }

  1059.     /**
  1060.      * Convert a map of TimeStamped instances to a sorted set.
  1061.      * @param inputMap input map
  1062.      * @param <T> type of TimeStamped
  1063.      * @return corresponding sorted set, chronologically ordered
  1064.      */
  1065.     private <T extends TimeStamped> SortedSet<T> mapToSortedSet(final Map<AbsoluteDate, T> inputMap) {

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

  1068.         // Fill the set
  1069.         for (final Map.Entry<AbsoluteDate, T> entry : inputMap.entrySet()) {
  1070.             set.add(entry.getValue());
  1071.         }

  1072.         // Return the filled list
  1073.         return set;

  1074.     }

  1075.     /**
  1076.      * Copy an EOP entry.
  1077.      * <p>
  1078.      * The data epoch is updated.
  1079.      * </p>
  1080.      * @param date new epoch
  1081.      * @param reference reference used for the data
  1082.      * @return a copy of the reference with new epoch
  1083.      */
  1084.     private SinexEopEntry copyEopEntry(final AbsoluteDate date, final SinexEopEntry reference) {

  1085.         // Initialize
  1086.         final SinexEopEntry newEntry = new SinexEopEntry(date);

  1087.         // Fill
  1088.         newEntry.setLod(reference.getLod());
  1089.         newEntry.setUt1MinusUtc(reference.getUt1MinusUtc());
  1090.         newEntry.setxPo(reference.getXPo());
  1091.         newEntry.setyPo(reference.getYPo());
  1092.         newEntry.setNutX(reference.getNutX());
  1093.         newEntry.setNutY(reference.getNutY());
  1094.         newEntry.setNutLn(reference.getNutLn());
  1095.         newEntry.setNutOb(reference.getNutOb());

  1096.         // Return
  1097.         return newEntry;

  1098.     }

  1099. }