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