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 }