OrbexParseInfo.java

/* Copyright 2022-2026 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.sinex.orbex;

import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.sinex.ParseInfo;
import org.orekit.frames.Frame;
import org.orekit.gnss.SatInSystem;
import org.orekit.gnss.TimeSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeScales;
import org.orekit.time.clocks.ClockOffset;
import org.orekit.utils.AngularCoordinates;
import org.orekit.utils.TimeStampedAngularCoordinates;
import org.orekit.utils.TimeStampedPVCoordinates;
import org.orekit.utils.units.Unit;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/** Parse information for Orbit Exchange Format (ORBEX) files.
 * @author Luc Maisonobe
 * @since 14.0
 */
public class OrbexParseInfo extends ParseInfo<Orbex> {

    /** Mapping from frame identifier in the file to a {@link Frame}. */
    private final Function<? super String, ? extends Frame> frameBuilder;

    /** Mapper from string to time system. */
    private final Function<? super String, ? extends TimeSystem> timeSystemBuilder;

    /** Completed ephemeris data. */
    private final Map<SatInSystem, Data> ephemerisData;

    /** Satellite parsed. */
    private final HashMap<SatInSystem, SatData> parsedSatellites;

    /** Description of the file content. */
    private String description;

    /** Name of agency which created the file. */
    private String createdBy;

    /** Input used to generate this file. */
    private String inputData;

    /** E-mail address of the relevant contact person. */
    private String contact;

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

    /** Number of seconds between each epoch (NaN if irregular). */
    private double epochInterval;

    /** Reference frame. */
    private Frame coordinateSystem;

    /** Frame type. */
    private String frameType;

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

    /** Record types. */
    private List<EphemerisDataPredicate> recordTypes;

    /** Orbit reference. */
    private String orbitReference;

    /** Orbit position unit. */
    private Unit positionUnit;

    /** Orbit velocity unit. */
    private Unit velocityUnit;

    /** Clock correction unit. */
    private Unit clockCorrectionUnit;

    /** Clock rate unit. */
    private Unit clockRateUnit;

    /** Current date. */
    private AbsoluteDate date;

    /** Expected number of satellites for this time tag. */
    private int expectedSatellites;

    /** Simple constructor.
     * @param frameBuilder      is a function that can construct a frame from an orbex file
     *                          coordinate system string. The coordinate system can be
     *                          any 5 characters string e.g., ITR92, IGb08.
     * @param timeSystemBuilder mapper from string to time system (useful for user-defined time systems)
     * @param timeScales        the set of time scales used for parsing dates
     */
    OrbexParseInfo(final Function<? super String, ? extends Frame> frameBuilder,
                   final Function<? super String, ? extends TimeSystem> timeSystemBuilder,
                   final TimeScales timeScales) {
        super(timeScales);
        this.frameBuilder      = frameBuilder;
        this.timeSystemBuilder = timeSystemBuilder;
        this.ephemerisData     = new HashMap<>();
        this.parsedSatellites  = new HashMap<>();
    }

    /** {@inheritDoc} */
    @Override
    protected Orbex build() {

        // close last parsed group
        timeTag(DateTimeComponents.JULIAN_EPOCH, 0);

        return new Orbex(getVersion(), getTimeScales(), getCreationDate(),
                         getStartDate(), getEndDate(),
                         new Description(description, createdBy, inputData, contact, timeSystem,
                                         epochInterval, coordinateSystem, frameType,
                                         orbitType, recordTypes, orbitReference,
                                         positionUnit, velocityUnit, clockCorrectionUnit, clockRateUnit),
                         ephemerisData);

    }

    /** Set the description of the file content.
     * @param description description of the file content
     */
    void setDescription(final String description) {
        this.description = description;
    }

    /** Set the name of agency which created the file.
     * @param createdBy name of agency which created the file*/
    void setCreatedBy(final String createdBy) {
        this.createdBy = createdBy;
    }

    /** Set the input used to generate this file.
     * @param inputData input used to generate this file
     */
    void setInputData(final String inputData) {
        this.inputData = inputData;
    }

