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