Euler.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.ccsds.ndm.adm.apm;

import java.util.Arrays;

import org.hipparchus.geometry.euclidean.threed.RotationOrder;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.ndm.adm.AttitudeEndpoints;
import org.orekit.files.ccsds.section.CommentsContainer;

/**
 * Container for {@link Euler Euler rotations} entries.
 * @author Bryan Cazabonne
 * @since 10.2
 */
public class Euler extends CommentsContainer {

    /** Key for angles in ADM V1.
     * @since 12.0
     */
    private static final String KEY_ANGLES_V1 = "{X|Y|Z}_ANGLE";

    /** Key for angles in ADM V2.
     * @since 12.0
     */
    private static final String KEY_ANGLES_V2 = "ANGLE_{1|2|3}";

    /** Key for rates in ADM V1.
     * @since 12.0
     */
    private static final String KEY_RATES_V1 = "{X|Y|Z}_RATE";

    /** Key for rates in ADM V2.
     * @since 12.0
     */
    private static final String KEY_RATES_V2 = "ANGLE_{1|2|3}_DOT";

    /** Endpoints (i.e. frames A, B and their relationship). */
    private final AttitudeEndpoints endpoints;

    /** Rotation order of the Euler angles. */
    private RotationOrder eulerRotSeq;

    /** The frame in which rates are specified. */
    private Boolean rateFrameIsA;

    /** Euler angles [rad]. */
    private double[] rotationAngles;

    /** Rotation rate [rad/s]. */
    private double[] rotationRates;

    /** Indicator for rotation angles. */
    private boolean inRotationAngles;

    /** Simple constructor.
     */
    public Euler() {
        this.endpoints        = new AttitudeEndpoints();
        this.rotationAngles   = new double[3];
        this.rotationRates    = new double[3];
        this.inRotationAngles = false;
        Arrays.fill(rotationAngles, Double.NaN);
        Arrays.fill(rotationRates,  Double.NaN);
    }

    /** {@inheritDoc} */
    @Override
    public void validate(final double version) {

        super.validate(version);
        if (version < 2.0) {
            endpoints.checkMandatoryEntriesExceptExternalFrame(version,
                                                               EulerKey.EULER_FRAME_A,
                                                               EulerKey.EULER_FRAME_B,
                                                               EulerKey.EULER_DIR);
            endpoints.checkExternalFrame(EulerKey.EULER_FRAME_A, EulerKey.EULER_FRAME_B);
        } else {
            endpoints.checkMandatoryEntriesExceptExternalFrame(version,
                                                               EulerKey.REF_FRAME_A,
                                                               EulerKey.REF_FRAME_B,
                                                               EulerKey.EULER_DIR);
            endpoints.checkExternalFrame(EulerKey.REF_FRAME_A, EulerKey.REF_FRAME_B);
        }
        checkNotNull(eulerRotSeq, EulerKey.EULER_ROT_SEQ.name());

        if (!hasAngles()) {
            // if at least one angle is missing, all must be NaN (i.e. not initialized)
            for (final double ra : rotationAngles) {
                if (!Double.isNaN(ra)) {
                    throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY,
                                              version < 2.0 ? KEY_ANGLES_V1 : KEY_ANGLES_V2);
                }
            }
        }