    /** Set the E-mail address of the relevant contact person.
     * @param contact E-mail address of the relevant contact person
     */
    void setContact(final String contact) {
        this.contact = contact;
    }

    /** Set the time system.
     * @param timeSystem time system
     */
    void setTimeSystem(final String timeSystem) {
        this.timeSystem = timeSystemBuilder.apply(timeSystem);
        setTimeScale(this.timeSystem.getTimeScale(getTimeScales()));
    }

    /** Set the number of seconds between each epoch.
     * @param epochInterval number of seconds between each epoch (NaN if irregular)
     */
    void setEpochInterval(final double epochInterval) {
        this.epochInterval = epochInterval;
    }

    /** Set the name of reference frame.
     * @param coordinateSystem name of reference frame
     */
    void setCoordinateSystem(final String coordinateSystem) {
        this.coordinateSystem = frameBuilder.apply(coordinateSystem);
    }

    /** Set the frame type.
     * @param frameType frame type
     */
    void setFrameType(final String frameType) {
        this.frameType = frameType;
    }

    /** Set the orbit type.
     * @param orbitType orbit type
     */
    void setOrbitType(final String orbitType) {
        this.orbitType = orbitType;
    }

    /** Set the record types.
     * @param recordTypes recordTypes
     */
    void setRecordTypes(final List<EphemerisDataPredicate> recordTypes) {
        this.recordTypes = recordTypes;
    }

    /** Set the orbit reference.
     * @param orbitReference orbit reference
     */
    void setOrbitReference(final String orbitReference) {
        this.orbitReference = orbitReference;
    }

    /** Set the unit for position.
     * @param positionUnit unit for position
     */
    void setPositionUnit(final Unit positionUnit) {
        this.positionUnit = positionUnit;
    }

    /** Set the unit for velocity.
     * @param velocityUnit unit for velocity
     */
    void setVelocityUnit(final Unit velocityUnit) {
        this.velocityUnit = velocityUnit;
    }

    /** Set the unit for clock correction.
     * @param clockCorrectionUnit unit for clock correction
     */
    void setClockCorrectionUnit(final Unit clockCorrectionUnit) {
        this.clockCorrectionUnit = clockCorrectionUnit;
    }

    /** Set the unit for clock rate.
     * @param clockRateUnit unit for clock rate
     */
    void setClockRateUnit(final Unit clockRateUnit) {
        this.clockRateUnit = clockRateUnit;
    }

    /** Add a satellite id and description.
     * @param satId       satellite id
     * @param satDescription satellite description
     */
    void addSatIdAndDescription(final SatInSystem satId, final String satDescription) {
        if (ephemerisData.containsKey(satId)) {
            throw new OrekitException(OrekitMessages.DUPLICATED_SATELLITE,
                                      satId, getLineNumber(), getName());
        } else {
            ephemerisData.put(satId, new Data(satId, satDescription));
        }
    }

    /** Close a time tag.
     */
    private void closeTimeTag() {

        // check previous time tag was properly completed
        if (parsedSatellites.size() != expectedSatellites) {
            throw new OrekitException(OrekitMessages.INCOMPLETE_ORBEX_DATA,
                                      expectedSatellites, date.toString(getTimeScale()), parsedSatellites.size(),
                                      getLineNumber(), getName());
        }

        // store coordinates for current time tag
        for (final Map.Entry<SatInSystem, SatData> entry : parsedSatellites.entrySet()) {

            final Data orbexData = ephemerisData.get(entry.getKey());

            // check the satellite was properly declared in the SATELLITE/ID_AND_DESCRIPTION block
            if (orbexData == null) {
                throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, entry.getKey());
            }

            final SatData satData = entry.getValue();

            // orbit
            if (satData.position != null) {
                final TimeStampedPVCoordinates pv =
                    new TimeStampedPVCoordinates(date,
                                                 satData.position,
                                                 satData.velocity == null ? Vector3D.ZERO : satData.velocity);
                orbexData.orbit().add(pv);
            }

            // clock
            if (satData.clockCorrection != null) {
                final ClockOffset co =
                    new ClockOffset(date,
                                    satData.clockCorrection,
                                    satData.clockRate == null ? 0.0 : satData.clockRate,
                                    0.0);
                orbexData.clock().add(co);
            }

            // attitude
            if (satData.attitude != null) {
                final TimeStampedAngularCoordinates ac =
                    new TimeStampedAngularCoordinates(date,
                                                      new AngularCoordinates(satData.attitude, Vector3D.ZERO));
                orbexData.attitude().add(ac);
            }

        }

