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