1   /* Copyright 2002-2025 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.hipparchus.geometry.euclidean.threed.Vector3D;
23  import org.orekit.attitudes.Attitude;
24  import org.orekit.attitudes.AttitudeBuilder;
25  import org.orekit.attitudes.FieldAttitude;
26  import org.orekit.errors.OrekitException;
27  import org.orekit.errors.OrekitMessages;
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      /** Frame A. */
63      private FrameFacade frameA;
64  
65      /** Frame B. */
66      private FrameFacade frameB;
67  
68      /** Flag for frames direction. */
69      private Boolean a2b;
70  
71      /** Empty constructor.
72       * <p>
73       * This constructor is not strictly necessary, but it prevents spurious
74       * javadoc warnings with JDK 18 and later.
75       * </p>
76       * @since 12.0
77       */
78      public AttitudeEndpoints() {
79          // nothing to do
80      }
81  
82      /** Complain if a field is null.
83       * @param field field to check
84       * @param key key associated with the field
85       */
86      private void checkNotNull(final Object field, final Enum<?> key) {
87          if (field == null) {
88              throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, key.name());
89          }
90      }
91      /** Check external frame is properly initialized.
92       * @param aKey key for frame A
93       * @param bKey key for frame B
94       */
95      public void checkExternalFrame(final Enum<?> aKey, final Enum<?> bKey) {
96          checkNotNull(frameA, aKey);
97          checkNotNull(frameB, bKey);
98          if (frameA.asSpacecraftBodyFrame() != null && frameB.asSpacecraftBodyFrame() != null) {
99              // we cannot have two spacecraft body frames
100             throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, frameB.getName());
101         }
102     }
103 
104     /** Check is mandatory entries <em>except external frame</em> have been initialized.
105      * <p>
106      * Either frame A or frame B must be initialized with a {@link
107      * org.orekit.files.ccsds.definitions.SpacecraftBodyFrame spacecraft body frame}.
108      * </p>
109      * <p>
110      * This method should throw an exception if some mandatory entry is missing
111      * </p>
112      * @param version format version
113      * @param aKey key for frame A
114      * @param bKey key for frame B
115      * @param dirKey key for direction
116      */
117     public void checkMandatoryEntriesExceptExternalFrame(final double version,
118                                                          final Enum<?> aKey, final Enum<?> bKey,
119                                                          final Enum<?> dirKey) {
120 
121         if (frameA == null) {
122             if (frameB == null || frameB.asSpacecraftBodyFrame() == null) {
123                 throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, aKey.name());
124             }
125         } else if (frameA.asSpacecraftBodyFrame() == null) {
126             if (frameB == null) {
127                 throw new OrekitException(OrekitMessages.UNINITIALIZED_VALUE_FOR_KEY, bKey.name());
128             } else if (frameB.asSpacecraftBodyFrame() == null) {
129                 // at least one of the frame must be a spacecraft body frame
130                 throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, frameB.getName());
131             }
132         }
133 
134         if (version < 2.0) {
135             // in ADM version 1, direction is mandatory
136             checkNotNull(a2b, dirKey);
137         } else if (!isA2b()) {
138             // in ADM version 2, direction is always A → B
139             throw new OrekitException(OrekitMessages.CCSDS_KEYWORD_NOT_ALLOWED_IN_VERSION,
140                                       dirKey, version);
141         }
142 
143     }
144 
145     /** Set frame A.
146      * @param frameA frame A
147      */
148     public void setFrameA(final FrameFacade frameA) {
149         this.frameA = frameA;
150     }
151 
152     /** Get frame A.
153      * @return frame A
154      */
155     public FrameFacade getFrameA() {
156         return frameA;
157     }
158 
159     /** Set frame B.
160      * @param frameB frame B
161      */
162     public void setFrameB(final FrameFacade frameB) {
163         this.frameB = frameB;
164     }
165 
166     /** Get frame B.
167      * @return frame B
168      */
169     public FrameFacade getFrameB() {
170         return frameB;
171     }
172 
173     /** Set rotation direction.
174      * @param a2b if true, rotation is from {@link #getFrameA() frame A}
175      * to {@link #getFrameB() frame B}
176      */
177     public void setA2b(final boolean a2b) {
178         this.a2b = a2b;
179     }
180 
181     /** Check if rotation direction is from {@link #getFrameA() frame A} to {@link #getFrameB() frame B}.
182      * @return true if rotation direction is from {@link #getFrameA() frame A} to {@link #getFrameB() frame B}
183      */
184     public boolean isA2b() {
185         return a2b == null ? true : a2b;
186     }
187 
188     /** Get the external frame.
189      * @return external frame
190      */
191     public FrameFacade getExternalFrame() {
192         return frameA.asSpacecraftBodyFrame() == null ? frameA : frameB;
193     }
194 
195     /** Get the spacecraft body frame.
196      * @return spacecraft body frame
197      */
198     public FrameFacade getSpacecraftBodyFrame() {
199         return frameA.asSpacecraftBodyFrame() == null ? frameB : frameA;
200     }
201 
202     /** Check if attitude is from external frame to spacecraft body frame.
203      * <p>
204      * {@link #checkMandatoryEntriesExceptExternalFrame(double, Enum, Enum, Enum)
205      * Mandatory entries} must have been initialized properly to non-null
206      * values before this method is called, otherwise {@code NullPointerException}
207      * will be thrown.
208      * </p>
209      * @return true if attitude is from external frame to spacecraft body frame
210      */
211     public boolean isExternal2SpacecraftBody() {
212         return isA2b() ^ frameB.asSpacecraftBodyFrame() == null;
213     }
214 
215     /** Check if a endpoint is compatible with another one.
216      * <p>
217      * Endpoins are compatible if they refer o the same frame names,
218      * in the same order and in the same direction.
219      * </p>
220      * @param other other endpoints to check against
221      * @return true if both endpoints are compatible with each other
222      */
223     public boolean isCompatibleWith(final AttitudeEndpoints other) {
224         return frameA.getName().equals(other.frameA.getName()) &&
225                frameB.getName().equals(other.frameB.getName()) &&
226                a2b.equals(other.a2b);
227     }
228 
229     /**  {@inheritDoc} */
230     @Override
231     public Attitude build(final Frame frame, final PVCoordinatesProvider pvProv,
232                           final TimeStampedAngularCoordinates rawAttitude) {
233 
234         // attitude converted to Orekit conventions
235         final TimeStampedAngularCoordinates att =
236                         isExternal2SpacecraftBody() ? rawAttitude : rawAttitude.revert();
237 
238         final FrameFacade        external = getExternalFrame();
239         final OrbitRelativeFrame orf      = external.asOrbitRelativeFrame();
240         if (orf != null) {
241             // this is an orbit-relative attitude
242             if (orf.getLofType() == null) {
243                 throw new OrekitException(OrekitMessages.UNSUPPORTED_LOCAL_ORBITAL_FRAME, external.getName());
244             }
245 
246             // construction of the local orbital frame, using PV from reference frame
247             final PVCoordinates pv = pvProv.getPVCoordinates(rawAttitude.getDate(), frame);
248             final AngularCoordinates frame2Lof =
249                             orf.isQuasiInertial() ?
250                             new AngularCoordinates(orf.getLofType().rotationFromInertial(pv), Vector3D.ZERO) :
251                             orf.getLofType().transformFromInertial(att.getDate(), pv).getAngular();
252 
253             // compose with APM
254             return new Attitude(frame, att.addOffset(frame2Lof));
255 
256         } else {
257             // this is an absolute attitude
258             if (external.asFrame() == null) {
259                 // unknown frame
260                 throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, external.getName());
261             }
262             final Attitude attitude = new Attitude(external.asFrame(), att);
263             return frame == null ? attitude : attitude.withReferenceFrame(frame);
264         }
265 
266     }
267 
268     /**  {@inheritDoc} */
269     @Override
270     public <T extends CalculusFieldElement<T>>
271         FieldAttitude<T> build(final Frame frame, final FieldPVCoordinatesProvider<T> pvProv,
272                                final TimeStampedFieldAngularCoordinates<T> rawAttitude) {
273 
274         // attitude converted to Orekit conventions
275         final TimeStampedFieldAngularCoordinates<T> att =
276                         isExternal2SpacecraftBody() ? rawAttitude : rawAttitude.revert();
277 
278         final FrameFacade        external = getExternalFrame();
279         final OrbitRelativeFrame orf      = external.asOrbitRelativeFrame();
280         if (orf != null) {
281             // this is an orbit-relative attitude
282             if (orf.getLofType() == null) {
283                 throw new OrekitException(OrekitMessages.UNSUPPORTED_LOCAL_ORBITAL_FRAME, external.getName());
284             }
285 
286             // construction of the local orbital frame, using PV from reference frame
287             final FieldPVCoordinates<T> pv = pvProv.getPVCoordinates(rawAttitude.getDate(), frame);
288             final Field<T> field = rawAttitude.getDate().getField();
289             final FieldAngularCoordinates<T> referenceToLof =
290                             orf.isQuasiInertial() ?
291                             new FieldAngularCoordinates<>(orf.getLofType().rotationFromInertial(field, pv),
292                                                           FieldVector3D.getZero(field)) :
293                             orf.getLofType().transformFromInertial(att.getDate(), pv).getAngular();
294 
295             // compose with APM
296             return new FieldAttitude<>(frame, att.addOffset(referenceToLof));
297 
298         } else {
299             // this is an absolute attitude
300             if (external.asFrame() == null) {
301                 // this should never happen as all CelestialBodyFrame have an Orekit mapping
302                 throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, external.getName());
303             }
304             final FieldAttitude<T> attitude = new FieldAttitude<>(external.asFrame(), att);
305             return frame == null ? attitude : attitude.withReferenceFrame(frame);
306         }
307 
308     }
309 
310     /** {@inheritDoc} */
311     @Override
312     public String toString() {
313         return frameA.getName() + (isA2b() ? " → " : " ← ") + frameB.getName();
314     }
315 
316 }