RinexBaseHeader.java

/* Copyright 2002-2025 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.rinex.section;

import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.rinex.utils.ParsingUtils;
import org.orekit.files.rinex.utils.RinexFileType;
import org.orekit.gnss.PredefinedTimeSystem;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.Month;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Base container for Rinex headers.
 * @since 12.0
 */
public abstract class RinexBaseHeader {

    /** Pattern for splitting date, time and time zone. */
    private static final Pattern SPLITTING_PATTERN = Pattern.compile("([0-9]+[/ -]?[0-9A-Za-z]+[/ -]?[0-9]+) +([0-9:]+) *([A-Z]+)?");

    /** Pattern for dates with month abbrevation. */
    private static final Pattern DATE_DD_MMM_YY_PATTERN = Pattern.compile("([0-9]{1,2})-([A-Za-z]{3})-([0-9]{2,4})");

    /** Pattern for dates with month abbrevation.
     * @since 14.0
     */
    private static final Pattern DATE_YYYY_MMM_DD_PATTERN = Pattern.compile("([0-9]{4})[- ]([A-Za-z]{3})[- ]([0-9]{1,2})");

    /** Pattern for dates in ISO-8601 complete representation (basic or extended). */
    private static final Pattern DATE_ISO_8601_PATTERN = Pattern.compile("([0-9]{4})-?([0-9]{2})-?([0-9]{2})");

    /** Pattern for dates in european format. */
    private static final Pattern DATE_EUROPEAN_PATTERN = Pattern.compile("([0-9]{2})/([0-9]{2})/([0-9]{2})");

    /** Pattern for time. */
    private static final Pattern TIME_PATTERN = Pattern.compile("([0-9]{2}):?([0-9]{2})(?::?([0-9]{2}))?");

    /** Orekit program name.
     * @since 14.0
     */
    private static final String OREKIT = "Orekit";

    /** User name property.
     * @since 14.0
     */
    private static final String USER_NAME = "user.name";

    /** File type . */
    private final RinexFileType fileType;

    /** Rinex format Version. */
    private double formatVersion;

    /** Satellite System of the Rinex file (G/R/S/E/M). */
    private SatelliteSystem satelliteSystem;

    /** Name of the program creating current file. */
    private String programName;

    /** Name of the creator of the current file. */
    private String runByName;

    /** Date of the file creation. */
    private DateTimeComponents creationDateComponents;

    /** Time zone of the file creation. */
    private String creationTimeZone;

    /** Creation date as absolute date. */
    private AbsoluteDate creationDate;

    /** Receiver Number.
     * @since 14.0
     */
    private String receiverNumber;

    /** Receiver Type.
     * @since 14.0
     */
    private String receiverType;

    /** Receiver version.
     * @since 14.0
     */
    private String receiverVersion;

    /** Number of leap seconds separating UTC and GNSS time systems.
     * <p>
     * This is really the number of leap seconds since GPS epoch
     * on 1980-01-06.
     * </p>
     * @since 14.0
     */
    private int leapSecondsGNSS;

    /** Future or past leap seconds ΔtLSF (BNK).
     * i.e. future leap second if the week and day number are in the future.
     * @since 14.0
     */
    private int leapSecondsFuture;

    /** Respective leap second week number.
     * For GPS, GAL, QZS and IRN, weeks since 6-Jan-1980.
     * When BDS only file leap seconds specified, weeks since 1-Jan-2006
     * @since 14.0
     */
    private int leapSecondsWeekNum;

    /** Respective leap second day number.
     * @since 14.0
     */
    private int leapSecondsDayNum;

    /** Digital Object Identifier.
     * @since 12.0
     */
    private String doi;

    /** License of use.
     * @since 12.0
     */
    private String license;

    /** Station information.
     * @since 12.0
     */
    private String stationInformation;

    /** Simple constructor.
     * @param fileType file type
     */
    protected RinexBaseHeader(final RinexFileType fileType) {

        this.fileType      = fileType;
        this.formatVersion = Double.NaN;

        // set default creation date to now
        final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
        setCreationDateComponents(new DateTimeComponents(new DateComponents(now.getYear(),
                                                                            now.getMonthValue(),
                                                                            now.getDayOfMonth()),
                                                         new TimeComponents(now.getHour(),
                                                                            now.getMinute(),
                                                                            now.getSecond())));

        // set default program name to Orekit
        setProgramName(OREKIT);

        // set default run-by name to user
        setRunByName(System.getProperty(USER_NAME));

    }

    /**
     * Get the file type.
     * @return file type
     */
    public RinexFileType getFileType() {
        return fileType;
    }

    /**
     * Getter for the format version.
     * @return the format version
     */
    public double getFormatVersion() {
        return formatVersion;
    }

