SP3Header.java

/* Copyright 2023 Luc Maisonobe
 * 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.sp3;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.gnss.TimeSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.utils.CartesianDerivativesFilter;

/** Header for SP3 files.
 * @author Luc Maisonobe
 * @since 12.0
 */
public class SP3Header {

    /** String representation of the center of ephemeris coordinate system. **/
    public static final String SP3_FRAME_CENTER_STRING = "EARTH";

    /** Name for pos/vel accuracy base header entry. */
    private static final String POS_VEL_ACCURACY_BASE = "pos/vel accuracy base";

    /** Name for clock accuracy base header entry. */
    private static final String CLOCK_ACCURACY_BASE = "clock accuracy base";

    /** Name for comments header entry. */
    private static final String COMMENTS = "comments";

    /** File version. */
    private char version;

    /** File type. */
    private SP3FileType type;

    /** Time system. */
    private TimeSystem timeSystem;

    /** Epoch of the file. */
    private AbsoluteDate epoch;

    /** GPS week. */
    private int gpsWeek;

    /** Seconds of the current GPS week. */
    private double secondsOfWeek;

    /** Julian day. */
    private int modifiedJulianDay;

    /** Day fraction. */
    private double dayFraction;

    /** Time-interval between epochs. */
    private double epochInterval;

    /** Number of epochs. */
    private int numberOfEpochs;

    /** Coordinate system. */
    private String coordinateSystem;

    /** Data used indicator. */
    private List<DataUsed> dataUsed;

    /** Orbit type. */
    private SP3OrbitType orbitType;

    /** Key for orbit type. */
    private String orbitTypeKey;

    /** Agency providing the file. */
    private String agency;

    /** Indicates if data contains velocity or not. */
    private CartesianDerivativesFilter filter;

    /** Base for position/velocity accuracy. */
    private double posVelBase;

    /** Base for clock/clock-rate accuracy. */
    private double clockBase;

    /** Satellite identifiers. */
    private List<String> satIds;

    /** Satellite accuracies. */
    private double[] accuracies;

    /** Comments. */
    private final List<String> comments;

    /** Create a new SP3 header.
     */
    public SP3Header() {
        this.version    = '?';
        this.satIds     = new ArrayList<>();
        this.accuracies = null;
        this.comments   = new ArrayList<>();
    }

    /** Check header is valid.
     * @param parsing if true, we are parsing an existing file, and are more lenient
     * in order to accept some common errors (like between 86 and 99 satellites
     * in SP3a, SP3b or SP3c files)
     * @param hasAccuracy if true, there are accuracy data in the file
     * @param fileName file name to generate the error message
     * @exception OrekitException if file is not valid
     */
    void validate(final boolean parsing, final boolean hasAccuracy, final String fileName) throws OrekitException {

        // check version
        if ("abcd".indexOf(getVersion()) < 0) {
            throw new OrekitException(OrekitMessages.SP3_UNSUPPORTED_VERSION, getVersion());
        }

        if (getVersion() == 'a') {
            // in SP3 version a, the base accuracy must be set to 0
            if (getPosVelBase() != 0.0) {
                throw new OrekitException(OrekitMessages.SP3_INVALID_HEADER_ENTRY,
                                          POS_VEL_ACCURACY_BASE, getPosVelBase(), fileName, getVersion());
            }
            if (getClockBase() != 0.0) {
                throw new OrekitException(OrekitMessages.SP3_INVALID_HEADER_ENTRY,
                                          CLOCK_ACCURACY_BASE, getClockBase(), fileName, getVersion());
            }
        } else if (hasAccuracy) {
            // in SP3 versions after version a, the base accuracy must be set if entries specify accuracy
            if (getPosVelBase() <= 0.0) {
                throw new OrekitException(OrekitMessages.SP3_INVALID_HEADER_ENTRY,
                                          POS_VEL_ACCURACY_BASE, getPosVelBase(), fileName, getVersion());
            }
            if (getClockBase() <= 0.0) {
                throw new OrekitException(OrekitMessages.SP3_INVALID_HEADER_ENTRY,
                                          CLOCK_ACCURACY_BASE, getClockBase(), fileName, getVersion());
            }
        }
        if (getVersion() < 'd') {
            // in SP3 versions a, b, and c, there are exactly 4 comments with max length 57
            // (60 minus first three characters)
            if (comments.size() != 4 ||
                comments.get(0).length() > 57 ||
                comments.get(1).length() > 57 ||
                comments.get(2).length() > 57 ||
                comments.get(3).length() > 57) {
                throw new OrekitException(OrekitMessages.SP3_INVALID_HEADER_ENTRY,
                                          COMMENTS, "/* …", fileName, getVersion());
            }
        } else {
            // starting with SP3 version d, there is an unspecified number of comments with max length 77
            // (80 minus first three characters)
            for (final String c : comments) {
                if (c.length() > 77) {
                    throw new OrekitException(OrekitMessages.SP3_INVALID_HEADER_ENTRY,
                                              COMMENTS, c, fileName, getVersion());
                }
            }
        }

    }

