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