SinexLoader.java

  1. /* Copyright 2002-2022 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.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.Pattern;

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

  57. /**
  58.  * Loader for Solution INdependent EXchange (SINEX) files.
  59.  * <p>
  60.  * For now only few keys are supported: SITE/ID, SITE/ECCENTRICITY, SOLUTION/EPOCHS and SOLUTION/ESTIMATE.
  61.  * They represent the minimum set of parameters that are interesting to consider in a SINEX file.
  62.  * </p>
  63.  * <p>
  64.  * The parsing of EOP parameters for multiple files in different SinexLoader object, fed into the default DataContext
  65.  * might pose a problem in case validity dates are overlapping. As Sinex daily solution files provide a single EOP entry,
  66.  * the Sinex loader will add points at the limits of data dates (startDate, endDate) of the Sinex file, which in case of
  67.  * overlap will lead to inconsistencies in the final EOPHistory object. Multiple files can be parsed using a single SinexLoader
  68.  * with a regex to overcome this issue.
  69.  * </p>
  70.  *
  71.  * @author Bryan Cazabonne
  72.  * @since 10.3
  73.  */
  74. public class SinexLoader implements EOPHistoryLoader {

  75.     /** Length of day. */
  76.     private static final String LOD = "LOD";

  77.     /** UT1-UTC. */
  78.     private static final String UT = "UT";

  79.     /** X polar motion. */
  80.     private static final String XPO = "XPO";

  81.     /** Y polar motion. */
  82.     private static final String YPO = "YPO";

  83.     /** Nutation correction in longitude. */
  84.     private static final String NUT_LN = "NUT_LN";

  85.     /** Nutation correction in obliquity. */
  86.     private static final String NUT_OB = "NUT_OB";

  87.     /** Nutation correction X. */
  88.     private static final String NUT_X = "NUT_X";

  89.     /** Nutation correction Y. */
  90.     private static final String NUT_Y = "NUT_Y";

  91.     /** 00:000:00000 epoch. */
  92.     private static final String DEFAULT_EPOCH = "00:000:00000";

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

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

  97.     /** Pattern to check beginning of SINEX files.*/
  98.     private static final Pattern PATTERN_BEGIN = Pattern.compile("(%=).*");

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

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

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

  105.     /** Station data.
  106.      * Key: Site code
  107.      */
  108.     private final Map<String, Station> stations;

  109.     /** Data set. */
  110.     private Map<AbsoluteDate, SinexEopEntry> map;

  111.     /** ITRF Version used for EOP parsing. */
  112.     private ITRFVersion itrfVersionEop;

  113.     /** UTC time scale. */
  114.     private final TimeScale utc;

  115.     /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
  116.      * default data context}.
  117.      * @param supportedNames regular expression for supported files names
  118.      * @see #SinexLoader(String, DataProvidersManager, TimeScale)
  119.      */
  120.     @DefaultDataContext
  121.     public SinexLoader(final String supportedNames) {
  122.         this(supportedNames,
  123.              DataContext.getDefault().getDataProvidersManager(),
  124.              DataContext.getDefault().getTimeScales().getUTC());
  125.     }

  126.     /**
  127.      * Construct a loader by specifying the source of SINEX auxiliary data files.
  128.      * <p>
  129.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  130.      * possible to update the version using the {@link #setITRFVersion(int)}
  131.      * method.
  132.      * </p>
  133.      * @param supportedNames regular expression for supported files names
  134.      * @param dataProvidersManager provides access to auxiliary data.
  135.      * @param utc UTC time scale
  136.      */
  137.     public SinexLoader(final String supportedNames,
  138.                        final DataProvidersManager dataProvidersManager,
  139.                        final TimeScale utc) {
  140.         this.utc            = utc;
  141.         this.stations       = new HashMap<>();
  142.         this.itrfVersionEop = ITRFVersion.ITRF_2014;
  143.         this.map            = new HashMap<>();
  144.         dataProvidersManager.feed(supportedNames, new Parser());
  145.     }

  146.     /**
  147.      * Simple constructor. This constructor uses the {@link DataContext#getDefault()
  148.      * default data context}.
  149.      * <p>
  150.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  151.      * possible to update the version using the {@link #setITRFVersion(int)}
  152.      * method.
  153.      * </p>
  154.      * @param source source for the RINEX data
  155.      * @see #SinexLoader(String, DataProvidersManager, TimeScale)
  156.      */
  157.     @DefaultDataContext
  158.     public SinexLoader(final DataSource source) {
  159.         this(source, DataContext.getDefault().getTimeScales().getUTC());
  160.     }

  161.     /**
  162.      * Loads SINEX from the given input stream using the specified auxiliary data.
  163.      * <p>
  164.      * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
  165.      * possible to update the version using the {@link #setITRFVersion(int)}
  166.      * method.
  167.      * </p>
  168.      * @param source source for the RINEX data
  169.      * @param utc UTC time scale
  170.      */
  171.     public SinexLoader(final DataSource source, final TimeScale utc) {
  172.         try {
  173.             this.utc            = utc;
  174.             this.stations       = new HashMap<>();
  175.             this.itrfVersionEop = ITRFVersion.ITRF_2014;
  176.             this.map            = new HashMap<>();
  177.             try (InputStream         is  = source.getOpener().openStreamOnce();
  178.                  BufferedInputStream bis = new BufferedInputStream(is)) {
  179.                 new Parser().loadData(bis, source.getName());
  180.             }
  181.         } catch (IOException | ParseException ioe) {
  182.             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  183.         }
  184.     }

  185.     /**
  186.      * Set the ITRF version used in EOP entries processing.
  187.      * @param year Year of the ITRF Version used for parsing EOP.
  188.      * @since 11.2
  189.      */
  190.     public void setITRFVersion(final int year) {
  191.         this.itrfVersionEop = ITRFVersion.getITRFVersion(year);
  192.     }

  193.     /**
  194.      * Get the ITRF version used for the EOP entries processing.
  195.      * @return the ITRF Version used for the EOP processing.
  196.      * @since 11.2
  197.      */
  198.     public ITRFVersion getITRFVersion() {
  199.         return itrfVersionEop;
  200.     }

  201.     /**
  202.      * Get the parsed station data.
  203.      * @return unmodifiable view of parsed station data
  204.      */
  205.     public Map<String, Station> getStations() {
  206.         return Collections.unmodifiableMap(stations);
  207.     }

  208.     /**
  209.      * Get the parsed EOP data.
  210.      * @return unmodifiable view of parsed station data
  211.      * @since 11.2
  212.      */
  213.     public Map<AbsoluteDate, SinexEopEntry> getParsedEop() {
  214.         return Collections.unmodifiableMap(map);
  215.     }

  216.     /**
  217.      * Get the station corresponding to the given site code.
  218.      * @param siteCode site code
  219.      * @return the corresponding station
  220.      */
  221.     public Station getStation(final String siteCode) {
  222.         return stations.get(siteCode);
  223.     }

  224.     /** {@inheritDoc} */
  225.     @Override
  226.     public void fillHistory(final NutationCorrectionConverter converter,
  227.                             final SortedSet<EOPEntry> history) {
  228.         // Fill the history set with the content of the parsed data
  229.         history.addAll(getEopList(converter));
  230.     }

  231.     /** Parser for SINEX files. */
  232.     private class Parser implements DataLoader {

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

  235.         /** {@inheritDoc} */
  236.         @Override
  237.         public boolean stillAcceptsData() {
  238.             // We load all SINEX files we can find
  239.             return true;
  240.         }

  241.         /** {@inheritDoc} */
  242.         @Override
  243.         public void loadData(final InputStream input, final String name)
  244.             throws IOException, ParseException {

  245.             // Useful parameters
  246.             int lineNumber     = 0;
  247.             String line        = null;
  248.             boolean inId       = false;
  249.             boolean inEcc      = false;
  250.             boolean inEpoch    = false;
  251.             boolean inEstimate = false;
  252.             boolean firstEcc   = true;
  253.             Vector3D position  = Vector3D.ZERO;
  254.             Vector3D velocity  = Vector3D.ZERO;

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

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

  262.                     // The first line is parsed in order to get the creation date of the file, which might be used
  263.                     // in the case of an absent date as the final date of the data.
  264.                     // Its position is fixed in the file, at the first line, in the 4th column.
  265.                     if (lineNumber == 1 && PATTERN_BEGIN.matcher(line).matches()) {
  266.                         final String[]     splitFirstLine = PATTERN_SPACE.split(line);
  267.                         final AbsoluteDate fileStartDate  = stringEpochToAbsoluteDate(splitFirstLine[5]);
  268.                         final AbsoluteDate fileEndDate    = stringEpochToAbsoluteDate(splitFirstLine[6]);
  269.                         if (startDate == null) {
  270.                             // First data loading, needs to initialize the start and end dates for EOP history
  271.                             startDate = fileStartDate;
  272.                             endDate   = fileEndDate;
  273.                         }
  274.                         // In case of multiple files used for EOP history, the start and end dates can be updated
  275.                         startDate = fileStartDate.isBefore(startDate) ? fileStartDate : startDate;
  276.                         endDate   = fileEndDate.isAfter(endDate) ? fileEndDate : endDate;
  277.                     }

  278.                     switch (line.trim()) {
  279.                         case "+SITE/ID" :
  280.                             // Start of site id. data
  281.                             inId = true;
  282.                             break;
  283.                         case "-SITE/ID" :
  284.                             // End of site id. data
  285.                             inId = false;
  286.                             break;
  287.                         case "+SITE/ECCENTRICITY" :
  288.                             // Start of antenna eccentricities data
  289.                             inEcc = true;
  290.                             break;
  291.                         case "-SITE/ECCENTRICITY" :
  292.                             // End of antenna eccentricities data
  293.                             inEcc = false;
  294.                             break;
  295.                         case "+SOLUTION/EPOCHS" :
  296.                             // Start of epoch data
  297.                             inEpoch = true;
  298.                             break;
  299.                         case "-SOLUTION/EPOCHS" :
  300.                             // End of epoch data
  301.                             inEpoch = false;
  302.                             break;
  303.                         case "+SOLUTION/ESTIMATE" :
  304.                             // Start of coordinates data
  305.                             inEstimate = true;
  306.                             break;
  307.                         case "-SOLUTION/ESTIMATE" :
  308.                             // Start of coordinates data
  309.                             inEstimate = false;
  310.                             break;
  311.                         default:
  312.                             if (line.startsWith(COMMENT)) {
  313.                                 // ignore that line
  314.                             } else {
  315.                                 // parsing data
  316.                                 if (inId) {
  317.                                     // read site id. data
  318.                                     final Station station = new Station();
  319.                                     station.setSiteCode(parseString(line, 1, 4));
  320.                                     station.setDomes(parseString(line, 9, 9));
  321.                                     // add the station to the map
  322.                                     addStation(station);
  323.                                 } else if (inEcc) {

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

  326.                                     // check if it is the first eccentricity entry for this station
  327.                                     if (station.getEccentricitiesTimeSpanMap().getSpansNumber() == 1) {
  328.                                         // we are parsing eccentricity data for a new station
  329.                                         firstEcc = true;
  330.                                     }

  331.                                     // start and end of validity for the current entry
  332.                                     final AbsoluteDate start = stringEpochToAbsoluteDate(parseString(line, 16, 12));
  333.                                     final AbsoluteDate end   = stringEpochToAbsoluteDate(parseString(line, 29, 12));

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

  336.                                     // eccentricity vector
  337.                                     final Vector3D eccStation = new Vector3D(parseDouble(line, 46, 8),
  338.                                                                              parseDouble(line, 55, 8),
  339.                                                                              parseDouble(line, 64, 8));

  340.                                     // special implementation for the first entry
  341.                                     if (firstEcc) {
  342.                                         // we want null values outside validity limits of the station
  343.                                         station.addStationEccentricitiesValidBefore(eccStation, end);
  344.                                         station.addStationEccentricitiesValidBefore(null,       start);
  345.                                         // we parsed the first entry, set the flag to false
  346.                                         firstEcc = false;
  347.                                     } else {
  348.                                         station.addStationEccentricitiesValidBefore(eccStation, end);
  349.                                     }

  350.                                     // update the last known eccentricities entry
  351.                                     station.setEccentricities(eccStation);

  352.                                 } else if (inEpoch) {
  353.                                     // read epoch data
  354.                                     final Station station = getStation(parseString(line, 1, 4));
  355.                                     station.setValidFrom(stringEpochToAbsoluteDate(parseString(line, 16, 12)));
  356.                                     station.setValidUntil(stringEpochToAbsoluteDate(parseString(line, 29, 12)));
  357.                                 } else if (inEstimate) {
  358.                                     final Station       station     = getStation(parseString(line, 14, 4));
  359.                                     final AbsoluteDate  currentDate = stringEpochToAbsoluteDate(parseString(line, 27, 12));
  360.                                     final String        dataType    = parseString(line, 7, 6);
  361.                                     // check if this station exists or if we are parsing EOP
  362.                                     if (station != null || EOP_TYPES.contains(dataType)) {
  363.                                         // switch on coordinates data
  364.                                         switch (dataType) {
  365.                                             case "STAX":
  366.                                                 // station X coordinate
  367.                                                 final double x = parseDouble(line, 47, 22);
  368.                                                 position = new Vector3D(x, position.getY(), position.getZ());
  369.                                                 station.setPosition(position);
  370.                                                 break;
  371.                                             case "STAY":
  372.                                                 // station Y coordinate
  373.                                                 final double y = parseDouble(line, 47, 22);
  374.                                                 position = new Vector3D(position.getX(), y, position.getZ());
  375.                                                 station.setPosition(position);
  376.                                                 break;
  377.                                             case "STAZ":
  378.                                                 // station Z coordinate
  379.                                                 final double z = parseDouble(line, 47, 22);
  380.                                                 position = new Vector3D(position.getX(), position.getY(), z);
  381.                                                 station.setPosition(position);
  382.                                                 // set the reference epoch (identical for all coordinates)
  383.                                                 station.setEpoch(currentDate);
  384.                                                 // reset position vector
  385.                                                 position = Vector3D.ZERO;
  386.                                                 break;
  387.                                             case "VELX":
  388.                                                 // station X velocity (value is in m/y)
  389.                                                 final double vx = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  390.                                                 velocity = new Vector3D(vx, velocity.getY(), velocity.getZ());
  391.                                                 station.setVelocity(velocity);
  392.                                                 break;
  393.                                             case "VELY":
  394.                                                 // station Y velocity (value is in m/y)
  395.                                                 final double vy = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  396.                                                 velocity = new Vector3D(velocity.getX(), vy, velocity.getZ());
  397.                                                 station.setVelocity(velocity);
  398.                                                 break;
  399.                                             case "VELZ":
  400.                                                 // station Z velocity (value is in m/y)
  401.                                                 final double vz = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
  402.                                                 velocity = new Vector3D(velocity.getX(), velocity.getY(), vz);
  403.                                                 station.setVelocity(velocity);
  404.                                                 // reset position vector
  405.                                                 velocity = Vector3D.ZERO;
  406.                                                 break;
  407.                                             case XPO:
  408.                                                 // X polar motion
  409.                                                 final double xPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
  410.                                                 getSinexEopEntry(currentDate).setxPo(xPo);
  411.                                                 break;
  412.                                             case YPO:
  413.                                                 // Y polar motion
  414.                                                 final double yPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
  415.                                                 getSinexEopEntry(currentDate).setyPo(yPo);
  416.                                                 break;
  417.                                             case LOD:
  418.                                                 // length of day
  419.                                                 final double lod = parseDoubleWithUnit(line, 40, 4, 47, 21);
  420.                                                 getSinexEopEntry(currentDate).setLod(lod);
  421.                                                 break;
  422.                                             case UT:
  423.                                                 // delta time UT1-UTC
  424.                                                 final double dt = parseDoubleWithUnit(line, 40, 4, 47, 21);
  425.                                                 getSinexEopEntry(currentDate).setUt1MinusUtc(dt);
  426.                                                 break;
  427.                                             case NUT_LN:
  428.                                                 // nutation correction in longitude
  429.                                                 final double nutLn = parseDoubleWithUnit(line, 40, 4, 47, 21);
  430.                                                 getSinexEopEntry(currentDate).setNutLn(nutLn);
  431.                                                 break;
  432.                                             case NUT_OB:
  433.                                                 // nutation correction in obliquity
  434.                                                 final double nutOb = parseDoubleWithUnit(line, 40, 4, 47, 21);
  435.                                                 getSinexEopEntry(currentDate).setNutOb(nutOb);
  436.                                                 break;
  437.                                             case NUT_X:
  438.                                                 // nutation correction X
  439.                                                 final double nutX = parseDoubleWithUnit(line, 40, 4, 47, 21);
  440.                                                 getSinexEopEntry(currentDate).setNutX(nutX);
  441.                                                 break;
  442.                                             case NUT_Y:
  443.                                                 // nutation correction Y
  444.                                                 final double nutY = parseDoubleWithUnit(line, 40, 4, 47, 21);
  445.                                                 getSinexEopEntry(currentDate).setNutY(nutY);
  446.                                                 break;
  447.                                             default:
  448.                                                 // ignore that field
  449.                                                 break;
  450.                                         }
  451.                                     }
  452.                                 } else {
  453.                                     // not supported line, ignore it
  454.                                 }
  455.                             }
  456.                             break;
  457.                     }
  458.                 }

  459.             } catch (NumberFormatException nfe) {
  460.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  461.                         lineNumber, name, line);
  462.             }

  463.         }

  464.         /** Extract a string from a line.
  465.          * @param line to parse
  466.          * @param start start index of the string
  467.          * @param length length of the string
  468.          * @return parsed string
  469.          */
  470.         private String parseString(final String line, final int start, final int length) {
  471.             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
  472.         }

  473.         /** Extract a double from a line.
  474.          * @param line to parse
  475.          * @param start start index of the real
  476.          * @param length length of the real
  477.          * @return parsed real
  478.          */
  479.         private double parseDouble(final String line, final int start, final int length) {
  480.             return Double.parseDouble(parseString(line, start, length));
  481.         }

  482.         /** Extract a double from a line and convert in SI unit.
  483.          * @param line to parse
  484.          * @param startUnit start index of the unit
  485.          * @param lengthUnit length of the unit
  486.          * @param startDouble start index of the real
  487.          * @param lengthDouble length of the real
  488.          * @return parsed double in SI unit
  489.          */
  490.         private double parseDoubleWithUnit(final String line, final int startUnit, final int lengthUnit,
  491.                                            final int startDouble, final int lengthDouble) {
  492.             final Unit unit = Unit.parse(parseString(line, startUnit, lengthUnit));
  493.             return unit.toSI(parseDouble(line, startDouble, lengthDouble));
  494.         }

  495.     }

  496.     /**
  497.      * Transform a String epoch to an AbsoluteDate.
  498.      * @param stringDate string epoch
  499.      * @return the corresponding AbsoluteDate
  500.      */
  501.     private AbsoluteDate stringEpochToAbsoluteDate(final String stringDate) {

  502.         // Deal with 00:000:00000 epochs
  503.         if (DEFAULT_EPOCH.equals(stringDate)) {
  504.             // Data is still available, return a dummy date at infinity in the future direction
  505.             return AbsoluteDate.FUTURE_INFINITY;
  506.         }

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

  509.         // Read fields
  510.         final int twoDigitsYear = Integer.parseInt(fields[0]);
  511.         final int day           = Integer.parseInt(fields[1]);
  512.         final int secInDay      = Integer.parseInt(fields[2]);

  513.         // Data year
  514.         final int year;
  515.         if (twoDigitsYear > 50) {
  516.             year = 1900 + twoDigitsYear;
  517.         } else {
  518.             year = 2000 + twoDigitsYear;
  519.         }

  520.         // Return an absolute date.
  521.         // Initialize to 1st January of the given year because
  522.         // sometimes day in equal to 0 in the file.
  523.         return new AbsoluteDate(new DateComponents(year, 1, 1), utc).
  524.                 shiftedBy(Constants.JULIAN_DAY * (day - 1)).
  525.                 shiftedBy(secInDay);

  526.     }

  527.     /**
  528.      * Add a new entry to the map of stations.
  529.      * @param station station entry to add
  530.      */
  531.     private void addStation(final Station station) {
  532.         // Check if station already exists
  533.         if (stations.get(station.getSiteCode()) == null) {
  534.             stations.put(station.getSiteCode(), station);
  535.         }
  536.     }

  537.     /**
  538.      * Get the EOP entry for the given epoch.
  539.      * @param date epoch
  540.      * @return the EOP entry corresponding to the epoch
  541.      */
  542.     private SinexEopEntry getSinexEopEntry(final AbsoluteDate date) {
  543.         map.putIfAbsent(date, new SinexEopEntry(date));
  544.         return map.get(date);
  545.     }

  546.     /**
  547.      * Converts parsed EOP lines a list of EOP entries.
  548.      * <p>
  549.      * The first read chronological EOP entry is duplicated at the start
  550.      * time of the data as read in the Sinex header. In addition, the last
  551.      * read chronological data is duplicated at the end time of the data.
  552.      * </p>
  553.      * @param converter converter to use for nutation corrections
  554.      * @return a list of EOP entries
  555.      */
  556.     private List<EOPEntry> getEopList(final IERSConventions.NutationCorrectionConverter converter) {

  557.         // Initialize the list
  558.         final List<EOPEntry> eop = new ArrayList<>();

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

  561.         // Loop on set
  562.         for (final SinexEopEntry entry : set) {
  563.             // Add to the list
  564.             eop.add(entry.toEopEntry(converter, itrfVersionEop, utc));
  565.         }

  566.         // Add first entry to the start time of the data
  567.         eop.add(copyEopEntry(startDate, set.first()).toEopEntry(converter, itrfVersionEop, utc));

  568.         // Add the last entry to the end time of the data
  569.         eop.add(copyEopEntry(endDate, set.last()).toEopEntry(converter, itrfVersionEop, utc));

  570.         // Return
  571.         eop.sort(new ChronologicalComparator());
  572.         return eop;

  573.     }

  574.     /**
  575.      * Convert a map of TimeStamped instances to a sorted set.
  576.      * @param inputMap input map
  577.      * @param <T> type of TimeStamped
  578.      * @return corresponding sorted set, chronologically ordered
  579.      */
  580.     private <T extends TimeStamped> SortedSet<T> mapToSortedSet(final Map<AbsoluteDate, T> inputMap) {

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

  583.         // Fill the set
  584.         for (final Map.Entry<AbsoluteDate, T> entry : inputMap.entrySet()) {
  585.             set.add(entry.getValue());
  586.         }

  587.         // Return the filled list
  588.         return set;

  589.     }

  590.     /**
  591.      * Copy an EOP entry.
  592.      * <p>
  593.      * The data epoch is updated.
  594.      * </p>
  595.      * @param date new epoch
  596.      * @param reference reference used for the data
  597.      * @return a copy of the reference with new epoch
  598.      */
  599.     private SinexEopEntry copyEopEntry(final AbsoluteDate date, final SinexEopEntry reference) {

  600.         // Initialize
  601.         final SinexEopEntry newEntry = new SinexEopEntry(date);

  602.         // Fill
  603.         newEntry.setLod(reference.getLod());
  604.         newEntry.setUt1MinusUtc(reference.getUt1MinusUtc());
  605.         newEntry.setxPo(reference.getXPo());
  606.         newEntry.setyPo(reference.getYPo());
  607.         newEntry.setNutX(reference.getNutX());
  608.         newEntry.setNutY(reference.getNutY());
  609.         newEntry.setNutLn(reference.getNutLn());
  610.         newEntry.setNutOb(reference.getNutOb());

  611.         // Return
  612.         return newEntry;

  613.     }

  614. }