        parsedSatellites.clear();
        expectedSatellites = 0;

    }

    /** Start a new time tag.
     * @param timeTag time tag
     * @param nbSats expected number of satellites
     */
    void timeTag(final DateTimeComponents timeTag, final int nbSats) {

        // close previous time tag
        closeTimeTag();

        // start new time tag
        this.expectedSatellites = nbSats;
        this.date         = new AbsoluteDate(timeTag, getTimeScale());

    }

    /** Add a position.
     * @param satId satellite id
     * @param parsedPosition parsed position (without unit conversion)
     * @param recordType type of record
     */
    void addPosition(final SatInSystem satId, final Vector3D parsedPosition,
                     final String recordType) {
        checkUnit(positionUnit, recordType);
        parsedSatellites.computeIfAbsent(satId, k -> new SatData()).position =
            new Vector3D(positionUnit.toSI(parsedPosition.getX()),
                         positionUnit.toSI(parsedPosition.getY()),
                         positionUnit.toSI(parsedPosition.getZ()));
    }

    /** Add a velocity.
     * @param satId satellite id
     * @param parsedVelocity parsed velocity (without unit conversion)
     * @param recordType type of record
     */
    void addVelocity(final SatInSystem satId, final Vector3D parsedVelocity,
                     final String recordType) {
        checkUnit(velocityUnit, recordType);
        parsedSatellites.computeIfAbsent(satId, k -> new SatData()).velocity =
            new Vector3D(velocityUnit.toSI(parsedVelocity.getX()),
                         velocityUnit.toSI(parsedVelocity.getY()),
                         velocityUnit.toSI(parsedVelocity.getZ()));
    }

    /** Add a clock correction.
     * @param satId satellite id
     * @param parsedClockCorrection parsed clock correction (without unit conversion)
     * @param recordType type of record
     */
    void addClockCorrection(final SatInSystem satId, final double parsedClockCorrection,
                     final String recordType) {
        checkUnit(clockCorrectionUnit, recordType);
        parsedSatellites.computeIfAbsent(satId, k -> new SatData()).clockCorrection =
            clockCorrectionUnit.toSI(parsedClockCorrection);
    }

    /** Add a clock rate.
     * @param satId satellite id
     * @param parsedClockRate parsed clock rate (without unit conversion)
     * @param recordType type of record
     */
    void addClockRate(final SatInSystem satId, final double parsedClockRate,
                     final String recordType) {
        checkUnit(clockRateUnit, recordType);
        parsedSatellites.computeIfAbsent(satId, k -> new SatData()).clockRate =
            clockRateUnit.toSI(parsedClockRate);
    }

    /** Add an attitude.
     * @param satId satellite id
     * @param bodyToCoordinateSystem rotation from spacecraft body to coordinate system
     */
    void addAttitude(final SatInSystem satId, final Rotation bodyToCoordinateSystem) {
        // beware attitude are stored using system frame to spacecraft body convention
        // which is the other way round with respect to Orbex format
        parsedSatellites.computeIfAbsent(satId, k -> new SatData()).attitude = bodyToCoordinateSystem.revert();
    }

    /** Check unit has been initialized.
     * @param unit unit to check
     * @param recordType type of record
     */
    private void checkUnit(final Unit unit, final String recordType) {
        if (unit == null) {
            throw new OrekitException(OrekitMessages.MISSING_ORBEX_UNIT,
                                      recordType, getLineNumber(), getName());
        }
    }

    /** Container for one satellite parsed data. */
    private static class SatData {

        /** Position. */
        private Vector3D position;

        /** Velocity. */
        private Vector3D velocity;

        /** Clock correction. */
        private Double clockCorrection;

        /** Clock rate. */
        private Double clockRate;

        /** Attitude. */
        private Rotation attitude;

    }

}