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