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