        if (!hasRates()) {
            // if at least one rate is missing, all must be NaN (i.e. not initialized)
            for (final double rr : rotationRates) {
                if (!Double.isNaN(rr)) {
                    throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY,
                                              version < 2.0 ? KEY_RATES_V1 : KEY_RATES_V2);
                }
            }
        }

        if (version < 2.0) {
            // in ADM V1, either angles or rates must be specified
            // (angles may be missing in the quaternion/Euler rate case)
            if (!hasAngles() && !hasRates()) {
                throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, KEY_ANGLES_V1 + "/" + KEY_RATES_V1);
            }
        } else {
            // in ADM V2, angles are mandatory
            if (!hasAngles()) {
                throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, KEY_ANGLES_V2);
            }
        }

    }

    /** Get the endpoints (i.e. frames A, B and their relationship).
     * @return endpoints
     */
    public AttitudeEndpoints getEndpoints() {
        return endpoints;
    }

    /**
     * Get the rotation order of Euler angles.
     * @return rotation order
     */
    public RotationOrder getEulerRotSeq() {
        return eulerRotSeq;
    }

    /**
     * Set the rotation order for Euler angles.
     * @param eulerRotSeq order to be set
     */
    public void setEulerRotSeq(final RotationOrder eulerRotSeq) {
        refuseFurtherComments();
        this.eulerRotSeq = eulerRotSeq;
    }

    /** Check if rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}.
     * @return true if rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}
     */
    public boolean rateFrameIsA() {
        return rateFrameIsA == null ? false : rateFrameIsA;
    }

    /** Set the frame in which rates are specified.
     * @param rateFrameIsA if true, rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}
     */
    public void setRateFrameIsA(final boolean rateFrameIsA) {
        refuseFurtherComments();
        this.rateFrameIsA = rateFrameIsA;
    }

    /** Check if rates are specified in spacecraft body frame.
     * <p>
     * {@link #validate(double) Mandatory entries} must have been
     * initialized properly to non-null values before this method is called,
     * otherwise {@code NullPointerException} will be thrown.
     * </p>
     * @return true if rates are specified in spacecraft body frame
     */
    public boolean isSpacecraftBodyRate() {
        return rateFrameIsA() ^ endpoints.getFrameA().asSpacecraftBodyFrame() == null;
    }

    /**
     * Get the coordinates of the Euler angles.
     * @return rotation angles (rad)
     */
    public double[] getRotationAngles() {
        return rotationAngles.clone();
    }

    /**
     * Set the Euler angle about axis.
     * @param axis rotation axis
     * @param angle angle to set (rad)
     */
    public void setLabeledRotationAngle(final char axis, final double angle) {
        if (eulerRotSeq != null) {
            for (int i = 0; i < rotationAngles.length; ++i) {
                if (eulerRotSeq.name().charAt(i) == axis && Double.isNaN(rotationAngles[i])) {
                    setIndexedRotationAngle(i, angle);
                    return;
                }
            }
        }
    }

    /**
     * Set the Euler angle about axis.
     * @param axis rotation axis
     * @param angle angle to set (rad)
     * @since 12.0
     */
    public void setIndexedRotationAngle(final int axis, final double angle) {
        refuseFurtherComments();
        rotationAngles[axis] = angle;
    }

    /**
     * Get the rates of the Euler angles.
     * @return rotation rates (rad/s)
     */
    public double[] getRotationRates() {
        return rotationRates.clone();
    }

    /**
     * Set the rate of Euler angle about axis.
     * @param axis rotation axis
     * @param rate angle rate to set (rad/s)
     */
    public void setLabeledRotationRate(final char axis, final double rate) {
        if (eulerRotSeq != null) {
            for (int i = 0; i < rotationRates.length; ++i) {
                if (eulerRotSeq.name().charAt(i) == axis && Double.isNaN(rotationRates[i])) {
                    setIndexedRotationRate(i, rate);
                    return;
                }
            }
        }
    }

    /**
     * Set the rate of Euler angle about axis.
     * @param axis rotation axis
     * @param rate angle rate to set (rad/s)
     * @since 12.0
     */
    public void setIndexedRotationRate(final int axis, final double rate) {
        refuseFurtherComments();
        rotationRates[axis] = rate;
    }

    /** Check if we are in the rotationAngles part of XML files.
     * @return true if we are in the rotationAngles part of XML files
     */
    boolean inRotationAngles() {
        return inRotationAngles;
    }

    /** Set flag for rotation angle parsing.
     * @param inRotationAngles if true, we are in the rotationAngles part of XML files
     */
    public void setInRotationAngles(final boolean inRotationAngles) {
        refuseFurtherComments();
        this.inRotationAngles = inRotationAngles;
    }

    /** Check if the logical block includes angles.
     * <p>
     * This can be false only for ADM V1, as angles are mandatory since ADM V2.
     * </p>
     * @return true if logical block includes angles
     * @since 12.0
     */
    public boolean hasAngles() {
        return !Double.isNaN(rotationAngles[0] + rotationAngles[1] + rotationAngles[2]);
    }

    /** Check if the logical block includes rates.
     * @return true if logical block includes rates
     */
    public boolean hasRates() {
        return !Double.isNaN(rotationRates[0] + rotationRates[1] + rotationRates[2]);
    }

}