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.propagation.events;
18  
19  import java.util.List;
20  import java.util.stream.Collectors;
21  
22  import org.hipparchus.util.FastMath;
23  import org.orekit.errors.OrekitException;
24  import org.orekit.errors.OrekitMessages;
25  import org.orekit.propagation.SpacecraftState;
26  import org.orekit.propagation.events.handlers.EventHandler;
27  import org.orekit.propagation.events.handlers.StopOnDecreasing;
28  import org.orekit.propagation.events.intervals.DateDetectionAdaptableIntervalFactory;
29  import org.orekit.time.AbsoluteDate;
30  import org.orekit.utils.DateDriver;
31  import org.orekit.utils.ParameterDriver;
32  import org.orekit.utils.ParameterObserver;
33  import org.orekit.utils.TimeSpanMap;
34  import org.orekit.utils.TimeSpanMap.Span;
35  
36  /** Detector for date intervals that may be offset thanks to parameter drivers.
37   * <p>
38   * Two dual views can be used for date intervals: either start date/stop date or
39   * median date/duration. {@link #getStartDriver() start}/{@link #getStopDriver() stop}
40   * drivers and {@link #getMedianDriver() median}/{@link #getDurationDriver() duration}
41   * drivers work in pair. Both drivers in one pair can be selected and their changes will
42   * be propagated to the other pair, but attempting to select drivers in both
43   * pairs at the same time will trigger an exception. Changing the value of a driver
44   * that is not selected should be avoided as it leads to inconsistencies between the pairs.
45   * </p>. Warning, startDate driver, stopDate driver, duration driver and medianDate driver
46   * must all have the same number of values to estimate (same number of span in valueSpanMap), that is is to
47   * say that the {@link org.orekit.utils.ParameterDriver#addSpans(AbsoluteDate, AbsoluteDate, double)}
48   * should be called with same arguments.
49   * @see org.orekit.propagation.Propagator#addEventDetector(EventDetector)
50   * @author Luc Maisonobe
51   * @since 11.1
52   */
53  public class ParameterDrivenDateIntervalDetector extends AbstractDetector<ParameterDrivenDateIntervalDetector> {
54  
55      /** Default suffix for start driver. */
56      public static final String START_SUFFIX = "_START";
57  
58      /** Default suffix for stop driver. */
59      public static final String STOP_SUFFIX = "_STOP";
60  
61      /** Default suffix for median driver. */
62      public static final String MEDIAN_SUFFIX = "_MEDIAN";
63  
64      /** Default suffix for duration driver. */
65      public static final String DURATION_SUFFIX = "_DURATION";
66  
67      /** Reference interval start driver. */
68      private final DateDriver start;
69  
70      /** Reference interval stop driver. */
71      private final DateDriver stop;
72  
73      /** Median date driver. */
74      private final DateDriver median;
75  
76      /** Duration driver. */
77      private final ParameterDriver duration;
78  
79      /** Build a new instance.
80       * @param prefix prefix to use for parameter drivers names
81       * @param refMedian reference interval median date
82       * @param refDuration reference duration
83       */
84      public ParameterDrivenDateIntervalDetector(final String prefix,
85                                                 final AbsoluteDate refMedian, final double refDuration) {
86          this(prefix,
87               refMedian.shiftedBy(-0.5 * refDuration),
88               refMedian.shiftedBy(0.5 * refDuration));
89      }
90  
91      /** Build a new instance.
92       * @param prefix prefix to use for parameter drivers names
93       * @param refStart reference interval start date
94       * @param refStop reference interval stop date
95       */
96      public ParameterDrivenDateIntervalDetector(final String prefix,
97                                                 final AbsoluteDate refStart, final AbsoluteDate refStop) {
98          this(getDefaultDetectionSettings(refStart, refStop),
99               new StopOnDecreasing(),
100              new DateDriver(refStart, prefix + START_SUFFIX, true),
101              new DateDriver(refStop, prefix + STOP_SUFFIX, false),
102              new DateDriver(refStart.shiftedBy(0.5 * refStop.durationFrom(refStart)), prefix + MEDIAN_SUFFIX, true),
103              new ParameterDriver(prefix + DURATION_SUFFIX, refStop.durationFrom(refStart), 1.0, 0.0, Double.POSITIVE_INFINITY));
104     }
105 
106     /** Protected constructor with full parameters.
107      * <p>
108      * This constructor is not public as users are expected to use the builder
109      * API with the various {@code withXxx()} methods to set up the instance
110      * in a readable manner without using a huge amount of parameters.
111      * </p>
112      * @param detectionSettings event detection settings
113      * @param handler event handler to call at event occurrences
114      * @param start reference interval start driver
115      * @param stop reference interval stop driver
116      * @param median median date driver
117      * @param duration duration driver
118      * @since 13.0
119      */
120     protected ParameterDrivenDateIntervalDetector(final EventDetectionSettings detectionSettings,
121                                                   final EventHandler handler,
122                                                   final DateDriver start, final DateDriver stop,
123                                                   final DateDriver median, final ParameterDriver duration) {
124         super(detectionSettings, handler);
125         this.start    = start;
126         this.stop     = stop;
127         this.median   = median;
128         this.duration = duration;
129 
130         // set up delegation between drivers
131         replaceBindingObserver(start,    new StartObserver());
132         replaceBindingObserver(stop,     new StopObserver());
133         replaceBindingObserver(median,   new MedianObserver());
134         replaceBindingObserver(duration, new DurationObserver());
135 
136     }
137 
138     /**
139      * Get default detection settings.
140      * @param refStart reference interval start date
141      * @param refStop reference interval stop date
142      * @return default detection settings
143      * @since 13.0
144      */
145     public static EventDetectionSettings getDefaultDetectionSettings(final AbsoluteDate refStart,
146                                                                      final AbsoluteDate refStop) {
147         return new EventDetectionSettings(DateDetectionAdaptableIntervalFactory.getDatesDetectionInterval(refStart, refStop),
148                 DateDetector.DEFAULT_THRESHOLD, EventDetectionSettings.DEFAULT_MAX_ITER);
149     }
150 
151     /** Replace binding observers.
152      * @param driver driver for whose binding observers should be replaced
153      * @param bindingObserver new binding observer
154      */
155     private void replaceBindingObserver(final ParameterDriver driver, final BindingObserver bindingObserver) {
156 
157         // remove the previous binding observers
158         final List<ParameterObserver> original = driver.
159                                                  getObservers().
160                                                  stream().
161                                                  filter(observer -> observer instanceof ParameterDrivenDateIntervalDetector.BindingObserver).
162                                                  collect(Collectors.toList());
163         original.forEach(driver::removeObserver);
164 
165         driver.addObserver(bindingObserver);
166 
167     }
168 
169     /** {@inheritDoc} */
170     @Override
171     protected ParameterDrivenDateIntervalDetector create(final EventDetectionSettings detectionSettings,
172                                                          final EventHandler newHandler) {
173         return new ParameterDrivenDateIntervalDetector(detectionSettings, newHandler, start, stop, median, duration);
174     }
175 
176     /** Get the driver for start date.
177      * <p>
178      * Note that the start date is automatically adjusted if either
179      * {@link #getMedianDriver() median date} or {@link #getDurationDriver() duration}
180      * are {@link ParameterDriver#isSelected() selected} and changed.
181      * </p>
182      * @return driver for start date
183      */
184     public DateDriver getStartDriver() {
185         return start;
186     }
187 
188     /** Get the driver for stop date.
189      * <p>
190      * Note that the stop date is automatically adjusted if either
191      * {@link #getMedianDriver() median date} or {@link #getDurationDriver() duration}
192      * are {@link ParameterDriver#isSelected() selected} changed.
193      * </p>
194      * @return driver for stop date
195      */
196     public DateDriver getStopDriver() {
197         return stop;
198     }
199 
200     /** Get the driver for median date.
201      * <p>
202      * Note that the median date is automatically adjusted if either
203      * {@link #getStartDriver()} start date or {@link #getStopDriver() stop date}
204      * are {@link ParameterDriver#isSelected() selected} changed.
205      * </p>
206      * @return driver for median date
207      */
208     public DateDriver getMedianDriver() {
209         return median;
210     }
211 
212     /** Get the driver for duration.
213      * <p>
214      * Note that the duration is automatically adjusted if either
215      * {@link #getStartDriver()} start date or {@link #getStopDriver() stop date}
216      * are {@link ParameterDriver#isSelected() selected} changed.
217      * </p>
218      * @return driver for duration
219      */
220     public ParameterDriver getDurationDriver() {
221         return duration;
222     }
223 
224     /** Compute the value of the switching function.
225      * <p>
226      * The function is positive for dates within the interval defined
227      * by applying the parameter drivers shifts to reference dates,
228      * and negative for dates outside of this interval. Note that
229      * if Δt_start - Δt_stop is less than ref_stop.durationFrom(ref_start),
230      * then the interval degenerates to empty and the function never
231      * reaches positive values.
232      * </p>
233      * @param s the current state information: date, kinematics, attitude
234      * @return value of the switching function
235      */
236     public double g(final SpacecraftState s) {
237         return FastMath.min(s.getDate().durationFrom(start.getDate()),
238                             stop.getDate().durationFrom(s.getDate()));
239     }
240 
241     /** Base observer. */
242     private abstract class BindingObserver implements ParameterObserver {
243 
244         /** {@inheritDoc} */
245         @Override
246         public void valueChanged(final double previousValue, final ParameterDriver driver, final AbsoluteDate date) {
247             if (driver.isSelected()) {
248                 setDelta(driver.getValue(date) - previousValue, date);
249             }
250         }
251         /** {@inheritDoc} */
252         @Override
253         public void valueSpanMapChanged(final TimeSpanMap<Double> previousValue, final ParameterDriver driver) {
254             if (driver.isSelected()) {
255                 for (Span<Double> span = driver.getValueSpanMap().getFirstSpan(); span != null; span = span.next()) {
256                     setDelta(span.getData() - previousValue.get(span.getStart()), span.getStart());
257                 }
258             }
259         }
260         /** {@inheritDoc} */
261         @Override
262         public void selectionChanged(final boolean previousSelection, final ParameterDriver driver) {
263             if ((start.isSelected()  || stop.isSelected()) &&
264                 (median.isSelected() || duration.isSelected())) {
265                 throw new OrekitException(OrekitMessages.INCONSISTENT_SELECTION,
266                                           start.getName(), stop.getName(),
267                                           median.getName(), duration.getName());
268             }
269         }
270 
271         /** Change a value.
272          * @param delta change of value
273          * @param date date at which the delta wants to be set
274          */
275         protected abstract void setDelta(double delta, AbsoluteDate date);
276 
277     }
278 
279     /** Observer for start date. */
280     private class StartObserver extends BindingObserver {
281         /** {@inheritDoc} */
282         @Override
283         protected void setDelta(final double delta, final AbsoluteDate date) {
284             // date driver has no validity period, only 1 value is estimated
285             // over the all interval so there is no problem for calling getValue with null argument
286             // or any date, it would give the same result as there is only 1 span on the valueSpanMap
287             // of the driver
288             median.setValue(median.getValue(date) + 0.5 * delta, date);
289             duration.setValue(duration.getValue(date) - delta, date);
290         }
291     }
292 
293     /** Observer for stop date. */
294     private class StopObserver extends BindingObserver {
295         /** {@inheritDoc} */
296         @Override
297         protected void setDelta(final double delta, final AbsoluteDate date) {
298             // date driver has no validity period, only 1 value is estimated
299             // over the all interval so there is no problem for calling getValue with null argument
300             // or any date, it would give the same result as there is only 1 span on the valueSpanMap
301             // of the driver
302             median.setValue(median.getValue(date) + 0.5 * delta, date);
303             duration.setValue(duration.getValue(date) + delta, date);
304         }
305     }
306 
307     /** Observer for median date. */
308     private class MedianObserver extends BindingObserver {
309         /** {@inheritDoc} */
310         @Override
311         protected void setDelta(final double delta, final AbsoluteDate date) {
312             // date driver has no validity period, only 1 value is estimated
313             // over the all interval so there is no problem for calling getValue with null argument
314             // or any date, it would give the same result as there is only 1 span on the valueSpanMap
315             // of the driver
316             start.setValue(start.getValue(date) + delta, date);
317             stop.setValue(stop.getValue(date) + delta, date);
318         }
319     }
320 
321     /** Observer for duration. */
322     private class DurationObserver extends BindingObserver {
323         /** {@inheritDoc} */
324         @Override
325         protected void setDelta(final double delta, final AbsoluteDate date) {
326             // date driver has no validity period, only 1 value is estimated
327             // over the all interval so there is no problem for calling getValue with null argument
328             // or any date, it would give the same result as there is only 1 span on the valueSpanMap
329             // of the driver
330             start.setValue(start.getValue(date) - 0.5 * delta, date);
331             stop.setValue(stop.getValue(date) + 0.5 * delta, date);
332         }
333     }
334 
335 }