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.ArrayList;
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.Optional;
23
24 import org.hipparchus.complex.Quaternion;
25 import org.orekit.annotation.Nullable;
26 import org.orekit.attitudes.Attitude;
27 import org.orekit.errors.OrekitException;
28 import org.orekit.errors.OrekitMessages;
29 import org.orekit.files.ccsds.ndm.adm.AttitudeType;
30 import org.orekit.files.ccsds.section.CommentsContainer;
31 import org.orekit.files.ccsds.section.Data;
32 import org.orekit.frames.Frame;
33 import org.orekit.time.AbsoluteDate;
34 import org.orekit.utils.PVCoordinatesProvider;
35 import org.orekit.utils.TimeStampedAngularCoordinates;
36
37 /**
38 * Container for Attitude Parameter Message data.
39 * @author Bryan Cazabonne
40 * @since 10.2
41 */
42 public class ApmData implements Data {
43
44 /** General comments block. */
45 private final CommentsContainer commentsBlock;
46
47 /** Epoch of the data. */
48 private final AbsoluteDate epoch;
49
50 /** Quaternion block. */
51 @Nullable
52 private final ApmQuaternion quaternionBlock;
53
54 /** Euler angles block. */
55 @Nullable
56 private final Euler eulerBlock;
57
58 /** Angular velocity block.
59 * @since 12.0
60 */
61 @Nullable
62 private final AngularVelocity angularVelocityBlock;
63
64 /** Spin-stabilized block. */
65 @Nullable
66 private final SpinStabilized spinStabilizedBlock;
67
68 /** Inertia block. */
69 @Nullable
70 private final Inertia inertia;
71
72 /** Maneuvers. */
73 private final List<Maneuver> maneuvers;
74
75 /** Simple constructor.
76 * @param commentsBlock general comments block
77 * @param epoch epoch of the data
78 * @param quaternionBlock quaternion logical block (may be null in ADM V2 or later)
79 * @param eulerBlock Euler angles logicial block (may be null)
80 * @param angularVelocityBlock angular velocity block (may be null)
81 * @param spinStabilizedBlock spin-stabilized logical block (may be null)
82 * @param inertia inertia logical block (may be null)
83 */
84 public ApmData(final CommentsContainer commentsBlock,
85 final AbsoluteDate epoch,
86 final ApmQuaternion quaternionBlock,
87 final Euler eulerBlock,
88 final AngularVelocity angularVelocityBlock,
89 final SpinStabilized spinStabilizedBlock,
90 final Inertia inertia) {
91 this.commentsBlock = commentsBlock;
92 this.epoch = epoch;
93 this.quaternionBlock = quaternionBlock;
94 this.eulerBlock = eulerBlock;
95 this.angularVelocityBlock = angularVelocityBlock;
96 this.spinStabilizedBlock = spinStabilizedBlock;
97 this.inertia = inertia;
98 this.maneuvers = new ArrayList<>();
99 }
100
101 /** {@inheritDoc} */
102 @Override
103 public void validate(final double version) {
104
105 if (version < 2.0) {
106 // quaternion block is mandatory in ADM V1
107 if (quaternionBlock == null) {
108 // generate a dummy entry just for triggering the exception
109 new ApmQuaternion(null).validate(version);
110 }
111 } else {
112 // at least one logical block is mandatory in ADM V2
113 if (quaternionBlock == null && eulerBlock == null && angularVelocityBlock == null &&
114 spinStabilizedBlock == null && inertia == null) {
115 throw new OrekitException(OrekitMessages.CCSDS_INCOMPLETE_DATA);
116 }
117 }
118
119 if (quaternionBlock != null) {
120 quaternionBlock.validate(version);
121 }
122 if (eulerBlock != null) {
123 eulerBlock.validate(version);
124 }
125 if (angularVelocityBlock != null) {
126 angularVelocityBlock.validate(version);
127 }
128 if (spinStabilizedBlock != null) {
129 spinStabilizedBlock.validate(version);
130 }
131 if (inertia != null) {
132 inertia.validate(version);
133 }
134 for (final Maneuver maneuver : maneuvers) {
135 maneuver.validate(version);
136 }
137
138 }
139
140 /** Get the comments.
141 * @return comments
142 */
143 public List<String> getComments() {
144 return commentsBlock.getComments();
145 }
146
147 /**
148 * Get the epoch of the data.
149 * @return epoch the epoch
150 * @since 12.0
151 */
152 public AbsoluteDate getEpoch() {
153 return epoch;
154 }
155
156 /** Get the quaternion logical block.
157 * @return quaternion block
158 */
159 public Optional<ApmQuaternion> getQuaternionBlock() {
160 return Optional.ofNullable(quaternionBlock);
161 }
162
163 /** Get the Euler angles logical block.
164 * @return Euler angles block (may be empty)
165 */
166 public Optional<Euler> getEulerBlock() {
167 return Optional.ofNullable(eulerBlock);
168 }
169
170 /** Get the angular velocity logical block.
171 * @return angular velocity block (may be empty)
172 * @since 12.0
173 */
174 public Optional<AngularVelocity> getAngularVelocityBlock() {
175 return Optional.ofNullable(angularVelocityBlock);
176 }
177
178 /** Get the spin-stabilized logical block.
179 * @return spin-stabilized block (may be empty)
180 */
181 public Optional<SpinStabilized> getSpinStabilizedBlock() {
182 return Optional.ofNullable(spinStabilizedBlock);
183 }
184
185 /** Get the inertia logical block.
186 * @return inertia block (may be empty)
187 */
188 public Optional<Inertia> getInertiaBlock() {
189 return Optional.ofNullable(inertia);
190 }
191
192 /**
193 * Get the number of maneuvers present in the APM.
194 * @return the number of maneuvers
195 */
196 public int getNbManeuvers() {
197 return maneuvers.size();
198 }
199
200 /**
201 * Get a list of all maneuvers.
202 * @return unmodifiable list of all maneuvers.
203 */
204 public List<Maneuver> getManeuvers() {
205 return Collections.unmodifiableList(maneuvers);
206 }
207
208 /**
209 * Get a maneuver.
210 * @param index maneuver index, counting from 0
211 * @return maneuver
212 */
213 public Maneuver getManeuver(final int index) {
214 return maneuvers.get(index);
215 }
216
217 /**
218 * Add a maneuver.
219 * @param maneuver maneuver to be set
220 */
221 public void addManeuver(final Maneuver maneuver) {
222 maneuvers.add(maneuver);
223 }
224
225 /**
226 * Get boolean testing whether the APM contains at least one maneuver.
227 * @return true if APM contains at least one maneuver
228 * false otherwise
229 */
230 public boolean hasManeuvers() {
231 return !maneuvers.isEmpty();
232 }
233
234 /** Get the attitude.
235 * @param frame reference frame with respect to which attitude must be defined,
236 * (may be null if attitude is <em>not</em> orbit-relative and one wants
237 * attitude in the same frame as used in the attitude message)
238 * @param pvProvider provider for spacecraft position and velocity
239 * (may be null if attitude is <em>not</em> orbit-relative)
240 * @return attitude
241 * @since 12.0
242 */
243 public Attitude getAttitude(final Frame frame, final PVCoordinatesProvider pvProvider) {
244
245 if (quaternionBlock != null) {
246 // we have a quaternion
247 final Quaternion q = quaternionBlock.getQuaternion();
248
249 final TimeStampedAngularCoordinates tac;
250 if (quaternionBlock.hasRates()) {
251 // quaternion logical block includes everything we need
252 final Quaternion qDot = quaternionBlock.getQuaternionDot().orElseThrow(); // No problem, Optional presence is verified by hasRates()
253 tac = AttitudeType.QUATERNION_DERIVATIVE.build(true, quaternionBlock.getEndpoints().isExternal2SpacecraftBody(),
254 null, true, epoch,
255 q.getQ0(), q.getQ1(), q.getQ2(), q.getQ3(),
256 qDot.getQ0(), qDot.getQ1(), qDot.getQ2(), qDot.getQ3());
257 } else if (angularVelocityBlock != null) {
258 // get derivatives from the angular velocity logical block
259 tac = AttitudeType.QUATERNION_ANGVEL.build(true,
260 quaternionBlock.getEndpoints().isExternal2SpacecraftBody(),
261 null, true, epoch,
262 q.getQ0(), q.getQ1(), q.getQ2(), q.getQ3(),
263 angularVelocityBlock.getAngVelX(),
264 angularVelocityBlock.getAngVelY(),
265 angularVelocityBlock.getAngVelZ());
266 } else if (eulerBlock != null && eulerBlock.hasRates()) {
267 // get derivatives from the Euler logical block
268 final double[] rates = eulerBlock.getRotationRates().orElseThrow(); // Optional presence is verified by hasRates()
269 if (eulerBlock.hasAngles()) {
270 // the Euler block has everything we need
271 final double[] angles = eulerBlock.getRotationAngles();
272 tac = AttitudeType.EULER_ANGLE_DERIVATIVE.build(true,
273 eulerBlock.getEndpoints().isExternal2SpacecraftBody(),
274 eulerBlock.getEulerRotSeq(), eulerBlock.isSpacecraftBodyRate(), epoch,
275 angles[0], angles[1], angles[2],
276 rates[0], rates[1], rates[2]);
277 } else {
278 // the Euler block has only the rates (we are certainly using an ADM V1 message)
279 // we need to rebuild the rotation from the quaternion
280 tac = AttitudeType.QUATERNION_EULER_RATES.build(true,
281 eulerBlock.getEndpoints().isExternal2SpacecraftBody(),
282 eulerBlock.getEulerRotSeq(), eulerBlock.isSpacecraftBodyRate(), epoch,
283 q.getQ0(), q.getQ1(), q.getQ2(), q.getQ3(),
284 rates[0], rates[1], rates[2]);
285 }
286
287 } else {
288 // we rely only on the quaternion logical block, despite it doesn't include rates
289 tac = AttitudeType.QUATERNION.build(true, quaternionBlock.getEndpoints().isExternal2SpacecraftBody(),
290 null, true, epoch,
291 q.getQ0(), q.getQ1(), q.getQ2(), q.getQ3());
292 }
293
294 // build the attitude
295 return quaternionBlock.getEndpoints().build(frame, pvProvider, tac);
296
297 } else if (eulerBlock != null) {
298 // we have Euler angles
299 final double[] angles = eulerBlock.getRotationAngles();
300
301 final TimeStampedAngularCoordinates tac;
302 if (eulerBlock.hasRates()) {
303 // the Euler block has everything we need
304 final double[] rates = eulerBlock.getRotationRates().orElseThrow(); // Optional presence is verified by hasRates()
305 tac = AttitudeType.EULER_ANGLE_DERIVATIVE.build(true,
306 eulerBlock.getEndpoints().isExternal2SpacecraftBody(),
307 eulerBlock.getEulerRotSeq(), eulerBlock.isSpacecraftBodyRate(), epoch,
308 angles[0], angles[1], angles[2],
309 rates[0], rates[1], rates[2]);
310 } else if (angularVelocityBlock != null) {
311 // get derivatives from the angular velocity logical block
312 tac = AttitudeType.EULER_ANGLE_ANGVEL.build(true,
313 eulerBlock.getEndpoints().isExternal2SpacecraftBody(),
314 eulerBlock.getEulerRotSeq(), eulerBlock.isSpacecraftBodyRate(), epoch,
315 angles[0], angles[1], angles[2],
316 angularVelocityBlock.getAngVelX(),
317 angularVelocityBlock.getAngVelY(),
318 angularVelocityBlock.getAngVelZ());
319 } else {
320 // we rely only on the Euler logical block, despite it doesn't include rates
321 tac = AttitudeType.EULER_ANGLE.build(true,
322 eulerBlock.getEndpoints().isExternal2SpacecraftBody(),
323 eulerBlock.getEulerRotSeq(), eulerBlock.isSpacecraftBodyRate(), epoch,
324 angles[0], angles[1], angles[2]);
325 }
326
327 // build the attitude
328 return eulerBlock.getEndpoints().build(frame, pvProvider, tac);
329
330 } else if (spinStabilizedBlock != null) {
331 // we have a spin block
332
333 final TimeStampedAngularCoordinates tac;
334 if (spinStabilizedBlock.hasNutation()) {
335 // we rely only on nutation
336 tac = AttitudeType.SPIN_NUTATION.build(true, true, null, true, epoch,
337 spinStabilizedBlock.getSpinAlpha(),
338 spinStabilizedBlock.getSpinDelta(),
339 spinStabilizedBlock.getSpinAngle(),
340 spinStabilizedBlock.getSpinAngleVel(),
341 spinStabilizedBlock.getNutation().orElseThrow(), // Optional presence is verified by hasNutation()
342 spinStabilizedBlock.getNutationPeriod().orElseThrow(), // Optional presence is verified by hasNutation()
343 spinStabilizedBlock.getNutationPhase().orElseThrow()); // Optional presence is verified by hasNutation()
344 } else if (spinStabilizedBlock.hasMomentum()) {
345 // we rely only on momentum
346 tac = AttitudeType.SPIN_NUTATION_MOMENTUM.build(true, true, null, true, epoch,
347 spinStabilizedBlock.getSpinAlpha(),
348 spinStabilizedBlock.getSpinDelta(),
349 spinStabilizedBlock.getSpinAngle(),
350 spinStabilizedBlock.getSpinAngleVel(),
351 spinStabilizedBlock.getMomentumAlpha().orElseThrow(), // Optional presence is verified by hasMomentum()
352 spinStabilizedBlock.getMomentumDelta().orElseThrow(), // Optional presence is verified by hasMomentum()
353 spinStabilizedBlock.getNutationVel().orElseThrow()); // Optional presence is verified by hasMomentum()
354 } else {
355 // we rely only on the spin logical block, despite it doesn't include rates
356 tac = AttitudeType.SPIN.build(true, true, null, true, epoch,
357 spinStabilizedBlock.getSpinAlpha(),
358 spinStabilizedBlock.getSpinDelta(),
359 spinStabilizedBlock.getSpinAngle(),
360 spinStabilizedBlock.getSpinAngleVel());
361 }
362
363 // build the attitude
364 return spinStabilizedBlock.getEndpoints().build(frame, pvProvider, tac);
365
366 } else {
367 throw new OrekitException(OrekitMessages.CCSDS_INCOMPLETE_DATA);
368 }
369
370 }
371
372 }