AttitudeType.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;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.hipparchus.analysis.differentiation.UnivariateDerivative1;
import org.hipparchus.analysis.differentiation.UnivariateDerivative2;
import org.hipparchus.geometry.euclidean.threed.FieldRotation;
import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
import org.hipparchus.geometry.euclidean.threed.Rotation;
import org.hipparchus.geometry.euclidean.threed.RotationConvention;
import org.hipparchus.geometry.euclidean.threed.RotationOrder;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathUtils;
import org.hipparchus.util.SinCos;
import org.orekit.attitudes.Attitude;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.definitions.Units;
import org.orekit.files.ccsds.utils.ContextBinding;
import org.orekit.time.AbsoluteDate;
import org.orekit.utils.AccurateFormatter;
import org.orekit.utils.AngularDerivativesFilter;
import org.orekit.utils.TimeStampedAngularCoordinates;
import org.orekit.utils.units.Unit;

/** Enumerate for ADM attitude type.
 * @author Bryan Cazabonne
 * @since 10.2
 */
public enum AttitudeType {

    /** Quaternion. */
    QUATERNION(Collections.singleton(new VersionedName(1.0, "QUATERNION")),
               AngularDerivativesFilter.USE_R,
               Unit.ONE, Unit.ONE, Unit.ONE, Unit.ONE) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // Data index
            final int[] quaternionIndex = isFirst ? new int[] {0, 1, 2, 3} : new int[] {3, 0, 1, 2};

            Rotation rotation  = coordinates.getRotation();
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            // Fill the array, taking care of quaternion ordering
            final double[] data = new double[4];
            data[quaternionIndex[0]] = rotation.getQ0();
            data[quaternionIndex[1]] = rotation.getQ1();
            data[quaternionIndex[2]] = rotation.getQ2();
            data[quaternionIndex[3]] = rotation.getQ3();

