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 }