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 }