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