    /**
     * Setter for the format version.
     * @param formatVersion the format version to set
     */
    public void setFormatVersion(final double formatVersion) {
        this.formatVersion = formatVersion;
    }

    /**
     * Getter for the satellite system.
     * <p>
     * Not specified for RINEX 2.X versions (value is null).
     * </p>
     * @return the satellite system
     */
    public SatelliteSystem getSatelliteSystem() {
        return satelliteSystem;
    }

    /**
     * Setter for the satellite system.
     * @param satelliteSystem the satellite system to set
     */
    public void setSatelliteSystem(final SatelliteSystem satelliteSystem) {
        this.satelliteSystem = satelliteSystem;
    }

    /**
     * Parse satellite system.
     * @param line header line
     * @param defaultSatelliteSystem satellite system to use if string is null or empty
     * @return parsed satellite system
     * @since 14.0
     */
    public abstract SatelliteSystem parseSatelliteSystem(String line, SatelliteSystem defaultSatelliteSystem);

    /**
     * Getter for the program name.
     * @return the program name
     */
    public String getProgramName() {
        return programName;
    }

    /**
     * Setter for the program name.
     * @param programName the program name to set
     */
    public void setProgramName(final String programName) {
        this.programName = programName;
    }

    /**
     * Getter for the run/by name.
     * @return the run/by name
     */
    public String getRunByName() {
        return runByName;
    }

    /**
     * Setter for the run/by name.
     * @param runByName the run/by name to set
     */
    public void setRunByName(final String runByName) {
        this.runByName = runByName;
    }

    /**
     * Getter for the creation date of the file as a string.
     * @return the creation date
     */
    public DateTimeComponents getCreationDateComponents() {
        return creationDateComponents;
    }

    /**
     * Setter for the creation date as a string.
     * @param creationDateComponents the creation date to set
     */
    public void setCreationDateComponents(final DateTimeComponents creationDateComponents) {
        this.creationDateComponents = creationDateComponents;
    }

    /**
     * Getter for the creation time zone of the file as a string.
     * @return the creation time zone as a string
     */
    public String getCreationTimeZone() {
        return creationTimeZone;
    }

    /**
     * Setter for the creation time zone.
     * @param creationTimeZone the creation time zone to set
     */
    public void setCreationTimeZone(final String creationTimeZone) {
        this.creationTimeZone = creationTimeZone;
    }

    /**
     * Getter for the creation date.
     * <p>
     * The creation date seems to be mandatory, but we have seen several files
     * missing it, even files created by IGS itself (in clock files, essentially).
     * We accept these null dates to at least allow parsing the files
     * as this header information does not really seem essential
     * </p>
     * @return the creation date
     */
    public AbsoluteDate getCreationDate() {
        return creationDate;
    }

    /**
     * Setter for the creation date.
     * @param creationDate the creation date to set
     */
    public void setCreationDate(final AbsoluteDate creationDate) {
        this.creationDate = creationDate;
    }

    /** Set the number of the receiver.
     * @param receiverNumber number of the receiver
     */
    public void setReceiverNumber(final String receiverNumber) {
        this.receiverNumber = receiverNumber;
    }

    /** Get the number of the receiver.
     * @return number of the receiver
     */
    public String getReceiverNumber() {
        return receiverNumber;
    }

    /** Set the type of the receiver.
     * @param receiverType type of the receiver
     */
    public void setReceiverType(final String receiverType) {
        this.receiverType = receiverType;
    }

    /** Get the type of the receiver.
     * @return type of the receiver
     */
    public String getReceiverType() {
        return receiverType;
    }

    /** Set the version of the receiver.
     * @param receiverVersion version of the receiver
     */
    public void setReceiverVersion(final String receiverVersion) {
        this.receiverVersion = receiverVersion;
    }

    /** Get the version of the receiver.
     * @return version of the receiver
     */
    public String getReceiverVersion() {
        return receiverVersion;
    }

    /** Getter for the number of leap second for GNSS time scales.
     * @return the number of leap seconds for GNSS time scales
     * @since 14.0
     */
    public int getLeapSecondsGNSS() {
        return leapSecondsGNSS;
    }

    /** Setter for the number of leap seconds for GNSS time scales.
     * @param leapSecondsGNSS the number of leap seconds for GNSS time scales to set
     * @since 14.0
     */
    public void setLeapSecondsGNSS(final int leapSecondsGNSS) {
        this.leapSecondsGNSS = leapSecondsGNSS;
    }

    /** Set the future or past leap seconds.
     * @param leapSecondsFuture Future or past leap seconds
     * @since 14.0
     */
    public void setLeapSecondsFuture(final int leapSecondsFuture) {
        this.leapSecondsFuture = leapSecondsFuture;
    }

    /** Get the future or past leap seconds.
     * @return Future or past leap seconds
     * @since 14.0
     */
    public int getLeapSecondsFuture() {
        return leapSecondsFuture;
    }

