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.aem;
18  
19  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
20  import org.orekit.errors.OrekitException;
21  import org.orekit.errors.OrekitMessages;
22  import org.orekit.files.ccsds.definitions.FrameFacade;
23  import org.orekit.files.ccsds.ndm.adm.AdmMetadata;
24  import org.orekit.files.ccsds.ndm.adm.AttitudeEndpoints;
25  import org.orekit.files.ccsds.ndm.adm.AttitudeType;
26  import org.orekit.time.AbsoluteDate;
27  
28  /** This class gathers the meta-data present in the Attitude Data Message (ADM).
29   * @author Bryan Cazabonne
30   * @since 10.2
31   */
32  public class AemMetadata extends AdmMetadata {
33  
34      /** Endpoints (i.e. frames A, B and their relationship). */
35      private final AttitudeEndpoints endpoints;
36  
37      /** Start of total time span covered by attitude data. */
38      private AbsoluteDate startTime;
39  
40      /** End of total time span covered by attitude data. */
41      private AbsoluteDate stopTime;
42  
43      /** Start of useable time span covered by attitude data. */
44      private AbsoluteDate useableStartTime;
45  
46      /** End of useable time span covered by attitude data. */
47      private AbsoluteDate useableStopTime;
48  
49      /** The format of the data lines in the message. */
50      private AttitudeType attitudeType;
51  
52      /** The placement of the scalar portion of the quaternion (QC) in the attitude data. */
53      private Boolean isFirst;
54  
55      /** The rotation sequence of the Euler angles. */
56      private RotationOrder eulerRotSeq;
57  
58      /** The frame in which rates are specified (only for ADM V1). */
59      private Boolean rateFrameIsA;
60  
61      /** The frame in which angular velocities are specified.
62       * @since 12.0
63       */
64      private FrameFacade angvelFrame;
65  
66      /** The interpolation method to be used. */
67      private String interpolationMethod;
68  
69      /** The interpolation degree. */
70      private int interpolationDegree;
71  
72      /** Simple constructor.
73       * @param defaultInterpolationDegree default interpolation degree
74       */
75      public AemMetadata(final int defaultInterpolationDegree) {
76          endpoints           = new AttitudeEndpoints();
77          interpolationDegree = defaultInterpolationDegree;
78      }
79  
80      /** {@inheritDoc} */
81      @Override
82      public void validate(final double version) {
83  
84          super.validate(version);
85  
86          checkMandatoryEntriesExceptDatesAndExternalFrame(version);
87          endpoints.checkExternalFrame(AemMetadataKey.REF_FRAME_A, AemMetadataKey.REF_FRAME_B);
88  
89          checkNotNull(startTime, AemMetadataKey.START_TIME.name());
90          checkNotNull(stopTime,  AemMetadataKey.STOP_TIME.name());
91  
92          if (version >= 2.0 && isFirst()) {
93              throw new OrekitException(OrekitMessages.CCSDS_KEYWORD_NOT_ALLOWED_IN_VERSION,
94                                        AemMetadataKey.QUATERNION_TYPE, version);
95          }
96  
97      }
98  
99      /** Check is mandatory entries EXCEPT DATES AND EXTERNAL FRAME have been initialized.
100      * <p>
101      * Either frame A or frame B must be initialized with a {@link
102      * org.orekit.files.ccsds.definitions.SpacecraftBodyFrame spacecraft body frame}.
103      * </p>
104      * <p>
105      * This method should throw an exception if some mandatory entry is missing
106      * </p>
107      * @param version format version
108      */
109     void checkMandatoryEntriesExceptDatesAndExternalFrame(final double version) {
110 
111         super.validate(version);
112 
113         endpoints.checkMandatoryEntriesExceptExternalFrame(version,
114                                                            AemMetadataKey.REF_FRAME_A,
115                                                            AemMetadataKey.REF_FRAME_B,
116                                                            AemMetadataKey.ATTITUDE_DIR);
117 
118         checkNotNull(attitudeType, AemMetadataKey.ATTITUDE_TYPE.name());
119         if (version < 2.0) {
120             if (attitudeType == AttitudeType.QUATERNION ||
121                 attitudeType == AttitudeType.QUATERNION_DERIVATIVE) {
122                 checkNotNull(isFirst, AemMetadataKey.QUATERNION_TYPE.name());
123             }
124             if (attitudeType == AttitudeType.EULER_ANGLE_DERIVATIVE) {
125                 checkNotNull(rateFrameIsA, AemMetadataKey.RATE_FRAME.name());
126             }
127         } else {
128             if (attitudeType == AttitudeType.QUATERNION_ANGVEL) {
129                 checkNotNull(angvelFrame, AemMetadataKey.ANGVEL_FRAME.name());
130             }
131         }
132 
133         if (attitudeType == AttitudeType.EULER_ANGLE ||
134             attitudeType == AttitudeType.EULER_ANGLE_DERIVATIVE) {
135             checkNotNull(eulerRotSeq, AemMetadataKey.EULER_ROT_SEQ.name());
136         }
137 
138     }
139 
140     /** Get the endpoints (i.e. frames A, B and their relationship).
141      * @return endpoints
142      */
143     public AttitudeEndpoints getEndpoints() {
144         return endpoints;
145     }
146 
147     /** Check if rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}.
148      * @return true if rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}
149      */
150     public boolean rateFrameIsA() {
151         return rateFrameIsA == null ? false : rateFrameIsA;
152     }
153 
154     /** Set the frame in which rates are specified.
155      * @param rateFrameIsA if true, rates are specified in {@link AttitudeEndpoints#getFrameA() frame A}
156      */
157     public void setRateFrameIsA(final boolean rateFrameIsA) {
158         refuseFurtherComments();
159         this.rateFrameIsA = rateFrameIsA;
160     }
161 
162     /** Set frame in which angular velocities are specified.
163      * @param angvelFrame frame in which angular velocities are specified
164      * @since 12.0
165      */
166     public void setAngvelFrame(final FrameFacade angvelFrame) {
167         this.angvelFrame = angvelFrame;
168     }
169 
170     /** Get frame in which angular velocities are specified.
171      * @return frame in which angular velocities are specified
172      * @since 12.0
173      */
174     public FrameFacade getFrameAngvelFrame() {
175         return angvelFrame;
176     }
177 
178     /** Check if rates are specified in spacecraft body frame.
179      * <p>
180      * {@link #validate(double) Mandatory entries} must have been
181      * initialized properly to non-null values before this method is called,
182      * otherwise {@code NullPointerException} will be thrown.
183      * </p>
184      * @return true if rates are specified in spacecraft body frame
185      */
186     public boolean isSpacecraftBodyRate() {
187         return rateFrameIsA() ^ endpoints.getFrameA().asSpacecraftBodyFrame() == null;
188     }
189 
190     /**
191      * Get the format of the data lines in the message.
192      *
193      * @return the format of the data lines in the message
194      */
195     public AttitudeType getAttitudeType() {
196         return attitudeType;
197     }
198 
199     /**
200      * Set the format of the data lines in the message.
201      * @param type format to be set
202      */
203     public void setAttitudeType(final AttitudeType type) {
204         refuseFurtherComments();
205         this.attitudeType = type;
206     }
207 
208     /**
209      * Get the flag for the placement of the quaternion QC in the attitude data.
210      *
211      * @return true if QC is the first element in the attitude data,
212      * false if not initialized
213      */
214     public Boolean isFirst() {
215         return isFirst == null ? Boolean.FALSE : isFirst;
216     }
217 
218     /**
219      * Set the flag for the placement of the quaternion QC in the attitude data.
220      * @param isFirst true if QC is the first element in the attitude data
221      */
222     public void setIsFirst(final boolean isFirst) {
223         refuseFurtherComments();
224         this.isFirst = isFirst;
225     }
226 
227     /**
228      * Get the rotation order of Euler angles.
229      * @return rotation order
230      */
231     public RotationOrder getEulerRotSeq() {
232         return eulerRotSeq;
233     }
234 
235     /**
236      * Set the rotation order for Euler angles.
237      * @param eulerRotSeq order to be set
238      */
239     public void setEulerRotSeq(final RotationOrder eulerRotSeq) {
240         refuseFurtherComments();
241         this.eulerRotSeq = eulerRotSeq;
242     }
243 
244     /**
245      * Get start of total time span covered by attitude data.
246      * @return the start time
247      */
248     public AbsoluteDate getStartTime() {
249         return startTime;
250     }
251 
252     /**
253      * Set start of total time span covered by attitude data.
254      * @param startTime the time to be set
255      */
256     public void setStartTime(final AbsoluteDate startTime) {
257         refuseFurtherComments();
258         this.startTime = startTime;
259     }
260 
261     /**
262      * Get end of total time span covered by attitude data.
263      * @return the stop time
264      */
265     public AbsoluteDate getStopTime() {
266         return stopTime;
267     }
268 
269     /**
270      * Set end of total time span covered by attitude data.
271      * @param stopTime the time to be set
272      */
273     public void setStopTime(final AbsoluteDate stopTime) {
274         refuseFurtherComments();
275         this.stopTime = stopTime;
276     }
277 
278     /**
279      * Get start of useable time span covered by attitude data.
280      * @return the useable start time
281      */
282     public AbsoluteDate getUseableStartTime() {
283         return useableStartTime;
284     }
285 
286     /**
287      * Set start of useable time span covered by attitude data.
288      * @param useableStartTime the time to be set
289      */
290     public void setUseableStartTime(final AbsoluteDate useableStartTime) {
291         refuseFurtherComments();
292         this.useableStartTime = useableStartTime;
293     }
294 
295     /**
296      * Get end of useable time span covered by ephemerides data.
297      * @return the useable stop time
298      */
299     public AbsoluteDate getUseableStopTime() {
300         return useableStopTime;
301     }
302 
303     /**
304      * Set end of useable time span covered by ephemerides data.
305      * @param useableStopTime the time to be set
306      */
307     public void setUseableStopTime(final AbsoluteDate useableStopTime) {
308         refuseFurtherComments();
309         this.useableStopTime = useableStopTime;
310     }
311 
312     /**
313      * Get the start date of this ephemeris segment.
314      *
315      * @return ephemeris segment start date.
316      */
317     public AbsoluteDate getStart() {
318         // usable start time overrides start time if it is set
319         final AbsoluteDate start = this.getUseableStartTime();
320         if (start != null) {
321             return start;
322         } else {
323             return this.getStartTime();
324         }
325     }
326 
327     /**
328      * Get the end date of this ephemeris segment.
329      *
330      * @return ephemeris segment end date.
331      */
332     public AbsoluteDate getStop() {
333         // useable stop time overrides stop time if it is set
334         final AbsoluteDate stop = this.getUseableStopTime();
335         if (stop != null) {
336             return stop;
337         } else {
338             return this.getStopTime();
339         }
340     }
341 
342     /**
343      * Get the interpolation method to be used.
344      *
345      * @return the interpolation method
346      */
347     public String getInterpolationMethod() {
348         return interpolationMethod;
349     }
350 
351     /**
352      * Set the interpolation method to be used.
353      * @param interpolationMethod the interpolation method to be set
354      */
355     public void setInterpolationMethod(final String interpolationMethod) {
356         refuseFurtherComments();
357         this.interpolationMethod = interpolationMethod;
358     }
359 
360     /**
361      * Get the interpolation degree.
362      * @return the interpolation degree
363      */
364     public int getInterpolationDegree() {
365         return interpolationDegree;
366     }
367 
368     /**
369      * Set the interpolation degree.
370      * @param interpolationDegree the interpolation degree to be set
371      */
372     public void setInterpolationDegree(final int interpolationDegree) {
373         refuseFurtherComments();
374         this.interpolationDegree = interpolationDegree;
375     }
376 
377     /**
378      * Get the number of samples to use in interpolation.
379      *
380      * @return the number of points to use for interpolation.
381      */
382     public int getInterpolationSamples() {
383         // From the standard it is not entirely clear how to interpret the degree.
384         return getInterpolationDegree() + 1;
385     }
386 
387     /** Copy the instance, making sure mandatory fields have been initialized.
388      * @param version format version
389      * @return a new copy
390      */
391     AemMetadata copy(final double version) {
392 
393         checkMandatoryEntriesExceptDatesAndExternalFrame(version);
394 
395         // allocate new instance
396         final AemMetadata copy = new AemMetadata(getInterpolationDegree());
397 
398         // copy comments
399         for (String comment : getComments()) {
400             copy.addComment(comment);
401         }
402 
403         // copy object
404         copy.setObjectName(getObjectName());
405         copy.setObjectID(getObjectID());
406         if (getCenter() != null) {
407             copy.setCenter(getCenter());
408         }
409 
410         // copy frames (we may copy null references here)
411         copy.getEndpoints().setFrameA(getEndpoints().getFrameA());
412         copy.getEndpoints().setFrameB(getEndpoints().getFrameB());
413         copy.getEndpoints().setA2b(getEndpoints().isA2b());
414         copy.setRateFrameIsA(rateFrameIsA());
415 
416         // copy time system only (ignore times themselves)
417         copy.setTimeSystem(getTimeSystem());
418 
419         // copy attitude definitions
420         copy.setAttitudeType(getAttitudeType());
421         if (isFirst() != null) {
422             copy.setIsFirst(isFirst());
423         }
424         if (getEulerRotSeq() != null) {
425             copy.setEulerRotSeq(getEulerRotSeq());
426         }
427 
428         // copy interpolation (degree has already been set up at construction)
429         if (getInterpolationMethod() != null) {
430             copy.setInterpolationMethod(getInterpolationMethod());
431         }
432 
433         return copy;
434 
435     }
436 
437 }