    /** Set the file version.
     * @param version file version
     */
    public void setVersion(final char version) {
        this.version = version;
    }

    /** Get the file version.
     * @return file version
     */
    public char getVersion() {
        return version;
    }

    /** Set the derivatives filter.
     * @param filter that indicates which derivatives of position are available.
     */
    public void setFilter(final CartesianDerivativesFilter filter) {
        this.filter = filter;
    }

    /** Get the derivatives filter.
     * @return filter with available derivatives
     */
    public CartesianDerivativesFilter getFilter() {
        return filter;
    }

    /** Returns the {@link SP3FileType} associated with this SP3 file.
     * @return the file type for this SP3 file
     */
    public SP3FileType getType() {
        return type;
    }

    /** Set the file type for this SP3 file.
     * @param fileType the file type to be set
     */
    public void setType(final SP3FileType fileType) {
        this.type = fileType;
    }

    /** Returns the {@link TimeSystem} used to time-stamp position entries.
     * @return the {@link TimeSystem} of the orbit file
     */
    public TimeSystem getTimeSystem() {
        return timeSystem;
    }

    /** Set the time system used in this SP3 file.
     * @param system the time system to be set
     */
    public void setTimeSystem(final TimeSystem system) {
        this.timeSystem = system;
    }

    /** Returns the data used indicator from the SP3 file.
     * @return the data used indicator
     */
    public List<DataUsed> getDataUsed() {
        return dataUsed;
    }

    /** Set the data used indicator for this SP3 file.
     * @param dataUsed the data used indicator to be set
     */
    public void setDataUsed(final List<DataUsed> dataUsed) {
        this.dataUsed = dataUsed;
    }

    /** Returns the start epoch of the orbit file.
     * @return the start epoch
     */
    public AbsoluteDate getEpoch() {
        return epoch;
    }

    /** Set the epoch of the SP3 file.
     * @param time the epoch to be set
     */
    public void setEpoch(final AbsoluteDate time) {
        this.epoch = time;
    }

    /** Returns the GPS week as contained in the SP3 file.
     * @return the GPS week of the SP3 file
     */
    public int getGpsWeek() {
        return gpsWeek;
    }

    /** Set the GPS week of the SP3 file.
     * @param week the GPS week to be set
     */
    public void setGpsWeek(final int week) {
        this.gpsWeek = week;
    }

    /** Returns the seconds of the GPS week as contained in the SP3 file.
     * @return the seconds of the GPS week
     */
    public double getSecondsOfWeek() {
        return secondsOfWeek;
    }

    /** Set the seconds of the GPS week for this SP3 file.
     * @param seconds the seconds to be set
     */
    public void setSecondsOfWeek(final double seconds) {
        this.secondsOfWeek = seconds;
    }

    /** Returns the modified julian day for this SP3 file.
     * @return the modified julian day
     */
    public int getModifiedJulianDay() {
        return modifiedJulianDay;
    }

    /** Set the modified julian day for this SP3 file.
     * @param day the modified julian day to be set
     */
    public void setModifiedJulianDay(final int day) {
        this.modifiedJulianDay = day;
    }

    /** Returns the day fraction for this SP3 file.
     * @return the day fraction
     */
    public double getDayFraction() {
        return dayFraction;
    }

    /** Set the day fraction for this SP3 file.
     * @param fraction the day fraction to be set
     */
    public void setDayFraction(final double fraction) {
        this.dayFraction = fraction;
    }

