1   /* Copyright 2002-2026 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.files.ccsds.ndm.adm.apm;
18  
19  import java.util.Arrays;
20  import java.util.Optional;
21  
22  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
23  import org.orekit.annotation.Nullable;
24  import org.orekit.errors.OrekitException;
25  import org.orekit.errors.OrekitMessages;
26  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
27  import org.orekit.files.ccsds.ndm.adm.AttitudeEndpoints;
28  import org.orekit.files.ccsds.section.CommentsContainer;
29  import org.orekit.frames.Frame;
30  
31  /**
32   * Container for {@link Euler Euler rotations} entries.
33   * <p>
34   * Beware that the Orekit getters and setters all rely on SI units. The parsers
35   * and writers take care of converting these SI units into CCSDS mandatory units.
36   * The {@link org.orekit.utils.units.Unit Unit} class provides useful
37   * {@link org.orekit.utils.units.Unit#fromSI(double) fromSi} and
38   * {@link org.orekit.utils.units.Unit#toSI(double) toSI} methods in case the callers
39   * already use CCSDS units instead of the API SI units. The general-purpose
40   * {@link org.orekit.utils.units.Unit Unit} class (without an 's') and the
41   * CCSDS-specific {@link org.orekit.files.ccsds.definitions.Units Units} class
42   * (with an 's') also provide some predefined units. These predefined units and the
43   * {@link org.orekit.utils.units.Unit#fromSI(double) fromSi} and
44   * {@link org.orekit.utils.units.Unit#toSI(double) toSI} conversion methods are indeed
45   * what the parsers and writers use for the conversions.
46   * </p>
47   * @author Bryan Cazabonne
48   * @since 10.2
49   */
50  public class Euler extends CommentsContainer {
51  
52      /** Key for angles in ADM V1.
53       * @since 12.0
54       */
55      private static final String KEY_ANGLES_V1 = "{X|Y|Z}_ANGLE";
56  
57      /** Key for angles in ADM V2.
58       * @since 12.0
59       */
60      private static final String KEY_ANGLES_V2 = "ANGLE_{1|2|3}";
61  
62      /** Key for rates in ADM V1.
63       * @since 12.0
64       */
65      private static final String KEY_RATES_V1 = "{X|Y|Z}_RATE";
66  
67      /** Key for rates in ADM V2.
68       * @since 12.0
69       */
70      private static final String KEY_RATES_V2 = "ANGLE_{1|2|3}_DOT";
71  
72      /** Endpoints (i.e. frames A, B and their relationship). */
73      private final AttitudeEndpoints endpoints;
74  
75      /** Rotation order of the Euler angles. */
76      private RotationOrder eulerRotSeq;
77  
78      /** The frame in which rates are specified. */
79      @Nullable
80      private Boolean rateFrameIsA;
81  
82      /** Euler angles [rad]. */
83      private final double[] rotationAngles;
84  
85      /** Rotation rate [rad/s]. */
86      @Nullable
87      private double[] rotationRates;
88  
89      /** Indicator for rotation angles. */
90      private boolean inRotationAngles;
91  
92      /**
93       * Simple constructor.
94       *
95       * @param frameMapper for creating a {@link Frame}.
96       * @since 13.1.5
97       */
98      public Euler(final CcsdsFrameMapper frameMapper) {
99          this.endpoints        = new AttitudeEndpoints(frameMapper);
100         this.rotationAngles   = new double[3];
101         this.inRotationAngles = false;
102         Arrays.fill(rotationAngles, Double.NaN);
103     }
104 
105     /** {@inheritDoc} */
106     @Override
107     public void validate(final double version) {
108         super.validate(version);
109         if (version < 2.0) {
110             endpoints.checkMandatoryEntriesExceptExternalFrame(version,
111                                                                EulerKey.EULER_FRAME_A,
112                                                                EulerKey.EULER_FRAME_B,
113                                                                EulerKey.EULER_DIR);
114             endpoints.checkExternalFrame(EulerKey.EULER_FRAME_A, EulerKey.EULER_FRAME_B);
115         } else {
116             endpoints.checkMandatoryEntriesExceptExternalFrame(version,
117                                                                EulerKey.REF_FRAME_A,
118                                                                EulerKey.REF_FRAME_B,
119                                                                EulerKey.EULER_DIR);
120             endpoints.checkExternalFrame(EulerKey.REF_FRAME_A, EulerKey.REF_FRAME_B);
121         }
122         checkNotNull(eulerRotSeq, EulerKey.EULER_ROT_SEQ.name());
123 
124         if (!hasAngles()) {
125             // if at least one angle is missing, all must be NaN (i.e. not initialized)
126             for (final double ra : rotationAngles) {
127                 if (!Double.isNaN(ra)) {
128                     throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY,
129                                               version < 2.0 ? KEY_ANGLES_V1 : KEY_ANGLES_V2);
130                 }
131             }
132         }
133 
134         if (!hasRates()) {
135             if (rotationRates != null) {
136                 // if at least one rate is missing, all must be NaN (i.e. not initialized)
137                 for (final double rr : rotationRates) {
138                     if (!Double.isNaN(rr)) {
139                         throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY,
140                                 version < 2.0 ? KEY_RATES_V1 : KEY_RATES_V2);
141                     }
142                 }
143             }
144         }
145 
146         if (version < 2.0) {
147             // in ADM V1, either angles or rates must be specified
148             // (angles may be missing in the quaternion/Euler rate case)
149             if (!hasAngles() && !hasRates()) {
150                 throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, KEY_ANGLES_V1 + "/" + KEY_RATES_V1);
151             }
152         } else {
153             // in ADM V2, angles are mandatory
154             if (!hasAngles()) {
155                 throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, KEY_ANGLES_V2);
156             }
157         }
158 
159     }
160 
161     /** Get the endpoints (i.e. frames A, B and their relationship).
162      * @return endpoints
163      */
164     public AttitudeEndpoints getEndpoints() {
165         return endpoints;
166     }
167 
168     /**
169      * Get the rotation order of Euler angles.
170      * @return rotation order
171      */
172     public RotationOrder getEulerRotSeq() {
173         return eulerRotSeq;
174     }
175 
176     /**
177      * Set the rotation order for Euler angles.
178      * @param eulerRotSeq order to be set
179      */
180     public void setEulerRotSeq(final RotationOrder eulerRotSeq) {
181         refuseFurtherComments();
182         this.eulerRotSeq = eulerRotSeq;
183     }
184 
185     /** Check if rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}.
186      * @return true if rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}
187      */
188     public boolean rateFrameIsA() {
189         return rateFrameIsA == null ? false : rateFrameIsA;
190     }
191 
192     /** Set the frame in which rates are specified.
193      * @param rateFrameIsA if true, rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}
194      */
195     public void setRateFrameIsA(final boolean rateFrameIsA) {
196         refuseFurtherComments();
197         this.rateFrameIsA = rateFrameIsA;
198     }
199 
200     /** Check if rates are specified in spacecraft body frame.
201      * <p>
202      * {@link #validate(double) Mandatory entries} must have been
203      * initialized properly to non-null values before this method is called,
204      * otherwise {@code NullPointerException} will be thrown.
205      * </p>
206      * @return true if rates are specified in spacecraft body frame
207      */
208     public boolean isSpacecraftBodyRate() {
209         return rateFrameIsA() ^ endpoints.getFrameA().asSpacecraftBodyFrame().isEmpty();
210     }
211 
212     /**
213      * Get the coordinates of the Euler angles.
214      * @return rotation angles (rad)
215      */
216     public double[] getRotationAngles() {
217         return rotationAngles.clone();
218     }
219 
220     /**
221      * Set the Euler angle about axis.
222      * @param axis rotation axis
223      * @param angle angle to set (rad)
224      */
225     public void setLabeledRotationAngle(final char axis, final double angle) {
226         if (eulerRotSeq != null) {
227             for (int i = 0; i < rotationAngles.length; ++i) {
228                 if (eulerRotSeq.name().charAt(i) == axis && Double.isNaN(rotationAngles[i])) {
229                     setIndexedRotationAngle(i, angle);
230                     return;
231                 }
232             }
233         }
234     }
235 
236     /**
237      * Set the Euler angle about axis.
238      * @param axis rotation axis
239      * @param angle angle to set (rad)
240      * @since 12.0
241      */
242     public void setIndexedRotationAngle(final int axis, final double angle) {
243         refuseFurtherComments();
244         rotationAngles[axis] = angle;
245     }
246 
247     /**
248      * Get the rates of the Euler angles.
249      * @return rotation rates (rad/s)
250      */
251     public Optional<double[]> getRotationRates() {
252         return rotationRates == null ? Optional.empty() : Optional.of(rotationRates.clone());
253     }
254 
255     /**
256      * Set the rate of Euler angle about axis.
257      * @param axis rotation axis
258      * @param rate angle rate to set (rad/s)
259      */
260     public void setLabeledRotationRate(final char axis, final double rate) {
261         if (eulerRotSeq != null) {
262             if (rotationRates == null) {
263                 rotationRates = new double[3];
264                 Arrays.fill(rotationRates, Double.NaN);
265             }
266             for (int i = 0; i < rotationRates.length; ++i) {
267                 if (eulerRotSeq.name().charAt(i) == axis && Double.isNaN(rotationRates[i])) {
268                     setIndexedRotationRate(i, rate);
269                     return;
270                 }
271             }
272         }
273     }
274 
275     /**
276      * Set the rate of Euler angle about axis.
277      * @param axis rotation axis
278      * @param rate angle rate to set (rad/s)
279      * @since 12.0
280      */
281     public void setIndexedRotationRate(final int axis, final double rate) {
282         refuseFurtherComments();
283         if (rotationRates == null) {
284             rotationRates = new double[3];
285             Arrays.fill(rotationRates, Double.NaN);
286         }
287         rotationRates[axis] = rate;
288     }
289 
290     /** Check if we are in the rotationAngles part of XML files.
291      * @return true if we are in the rotationAngles part of XML files
292      */
293     boolean inRotationAngles() {
294         return inRotationAngles;
295     }
296 
297     /** Set flag for rotation angle parsing.
298      * @param inRotationAngles if true, we are in the rotationAngles part of XML files
299      */
300     public void setInRotationAngles(final boolean inRotationAngles) {
301         refuseFurtherComments();
302         this.inRotationAngles = inRotationAngles;
303     }
304 
305     /** Check if the logical block includes angles.
306      * <p>
307      * This can be false only for ADM V1, as angles are mandatory since ADM V2.
308      * </p>
309      * @return true if logical block includes angles
310      * @since 12.0
311      */
312     public boolean hasAngles() {
313         return !Double.isNaN(rotationAngles[0] + rotationAngles[1] + rotationAngles[2]);
314     }
315 
316     /** Check if the logical block includes rates.
317      * @return true if logical block includes rates
318      */
319     public boolean hasRates() {
320         return getRotationRates().isPresent() && !Double.isNaN(rotationRates[0] + rotationRates[1] + rotationRates[2]);
321     }
322 
323 }