SinexLoader.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.files.sinex;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hipparchus.exception.DummyLocalizable;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataLoader;
import org.orekit.data.DataProvidersManager;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.sinex.Station.ReferenceSystem;
import org.orekit.frames.EOPEntry;
import org.orekit.frames.EopHistoryLoader;
import org.orekit.frames.ITRFVersion;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.gnss.TimeSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.ChronologicalComparator;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.time.TimeStamped;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
import org.orekit.utils.units.Unit;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /**
     * DCB data.
     * Key: Site code
     */
    private final Map<String, DcbStation> dcbStations;

    /**
     * DCB data.
     * Key: Satellite PRN
     */
    private final Map<String, DcbSatellite> dcbSatellites;

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

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

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

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

    /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
     * default data context}.
     * @param supportedNames regular expression for supported files names
     * @see #SinexLoader(String, DataProvidersManager, TimeScales)
     */
    @DefaultDataContext
    public SinexLoader(final String supportedNames) {
        this(supportedNames,
             DataContext.getDefault().getDataProvidersManager(),
             DataContext.getDefault().getTimeScales());
    }

    /**
     * Construct a loader by specifying the source of SINEX auxiliary data files.
     * <p>
     * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
     * possible to update the version using the {@link #setITRFVersion(int)}
     * method.
     * </p>
     * @param supportedNames regular expression for supported files names
     * @param dataProvidersManager provides access to auxiliary data.
     * @param scales time scales
     */
    public SinexLoader(final String supportedNames,
                       final DataProvidersManager dataProvidersManager,
                       final TimeScales scales) {
        // Common data
        this.scales         = scales;
        this.creationDate   = AbsoluteDate.FUTURE_INFINITY;
        // DCB parameters
        this.dcbDescription = new DcbDescription();
        this.dcbStations    = new HashMap<>();
        this.dcbSatellites  = new HashMap<>();
        // EOP parameters
        this.eop            = new HashMap<>();
        this.itrfVersionEop = ITRFVersion.ITRF_2014;
        // Station data
        this.stations       = new HashMap<>();

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

    /**
     * Simple constructor. This constructor uses the {@link DataContext#getDefault()
     * default data context}.
     * <p>
     * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
     * possible to update the version using the {@link #setITRFVersion(int)}
     * method.
     * </p>
     * @param source source for the RINEX data
     * @see #SinexLoader(String, DataProvidersManager, TimeScales)
     */
    @DefaultDataContext
    public SinexLoader(final DataSource source) {
        this(source, DataContext.getDefault().getTimeScales());
    }

    /**
     * Loads SINEX from the given input stream using the specified auxiliary data.
     * <p>
     * For EOP loading, a default {@link ITRFVersion#ITRF_2014} is used. It is
     * possible to update the version using the {@link #setITRFVersion(int)}
     * method.
     * </p>
     * @param source source for the RINEX data
     * @param scales time scales
     */
    public SinexLoader(final DataSource source, final TimeScales scales) {
        try {
            // Common data
            this.scales         = scales;
            this.creationDate   = AbsoluteDate.FUTURE_INFINITY;
            // EOP data
            this.itrfVersionEop = ITRFVersion.ITRF_2014;
            this.eop            = new HashMap<>();
            // DCB data
            this.dcbStations    = new HashMap<>();
            this.dcbSatellites  = new HashMap<>();
            this.dcbDescription = new DcbDescription();
            // Station data
            this.stations       = new HashMap<>();

            // Read the file
            try (InputStream         is  = source.getOpener().openStreamOnce();
                 BufferedInputStream bis = new BufferedInputStream(is)) {
                new Parser().loadData(bis, source.getName());
            }
        } catch (IOException | ParseException ioe) {
            throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
        }
    }

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

    /**
     * Get the ITRF version used for the EOP entries processing.
     * @return the ITRF Version used for the EOP processing.
     * @since 11.2
     */
    public ITRFVersion getITRFVersion() {
        return itrfVersionEop;
    }

    /**
     * Get the creation date of the parsed SINEX file.
     * @return SINEX file creation date as an AbsoluteDate
     * @since 12.0
     */
    public AbsoluteDate getCreationDate() {
        return creationDate;
    }

    /**
     * Get the file epoch start time.
     * @return the file epoch start time
     * @since 12.0
     */
    public AbsoluteDate getFileEpochStartTime() {
        return startDate;
    }

    /**
     * Get the file epoch end time.
     * @return the file epoch end time
     * @since 12.0
     */
    public AbsoluteDate getFileEpochEndTime() {
        return endDate;
    }

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

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

    /**
     * Get the station corresponding to the given site code.
     *
     * @param siteCode site code
     * @return the corresponding station
     */
    public Station getStation(final String siteCode) {
        return stations.get(siteCode);
    }

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

    /**
     * Get the DCB data for a given station.
     * @param siteCode site code
     * @return DCB data for the station
     * @since 12.0
     */
    public DcbStation getDcbStation(final String siteCode) {
        return dcbStations.get(siteCode);
    }

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

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

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

        /** {@inheritDoc} */
        @Override
        public boolean stillAcceptsData() {
            // We load all SINEX files we can find
            return true;
        }

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

            // Useful parameters
            int lineNumber            = 0;
            String line               = null;
            boolean inDcbDesc         = false;
            boolean inDcbSol          = false;
            boolean inId              = false;
            boolean inAntenna         = false;
            boolean inEcc             = false;
            boolean inEpoch           = false;
            boolean inEstimate        = false;
            Vector3D position         = Vector3D.ZERO;
            Vector3D velocity         = Vector3D.ZERO;
            String startDateString    = "";
            String endDateString      = "";
            String creationDateString = "";

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

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

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

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

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

                            if (startDate == null) {
                                // First data loading, needs to initialize the start and end dates for EOP history
                                startDate = stringEpochToAbsoluteDate(startDateString, true,  scale);
                                endDate   = stringEpochToAbsoluteDate(endDateString,   false, scale);
                            }
                        } else {
                            throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                      lineNumber, name, line);
                        }
                    } else {
                        switch (line.trim()) {
                            case "+SITE/ID" :
                                // Start of site id. data
                                inId = true;
                                break;
                            case "-SITE/ID" :
                                // End of site id. data
                                inId = false;
                                break;
                            case "+SITE/ANTENNA" :
                                // Start of site antenna data
                                inAntenna = true;
                                break;
                            case "-SITE/ANTENNA" :
                                // End of site antenna data
                                inAntenna = false;
                                break;
                            case "+SITE/ECCENTRICITY" :
                                // Start of antenna eccentricities data
                                inEcc = true;
                                break;
                            case "-SITE/ECCENTRICITY" :
                                // End of antenna eccentricities data
                                inEcc = false;
                                break;
                            case "+SOLUTION/EPOCHS" :
                                // Start of epoch data
                                inEpoch = true;
                                break;
                            case "-SOLUTION/EPOCHS" :
                                // End of epoch data
                                inEpoch = false;
                                break;
                            case "+SOLUTION/ESTIMATE" :
                                // Start of coordinates data
                                inEstimate = true;
                                break;
                            case "-SOLUTION/ESTIMATE" :
                                // End of coordinates data
                                inEstimate = false;
                                break;
                            case "+BIAS/DESCRIPTION" :
                                // Start of Bias description block data
                                inDcbDesc = true;
                                break;
                            case "-BIAS/DESCRIPTION" :
                                // End of Bias description block data
                                inDcbDesc = false;
                                break;
                            case "+BIAS/SOLUTION" :
                                // Start of Bias solution block data
                                inDcbSol = true;
                                break;
                            case "-BIAS/SOLUTION" :
                                // End of Bias solution block data
                                inDcbSol = false;
                                break;
                            default:
                                if (line.startsWith(COMMENT)) {
                                    // ignore that line
                                } else {
                                    // parsing data
                                    if (inId) {
                                        // read site id. data
                                        final Station station = new Station();
                                        station.setSiteCode(parseString(line, 1, 4));
                                        station.setDomes(parseString(line, 9, 9));
                                        // add the station to the map
                                        addStation(station);
                                    } else if (inAntenna) {

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

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

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

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

                                    } else if (inEcc) {

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

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

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

                                        // eccentricity vector
                                        final Vector3D eccStation = new Vector3D(parseDouble(line, 46, 8),
                                                                                 parseDouble(line, 55, 8),
                                                                                 parseDouble(line, 64, 8));

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

                                    } else if (inEpoch) {
                                        // read epoch data
                                        final Station station = getStation(parseString(line, 1, 4));
                                        station.setValidFrom(stringEpochToAbsoluteDate(parseString(line, 16, 12), true, scale));
                                        station.setValidUntil(stringEpochToAbsoluteDate(parseString(line, 29, 12), false, scale));
                                    } else if (inEstimate) {
                                        final Station       station     = getStation(parseString(line, 14, 4));
                                        final AbsoluteDate  currentDate = stringEpochToAbsoluteDate(parseString(line, 27, 12), false, scale);
                                        final String        dataType    = parseString(line, 7, 6);
                                        // check if this station exists or if we are parsing EOP
                                        if (station != null || EOP_TYPES.contains(dataType)) {
                                            // switch on coordinates data
                                            switch (dataType) {
                                                case "STAX":
                                                    // station X coordinate
                                                    final double x = parseDouble(line, 47, 22);
                                                    position = new Vector3D(x, position.getY(), position.getZ());
                                                    station.setPosition(position);
                                                    break;
                                                case "STAY":
                                                    // station Y coordinate
                                                    final double y = parseDouble(line, 47, 22);
                                                    position = new Vector3D(position.getX(), y, position.getZ());
                                                    station.setPosition(position);
                                                    break;
                                                case "STAZ":
                                                    // station Z coordinate
                                                    final double z = parseDouble(line, 47, 22);
                                                    position = new Vector3D(position.getX(), position.getY(), z);
                                                    station.setPosition(position);
                                                    // set the reference epoch (identical for all coordinates)
                                                    station.setEpoch(currentDate);
                                                    // reset position vector
                                                    position = Vector3D.ZERO;
                                                    break;
                                                case "VELX":
                                                    // station X velocity (value is in m/y)
                                                    final double vx = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
                                                    velocity = new Vector3D(vx, velocity.getY(), velocity.getZ());
                                                    station.setVelocity(velocity);
                                                    break;
                                                case "VELY":
                                                    // station Y velocity (value is in m/y)
                                                    final double vy = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
                                                    velocity = new Vector3D(velocity.getX(), vy, velocity.getZ());
                                                    station.setVelocity(velocity);
                                                    break;
                                                case "VELZ":
                                                    // station Z velocity (value is in m/y)
                                                    final double vz = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
                                                    velocity = new Vector3D(velocity.getX(), velocity.getY(), vz);
                                                    station.setVelocity(velocity);
                                                    // reset position vector
                                                    velocity = Vector3D.ZERO;
                                                    break;
                                                case XPO:
                                                    // X polar motion
                                                    final double xPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setxPo(xPo);
                                                    break;
                                                case YPO:
                                                    // Y polar motion
                                                    final double yPo = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setyPo(yPo);
                                                    break;
                                                case LOD:
                                                    // length of day
                                                    final double lod = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setLod(lod);
                                                    break;
                                                case UT:
                                                    // delta time UT1-UTC
                                                    final double dt = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setUt1MinusUtc(dt);
                                                    break;
                                                case NUT_LN:
                                                    // nutation correction in longitude
                                                    final double nutLn = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setNutLn(nutLn);
                                                    break;
                                                case NUT_OB:
                                                    // nutation correction in obliquity
                                                    final double nutOb = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setNutOb(nutOb);
                                                    break;
                                                case NUT_X:
                                                    // nutation correction X
                                                    final double nutX = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setNutX(nutX);
                                                    break;
                                                case NUT_Y:
                                                    // nutation correction Y
                                                    final double nutY = parseDoubleWithUnit(line, 40, 4, 47, 21);
                                                    getSinexEopEntry(currentDate).setNutY(nutY);
                                                    break;
                                                default:
                                                    // ignore that field
                                                    break;
                                            }
                                        }
                                    } else if (inDcbDesc) {
                                        // Determining the data type for the DCBDescription object
                                        final String[] splitLine = PATTERN_SPACE.split(line.trim());
                                        final String dataType = splitLine[0];
                                        final String data = splitLine[1];
                                        switch (dataType) {
                                            case "OBSERVATION_SAMPLING":
                                                dcbDescription.setObservationSampling(Integer.parseInt(data));
                                                break;
                                            case "PARAMETER_SPACING":
                                                dcbDescription.setParameterSpacing(Integer.parseInt(data));
                                                break;
                                            case "DETERMINATION_METHOD":
                                                dcbDescription.setDeterminationMethod(data);
                                                break;
                                            case "BIAS_MODE":
                                                dcbDescription.setBiasMode(data);
                                                break;
                                            case "TIME_SYSTEM":
                                                if ("UTC".equals(data)) {
                                                    dcbDescription.setTimeSystem(TimeSystem.UTC);
                                                } else if ("TAI".equals(data)) {
                                                    dcbDescription.setTimeSystem(TimeSystem.TAI);
                                                } else {
                                                    dcbDescription.setTimeSystem(TimeSystem.parseOneLetterCode(data));
                                                }
                                                scale = dcbDescription.getTimeSystem().getTimeScale(scales);
                                                // A time scale has been parsed, update start, end, and creation dates
                                                // to take into account the time scale
                                                startDate    = stringEpochToAbsoluteDate(startDateString,    true,  scale);
                                                endDate      = stringEpochToAbsoluteDate(endDateString,      false, scale);
                                                creationDate = stringEpochToAbsoluteDate(creationDateString, false, scale);
                                                break;
                                            default:
                                                break;
                                        }
                                    } else if (inDcbSol) {

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

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

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

                                        // Verifying if present
                                        if (siteCode.equals("")) {
                                            final String id = satellitePrn;
                                            DcbSatellite dcbSatellite = getDcbSatellite(id);
                                            if (dcbSatellite == null) {
                                                dcbSatellite = new DcbSatellite(id);
                                                dcbSatellite.setDescription(dcbDescription);
                                            }
                                            final Dcb dcb = dcbSatellite.getDcbData();
                                            // Add the data to the DCB object.
                                            dcb.addDcbLine(obs1, obs2, beginDate, finalDate, valueDcb);
                                            // Adding the object to the HashMap if not present.
                                            addDcbSatellite(dcbSatellite, id);
                                        } else {
                                            final String id = siteCode;
                                            DcbStation dcbStation = getDcbStation(id);
                                            if (dcbStation == null) {
                                                dcbStation = new DcbStation(id);
                                                dcbStation.setDescription(dcbDescription);
                                            }
                                            final SatelliteSystem satSystem = SatelliteSystem.parseSatelliteSystem(satellitePrn);
                                            // Add the data to the DCB object.
                                            final Dcb dcb = dcbStation.getDcbData(satSystem);
                                            if (dcb == null) {
                                                dcbStation.addDcb(satSystem, new Dcb());
                                            }
                                            dcbStation.getDcbData(satSystem).addDcbLine(obs1, obs2, beginDate, finalDate, valueDcb);
                                            // Adding the object to the HashMap if not present.
                                            addDcbStation(dcbStation, id);
                                        }

                                    } else {
                                        // not supported line, ignore it
                                    }
                                }
                                break;
                        }
                    }
                }

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

        }

        /** Extract a string from a line.
         * @param line to parse
         * @param start start index of the string
         * @param length length of the string
         * @return parsed string
         */
        private String parseString(final String line, final int start, final int length) {
            return line.substring(start, FastMath.min(line.length(), start + length)).trim();
        }

        /** Extract a double from a line.
         * @param line to parse
         * @param start start index of the real
         * @param length length of the real
         * @return parsed real
         */
        private double parseDouble(final String line, final int start, final int length) {
            return Double.parseDouble(parseString(line, start, length));
        }

        /** Extract a double from a line and convert in SI unit.
         * @param line to parse
         * @param startUnit start index of the unit
         * @param lengthUnit length of the unit
         * @param startDouble start index of the real
         * @param lengthDouble length of the real
         * @return parsed double in SI unit
         */
        private double parseDoubleWithUnit(final String line, final int startUnit, final int lengthUnit,
                                           final int startDouble, final int lengthDouble) {
            final Unit unit = Unit.parse(parseString(line, startUnit, lengthUnit));
            return unit.toSI(parseDouble(line, startDouble, lengthDouble));
        }

    }

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

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

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

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

        // Data year
        final int year;
        if (digitsYear > 50 && digitsYear < 100) {
            year = 1900 + digitsYear;
        } else if (digitsYear < 100) {
            year = 2000 + digitsYear;
        } else {
            year = digitsYear;
        }

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

    }

    /**
     * Add a new entry to the map of stations.
     * @param station station entry to add
     */
    private void addStation(final Station station) {
        // Check if the station already exists
        if (stations.get(station.getSiteCode()) == null) {
            stations.put(station.getSiteCode(), station);
        }
    }

    /**
     * Add a new entry to the map of stations DCB.
     * @param dcb DCB entry
     * @param siteCode site code
     * @since 12.0
     */
    private void addDcbStation(final DcbStation dcb, final String siteCode) {
        // Check if the DCB for the current station already exists
        if (dcbStations.get(siteCode) == null) {
            dcbStations.put(siteCode, dcb);
        }
    }

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

    /**
     * Get the EOP entry for the given epoch.
     * @param date epoch
     * @return the EOP entry corresponding to the epoch
     */
    private SinexEopEntry getSinexEopEntry(final AbsoluteDate date) {
        eop.putIfAbsent(date, new SinexEopEntry(date));
        return eop.get(date);
    }

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

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

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

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

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

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

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

        // Return
        eopEntries.sort(new ChronologicalComparator());
        return eopEntries;

    }

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

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

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

        // Return the filled list
        return set;

    }

    /**
     * Copy an EOP entry.
     * <p>
     * The data epoch is updated.
     * </p>
     * @param date new epoch
     * @param reference reference used for the data
     * @return a copy of the reference with new epoch
     */
    private SinexEopEntry copyEopEntry(final AbsoluteDate date, final SinexEopEntry reference) {

        // Initialize
        final SinexEopEntry newEntry = new SinexEopEntry(date);

        // Fill
        newEntry.setLod(reference.getLod());
        newEntry.setUt1MinusUtc(reference.getUt1MinusUtc());
        newEntry.setxPo(reference.getXPo());
        newEntry.setyPo(reference.getYPo());
        newEntry.setNutX(reference.getNutX());
        newEntry.setNutY(reference.getNutY());
        newEntry.setNutLn(reference.getNutLn());
        newEntry.setNutOb(reference.getNutOb());

        // Return
        return newEntry;

    }

}