            return data;

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            Rotation rotation = isFirst ?
                                new Rotation(components[0], components[1], components[2], components[3], true) :
                                new Rotation(components[3], components[0], components[1], components[2], true);
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            // Return
            return new TimeStampedAngularCoordinates(date, rotation, Vector3D.ZERO, Vector3D.ZERO);

        }

    },

    /** Quaternion and derivatives. */
    QUATERNION_DERIVATIVE(Collections.singleton(new VersionedName(1.0, "QUATERNION/DERIVATIVE")),
                          AngularDerivativesFilter.USE_RR,
                          Unit.ONE, Unit.ONE, Unit.ONE, Unit.ONE,
                          Units.ONE_PER_S, Units.ONE_PER_S, Units.ONE_PER_S, Units.ONE_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            FieldRotation<UnivariateDerivative1> rotation = coordinates.toUnivariateDerivative1Rotation();
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            // Data index
            final int[] quaternionIndex = isFirst ?
                                          new int[] {0, 1, 2, 3, 4, 5, 6, 7} :
                                          new int[] {3, 0, 1, 2, 7, 4, 5, 6};

            // Fill the array, taking care of quaternion ordering
            final double[] data = new double[8];
            data[quaternionIndex[0]] = rotation.getQ0().getValue();
            data[quaternionIndex[1]] = rotation.getQ1().getValue();
            data[quaternionIndex[2]] = rotation.getQ2().getValue();
            data[quaternionIndex[3]] = rotation.getQ3().getValue();
            data[quaternionIndex[4]] = rotation.getQ0().getFirstDerivative();
            data[quaternionIndex[5]] = rotation.getQ1().getFirstDerivative();
            data[quaternionIndex[6]] = rotation.getQ2().getFirstDerivative();
            data[quaternionIndex[7]] = rotation.getQ3().getFirstDerivative();

            return data;

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {
            FieldRotation<UnivariateDerivative1> rotation =
                            isFirst ?
                            new FieldRotation<>(new UnivariateDerivative1(components[0], components[4]),
                                                new UnivariateDerivative1(components[1], components[5]),
                                                new UnivariateDerivative1(components[2], components[6]),
                                                new UnivariateDerivative1(components[3], components[7]),
                                                true) :
                            new FieldRotation<>(new UnivariateDerivative1(components[3], components[7]),
                                                new UnivariateDerivative1(components[0], components[4]),
                                                new UnivariateDerivative1(components[1], components[5]),
                                                new UnivariateDerivative1(components[2], components[6]),
                                                true);
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            return new TimeStampedAngularCoordinates(date, rotation);

        }

    },

    /** Quaternion and Euler angles rates (only in ADM V1). */
    QUATERNION_EULER_RATES(Collections.singleton(new VersionedName(1.0, "QUATERNION/RATE")),
                           AngularDerivativesFilter.USE_RR,
                           Unit.ONE, Unit.ONE, Unit.ONE, Unit.ONE,
                           Units.DEG_PER_S, Units.DEG_PER_S, Units.DEG_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // Data index
            final int[] quaternionIndex = isFirst ? new int[] {0, 1, 2, 3} : new int[] {3, 0, 1, 2};

            // Attitude
            FieldRotation<UnivariateDerivative1> rotation = coordinates.toUnivariateDerivative1Rotation();
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }
            final UnivariateDerivative1[] euler = rotation.getAngles(eulerRotSequence, RotationConvention.FRAME_TRANSFORM);

            // Fill the array, taking care of quaternion ordering
            final double[] data = new double[7];
            data[quaternionIndex[0]] = rotation.getQ0().getValue();
            data[quaternionIndex[1]] = rotation.getQ1().getValue();
            data[quaternionIndex[2]] = rotation.getQ2().getValue();
            data[quaternionIndex[3]] = rotation.getQ3().getValue();
            data[4]                  = euler[0].getFirstDerivative();
            data[5]                  = euler[1].getFirstDerivative();
            data[6]                  = euler[2].getFirstDerivative();

            return data;

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {
            // Build the needed objects
            final Rotation rotation = isFirst ?
                                      new Rotation(components[0], components[1], components[2], components[3], true) :
                                      new Rotation(components[3], components[0], components[1], components[2], true);
            final double[] euler = rotation.getAngles(eulerRotSequence, RotationConvention.FRAME_TRANSFORM);
            final FieldRotation<UnivariateDerivative1> rUD1 =
                            new FieldRotation<>(eulerRotSequence, RotationConvention.FRAME_TRANSFORM,
                                                new UnivariateDerivative1(euler[0], components[4]),
                                                new UnivariateDerivative1(euler[1], components[5]),
                                                new UnivariateDerivative1(euler[2], components[6]));

            // Return
            final TimeStampedAngularCoordinates ac = new TimeStampedAngularCoordinates(date, rUD1);
            return isExternal2SpacecraftBody ? ac : ac.revert();

        }

    },

    /** Quaternion and angular velocity. */
    QUATERNION_ANGVEL(Collections.singleton(new VersionedName(2.0, "QUATERNION/ANGVEL")),
                      AngularDerivativesFilter.USE_RR,
                      Unit.ONE, Unit.ONE, Unit.ONE, Unit.ONE,
                      Units.DEG_PER_S, Units.DEG_PER_S, Units.DEG_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // Data index
            final int[] quaternionIndex = isFirst ? new int[] {0, 1, 2, 3} : new int[] {3, 0, 1, 2};

            // Attitude
            final TimeStampedAngularCoordinates c = isExternal2SpacecraftBody ? coordinates : coordinates.revert();
            final Vector3D rotationRate = QUATERNION_ANGVEL.metadataRate(isSpacecraftBodyRate, c.getRotationRate(), c.getRotation());

            // Fill the array, taking care of quaternion ordering
            final double[] data = new double[7];
            data[quaternionIndex[0]] = c.getRotation().getQ0();
            data[quaternionIndex[1]] = c.getRotation().getQ1();
            data[quaternionIndex[2]] = c.getRotation().getQ2();
            data[quaternionIndex[3]] = c.getRotation().getQ3();
            data[4] = rotationRate.getX();
            data[5] = rotationRate.getY();
            data[6] = rotationRate.getZ();

            return data;

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {
            // Build the needed objects
            final Rotation rotation = isFirst ?
                                      new Rotation(components[0], components[1], components[2], components[3], true) :
                                      new Rotation(components[3], components[0], components[1], components[2], true);
            final Vector3D rotationRate = QUATERNION_ANGVEL.orekitRate(isSpacecraftBodyRate,
                                                                       new Vector3D(components[4], components[5], components[6]),
                                                                       rotation);

            // Return
            final TimeStampedAngularCoordinates ac =
                            new TimeStampedAngularCoordinates(date, rotation, rotationRate, Vector3D.ZERO);
            return isExternal2SpacecraftBody ? ac : ac.revert();

        }

    },

    /** Euler angles. */
    EULER_ANGLE(Collections.singleton(new VersionedName(1.0, "EULER_ANGLE")),
                AngularDerivativesFilter.USE_R,
                Unit.DEGREE, Unit.DEGREE, Unit.DEGREE) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // Attitude
            Rotation rotation = coordinates.getRotation();
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            return rotation.getAngles(eulerRotSequence, RotationConvention.FRAME_TRANSFORM);

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            // Build the needed objects
            Rotation rotation = new Rotation(eulerRotSequence, RotationConvention.FRAME_TRANSFORM,
                                             components[0], components[1], components[2]);
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            // Return
            return new TimeStampedAngularCoordinates(date, rotation, Vector3D.ZERO, Vector3D.ZERO);
        }

    },

    /** Euler angles and rotation rate. */
    EULER_ANGLE_DERIVATIVE(Arrays.asList(new VersionedName(1.0, "EULER_ANGLE/RATE"),
                                         new VersionedName(2.0, "EULER_ANGLE/DERIVATIVE")),
                           AngularDerivativesFilter.USE_RR,
                           Unit.DEGREE, Unit.DEGREE, Unit.DEGREE,
                           Units.DEG_PER_S, Units.DEG_PER_S, Units.DEG_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // Attitude
            FieldRotation<UnivariateDerivative1> rotation = coordinates.toUnivariateDerivative1Rotation();
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            final UnivariateDerivative1[] angles = rotation.getAngles(eulerRotSequence, RotationConvention.FRAME_TRANSFORM);

            return new double[] {
                angles[0].getValue(),
                angles[1].getValue(),
                angles[2].getValue(),
                angles[0].getFirstDerivative(),
                angles[1].getFirstDerivative(),
                angles[2].getFirstDerivative()
            };

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            // Build the needed objects
            FieldRotation<UnivariateDerivative1> rotation =
                            new FieldRotation<>(eulerRotSequence, RotationConvention.FRAME_TRANSFORM,
                                                new UnivariateDerivative1(components[0], components[3]),
                                                new UnivariateDerivative1(components[1], components[4]),
                                                new UnivariateDerivative1(components[2], components[5]));
            if (!isExternal2SpacecraftBody) {
                rotation = rotation.revert();
            }

            return new TimeStampedAngularCoordinates(date, rotation);

        }

    },

    /** Euler angles and angular velocity.
     * @since 12.0
     */
    EULER_ANGLE_ANGVEL(Collections.singleton(new VersionedName(2.0, "EULER_ANGLE/ANGVEL")),
                       AngularDerivativesFilter.USE_RR,
                       Unit.DEGREE, Unit.DEGREE, Unit.DEGREE,
                       Units.DEG_PER_S, Units.DEG_PER_S, Units.DEG_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // Attitude
            final TimeStampedAngularCoordinates c = isExternal2SpacecraftBody ? coordinates : coordinates.revert();
            final Vector3D rotationRate = EULER_ANGLE_ANGVEL.metadataRate(isSpacecraftBodyRate, c.getRotationRate(), c.getRotation());
            final double[] angles       = c.getRotation().getAngles(eulerRotSequence, RotationConvention.FRAME_TRANSFORM);

            return new double[] {
                angles[0],
                angles[1],
                angles[2],
                rotationRate.getX(),
                rotationRate.getY(),
                rotationRate.getZ()
            };

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            // Build the needed objects
            final Rotation rotation = new Rotation(eulerRotSequence,
                                                   RotationConvention.FRAME_TRANSFORM,
                                                   components[0],
                                                   components[1],
                                                   components[2]);
            final Vector3D rotationRate = EULER_ANGLE_ANGVEL.orekitRate(isSpacecraftBodyRate,
                                                                        new Vector3D(components[3], components[4], components[5]),
                                                                        rotation);
            // Return
            final TimeStampedAngularCoordinates ac =
                            new TimeStampedAngularCoordinates(date, rotation, rotationRate, Vector3D.ZERO);
            return isExternal2SpacecraftBody ? ac : ac.revert();

        }

    },

    /** Spin. */
    SPIN(Collections.singleton(new VersionedName(1.0, "SPIN")),
         AngularDerivativesFilter.USE_R,
         Unit.DEGREE, Unit.DEGREE, Unit.DEGREE, Units.DEG_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // spin axis is forced to Z (but it is not the instantaneous rotation rate as it also moves)
            final TimeStampedAngularCoordinates c = isExternal2SpacecraftBody ? coordinates : coordinates.revert();
            final SpinFinder sf = new SpinFinder(c);
            final double spinAngleVel = coordinates.getRotationRate().getZ();

            return new double[] {
                sf.getSpinAlpha(), sf.getSpinDelta(), sf.getSpinAngle(), spinAngleVel
            };

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            // Build the needed objects
            final Rotation rotation = new Rotation(RotationOrder.ZXZ,
                                                   RotationConvention.FRAME_TRANSFORM,
                                                   MathUtils.SEMI_PI + components[0],
                                                   MathUtils.SEMI_PI - components[1],
                                                   components[2]);
            final Vector3D rotationRate = new Vector3D(0, 0, components[3]);

            // Return
            final TimeStampedAngularCoordinates ac =
                            new TimeStampedAngularCoordinates(date, rotation, rotationRate, Vector3D.ZERO);
            return isExternal2SpacecraftBody ? ac : ac.revert();

        }

    },

    /** Spin and nutation. */
    SPIN_NUTATION(Collections.singleton(new VersionedName(1.0, "SPIN/NUTATION")),
                  AngularDerivativesFilter.USE_RR,
                  Unit.DEGREE, Unit.DEGREE, Unit.DEGREE, Units.DEG_PER_S,
                  Unit.DEGREE, Unit.SECOND, Unit.DEGREE) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // spin data
            final TimeStampedAngularCoordinates c = isExternal2SpacecraftBody ? coordinates : coordinates.revert();
            final SpinFinder sf = new SpinFinder(c);

            // Orekit/CCSDS naming difference: for CCSDS this is nutation, for Orekit this is precession
            final FieldRotation<UnivariateDerivative2> c2       = c.toUnivariateDerivative2Rotation();
            final FieldVector3D<UnivariateDerivative2> spinAxis = c2.applyInverseTo(Vector3D.PLUS_K);
            final PrecessionFinder                     pf       = new PrecessionFinder(spinAxis);

            // intermediate inertial frame, with Z axis aligned with angular momentum
            final Rotation intermediate2Inert = new Rotation(Vector3D.PLUS_K, pf.getAxis());

            // recover Euler rotations starting from frame aligned with angular momentum
            final FieldRotation<UnivariateDerivative2> intermediate2Body = c2.applyTo(intermediate2Inert);
            final UnivariateDerivative2[] euler = intermediate2Body.
                                                  getAngles(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM);

            return new double[] {
                sf.getSpinAlpha(),
                sf.getSpinDelta(),
                sf.getSpinAngle(),
                euler[2].getFirstDerivative(),
                pf.getPrecessionAngle(),
                MathUtils.TWO_PI / pf.getAngularVelocity(),
                euler[2].getValue() - MathUtils.SEMI_PI
            };

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            // Build the needed objects
            final Rotation inert2Body0 = new Rotation(RotationOrder.ZXZ,
                                                      RotationConvention.FRAME_TRANSFORM,
                                                      MathUtils.SEMI_PI + components[0],
                                                      MathUtils.SEMI_PI - components[1],
                                                      components[2]);

            // intermediate inertial frame, with Z axis aligned with angular momentum
            final SinCos   scNutation         = FastMath.sinCos(components[4]);
            final SinCos   scPhase            = FastMath.sinCos(components[6]);
            final Vector3D momentumBody       = new Vector3D( scNutation.sin() * scPhase.cos(),
                                                             -scNutation.sin() * scPhase.sin(),
                                                              scNutation.cos());
            final Vector3D momentumInert      = inert2Body0.applyInverseTo(momentumBody);
            final Rotation inert2Intermediate = new Rotation(momentumInert, Vector3D.PLUS_K);

            // base Euler angles from the intermediate frame to body
            final Rotation intermediate2Body0 = inert2Body0.applyTo(inert2Intermediate.revert());
            final double[] euler0             = intermediate2Body0.getAngles(RotationOrder.ZXZ,
                                                                             RotationConvention.FRAME_TRANSFORM);

            // add Euler angular rates to base Euler angles
            final FieldRotation<UnivariateDerivative2> intermediate2Body =
                            new FieldRotation<>(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM,
                                                new UnivariateDerivative2(euler0[0], MathUtils.TWO_PI / components[5], 0.0),
                                                new UnivariateDerivative2(euler0[1], 0.0,           0.0),
                                                new UnivariateDerivative2(euler0[2], components[3], 0.0));

            // final rotation, including derivatives
            final FieldRotation<UnivariateDerivative2> inert2Body = intermediate2Body.applyTo(inert2Intermediate);

            final TimeStampedAngularCoordinates ac =
                            new TimeStampedAngularCoordinates(date, inert2Body);
            return isExternal2SpacecraftBody ? ac : ac.revert();

        }

    },

    /** Spin and momentum.
     * @since 12.0
     */
    SPIN_NUTATION_MOMENTUM(Collections.singleton(new VersionedName(2.0, "SPIN/NUTATION_MOM")),
                           AngularDerivativesFilter.USE_RR,
                           Unit.DEGREE, Unit.DEGREE, Unit.DEGREE, Units.DEG_PER_S,
                           Unit.DEGREE, Unit.DEGREE, Units.DEG_PER_S) {

        /** {@inheritDoc} */
        @Override
        public double[] generateData(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates coordinates) {

            // spin data
            final TimeStampedAngularCoordinates c = isExternal2SpacecraftBody ? coordinates : coordinates.revert();
            final SpinFinder sf = new SpinFinder(c);

            // Orekit/CCSDS naming difference: for CCSDS this is nutation, for Orekit this is precession
            final FieldRotation<UnivariateDerivative2> c2       = c.toUnivariateDerivative2Rotation();
            final FieldVector3D<UnivariateDerivative2> spinAxis = c2.applyInverseTo(Vector3D.PLUS_K);
            final PrecessionFinder                     pf       = new PrecessionFinder(spinAxis);

            // intermediate inertial frame, with Z axis aligned with angular momentum
            final Rotation intermediate2Inert = new Rotation(Vector3D.PLUS_K, pf.getAxis());

            // recover spin angle velocity
            final FieldRotation<UnivariateDerivative2> intermediate2Body = c2.applyTo(intermediate2Inert);
            final double spinAngleVel = intermediate2Body.
                                        getAngles(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM)[2].
                                        getFirstDerivative();

            return new double[] {
                sf.getSpinAlpha(),
                sf.getSpinDelta(),
                sf.getSpinAngle(),
                spinAngleVel,
                pf.getAxis().getAlpha(),
                pf.getAxis().getDelta(),
                pf.getAngularVelocity()
            };

        }

        /** {@inheritDoc} */
        @Override
        public TimeStampedAngularCoordinates build(final boolean isFirst,
                                                   final boolean isExternal2SpacecraftBody,
                                                   final RotationOrder eulerRotSequence,
                                                   final boolean isSpacecraftBodyRate,
                                                   final AbsoluteDate date,
                                                   final double... components) {

            // Build the needed objects
            final SinCos   scAlpha            = FastMath.sinCos(components[4]);
            final SinCos   scDelta            = FastMath.sinCos(components[5]);
            final Vector3D momentumInert      = new Vector3D(scAlpha.cos() * scDelta.cos(),
                                                             scAlpha.sin() * scDelta.cos(),
                                                             scDelta.sin());
            final Rotation inert2Intermediate = new Rotation(momentumInert, Vector3D.PLUS_K);

            // base Euler angles from the intermediate frame to body
            final Rotation inert2Body0 = new Rotation(RotationOrder.ZXZ,
                                                      RotationConvention.FRAME_TRANSFORM,
                                                      MathUtils.SEMI_PI + components[0],
                                                      MathUtils.SEMI_PI - components[1],
                                                      components[2]);
            final Rotation intermediate2Body0 = inert2Body0.applyTo(inert2Intermediate.revert());
            final double[] euler0             = intermediate2Body0.getAngles(RotationOrder.ZXZ,
                                                                             RotationConvention.FRAME_TRANSFORM);

            // add Euler angular rates to base Euler angles
            final FieldRotation<UnivariateDerivative2> intermediate2Body =
                            new FieldRotation<>(RotationOrder.ZXZ, RotationConvention.FRAME_TRANSFORM,
                                                new UnivariateDerivative2(euler0[0], components[6], 0.0),
                                                new UnivariateDerivative2(euler0[1], 0.0,           0.0),
                                                new UnivariateDerivative2(euler0[2], components[3], 0.0));

            // final rotation, including derivatives
            final FieldRotation<UnivariateDerivative2> inert2Body = intermediate2Body.applyTo(inert2Intermediate);

            // return
            final TimeStampedAngularCoordinates ac =
                            new TimeStampedAngularCoordinates(date, inert2Body);
            return isExternal2SpacecraftBody ? ac : ac.revert();

        }

    };

    /** Names map.
     * @since 12.0
     */
    private static final Map<String, AttitudeType> MAP = new HashMap<>();
    static {
        for (final AttitudeType type : values()) {
            for (final VersionedName vn : type.ccsdsNames) {
                MAP.put(vn.name, type);
            }
        }
    }

    /** CCSDS names of the attitude type. */
    private final Iterable<VersionedName> ccsdsNames;

    /** Derivatives filter. */
    private final AngularDerivativesFilter filter;

    /** Components units (used only for parsing). */
    private final Unit[] units;

    /** Private constructor.
     * @param ccsdsNames CCSDS names of the attitude type
     * @param filter derivative filter
     * @param units components units (used only for parsing)
     */
    AttitudeType(final Iterable<VersionedName> ccsdsNames, final AngularDerivativesFilter filter, final Unit... units) {
        this.ccsdsNames = ccsdsNames;
        this.filter     = filter;
        this.units      = units.clone();
    }

    /** Get the type name for a given format version.
     * @param formatVersion format version
     * @return type name
     * @since 12.0
     */
    public String getName(final double formatVersion) {
        String name = null;
        for (final VersionedName vn : ccsdsNames) {
            if (name == null || formatVersion >= vn.since) {
                name = vn.name;
            }
        }
        return name;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        // use the most recent name by default
        return getName(Double.POSITIVE_INFINITY);
    }

    /** Parse an attitude type.
     * @param typeSpecification unnormalized type name
     * @return parsed type
     */
    public static AttitudeType parseType(final String typeSpecification) {
        final AttitudeType type = MAP.get(typeSpecification);
        if (type == null) {
            throw new OrekitException(OrekitMessages.CCSDS_UNKNOWN_ATTITUDE_TYPE, typeSpecification);
        }
        return type;
    }

    /**
     * Get the attitude data fields corresponding to the attitude type.
     * <p>
     * This method returns the components in CCSDS units (i.e. degrees, degrees per seconds…).
     * </p>
     * @param isFirst if true the first quaternion component is the scalar component
     * @param isExternal2SpacecraftBody true attitude is from external frame to spacecraft body frame
     * @param eulerRotSequence sequance of Euler angles
     * @param isSpacecraftBodyRate if true Euler rates are specified in spacecraft body frame
     * @param attitude angular coordinates, using {@link Attitude Attitude} convention
     * (i.e. from inertial frame to spacecraft frame)
     * @return the attitude data in CCSDS units
     */
    public String[] createDataFields(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                     final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                     final TimeStampedAngularCoordinates attitude) {

        // generate the double data
        final double[] data = generateData(isFirst, isExternal2SpacecraftBody,
                                           eulerRotSequence, isSpacecraftBodyRate, attitude);

        // format as string array with CCSDS units
        final String[] fields = new String[data.length];
        for (int i = 0; i < data.length; ++i) {
            fields[i] = AccurateFormatter.format(units[i].fromSI(data[i]));
        }

        return fields;

    }

    /**
     * Generate the attitude data corresponding to the attitude type.
     * <p>
     * This method returns the components in SI units.
     * </p>
     * @param isFirst if true the first quaternion component is the scalar component
     * @param isExternal2SpacecraftBody true attitude is from external frame to spacecraft body frame
     * @param eulerRotSequence sequance of Euler angles
     * @param isSpacecraftBodyRate if true Euler rates are specified in spacecraft body frame
     * @param attitude angular coordinates, using {@link Attitude Attitude} convention
     * (i.e. from inertial frame to spacecraft frame)
     * @return the attitude data in CCSDS units
     * @since 12.0
     */
    public abstract double[] generateData(boolean isFirst, boolean isExternal2SpacecraftBody,
                                          RotationOrder eulerRotSequence, boolean isSpacecraftBodyRate,
                                          TimeStampedAngularCoordinates attitude);

    /**
     * Get the angular coordinates corresponding to the attitude data.
     * <p>
     * This method assumes the text fields are in CCSDS units and will convert to SI units.
     * </p>
     * @param isFirst if true the first quaternion component is the scalar component
     * @param isExternal2SpacecraftBody true attitude is from external frame to spacecraft body frame
     * @param eulerRotSequence sequance of Euler angles
     * @param isSpacecraftBodyRate if true Euler rates are specified in spacecraft body frame
     * @param context context binding
     * @param fields raw data fields
     * @return the angular coordinates, using {@link Attitude Attitude} convention
     * (i.e. from inertial frame to spacecraft frame)
     */
    public TimeStampedAngularCoordinates parse(final boolean isFirst, final boolean isExternal2SpacecraftBody,
                                               final RotationOrder eulerRotSequence, final boolean isSpacecraftBodyRate,
                                               final ContextBinding context, final String[] fields) {

        // parse the text fields
        final AbsoluteDate date = context.getTimeSystem().getConverter(context).parse(fields[0]);
        final double[] components = new double[fields.length - 1];
        for (int i = 0; i < components.length; ++i) {
            components[i] = units[i].toSI(Double.parseDouble(fields[i + 1]));
        }

        // build the coordinates
        return build(isFirst, isExternal2SpacecraftBody, eulerRotSequence, isSpacecraftBodyRate,
                     date, components);

    }

    /** Get the angular coordinates corresponding to the attitude data.
     * @param isFirst if true the first quaternion component is the scalar component
     * @param isExternal2SpacecraftBody true attitude is from external frame to spacecraft body frame
     * @param eulerRotSequence sequance of Euler angles
     * @param isSpacecraftBodyRate if true Euler rates are specified in spacecraft body frame
     * @param date entry date
     * @param components entry components with SI units, semantic depends on attitude type
     * @return the angular coordinates, using {@link Attitude Attitude} convention
     * (i.e. from inertial frame to spacecraft frame)
     */
    public abstract TimeStampedAngularCoordinates build(boolean isFirst, boolean isExternal2SpacecraftBody,
                                                        RotationOrder eulerRotSequence, boolean isSpacecraftBodyRate,
                                                        AbsoluteDate date, double... components);

    /**
     * Get the angular derivative filter corresponding to the attitude data.
     * @return the angular derivative filter corresponding to the attitude data
     */
    public AngularDerivativesFilter getAngularDerivativesFilter() {
        return filter;
    }

    /** Convert a rotation rate for Orekit convention to metadata convention.
     * @param isSpacecraftBodyRate if true Euler rates are specified in spacecraft body frame
     * @param rate rotation rate from Orekit attitude
     * @param rotation corresponding rotation
     * @return rotation rate in metadata convention
     */
    private Vector3D metadataRate(final boolean isSpacecraftBodyRate, final Vector3D rate, final Rotation rotation) {
        return isSpacecraftBodyRate ? rate : rotation.applyInverseTo(rate);
    }

    /** Convert a rotation rate for metadata convention to Orekit convention.
     * @param isSpacecraftBodyRate if true Euler rates are specified in spacecraft body frame
     * @param rate rotation rate read from the data line
     * @param rotation corresponding rotation
     * @return rotation rate in Orekit convention (i.e. in spacecraft body local frame)
     */
    private Vector3D orekitRate(final boolean isSpacecraftBodyRate, final Vector3D rate, final Rotation rotation) {
        return isSpacecraftBodyRate ? rate : rotation.applyTo(rate);
    }

    /** Container for a name associated to a format version.
     * @since 12.0
     */
    private static class VersionedName {

        /** Version at which this name was defined. */
        private final double since;

        /** Name. */
        private final String name;

        /** Simple constructor.
         * @param since version at which this name was defined
         * @param name name
         */
        VersionedName(final double since, final String name) {
            this.since = since;
            this.name  = name;
        }

    }

}