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.forces.maneuvers.trigger;
18  
19  import java.util.HashMap;
20  import java.util.Map;
21  import java.util.stream.Stream;
22  
23  import org.hipparchus.CalculusFieldElement;
24  import org.hipparchus.Field;
25  import org.hipparchus.ode.events.Action;
26  import org.orekit.propagation.FieldSpacecraftState;
27  import org.orekit.propagation.SpacecraftState;
28  import org.orekit.propagation.events.EventDetector;
29  import org.orekit.propagation.events.FieldEventDetector;
30  import org.orekit.propagation.events.handlers.EventHandler;
31  import org.orekit.propagation.events.handlers.FieldEventHandler;
32  import org.orekit.time.AbsoluteDate;
33  import org.orekit.time.FieldAbsoluteDate;
34  
35  /**
36   * Maneuver triggers based on a pair of event detectors that defines firing start and stop.
37   * <p>
38   * The thruster starts firing when the start detector becomes
39   * positive. The thruster stops firing when the stop detector becomes positive.
40   * The 2 detectors should not be positive at the same time. A date detector is
41   * not suited as it does not delimit an interval. They can be both negative at
42   * the same time.
43   * </p>
44   * @param <A> type of the start detector
45   * @param <O> type of the stop detector
46   * @see IntervalEventTrigger
47   * @author Luc Maisonobe
48   * @since 11.1
49   */
50  public abstract class StartStopEventsTrigger<A extends EventDetector, O extends EventDetector> extends AbstractManeuverTriggers {
51  
52      /** Start detector. */
53      private final ManeuverTriggerDetector<A> startDetector;
54  
55      /** Stop detector. */
56      private final ManeuverTriggerDetector<O> stopDetector;
57  
58      /** Cached field-based start detectors. */
59      private final transient Map<Field<? extends CalculusFieldElement<?>>, FieldEventDetector<? extends CalculusFieldElement<?>>> cachedStart;
60  
61      /** Cached field-based stop detectors. */
62      private final transient Map<Field<? extends CalculusFieldElement<?>>, FieldEventDetector<? extends CalculusFieldElement<?>>> cachedStop;
63  
64      /** Simple constructor.
65       * <p>
66       * Note that the {@code startDetector} and {@code stopDetector} passed as an argument are used only
67       * as a <em>prototypes</em> from which new detectors will be built using
68       * {@link ManeuverTriggerDetector}. The original event handlers from the prototype
69       * will be <em>ignored</em> and never called.
70       * </p>
71       * <p>
72       * If the trigger is used in a {@link org.orekit.propagation.FieldPropagator field-based propagation},
73       * the detector will be automatically converted to a field equivalent. Beware however that the
74       * {@link FieldEventHandler#eventOccurred(FieldSpacecraftState, FieldEventDetector, boolean) eventOccurred}
75       * of the converted propagator <em>will</em> call the method with the same name in the prototype
76       * detector, in order to get the correct return value.
77       * </p>
78       * @param prototypeStartDetector prototype detector for firing start
79       * @param prototypeStopDetector prototype detector for firing stop
80       */
81      protected StartStopEventsTrigger(final A prototypeStartDetector, final O prototypeStopDetector) {
82  
83          this.startDetector = new ManeuverTriggerDetector<>(prototypeStartDetector, new StartHandler());
84          this.stopDetector  = new ManeuverTriggerDetector<>(prototypeStopDetector, new StopHandler());
85          this.cachedStart   = new HashMap<>();
86          this.cachedStop    = new HashMap<>();
87  
88      }
89  
90      /**
91       * Getter for the firing start detector.
92       * @return firing start detector
93       */
94      public A getStartDetector() {
95          return startDetector.getDetector();
96      }
97  
98      /**
99       * Getter for the firing stop detector.
100      * @return firing stop detector
101      */
102     public O getStopDetector() {
103         return stopDetector.getDetector();
104     }
105 
106     /** {@inheritDoc} */
107     @Override
108     public void init(final SpacecraftState initialState, final AbsoluteDate target) {
109         startDetector.init(initialState, target);
110         stopDetector.init(initialState, target);
111         super.init(initialState, target);
112     }
113 
114     /** {@inheritDoc} */
115     @Override
116     protected boolean isFiringOnInitialState(final SpacecraftState initialState, final boolean isForward) {
117 
118         final double startG = startDetector.g(initialState);
119         if (startG == 0) {
120             final boolean increasing = startDetector.g(initialState.shiftedBy(2 * startDetector.getThreshold())) > 0;
121             if (increasing) {
122                 // we are at maneuver start
123                 notifyResetters(initialState, true);
124                 // if propagating forward, we start firing
125                 return isForward;
126             } else {
127                 // not a meaningful crossing
128                 return false;
129             }
130         } else if (startG < 0) {
131             // we are before start
132             return false;
133         } else {
134             // we are after start
135             final double stopG = stopDetector.g(initialState);
136             if (stopG == 0) {
137                 final boolean increasing = stopDetector.g(initialState.shiftedBy(2 * stopDetector.getThreshold())) > 0;
138                 if (increasing) {
139                     // we are at maneuver end
140                     notifyResetters(initialState, false);
141                     // if propagating backward, we start firing
142                     return !isForward;
143                 } else {
144                     // not a meaningful crossing
145                     return false;
146                 }
147             } else if (stopG > 0) {
148                 // we are after stop
149                 return false;
150             } else {
151                 // we are between start and stop
152                 return true;
153             }
154         }
155 
156     }
157 
158     /** {@inheritDoc} */
159     @Override
160     public Stream<EventDetector> getEventDetectors() {
161         return Stream.of(startDetector, stopDetector);
162     }
163 
164     /** {@inheritDoc} */
165     @Override
166     public <S extends CalculusFieldElement<S>> Stream<FieldEventDetector<S>> getFieldEventDetectors(final Field<S> field) {
167 
168         // get the field version of the start detector
169         @SuppressWarnings("unchecked")
170         FieldEventDetector<S> fStart = (FieldEventDetector<S>) cachedStart.get(field);
171         if (fStart == null) {
172             fStart = convertAndSetUpStartHandler(field);
173             cachedStart.put(field, fStart);
174         }
175 
176         // get the field version of the stop detector
177         @SuppressWarnings("unchecked")
178         FieldEventDetector<S> fStop = (FieldEventDetector<S>) cachedStop.get(field);
179         if (fStop == null) {
180             fStop = convertAndSetUpStopHandler(field);
181             cachedStop.put(field, fStop);
182         }
183 
184         return Stream.of(fStart, fStop);
185 
186     }
187 
188     /** Convert a detector and set up new handler.
189      * <p>
190      * This method is not inlined in {@link #getFieldEventDetectors(Field)} because the
191      * parameterized types confuses the Java compiler.
192      * </p>
193      * @param field field to which the state belongs
194      * @param <D> type of the event detector
195      * @param <S> type of the field elements
196      * @return converted firing intervals detector
197      */
198     private <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>> FieldManeuverTriggerDetector<S, D> convertAndSetUpStartHandler(final Field<S> field) {
199         final D converted = convertStartDetector(field, startDetector.getDetector());
200         return new FieldManeuverTriggerDetector<>(converted, new FieldStartHandler<>());
201     }
202 
203     /** Convert a detector and set up new handler.
204      * <p>
205      * This method is not inlined in {@link #getFieldEventDetectors(Field)} because the
206      * parameterized types confuses the Java compiler.
207      * </p>
208      * @param field field to which the state belongs
209      * @param <D> type of the event detector
210      * @param <S> type of the field elements
211      * @return converted firing intervals detector
212      */
213     private <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>> FieldManeuverTriggerDetector<S, D> convertAndSetUpStopHandler(final Field<S> field) {
214         final D converted = convertStopDetector(field, stopDetector.getDetector());
215         return new FieldManeuverTriggerDetector<>(converted, new FieldStopHandler<>());
216     }
217 
218     /** Convert a primitive firing start detector into a field firing start detector.
219      * <p>
220      * The {@link org.orekit.propagation.events.FieldEventDetectionSettings} must be set up in conformance with the
221      * non-field detector.
222      * </p>
223      * <p>
224      * A skeleton implementation of this method to convert some {@code XyzDetector} into {@code FieldXyzDetector},
225      * considering these detectors have a withDetectionSettings method and are created from a date and a number parameter is:
226      * </p>
227      * <pre>{@code
228      *     protected <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>>
229      *         D convertStartDetector(final Field<S> field, final XyzDetector detector) {
230      *
231      *         final FieldAbsoluteDate<S> date  = new FieldAbsoluteDate<>(field, detector.getDate());
232      *         final S                    param = field.getZero().newInstance(detector.getParam());
233      *
234      *         final D converted = (D) new FieldXyzDetector<>(date, param)
235      *         .withDetectionSettings(field, detector.getDetectionSettings());
236      *         return converted;
237      *
238      *     }
239      * }
240      * </pre>
241      * @param field field to which the state belongs
242      * @param detector primitive firing start detector to convert
243      * @param <D> type of the event detector
244      * @param <S> type of the field elements
245      * @return converted firing start detector
246      */
247     protected abstract <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>> D
248         convertStartDetector(Field<S> field, A detector);
249 
250     /** Convert a primitive firing stop detector into a field firing stop detector.
251      * <p>
252      * The {@link org.orekit.propagation.events.FieldEventDetectionSettings} must be set up in conformance with the
253      * non-field detector.
254      * </p>
255      * <p>
256      * A skeleton implementation of this method to convert some {@code XyzDetector} into {@code FieldXyzDetector},
257      * considering these detectors have a withDetectionSettings method and are created from a date and a number parameter is:
258      * </p>
259      * <pre>{@code
260      *     protected <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>>
261      *         D convertEndDetector(final Field<S> field, final XyzDetector detector) {
262      *
263      *         final FieldAbsoluteDate<S> date  = new FieldAbsoluteDate<>(field, detector.getDate());
264      *         final S                    param = field.getZero().newInstance(detector.getParam());
265      *
266      *         final D converted = (D) new FieldXyzDetector<>(date, param)
267      *         .withDetectionSettings(field, detector.getDetectionSettings());
268      *         return converted;
269      *
270      *     }
271      * }
272      * </pre>
273      * @param field field to which the state belongs
274      * @param detector primitive firing stop detector to convert
275      * @param <D> type of the event detector
276      * @param <S> type of the field elements
277      * @return converted firing stop detector
278      */
279     protected abstract <D extends FieldEventDetector<S>, S extends CalculusFieldElement<S>> D
280         convertStopDetector(Field<S> field, O detector);
281 
282     /** Local handler for start triggers. */
283     private class StartHandler implements EventHandler {
284 
285         /** Propagation direction. */
286         private boolean forward;
287 
288         /** {@inheritDoc} */
289         @Override
290         public void init(final SpacecraftState initialState, final AbsoluteDate target, final EventDetector detector) {
291             forward = target.isAfterOrEqualTo(initialState);
292             initializeResetters(initialState, target);
293         }
294 
295         /** {@inheritDoc} */
296         @Override
297         public Action eventOccurred(final SpacecraftState s, final EventDetector detector, final boolean increasing) {
298             if (increasing) {
299                 // the event is meaningful for maneuver firing
300                 if (forward) {
301                     getFirings().addValidAfter(true, s.getDate(), false);
302                 } else {
303                     getFirings().addValidBefore(false, s.getDate(), false);
304                 }
305                 notifyResetters(s, true);
306                 return Action.RESET_STATE;
307             } else {
308                 // the event is not meaningful for maneuver firing
309                 return Action.CONTINUE;
310             }
311         }
312 
313         /** {@inheritDoc} */
314         @Override
315         public SpacecraftState resetState(final EventDetector detector, final SpacecraftState oldState) {
316             return applyResetters(oldState);
317         }
318 
319     }
320 
321     /** Local handler for stop triggers. */
322     private class StopHandler implements EventHandler {
323 
324         /** Propagation direction. */
325         private boolean forward;
326 
327         /** {@inheritDoc} */
328         @Override
329         public void init(final SpacecraftState initialState, final AbsoluteDate target, final EventDetector detector) {
330             forward = target.isAfterOrEqualTo(initialState);
331             initializeResetters(initialState, target);
332         }
333 
334         /** {@inheritDoc} */
335         @Override
336         public Action eventOccurred(final SpacecraftState s, final EventDetector detector, final boolean increasing) {
337             if (increasing) {
338                 // the event is meaningful for maneuver firing
339                 if (forward) {
340                     getFirings().addValidAfter(false, s.getDate(), false);
341                 } else {
342                     getFirings().addValidBefore(true, s.getDate(), false);
343                 }
344                 notifyResetters(s, false);
345                 return Action.RESET_STATE;
346             } else {
347                 // the event is not meaningful for maneuver firing
348                 return Action.CONTINUE;
349             }
350         }
351 
352         /** {@inheritDoc} */
353         @Override
354         public SpacecraftState resetState(final EventDetector detector, final SpacecraftState oldState) {
355             return applyResetters(oldState);
356         }
357 
358     }
359 
360     /** Local handler for start triggers.
361      * @param <S> type of the field elements
362      */
363     private class FieldStartHandler<S extends CalculusFieldElement<S>> implements FieldEventHandler<S> {
364 
365         /** Propagation direction. */
366         private boolean forward;
367 
368         /** {@inheritDoc} */
369         @Override
370         public void init(final FieldSpacecraftState<S> initialState,
371                          final FieldAbsoluteDate<S> target,
372                          final FieldEventDetector<S> detector) {
373             forward = target.isAfterOrEqualTo(initialState);
374             initializeResetters(initialState, target);
375         }
376 
377         /** {@inheritDoc} */
378         @Override
379         public Action eventOccurred(final FieldSpacecraftState<S> s, final FieldEventDetector<S> detector, final boolean increasing) {
380             if (increasing) {
381                 // the event is meaningful for maneuver firing
382                 if (forward) {
383                     getFirings().addValidAfter(true, s.getDate().toAbsoluteDate(), false);
384                 } else {
385                     getFirings().addValidBefore(false, s.getDate().toAbsoluteDate(), false);
386                 }
387                 notifyResetters(s, true);
388                 return Action.RESET_STATE;
389             } else {
390                 // the event is not meaningful for maneuver firing
391                 return Action.CONTINUE;
392             }
393         }
394 
395         /** {@inheritDoc} */
396         @Override
397         public FieldSpacecraftState<S> resetState(final FieldEventDetector<S> detector, final FieldSpacecraftState<S> oldState) {
398             return applyResetters(oldState);
399         }
400 
401     }
402 
403     /** Local handler for stop triggers.
404      * @param <S> type of the field elements
405      */
406     private class FieldStopHandler<S extends CalculusFieldElement<S>> implements FieldEventHandler<S> {
407 
408         /** Propagation direction. */
409         private boolean forward;
410 
411         /** {@inheritDoc} */
412         @Override
413         public void init(final FieldSpacecraftState<S> initialState,
414                          final FieldAbsoluteDate<S> target,
415                          final FieldEventDetector<S> detector) {
416             forward = target.isAfterOrEqualTo(initialState);
417             initializeResetters(initialState, target);
418         }
419 
420         /** {@inheritDoc} */
421         @Override
422         public Action eventOccurred(final FieldSpacecraftState<S> s, final FieldEventDetector<S> detector, final boolean increasing) {
423             if (increasing) {
424                 // the event is meaningful for maneuver firing
425                 if (forward) {
426                     getFirings().addValidAfter(false, s.getDate().toAbsoluteDate(), false);
427                 } else {
428                     getFirings().addValidBefore(true, s.getDate().toAbsoluteDate(), false);
429                 }
430                 notifyResetters(s, false);
431                 return Action.RESET_STATE;
432             } else {
433                 // the event is not meaningful for maneuver firing
434                 return Action.CONTINUE;
435             }
436         }
437 
438         /** {@inheritDoc} */
439         @Override
440         public FieldSpacecraftState<S> resetState(final FieldEventDetector<S> detector, final FieldSpacecraftState<S> oldState) {
441             return applyResetters(oldState);
442         }
443 
444     }
445 
446 }