1   /* Copyright 2022-2025 Romain Serra
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.intervals;
18  
19  import org.hipparchus.CalculusFieldElement;
20  import org.hipparchus.util.FastMath;
21  import org.orekit.time.AbsoluteDate;
22  import org.orekit.time.ChronologicalComparator;
23  import org.orekit.time.FieldTimeStamped;
24  import org.orekit.time.TimeStamped;
25  
26  import java.util.Arrays;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.Optional;
31  import java.util.SortedSet;
32  import java.util.TreeSet;
33  
34  /**
35   * Factory for adaptable interval tuned for date(s) detection.
36   *
37   * @see org.orekit.propagation.events.DateDetector
38   * @see org.orekit.propagation.events.FieldDateDetector
39   * @author Romain Serra
40   * @since 13.0
41   */
42  public class DateDetectionAdaptableIntervalFactory {
43  
44      /** Default value for max check. */
45      public static final double DEFAULT_MAX_CHECK = 1.0e10;
46  
47      /**
48       * Private constructor.
49       */
50      private DateDetectionAdaptableIntervalFactory() {
51          // factory class
52      }
53  
54      /**
55       * Return a candidate {@link AdaptableInterval} for single date detection.
56       * @return adaptable interval
57       */
58      public static AdaptableInterval getSingleDateDetectionAdaptableInterval() {
59          return AdaptableInterval.of(DEFAULT_MAX_CHECK);
60      }
61  
62      /**
63       * Return a candidate {@link AdaptableInterval} for multiple dates detection with a constant max. check.
64       * @param timeStampeds event dates
65       * @return adaptable interval
66       */
67      public static AdaptableInterval getDatesDetectionConstantInterval(final TimeStamped... timeStampeds) {
68          if (timeStampeds == null || timeStampeds.length < 2) {
69              return getSingleDateDetectionAdaptableInterval();
70          }
71          return AdaptableInterval.of(getMinGap(timeStampeds) * 0.5);
72      }
73  
74      /**
75       * Return a candidate {@link AdaptableInterval} for multiple dates detection.
76       * @param timeStampeds event dates
77       * @return adaptable interval
78       */
79      public static AdaptableInterval getDatesDetectionInterval(final TimeStamped... timeStampeds) {
80          if (timeStampeds == null || timeStampeds.length < 2) {
81              return getSingleDateDetectionAdaptableInterval();
82          }
83          final double minGap = getMinGap(timeStampeds);
84          final SortedSet<TimeStamped> sortedSet = new TreeSet<>(new ChronologicalComparator());
85          sortedSet.addAll(Arrays.asList(timeStampeds));
86          return (state, isForward) -> {
87              final AbsoluteDate date = state.getDate();
88              double minDistance = Double.POSITIVE_INFINITY;
89              if (isForward) {
90                  for (final TimeStamped ts : sortedSet) {
91                      final AbsoluteDate nextDate = ts.getDate();
92                      if (date.isBefore(nextDate)) {
93                          minDistance = nextDate.durationFrom(date);
94                          break;
95                      }
96                  }
97              } else {
98                  final List<TimeStamped> inverted = new ArrayList<>(sortedSet);
99                  Collections.reverse(inverted);
100                 for (final TimeStamped ts : inverted) {
101                     final AbsoluteDate nextDate = ts.getDate();
102                     if (date.isAfter(nextDate)) {
103                         minDistance = date.durationFrom(nextDate);
104                         break;
105                     }
106                 }
107             }
108             return FastMath.abs(minDistance) + minGap / 2;
109         };
110     }
111 
112     /**
113      * Return a candidate {@link FieldAdaptableInterval} for single date detection.
114      * @param <T> field type
115      * @return adaptable interval
116      */
117     public static <T extends CalculusFieldElement<T>> FieldAdaptableInterval<T> getSingleDateDetectionFieldAdaptableInterval() {
118         return FieldAdaptableInterval.of(DEFAULT_MAX_CHECK);
119     }
120 
121     /**
122      * Return a candidate {@link FieldAdaptableInterval} for multiple dates detection with a constant max. check.
123      * @param timeStampeds event dates
124      * @param <T> field type
125      * @return adaptable interval
126      */
127     @SafeVarargs
128     public static <T extends CalculusFieldElement<T>> FieldAdaptableInterval<T> getDatesDetectionFieldConstantInterval(final FieldTimeStamped<T>... timeStampeds) {
129         if (timeStampeds == null || timeStampeds.length < 2) {
130             return getSingleDateDetectionFieldAdaptableInterval();
131         }
132         final double minGap = getMinGap(Arrays.stream(timeStampeds).map(t -> (TimeStamped) t.getDate().toAbsoluteDate())
133                 .toArray(TimeStamped[]::new));
134         return FieldAdaptableInterval.of(minGap * 0.5);
135     }
136 
137     /**
138      * Return a candidate {@link FieldAdaptableInterval} for multiple dates detection.
139      * @param timeStampeds event dates
140      * @param <T> field type
141      * @return adaptable interval
142      */
143     @SafeVarargs
144     public static <T extends CalculusFieldElement<T>> FieldAdaptableInterval<T> getDatesDetectionFieldInterval(final FieldTimeStamped<T>... timeStampeds) {
145         if (timeStampeds == null || timeStampeds.length < 2) {
146             return getSingleDateDetectionFieldAdaptableInterval();
147         }
148         return FieldAdaptableInterval.of(getDatesDetectionInterval(Arrays.stream(timeStampeds)
149                 .map(t -> (TimeStamped) t.getDate().toAbsoluteDate()).toArray(TimeStamped[]::new)));
150     }
151 
152     /**
153      * Compute min. gap between dated objects if applicable. It ignores duplicates.
154      * @param timeStampeds time stamped objects
155      * @return minimum gap
156      */
157     public static double getMinGap(final TimeStamped... timeStampeds) {
158         double minGap = DEFAULT_MAX_CHECK;
159         for (final TimeStamped timeStamped : timeStampeds) {
160             final Optional<Double> minDistance = Arrays.stream(timeStampeds)
161                     .map(t -> (!t.getDate().isEqualTo(timeStamped.getDate())) ? FastMath.abs(t.durationFrom(timeStamped)) : Double.POSITIVE_INFINITY)
162                     .min(Double::compareTo);
163             if (minDistance.isPresent()) {
164                 minGap = FastMath.min(minGap, minDistance.get());
165             }
166         }
167         return minGap;
168     }
169 }