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 }