    /** Set the respective leap second week number.
     * @param leapSecondsWeekNum Respective leap second week number
     * @since 14.0
     */
    public void setLeapSecondsWeekNum(final int leapSecondsWeekNum) {
        this.leapSecondsWeekNum = leapSecondsWeekNum;
    }

    /** Get the respective leap second week number.
     * @return Respective leap second week number
     * @since 14.0
     */
    public int getLeapSecondsWeekNum() {
        return leapSecondsWeekNum;
    }

    /** Set the respective leap second day number.
     * @param leapSecondsDayNum Respective leap second day number
     * @since 14.0
     */
    public void setLeapSecondsDayNum(final int leapSecondsDayNum) {
        this.leapSecondsDayNum = leapSecondsDayNum;
    }

    /** Get the respective leap second day number.
     * @return Respective leap second day number
     * @since 14.0
     */
    public int getLeapSecondsDayNum() {
        return leapSecondsDayNum;
    }

    /**
     *  Getter for the Digital Object Information.
     * @return the Digital Object Information
     * @since 12.0
     */
    public String getDoi() {
        return doi;
    }

    /**
     * Setter for the Digital Object Information.
     * @param doi the Digital Object Information to set
     * @since 12.0
     */
    public void setDoi(final String doi) {
        this.doi = doi;
    }

    /**
     *  Getter for the license of use.
     * @return the license of use
     * @since 12.0
     */
    public String getLicense() {
        return license;
    }

    /**
     * Setter for the license of use.
     * @param license the license of use
     * @since 12.0
     */
    public void setLicense(final String license) {
        this.license = license;
    }

    /**
     *  Getter for the station information.
     * @return the station information
     * @since 12.0
     */
    public String getStationInformation() {
        return stationInformation;
    }

    /**
     * Setter for the station information.
     * @param stationInformation the station information to set
     * @since 12.0
     */
    public void setStationInformation(final String stationInformation) {
        this.stationInformation = stationInformation;
    }

    /** Parse version, file type and satellite system.
     * @param line line to parse
     * @param defaultSatelliteSystem satellite system to use if string is null or empty
     * @param name file name (for error message generation)
     * @param supportedVersions supported versions
     * @since 14.0
     */
    public void parseVersionFileTypeSatelliteSystem(final String line, final SatelliteSystem defaultSatelliteSystem,
                                                    final String name, final double... supportedVersions) {

        // Rinex version
        final double parsedVersion = ParsingUtils.parseDouble(line, 0, 9);

        boolean found = false;
        for (final double supported : supportedVersions) {
            if (FastMath.abs(parsedVersion - supported) < 1.0e-4) {
                found = true;
                break;
            }
        }
        if (!found) {
            final StringBuilder builder = new StringBuilder();
            for (final double supported : supportedVersions) {
                if (builder.length() > 0) {
                    builder.append(", ");
                }
                builder.append(supported);
            }
            throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT_VERSION,
                                      parsedVersion, name, builder.toString());
        }
        setFormatVersion(parsedVersion);

        // file type
        checkType(line, name);

