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;
18  
19  import java.util.Optional;
20  
21  import org.hipparchus.CalculusFieldElement;
22  import org.hipparchus.Field;
23  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
24  import org.orekit.annotation.Nullable;
25  import org.orekit.attitudes.Attitude;
26  import org.orekit.attitudes.AttitudeBuilder;
27  import org.orekit.attitudes.FieldAttitude;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
31  import org.orekit.files.ccsds.definitions.FrameFacade;
32  import org.orekit.files.ccsds.definitions.OrbitRelativeFrame;
33  import org.orekit.frames.Frame;
34  import org.orekit.utils.AngularCoordinates;
35  import org.orekit.utils.FieldAngularCoordinates;
36  import org.orekit.utils.FieldPVCoordinates;
37  import org.orekit.utils.FieldPVCoordinatesProvider;
38  import org.orekit.utils.PVCoordinates;
39  import org.orekit.utils.PVCoordinatesProvider;
40  import org.orekit.utils.TimeStampedAngularCoordinates;
41  import org.orekit.utils.TimeStampedFieldAngularCoordinates;
42  
43  /** Endpoints for attitude definition.
44   * <p>
45   * This class provides a bridge between two different views of attitude definition.
46   * In both views, there is an external frame, based on either celestial body or orbit-relative
47   * and there is a spacecraft body frame.
48   * <ul>
49   *   <li>CCSDS ADM view: frames are labeled as A and B but nothing tells which is which
50   *   and attitude can be defined in any direction</li>
51   *   <li>{@link Attitude Orekit attitude} view: attitude is always from external to
52   *   spacecraft body</li>
53   * </ul>
54   * @author Luc Maisonobe
55   * @since 11.0
56   */
57  public class AttitudeEndpoints implements AttitudeBuilder {
58  
59      /** Constant for A → B diraction. */
60      public static final String A2B = "A2B";
61  
62      /** Constant for A ← B direction. */
63      public static final String B2A = "B2A";
64  
65      /** For creating a {@link Frame}. */
66      private final CcsdsFrameMapper frameMapper;
67  
68      /** Frame A. */
69      private FrameFacade frameA;
70  
71      /** Frame B. */
72      private FrameFacade frameB;
73  
74      /** Flag for frames direction. */
75      @Nullable
76      private Boolean a2b;
77  
78      /**
79       * Simple constructor.
80       *
81       * @param frameMapper for creating a {@link Frame}.
82       * @since 13.1.5
83       */
84      public AttitudeEndpoints(final CcsdsFrameMapper frameMapper) {
85          this.frameMapper = frameMapper;
86      }
87  
88      /** Complain if a field is null.
89       * @param field field to check
90       * @param key key associated with the field
91       */
92      private void checkNotNull(final Object field, final Enum<?> key) {
93          if (field == null) {
94              throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, key.name());
95          }
96      }
97      /** Check external frame is properly initialized.
98       * @param aKey key for frame A
99       * @param bKey key for frame B
100      */
101     public void checkExternalFrame(final Enum<?> aKey, final Enum<?> bKey) {
102         checkNotNull(frameA, aKey);
103         checkNotNull(frameB, bKey);
104         if (frameA.asSpacecraftBodyFrame().isPresent() && frameB.asSpacecraftBodyFrame().isPresent()) {
105             // we cannot have two spacecraft body frames
106             throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, frameB.getName());
107         }
108     }
109 
110     /** Check is mandatory entries <em>except external frame</em> have been initialized.
111      * <p>
112      * Either frame A or frame B must be initialized with a {@link
113      * org.orekit.files.ccsds.definitions.SpacecraftBodyFrame spacecraft body frame}.
114      * </p>
115      * <p>
116      * This method should throw an exception if some mandatory entry is missing
117      * </p>
118      * @param version format version
119      * @param aKey key for frame A
120      * @param bKey key for frame B
121      * @param dirKey key for direction
122      */
123     public void checkMandatoryEntriesExceptExternalFrame(final double version,
124                                                          final Enum<?> aKey, final Enum<?> bKey,
125                                                          final Enum<?> dirKey) {
126 
127         if (frameA == null) {
128             if (frameB == null || frameB.asSpacecraftBodyFrame().isEmpty()) {
129                 throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, aKey.name());
130             }
131         } else if (frameA.asSpacecraftBodyFrame().isEmpty()) {
132             if (frameB == null) {
133                 throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, bKey.name());
134             } else if (frameB.asSpacecraftBodyFrame().isEmpty()) {
135                 // at least one of the frame must be a spacecraft body frame
136                 throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, frameB.getName());
137             }
138         }
139 
140         if (version < 2.0) {
141             // in ADM version 1, direction is mandatory
142             checkNotNull(a2b, dirKey);
143         } else if (!isA2b()) {
144             // in ADM version 2, direction is always A → B
145             throw new OrekitException(OrekitMessages.CCSDS_KEYWORD_NOT_ALLOWED_IN_VERSION,
146                                       dirKey, version);
147         }
148 
149     }
150 
151     /** Set frame A.
152      * @param frameA frame A
153      */
154     public void setFrameA(final FrameFacade frameA) {
155         this.frameA = frameA;
156     }
157 
158     /** Get frame A.
159      * @return frame A
160      */
161     public FrameFacade getFrameA() {
162         return frameA;
163     }
164 
165     /** Set frame B.
166      * @param frameB frame B
167      */
168     public void setFrameB(final FrameFacade frameB) {
169         this.frameB = frameB;
170     }
171 
172     /** Get frame B.
173      * @return frame B
174      */
175     public FrameFacade getFrameB() {
176         return frameB;
177     }
178 
179     /** Set rotation direction.
180      * @param a2b if true, rotation is from {@link #getFrameA() frame A}
181      * to {@link #getFrameB() frame B}
182      */
183     public void setA2b(final boolean a2b) {
184         this.a2b = a2b;
185     }
186 
187     /** Check if rotation direction is from {@link #getFrameA() frame A} to {@link #getFrameB() frame B}.
188      * @return true if rotation direction is from {@link #getFrameA() frame A} to {@link #getFrameB() frame B}
189      */
190     public boolean isA2b() {
191         return a2b == null ? true : a2b;
192     }
193 
194     /** Get the external frame.
195      * @return external frame
196      * @see #getExternal()
197      */
198     public FrameFacade getExternalFrame() {
199         return frameA.asSpacecraftBodyFrame().isEmpty() ? frameA : frameB;
200     }
201 
202     /**
203      * Get the mapping between a CCSDS frame and a {@link Frame}.
204      *
205      * @return the frame mapper.
206      * @since 13.1.5
207      */
208     public CcsdsFrameMapper getFrameMapper() {
209         return frameMapper;
210     }
211 
212     /**
213      * Get the external reference frame. Only the orientation is significant.
214      *
215      * @return the external frame.
216      * @see #getExternalFrame()
217      * @since 13.1.5
218      */
219     public Frame getExternal() {
220         // no reference frame epoch
221         return getFrameMapper().buildCcsdsFrame(getExternalFrame(), null);
222     }
223 
224     /** Get the spacecraft body frame.
225      * @return spacecraft body frame
226      */
227     public FrameFacade getSpacecraftBodyFrame() {
228         return frameA.asSpacecraftBodyFrame().isEmpty() ? frameB : frameA;
229     }
230 
231     /** Check if attitude is from external frame to spacecraft body frame.
232      * <p>
233      * {@link #checkMandatoryEntriesExceptExternalFrame(double, Enum, Enum, Enum)
234      * Mandatory entries} must have been initialized properly to non-null
235      * values before this method is called, otherwise {@code NullPointerException}
236      * will be thrown.
237      * </p>
238      * @return true if attitude is from external frame to spacecraft body frame
239      */
240     public boolean isExternal2SpacecraftBody() {
241         return isA2b() ^ frameB.asSpacecraftBodyFrame().isEmpty();
242     }
243 
244     /** Check if a endpoint is compatible with another one.
245      * <p>
246      * Endpoins are compatible if they refer o the same frame names,
247      * in the same order and in the same direction.
248      * </p>
249      * @param other other endpoints to check against
250      * @return true if both endpoints are compatible with each other
251      */
252     public boolean isCompatibleWith(final AttitudeEndpoints other) {
253         return frameA.getName().equals(other.frameA.getName()) &&
254                frameB.getName().equals(other.frameB.getName()) &&
255                a2b.equals(other.a2b);
256     }
257 
258     /**  {@inheritDoc} */
259     @Override
260     public Attitude build(final Frame frame, final PVCoordinatesProvider pvProv,
261                           final TimeStampedAngularCoordinates rawAttitude) {
262 
263         // attitude converted to Orekit conventions
264         final TimeStampedAngularCoordinates att =
265                         isExternal2SpacecraftBody() ? rawAttitude : rawAttitude.revert();
266 
267         final FrameFacade                  external = getExternalFrame();
268         final Optional<OrbitRelativeFrame> orf      = external.asOrbitRelativeFrame();
269         if (orf.isPresent()) {
270             // this is an orbit-relative attitude
271             if (orf.get().getLofType() == null) {
272                 throw new OrekitException(OrekitMessages.UNSUPPORTED_LOCAL_ORBITAL_FRAME, external.getName());
273             }
274 
275             // construction of the local orbital frame, using PV from reference frame
276             final PVCoordinates pv = pvProv.getPVCoordinates(rawAttitude.getDate(), frame);
277             final AngularCoordinates frame2Lof =
278                             orf.get().isQuasiInertial() ?
279                             new AngularCoordinates(orf.get().getLofType().rotationFromInertial(pv)) :
280                             orf.get().getLofType().transformFromInertial(att.getDate(), pv).getAngular();
281 
282             // compose with APM
283             return new Attitude(frame, att.addOffset(frame2Lof));
284 
285         } else {
286             // this is an absolute attitude
287             final Frame externalFrame = external.
288                                         asFrame().
289                                         orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME,
290                                                                               external.getName()));
291             final Attitude attitude = new Attitude(externalFrame, att);
292             return frame == null ? attitude : attitude.withReferenceFrame(frame);
293         }
294 
295     }
296 
297     /**  {@inheritDoc} */
298     @Override
299     public <T extends CalculusFieldElement<T>>
300         FieldAttitude<T> build(final Frame frame, final FieldPVCoordinatesProvider<T> pvProv,
301                                final TimeStampedFieldAngularCoordinates<T> rawAttitude) {
302 
303         // attitude converted to Orekit conventions
304         final TimeStampedFieldAngularCoordinates<T> att =
305                         isExternal2SpacecraftBody() ? rawAttitude : rawAttitude.revert();
306 
307         final FrameFacade        external = getExternalFrame();
308         final Optional<OrbitRelativeFrame> orf      = external.asOrbitRelativeFrame();
309         if (orf.isPresent()) {
310             // this is an orbit-relative attitude
311             if (orf.get().getLofType() == null) {
312                 throw new OrekitException(OrekitMessages.UNSUPPORTED_LOCAL_ORBITAL_FRAME, external.getName());
313             }
314 
315             // construction of the local orbital frame, using PV from reference frame
316             final FieldPVCoordinates<T> pv = pvProv.getPVCoordinates(rawAttitude.getDate(), frame);
317             final Field<T> field = rawAttitude.getDate().getField();
318             final FieldAngularCoordinates<T> referenceToLof =
319                             orf.get().isQuasiInertial() ?
320                             new FieldAngularCoordinates<>(orf.get().getLofType().rotationFromInertial(field, pv),
321                                                           FieldVector3D.getZero(field)) :
322                             orf.get().getLofType().transformFromInertial(att.getDate(), pv).getAngular();
323 
324             // compose with APM
325             return new FieldAttitude<>(frame, att.addOffset(referenceToLof));
326 
327         } else {
328             // this is an absolute attitude
329             // exception should never be thrown as all CelestialBodyFrame have an Orekit mapping
330             final Frame externalFrame = external.
331                                         asFrame().
332                                         orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME,
333                                                                               external.getName()));
334             final FieldAttitude<T> attitude = new FieldAttitude<>(externalFrame, att);
335             return frame == null ? attitude : attitude.withReferenceFrame(frame);
336         }
337 
338     }
339 
340     /** {@inheritDoc} */
341     @Override
342     public String toString() {
343         return frameA.getName() + (isA2b() ? " → " : " ← ") + frameB.getName();
344     }
345 
346 }