    /** Returns the time interval between epochs (in seconds).
     * @return the time interval between epochs
     */
    public double getEpochInterval() {
        return epochInterval;
    }

    /** Set the epoch interval for this SP3 file.
     * @param interval the interval between orbit entries
     */
    public void setEpochInterval(final double interval) {
        this.epochInterval = interval;
    }

    /** Returns the number of epochs contained in this orbit file.
     * @return the number of epochs
     */
    public int getNumberOfEpochs() {
        return numberOfEpochs;
    }

    /** Set the number of epochs as contained in the SP3 file.
     * @param epochCount the number of epochs to be set
     */
    public void setNumberOfEpochs(final int epochCount) {
        this.numberOfEpochs = epochCount;
    }

    /** Returns the coordinate system of the entries in this orbit file.
     * @return the coordinate system
     */
    public String getCoordinateSystem() {
        return coordinateSystem;
    }

    /** Set the coordinate system used for the orbit entries.
     * @param system the coordinate system to be set
     */
    public void setCoordinateSystem(final String system) {
        this.coordinateSystem = system;
    }

    /** Returns the {@link SP3OrbitType} for this SP3 file.
     * @return the orbit type
     */
    public SP3OrbitType getOrbitType() {
        return orbitType;
    }

    /** Returns the orbit type key for this SP3 file.
     * @return the orbit type key
     */
    public String getOrbitTypeKey() {
        return orbitTypeKey;
    }

    /** Set the orbit type key for this SP3 file.
     * @param oTypeKey the orbit type key to be set
     */
    public void setOrbitTypeKey(final String oTypeKey) {
        this.orbitTypeKey = oTypeKey;
        this.orbitType    = SP3OrbitType.parseType(oTypeKey);
    }

    /** Returns the agency that prepared this SP3 file.
     * @return the agency
     */
    public String getAgency() {
        return agency;
    }

    /** Set the agency string for this SP3 file.
     * @param agencyStr the agency string to be set
     */
    public void setAgency(final String agencyStr) {
        this.agency = agencyStr;
    }

    /** Set the base for position/velocity accuracy.
     * @param posVelBase base for position/velocity accuracy
     */
    public void setPosVelBase(final double posVelBase) {
        this.posVelBase = posVelBase;
    }

    /** Get the base for position/velocity accuracy.
     * @return base for position/velocity accuracy
     */
    public double getPosVelBase() {
        return posVelBase;
    }

    /** Set the base for clock/clock-rate accuracy.
     * @param clockBase base for clock/clock-rate accuracy
     */
    public void setClockBase(final double clockBase) {
        this.clockBase = clockBase;
    }

    /** Get the base for clock/clock-rate accuracy.
     * @return base for clock/clock-rate accuracy
     */
    public double getClockBase() {
        return clockBase;
    }

    /** Add a satellite identifier.
     * @param satId satellite identifier
     */
    public void addSatId(final String satId) {
        satIds.add(satId);
    }

    /** Get the satellite identifiers.
     * @return satellites identifiers
     */
    public List<String> getSatIds() {
        return Collections.unmodifiableList(satIds);
    }

    /** Set the accuracy.
     * @param index satellite index in {@link #getSatIds()}
     * @param accuracy in m
     */
    public void setAccuracy(final int index, final double accuracy) {
        if (accuracies == null) {
            // lazy allocation of the array
            accuracies = new double[satIds.size()];
        }
        accuracies[index] = accuracy;
    }

    /** Get the formal accuracy.
     * <p>
     * The accuracy is limited by the SP3 standard to be a power of 2 in mm.
     * The value returned here is in meters.
     * </p>
     * @param satId satellite identifier
     * @return magnitude of one standard deviation, in m.
     */
    public double getAccuracy(final String satId) {
        for (int i = 0; i < satIds.size(); ++i) {
            if (satIds.get(i).equals(satId)) {
                return accuracies[i];
            }
        }
        return Double.NaN;
    }

    /** Get the comments.
     * @return an unmodifiable view of comments
     */
    public List<String> getComments() {
        return Collections.unmodifiableList(comments);
    }

    /** Add a comment.
     * @param comment comment to add
     */
    public void addComment(final String comment) {
        comments.add(comment);
    }

}