1   /* Copyright 2002-2024 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.attitudes;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.List;
22  
23  import org.hipparchus.CalculusFieldElement;
24  import org.hipparchus.Field;
25  import org.hipparchus.geometry.euclidean.threed.FieldRotation;
26  import org.hipparchus.geometry.euclidean.threed.Rotation;
27  import org.hipparchus.ode.events.Action;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.frames.Frame;
31  import org.orekit.orbits.Orbit;
32  import org.orekit.propagation.FieldPropagator;
33  import org.orekit.propagation.FieldSpacecraftState;
34  import org.orekit.propagation.Propagator;
35  import org.orekit.propagation.SpacecraftState;
36  import org.orekit.propagation.events.AdaptableInterval;
37  import org.orekit.propagation.events.EventDetector;
38  import org.orekit.propagation.events.FieldAdaptableInterval;
39  import org.orekit.propagation.events.FieldEventDetector;
40  import org.orekit.propagation.events.handlers.EventHandler;
41  import org.orekit.propagation.events.handlers.FieldEventHandler;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.time.FieldAbsoluteDate;
44  import org.orekit.time.FieldTimeInterpolator;
45  import org.orekit.time.TimeInterpolator;
46  import org.orekit.utils.AngularDerivativesFilter;
47  import org.orekit.utils.DoubleArrayDictionary;
48  import org.orekit.utils.FieldPVCoordinatesProvider;
49  import org.orekit.utils.PVCoordinatesProvider;
50  import org.orekit.utils.TimeSpanMap;
51  import org.orekit.utils.TimeStampedAngularCoordinates;
52  import org.orekit.utils.TimeStampedAngularCoordinatesHermiteInterpolator;
53  import org.orekit.utils.TimeStampedFieldAngularCoordinates;
54  import org.orekit.utils.TimeStampedFieldAngularCoordinatesHermiteInterpolator;
55  
56  /** This classes manages a sequence of different attitude providers that are activated
57   * in turn according to switching events.
58   * <p>Only one attitude provider in the sequence is in an active state. When one of
59   * the switch event associated with the active provider occurs, the active provider becomes
60   * the one specified with the event. A simple example is a provider for the sun lighted part
61   * of the orbit and another provider for the eclipse time. When the sun lighted provider is active,
62   * the eclipse entry event is checked and when it occurs the eclipse provider is activated.
63   * When the eclipse provider is active, the eclipse exit event is checked and when it occurs
64   * the sun lighted provider is activated again. This sequence is a simple loop.</p>
65   * <p>An active attitude provider may have several switch events and next provider settings, leading
66   * to different activation patterns depending on which events are triggered first. An example
67   * of this feature is handling switches to safe mode if some contingency condition is met, in
68   * addition to the nominal switches that correspond to proper operations. Another example
69   * is handling of maneuver mode.
70   * <p>
71   * Note that this attitude provider is stateful, it keeps in memory the sequence of active
72   * underlying providers with their switch dates and the transitions from one provider to
73   * the other. This implies that this provider should <em>not</em> be shared among different
74   * propagators at the same time, each propagator should use its own instance of this provider.
75   * <p>
76   * The sequence kept in memory is reset when {@link #resetActiveProvider(AttitudeProvider)}
77   * is called, and only the specify provider is kept. The sequence is also partially
78   * reset each time a propagation starts. If a new propagation is started after a first
79   * propagation has been run, all the already computed switches that occur after propagation
80   * start for forward propagation or before propagation start for backward propagation will
81   * be erased. New switches will be computed and applied properly according to the new
82   * propagation settings. The already computed switches that are not in covered are kept
83   * in memory. This implies that if a propagation is interrupted and restarted in the
84   * same direction, then attitude switches will remain in place, ensuring that even if the
85   * interruption occurred in the middle of an attitude transition the second propagation will
86   * properly complete the transition that was started by the first propagator.
87   * </p>
88   * @author Luc Maisonobe
89   * @since 5.1
90   */
91  public class AttitudesSequence implements AttitudeProvider {
92  
93      /** Providers that have been activated. */
94      private transient TimeSpanMap<AttitudeProvider> activated;
95  
96      /** Switching events list. */
97      private final List<Switch> switches;
98  
99      /** Constructor for an initially empty sequence.
100      */
101     public AttitudesSequence() {
102         activated = null;
103         switches  = new ArrayList<>();
104     }
105 
106     /** Reset the active provider.
107      * <p>
108      * Calling this method clears all already seen switch history,
109      * so it should <em>not</em> be used during the propagation itself,
110      * it is intended to be used only at start
111      * </p>
112      * @param provider provider to activate
113      */
114     public void resetActiveProvider(final AttitudeProvider provider) {
115         activated = new TimeSpanMap<>(provider);
116     }
117 
118     /** Register all wrapped switch events to the propagator.
119      * <p>
120      * This method must be called once before propagation, after the
121      * switching conditions have been set up by calls to {@link
122      * #addSwitchingCondition(AttitudeProvider, AttitudeProvider, EventDetector,
123      * boolean, boolean, double, AngularDerivativesFilter, SwitchHandler)
124      * addSwitchingCondition}.
125      * </p>
126      * @param propagator propagator that will handle the events
127      */
128     public void registerSwitchEvents(final Propagator propagator) {
129         for (final Switch s : switches) {
130             propagator.addEventDetector(s);
131         }
132     }
133 
134     /** Register all wrapped switch events to the propagator.
135      * <p>
136      * This method must be called once before propagation, after the
137      * switching conditions have been set up by calls to {@link
138      * #addSwitchingCondition(AttitudeProvider, AttitudeProvider, EventDetector,
139      * boolean, boolean, double, AngularDerivativesFilter, SwitchHandler)
140      * addSwitchingCondition}.
141      * </p>
142      * @param field field to which the elements belong
143      * @param propagator propagator that will handle the events
144      * @param <T> type of the field elements
145      */
146     public <T extends CalculusFieldElement<T>> void registerSwitchEvents(final Field<T> field, final FieldPropagator<T> propagator) {
147         for (final Switch sw : switches) {
148             propagator.addEventDetector(new FieldEventDetector<T>() {
149 
150                 /** {@inheritDoc} */
151                 @Override
152                 public void init(final FieldSpacecraftState<T> s0,
153                                  final FieldAbsoluteDate<T> t) {
154                     sw.init(s0.toSpacecraftState(), t.toAbsoluteDate());
155                 }
156 
157                 /** {@inheritDoc} */
158                 @Override
159                 public T g(final FieldSpacecraftState<T> s) {
160                     return field.getZero().newInstance(sw.g(s.toSpacecraftState()));
161                 }
162 
163                 /** {@inheritDoc} */
164                 @Override
165                 public T getThreshold() {
166                     return field.getZero().newInstance(sw.getThreshold());
167                 }
168 
169                 /** {@inheritDoc} */
170                 @Override
171                 public FieldAdaptableInterval<T> getMaxCheckInterval() {
172                     return s -> sw.getMaxCheckInterval().currentInterval(s.toSpacecraftState());
173                 }
174 
175                 /** {@inheritDoc} */
176                 @Override
177                 public int getMaxIterationCount() {
178                     return sw.getMaxIterationCount();
179                 }
180 
181                 /** {@inheritDoc} */
182                 @Override
183                 public FieldEventHandler<T> getHandler() {
184                     return new FieldEventHandler<T>() {
185                         /** {@inheritDoc} */
186                         @Override
187                         public Action eventOccurred(final FieldSpacecraftState<T> s,
188                                                     final FieldEventDetector<T> detector,
189                                                     final boolean increasing) {
190                             return sw.eventOccurred(s.toSpacecraftState(), sw, increasing);
191                         }
192 
193                         /** {@inheritDoc} */
194                         @Override
195                         public FieldSpacecraftState<T> resetState(final FieldEventDetector<T> detector,
196                                                                   final FieldSpacecraftState<T> oldState) {
197                             return new FieldSpacecraftState<>(field, sw.resetState(sw, oldState.toSpacecraftState()));
198                         }
199                     };
200                 }
201 
202             });
203         }
204     }
205 
206     /** Add a switching condition between two attitude providers.
207      * <p>
208      * The {@code past} and {@code future} attitude providers are defined with regard
209      * to the natural flow of time. This means that if the propagation is forward, the
210      * propagator will switch from {@code past} provider to {@code future} provider at
211      * event occurrence, but if the propagation is backward, the propagator will switch
212      * from {@code future} provider to {@code past} provider at event occurrence. The
213      * transition between the two attitude laws is not instantaneous, the switch event
214      * defines the start of the transition (i.e. when leaving the {@code past} attitude
215      * law and entering the interpolated transition law). The end of the transition
216      * (i.e. when leaving the interpolating transition law and entering the {@code future}
217      * attitude law) occurs at switch time plus {@code transitionTime}.
218      * </p>
219      * <p>
220      * An attitude provider may have several different switch events associated to
221      * it. Depending on which event is triggered, the appropriate provider is
222      * switched to.
223      * </p>
224      * <p>
225      * The switch events specified here must <em>not</em> be registered to the
226      * propagator directly. The proper way to register these events is to
227      * call {@link #registerSwitchEvents(Propagator)} once after all switching
228      * conditions have been set up. The reason for this is that the events will
229      * be wrapped before being registered.
230      * </p>
231      * <p>
232      * If the underlying detector has an event handler associated to it, this handler
233      * will be triggered (i.e. its {@link org.orekit.propagation.events.handlers.EventHandler#eventOccurred(SpacecraftState,
234      * EventDetector, boolean) eventOccurred} method will be called), <em>regardless</em>
235      * of the event really triggering an attitude switch or not. As an example, if an
236      * eclipse detector is used to switch from day to night attitude mode when entering
237      * eclipse, with {@code switchOnIncrease} set to {@code false} and {@code switchOnDecrease}
238      * set to {@code true}. Then a handler set directly at eclipse detector level would
239      * be triggered at both eclipse entry and eclipse exit, but attitude switch would
240      * occur <em>only</em> at eclipse entry. Note that for the sake of symmetry, the
241      * transition start and end dates should match for both forward and backward propagation.
242      * This implies that for backward propagation, we have to compensate for the {@code
243      * transitionTime} when looking for the event. An unfortunate consequence is that the
244      * {@link org.orekit.propagation.events.handlers.EventHandler#eventOccurred(SpacecraftState, EventDetector, boolean)
245      * eventOccurred} method may appear to be called out of sync with respect to the
246      * propagation (it will be called when propagator reaches transition end, despite it
247      * refers to transition start, as per {@code transitionTime} compensation), and if the
248      * method returns {@link Action#STOP}, it will stop at the end of the
249      * transition instead of at the start. For these reasons, it is not recommended to
250      * set up an event handler for events that are used to switch attitude. If an event
251      * handler is needed for other purposes, a second handler should be registered to
252      * the propagator rather than relying on the side effects of attitude switches.
253      * </p>
254      * <p>
255      * The smoothness of the transition between past and future attitude laws can be tuned
256      * using the {@code transitionTime} and {@code transitionFilter} parameters. The {@code
257      * transitionTime} parameter specifies how much time is spent to switch from one law to
258      * the other law. It should be larger than the event {@link EventDetector#getThreshold()
259      * convergence threshold} in order to ensure attitude continuity. The {@code
260      * transitionFilter} parameter specifies the attitude time derivatives that should match
261      * at the boundaries between past attitude law and transition law on one side, and
262      * between transition law and future law on the other side.
263      * {@link AngularDerivativesFilter#USE_R} means only the rotation should be identical,
264      * {@link AngularDerivativesFilter#USE_RR} means both rotation and rotation rate
265      * should be identical, {@link AngularDerivativesFilter#USE_RRA} means both rotation,
266      * rotation rate and rotation acceleration should be identical. During the transition,
267      * the attitude law is computed by interpolating between past attitude law at switch time
268      * and future attitude law at current intermediate time.
269      * </p>
270      * @param past attitude provider applicable for times in the switch event occurrence past
271      * @param future attitude provider applicable for times in the switch event occurrence future
272      * @param switchEvent event triggering the attitude providers switch
273      * @param switchOnIncrease if true, switch is triggered on increasing event
274      * @param switchOnDecrease if true, switch is triggered on decreasing event
275      * @param transitionTime duration of the transition between the past and future attitude laws
276      * @param transitionFilter specification of transition law time derivatives that
277      * should match past and future attitude laws
278      * @param handler handler to call for notifying when switch occurs (may be null)
279      * @param <T> class type for the switch event
280      * @since 7.1
281      */
282     public <T extends EventDetector> void addSwitchingCondition(final AttitudeProvider past,
283                                                                 final AttitudeProvider future,
284                                                                 final T switchEvent,
285                                                                 final boolean switchOnIncrease,
286                                                                 final boolean switchOnDecrease,
287                                                                 final double transitionTime,
288                                                                 final AngularDerivativesFilter transitionFilter,
289                                                                 final SwitchHandler handler) {
290 
291         // safety check, for ensuring attitude continuity
292         if (transitionTime < switchEvent.getThreshold()) {
293             throw new OrekitException(OrekitMessages.TOO_SHORT_TRANSITION_TIME_FOR_ATTITUDES_SWITCH,
294                                       transitionTime, switchEvent.getThreshold());
295         }
296 
297         // if it is the first switching condition, assume first active law is the past one
298         if (activated == null) {
299             resetActiveProvider(past);
300         }
301 
302         // add the switching condition
303         switches.add(new Switch(switchEvent, switchOnIncrease, switchOnDecrease,
304                                 past, future, transitionTime, transitionFilter, handler));
305 
306     }
307 
308     /** {@inheritDoc} */
309     @Override
310     public Attitude getAttitude(final PVCoordinatesProvider pvProv,
311                                 final AbsoluteDate date, final Frame frame) {
312         return activated.get(date).getAttitude(pvProv, date, frame);
313     }
314 
315     /** {@inheritDoc} */
316     @Override
317     public <T extends CalculusFieldElement<T>> FieldAttitude<T> getAttitude(final FieldPVCoordinatesProvider<T> pvProv,
318                                                                             final FieldAbsoluteDate<T> date,
319                                                                             final Frame frame) {
320         return activated.get(date.toAbsoluteDate()).getAttitude(pvProv, date, frame);
321     }
322 
323     /** {@inheritDoc} */
324     @Override
325     public Rotation getAttitudeRotation(final PVCoordinatesProvider pvProv, final AbsoluteDate date, final Frame frame) {
326         return activated.get(date).getAttitudeRotation(pvProv, date, frame);
327     }
328 
329     @Override
330     public <T extends CalculusFieldElement<T>> FieldRotation<T> getAttitudeRotation(final FieldPVCoordinatesProvider<T> pvProv,
331                                                                                     final FieldAbsoluteDate<T> date,
332                                                                                     final Frame frame) {
333         return activated.get(date.toAbsoluteDate()).getAttitudeRotation(pvProv, date, frame);
334     }
335 
336     /** Switch specification. */
337     private class Switch implements EventDetector, EventHandler {
338 
339         /** Event. */
340         private final EventDetector event;
341 
342         /** Event direction triggering the switch. */
343         private final boolean switchOnIncrease;
344 
345         /** Event direction triggering the switch. */
346         private final boolean switchOnDecrease;
347 
348         /** Attitude provider applicable for times in the switch event occurrence past. */
349         private final AttitudeProvider past;
350 
351         /** Attitude provider applicable for times in the switch event occurrence future. */
352         private final AttitudeProvider future;
353 
354         /** Duration of the transition between the past and future attitude laws. */
355         private final double transitionTime;
356 
357         /** Order at which the transition law time derivatives should match past and future attitude laws. */
358         private final AngularDerivativesFilter transitionFilter;
359 
360         /** Handler to call for notifying when switch occurs (may be null). */
361         private final SwitchHandler switchHandler;
362 
363         /** Propagation direction. */
364         private boolean forward;
365 
366         /** Simple constructor.
367          * @param event event
368          * @param switchOnIncrease if true, switch is triggered on increasing event
369          * @param switchOnDecrease if true, switch is triggered on decreasing event
370          * otherwise switch is triggered on decreasing event
371          * @param past attitude provider applicable for times in the switch event occurrence past
372          * @param future attitude provider applicable for times in the switch event occurrence future
373          * @param transitionTime duration of the transition between the past and future attitude laws
374          * @param transitionFilter order at which the transition law time derivatives
375          * should match past and future attitude laws
376          * @param switchHandler handler to call for notifying when switch occurs (may be null)
377          */
378         Switch(final EventDetector event,
379                final boolean switchOnIncrease, final boolean switchOnDecrease,
380                final AttitudeProvider past, final AttitudeProvider future,
381                final double transitionTime, final AngularDerivativesFilter transitionFilter,
382                final SwitchHandler switchHandler) {
383             this.event            = event;
384             this.switchOnIncrease = switchOnIncrease;
385             this.switchOnDecrease = switchOnDecrease;
386             this.past             = past;
387             this.future           = future;
388             this.transitionTime   = transitionTime;
389             this.transitionFilter = transitionFilter;
390             this.switchHandler    = switchHandler;
391         }
392 
393         /** {@inheritDoc} */
394         @Override
395         public double getThreshold() {
396             return event.getThreshold();
397         }
398 
399         /** {@inheritDoc} */
400         @Override
401         public AdaptableInterval getMaxCheckInterval() {
402             return event.getMaxCheckInterval();
403         }
404 
405         /** {@inheritDoc} */
406         @Override
407         public int getMaxIterationCount() {
408             return event.getMaxIterationCount();
409         }
410 
411         /** {@inheritDoc} */
412         public void init(final SpacecraftState s0, final AbsoluteDate t) {
413 
414             // reset the transition parameters (this will be done once for each switch,
415             //  despite doing it only once would have sufficient; it's not really a problem)
416             forward = t.durationFrom(s0.getDate()) >= 0.0;
417             if (activated.getSpansNumber() > 1) {
418                 // remove transitions that will be overridden during upcoming propagation
419                 if (forward) {
420                     activated = activated.extractRange(AbsoluteDate.PAST_INFINITY, s0.getDate().shiftedBy(transitionTime));
421                 } else {
422                     activated = activated.extractRange(s0.getDate().shiftedBy(-transitionTime), AbsoluteDate.FUTURE_INFINITY);
423                 }
424             }
425 
426             // initialize the underlying event
427             event.init(s0, t);
428 
429         }
430 
431         /** {@inheritDoc} */
432         public double g(final SpacecraftState s) {
433             return event.g(forward ? s : s.shiftedBy(-transitionTime));
434         }
435 
436         /** {@inheritDoc} */
437         public EventHandler getHandler() {
438             return this;
439         }
440 
441         /** {@inheritDoc} */
442         public Action eventOccurred(final SpacecraftState s, final EventDetector detector, final boolean increasing) {
443 
444             final AbsoluteDate date = s.getDate();
445             if (activated.get(date) == (forward ? past : future) &&
446                 (increasing && switchOnIncrease || !increasing && switchOnDecrease)) {
447 
448                 if (forward) {
449 
450                     // prepare transition
451                     final AbsoluteDate transitionEnd = date.shiftedBy(transitionTime);
452                     activated.addValidAfter(new TransitionProvider(s.getAttitude(), transitionEnd), date, false);
453 
454                     // prepare future law after transition
455                     activated.addValidAfter(future, transitionEnd, false);
456 
457                     // notify about the switch
458                     if (switchHandler != null) {
459                         switchHandler.switchOccurred(past, future, s);
460                     }
461 
462                     return event.getHandler().eventOccurred(s, event, increasing);
463 
464                 } else {
465 
466                     // estimate state at transition start, according to the past attitude law
467                     final Orbit     sOrbit    = s.getOrbit().shiftedBy(-transitionTime);
468                     final Attitude  sAttitude = past.getAttitude(sOrbit, sOrbit.getDate(), sOrbit.getFrame());
469                     SpacecraftState sState    = new SpacecraftState(sOrbit, sAttitude, s.getMass());
470                     for (final DoubleArrayDictionary.Entry entry : s.getAdditionalStatesValues().getData()) {
471                         sState = sState.addAdditionalState(entry.getKey(), entry.getValue());
472                     }
473 
474                     // prepare transition
475                     activated.addValidBefore(new TransitionProvider(sAttitude, date), date, false);
476 
477                     // prepare past law before transition
478                     activated.addValidBefore(past, sOrbit.getDate(), false);
479 
480                     // notify about the switch
481                     if (switchHandler != null) {
482                         switchHandler.switchOccurred(future, past, sState);
483                     }
484 
485                     return event.getHandler().eventOccurred(sState, event, increasing);
486 
487                 }
488 
489             } else {
490                 // trigger the underlying event despite no attitude switch occurred
491                 return event.getHandler().eventOccurred(s, event, increasing);
492             }
493 
494         }
495 
496         /** {@inheritDoc} */
497         @Override
498         public SpacecraftState resetState(final EventDetector detector, final SpacecraftState oldState) {
499             // delegate to underlying event
500             return event.getHandler().resetState(event, oldState);
501         }
502 
503         /** Provider for transition phases.
504          * @since 9.2
505          */
506         private class TransitionProvider implements AttitudeProvider {
507 
508             /** Attitude at preceding transition. */
509             private final Attitude transitionPreceding;
510 
511             /** Date of final switch to following attitude law. */
512             private final AbsoluteDate transitionEnd;
513 
514             /** Simple constructor.
515              * @param transitionPreceding attitude at preceding transition
516              * @param transitionEnd date of final switch to following attitude law
517              */
518             TransitionProvider(final Attitude transitionPreceding, final AbsoluteDate transitionEnd) {
519                 this.transitionPreceding = transitionPreceding;
520                 this.transitionEnd       = transitionEnd;
521             }
522 
523             /** {@inheritDoc} */
524             public Attitude getAttitude(final PVCoordinatesProvider pvProv,
525                                         final AbsoluteDate date, final Frame frame) {
526 
527                 // Create sample
528                 final TimeStampedAngularCoordinates start =
529                         transitionPreceding.withReferenceFrame(frame).getOrientation();
530                 final TimeStampedAngularCoordinates end =
531                         future.getAttitude(pvProv, transitionEnd, frame).getOrientation();
532                 final List<TimeStampedAngularCoordinates> sample =  Arrays.asList(start, end);
533 
534                 // Create interpolator
535                 final TimeInterpolator<TimeStampedAngularCoordinates> interpolator =
536                         new TimeStampedAngularCoordinatesHermiteInterpolator(sample.size(), transitionFilter);
537 
538                 // interpolate between the two boundary attitudes
539                 final TimeStampedAngularCoordinates interpolated = interpolator.interpolate(date, sample);
540 
541                 return new Attitude(frame, interpolated);
542 
543             }
544 
545             /** {@inheritDoc} */
546             public <S extends CalculusFieldElement<S>> FieldAttitude<S> getAttitude(final FieldPVCoordinatesProvider<S> pvProv,
547                                                                                 final FieldAbsoluteDate<S> date,
548                                                                                 final Frame frame) {
549 
550                 // create sample
551                 final TimeStampedFieldAngularCoordinates<S> start =
552                         new TimeStampedFieldAngularCoordinates<>(date.getField(),
553                                                                  transitionPreceding.withReferenceFrame(frame).getOrientation());
554                 final TimeStampedFieldAngularCoordinates<S> end =
555                         future.getAttitude(pvProv,
556                                            new FieldAbsoluteDate<>(date.getField(), transitionEnd),
557                                            frame).getOrientation();
558                 final List<TimeStampedFieldAngularCoordinates<S>> sample = Arrays.asList(start, end);
559 
560                 // create interpolator
561                 final FieldTimeInterpolator<TimeStampedFieldAngularCoordinates<S>, S> interpolator =
562                         new TimeStampedFieldAngularCoordinatesHermiteInterpolator<>(sample.size(), transitionFilter);
563 
564                 // interpolate between the two boundary attitudes
565                 final TimeStampedFieldAngularCoordinates<S> interpolated = interpolator.interpolate(date, sample);
566 
567                 return new FieldAttitude<>(frame, interpolated);
568             }
569 
570         }
571 
572     }
573 
574     /** Interface for attitude switch notifications.
575      * <p>
576      * This interface is intended to be implemented by users who want to be
577      * notified when an attitude switch occurs.
578      * </p>
579      * @since 7.1
580      */
581     public interface SwitchHandler {
582 
583         /** Method called when attitude is switched from one law to another law.
584          * @param preceding attitude law used preceding the switch (i.e. in the past
585          * of the switch event for a forward propagation, or in the future
586          * of the switch event for a backward propagation)
587          * @param following attitude law used following the switch (i.e. in the future
588          * of the switch event for a forward propagation, or in the past
589          * of the switch event for a backward propagation)
590          * @param state state at switch time (with attitude computed using the {@code preceding} law)
591          */
592         void switchOccurred(AttitudeProvider preceding, AttitudeProvider following, SpacecraftState state);
593 
594     }
595 
596 }