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