        // Satellite system
        setSatelliteSystem(parseSatelliteSystem(line, defaultSatelliteSystem));

    }

    /** Parse program, run/by and date.
     * @param line line to parse
     * @param timeScales the set of time scales used for parsing dates
     * @since 14.0
     */
    public abstract void parseProgramRunByDate(String line, TimeScales timeScales);

    /** Parse program, run/by and date.
     * @param prgm  PGM field
     * @param run  RUN BY field
     * @param date  date field
     * @param timeScales the set of time scales used for parsing dates
     * @since 14.0
     */
    protected void parseProgramRunByDate(final String prgm, final String run, final String date,
                                         final TimeScales timeScales) {

        // Name of the generating program
        setProgramName(prgm);

        // Name of the run/by name
        setRunByName(run);

        // there are several variations for date formatting in the PGM / RUN BY / DATE line

        // in versions 2.x, the pattern is expected to be:
        // XXRINEXO V9.9       AIUB                24-MAR-01 14:43     PGM / RUN BY / DATE
        // however, we have also found this:
        // teqc  2016Nov7      root                20180130 10:38:06UTCPGM / RUN BY / DATE
        // BJFMTLcsr           UTCSR               2007-09-30 05:30:06 PGM / RUN BY / DATE
        // NEODIS              TAS                 27/05/22 10:28      PGM / RUN BY / DATE

        // in versions 3.x, the pattern is expected to be:
        // sbf2rin-11.3.3                          20180130 002558 LCL PGM / RUN BY / DATE
        // however, we have also found:
        // NetR9 5.03          Receiver Operator   11-JAN-16 00:00:00  PGM / RUN BY / DATE

        // in clock files, we have found patterns like:
        // CLKRINEX V1.0       NRCan               1-Mar-2000 20:36    PGM / RUN BY / DATE
        // tdp2clk v1.13       JPL                 2002 Jan 3 13:36:17 PGM / RUN BY / DATE

        // so we cannot rely on the format version, we have to check several variations
        final Matcher splittingMatcher = SPLITTING_PATTERN.matcher(date);
        if (splittingMatcher.matches()) {

            // date part
            final DateComponents dc;
            final Matcher abbrev1Matcher = DATE_DD_MMM_YY_PATTERN.matcher(splittingMatcher.group(1));
            if (abbrev1Matcher.matches()) {
                final int rawYear = Integer.parseInt(abbrev1Matcher.group(3));
                // hoping this obsolete format will not be used past year 2079…
                dc = new DateComponents(rawYear < 100 ? ParsingUtils.convert2DigitsYear(rawYear) : rawYear,
                                        Month.parseMonth(abbrev1Matcher.group(2)).getNumber(),
                                        Integer.parseInt(abbrev1Matcher.group(1)));
            } else {
                final Matcher abbrev2Matcher = DATE_YYYY_MMM_DD_PATTERN.matcher(splittingMatcher.group(1));
                if (abbrev2Matcher.matches()) {
                    dc = new DateComponents(Integer.parseInt(abbrev2Matcher.group(1)),
                                            Month.parseMonth(abbrev2Matcher.group(2)).getNumber(),
                                            Integer.parseInt(abbrev2Matcher.group(3)));
                } else {
                    final Matcher isoMatcher = DATE_ISO_8601_PATTERN.matcher(splittingMatcher.group(1));
                    if (isoMatcher.matches()) {
                        dc = new DateComponents(Integer.parseInt(isoMatcher.group(1)),
                                               Integer.parseInt(isoMatcher.group(2)),
                                               Integer.parseInt(isoMatcher.group(3)));
                    } else {
                        final Matcher europeanMatcher = DATE_EUROPEAN_PATTERN.matcher(splittingMatcher.group(1));
                        if (europeanMatcher.matches()) {
                            dc = new DateComponents(
                                ParsingUtils.convert2DigitsYear(Integer.parseInt(europeanMatcher.group(3))),
                                Integer.parseInt(europeanMatcher.group(2)),
                                Integer.parseInt(europeanMatcher.group(1)));
                        } else {
                            dc = null;
                        }
                    }
                }
            }

            // time part
            final TimeComponents tc;
            final Matcher timeMatcher = TIME_PATTERN.matcher(splittingMatcher.group(2));
            if (timeMatcher.matches()) {
                tc = new TimeComponents(Integer.parseInt(timeMatcher.group(1)),
                                        Integer.parseInt(timeMatcher.group(2)),
                                        timeMatcher.group(3) != null ? Integer.parseInt(timeMatcher.group(3)) : 0);
            } else {
                tc = TimeComponents.H00;
            }

            // zone part
            final String zone = splittingMatcher.group(3);
            setCreationTimeZone(zone == null ? "" : zone);

            if (dc == null) {
                // despite the creation date seems to be mandatory, we have seen several files
                // missing it, even files created by IGS itself (in clock files, essentially).
                // We accept these null dates to at least allow parsing the files
                // as this header information does not really seem essential
                setCreationDate(null);
            } else {
                // we successfully parsed everything
                final DateTimeComponents dtc = new DateTimeComponents(dc, tc);
                setCreationDateComponents(dtc);
                final TimeScale timeScale = zone == null ?
                                            timeScales.getUTC() :
                                            PredefinedTimeSystem.parseTimeSystem(zone).getTimeScale(timeScales);
                setCreationDate(new AbsoluteDate(dtc, timeScale));
            }

        } else {
            setCreationDate(null);
            setCreationTimeZone("");
        }

    }

    /** Check file type.
     * @param line header line
     * @param name file name (for error message)
     * @since 14.0
     */
    public abstract void checkType(String line, String name);

    /** Check file type.
     * @param line header line
     * @param typeIndex index of the file type in the line
     * @param name file name (for error message)
     * @since 14.0
     */
    protected void checkType(final String line, final int typeIndex, final String name) {
        if (fileType != RinexFileType.parseRinexFileType(line.substring(typeIndex, typeIndex + 1))) {
            throw new OrekitException(OrekitMessages.WRONG_PARSING_TYPE, name);
        }
    }

    /** Get the index of the header label.
     * @return index of the header label
     * @since 14.0
     */
    public abstract int getLabelIndex();

    /** Check if a label is found in a line.
     * @param label label to check
     * @param line header line
     * @return true if label is found in the header line
     * @since 14.0
     */
    public abstract boolean matchFound(Label label, String line);

}