DateDetectionAdaptableIntervalFactory.java

/* Copyright 2022-2024 Romain Serra
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.propagation.events.intervals;

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.util.FastMath;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.ChronologicalComparator;
import org.orekit.time.FieldTimeStamped;
import org.orekit.time.TimeStamped;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Factory for adaptable interval tuned for date(s) detection.
 *
 * @see org.orekit.propagation.events.DateDetector
 * @see org.orekit.propagation.events.FieldDateDetector
 * @author Romain Serra
 * @since 13.0
 */
public class DateDetectionAdaptableIntervalFactory {

    /** Default value for max check. */
    public static final double DEFAULT_MAX_CHECK = 1.0e10;

    /**
     * Private constructor.
     */
    private DateDetectionAdaptableIntervalFactory() {
        // factory class
    }

    /**
     * Return a candidate {@link AdaptableInterval} for single date detection.
     * @return adaptable interval
     */
    public static AdaptableInterval getSingleDateDetectionAdaptableInterval() {
        return AdaptableInterval.of(DEFAULT_MAX_CHECK);
    }

    /**
     * Return a candidate {@link AdaptableInterval} for multiple dates detection with a constant max. check.
     * @param timeStampeds event dates
     * @return adaptable interval
     */
    public static AdaptableInterval getDatesDetectionConstantInterval(final TimeStamped... timeStampeds) {
        if (timeStampeds == null || timeStampeds.length < 2) {
            return getSingleDateDetectionAdaptableInterval();
        }
        return AdaptableInterval.of(getMinGap(timeStampeds) * 0.5);
    }

    /**
     * Return a candidate {@link AdaptableInterval} for multiple dates detection.
     * @param timeStampeds event dates
     * @return adaptable interval
     */
    public static AdaptableInterval getDatesDetectionInterval(final TimeStamped... timeStampeds) {
        if (timeStampeds == null || timeStampeds.length < 2) {
            return getSingleDateDetectionAdaptableInterval();
        }
        final double minGap = getMinGap(timeStampeds);
        final SortedSet<TimeStamped> sortedSet = new TreeSet<>(new ChronologicalComparator());
        sortedSet.addAll(Arrays.asList(timeStampeds));
        return (state, isForward) -> {
            final AbsoluteDate date = state.getDate();
            double minDistance = Double.POSITIVE_INFINITY;
            if (isForward) {
                for (final TimeStamped ts : sortedSet) {
                    final AbsoluteDate nextDate = ts.getDate();
                    if (date.isBefore(nextDate)) {
                        minDistance = nextDate.durationFrom(date);
                        break;
                    }
                }
            } else {
                final List<TimeStamped> inverted = new ArrayList<>(sortedSet);
                Collections.reverse(inverted);
                for (final TimeStamped ts : inverted) {
                    final AbsoluteDate nextDate = ts.getDate();
                    if (date.isAfter(nextDate)) {
                        minDistance = date.durationFrom(nextDate);
                        break;
                    }
                }
            }
            return FastMath.abs(minDistance) + minGap / 2;
        };
    }

    /**
     * Return a candidate {@link FieldAdaptableInterval} for single date detection.
     * @param <T> field type
     * @return adaptable interval
     */
    public static <T extends CalculusFieldElement<T>> FieldAdaptableInterval<T> getSingleDateDetectionFieldAdaptableInterval() {
        return FieldAdaptableInterval.of(DEFAULT_MAX_CHECK);
    }

    /**
     * Return a candidate {@link FieldAdaptableInterval} for multiple dates detection with a constant max. check.
     * @param timeStampeds event dates
     * @param <T> field type
     * @return adaptable interval
     */
    @SafeVarargs
    public static <T extends CalculusFieldElement<T>> FieldAdaptableInterval<T> getDatesDetectionFieldConstantInterval(final FieldTimeStamped<T>... timeStampeds) {
        if (timeStampeds == null || timeStampeds.length < 2) {
            return getSingleDateDetectionFieldAdaptableInterval();
        }
        final double minGap = getMinGap(Arrays.stream(timeStampeds).map(t -> (TimeStamped) t.getDate().toAbsoluteDate())
                .toArray(TimeStamped[]::new));
        return FieldAdaptableInterval.of(minGap * 0.5);
    }

    /**
     * Return a candidate {@link FieldAdaptableInterval} for multiple dates detection.
     * @param timeStampeds event dates
     * @param <T> field type
     * @return adaptable interval
     */
    @SafeVarargs
    public static <T extends CalculusFieldElement<T>> FieldAdaptableInterval<T> getDatesDetectionFieldInterval(final FieldTimeStamped<T>... timeStampeds) {
        if (timeStampeds == null || timeStampeds.length < 2) {
            return getSingleDateDetectionFieldAdaptableInterval();
        }
        return FieldAdaptableInterval.of(getDatesDetectionInterval(Arrays.stream(timeStampeds)
                .map(t -> (TimeStamped) t.getDate().toAbsoluteDate()).toArray(TimeStamped[]::new)));
    }

    /**
     * Compute min. gap between dated objects if applicable. It ignores duplicates.
     * @param timeStampeds time stamped objects
     * @return minimum gap
     */
    public static double getMinGap(final TimeStamped... timeStampeds) {
        double minGap = DEFAULT_MAX_CHECK;
        for (final TimeStamped timeStamped : timeStampeds) {
            final Optional<Double> minDistance = Arrays.stream(timeStampeds)
                    .map(t -> (!t.getDate().isEqualTo(timeStamped.getDate())) ? FastMath.abs(t.durationFrom(timeStamped)) : Double.POSITIVE_INFINITY)
                    .min(Double::compareTo);
            if (minDistance.isPresent()) {
                minGap = FastMath.min(minGap, minDistance.get());
            }
        }
        return minGap;
    }
}