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