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 }